@saadnvd1/agent-os 0.3.821 → 0.3.825
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/api/fork/cancel/route.ts +19 -0
- package/app/api/fork/children/route.ts +52 -0
- package/app/api/fork/result/route.ts +29 -0
- package/app/api/fork/return/route.ts +20 -0
- package/app/api/fork/route.ts +32 -0
- package/app/api/fork/wait/route.ts +27 -0
- package/app/api/load/heavy/lease/route.ts +26 -0
- package/app/api/sessions/[id]/archive/route.ts +17 -4
- package/app/api/sessions/[id]/route.ts +2 -3
- package/app/page.tsx +0 -5
- package/components/Chat/ChatPanel.tsx +33 -206
- package/components/Chat/Forks.tsx +278 -0
- package/components/Chat/ReplyingTo.tsx +24 -0
- package/components/Chat/TimelineRows.tsx +194 -0
- package/components/Pane/DesktopTabBar.tsx +0 -6
- package/components/Pane/MobileTabBar.tsx +0 -6
- package/components/Pane/MobileViewPicker.test.ts +1 -8
- package/components/Pane/MobileViewPicker.tsx +0 -11
- package/components/Pane/PaneViewToggle.test.ts +0 -2
- package/components/Pane/PaneViewToggle.tsx +0 -17
- package/components/Pane/index.tsx +0 -50
- package/components/SessionList/useDoneAction.ts +18 -3
- package/content/whats-new.json +16 -0
- package/contexts/PaneContext.tsx +1 -1
- package/data/done/index.ts +26 -2
- package/data/fork/index.ts +83 -0
- package/lib/chat/commands.ts +15 -0
- package/lib/chat/drivers/copilot-ask.ts +38 -5
- package/lib/chat/runner.ts +8 -2
- package/lib/chat/worker/extras.ts +7 -1
- package/lib/chat/worker/host.ts +2 -0
- package/lib/config/client-config-types.ts +7 -0
- package/lib/config/client-config.ts +2 -0
- package/lib/db/migrations/20261102T0100_forks.ts +87 -0
- package/lib/done/archive.ts +21 -1
- package/lib/experiments/registry.ts +8 -0
- package/lib/fork/api.ts +10 -0
- package/lib/fork/brief.ts +63 -0
- package/lib/fork/conversation.ts +75 -0
- package/lib/fork/deliver.ts +91 -0
- package/lib/fork/extras.ts +53 -0
- package/lib/fork/guard.ts +323 -0
- package/lib/fork/heavy-lease.ts +104 -0
- package/lib/fork/held.ts +26 -0
- package/lib/fork/ledger.ts +324 -0
- package/lib/fork/options.ts +46 -0
- package/lib/fork/point.ts +41 -0
- package/lib/fork/scratch.ts +67 -0
- package/lib/fork/settle.ts +215 -0
- package/lib/fork/snapshot.ts +69 -0
- package/lib/fork/start.ts +273 -0
- package/lib/fork/tick.ts +54 -0
- package/lib/fork/types.ts +79 -0
- package/lib/fork/view.ts +116 -0
- package/lib/fork/waits.ts +83 -0
- package/lib/fork/worktree.ts +115 -0
- package/lib/orchestrator/shell-guard.ts +6 -3
- package/lib/orchestrator/write-guard.ts +1 -1
- package/lib/security/demo.ts +1 -2
- package/lib/sessions/end.ts +5 -0
- package/package.json +1 -1
- package/server.ts +2 -0
- package/app/api/orchestrate/spawn/route.ts +0 -54
- package/app/api/orchestrate/workers/[id]/route.ts +0 -89
- package/app/api/orchestrate/workers/route.ts +0 -31
- package/app/api/sessions/[id]/mcp-config/route.ts +0 -34
- package/components/ConductorPanel.tsx +0 -258
- package/lib/mcp-config.ts +0 -80
- package/lib/orchestration.ts +0 -463
- package/mcp/orchestration-server.ts +0 -441
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { fail, text } from "@/lib/fork/api";
|
|
3
|
+
import { cancelChildren } from "@/lib/fork/settle";
|
|
4
|
+
|
|
5
|
+
// Cancels one child (`ref`: its number, label or id) or all of a session's.
|
|
6
|
+
export async function POST(request: NextRequest) {
|
|
7
|
+
try {
|
|
8
|
+
const body = await request.json();
|
|
9
|
+
const parent = text(body.from) ?? text(body.session);
|
|
10
|
+
if (!parent) throw new Error("Which session's forks?");
|
|
11
|
+
const ref = text(body.ref) ?? undefined;
|
|
12
|
+
if (!ref && body.all !== true)
|
|
13
|
+
throw new Error("Which fork, or all of them?");
|
|
14
|
+
const done = await cancelChildren(parent, ref);
|
|
15
|
+
return NextResponse.json({ cancelled: done.map((c) => c.label) });
|
|
16
|
+
} catch (error) {
|
|
17
|
+
return fail(error);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { fail } from "@/lib/fork/api";
|
|
3
|
+
import { db } from "@/lib/db";
|
|
4
|
+
import { allChildren, childrenOf, getChild, numberOf } from "@/lib/fork/ledger";
|
|
5
|
+
import { childrenView, statsOf, statsText } from "@/lib/fork/view";
|
|
6
|
+
import { forksOn } from "@/lib/fork/options";
|
|
7
|
+
|
|
8
|
+
// A session's forks, for `aos children` and the chat's strip. `stats` adds
|
|
9
|
+
// the numbers; with no session, the totals over every fork.
|
|
10
|
+
export async function GET(request: NextRequest) {
|
|
11
|
+
try {
|
|
12
|
+
const params = request.nextUrl.searchParams;
|
|
13
|
+
const session = params.get("session");
|
|
14
|
+
if (!session && params.get("stats") !== "1")
|
|
15
|
+
throw new Error("Which session?");
|
|
16
|
+
if (!session) {
|
|
17
|
+
const { db } = await import("@/lib/db");
|
|
18
|
+
const rows = db
|
|
19
|
+
.prepare(`SELECT * FROM session_children`)
|
|
20
|
+
.all() as Parameters<typeof statsOf>[0];
|
|
21
|
+
const stats = statsOf(rows);
|
|
22
|
+
return NextResponse.json({ stats, text: statsText(stats) });
|
|
23
|
+
}
|
|
24
|
+
const view = childrenView(session);
|
|
25
|
+
const own = getChild(session);
|
|
26
|
+
const asChild = own
|
|
27
|
+
? {
|
|
28
|
+
parentId: own.parent_session_id,
|
|
29
|
+
parentName:
|
|
30
|
+
(
|
|
31
|
+
db
|
|
32
|
+
.prepare(`SELECT name FROM sessions WHERE id = ?`)
|
|
33
|
+
.get(own.parent_session_id) as { name: string } | undefined
|
|
34
|
+
)?.name ?? "its session",
|
|
35
|
+
n: numberOf(own.parent_session_id, session),
|
|
36
|
+
label: own.label,
|
|
37
|
+
state: own.state,
|
|
38
|
+
worktree: !!own.worktree,
|
|
39
|
+
}
|
|
40
|
+
: null;
|
|
41
|
+
const rows = childrenOf(session);
|
|
42
|
+
const stats = params.get("stats") === "1" ? statsOf(rows) : undefined;
|
|
43
|
+
return NextResponse.json({
|
|
44
|
+
enabled: forksOn(),
|
|
45
|
+
asChild,
|
|
46
|
+
...view,
|
|
47
|
+
...(stats && { stats, text: statsText(stats) }),
|
|
48
|
+
});
|
|
49
|
+
} catch (error) {
|
|
50
|
+
return fail(error);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { fail, text } from "@/lib/fork/api";
|
|
3
|
+
import { findChild, markUsed, numberOf } from "@/lib/fork/ledger";
|
|
4
|
+
|
|
5
|
+
// One child's whole result (`aos children <n>`); reading it counts as
|
|
6
|
+
// opening it, for the stats.
|
|
7
|
+
export async function GET(request: NextRequest) {
|
|
8
|
+
try {
|
|
9
|
+
const params = request.nextUrl.searchParams;
|
|
10
|
+
const parent = text(params.get("session"));
|
|
11
|
+
const ref = text(params.get("ref"));
|
|
12
|
+
if (!parent || !ref) throw new Error("session and ref are required");
|
|
13
|
+
const c = findChild(parent, ref);
|
|
14
|
+
markUsed(c.child_session_id, "opened");
|
|
15
|
+
return NextResponse.json({
|
|
16
|
+
id: c.child_session_id,
|
|
17
|
+
n: numberOf(parent, c.child_session_id),
|
|
18
|
+
label: c.label,
|
|
19
|
+
state: c.state,
|
|
20
|
+
outcome: c.outcome,
|
|
21
|
+
kind: c.result_kind,
|
|
22
|
+
summary: c.summary,
|
|
23
|
+
result: c.result,
|
|
24
|
+
instruction: c.instruction,
|
|
25
|
+
});
|
|
26
|
+
} catch (error) {
|
|
27
|
+
return fail(error);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { fail, text } from "@/lib/fork/api";
|
|
3
|
+
import { returnChild } from "@/lib/fork/settle";
|
|
4
|
+
|
|
5
|
+
// `aos return`: a fork child hands in its result.
|
|
6
|
+
export async function POST(request: NextRequest) {
|
|
7
|
+
try {
|
|
8
|
+
const body = await request.json();
|
|
9
|
+
const child = text(body.from);
|
|
10
|
+
if (!child) throw new Error("Only a fork child can return a result");
|
|
11
|
+
const state = await returnChild(
|
|
12
|
+
child,
|
|
13
|
+
String(body.text ?? ""),
|
|
14
|
+
body.failed === true
|
|
15
|
+
);
|
|
16
|
+
return NextResponse.json({ state });
|
|
17
|
+
} catch (error) {
|
|
18
|
+
return fail(error);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { randomUUID } from "crypto";
|
|
3
|
+
import { fail, text } from "@/lib/fork/api";
|
|
4
|
+
import { startFork } from "@/lib/fork/start";
|
|
5
|
+
import { DELIVERY_MODES } from "@/lib/fork/options";
|
|
6
|
+
|
|
7
|
+
// Starts forks of a session: `from` is the session asking (an agent's `aos
|
|
8
|
+
// fork`); a person's tap names it as `session`.
|
|
9
|
+
export async function POST(request: NextRequest) {
|
|
10
|
+
try {
|
|
11
|
+
const body = await request.json();
|
|
12
|
+
const agent = text(body.from);
|
|
13
|
+
const parentId = agent ?? text(body.session);
|
|
14
|
+
if (!parentId) throw new Error("Which session to fork?");
|
|
15
|
+
const instructions = Array.isArray(body.instructions)
|
|
16
|
+
? body.instructions.filter((t: unknown) => typeof t === "string")
|
|
17
|
+
: [];
|
|
18
|
+
const delivery = DELIVERY_MODES.find((d) => d === body.delivery);
|
|
19
|
+
const started = startFork({
|
|
20
|
+
parentId,
|
|
21
|
+
requestId: text(body.requestId) ?? randomUUID(),
|
|
22
|
+
instructions,
|
|
23
|
+
labels: Array.isArray(body.labels) ? body.labels : undefined,
|
|
24
|
+
worktree: body.worktree === true,
|
|
25
|
+
startedBy: agent ? "agent" : "user",
|
|
26
|
+
delivery,
|
|
27
|
+
});
|
|
28
|
+
return NextResponse.json(started, { status: started.repeat ? 200 : 201 });
|
|
29
|
+
} catch (error) {
|
|
30
|
+
return fail(error);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { fail, text } from "@/lib/fork/api";
|
|
3
|
+
import { FORKS_OFF, forksOn } from "@/lib/fork/options";
|
|
4
|
+
import { registerForkWait } from "@/lib/fork/waits";
|
|
5
|
+
import { parseTimeout } from "@/lib/bus/waits/register";
|
|
6
|
+
import { kickForks } from "@/lib/fork/tick";
|
|
7
|
+
|
|
8
|
+
// `aos wait children --all|--any [--timeout 30m]`: records the wait and
|
|
9
|
+
// returns; the parent is woken with the results.
|
|
10
|
+
export async function POST(request: NextRequest) {
|
|
11
|
+
try {
|
|
12
|
+
if (!forksOn()) throw new Error(FORKS_OFF);
|
|
13
|
+
const body = await request.json();
|
|
14
|
+
const parent = text(body.from);
|
|
15
|
+
if (!parent) throw new Error("Only a session can wait on its forks");
|
|
16
|
+
const mode = body.mode === "any" ? "any" : "all";
|
|
17
|
+
const r = registerForkWait(
|
|
18
|
+
parent,
|
|
19
|
+
mode,
|
|
20
|
+
parseTimeout(body.timeout == null ? null : String(body.timeout))
|
|
21
|
+
);
|
|
22
|
+
kickForks();
|
|
23
|
+
return NextResponse.json(r);
|
|
24
|
+
} catch (error) {
|
|
25
|
+
return fail(error);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { isChild } from "@/lib/fork/ledger";
|
|
3
|
+
import { releaseLease, requestLease } from "@/lib/fork/heavy-lease";
|
|
4
|
+
|
|
5
|
+
// A fork child's `aos heavy` asks for a slot here and waits its turn; any
|
|
6
|
+
// other session isn't queued (it only gets the advice in /api/load/heavy).
|
|
7
|
+
export async function POST(request: NextRequest) {
|
|
8
|
+
try {
|
|
9
|
+
const { session, pid, action = "request" } = await request.json();
|
|
10
|
+
if (typeof session !== "string" || !Number.isInteger(pid) || pid <= 0)
|
|
11
|
+
return NextResponse.json(
|
|
12
|
+
{ error: "session and pid are required" },
|
|
13
|
+
{ status: 400 }
|
|
14
|
+
);
|
|
15
|
+
if (!isChild(session))
|
|
16
|
+
return NextResponse.json({ granted: true, ahead: 0 });
|
|
17
|
+
if (action === "release") {
|
|
18
|
+
releaseLease(session, pid);
|
|
19
|
+
return NextResponse.json({ released: true });
|
|
20
|
+
}
|
|
21
|
+
return NextResponse.json(requestLease(session, pid));
|
|
22
|
+
} catch (error) {
|
|
23
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
24
|
+
return NextResponse.json({ error: message }, { status: 400 });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -1,17 +1,30 @@
|
|
|
1
1
|
import { NextResponse } from "next/server";
|
|
2
|
-
import { archiveChecked } from "@/lib/done/archive";
|
|
2
|
+
import { archiveChecked, ForkChildrenOpen } from "@/lib/done/archive";
|
|
3
3
|
|
|
4
4
|
// POST /api/sessions/:id/archive - put a session away, out of the sidebar.
|
|
5
5
|
// 409 with the reason when refused.
|
|
6
6
|
export async function POST(
|
|
7
|
-
|
|
7
|
+
request: Request,
|
|
8
8
|
{ params }: { params: Promise<{ id: string }> }
|
|
9
9
|
) {
|
|
10
10
|
try {
|
|
11
|
-
const
|
|
11
|
+
const body = (await request.json().catch(() => ({}))) as {
|
|
12
|
+
children?: unknown;
|
|
13
|
+
};
|
|
14
|
+
const children =
|
|
15
|
+
body.children === "stop" || body.children === "keep"
|
|
16
|
+
? body.children
|
|
17
|
+
: undefined;
|
|
18
|
+
const session = await archiveChecked((await params).id, { children });
|
|
12
19
|
return NextResponse.json({ session });
|
|
13
20
|
} catch (error) {
|
|
14
21
|
const message = error instanceof Error ? error.message : String(error);
|
|
15
|
-
return NextResponse.json(
|
|
22
|
+
return NextResponse.json(
|
|
23
|
+
{
|
|
24
|
+
error: message,
|
|
25
|
+
...(error instanceof ForkChildrenOpen && { children: error.open }),
|
|
26
|
+
},
|
|
27
|
+
{ status: 409 }
|
|
28
|
+
);
|
|
16
29
|
}
|
|
17
30
|
}
|
|
@@ -9,7 +9,6 @@ import {
|
|
|
9
9
|
} from "@/lib/worktrees";
|
|
10
10
|
import { releasePort } from "@/lib/ports";
|
|
11
11
|
import { releaseSessionLeftovers } from "@/lib/sessions/leftovers";
|
|
12
|
-
import { killWorker } from "@/lib/orchestration";
|
|
13
12
|
import { endSessionAgent } from "@/lib/sessions/end";
|
|
14
13
|
import { endTmuxSession } from "@/lib/tmux/end";
|
|
15
14
|
import { hostExecFile } from "@/lib/hosts";
|
|
@@ -264,11 +263,11 @@ export async function DELETE(request: NextRequest, { params }: RouteParams) {
|
|
|
264
263
|
);
|
|
265
264
|
}
|
|
266
265
|
|
|
267
|
-
//
|
|
266
|
+
// Children nested under this session go with it
|
|
268
267
|
const workers = queries.getWorkersByConductor(db).all(id) as Session[];
|
|
269
268
|
for (const worker of workers) {
|
|
270
269
|
try {
|
|
271
|
-
await
|
|
270
|
+
await endSessionAgent(worker);
|
|
272
271
|
} catch (error) {
|
|
273
272
|
console.error(`Failed to kill worker ${worker.id}:`, error);
|
|
274
273
|
}
|
package/app/page.tsx
CHANGED
|
@@ -318,11 +318,6 @@ function HomeContent() {
|
|
|
318
318
|
return { sessionName, cwd, hostId, sessionId };
|
|
319
319
|
}
|
|
320
320
|
|
|
321
|
-
// TODO: Add explicit "Enable Orchestration" toggle that creates .mcp.json
|
|
322
|
-
// for conductor sessions. Removed auto-creation because it pollutes projects
|
|
323
|
-
// with .mcp.json files that aren't in their .gitignore.
|
|
324
|
-
// See: /api/sessions/[id]/mcp-config, lib/mcp-config.ts
|
|
325
|
-
|
|
326
321
|
// Get parent session ID for forking
|
|
327
322
|
let parentSessionId: string | null = null;
|
|
328
323
|
if (!session.claude_session_id && isFork(session)) {
|
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import type { StepCounts } from "@/lib/chat/tool-outcome";
|
|
4
3
|
import {
|
|
5
|
-
Fragment,
|
|
6
|
-
memo,
|
|
7
4
|
useCallback,
|
|
8
5
|
useEffect,
|
|
9
6
|
useLayoutEffect,
|
|
@@ -15,14 +12,17 @@ import { ArrowDown, Loader2 } from "lucide-react";
|
|
|
15
12
|
import type { ClipboardEvent } from "react";
|
|
16
13
|
import { useChat } from "@/data/chat/useChat";
|
|
17
14
|
import { PausedBanner } from "./PausedBanner";
|
|
15
|
+
import { groupTimeline } from "@/lib/chat/group";
|
|
16
|
+
import type { ChatItem } from "@/lib/chat/events";
|
|
18
17
|
import {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
} from "@/lib/chat/
|
|
24
|
-
import
|
|
25
|
-
import {
|
|
18
|
+
isForkCommand,
|
|
19
|
+
isFreshCommand,
|
|
20
|
+
withFork,
|
|
21
|
+
withFresh,
|
|
22
|
+
} from "@/lib/chat/commands";
|
|
23
|
+
import { useExperiment } from "@/data/experiments";
|
|
24
|
+
import { ForkDialog, ForksStrip } from "./Forks";
|
|
25
|
+
import { ReplyingTo } from "./ReplyingTo";
|
|
26
26
|
import { compactsFirst } from "@/lib/chat/pending";
|
|
27
27
|
import { useFreshStart } from "@/data/sessions";
|
|
28
28
|
import {
|
|
@@ -35,10 +35,8 @@ import { quoteInsert, quoteMarkdown } from "@/lib/chat/quote";
|
|
|
35
35
|
import { OrchestratorBar } from "@/components/Orchestrator/OrchestratorBar";
|
|
36
36
|
import { ActivityLine } from "./Activity";
|
|
37
37
|
import { modelLabel as pickedModelLabel } from "./ModelPicker";
|
|
38
|
-
import { Approval } from "./Approval";
|
|
39
38
|
import { BackgroundTasks } from "./BackgroundTasks";
|
|
40
|
-
import {
|
|
41
|
-
import { UndoDialog, UndoneBlock } from "./Undo";
|
|
39
|
+
import { UndoDialog } from "./Undo";
|
|
42
40
|
import { Composer } from "./Composer";
|
|
43
41
|
import { Queue } from "./Queue";
|
|
44
42
|
import { LivePanel } from "@/components/Live/LivePanel";
|
|
@@ -48,25 +46,19 @@ import { composerCompacts } from "@/lib/chat/stale";
|
|
|
48
46
|
import { useFreshStartQuery } from "@/data/fresh-start";
|
|
49
47
|
import { QuoteButton } from "./QuoteButton";
|
|
50
48
|
import { quoteIntoComposer } from "./quote-selection";
|
|
51
|
-
import { ToolBodies
|
|
49
|
+
import { ToolBodies } from "./Tools";
|
|
52
50
|
import { RowState, RowStore } from "./rowState";
|
|
53
51
|
import { useKeybindings } from "@/hooks/useKeybindings";
|
|
54
52
|
import { Background, type BackgroundValue } from "./background";
|
|
55
53
|
import { FindBar } from "./FindBar";
|
|
56
54
|
import { useFind, useFindShortcut } from "./useFind";
|
|
57
|
-
import { McpServers } from "./McpServers";
|
|
58
55
|
import { ChatSessionId } from "./session";
|
|
59
|
-
import { ArtifactCard } from "./Artifact";
|
|
60
|
-
import { PlanCard } from "./PlanCard";
|
|
61
56
|
import { ChatIdle, RetryTurn } from "./idle";
|
|
62
57
|
import { RETRY_TEXT } from "@/lib/chat/turn-end";
|
|
63
58
|
import { ContextMeter } from "./ContextMeter";
|
|
64
59
|
import { SetupCard } from "./SetupCard";
|
|
65
|
-
import { UserTurn } from "./EventRow";
|
|
66
|
-
import { EventGroup } from "./EventTurns";
|
|
67
60
|
import { needsMoreHistory } from "./needs-more-history";
|
|
68
61
|
import { useGoToItem } from "./useGoToItem";
|
|
69
|
-
import { NewDivider } from "./NewDivider";
|
|
70
62
|
import { usePosition } from "./usePosition";
|
|
71
63
|
import { NeedsYouStrip } from "@/components/Orchestrator/NeedsYouStrip";
|
|
72
64
|
import { useResolveQuestion } from "@/data/orchestrators";
|
|
@@ -83,180 +75,12 @@ import {
|
|
|
83
75
|
TurnStarts,
|
|
84
76
|
type BookmarkChat,
|
|
85
77
|
} from "@/components/Bookmarks/context";
|
|
86
|
-
import {
|
|
87
|
-
AssistantMessage,
|
|
88
|
-
CommandOutput,
|
|
89
|
-
Compacted,
|
|
90
|
-
SkillChip,
|
|
91
|
-
UserMessage,
|
|
92
|
-
ErrorMessage,
|
|
93
|
-
NoteLine,
|
|
94
|
-
Reasoning,
|
|
95
|
-
Todos,
|
|
96
|
-
TurnEnd,
|
|
97
|
-
} from "./Messages";
|
|
98
|
-
|
|
99
|
-
interface ItemActions {
|
|
100
|
-
respond: (id: string, answer: ApprovalDecision) => void;
|
|
101
|
-
// Absent inside undone messages; hidden while the agent works (ChatIdle).
|
|
102
|
-
onCarryPlan?: (id: string) => void;
|
|
103
|
-
onKeepPlanning?: () => void;
|
|
104
|
-
// Absent inside undone messages; hidden while the agent works (ChatIdle).
|
|
105
|
-
onUndo?: (id: string) => void;
|
|
106
|
-
onQuote?: (text: string) => void;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function Item({
|
|
110
|
-
item,
|
|
111
|
-
actions,
|
|
112
|
-
steps,
|
|
113
|
-
}: {
|
|
114
|
-
item: ChatItem;
|
|
115
|
-
actions: ItemActions;
|
|
116
|
-
steps?: StepCounts;
|
|
117
|
-
}) {
|
|
118
|
-
switch (item.kind) {
|
|
119
|
-
case "user":
|
|
120
|
-
return (
|
|
121
|
-
<UserTurn
|
|
122
|
-
item={item}
|
|
123
|
-
onUndo={
|
|
124
|
-
item.checkpoint && actions.onUndo
|
|
125
|
-
? () => actions.onUndo?.(item.id)
|
|
126
|
-
: undefined
|
|
127
|
-
}
|
|
128
|
-
/>
|
|
129
|
-
);
|
|
130
|
-
case "assistant":
|
|
131
|
-
return <AssistantMessage item={item} onQuote={actions.onQuote} />;
|
|
132
|
-
case "reasoning":
|
|
133
|
-
return <Reasoning item={item} />;
|
|
134
|
-
case "todos":
|
|
135
|
-
return <Todos item={item} />;
|
|
136
|
-
case "turn_end":
|
|
137
|
-
return <TurnEnd item={item} steps={steps} />;
|
|
138
|
-
case "error":
|
|
139
|
-
return <ErrorMessage item={item} />;
|
|
140
|
-
case "command_output":
|
|
141
|
-
return <CommandOutput item={item} />;
|
|
142
|
-
case "mcp":
|
|
143
|
-
return <McpServers item={item} />;
|
|
144
|
-
case "compacted":
|
|
145
|
-
return <Compacted item={item} />;
|
|
146
|
-
case "note":
|
|
147
|
-
return <NoteLine item={item} />;
|
|
148
|
-
case "artifact":
|
|
149
|
-
return <ArtifactCard item={item} />;
|
|
150
|
-
case "approval":
|
|
151
|
-
return <Approval item={item} respond={actions.respond} />;
|
|
152
|
-
case "plan":
|
|
153
|
-
return (
|
|
154
|
-
<PlanCard
|
|
155
|
-
item={item}
|
|
156
|
-
onCarryOut={
|
|
157
|
-
actions.onCarryPlan
|
|
158
|
-
? () => actions.onCarryPlan?.(item.id)
|
|
159
|
-
: undefined
|
|
160
|
-
}
|
|
161
|
-
onKeepPlanning={() => actions.onKeepPlanning?.()}
|
|
162
|
-
/>
|
|
163
|
-
);
|
|
164
|
-
case "tool":
|
|
165
|
-
return item.name === "Skill" ? (
|
|
166
|
-
<SkillChip item={item} />
|
|
167
|
-
) : STANDALONE_TOOLS.has(item.name) ? (
|
|
168
|
-
<SubagentCard item={item} />
|
|
169
|
-
) : null;
|
|
170
|
-
default:
|
|
171
|
-
return null;
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function Timeline({
|
|
176
|
-
blocks,
|
|
177
|
-
actions,
|
|
178
|
-
}: {
|
|
179
|
-
blocks: TimelineBlock[];
|
|
180
|
-
actions: ItemActions;
|
|
181
|
-
}) {
|
|
182
|
-
return blocks.map((b) => (
|
|
183
|
-
<Block
|
|
184
|
-
key={b.type === "item" ? b.item.id : b.id}
|
|
185
|
-
block={b}
|
|
186
|
-
actions={actions}
|
|
187
|
-
/>
|
|
188
|
-
));
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
const blockKey = (b: TimelineBlock) => (b.type === "item" ? b.item.id : b.id);
|
|
78
|
+
import { UserMessage } from "./Messages";
|
|
192
79
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
const Block = memo(
|
|
196
|
-
function Block({
|
|
197
|
-
block: b,
|
|
198
|
-
actions,
|
|
199
|
-
}: {
|
|
200
|
-
block: TimelineBlock;
|
|
201
|
-
actions: ItemActions;
|
|
202
|
-
}) {
|
|
203
|
-
const respond = actions.respond;
|
|
204
|
-
const undoneActions = useMemo(
|
|
205
|
-
() => ({ respond, onQuote: actions.onQuote }),
|
|
206
|
-
[respond, actions.onQuote]
|
|
207
|
-
);
|
|
208
|
-
return b.type === "tools" ? (
|
|
209
|
-
<ToolGroup tools={b.tools} />
|
|
210
|
-
) : b.type === "events" ? (
|
|
211
|
-
<EventGroup
|
|
212
|
-
id={b.id}
|
|
213
|
-
turns={b.turns}
|
|
214
|
-
renderReplies={(items) => (
|
|
215
|
-
<Timeline blocks={groupTimeline(items)} actions={undoneActions} />
|
|
216
|
-
)}
|
|
217
|
-
/>
|
|
218
|
-
) : b.type === "undone" ? (
|
|
219
|
-
<UndoneBlock
|
|
220
|
-
undo={b.undo}
|
|
221
|
-
count={b.items.filter((i) => i.kind === "user").length}
|
|
222
|
-
>
|
|
223
|
-
<Timeline blocks={groupTimeline(b.items)} actions={undoneActions} />
|
|
224
|
-
</UndoneBlock>
|
|
225
|
-
) : (
|
|
226
|
-
<div data-item={b.item.id}>
|
|
227
|
-
<Item item={b.item} actions={actions} steps={b.steps} />
|
|
228
|
-
</div>
|
|
229
|
-
);
|
|
230
|
-
},
|
|
231
|
-
(prev, next) =>
|
|
232
|
-
prev.actions === next.actions && sameBlock(prev.block, next.block)
|
|
233
|
-
);
|
|
80
|
+
import type { ItemActions } from "./TimelineRows";
|
|
81
|
+
import { TimelineRows } from "./TimelineRows";
|
|
234
82
|
|
|
235
|
-
|
|
236
|
-
// selection follows the DOM, and a list that recycles and re-orders its rows
|
|
237
|
-
// breaks it (docs/decisions/2026-10-11-chat-timeline-in-flow.md).
|
|
238
|
-
export function TimelineRows({
|
|
239
|
-
blocks,
|
|
240
|
-
actions,
|
|
241
|
-
newFrom,
|
|
242
|
-
}: {
|
|
243
|
-
blocks: TimelineBlock[];
|
|
244
|
-
actions: ItemActions;
|
|
245
|
-
// The block the reader hasn't seen yet begins at, when they were away.
|
|
246
|
-
newFrom?: string | null;
|
|
247
|
-
}) {
|
|
248
|
-
return blocks.map((b) => (
|
|
249
|
-
<Fragment key={blockKey(b)}>
|
|
250
|
-
{newFrom === blockKey(b) && <NewDivider />}
|
|
251
|
-
<div
|
|
252
|
-
data-block={blockKey(b)}
|
|
253
|
-
className="chat-row mx-auto w-full max-w-3xl px-4 pb-4"
|
|
254
|
-
>
|
|
255
|
-
<Block block={b} actions={actions} />
|
|
256
|
-
</div>
|
|
257
|
-
</Fragment>
|
|
258
|
-
));
|
|
259
|
-
}
|
|
83
|
+
export { TimelineRows };
|
|
260
84
|
|
|
261
85
|
export function ChatPanel({
|
|
262
86
|
sessionId,
|
|
@@ -282,6 +106,9 @@ export function ChatPanel({
|
|
|
282
106
|
at: number;
|
|
283
107
|
append?: boolean;
|
|
284
108
|
}>();
|
|
109
|
+
const forksOn = useExperiment("forks");
|
|
110
|
+
const [forkOpen, setForkOpen] = useState(false);
|
|
111
|
+
const withForkIf = (c: typeof commands) => (forksOn ? withFork(c) : c);
|
|
285
112
|
const {
|
|
286
113
|
items,
|
|
287
114
|
hasMore,
|
|
@@ -799,19 +626,7 @@ export function ChatPanel({
|
|
|
799
626
|
/>
|
|
800
627
|
)}
|
|
801
628
|
{replyTo && (
|
|
802
|
-
<
|
|
803
|
-
<span className="min-w-0 flex-1 truncate">
|
|
804
|
-
Answering: {replyTo.text}
|
|
805
|
-
</span>
|
|
806
|
-
<button
|
|
807
|
-
type="button"
|
|
808
|
-
onClick={() => setReplyTo(null)}
|
|
809
|
-
aria-label="Stop answering"
|
|
810
|
-
className="hover:text-foreground flex size-11 shrink-0 items-center justify-center"
|
|
811
|
-
>
|
|
812
|
-
×
|
|
813
|
-
</button>
|
|
814
|
-
</div>
|
|
629
|
+
<ReplyingTo text={replyTo.text} onStop={() => setReplyTo(null)} />
|
|
815
630
|
)}
|
|
816
631
|
<ActivityLine
|
|
817
632
|
items={items}
|
|
@@ -830,6 +645,7 @@ export function ChatPanel({
|
|
|
830
645
|
/>
|
|
831
646
|
<QuoteButton rootRef={scrollRef} onQuote={quote} />
|
|
832
647
|
<PausedBanner sessionId={sessionId} />
|
|
648
|
+
<ForksStrip sessionId={sessionId} />
|
|
833
649
|
<Queue
|
|
834
650
|
queue={queue}
|
|
835
651
|
running={running}
|
|
@@ -872,6 +688,10 @@ export function ChatPanel({
|
|
|
872
688
|
startFresh.mutate(sessionId);
|
|
873
689
|
return;
|
|
874
690
|
}
|
|
691
|
+
if (forksOn && isForkCommand(text)) {
|
|
692
|
+
setForkOpen(true);
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
875
695
|
toEnd();
|
|
876
696
|
const compact = composerCompacts(text, context, {
|
|
877
697
|
state,
|
|
@@ -891,7 +711,7 @@ export function ChatPanel({
|
|
|
891
711
|
}
|
|
892
712
|
}}
|
|
893
713
|
onStop={interrupt}
|
|
894
|
-
commands={freshStart ? withFresh(commands) : commands}
|
|
714
|
+
commands={withForkIf(freshStart ? withFresh(commands) : commands)}
|
|
895
715
|
onRefreshCommands={refreshCommands}
|
|
896
716
|
refreshingCommands={refreshingCommands}
|
|
897
717
|
models={models}
|
|
@@ -911,6 +731,13 @@ export function ChatPanel({
|
|
|
911
731
|
focusKey={active ? sessionId : null}
|
|
912
732
|
/>
|
|
913
733
|
</div>
|
|
734
|
+
{forksOn && (
|
|
735
|
+
<ForkDialog
|
|
736
|
+
sessionId={sessionId}
|
|
737
|
+
open={forkOpen}
|
|
738
|
+
onOpenChange={setForkOpen}
|
|
739
|
+
/>
|
|
740
|
+
)}
|
|
914
741
|
{undoPreview && (
|
|
915
742
|
<UndoDialog
|
|
916
743
|
preview={undoPreview.preview}
|