Repository navigation
feat(chat): read-only subagent sessions surface (#187) - #241
Conversation
Adds a collapsible Subagents section to the parent detail screen listing child sessions with a live status badge, per the Stage 2 design. Children come from the existing global session list filtered client-side on parentID; status is derived from the SSE sessionStatus map + a new sessionErrors record; the session.created leak (children prepended to the root list) is closed by routing on parentID. No release in this task. OQ1 verified against Qiiks/opencode v1.18.18-orw.1 server source: the /api/session pool returns children carrying parentID (roots filter applies only when roots=true), and the session.created SSE payload carries parentID (v1 SessionInfo). OQ3 answered: the server does NOT populate time.idle, so cold-loaded children read Idle until observed live. Gates on the exact head SHA: check:versions exit 0, typecheck exit 0, full suite 414 pass / 0 fail (baseline 395 + 19: 15 new unit tests for the three pure helpers, 4 new integration tests). App code implemented mechanically in this lane (no Google AI Studio access); see PR body.
|
PASS — Stage 4 review of PR #241 (issue #187), head Test-count delta: baseline at CI this verdict is delivered against (checked ~00:15 WITA Nov 14): Typecheck and unit tests PASS (job 131993211037), test PASS (job 131993209742), build PASS 14m59s (job 131993210198), Secret scan (gitleaks) PASS; build-fdroid/release skipping (expected on a PR). Still pending, no failures: Unsigned iOS Simulator build, activation-e2e. The subagents Maestro flow runs inside activation-e2e as a NEWER non-blocking flow ( Exception compliance (issue #187, comment 6857247200): all 15 new unit tests cover only the three helpers — Validated findings: none. AC-by-AC evidence (diff actually read against the design + PRD):
Filed notes (not fails, no implementer action required before merge):
|
|
Triage: Unsigned iOS Simulator build on head cc0a3e5 (Stage 5, recorded by the lead). Run 43459869318 (iOS CI, pull_request, head cc0a3e5): conclusion failure. The job itself (132004118830) has conclusion cancelled with zero steps recorded — no step of it ever executed, so no code from this PR ran in it. This change could not have caused it. Same workflow is green on main: iOS CI run 43455305640 on cad292a (success) and run 43459958942 on fb7344d (success). The job is not a required check (required: Typecheck and unit tests, test, build — all pass on cc0a3e5). Per the workflow rule, this cancelled non-required job does not block the merge. If it cancels again on the next head, the same triage applies. |
…s-187 # Conflicts: # src/lib/session-list.integration.test.ts
Summary
Read-only subagent surface for #187, per the Stage 2 design (design, review PASS conditional on the recorded test exception, comment 6857247200). A collapsible Subagents section on the parent detail screen lists child sessions with a live status badge. No release in this task.
SubagentSectionmounted inapp/session/[id].tsxabove the message list, outside theFlatList. One row per child (newest first): title (fallbackUntitled subagent),agent · provider/modelonly when the server provides them, relative started time, state pill.loadChildren(parentID)fetches the global pool via the existingclient.session.list()and filters with the new pureselectChildren(all, parentID).deriveChildStatus(child, sessionStatus, sessionErrors)— Failed (recordedsession.error) > Running (busy/retry) > Done (time.idlepresent, or anidlestatus entry observed live this app session) > Idle. Parent summary badge viasummarizeChildren(counts + worst-state dot). No polling; SSE only.session.createdleak is closed inevents.ts: roots prepend as before; children upsert intochildrenByParent[parentID]only if that parent's key already exists, otherwise ignored (nextloadChildrenpicks them up). Root list never receives a child.sessionErrors: Record<sessionID, string>in the events store, set onsession.errorfor any session id, cleared on nextbusy. Failed rows show the server message (2 lines max). ExistingerroredSessionsset and all notification logic untouched.parentIDmatches yields theNo subagents yetempty state, not an error; rows without agent/model render without that line. A section load failure shows its own error + Retry and never blocks the transcript.loadSessions,normalizeSessions, transcript render path untouched.Out of scope (per design): tap-to-inspect, nested navigation, prompting/aborting/managing children, permission handling, creating subagents, editing/deleting children, child notifications, iOS/tablet layout.
OQ1 verification (blocking gate, done before any UI code)
Verified against the server source, Qiiks/opencode tag
v1.18.18-orw.1(commit7731d43f):/api/sessionpool returns children carryingparentID.packages/opencode/src/session/session.ts:Session.InfohasparentID: optional(SessionID)(:231) plusagent/model;fromRowmapsparentID: row.parent_id ?? undefined(:85);listByProject(:987) andlistGlobal(:560) apply theisNull(parent_id)roots filter only whenroots=true— a call without it returns children too.session.createdSSE payload carriesparentID.packages/schema/src/v1/session.ts:571-579:Created: define({ type: "session.created", schema: { sessionID, info: SessionInfo } }), andSessionInfo.parentID: optional(SessionID)(:550). Publish sitesession.ts:537:events.publish(SessionV1.Event.Created, { sessionID: result.id, info: result })with the fullInfoincludingparentID. The bridge forwards the full event data to SSE clients (event-v2-bridge.ts:payload: { id, type, properties: event.data }).Related findings from the same read:
time.idle— theTimeschema (session.ts:203-208, v1SessionInfo.time) has only created/updated/compacting/archived. Cold-loaded children therefore read Idle until a live busy → idle transition is observed, exactly as the design accepts.sessions.ts handleEventsession.updatedis map-only (replaces a matching id, never adds) — no leak path, no guard needed.session.deletedevent (v1session.ts:588, published atsession.ts:624); the mobile store has no handler for it, so a server-side-deleted child stays inchildrenByParentuntil the next focus-load. Accepted for this read-only slice, per design.SessionHttpApi.children→session.children(parentID),session.ts:598-606). This slice implements the design as written (list-all + filter, no SDK change); swapping the fetch behindselectChildrenis a follow-up, no UI/store change needed.Files changed (16, +933/−14)
src/lib/session-children.ts(new) —selectChildren,deriveChildStatus,summarizeChildrensrc/lib/session-children.test.ts(new) — 15 AC-mapped unit tests, no mockssrc/components/chat/SubagentSection.tsx(new) — collapsible section + rowssrc/stores/sessions.ts—childrenByParent,childrenLoading,childrenError,loadChildren(parentID)src/stores/events.ts—sessionErrorsrecord;session.createdrouting byparentIDsrc/lib/sdk.ts+src/lib/v2-shape.ts—Session/toSessioncarryagent,model {providerID, modelID},time.idle(one mapping point; fields omitted, not undefined-valued, when the server sends none, so the mapped shape is unchanged for sessions without them)app/session/[id].tsx+src/components/chat/index.ts— mount (one line + barrel export)src/lib/i18n/{en,ru,zh-Hans}.json—subagents.*keys (title, running, done, idle, failed, none, loadError, retry, summary)src/lib/session-list.integration.test.ts— 4 new tests on the existing integration surface (subagents-seeded mock over real HTTP)tests/fixtures/mock-opencode-server.ts—--seed-subagents(parent + done/idle children).maestro/flows/subagents.yaml(new, NEWER/non-blocking) +scripts/run-e2e-flows.sh— flow + 4101 mock startupNot touched, per brief:
ToolCallCard,SessionInfo, root list, notifications,sdk-v1.ts.Local test evidence (exact head SHA
eff8c468)npm run check:versions— exit 0 (Version metadata aligned: 0.4.17 (44))npm run typecheck— exit 0npm test(full suite) — 414 pass / 0 fail (baseline at38316ae5: 395 pass / 0 fail; delta +19: 15 new unit tests insession-children.test.ts+ 4 new integration tests; 0 removed; test files 50 → 51)/experimental/sessionreturns the parent (noparentID) + 2 children (parentID: seed-parent, one with agent/model/time.idle, one without);roots:trueload path excludes children (integration test).v2-shapetest doesdeepStrictEqualontoSessionoutput, so the mapping now omits absentagent/model/idlekeys instead of emittingundefinedvalues. No existing test was edited.Deviations and notes
session-children.test.ts(three helpers); thesession.createdrouting is covered via the existing integration/e2e surface (integration tests + Maestro flow), not a unit test. The design §2 row suggesting edits tosse.test.ts/v2-shape.test.tswas not applied — that would be net test growth beyond the recorded exception. (Design §2's test pointer should also read §3, not §5 — a design-doc fix for Stage 5.5, not this PR.)formatTime/statusColorduplicated, not exported:SessionInfo.formatTimeandToolCallCard.statusColorare module-private and both files are out of scope for this slice, so the component duplicates the relative-time format (samechat.sessionInfo.time.*keys) and the four pill hex values, each with a comment saying so.3 subagents · 3 Done(count + pill label) rather than the design's3 subagents · all done, to stay within the design's nine i18n keys.session.createdrouting branch has no automated cover (the mock emits nosession.createdbroadcast and no UI creates a child) — verified by inspection; live-pair proof stays a release-time phone check.workflowscope and a plaingit pushrefuses any ref touching.github/workflows/*. No workflow file is touched by this PR — the 4101 mock startup lives inrun-e2e-flows.shinstead. The pushed tree was verified byte-identical to the local tree (7153ddb8…, incl. the100755mode onrun-e2e-flows.sh).