add tmux team observer + member lifecycle events (T-139, D-75)
test / unit + widget + golden + a11y (push) Failing after 27s
test / integration_test (xvfb) (push) Has been skipped
test / bundle smoke (xvfb 5s) (push) Has been skipped
test / daemon subprocess + web WASM smoke (push) Has been skipped
test / dart doc (lib API) (push) Failing after 24s

team_observer.dart is the single drift-containment point for Claude
Code's experimental tmux team mode. It discovers the active team for a
workspace (~/.claude/teams/<team>/config.json, matched by member cwd),
polls `tmux -L clide list-panes -a`, and correlates live panes with the
config's tmuxPaneId to emit TeamMemberBorn / TeamMemberDied — identity
(name, agentType, model, colour, pane) comes from the config, so it's
reliable regardless of transcript drift.

Each teammate's subagent transcript is resolved best-effort and streamed
on a per-agent MessageBus channel via TranscriptPublisher (TranscriptReader
gains an explicit `file:` for this). The config<->transcript join is the
fragile part: no shared key, so it uses a sibling .meta.json agentType
when present, else zips members-by-joinedAt against files-by-mtime. This
join needs validation against a live team run.

App wiring + visible surfacing land with the teammate tiles (T-140).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-23 01:13:46 +02:00
co-authored by Claude Opus 4.7
parent cda289b8b1
commit fc55f58e6b
7 changed files with 735 additions and 5 deletions
@@ -718,6 +718,31 @@ void main() {
'reply number 199',
);
});
test('explicit file: tails that exact file, ignoring newest-discovery', () async {
// A teammate reader (T-139) points at one fixed subagent file rather
// than the newest .jsonl in the munged dir.
final dir = await Directory.systemTemp.createTemp('explicit_file_');
addTearDown(() => dir.delete(recursive: true));
final target = File('${dir.path}/agent-abc.jsonl');
writeLines(target, [assistantText('a1', 'from the explicit file')]);
final reader = TranscriptReader(
'/unused/workspace',
projectsBase: '/nonexistent',
pollInterval: const Duration(milliseconds: 20),
file: target.path,
);
final collected = <ConversationItem>[];
final sub = reader.stream.listen(collected.add);
await pumpUntil(() => collected.isNotEmpty);
await sub.cancel();
await reader.dispose();
expect(collected.whereType<AssistantTextMessage>().single.text, 'from the explicit file');
});
});
}