Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 104 additions & 0 deletions .maestro/flows/subagents.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
appId: cc.agentlabs.opencode
name: Subagents section - parent detail lists children with live status (#187)
---
# Read-only subagent surface (issue #187): a parent detail screen shows a
# collapsible Subagents section listing its child sessions with a live status
# badge. AC1 (child list) + AC3-at-render (root list stays roots-only) at the
# UI layer; the data path is covered by
# src/lib/session-list.integration.test.ts (subagents block).
#
# The subagents flow's mock is started by scripts/run-e2e-flows.sh itself
# (port 4101, --seed-subagents) before the flows run, so no workflow-file
# change is needed for it. --seed-subagents pre-populates one parent + two
# children, all in /mock/project:
# seed-parent "Parent Session" (updated now-60s)
# seed-child-done "Done Subagent", parentID seed-parent, agent explore,
# model opencode/deepseek-v4-flash-free, time.idle set
# seed-child-idle "Idle Subagent", parentID seed-parent, no agent/model/idle
#
# Why this can actually FAIL if #187 regresses: the section loads children via
# loadChildren() (GET /session pool, filtered client-side on parentID). If the
# filter, the mount, or the store wiring breaks, "Subagents (2)" never appears
# or the child rows never render. If the roots filter regresses, the child
# titles leak onto the list screen — this flow opens the parent from a list
# that must show "Parent Session" only.
#
# Known limit: the LIVE session.created routing (a child created while viewing
# its parent appears in the section, never in the root list) is NOT covered
# here — the mock emits no session.created broadcast and no UI creates a
# child, so there is no way to trigger it from a flow. That branch is verified
# by inspection; a live check needs a real server with a task-tool subagent.
# The flow also does not assert the Running badge: the mock never emits a
# session.status busy for a child, so no child is live-running in this harness.
# NEWER flow (non-blocking): see scripts/run-e2e-flows.sh.

- launchApp:
clearState: true
- assertVisible:
id: "telemetry-consent-card"
- tapOn:
id: "telemetry-decline-button"

- assertVisible:
text: "No Connection"
- tapOn:
id: "add-connection-button"
- tapOn:
id: "connect-ip-input"
- inputText: "127.0.0.1:4101"
- hideKeyboard
- tapOn:
id: "connect-submit-button"

# 40s margin: see activation-positive.yaml — the connect handshake's own
# fetches are individually capped at 30s (src/lib/sdk.ts REQUEST_TIMEOUT_MS).
- extendedWaitUntil:
visible:
id: "connection-status-dot"
timeout: 40000
- takeScreenshot: subagents-S1_connected

# The parent must appear on the root list; the children must not leak onto it.
- extendedWaitUntil:
visible:
text: "Parent Session"
timeout: 15000
- takeScreenshot: subagents-S2_parent_listed

# Open the parent.
- tapOn:
id: "session-item-seed-parent"
- extendedWaitUntil:
visible:
id: "chat-message-input"
timeout: 15000
- assertVisible:
text: "Parent Session"
# Confirms GET /session/seed-parent/message actually resolved (empty state,
# not stuck loading / errored).
- assertVisible:
text: "Start a conversation"

# The section header renders collapsed with the child count once loadChildren
# resolves (the count starts at 0, so wait for the loaded value).
- extendedWaitUntil:
visible:
text: "Subagents (2)"
timeout: 15000
- takeScreenshot: subagents-S3_section_header

# Expand and check both rows: titles, the agent · model line only where the
# server provides it, and the Done pill for the finished child.
- tapOn:
text: "Subagents (2)"
- extendedWaitUntil:
visible:
text: "Done Subagent"
timeout: 15000
- assertVisible:
text: "Idle Subagent"
- assertVisible:
text: "explore · opencode/deepseek-v4-flash-free"
- assertVisible:
text: "Done"
- takeScreenshot: subagents-S4_children_expanded
6 changes: 6 additions & 0 deletions app/session/[id].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
ImageAttachments,
SessionInfo,
MessageTextSheet,
SubagentSection,
type SlashCommand,
type Attachment,
} from "../../src/components/chat"
Expand Down Expand Up @@ -783,6 +784,11 @@ export default function SessionScreen() {
</View>
)}

{/* Subagent children (#187) — above the message list, outside the
FlatList. Read-only; a load failure shows its own error + retry
and never blocks the transcript. */}
{id ? <SubagentSection parentId={id} isDark={isDark} /> : null}

{isLoading ? (
<View style={s.loading}>
<ActivityIndicator size="large" color={isDark ? "#ffffff" : "#0a0a0a"} />
Expand Down
25 changes: 23 additions & 2 deletions scripts/run-e2e-flows.sh
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,36 @@ APK="android/app/build/outputs/apk/release/app-release.apk"
CORE_FLOWS=(activation-positive activation-negative-401)
# demo.yaml (offline "Try a demo" mode) needs no mock server — it never
# connects — so it's safe to run alongside the others here unmodified.
NEWER_FLOWS=(directory-picker all-sessions variant-picker diff-scroll demo)
NEWER_FLOWS=(directory-picker all-sessions variant-picker diff-scroll demo subagents)
mkdir -p "$ROOT/artifacts/screenshots" "$ROOT/artifacts/diag"

echo "== installing APK =="
adb install "$APK"

echo "== forwarding mock ports into the emulator (adb reverse) =="
for p in 4096 4097 4098 4099 4100; do adb reverse "tcp:$p" "tcp:$p"; done
for p in 4096 4097 4098 4099 4100 4101; do adb reverse "tcp:$p" "tcp:$p"; done
adb reverse --list

# The subagents flow needs its own mock instance (port 4101, --seed-subagents:
# one parent + two children, for subagents.yaml / issue #187). It starts here,
# not in activation-e2e.yml, so this flow needs no workflow-file change: the
# workflow only starts ports 4096-4100. Same node invocation the workflow uses
# for the other mocks; wait until its health endpoint answers.
echo "== starting subagents mock on 4101 =="
nohup node tests/fixtures/mock-opencode-server.ts --port 4101 --seed-subagents > /tmp/mock-4101.log 2>&1 &
for i in $(seq 1 30); do
if curl -sf --connect-timeout 1 -m 3 "http://127.0.0.1:4101/global/health" > /dev/null 2>&1; then
echo "subagents mock on 4101 is up"
break
fi
if [ "$i" = "30" ]; then
echo "::error::subagents mock server on port 4101 did not come up in 30s"
cat /tmp/mock-4101.log || true
exit 1
fi
sleep 1
done

# Decisive probe: can the EMULATOR actually reach the host mock via 127.0.0.1?
# If any in-emulator method prints the health JSON, the network path is good
# and any flow failure is app-side; if none succeed, attribution falls back to
Expand Down Expand Up @@ -96,6 +116,7 @@ adb logcat -c || true # clear, so the captured log is just this run
dump_diag() {
echo "== capturing diagnostics =="
adb logcat -d > "$ROOT/artifacts/diag/logcat.txt" 2>&1 || true
cp /tmp/mock-4101.log "$ROOT/artifacts/diag/mock-4101.log" 2>/dev/null || true
# Maestro writes per-run debug (UI hierarchy + commands) under ~/.maestro/tests
cp -r "$HOME/.maestro/tests" "$ROOT/artifacts/diag/maestro-tests" 2>/dev/null || true
}
Expand Down
Loading
Loading