@saadnvd1/agent-os 0.3.824 → 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/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/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,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A throwaway checkout for a fork child that has to edit code to find its
|
|
3
|
+
* answer (`aos fork --worktree`): detached at the parent's HEAD, with the
|
|
4
|
+
* parent's uncommitted changes applied on top, so it sees what the parent
|
|
5
|
+
* sees. No branch is made and nothing from it is merged; it is removed when
|
|
6
|
+
* the child settles.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import fs from "fs";
|
|
10
|
+
import path from "path";
|
|
11
|
+
import { git } from "../git-exec";
|
|
12
|
+
import { setupWorktree } from "../env-setup";
|
|
13
|
+
import { forkHome, worktreeOf } from "./scratch";
|
|
14
|
+
|
|
15
|
+
const MAX_UNTRACKED = 200;
|
|
16
|
+
const MAX_UNTRACKED_BYTES = 5_000_000;
|
|
17
|
+
// A change to these means the parent's installed packages may not match.
|
|
18
|
+
const DEPENDENCY_FILES =
|
|
19
|
+
/(^|\/)(package\.json|package-lock\.json|pnpm-lock\.yaml|yarn\.lock|bun\.lockb?)$/;
|
|
20
|
+
|
|
21
|
+
export interface ForkWorktree {
|
|
22
|
+
// Where the child runs: the parent's folder, in the checkout.
|
|
23
|
+
cwd: string;
|
|
24
|
+
// The checkout's root.
|
|
25
|
+
root: string;
|
|
26
|
+
// The parent's packages are linked in instead of installed.
|
|
27
|
+
linked: boolean;
|
|
28
|
+
// The commit it started from, for the child's return.
|
|
29
|
+
head: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function createForkWorktree(opts: {
|
|
33
|
+
childId: string;
|
|
34
|
+
parentCwd: string;
|
|
35
|
+
}): Promise<ForkWorktree> {
|
|
36
|
+
const cwd = opts.parentCwd;
|
|
37
|
+
const top = (await git(cwd, ["rev-parse", "--show-toplevel"])).trim();
|
|
38
|
+
const head = (await git(top, ["rev-parse", "HEAD"])).trim();
|
|
39
|
+
const wt = worktreeOf(opts.childId);
|
|
40
|
+
await fs.promises.mkdir(forkHome(opts.childId), { recursive: true });
|
|
41
|
+
try {
|
|
42
|
+
await git(top, ["worktree", "add", "--detach", "--", wt, head], {
|
|
43
|
+
timeout: 5 * 60_000,
|
|
44
|
+
});
|
|
45
|
+
const diff = await git(top, ["diff", "HEAD", "--binary"], {
|
|
46
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
47
|
+
});
|
|
48
|
+
if (diff.trim())
|
|
49
|
+
await git(wt, ["apply", "--whitespace=nowarn", "--binary", "-"], {
|
|
50
|
+
input: diff,
|
|
51
|
+
});
|
|
52
|
+
await copyUntracked(top, wt);
|
|
53
|
+
const changed = (await git(top, ["diff", "HEAD", "--name-only"]))
|
|
54
|
+
.split("\n")
|
|
55
|
+
.filter(Boolean);
|
|
56
|
+
const hasPackages = await exists(path.join(top, "node_modules"));
|
|
57
|
+
const linked =
|
|
58
|
+
hasPackages && !changed.some((f) => DEPENDENCY_FILES.test(f));
|
|
59
|
+
if (linked)
|
|
60
|
+
await fs.promises.symlink(
|
|
61
|
+
path.join(top, "node_modules"),
|
|
62
|
+
path.join(wt, "node_modules")
|
|
63
|
+
);
|
|
64
|
+
const setup = await setupWorktree({
|
|
65
|
+
worktreePath: wt,
|
|
66
|
+
sourcePath: top,
|
|
67
|
+
skipInstall: linked,
|
|
68
|
+
});
|
|
69
|
+
if (!setup.success)
|
|
70
|
+
throw new Error(
|
|
71
|
+
setup.steps.find((s) => !s.success)?.error ?? "worktree setup failed"
|
|
72
|
+
);
|
|
73
|
+
return {
|
|
74
|
+
cwd: path.join(wt, path.relative(top, cwd)),
|
|
75
|
+
root: wt,
|
|
76
|
+
linked,
|
|
77
|
+
head,
|
|
78
|
+
};
|
|
79
|
+
} catch (error) {
|
|
80
|
+
const { removeFork } = await import("./scratch");
|
|
81
|
+
await removeFork(opts.childId);
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const exists = (p: string) =>
|
|
87
|
+
fs.promises.access(p).then(
|
|
88
|
+
() => true,
|
|
89
|
+
() => false
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
// Files the parent made and hasn't added yet, which a diff doesn't carry.
|
|
93
|
+
async function copyUntracked(top: string, wt: string): Promise<void> {
|
|
94
|
+
const out = await git(top, [
|
|
95
|
+
"ls-files",
|
|
96
|
+
"--others",
|
|
97
|
+
"--exclude-standard",
|
|
98
|
+
"-z",
|
|
99
|
+
]);
|
|
100
|
+
const files = out.split("\0").filter(Boolean);
|
|
101
|
+
if (files.length > MAX_UNTRACKED)
|
|
102
|
+
throw new Error(
|
|
103
|
+
`the parent has ${files.length} new files; commit or stage them first`
|
|
104
|
+
);
|
|
105
|
+
for (const rel of files) {
|
|
106
|
+
const from = path.join(top, rel);
|
|
107
|
+
const stat = await fs.promises.lstat(from);
|
|
108
|
+
if (stat.isSymbolicLink() || !stat.isFile()) continue;
|
|
109
|
+
if (stat.size > MAX_UNTRACKED_BYTES)
|
|
110
|
+
throw new Error(`${rel} is too large to carry into a checkout`);
|
|
111
|
+
const to = path.join(wt, rel);
|
|
112
|
+
await fs.promises.mkdir(path.dirname(to), { recursive: true });
|
|
113
|
+
await fs.promises.copyFile(from, to);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -37,7 +37,7 @@ export interface ShellContext {
|
|
|
37
37
|
protectedPids: () => Promise<Set<number>>;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
interface Invocation {
|
|
40
|
+
export interface Invocation {
|
|
41
41
|
name: string;
|
|
42
42
|
args: string[];
|
|
43
43
|
cmd: SimpleCommand;
|
|
@@ -87,7 +87,10 @@ const SHELLS = /^(ba|z|da|k|fi|c|tc)?sh$/;
|
|
|
87
87
|
|
|
88
88
|
// What a simple command runs once its wrappers are taken off: one
|
|
89
89
|
// invocation, or several (`bash -c '…'`, `find -exec`).
|
|
90
|
-
function invocations(
|
|
90
|
+
export function invocations(
|
|
91
|
+
cmd: SimpleCommand,
|
|
92
|
+
depth = 0
|
|
93
|
+
): Invocation[] | string {
|
|
91
94
|
let words = cmd.words;
|
|
92
95
|
for (;;) {
|
|
93
96
|
while (
|
|
@@ -183,7 +186,7 @@ function findInvocations(
|
|
|
183
186
|
}
|
|
184
187
|
|
|
185
188
|
// Words that aren't options; `--` ends options. `values` take the next word.
|
|
186
|
-
function positionals(args: string[], values: string[] = []): string[] {
|
|
189
|
+
export function positionals(args: string[], values: string[] = []): string[] {
|
|
187
190
|
const out: string[] = [];
|
|
188
191
|
for (let i = 0; i < args.length; i++) {
|
|
189
192
|
const a = args[i];
|
|
@@ -130,7 +130,7 @@ const FOLDS_CASE =
|
|
|
130
130
|
const key = (p: string) =>
|
|
131
131
|
FOLDS_CASE ? p.normalize("NFC").toLowerCase() : p.normalize("NFC");
|
|
132
132
|
|
|
133
|
-
const within = (p: string, root: string) => {
|
|
133
|
+
export const within = (p: string, root: string) => {
|
|
134
134
|
const [a, b] = [key(p), key(root)];
|
|
135
135
|
return a === b || a.startsWith(b === "/" ? b : b + "/");
|
|
136
136
|
};
|
package/lib/security/demo.ts
CHANGED
|
@@ -120,10 +120,9 @@ const READS = [
|
|
|
120
120
|
"schedules",
|
|
121
121
|
"schedules/[id]",
|
|
122
122
|
"orchestrators",
|
|
123
|
-
"orchestrate/workers",
|
|
124
|
-
"orchestrate/workers/[id]",
|
|
125
123
|
"bus/messages",
|
|
126
124
|
"bus/peers",
|
|
125
|
+
"fork/children",
|
|
127
126
|
"usage",
|
|
128
127
|
"vendor/mermaid",
|
|
129
128
|
"artifacts/[id]",
|
package/lib/sessions/end.ts
CHANGED
|
@@ -5,6 +5,8 @@ import { endTmuxSession } from "../tmux/end";
|
|
|
5
5
|
import { releaseOnEnd, shutTaskSimulators } from "../simulator";
|
|
6
6
|
import { forgetPlanWait } from "../models/plan-step";
|
|
7
7
|
import { kickWaits } from "../bus/waits/kick";
|
|
8
|
+
import { openChildren } from "../fork/ledger";
|
|
9
|
+
import { cancelChildren } from "../fork/settle";
|
|
8
10
|
|
|
9
11
|
type Agent = Pick<Session, "id" | "host_id" | "tmux_name"> &
|
|
10
12
|
Partial<Pick<Session, "worktree_path" | "branch_name">>;
|
|
@@ -14,6 +16,9 @@ type Agent = Pick<Session, "id" | "host_id" | "tmux_name"> &
|
|
|
14
16
|
// finish share.
|
|
15
17
|
export async function endSessionAgent(s: Agent): Promise<void> {
|
|
16
18
|
forgetPlanWait(s.id);
|
|
19
|
+
// Its forks have no one to report to.
|
|
20
|
+
if (openChildren(s.id).length)
|
|
21
|
+
void cancelChildren(s.id).catch(() => undefined);
|
|
17
22
|
stopChat(s.id);
|
|
18
23
|
// The task's simulator goes with it; no row, no work.
|
|
19
24
|
releaseOnEnd(s.id, { remove: true });
|
package/package.json
CHANGED
package/server.ts
CHANGED
|
@@ -17,6 +17,7 @@ import { stackStartGate } from "./lib/orchestrator/brakes";
|
|
|
17
17
|
import { buildId } from "./lib/build";
|
|
18
18
|
import { startOrchestratorWatcher } from "./lib/orchestrator/watcher";
|
|
19
19
|
import { startWaitWatcher } from "./lib/bus/waits/tick";
|
|
20
|
+
import { startForkWatcher } from "./lib/fork/tick";
|
|
20
21
|
import { installErrorCounter } from "./lib/health/errors";
|
|
21
22
|
import { recordBoot } from "./lib/health/boots";
|
|
22
23
|
import { realDeps, schedulesEnabled, startScheduler } from "./lib/schedules";
|
|
@@ -498,6 +499,7 @@ app.prepare().then(async () => {
|
|
|
498
499
|
if (process.env.AGENTOS_ORCHESTRATOR !== "off") startOrchestratorWatcher();
|
|
499
500
|
// Sessions waiting on each other (aos send --await, aos wait).
|
|
500
501
|
startWaitWatcher();
|
|
502
|
+
startForkWatcher();
|
|
501
503
|
// Schedules tick here, once a minute, and nowhere else.
|
|
502
504
|
if (schedulesEnabled(process.env)) startScheduler(realDeps);
|
|
503
505
|
// Looking for a newer version, and running an update when one is asked
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { NextResponse } from "next/server";
|
|
2
|
-
import { resolveModelForAgent } from "@/lib/model-catalog";
|
|
3
|
-
import { isValidAgentType, type AgentType } from "@/lib/providers";
|
|
4
|
-
import { spawnWorker } from "@/lib/orchestration";
|
|
5
|
-
|
|
6
|
-
export async function POST(request: Request) {
|
|
7
|
-
try {
|
|
8
|
-
const body = await request.json();
|
|
9
|
-
const {
|
|
10
|
-
conductorSessionId,
|
|
11
|
-
task,
|
|
12
|
-
workingDirectory,
|
|
13
|
-
branchName,
|
|
14
|
-
useWorktree = true,
|
|
15
|
-
model,
|
|
16
|
-
agentType: rawAgentType = "claude",
|
|
17
|
-
} = body;
|
|
18
|
-
const agentType: AgentType = isValidAgentType(rawAgentType)
|
|
19
|
-
? rawAgentType
|
|
20
|
-
: "claude";
|
|
21
|
-
const resolvedModel = resolveModelForAgent(
|
|
22
|
-
agentType,
|
|
23
|
-
typeof model === "string" ? model.trim() : model
|
|
24
|
-
);
|
|
25
|
-
|
|
26
|
-
if (!conductorSessionId || !task || !workingDirectory) {
|
|
27
|
-
return NextResponse.json(
|
|
28
|
-
{
|
|
29
|
-
error:
|
|
30
|
-
"Missing required fields: conductorSessionId, task, workingDirectory",
|
|
31
|
-
},
|
|
32
|
-
{ status: 400 }
|
|
33
|
-
);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const session = await spawnWorker({
|
|
37
|
-
conductorSessionId,
|
|
38
|
-
task,
|
|
39
|
-
workingDirectory,
|
|
40
|
-
branchName,
|
|
41
|
-
useWorktree,
|
|
42
|
-
model: resolvedModel,
|
|
43
|
-
agentType,
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
return NextResponse.json({ session });
|
|
47
|
-
} catch (error) {
|
|
48
|
-
console.error("Failed to spawn worker:", error);
|
|
49
|
-
return NextResponse.json(
|
|
50
|
-
{ error: "Failed to spawn worker" },
|
|
51
|
-
{ status: 500 }
|
|
52
|
-
);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import { NextResponse } from "next/server";
|
|
2
|
-
import {
|
|
3
|
-
getWorkerOutput,
|
|
4
|
-
sendToWorker,
|
|
5
|
-
completeWorker,
|
|
6
|
-
failWorker,
|
|
7
|
-
killWorker,
|
|
8
|
-
} from "@/lib/orchestration";
|
|
9
|
-
|
|
10
|
-
interface RouteParams {
|
|
11
|
-
params: Promise<{ id: string }>;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
// GET /api/orchestrate/workers/[id] - Get worker output
|
|
15
|
-
export async function GET(request: Request, { params }: RouteParams) {
|
|
16
|
-
try {
|
|
17
|
-
const { id } = await params;
|
|
18
|
-
const { searchParams } = new URL(request.url);
|
|
19
|
-
const lines = parseInt(searchParams.get("lines") || "50", 10);
|
|
20
|
-
|
|
21
|
-
const output = await getWorkerOutput(id, lines);
|
|
22
|
-
return NextResponse.json({ output });
|
|
23
|
-
} catch (error) {
|
|
24
|
-
console.error("Failed to get worker output:", error);
|
|
25
|
-
return NextResponse.json(
|
|
26
|
-
{ error: "Failed to get worker output" },
|
|
27
|
-
{ status: 500 }
|
|
28
|
-
);
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// POST /api/orchestrate/workers/[id] - Send message or update status
|
|
33
|
-
export async function POST(request: Request, { params }: RouteParams) {
|
|
34
|
-
try {
|
|
35
|
-
const { id } = await params;
|
|
36
|
-
const body = await request.json();
|
|
37
|
-
const { action, message } = body;
|
|
38
|
-
|
|
39
|
-
switch (action) {
|
|
40
|
-
case "send":
|
|
41
|
-
if (!message) {
|
|
42
|
-
return NextResponse.json(
|
|
43
|
-
{ error: "Missing message" },
|
|
44
|
-
{ status: 400 }
|
|
45
|
-
);
|
|
46
|
-
}
|
|
47
|
-
const sent = await sendToWorker(id, message);
|
|
48
|
-
return NextResponse.json({ success: sent });
|
|
49
|
-
|
|
50
|
-
case "complete":
|
|
51
|
-
completeWorker(id);
|
|
52
|
-
return NextResponse.json({ success: true });
|
|
53
|
-
|
|
54
|
-
case "fail":
|
|
55
|
-
failWorker(id);
|
|
56
|
-
return NextResponse.json({ success: true });
|
|
57
|
-
|
|
58
|
-
default:
|
|
59
|
-
return NextResponse.json(
|
|
60
|
-
{ error: "Invalid action. Use: send, complete, or fail" },
|
|
61
|
-
{ status: 400 }
|
|
62
|
-
);
|
|
63
|
-
}
|
|
64
|
-
} catch (error) {
|
|
65
|
-
console.error("Failed to perform worker action:", error);
|
|
66
|
-
return NextResponse.json(
|
|
67
|
-
{ error: "Failed to perform worker action" },
|
|
68
|
-
{ status: 500 }
|
|
69
|
-
);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// DELETE /api/orchestrate/workers/[id] - Kill worker
|
|
74
|
-
export async function DELETE(request: Request, { params }: RouteParams) {
|
|
75
|
-
try {
|
|
76
|
-
const { id } = await params;
|
|
77
|
-
const { searchParams } = new URL(request.url);
|
|
78
|
-
const cleanupWorktree = searchParams.get("cleanup") === "true";
|
|
79
|
-
|
|
80
|
-
await killWorker(id, cleanupWorktree);
|
|
81
|
-
return NextResponse.json({ success: true });
|
|
82
|
-
} catch (error) {
|
|
83
|
-
console.error("Failed to kill worker:", error);
|
|
84
|
-
return NextResponse.json(
|
|
85
|
-
{ error: "Failed to kill worker" },
|
|
86
|
-
{ status: 500 }
|
|
87
|
-
);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import { NextResponse } from "next/server";
|
|
2
|
-
import { getWorkers, getWorkersSummary } from "@/lib/orchestration";
|
|
3
|
-
|
|
4
|
-
export async function GET(request: Request) {
|
|
5
|
-
try {
|
|
6
|
-
const { searchParams } = new URL(request.url);
|
|
7
|
-
const conductorId = searchParams.get("conductorId");
|
|
8
|
-
const summaryOnly = searchParams.get("summary") === "true";
|
|
9
|
-
|
|
10
|
-
if (!conductorId) {
|
|
11
|
-
return NextResponse.json(
|
|
12
|
-
{ error: "Missing conductorId parameter" },
|
|
13
|
-
{ status: 400 }
|
|
14
|
-
);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
if (summaryOnly) {
|
|
18
|
-
const summary = await getWorkersSummary(conductorId);
|
|
19
|
-
return NextResponse.json({ summary });
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const workers = await getWorkers(conductorId);
|
|
23
|
-
return NextResponse.json({ workers });
|
|
24
|
-
} catch (error) {
|
|
25
|
-
console.error("Failed to get workers:", error);
|
|
26
|
-
return NextResponse.json(
|
|
27
|
-
{ error: "Failed to get workers" },
|
|
28
|
-
{ status: 500 }
|
|
29
|
-
);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
-
import { db, queries, type Session } from "@/lib/db";
|
|
3
|
-
import { ensureMcpConfig } from "@/lib/mcp-config";
|
|
4
|
-
|
|
5
|
-
// POST /api/sessions/[id]/mcp-config - Ensure MCP config exists for this session
|
|
6
|
-
export async function POST(
|
|
7
|
-
request: NextRequest,
|
|
8
|
-
{ params }: { params: Promise<{ id: string }> }
|
|
9
|
-
) {
|
|
10
|
-
try {
|
|
11
|
-
const { id } = await params;
|
|
12
|
-
const session = queries.getSession(db).get(id) as Session | undefined;
|
|
13
|
-
|
|
14
|
-
if (!session) {
|
|
15
|
-
return NextResponse.json({ error: "Session not found" }, { status: 404 });
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
// Expand ~ to home directory
|
|
19
|
-
const workingDirectory = session.working_directory.replace(
|
|
20
|
-
/^~/,
|
|
21
|
-
process.env.HOME || ""
|
|
22
|
-
);
|
|
23
|
-
|
|
24
|
-
ensureMcpConfig(workingDirectory, id);
|
|
25
|
-
|
|
26
|
-
return NextResponse.json({ success: true });
|
|
27
|
-
} catch (error) {
|
|
28
|
-
console.error("Failed to write MCP config:", error);
|
|
29
|
-
return NextResponse.json(
|
|
30
|
-
{ error: "Failed to write MCP config" },
|
|
31
|
-
{ status: 500 }
|
|
32
|
-
);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
@@ -1,258 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import { useState, useEffect, useCallback } from "react";
|
|
4
|
-
import { WorkerCard, type WorkerInfo } from "./WorkerCard";
|
|
5
|
-
import { Button } from "./ui/button";
|
|
6
|
-
import {
|
|
7
|
-
RefreshCw,
|
|
8
|
-
Users,
|
|
9
|
-
CheckCircle,
|
|
10
|
-
Loader2,
|
|
11
|
-
AlertCircle,
|
|
12
|
-
XCircle,
|
|
13
|
-
} from "lucide-react";
|
|
14
|
-
import { cn } from "@/lib/utils";
|
|
15
|
-
|
|
16
|
-
interface WorkersSummary {
|
|
17
|
-
total: number;
|
|
18
|
-
pending: number;
|
|
19
|
-
running: number;
|
|
20
|
-
waiting: number;
|
|
21
|
-
completed: number;
|
|
22
|
-
failed: number;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
interface ConductorPanelProps {
|
|
26
|
-
conductorSessionId: string;
|
|
27
|
-
onAttachToWorker?: (workerId: string) => void;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function ConductorPanel({
|
|
31
|
-
conductorSessionId,
|
|
32
|
-
onAttachToWorker,
|
|
33
|
-
}: ConductorPanelProps) {
|
|
34
|
-
const [workers, setWorkers] = useState<WorkerInfo[]>([]);
|
|
35
|
-
const [summary, setSummary] = useState<WorkersSummary | null>(null);
|
|
36
|
-
const [expandedWorkers, setExpandedWorkers] = useState<Set<string>>(
|
|
37
|
-
new Set()
|
|
38
|
-
);
|
|
39
|
-
const [workerOutputs, setWorkerOutputs] = useState<Record<string, string>>(
|
|
40
|
-
{}
|
|
41
|
-
);
|
|
42
|
-
const [loading, setLoading] = useState(true);
|
|
43
|
-
const [refreshing, setRefreshing] = useState(false);
|
|
44
|
-
|
|
45
|
-
const fetchWorkers = useCallback(async () => {
|
|
46
|
-
try {
|
|
47
|
-
const res = await fetch(
|
|
48
|
-
`/api/orchestrate/workers?conductorId=${conductorSessionId}`
|
|
49
|
-
);
|
|
50
|
-
const data = await res.json();
|
|
51
|
-
if (data.workers) {
|
|
52
|
-
setWorkers(data.workers);
|
|
53
|
-
}
|
|
54
|
-
} catch (error) {
|
|
55
|
-
console.error("Failed to fetch workers:", error);
|
|
56
|
-
}
|
|
57
|
-
}, [conductorSessionId]);
|
|
58
|
-
|
|
59
|
-
const fetchSummary = useCallback(async () => {
|
|
60
|
-
try {
|
|
61
|
-
const res = await fetch(
|
|
62
|
-
`/api/orchestrate/workers?conductorId=${conductorSessionId}&summary=true`
|
|
63
|
-
);
|
|
64
|
-
const data = await res.json();
|
|
65
|
-
if (data.summary) {
|
|
66
|
-
setSummary(data.summary);
|
|
67
|
-
}
|
|
68
|
-
} catch (error) {
|
|
69
|
-
console.error("Failed to fetch summary:", error);
|
|
70
|
-
}
|
|
71
|
-
}, [conductorSessionId]);
|
|
72
|
-
|
|
73
|
-
const fetchWorkerOutput = useCallback(async (workerId: string) => {
|
|
74
|
-
try {
|
|
75
|
-
const res = await fetch(`/api/orchestrate/workers/${workerId}?lines=30`);
|
|
76
|
-
const data = await res.json();
|
|
77
|
-
if (data.output) {
|
|
78
|
-
setWorkerOutputs((prev) => ({ ...prev, [workerId]: data.output }));
|
|
79
|
-
}
|
|
80
|
-
} catch (error) {
|
|
81
|
-
console.error("Failed to fetch worker output:", error);
|
|
82
|
-
}
|
|
83
|
-
}, []);
|
|
84
|
-
|
|
85
|
-
const refresh = useCallback(async () => {
|
|
86
|
-
setRefreshing(true);
|
|
87
|
-
await Promise.all([fetchWorkers(), fetchSummary()]);
|
|
88
|
-
setRefreshing(false);
|
|
89
|
-
}, [fetchWorkers, fetchSummary]);
|
|
90
|
-
|
|
91
|
-
// Initial load
|
|
92
|
-
useEffect(() => {
|
|
93
|
-
const load = async () => {
|
|
94
|
-
setLoading(true);
|
|
95
|
-
await Promise.all([fetchWorkers(), fetchSummary()]);
|
|
96
|
-
setLoading(false);
|
|
97
|
-
};
|
|
98
|
-
load();
|
|
99
|
-
}, [fetchWorkers, fetchSummary]);
|
|
100
|
-
|
|
101
|
-
// Poll for updates every 5 seconds
|
|
102
|
-
useEffect(() => {
|
|
103
|
-
const interval = setInterval(refresh, 5000);
|
|
104
|
-
return () => clearInterval(interval);
|
|
105
|
-
}, [refresh]);
|
|
106
|
-
|
|
107
|
-
// Fetch output for expanded workers
|
|
108
|
-
useEffect(() => {
|
|
109
|
-
expandedWorkers.forEach((workerId) => {
|
|
110
|
-
if (!workerOutputs[workerId]) {
|
|
111
|
-
fetchWorkerOutput(workerId);
|
|
112
|
-
}
|
|
113
|
-
});
|
|
114
|
-
}, [expandedWorkers, workerOutputs, fetchWorkerOutput]);
|
|
115
|
-
|
|
116
|
-
const toggleExpand = (workerId: string) => {
|
|
117
|
-
setExpandedWorkers((prev) => {
|
|
118
|
-
const next = new Set(prev);
|
|
119
|
-
if (next.has(workerId)) {
|
|
120
|
-
next.delete(workerId);
|
|
121
|
-
} else {
|
|
122
|
-
next.add(workerId);
|
|
123
|
-
// Fetch output when expanding
|
|
124
|
-
fetchWorkerOutput(workerId);
|
|
125
|
-
}
|
|
126
|
-
return next;
|
|
127
|
-
});
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
const handleSendMessage = async (workerId: string, message: string) => {
|
|
131
|
-
try {
|
|
132
|
-
await fetch(`/api/orchestrate/workers/${workerId}`, {
|
|
133
|
-
method: "POST",
|
|
134
|
-
headers: { "Content-Type": "application/json" },
|
|
135
|
-
body: JSON.stringify({ action: "send", message }),
|
|
136
|
-
});
|
|
137
|
-
// Refresh output after sending
|
|
138
|
-
setTimeout(() => fetchWorkerOutput(workerId), 1000);
|
|
139
|
-
} catch (error) {
|
|
140
|
-
console.error("Failed to send message:", error);
|
|
141
|
-
}
|
|
142
|
-
};
|
|
143
|
-
|
|
144
|
-
const handleKillWorker = async (workerId: string) => {
|
|
145
|
-
if (!confirm("Kill this worker?")) return;
|
|
146
|
-
try {
|
|
147
|
-
await fetch(`/api/orchestrate/workers/${workerId}`, {
|
|
148
|
-
method: "DELETE",
|
|
149
|
-
});
|
|
150
|
-
await refresh();
|
|
151
|
-
} catch (error) {
|
|
152
|
-
console.error("Failed to kill worker:", error);
|
|
153
|
-
}
|
|
154
|
-
};
|
|
155
|
-
|
|
156
|
-
if (loading) {
|
|
157
|
-
return (
|
|
158
|
-
<div className="flex h-full items-center justify-center">
|
|
159
|
-
<Loader2 className="text-muted-foreground h-6 w-6 animate-spin" />
|
|
160
|
-
</div>
|
|
161
|
-
);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
if (workers.length === 0) {
|
|
165
|
-
return (
|
|
166
|
-
<div className="text-muted-foreground flex h-full flex-col items-center justify-center">
|
|
167
|
-
<Users className="mb-4 h-12 w-12 opacity-50" />
|
|
168
|
-
<p className="text-lg font-medium">No workers yet</p>
|
|
169
|
-
<p className="text-sm">
|
|
170
|
-
This conductor hasn't spawned any workers.
|
|
171
|
-
</p>
|
|
172
|
-
<p className="mt-4 max-w-md text-center text-xs">
|
|
173
|
-
Use the MCP tools or API to spawn workers. The conductor can delegate
|
|
174
|
-
tasks to parallel worker sessions.
|
|
175
|
-
</p>
|
|
176
|
-
</div>
|
|
177
|
-
);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
return (
|
|
181
|
-
<div className="flex h-full flex-col">
|
|
182
|
-
{/* Header with summary */}
|
|
183
|
-
<div className="flex items-center justify-between border-b p-4">
|
|
184
|
-
<div className="flex items-center gap-4">
|
|
185
|
-
<div className="flex items-center gap-2">
|
|
186
|
-
<Users className="text-primary h-5 w-5" />
|
|
187
|
-
<span className="font-semibold">Workers</span>
|
|
188
|
-
<span className="text-muted-foreground">
|
|
189
|
-
({summary?.total || workers.length})
|
|
190
|
-
</span>
|
|
191
|
-
</div>
|
|
192
|
-
|
|
193
|
-
{summary && (
|
|
194
|
-
<div className="flex items-center gap-3 text-sm">
|
|
195
|
-
{summary.running > 0 && (
|
|
196
|
-
<div className="flex items-center gap-1 text-green-500">
|
|
197
|
-
<Loader2 className="h-3 w-3 animate-spin" />
|
|
198
|
-
<span>{summary.running} running</span>
|
|
199
|
-
</div>
|
|
200
|
-
)}
|
|
201
|
-
{summary.waiting > 0 && (
|
|
202
|
-
<div className="flex items-center gap-1 text-yellow-500">
|
|
203
|
-
<AlertCircle className="h-3 w-3" />
|
|
204
|
-
<span>{summary.waiting} waiting</span>
|
|
205
|
-
</div>
|
|
206
|
-
)}
|
|
207
|
-
{summary.completed > 0 && (
|
|
208
|
-
<div className="flex items-center gap-1 text-green-500">
|
|
209
|
-
<CheckCircle className="h-3 w-3" />
|
|
210
|
-
<span>{summary.completed} done</span>
|
|
211
|
-
</div>
|
|
212
|
-
)}
|
|
213
|
-
{summary.failed > 0 && (
|
|
214
|
-
<div className="flex items-center gap-1 text-red-500">
|
|
215
|
-
<XCircle className="h-3 w-3" />
|
|
216
|
-
<span>{summary.failed} failed</span>
|
|
217
|
-
</div>
|
|
218
|
-
)}
|
|
219
|
-
</div>
|
|
220
|
-
)}
|
|
221
|
-
</div>
|
|
222
|
-
|
|
223
|
-
<Button
|
|
224
|
-
variant="ghost"
|
|
225
|
-
size="icon-sm"
|
|
226
|
-
onClick={refresh}
|
|
227
|
-
disabled={refreshing}
|
|
228
|
-
>
|
|
229
|
-
<RefreshCw className={cn("h-4 w-4", refreshing && "animate-spin")} />
|
|
230
|
-
</Button>
|
|
231
|
-
</div>
|
|
232
|
-
|
|
233
|
-
{/* Workers grid */}
|
|
234
|
-
<div className="flex-1 overflow-auto p-4">
|
|
235
|
-
<div className="grid grid-cols-1 gap-3 lg:grid-cols-2">
|
|
236
|
-
{workers.map((worker) => (
|
|
237
|
-
<WorkerCard
|
|
238
|
-
key={worker.id}
|
|
239
|
-
worker={worker}
|
|
240
|
-
isExpanded={expandedWorkers.has(worker.id)}
|
|
241
|
-
output={workerOutputs[worker.id]}
|
|
242
|
-
onToggleExpand={() => toggleExpand(worker.id)}
|
|
243
|
-
onViewOutput={() => {
|
|
244
|
-
toggleExpand(worker.id);
|
|
245
|
-
fetchWorkerOutput(worker.id);
|
|
246
|
-
}}
|
|
247
|
-
onSendMessage={(msg) => handleSendMessage(worker.id, msg)}
|
|
248
|
-
onKill={() => handleKillWorker(worker.id)}
|
|
249
|
-
onAttach={
|
|
250
|
-
onAttachToWorker ? () => onAttachToWorker(worker.id) : undefined
|
|
251
|
-
}
|
|
252
|
-
/>
|
|
253
|
-
))}
|
|
254
|
-
</div>
|
|
255
|
-
</div>
|
|
256
|
-
</div>
|
|
257
|
-
);
|
|
258
|
-
}
|