Pressing Enter to confirm a Chinese IME candidate sent the message because the
team composer had no composition guard; it also ignored the shared
'hapi-composer-enter-behavior' setting. Now it never sends while composing and
follows the setting (send on Enter, or newline with Cmd/Ctrl+Enter).
Grouping every message into collapsed requirement cards made the team page read
as a wall of summaries. The default is now a chronological timeline (with the
AI chatter folding) and the requirement cards moved behind a 'By requirement'
filter; 'Key' and 'Task' views are unchanged.
Cards kept showing 待开始 for requirements whose tasks were all finished,
because the status field is only written when the lead explicitly closes a
requirement. The card now falls back to the task states (blocked > done >
doing) and treats teammate activity without tasks as in progress; an explicit
status still wins.
The card showed the same sentence up to four times: as the title, in the
'your ask' block, in a separate 'original ask' block and again as the opening
human message in the timeline. Now the ask appears once at the top (clamped to
three lines while collapsed, full text when expanded) and the opening human
message is hidden inside the process view.
- cards show 'your ask' and a result block at a glance: the lead's conclusion
when present, otherwise a result derived from task states (n/m done plus the
latest deliverable); the original ask stays one click away
- the process opens to key milestones only (human messages, decisions, task
hand-offs, done/blocked tasks, system notices) with a 'show full process (+n)'
switch for the raw stream
- hub: one-time idempotent backfill files pre-requirement history under
requirements (fresh human asks become requirements; messages/tasks follow by
time window; short acknowledgements do not open requirements)
- task update broadcasts now carry the status in meta so the UI can pick
milestones without parsing text
- requirement cards keep the conclusion (result) and task progress visible while
collapsed, show the original ask when expanded, and offer a 补充说明 shortcut
that threads the next message under the requirement
- composer gains a requirement selector (auto-file / new / a specific one) and
the human message API accepts requirementId + newRequirement
- closing a requirement (or writing its conclusion) now notifies the human
out-of-band via a team-attention event
- AI messages inside a thread lose their heavy card styling (left rule only);
decisions keep the emphasised card
- requirements: a fresh human ask opens one automatically (replies inherit);
tasks and messages carry requirementId (spawn_peer / team_task / team_send
taskId); the lead closes one with team_requirement (status + conclusion)
- web: the team timeline groups messages into collapsible requirement cards
(title, status, conclusion, message count; a card with a pending decision
opens automatically); unassigned messages fall into an 'Other' card
- web: entering a team scrolls to the newest message, follows new messages only
while pinned to the bottom, and offers a 'jump to latest' button otherwise
- web: AI-to-AI chatter (status/chat/question/task-update, not addressed to the
human) folds into a one-line summary by default (runs of >= 2), with the
participants and latest line preview
- tests: requirement store/service coverage and grouping helper tests
to=human alone is an out-of-band notification, not a request for an answer;
the inbox (待你确认) now only contains kind=decision messages (legacy rows
with awaitingHuman still count). Prompts and tool descriptions now tell agents
to use kind=decision when the human must decide and plain broadcasts for
progress they merely need to see.
- team settings can edit the budget (max members / messages per minute / chain
depth); changes merge into the existing config and apply immediately
- default member cap 5 -> 8 (lead included)
- new DELETE /api/teams/:id/members/:sessionId (optional ?stopSession=1),
lead cannot be removed; removal is announced in the team log, the removed
session is notified, and its session is archived only when requested
- member list gets a human-confirmed remove action with a stop-session checkbox
- tests: service config merge + route budget/removal coverage
- spawned members inherit the caller's tool/model/thinking level/permission
(spawn request schema + hub runtime + spawn_peer tool); explicit overrides win
- replies from turns triggered by teammates (not by the human) no longer vanish:
system prompt and ACP briefs now require an explicit team_send(to=human) for
human-facing conclusions produced on those turns
- task loop: team_task tool (list/update), dependsOn gating, deliverable evidence
required when a member marks done, auto broadcast on change, web shows deps and
deliverable on the task board
- team-scoped agent tokens (hapi_team_*): issued per team, injected as
HAPI_TEAM_TOKEN at spawn, restricted by the auth middleware to this team's
messages/tasks/status and audited via [TeamToken] hub log lines
- Team chat now talks to the lead by default; drop the 'everyone' recipient
(broadcast never woke anyone, which read as 'no response')
- Member decisions (kind=decision / to:human) are flagged awaitingHuman; the
human's reply carries inReplyTo and clears the decision, and a new dismiss
endpoint waves one off without replying
- Web: 'Needs your reply / Replied / Dismissed' chips on decisions, an inline
Reply button that binds the asking member, and a 'Needs you' inbox in the
side panel with a badge on the panel toggle
Old runners ignore the team payload, so the lead was spawned without
HAPI_TEAM_*: no team tools, no team prompt, no repo memory — it then
improvised (reverse-engineering the HAPI API, poking the project) instead
of replying. Gate team spawns in SyncEngine.spawnSession, share the
capability predicate via runnerCapabilities, and mark machines without it
disabled in team mode with an upgrade hint.
Team memory now lives with the code, like CLAUDE.md: <main repo>/.hapi/team/
on the machine where the agent runs. Worktrees resolve to the main repo so
every member shares one directory. The CLI creates charter.md + handoffs/ at
session start (team env only), points the agent at it in the system prompt,
and reports the path in session metadata; the web reads it through the
session file API, so remote runners work too.
Hub keeps only coordination data (teams.db): members/tasks/messages/status.
Its memory-dir management and the /memory API are removed.
Root cause of 'lead never answers': sessions created through the web form
never received HAPI_TEAM_* env, so their system prompts had no team rules
(only spawn_peer-created members did). Now:
- /api/machines/:id/spawn accepts a team descriptor; the runner exports the
env as before
- team mode creates the team first, spawns the lead with the team context,
then attaches it as lead; member mode spawns with the team context before
joining
- OpenCode system prompts (title + native tool instruction) carry the team
block too
- human ping copy is action-first: reply directly (auto-synced to the
group); do not run shell commands or explore the team
The add-member dialog only offered a hardcoded agent list and no model
picker. 'Add member' now opens the New Session form in member mode: a
banner with the team name plus role/task fields, then the full launch
options (every agent, every model, effort, permission mode, worktree).
On create, POST /api/teams/:id/members adopts the session, records the
task and delivers the assignment brief. The team session type is hidden
in this mode.
Member sessions (lead included) no longer scatter across the session
list: they live inside an expandable team row with role + live status,
and are filtered out of the standalone list. GET /api/teams now returns
members so the sidebar can render them without extra requests.
Team creation now lives in the New Session form (session type: Team);
a second, less capable dialog in the sidebar only caused confusion.
The sidebar team section hides itself when there are no teams.
- New Session gains a third session type: Team. All launch options
(agent, every model, effort, permission mode, machine, directory) are
reused; only a team name is added.
- History-import affordances are hidden in team mode; the lead is spawned
as a plain session and the team is created with it as lead, then the app
navigates to the team chat.
- standalone TeamCreateDialog stays for quick creation from the sidebar.
Teams created without a lead previously dead-ended: members cannot be
spawned until a lead exists. PATCH /api/teams/:id now accepts
leadSessionId; the settings dialog exposes a lead picker and delivers the
lead brief on save.
- TeamCreateDialog: pick machine/directory/agent (optionally a worktree),
spawns the lead session, then creates the team - no more adopting an
existing session implicitly
- team chat header and side panel honor env(safe-area-inset-top) so the
mobile status bar / dynamic island no longer covers the title
- hub: deliver a lead brief to the lead session on team creation (a lead
spawned through the normal path has no HAPI_TEAM_* env)
- add-member dialog hints that members follow the lead's machine/directory
Hub
- independent teams.db (own user_version ladder); feature gate default OFF,
so a disabled hub never creates the file and team routes simply do not exist
- /api/teams: list/detail/by-session/memory/messages/spawn/tasks,
human message channel, team rename/archive/delete
- team runtime: spawn members via the existing runner RPC (worktree/yolo
options), peer message routing (mention/dm push, broadcast pull-only),
budgets (max members, message rate, reply chain depth), member-offline
announcements that wake the lead or escalate to the human
- team-attention notifications: in-app toast when visible, web push otherwise
- team memory dir {HAPI_HOME}/teams/<id>/ with charter.md template; read-only
browse API with path-traversal guards
CLI
- MCP tools: team_status / team_read / team_send / spawn_peer (claude, codex
bridge and ACP flavors); spawn_peer stays behind manual approval
- HAPI_TEAM_* env for spawned members; system-prompt block for Claude/Codex;
assignment brief carries team rules for flavors without prompt injection
- RUNNER_CAPABILITIES.agentTeam advertised to the hub
Web
- team group chat route with folded peer threads, key/task filters,
human composer, member/decision/system message cards
- side panel: live member status, interactive task board (create/status/
assignee), team memory browser, budget
- team create / add-member / settings dialogs; sidebar team section;
hub settings toggle for the feature (restart to apply)
- SSE team-updated invalidation
Zero impact when disabled: no teams.db, no routes, no tool or prompt changes.
* fix(web): force-dismiss indeterminate queued rows on cancel/edit
When cancel returns busy for an already-indeterminate QueuedMessagesBar
row, keep the optimistic removal instead of re-sticking the row so Edit/X
cannot leave the UI dead. Upgrade busy to invoked when queued-state
reconcile shows the steer already landed, so Edit toasts instead of
prefilling a duplicate.
Fixes#1839
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): never prefill Edit when queued cancel returns busy
Indeterminate rows can still be a live dispatch. Prefilling on busy
risked a duplicate send if the original steer later landed. Keep
force-dismiss for Cancel; toast on Edit+busy without restoring the draft.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): keep force-dismissed indeterminate rows for late consume
Hide abandoned outcome-unknown rows from the queued bar via
queueDismissed so Cancel/Edit still clear the stuck chip, while leaving
the message in the window for a later messages-consumed SSE.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): close queueDismissed races on cancel busy and requeue
Restore the hidden hold before getQueuedState so concurrent consume
SSE can land, skip overwriting it in onSuccess, and clear dismissal
when markMessagesRequeued confirms FIFO again.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): undismiss force-held row when cancel busy finds FIFO
If getQueuedState reports the localId is queued again after a failed
steer, clear queueDismissed so Edit/Cancel return without prefilling.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): preserve queueDismissed across refresh and merges
Force-dismiss of indeterminate rows survived Cancel, but latest-page
refetch and same-ID merges dropped the client-only flag. Carry it while
the authoritative row remains uninvoked and indeterminate.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): clear queueDismissed on reconnect FIFO reconcile
Reconnect queued-state reconciliation now markMessagesRequeued for
confirmed queuedLocalIds so force-dismissed rows regain Edit/Cancel
after a missed messages-requeued SSE.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Group ordinary Codex commands with default tools across web, iOS and Android while preserving exploration and user-shell boundaries.
Add regression tests, generated protocol fixtures and shared-command coverage in the iOS transcript UI suite.
Revert 8357da0a9d and its later shared-runtime integration.
Remove automatic model fallback, account usage polling, and the related UI, protocol fields, tests, and documentation without adding replacement quota handling.
Keep generic model pagination and method probing required by the current shared-session architecture. Cover idle sessions staying online without usage polling or agent-state churn.
Use one native app-server for terminal, Web and phone clients while retaining the existing CLI and Runner lifecycle.
Synchronize native queues, permissions, question history and steering state; preserve explicit permission precedence and per-turn usage models. Resume inactive clear commands through Runner and reject independent child cold resumes.
Add shared-runtime regression tests, generated protocol fixtures and lifecycle documentation.
Bridge main-session PermissionRequest hooks without suppressing the native
terminal dialog. Reconcile replies against native results and clean up on
timeout, cancellation, mode switches, and session changes.
Keep reply IDs distinct from native tool IDs across web and native clients;
add protocol fixtures and regression tests.
Refs #1796
Add a right-click (desktop) / long-press (mobile) menu to file entries on
the Files page: git change rows, file search results, and the directory
tree. Actions:
- Copy path (workspace-relative)
- Copy absolute path (resolved against session.metadata.path)
- Add to composer (append a backticked reference to the session composer
draft and navigate back to the chat)
Closes#1807
* fix(agy): say what the model picker is actually waiting on
The spinner in the New Session AGY picker read "Checking Antigravity
authentication…", but nothing at that point checks authentication — the
machine is running `agy models`, and the sign-in prompt is a separate
branch below it, shown only when agy reports the failure.
Name the wait after the work: "Fetching available models…", the same
words agy prints while it fetches.
* refactor(agy): describe a probe by its outcome, not by its response
The probe function returned a finished `AgyModelsResponse`, so "agy could
not be reached" and "agy listed no models" both arrived as a successful
response carrying the hardcoded mirror, and the caller could no longer
tell which had happened. Every policy decision about that answer has to
live inside the probe as a result.
Hand back what the probe observed — a live catalog, an auth failure, or
nothing usable — and let the caller turn it into a response. Same
behaviour: the mirror still stands in for both failure modes, and the
60s cache still holds whatever came out.
* fix(agy): serve the model catalog stale-while-revalidate
The `agy models` probe is a whole agy invocation — around 3s on a good
day, 15s when it times out — and the 60s window meant the New Session
picker paid that again a minute after the last look.
Keep the last listing agy actually returned and answer from it: fresh for
ten minutes, then still answered while a probe refreshes behind it, until
the entry is a day old and stops standing in for the machine at all. A
probe that times out or loses auth leaves that entry alone, so a blip no
longer empties a working picker, and the hardcoded mirror is no longer
recorded as if the machine had reported it.
Three things fall out of that and are handled here. A machine whose
sign-in has actually gone bad would otherwise look healthy for a day, so
an auth failure rides along with the catalog it can still serve — and,
because nothing else would re-probe a catalog that is still fresh, a
warning riding on the answer is itself a reason to look again. A failed
probe is not repeated on the very next request either, or a machine where
agy hangs would spawn it once per poll.
An explicit refresh always costs a probe, and never rides one that was
already running when it was asked for.
* fix(agy): let Retry force a fresh model catalog probe
With the catalog held for ten minutes, Retry would otherwise hand back the
answer it was pressed to replace, so the intent travels to the machine:
`?refresh=true` on the machine route, an optional RPC param, and a
one-shot flag on the query so ordinary mount and focus refetches stay
cheap. Every hop is optional, so a hub and a runner on different versions
still talk — the older side ignores it and answers from its cache.
Retry also has to be reachable, and honest, in the state that needs it.
The machine now answers with both a usable catalog and the sign-in failure
behind it, so the picker keeps the list and says why it may be out of
date, with the button right there rather than only once there is nothing
left to show. Pressing it runs agy, which can take tens of seconds, so the
button says so while it does.
The client contract covers both: `agy-models` is the one catalog route
that can carry an `error` on a successful response, and the one that
takes a refresh parameter.
* fix(agy): use the machine catalog in the in-session model picker
New Session already asks the machine what `agy models` lists, but a
session that is already open offered the built-in list in
`shared/src/models.ts`. That list is a hand-maintained mirror, so a model
agy started offering after the last release could be picked for a new
session and not for the one already running.
Point the composer at the same machine catalog. The mirror stays as the
fallback for the moment before the machine answers, and a model the
session is already on is kept selectable — and readable, when it is one
of the known presets — even after the catalog moves on without it.
* fix(agy): announce a model catalog re-check that changed the answer
Serving the last known catalog answers the picker instantly, but a picker
that was already open kept showing that answer until the user closed and
reopened it — the machine had no way to say it had found something newer.
Say it on the stream that already carries machine changes. The machine
daemon — the only process that answers `<machineId>:listAgyModels` —
emits it, and the hub forwards it as `machine-agy-models-updated` with
nothing but the machineId. Namespace resolution, per-machine delivery and
reconnect replay all come from the existing path.
What counts as a change is what the route would answer, not what sits in
the cache. That distinction carries the cases: a sign-in that lapsed or
came back changes no models yet changes what the user is told; a machine
whose agy was signed out has been answering from the hardcoded mirror, and
its first real listing is the largest change there is, for every client
except the one awaiting it.
* fix(agy): re-read the announced machine's model catalog
On `machine-agy-models-updated`, cancel and refetch that one machine's
catalog query — the app's global connection is always subscribed, so an
open picker redraws wherever it is.
Cancelling first is what makes it correct rather than merely likely.
query-core cancels an in-flight fetch only when the query already holds
data, so a picker opening for the first time would otherwise join the
request already on its way and settle on the listing the announcement
replaced. The refetch is answered from the machine's cache, so it starts
no probe and cannot bounce another announcement back.
A reconnect the hub could not replay takes the resync path, which clears
the agy catalogs the same way — that path has no announcement to fall back
on, so it is the one that can least afford to join a stale request.
* fix(agy): keep a model the user picked when the catalog moves under them
The catalog can now change while the New Session form is open, and the
form dropped any selection the machine no longer advertised — including
one the user had just made.
Keep that one, and list it as no longer listed so the form does not imply
agy is still offering it. A model restored from a draft or a saved
preference is still dropped: it may never have been runnable here.
* fix(agy): announce uncached authentication changes
* refactor(web): route create-form permission control through one native-select predicate
Extract usesNativePermissionSelect(flavor) (grok || codex-family, matching
the existing iOS/Android predicate of the same name) and route
PermissionField's select-vs-toggle gate through it instead of an inline
condition. Rename the codex-family-only state codexFamilyPermissionMode to
nativePermissionMode since it now backs a shared predicate, not just the
codex family. Behavior is unchanged: any stale sessionStorage draft written
under the old codexFamilyPermissionMode key has no value under the new key
and falls back to 'default', which only matters within a single browser
tab's lifetime.
* feat(web): let Claude pick a permission mode when creating a session
Claude was the only create-form flavor still on the global HAPI YOLO toggle
while grok and the codex family got the native permission select, so there was
no way to start a session in Plan Mode without creating it first and switching
the mode from the composer. usesNativePermissionSelect now gates the control
for claude as well, and the spawn body carries permissionMode (including
'default') instead of yolo, which is the shape the other native-select flavors
already send.
The stored hapi:newSession:yolo preference is bridged into the select rather
than dropped, but only for the flavors that have actually moved onto it
(LEGACY_YOLO_BRIDGE_AGENTS = codex, claude). copilot, gemini, kimi and opencode
moved earlier and settled on 'default'; re-enabling Yolo for them now would
widen permissions rather than migrate a preference. This narrows the
sessionStorage draft bridge too, which until now fired for the whole codex
family with no allow-list, so their draft restores yield 'default' instead of
'yolo' — same-tab-lifetime state only.
Claude and the codex family share one nativePermissionMode state and their mode
sets do not overlap, so the existing agent-change reset plus the flavor filters
in the draft loader and the stored launch settings are what keep a codex mode
out of a Claude spawn. Adds the regression test that pins it: pick a mode under
codex, switch to Claude, create, assert the payload carries 'default'.
* feat(ios): let Claude pick a permission mode when creating a session
Extend usesNativePermissionSelect to include claude alongside grok and the
codex family, matching the web change. buildSpawnRequest now derives both
yolo and permissionMode from that single predicate instead of two separate
local flags, so claude sends permissionMode (including 'default') and no
longer sends yolo. Unlike web, iOS carries no persistent YOLO preference
across sessions to migrate — the toggle only lives in the in-memory form or
a draft deleted on success — so there is no bridging logic to add here.
* feat(android): let Claude pick a permission mode when creating a session
Extend usesNativePermissionSelect to include claude alongside grok and the
codex family, matching the web and iOS changes. buildSpawnRequest derives
both yolo and permissionMode from that single predicate, so claude sends
permissionMode (including 'default') and no longer sends yolo. Unlike web,
Android has no persistent YOLO preference to migrate: the toggle only lives
in the form draft, which is deleted once a session is created.
The agent-switch test asserted claude renders the YOLO toggle; it now checks
the native select for claude and keeps the toggle assertion on cursor, which
still carries it.