feat(web): subagent side panel with a live call-graph, compact chips in the stream (#78)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d3c8f1da7b
commit
08f59baa70
22 files changed
+2330
-175
No files matched your search
@@ -605,7 +605,19 @@ export class Agent {
|
||||
const childApprove = approve
|
||||
? (tc: OmniMessage<ToolCallPayload>) => approve(withOrigin(tc, hop))
|
||||
: undefined;
|
||||
for await (const msg of childSession.run([userText(prompt)], {
|
||||
// The engine writes a run's input to the CHILD's own Trace but never replays
|
||||
// it to its consumer (a session's normal caller typed that input itself) —
|
||||
// here the consumer is the PARENT, whose frontend has never seen the child's
|
||||
// prompt. Forward the input message itself (origin-tagged, ahead of the run's
|
||||
// output) so the live nested view shows the child's user side exactly like a
|
||||
// reloaded one (history expansion splices the child Trace, which carries this
|
||||
// same message). The parent engine drops origin messages from the parent
|
||||
// Trace, so replay never duplicates it. Later rounds (input_subagent
|
||||
// follow-up prompts) come through this same generator and are forwarded the
|
||||
// same way.
|
||||
const input = userText(prompt);
|
||||
yield withOrigin(input, hop);
|
||||
for await (const msg of childSession.run([input], {
|
||||
...(signal ? { signal } : {}),
|
||||
...(childApprove ? { approve: childApprove } : {}),
|
||||
})) {
|
||||
|
||||
@@ -473,6 +473,47 @@ describe("run_subagent spawning follows the PARENT session (never the Project de
|
||||
parent.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
it("forwards the run input as an origin-tagged user message, after session_meta and before any model output", async () => {
|
||||
const agent = await createAgent();
|
||||
const ws = path.join(tmpRoot, "ws-input-forward");
|
||||
await fs.mkdir(ws, { recursive: true });
|
||||
const parent = await agent.createSession({
|
||||
workspaceDir: ws,
|
||||
modelId: "claude-sonnet-4-6",
|
||||
provider: "anthropic",
|
||||
});
|
||||
const runner = lastSpawnedRunner();
|
||||
const handle = await runner.spawn({});
|
||||
try {
|
||||
// The child's user side has no other live source: a session's normal caller typed its
|
||||
// own input, but this caller is the PARENT — the frontend renders the child conversation
|
||||
// purely from these forwarded messages. (Regression: the panel showed no user messages
|
||||
// live, while a reload — child-Trace expansion — did show them.)
|
||||
const gen = handle.run({ prompt: "count the TODO items" });
|
||||
const first = (await gen.next()).value as { type: string };
|
||||
expect(first.type).toBe("session_meta");
|
||||
// The input yield precedes childSession.run: pulling it never issues an LLM request.
|
||||
const second = await gen.next();
|
||||
expect(second.done).toBe(false);
|
||||
const msg = second.value as {
|
||||
type: string;
|
||||
origin?: string[];
|
||||
payload: { type: string; role?: string; text?: string };
|
||||
};
|
||||
expect(msg.origin).toEqual([handle.sessionId]);
|
||||
expect(msg.type).toBe("model_msg");
|
||||
expect(msg.payload).toMatchObject({
|
||||
type: "text",
|
||||
role: "user",
|
||||
text: "count the TODO items",
|
||||
});
|
||||
await gen.return(undefined);
|
||||
} finally {
|
||||
handle.dispose();
|
||||
parent.dispose();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("Agent.createSession max output tokens (per-model cap wins over the Agent config)", () => {
|
||||
|
||||
Reference in new issue
Block a user