@kolisachint/hoocode-agent 0.4.52 → 0.4.53
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/CHANGELOG.md +21 -0
- package/dist/cli/args.d.ts +1 -1
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +3 -1
- package/dist/cli/args.js.map +1 -1
- package/dist/core/keybindings.d.ts +5 -0
- package/dist/core/keybindings.d.ts.map +1 -1
- package/dist/core/keybindings.js +8 -0
- package/dist/core/keybindings.js.map +1 -1
- package/dist/core/team-auto.d.ts +46 -0
- package/dist/core/team-auto.d.ts.map +1 -0
- package/dist/core/team-auto.js +163 -0
- package/dist/core/team-auto.js.map +1 -0
- package/dist/core/team-view.d.ts +29 -2
- package/dist/core/team-view.d.ts.map +1 -1
- package/dist/core/team-view.js +39 -3
- package/dist/core/team-view.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +28 -7
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/task-panel.d.ts +14 -2
- package/dist/modes/interactive/components/task-panel.d.ts.map +1 -1
- package/dist/modes/interactive/components/task-panel.js +69 -5
- package/dist/modes/interactive/components/task-panel.js.map +1 -1
- package/dist/modes/interactive/components/team-attach-panel.d.ts +60 -0
- package/dist/modes/interactive/components/team-attach-panel.d.ts.map +1 -0
- package/dist/modes/interactive/components/team-attach-panel.js +222 -0
- package/dist/modes/interactive/components/team-attach-panel.js.map +1 -0
- package/dist/modes/interactive/interactive-mode.d.ts +24 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +106 -0
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package.json +1 -1
- package/package.json +4 -4
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"team-auto.d.ts","sourceRoot":"","sources":["../../src/core/team-auto.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAOH,0EAA0E;AAC1E,eAAO,MAAM,sBAAsB,UAA0E,CAAC;AAE9G;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAWnE;AAED,+EAA+E;AAC/E,wBAAgB,YAAY,IAAI,OAAO,CAAC,MAAM,CAAC,CAc9C;AAmBD,mEAAmE;AACnE,wBAAgB,uBAAuB,CACtC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAClC;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,EAAE,CAAA;CAAE,GAAG,SAAS,CAIvD;AAED,MAAM,WAAW,QAAQ;IACxB,+CAA+C;IAC/C,GAAG,EAAE,MAAM,CAAC;IACZ,0EAA0E;IAC1E,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACtB;AAED,MAAM,WAAW,eAAe;IAC/B,2DAA2D;IAC3D,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,sDAAsD;IACtD,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;CACxB;AAkCD;;;;GAIG;AACH,wBAAsB,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,eAAoB,GAAG,OAAO,CAAC,QAAQ,CAAC,CAoDjG","sourcesContent":["/**\n * `--team auto`: discover a team config, spawn a local hooteams server as a\n * child process, and hand back its URL so the rest of the pipeline behaves\n * exactly as if `--team http://localhost:<port>` had been passed.\n *\n * hooteams is intentionally not bundled — the launcher is resolved from PATH\n * (`hooteams`, falling back to `bunx hooteams`) and missing pieces fail with\n * a clear, actionable error. The child is reaped on hoocode exit, clean or\n * signalled, via a process \"exit\" hook (the interactive shutdown path calls\n * process.exit directly, so an async cleanup would never run).\n */\n\nimport { type ChildProcess, spawn } from \"node:child_process\";\nimport { accessSync, constants, existsSync } from \"node:fs\";\nimport { createServer } from \"node:net\";\nimport path from \"node:path\";\n\n/** Config locations probed at each directory level, in priority order. */\nexport const TEAM_CONFIG_CANDIDATES = [path.join(\".agents\", \"teams\", \"default.json\"), \"hooteams.config.json\"];\n\n/**\n * Walk up from startDir to the filesystem root, returning the first config\n * found. Both candidates are probed per level (.agents/teams/default.json\n * wins over hooteams.config.json in the same directory).\n */\nexport function findTeamConfig(startDir: string): string | undefined {\n\tlet dir = path.resolve(startDir);\n\twhile (true) {\n\t\tfor (const candidate of TEAM_CONFIG_CANDIDATES) {\n\t\t\tconst candidatePath = path.join(dir, candidate);\n\t\t\tif (existsSync(candidatePath)) return candidatePath;\n\t\t}\n\t\tconst parent = path.dirname(dir);\n\t\tif (parent === dir) return undefined;\n\t\tdir = parent;\n\t}\n}\n\n/** Ask the OS for a free port by binding port 0 and reading the assignment. */\nexport function findFreePort(): Promise<number> {\n\treturn new Promise((resolve, reject) => {\n\t\tconst server = createServer();\n\t\tserver.once(\"error\", reject);\n\t\tserver.listen(0, \"127.0.0.1\", () => {\n\t\t\tconst address = server.address();\n\t\t\tif (address === null || typeof address === \"string\") {\n\t\t\t\tserver.close(() => reject(new Error(\"could not determine a free port\")));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst { port } = address;\n\t\t\tserver.close(() => resolve(port));\n\t\t});\n\t});\n}\n\nfunction isExecutableOnPath(name: string, env: NodeJS.ProcessEnv): boolean {\n\tconst pathVar = env.PATH ?? \"\";\n\tconst extensions = process.platform === \"win32\" ? (env.PATHEXT ?? \".EXE;.CMD;.BAT;.COM\").split(\";\") : [\"\"];\n\tfor (const dir of pathVar.split(path.delimiter)) {\n\t\tif (!dir) continue;\n\t\tfor (const extension of extensions) {\n\t\t\ttry {\n\t\t\t\taccessSync(path.join(dir, name + extension.toLowerCase()), constants.X_OK);\n\t\t\t\treturn true;\n\t\t\t} catch {\n\t\t\t\t// keep probing\n\t\t\t}\n\t\t}\n\t}\n\treturn false;\n}\n\n/** How to launch hooteams: directly from PATH, or through bunx. */\nexport function resolveHooteamsLauncher(\n\tenv: NodeJS.ProcessEnv = process.env,\n): { command: string; prefixArgs: string[] } | undefined {\n\tif (isExecutableOnPath(\"hooteams\", env)) return { command: \"hooteams\", prefixArgs: [] };\n\tif (isExecutableOnPath(\"bunx\", env)) return { command: \"bunx\", prefixArgs: [\"hooteams\"] };\n\treturn undefined;\n}\n\nexport interface AutoTeam {\n\t/** Base URL of the spawned hooteams server. */\n\turl: string;\n\t/** Graceful shutdown: POST /stop, then kill the child's process group. */\n\tstop(): Promise<void>;\n}\n\nexport interface AutoTeamOptions {\n\t/** Startup progress sink (pre-TUI, so console is fine). */\n\tlog?: (message: string) => void;\n\t/** How long to wait for GET /health (default 15s). */\n\thealthTimeoutMs?: number;\n\tenv?: NodeJS.ProcessEnv;\n}\n\nfunction killChild(child: ChildProcess): void {\n\tif (child.pid === undefined || child.exitCode !== null || child.signalCode !== null) return;\n\ttry {\n\t\t// POSIX: the child leads its own process group (detached), so a negative\n\t\t// pid reaches hooteams even when launched through a bunx wrapper.\n\t\tif (process.platform !== \"win32\") process.kill(-child.pid, \"SIGTERM\");\n\t\telse child.kill();\n\t} catch {\n\t\t// Already gone.\n\t}\n}\n\nasync function waitForHealth(url: string, child: ChildProcess, timeoutMs: number): Promise<void> {\n\tconst deadline = Date.now() + timeoutMs;\n\twhile (Date.now() < deadline) {\n\t\tif (child.exitCode !== null || child.signalCode !== null) {\n\t\t\tthrow new Error(`--team auto: hooteams exited (code ${child.exitCode ?? \"signal\"}) before becoming healthy`);\n\t\t}\n\t\ttry {\n\t\t\tconst response = await fetch(`${url}/health`, { signal: AbortSignal.timeout(1000) });\n\t\t\tif (response.ok) {\n\t\t\t\tconst body = (await response.json()) as { ok?: boolean };\n\t\t\t\tif (body.ok === true) return;\n\t\t\t}\n\t\t} catch {\n\t\t\t// Not up yet; keep polling.\n\t\t}\n\t\tawait new Promise((resolve) => setTimeout(resolve, 150));\n\t}\n\tthrow new Error(`--team auto: hooteams did not report healthy at ${url}/health within ${timeoutMs}ms`);\n}\n\n/**\n * Resolve the config, spawn hooteams on a free port, and wait for /health.\n * Throws (with a message ready for the terminal) when no config is found, no\n * launcher resolves, or the server never becomes healthy.\n */\nexport async function startAutoTeam(cwd: string, options: AutoTeamOptions = {}): Promise<AutoTeam> {\n\tconst env = options.env ?? process.env;\n\tconst config = findTeamConfig(cwd);\n\tif (!config) {\n\t\tthrow new Error(\n\t\t\t`--team auto: no team config found. Looked for ${TEAM_CONFIG_CANDIDATES.join(\" or \")} in ${cwd} and every parent directory.`,\n\t\t);\n\t}\n\tconst launcher = resolveHooteamsLauncher(env);\n\tif (!launcher) {\n\t\tthrow new Error(\n\t\t\t\"--team auto: hooteams is not on PATH and bunx is unavailable. Install hooteams (or bun) or pass --team <url> to use a running server.\",\n\t\t);\n\t}\n\n\tconst port = await findFreePort();\n\tconst url = `http://localhost:${port}`;\n\toptions.log?.(`Starting hooteams (config ${config}) on port ${port}…`);\n\n\tconst child = spawn(\n\t\tlauncher.command,\n\t\t[...launcher.prefixArgs, \"start\", \"--config\", config, \"--port\", String(port)],\n\t\t{\n\t\t\tstdio: \"ignore\",\n\t\t\tenv,\n\t\t\tdetached: process.platform !== \"win32\",\n\t\t},\n\t);\n\tchild.unref();\n\tconst reapOnExit = () => killChild(child);\n\tprocess.on(\"exit\", reapOnExit);\n\n\ttry {\n\t\tawait waitForHealth(url, child, options.healthTimeoutMs ?? 15000);\n\t} catch (error) {\n\t\tprocess.off(\"exit\", reapOnExit);\n\t\tkillChild(child);\n\t\tthrow error;\n\t}\n\n\treturn {\n\t\turl,\n\t\tasync stop() {\n\t\t\tprocess.off(\"exit\", reapOnExit);\n\t\t\ttry {\n\t\t\t\tawait fetch(`${url}/stop`, { method: \"POST\", signal: AbortSignal.timeout(2000) });\n\t\t\t} catch {\n\t\t\t\t// Graceful stop is best-effort; the kill below is the guarantee.\n\t\t\t}\n\t\t\tkillChild(child);\n\t\t},\n\t};\n}\n"]}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `--team auto`: discover a team config, spawn a local hooteams server as a
|
|
3
|
+
* child process, and hand back its URL so the rest of the pipeline behaves
|
|
4
|
+
* exactly as if `--team http://localhost:<port>` had been passed.
|
|
5
|
+
*
|
|
6
|
+
* hooteams is intentionally not bundled — the launcher is resolved from PATH
|
|
7
|
+
* (`hooteams`, falling back to `bunx hooteams`) and missing pieces fail with
|
|
8
|
+
* a clear, actionable error. The child is reaped on hoocode exit, clean or
|
|
9
|
+
* signalled, via a process "exit" hook (the interactive shutdown path calls
|
|
10
|
+
* process.exit directly, so an async cleanup would never run).
|
|
11
|
+
*/
|
|
12
|
+
import { spawn } from "node:child_process";
|
|
13
|
+
import { accessSync, constants, existsSync } from "node:fs";
|
|
14
|
+
import { createServer } from "node:net";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
/** Config locations probed at each directory level, in priority order. */
|
|
17
|
+
export const TEAM_CONFIG_CANDIDATES = [path.join(".agents", "teams", "default.json"), "hooteams.config.json"];
|
|
18
|
+
/**
|
|
19
|
+
* Walk up from startDir to the filesystem root, returning the first config
|
|
20
|
+
* found. Both candidates are probed per level (.agents/teams/default.json
|
|
21
|
+
* wins over hooteams.config.json in the same directory).
|
|
22
|
+
*/
|
|
23
|
+
export function findTeamConfig(startDir) {
|
|
24
|
+
let dir = path.resolve(startDir);
|
|
25
|
+
while (true) {
|
|
26
|
+
for (const candidate of TEAM_CONFIG_CANDIDATES) {
|
|
27
|
+
const candidatePath = path.join(dir, candidate);
|
|
28
|
+
if (existsSync(candidatePath))
|
|
29
|
+
return candidatePath;
|
|
30
|
+
}
|
|
31
|
+
const parent = path.dirname(dir);
|
|
32
|
+
if (parent === dir)
|
|
33
|
+
return undefined;
|
|
34
|
+
dir = parent;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** Ask the OS for a free port by binding port 0 and reading the assignment. */
|
|
38
|
+
export function findFreePort() {
|
|
39
|
+
return new Promise((resolve, reject) => {
|
|
40
|
+
const server = createServer();
|
|
41
|
+
server.once("error", reject);
|
|
42
|
+
server.listen(0, "127.0.0.1", () => {
|
|
43
|
+
const address = server.address();
|
|
44
|
+
if (address === null || typeof address === "string") {
|
|
45
|
+
server.close(() => reject(new Error("could not determine a free port")));
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const { port } = address;
|
|
49
|
+
server.close(() => resolve(port));
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
function isExecutableOnPath(name, env) {
|
|
54
|
+
const pathVar = env.PATH ?? "";
|
|
55
|
+
const extensions = process.platform === "win32" ? (env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM").split(";") : [""];
|
|
56
|
+
for (const dir of pathVar.split(path.delimiter)) {
|
|
57
|
+
if (!dir)
|
|
58
|
+
continue;
|
|
59
|
+
for (const extension of extensions) {
|
|
60
|
+
try {
|
|
61
|
+
accessSync(path.join(dir, name + extension.toLowerCase()), constants.X_OK);
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
// keep probing
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
/** How to launch hooteams: directly from PATH, or through bunx. */
|
|
72
|
+
export function resolveHooteamsLauncher(env = process.env) {
|
|
73
|
+
if (isExecutableOnPath("hooteams", env))
|
|
74
|
+
return { command: "hooteams", prefixArgs: [] };
|
|
75
|
+
if (isExecutableOnPath("bunx", env))
|
|
76
|
+
return { command: "bunx", prefixArgs: ["hooteams"] };
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
function killChild(child) {
|
|
80
|
+
if (child.pid === undefined || child.exitCode !== null || child.signalCode !== null)
|
|
81
|
+
return;
|
|
82
|
+
try {
|
|
83
|
+
// POSIX: the child leads its own process group (detached), so a negative
|
|
84
|
+
// pid reaches hooteams even when launched through a bunx wrapper.
|
|
85
|
+
if (process.platform !== "win32")
|
|
86
|
+
process.kill(-child.pid, "SIGTERM");
|
|
87
|
+
else
|
|
88
|
+
child.kill();
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
// Already gone.
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
async function waitForHealth(url, child, timeoutMs) {
|
|
95
|
+
const deadline = Date.now() + timeoutMs;
|
|
96
|
+
while (Date.now() < deadline) {
|
|
97
|
+
if (child.exitCode !== null || child.signalCode !== null) {
|
|
98
|
+
throw new Error(`--team auto: hooteams exited (code ${child.exitCode ?? "signal"}) before becoming healthy`);
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
const response = await fetch(`${url}/health`, { signal: AbortSignal.timeout(1000) });
|
|
102
|
+
if (response.ok) {
|
|
103
|
+
const body = (await response.json());
|
|
104
|
+
if (body.ok === true)
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
// Not up yet; keep polling.
|
|
110
|
+
}
|
|
111
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
112
|
+
}
|
|
113
|
+
throw new Error(`--team auto: hooteams did not report healthy at ${url}/health within ${timeoutMs}ms`);
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Resolve the config, spawn hooteams on a free port, and wait for /health.
|
|
117
|
+
* Throws (with a message ready for the terminal) when no config is found, no
|
|
118
|
+
* launcher resolves, or the server never becomes healthy.
|
|
119
|
+
*/
|
|
120
|
+
export async function startAutoTeam(cwd, options = {}) {
|
|
121
|
+
const env = options.env ?? process.env;
|
|
122
|
+
const config = findTeamConfig(cwd);
|
|
123
|
+
if (!config) {
|
|
124
|
+
throw new Error(`--team auto: no team config found. Looked for ${TEAM_CONFIG_CANDIDATES.join(" or ")} in ${cwd} and every parent directory.`);
|
|
125
|
+
}
|
|
126
|
+
const launcher = resolveHooteamsLauncher(env);
|
|
127
|
+
if (!launcher) {
|
|
128
|
+
throw new Error("--team auto: hooteams is not on PATH and bunx is unavailable. Install hooteams (or bun) or pass --team <url> to use a running server.");
|
|
129
|
+
}
|
|
130
|
+
const port = await findFreePort();
|
|
131
|
+
const url = `http://localhost:${port}`;
|
|
132
|
+
options.log?.(`Starting hooteams (config ${config}) on port ${port}…`);
|
|
133
|
+
const child = spawn(launcher.command, [...launcher.prefixArgs, "start", "--config", config, "--port", String(port)], {
|
|
134
|
+
stdio: "ignore",
|
|
135
|
+
env,
|
|
136
|
+
detached: process.platform !== "win32",
|
|
137
|
+
});
|
|
138
|
+
child.unref();
|
|
139
|
+
const reapOnExit = () => killChild(child);
|
|
140
|
+
process.on("exit", reapOnExit);
|
|
141
|
+
try {
|
|
142
|
+
await waitForHealth(url, child, options.healthTimeoutMs ?? 15000);
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
process.off("exit", reapOnExit);
|
|
146
|
+
killChild(child);
|
|
147
|
+
throw error;
|
|
148
|
+
}
|
|
149
|
+
return {
|
|
150
|
+
url,
|
|
151
|
+
async stop() {
|
|
152
|
+
process.off("exit", reapOnExit);
|
|
153
|
+
try {
|
|
154
|
+
await fetch(`${url}/stop`, { method: "POST", signal: AbortSignal.timeout(2000) });
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
// Graceful stop is best-effort; the kill below is the guarantee.
|
|
158
|
+
}
|
|
159
|
+
killChild(child);
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
//# sourceMappingURL=team-auto.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"team-auto.js","sourceRoot":"","sources":["../../src/core/team-auto.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `--team auto`: discover a team config, spawn a local hooteams server as a\n * child process, and hand back its URL so the rest of the pipeline behaves\n * exactly as if `--team http://localhost:<port>` had been passed.\n *\n * hooteams is intentionally not bundled — the launcher is resolved from PATH\n * (`hooteams`, falling back to `bunx hooteams`) and missing pieces fail with\n * a clear, actionable error. The child is reaped on hoocode exit, clean or\n * signalled, via a process \"exit\" hook (the interactive shutdown path calls\n * process.exit directly, so an async cleanup would never run).\n */\n\nimport { type ChildProcess, spawn } from \"node:child_process\";\nimport { accessSync, constants, existsSync } from \"node:fs\";\nimport { createServer } from \"node:net\";\nimport path from \"node:path\";\n\n/** Config locations probed at each directory level, in priority order. */\nexport const TEAM_CONFIG_CANDIDATES = [path.join(\".agents\", \"teams\", \"default.json\"), \"hooteams.config.json\"];\n\n/**\n * Walk up from startDir to the filesystem root, returning the first config\n * found. Both candidates are probed per level (.agents/teams/default.json\n * wins over hooteams.config.json in the same directory).\n */\nexport function findTeamConfig(startDir: string): string | undefined {\n\tlet dir = path.resolve(startDir);\n\twhile (true) {\n\t\tfor (const candidate of TEAM_CONFIG_CANDIDATES) {\n\t\t\tconst candidatePath = path.join(dir, candidate);\n\t\t\tif (existsSync(candidatePath)) return candidatePath;\n\t\t}\n\t\tconst parent = path.dirname(dir);\n\t\tif (parent === dir) return undefined;\n\t\tdir = parent;\n\t}\n}\n\n/** Ask the OS for a free port by binding port 0 and reading the assignment. */\nexport function findFreePort(): Promise<number> {\n\treturn new Promise((resolve, reject) => {\n\t\tconst server = createServer();\n\t\tserver.once(\"error\", reject);\n\t\tserver.listen(0, \"127.0.0.1\", () => {\n\t\t\tconst address = server.address();\n\t\t\tif (address === null || typeof address === \"string\") {\n\t\t\t\tserver.close(() => reject(new Error(\"could not determine a free port\")));\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst { port } = address;\n\t\t\tserver.close(() => resolve(port));\n\t\t});\n\t});\n}\n\nfunction isExecutableOnPath(name: string, env: NodeJS.ProcessEnv): boolean {\n\tconst pathVar = env.PATH ?? \"\";\n\tconst extensions = process.platform === \"win32\" ? (env.PATHEXT ?? \".EXE;.CMD;.BAT;.COM\").split(\";\") : [\"\"];\n\tfor (const dir of pathVar.split(path.delimiter)) {\n\t\tif (!dir) continue;\n\t\tfor (const extension of extensions) {\n\t\t\ttry {\n\t\t\t\taccessSync(path.join(dir, name + extension.toLowerCase()), constants.X_OK);\n\t\t\t\treturn true;\n\t\t\t} catch {\n\t\t\t\t// keep probing\n\t\t\t}\n\t\t}\n\t}\n\treturn false;\n}\n\n/** How to launch hooteams: directly from PATH, or through bunx. */\nexport function resolveHooteamsLauncher(\n\tenv: NodeJS.ProcessEnv = process.env,\n): { command: string; prefixArgs: string[] } | undefined {\n\tif (isExecutableOnPath(\"hooteams\", env)) return { command: \"hooteams\", prefixArgs: [] };\n\tif (isExecutableOnPath(\"bunx\", env)) return { command: \"bunx\", prefixArgs: [\"hooteams\"] };\n\treturn undefined;\n}\n\nexport interface AutoTeam {\n\t/** Base URL of the spawned hooteams server. */\n\turl: string;\n\t/** Graceful shutdown: POST /stop, then kill the child's process group. */\n\tstop(): Promise<void>;\n}\n\nexport interface AutoTeamOptions {\n\t/** Startup progress sink (pre-TUI, so console is fine). */\n\tlog?: (message: string) => void;\n\t/** How long to wait for GET /health (default 15s). */\n\thealthTimeoutMs?: number;\n\tenv?: NodeJS.ProcessEnv;\n}\n\nfunction killChild(child: ChildProcess): void {\n\tif (child.pid === undefined || child.exitCode !== null || child.signalCode !== null) return;\n\ttry {\n\t\t// POSIX: the child leads its own process group (detached), so a negative\n\t\t// pid reaches hooteams even when launched through a bunx wrapper.\n\t\tif (process.platform !== \"win32\") process.kill(-child.pid, \"SIGTERM\");\n\t\telse child.kill();\n\t} catch {\n\t\t// Already gone.\n\t}\n}\n\nasync function waitForHealth(url: string, child: ChildProcess, timeoutMs: number): Promise<void> {\n\tconst deadline = Date.now() + timeoutMs;\n\twhile (Date.now() < deadline) {\n\t\tif (child.exitCode !== null || child.signalCode !== null) {\n\t\t\tthrow new Error(`--team auto: hooteams exited (code ${child.exitCode ?? \"signal\"}) before becoming healthy`);\n\t\t}\n\t\ttry {\n\t\t\tconst response = await fetch(`${url}/health`, { signal: AbortSignal.timeout(1000) });\n\t\t\tif (response.ok) {\n\t\t\t\tconst body = (await response.json()) as { ok?: boolean };\n\t\t\t\tif (body.ok === true) return;\n\t\t\t}\n\t\t} catch {\n\t\t\t// Not up yet; keep polling.\n\t\t}\n\t\tawait new Promise((resolve) => setTimeout(resolve, 150));\n\t}\n\tthrow new Error(`--team auto: hooteams did not report healthy at ${url}/health within ${timeoutMs}ms`);\n}\n\n/**\n * Resolve the config, spawn hooteams on a free port, and wait for /health.\n * Throws (with a message ready for the terminal) when no config is found, no\n * launcher resolves, or the server never becomes healthy.\n */\nexport async function startAutoTeam(cwd: string, options: AutoTeamOptions = {}): Promise<AutoTeam> {\n\tconst env = options.env ?? process.env;\n\tconst config = findTeamConfig(cwd);\n\tif (!config) {\n\t\tthrow new Error(\n\t\t\t`--team auto: no team config found. Looked for ${TEAM_CONFIG_CANDIDATES.join(\" or \")} in ${cwd} and every parent directory.`,\n\t\t);\n\t}\n\tconst launcher = resolveHooteamsLauncher(env);\n\tif (!launcher) {\n\t\tthrow new Error(\n\t\t\t\"--team auto: hooteams is not on PATH and bunx is unavailable. Install hooteams (or bun) or pass --team <url> to use a running server.\",\n\t\t);\n\t}\n\n\tconst port = await findFreePort();\n\tconst url = `http://localhost:${port}`;\n\toptions.log?.(`Starting hooteams (config ${config}) on port ${port}…`);\n\n\tconst child = spawn(\n\t\tlauncher.command,\n\t\t[...launcher.prefixArgs, \"start\", \"--config\", config, \"--port\", String(port)],\n\t\t{\n\t\t\tstdio: \"ignore\",\n\t\t\tenv,\n\t\t\tdetached: process.platform !== \"win32\",\n\t\t},\n\t);\n\tchild.unref();\n\tconst reapOnExit = () => killChild(child);\n\tprocess.on(\"exit\", reapOnExit);\n\n\ttry {\n\t\tawait waitForHealth(url, child, options.healthTimeoutMs ?? 15000);\n\t} catch (error) {\n\t\tprocess.off(\"exit\", reapOnExit);\n\t\tkillChild(child);\n\t\tthrow error;\n\t}\n\n\treturn {\n\t\turl,\n\t\tasync stop() {\n\t\t\tprocess.off(\"exit\", reapOnExit);\n\t\t\ttry {\n\t\t\t\tawait fetch(`${url}/stop`, { method: \"POST\", signal: AbortSignal.timeout(2000) });\n\t\t\t} catch {\n\t\t\t\t// Graceful stop is best-effort; the kill below is the guarantee.\n\t\t\t}\n\t\t\tkillChild(child);\n\t\t},\n\t};\n}\n"]}
|
package/dist/core/team-view.d.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* hooteams team client (`--team <url>`).
|
|
3
3
|
*
|
|
4
4
|
* Connects to a running hooteams server, registers every role as a
|
|
5
5
|
* kind="role" agent in the task store, and maps the server's TeamEvent SSE
|
|
6
6
|
* stream onto task-store patches so the task panel's existing "teams" view
|
|
7
|
-
* shows live role state.
|
|
7
|
+
* shows live role state. On top of that mirror the connection exposes
|
|
8
|
+
* steering (POST /steer) and an event subscription used by the attach
|
|
9
|
+
* side-panel — both share the single /events stream; no second SSE
|
|
10
|
+
* connection is ever opened.
|
|
8
11
|
*
|
|
9
12
|
* The connection is best-effort by design — a connect failure or a later
|
|
10
13
|
* drop logs a warning and never blocks (or crashes) the main agent. At most
|
|
@@ -23,9 +26,23 @@ export interface TeamViewEvent {
|
|
|
23
26
|
agentId?: string;
|
|
24
27
|
ts?: number;
|
|
25
28
|
toolName?: string;
|
|
29
|
+
args?: unknown;
|
|
30
|
+
isError?: boolean;
|
|
31
|
+
/** Streaming assistant-message delta carried by message_update events. */
|
|
32
|
+
assistantMessageEvent?: {
|
|
33
|
+
type?: string;
|
|
34
|
+
delta?: string;
|
|
35
|
+
};
|
|
26
36
|
message?: {
|
|
27
37
|
role?: string;
|
|
28
38
|
errorMessage?: string;
|
|
39
|
+
usage?: {
|
|
40
|
+
input?: number;
|
|
41
|
+
output?: number;
|
|
42
|
+
cost?: {
|
|
43
|
+
total?: number;
|
|
44
|
+
};
|
|
45
|
+
};
|
|
29
46
|
};
|
|
30
47
|
}
|
|
31
48
|
/**
|
|
@@ -67,6 +84,16 @@ export interface TeamViewOptions {
|
|
|
67
84
|
export interface TeamViewConnection {
|
|
68
85
|
/** Close the SSE connection and stop reconnecting. */
|
|
69
86
|
stop(): void;
|
|
87
|
+
/** POST /steer { role, message }. Rejects on network or HTTP error. */
|
|
88
|
+
steer(role: string, message: string): Promise<void>;
|
|
89
|
+
/**
|
|
90
|
+
* Subscribe to every TeamEvent delivered by the shared /events stream.
|
|
91
|
+
* Returns an unsubscribe function. Listeners receive events for all roles;
|
|
92
|
+
* per-role filtering is the subscriber's job (the attach panel filters).
|
|
93
|
+
*/
|
|
94
|
+
subscribe(listener: (event: TeamViewEvent) => void): () => void;
|
|
95
|
+
/** Number of live event subscribers. Exposed for leak tests. */
|
|
96
|
+
subscriberCount(): number;
|
|
70
97
|
}
|
|
71
98
|
/**
|
|
72
99
|
* Start the read-only team view against a hooteams server base URL.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"team-view.d.ts","sourceRoot":"","sources":["../../src/core/team-view.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAwC,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAElF,uEAAuE;AACvE,MAAM,MAAM,kBAAkB,GAAG,MAAM,CAAC,MAAM,EAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,aAAa,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAE7F,+EAA+E;AAC/E,MAAM,WAAW,aAAa;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CACnD;AAyCD;;;;;;;;;;GAUG;AACH,qBAAa,cAAc;IAC1B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAmB;IACzC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA6B;IAErD,YAAY,KAAK,GAAE,OAAO,SAAqB,EAE9C;IAED,kDAAkD;IAClD,WAAW,CAAC,QAAQ,EAAE,kBAAkB,GAAG,IAAI,CAM9C;IAED,yDAAyD;IACzD,UAAU,CAAC,KAAK,EAAE,aAAa,GAAG,IAAI,CA4CrC;IAED,OAAO,CAAC,OAAO;IAIf;;;;OAIG;IACH,OAAO,CAAC,UAAU;IAYlB,OAAO,CAAC,SAAS;CAMjB;AAED,MAAM,WAAW,eAAe;IAC/B,+CAA+C;IAC/C,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,gCAAgC;IAChC,KAAK,CAAC,EAAE,OAAO,SAAS,CAAC;IACzB,6DAA6D;IAC7D,YAAY,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,kBAAkB;IAClC,sDAAsD;IACtD,IAAI,IAAI,IAAI,CAAC;CACb;AAID;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,eAAoB,GAAG,kBAAkB,CA4E9F","sourcesContent":["/**\n * Read-only hooteams team view (`--team <url>`).\n *\n * Connects to a running hooteams server, registers every role as a\n * kind=\"role\" agent in the task store, and maps the server's TeamEvent SSE\n * stream onto task-store patches so the task panel's existing \"teams\" view\n * shows live role state. Strictly observational: no steering, no attach.\n *\n * The connection is best-effort by design — a connect failure or a later\n * drop logs a warning and never blocks (or crashes) the main agent. At most\n * one SSE connection (to /events) is open at any time.\n */\n\nimport { type TaskAgentState, type TaskStatus, taskStore } from \"./task-store.js\";\n\n/** Shape of GET /status: coarse per-role status keyed by role name. */\nexport type TeamStatusSnapshot = Record<string, { status?: string; lastEventType?: string }>;\n\n/** One frame of GET /events: a hoocode AgentEvent tagged with its producer. */\nexport interface TeamViewEvent {\n\ttype: string;\n\trole: string;\n\tagentId?: string;\n\tts?: number;\n\ttoolName?: string;\n\tmessage?: { role?: string; errorMessage?: string };\n}\n\n/** hooteams AgentStatus word → task panel agent state. */\nfunction stateFromStatus(status: string | undefined): TaskAgentState {\n\tswitch (status) {\n\t\tcase \"idle\":\n\t\t\treturn \"idle\";\n\t\tcase \"thinking\":\n\t\tcase \"streaming\":\n\t\t\treturn \"active\";\n\t\tcase \"tool\":\n\t\t\treturn \"running\";\n\t\tcase \"done\":\n\t\t\treturn \"done\";\n\t\tcase \"error\":\n\t\t\treturn \"failed\";\n\t\tdefault:\n\t\t\treturn \"idle\";\n\t}\n}\n\nfunction taskStatusFromState(state: TaskAgentState): TaskStatus {\n\tswitch (state) {\n\t\tcase \"done\":\n\t\t\treturn \"done\";\n\t\tcase \"failed\":\n\t\t\treturn \"failed\";\n\t\tcase \"idle\":\n\t\t\t// Idle is settled, not queued: a \"pending\" task here would survive every\n\t\t\t// taskStore.reset() and pin the pane at \"working\" for the whole session.\n\t\t\treturn \"done\";\n\t\tdefault:\n\t\t\treturn \"in_progress\";\n\t}\n}\n\n/** Only these states represent activity worth a task row of its own. */\nfunction stateWarrantsTask(state: TaskAgentState): boolean {\n\treturn state === \"active\" || state === \"running\" || state === \"failed\";\n}\n\n/**\n * Maps team status snapshots and TeamEvents onto task-store patches.\n *\n * Each role owns one roster entry (id `team:<role>`) and at most one task whose\n * title tracks the role's latest activity. Tasks exist only while a role is\n * actually doing something (active/running, or failed so the error is visible);\n * idle roles keep their roster entry but no task — the panel's teams lens\n * renders them as placeholder groups, so a quiet team reads as an idle roster\n * instead of pinning the pane at \"working\". Entries are re-created on demand\n * because taskStore.reset() wipes finished tasks between user turns.\n */\nexport class TeamViewMapper {\n\tprivate readonly store: typeof taskStore;\n\tprivate readonly taskIds = new Map<string, number>();\n\n\tconstructor(store: typeof taskStore = taskStore) {\n\t\tthis.store = store;\n\t}\n\n\t/** Register roles from a GET /status snapshot. */\n\tapplyStatus(snapshot: TeamStatusSnapshot): void {\n\t\tfor (const [role, info] of Object.entries(snapshot)) {\n\t\t\tconst state = stateFromStatus(info?.status);\n\t\t\tthis.ensureRole(role, state, info?.lastEventType ?? \"connected\");\n\t\t\tthis.patchRole(role, state);\n\t\t}\n\t}\n\n\t/** Map one TeamEvent from GET /events onto the store. */\n\tapplyEvent(event: TeamViewEvent): void {\n\t\tif (!event || typeof event.role !== \"string\" || event.role.length === 0) return;\n\t\tconst role = event.role;\n\t\tswitch (event.type) {\n\t\t\tcase \"agent_start\":\n\t\t\tcase \"turn_start\":\n\t\t\t\tthis.ensureRole(role, \"active\", \"thinking\");\n\t\t\t\tthis.patchRole(role, \"active\", \"thinking\");\n\t\t\t\tbreak;\n\t\t\tcase \"message_start\":\n\t\t\tcase \"message_update\":\n\t\t\tcase \"message_end\":\n\t\t\t\tthis.ensureRole(role, \"active\", \"responding\");\n\t\t\t\tthis.patchRole(role, \"active\");\n\t\t\t\tbreak;\n\t\t\tcase \"tool_execution_start\":\n\t\t\t\tthis.ensureRole(role, \"running\", `tool: ${event.toolName ?? \"?\"}`);\n\t\t\t\tthis.patchRole(role, \"running\", `tool: ${event.toolName ?? \"?\"}`);\n\t\t\t\tbreak;\n\t\t\tcase \"tool_execution_end\":\n\t\t\t\tthis.ensureRole(role, \"active\", \"thinking\");\n\t\t\t\tthis.patchRole(role, \"active\", \"thinking\");\n\t\t\t\tbreak;\n\t\t\tcase \"turn_end\":\n\t\t\t\tif (event.message?.role === \"assistant\" && event.message.errorMessage) {\n\t\t\t\t\tthis.ensureRole(role, \"failed\", \"error\");\n\t\t\t\t\tthis.patchRole(role, \"failed\", \"error\");\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\tcase \"agent_end\": {\n\t\t\t\t// A failed run stays failed; agent_end only marks clean completions\n\t\t\t\t// (mirrors hooteams' own status tracking).\n\t\t\t\tconst failed = this.store.agents().find((a) => a.id === this.agentId(role))?.state === \"failed\";\n\t\t\t\tif (!failed) {\n\t\t\t\t\tthis.ensureRole(role, \"done\", \"idle\");\n\t\t\t\t\tthis.patchRole(role, \"done\", \"idle\");\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tdefault:\n\t\t\t\t// Unknown event types still prove the role exists.\n\t\t\t\tthis.ensureRole(role, \"idle\", event.type);\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\tprivate agentId(role: string): string {\n\t\treturn `team:${role}`;\n\t}\n\n\t/**\n\t * Make sure the role's roster entry exists, plus its task when the state\n\t * warrants one (reset() may have dropped both). Idle/done states never\n\t * create a task — only patch one that live activity already opened.\n\t */\n\tprivate ensureRole(role: string, state: TaskAgentState, title: string): void {\n\t\tconst id = this.agentId(role);\n\t\tthis.store.upsertAgent({ id, name: role, kind: \"role\", state });\n\t\tconst taskId = this.taskIds.get(role);\n\t\tconst existing = taskId !== undefined ? this.store.list().find((task) => task.id === taskId) : undefined;\n\t\tif (!existing && stateWarrantsTask(state)) {\n\t\t\tconst task = this.store.create(title, { agent: id });\n\t\t\tthis.store.update(task.id, { status: taskStatusFromState(state) });\n\t\t\tthis.taskIds.set(role, task.id);\n\t\t}\n\t}\n\n\tprivate patchRole(role: string, state: TaskAgentState, title?: string): void {\n\t\tthis.store.patchAgent(this.agentId(role), { state });\n\t\tconst taskId = this.taskIds.get(role);\n\t\tif (taskId === undefined) return;\n\t\tthis.store.update(taskId, { status: taskStatusFromState(state), ...(title !== undefined ? { title } : {}) });\n\t}\n}\n\nexport interface TeamViewOptions {\n\t/** Warning sink; defaults to console.error. */\n\twarn?: (message: string) => void;\n\t/** Store override for tests. */\n\tstore?: typeof taskStore;\n\t/** Delay between reconnect attempts in ms (default 5000). */\n\tretryDelayMs?: number;\n}\n\nexport interface TeamViewConnection {\n\t/** Close the SSE connection and stop reconnecting. */\n\tstop(): void;\n}\n\nconst STATUS_TIMEOUT_MS = 5000;\n\n/**\n * Start the read-only team view against a hooteams server base URL.\n *\n * Returns immediately; all network work happens in the background and any\n * failure is reported through `warn` without ever throwing.\n */\nexport function connectTeamView(url: string, options: TeamViewOptions = {}): TeamViewConnection {\n\tconst base = url.replace(/\\/+$/, \"\");\n\tconst warn = options.warn ?? ((message: string) => console.error(message));\n\tconst retryDelayMs = options.retryDelayMs ?? 5000;\n\tconst mapper = new TeamViewMapper(options.store);\n\tconst controller = new AbortController();\n\tlet stopped = false;\n\n\tconst run = async (): Promise<void> => {\n\t\t// 1. Status snapshot: register the current roles.\n\t\ttry {\n\t\t\tconst response = await fetch(`${base}/status`, {\n\t\t\t\tsignal: AbortSignal.any([controller.signal, AbortSignal.timeout(STATUS_TIMEOUT_MS)]),\n\t\t\t});\n\t\t\tif (!response.ok) throw new Error(`HTTP ${response.status}`);\n\t\t\tmapper.applyStatus((await response.json()) as TeamStatusSnapshot);\n\t\t} catch (error) {\n\t\t\tif (stopped) return;\n\t\t\twarn(`team view: failed to fetch ${base}/status (${String(error)}); continuing without the team view`);\n\t\t}\n\n\t\t// 2. Single SSE subscription, reconnecting on drops.\n\t\tlet announcedDrop = false;\n\t\twhile (!stopped) {\n\t\t\ttry {\n\t\t\t\tconst response = await fetch(`${base}/events`, { signal: controller.signal });\n\t\t\t\tif (!response.ok || !response.body) throw new Error(`HTTP ${response.status}`);\n\t\t\t\tconst reader = response.body.getReader();\n\t\t\t\tconst decoder = new TextDecoder();\n\t\t\t\tlet buffer = \"\";\n\t\t\t\twhile (true) {\n\t\t\t\t\tconst { value, done } = await reader.read();\n\t\t\t\t\tif (done) break;\n\t\t\t\t\t// Only a stream that actually delivers data counts as recovered. A 200\n\t\t\t\t\t// that closes immediately (e.g. a server that answers /events without\n\t\t\t\t\t// streaming) used to re-arm the warning and repeat it every retry.\n\t\t\t\t\tannouncedDrop = false;\n\t\t\t\t\tbuffer += decoder.decode(value, { stream: true });\n\t\t\t\t\tlet index = buffer.indexOf(\"\\n\\n\");\n\t\t\t\t\twhile (index !== -1) {\n\t\t\t\t\t\tconst frame = buffer.slice(0, index);\n\t\t\t\t\t\tbuffer = buffer.slice(index + 2);\n\t\t\t\t\t\tfor (const line of frame.split(\"\\n\")) {\n\t\t\t\t\t\t\tif (!line.startsWith(\"data:\")) continue;\n\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\tmapper.applyEvent(JSON.parse(line.slice(5).trim()) as TeamViewEvent);\n\t\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\t\t// Malformed frames are dropped; the stream stays up.\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tindex = buffer.indexOf(\"\\n\\n\");\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (stopped) return;\n\t\t\t\tthrow new Error(\"stream ended\");\n\t\t\t} catch (error) {\n\t\t\t\tif (stopped || controller.signal.aborted) return;\n\t\t\t\tif (!announcedDrop) {\n\t\t\t\t\tannouncedDrop = true;\n\t\t\t\t\twarn(`team view: lost connection to ${base}/events (${String(error)}); retrying in background`);\n\t\t\t\t}\n\t\t\t\tawait new Promise((resolve) => setTimeout(resolve, retryDelayMs));\n\t\t\t}\n\t\t}\n\t};\n\n\tvoid run().catch((error) => {\n\t\tif (!stopped) warn(`team view: unexpected error (${String(error)})`);\n\t});\n\n\treturn {\n\t\tstop() {\n\t\t\tstopped = true;\n\t\t\tcontroller.abort();\n\t\t},\n\t};\n}\n"]}
|
|
1
|
+
{"version":3,"file":"team-view.d.ts","sourceRoot":"","sources":["../../src/core/team-view.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAwC,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAElF,uEAAuE;AACvE,MAAM,MAAM,kBAAkB,GAAG,MAAM,CAAC,MAAM,EAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,aAAa,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAE7F,+EAA+E;AAC/E,MAAM,WAAW,aAAa;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,0EAA0E;IAC1E,qBAAqB,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC1D,OAAO,CAAC,EAAE;QACT,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,KAAK,CAAC,EAAE;YAAE,KAAK,CAAC,EAAE,MAAM,CAAC;YAAC,MAAM,CAAC,EAAE,MAAM,CAAC;YAAC,IAAI,CAAC,EAAE;gBAAE,KAAK,CAAC,EAAE,MAAM,CAAA;aAAE,CAAA;SAAE,CAAC;KACvE,CAAC;CACF;AAyCD;;;;;;;;;;GAUG;AACH,qBAAa,cAAc;IAC1B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAmB;IACzC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA6B;IAErD,YAAY,KAAK,GAAE,OAAO,SAAqB,EAE9C;IAED,kDAAkD;IAClD,WAAW,CAAC,QAAQ,EAAE,kBAAkB,GAAG,IAAI,CAM9C;IAED,yDAAyD;IACzD,UAAU,CAAC,KAAK,EAAE,aAAa,GAAG,IAAI,CA4CrC;IAED,OAAO,CAAC,OAAO;IAIf;;;;OAIG;IACH,OAAO,CAAC,UAAU;IAYlB,OAAO,CAAC,SAAS;CAMjB;AAED,MAAM,WAAW,eAAe;IAC/B,+CAA+C;IAC/C,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,gCAAgC;IAChC,KAAK,CAAC,EAAE,OAAO,SAAS,CAAC;IACzB,6DAA6D;IAC7D,YAAY,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,kBAAkB;IAClC,sDAAsD;IACtD,IAAI,IAAI,IAAI,CAAC;IACb,uEAAuE;IACvE,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpD;;;;OAIG;IACH,SAAS,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;IAChE,gEAAgE;IAChE,eAAe,IAAI,MAAM,CAAC;CAC1B;AAKD;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,eAAoB,GAAG,kBAAkB,CA2G9F","sourcesContent":["/**\n * hooteams team client (`--team <url>`).\n *\n * Connects to a running hooteams server, registers every role as a\n * kind=\"role\" agent in the task store, and maps the server's TeamEvent SSE\n * stream onto task-store patches so the task panel's existing \"teams\" view\n * shows live role state. On top of that mirror the connection exposes\n * steering (POST /steer) and an event subscription used by the attach\n * side-panel — both share the single /events stream; no second SSE\n * connection is ever opened.\n *\n * The connection is best-effort by design — a connect failure or a later\n * drop logs a warning and never blocks (or crashes) the main agent. At most\n * one SSE connection (to /events) is open at any time.\n */\n\nimport { type TaskAgentState, type TaskStatus, taskStore } from \"./task-store.js\";\n\n/** Shape of GET /status: coarse per-role status keyed by role name. */\nexport type TeamStatusSnapshot = Record<string, { status?: string; lastEventType?: string }>;\n\n/** One frame of GET /events: a hoocode AgentEvent tagged with its producer. */\nexport interface TeamViewEvent {\n\ttype: string;\n\trole: string;\n\tagentId?: string;\n\tts?: number;\n\ttoolName?: string;\n\targs?: unknown;\n\tisError?: boolean;\n\t/** Streaming assistant-message delta carried by message_update events. */\n\tassistantMessageEvent?: { type?: string; delta?: string };\n\tmessage?: {\n\t\trole?: string;\n\t\terrorMessage?: string;\n\t\tusage?: { input?: number; output?: number; cost?: { total?: number } };\n\t};\n}\n\n/** hooteams AgentStatus word → task panel agent state. */\nfunction stateFromStatus(status: string | undefined): TaskAgentState {\n\tswitch (status) {\n\t\tcase \"idle\":\n\t\t\treturn \"idle\";\n\t\tcase \"thinking\":\n\t\tcase \"streaming\":\n\t\t\treturn \"active\";\n\t\tcase \"tool\":\n\t\t\treturn \"running\";\n\t\tcase \"done\":\n\t\t\treturn \"done\";\n\t\tcase \"error\":\n\t\t\treturn \"failed\";\n\t\tdefault:\n\t\t\treturn \"idle\";\n\t}\n}\n\nfunction taskStatusFromState(state: TaskAgentState): TaskStatus {\n\tswitch (state) {\n\t\tcase \"done\":\n\t\t\treturn \"done\";\n\t\tcase \"failed\":\n\t\t\treturn \"failed\";\n\t\tcase \"idle\":\n\t\t\t// Idle is settled, not queued: a \"pending\" task here would survive every\n\t\t\t// taskStore.reset() and pin the pane at \"working\" for the whole session.\n\t\t\treturn \"done\";\n\t\tdefault:\n\t\t\treturn \"in_progress\";\n\t}\n}\n\n/** Only these states represent activity worth a task row of its own. */\nfunction stateWarrantsTask(state: TaskAgentState): boolean {\n\treturn state === \"active\" || state === \"running\" || state === \"failed\";\n}\n\n/**\n * Maps team status snapshots and TeamEvents onto task-store patches.\n *\n * Each role owns one roster entry (id `team:<role>`) and at most one task whose\n * title tracks the role's latest activity. Tasks exist only while a role is\n * actually doing something (active/running, or failed so the error is visible);\n * idle roles keep their roster entry but no task — the panel's teams lens\n * renders them as placeholder groups, so a quiet team reads as an idle roster\n * instead of pinning the pane at \"working\". Entries are re-created on demand\n * because taskStore.reset() wipes finished tasks between user turns.\n */\nexport class TeamViewMapper {\n\tprivate readonly store: typeof taskStore;\n\tprivate readonly taskIds = new Map<string, number>();\n\n\tconstructor(store: typeof taskStore = taskStore) {\n\t\tthis.store = store;\n\t}\n\n\t/** Register roles from a GET /status snapshot. */\n\tapplyStatus(snapshot: TeamStatusSnapshot): void {\n\t\tfor (const [role, info] of Object.entries(snapshot)) {\n\t\t\tconst state = stateFromStatus(info?.status);\n\t\t\tthis.ensureRole(role, state, info?.lastEventType ?? \"connected\");\n\t\t\tthis.patchRole(role, state);\n\t\t}\n\t}\n\n\t/** Map one TeamEvent from GET /events onto the store. */\n\tapplyEvent(event: TeamViewEvent): void {\n\t\tif (!event || typeof event.role !== \"string\" || event.role.length === 0) return;\n\t\tconst role = event.role;\n\t\tswitch (event.type) {\n\t\t\tcase \"agent_start\":\n\t\t\tcase \"turn_start\":\n\t\t\t\tthis.ensureRole(role, \"active\", \"thinking\");\n\t\t\t\tthis.patchRole(role, \"active\", \"thinking\");\n\t\t\t\tbreak;\n\t\t\tcase \"message_start\":\n\t\t\tcase \"message_update\":\n\t\t\tcase \"message_end\":\n\t\t\t\tthis.ensureRole(role, \"active\", \"responding\");\n\t\t\t\tthis.patchRole(role, \"active\");\n\t\t\t\tbreak;\n\t\t\tcase \"tool_execution_start\":\n\t\t\t\tthis.ensureRole(role, \"running\", `tool: ${event.toolName ?? \"?\"}`);\n\t\t\t\tthis.patchRole(role, \"running\", `tool: ${event.toolName ?? \"?\"}`);\n\t\t\t\tbreak;\n\t\t\tcase \"tool_execution_end\":\n\t\t\t\tthis.ensureRole(role, \"active\", \"thinking\");\n\t\t\t\tthis.patchRole(role, \"active\", \"thinking\");\n\t\t\t\tbreak;\n\t\t\tcase \"turn_end\":\n\t\t\t\tif (event.message?.role === \"assistant\" && event.message.errorMessage) {\n\t\t\t\t\tthis.ensureRole(role, \"failed\", \"error\");\n\t\t\t\t\tthis.patchRole(role, \"failed\", \"error\");\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\tcase \"agent_end\": {\n\t\t\t\t// A failed run stays failed; agent_end only marks clean completions\n\t\t\t\t// (mirrors hooteams' own status tracking).\n\t\t\t\tconst failed = this.store.agents().find((a) => a.id === this.agentId(role))?.state === \"failed\";\n\t\t\t\tif (!failed) {\n\t\t\t\t\tthis.ensureRole(role, \"done\", \"idle\");\n\t\t\t\t\tthis.patchRole(role, \"done\", \"idle\");\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tdefault:\n\t\t\t\t// Unknown event types still prove the role exists.\n\t\t\t\tthis.ensureRole(role, \"idle\", event.type);\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\tprivate agentId(role: string): string {\n\t\treturn `team:${role}`;\n\t}\n\n\t/**\n\t * Make sure the role's roster entry exists, plus its task when the state\n\t * warrants one (reset() may have dropped both). Idle/done states never\n\t * create a task — only patch one that live activity already opened.\n\t */\n\tprivate ensureRole(role: string, state: TaskAgentState, title: string): void {\n\t\tconst id = this.agentId(role);\n\t\tthis.store.upsertAgent({ id, name: role, kind: \"role\", state });\n\t\tconst taskId = this.taskIds.get(role);\n\t\tconst existing = taskId !== undefined ? this.store.list().find((task) => task.id === taskId) : undefined;\n\t\tif (!existing && stateWarrantsTask(state)) {\n\t\t\tconst task = this.store.create(title, { agent: id });\n\t\t\tthis.store.update(task.id, { status: taskStatusFromState(state) });\n\t\t\tthis.taskIds.set(role, task.id);\n\t\t}\n\t}\n\n\tprivate patchRole(role: string, state: TaskAgentState, title?: string): void {\n\t\tthis.store.patchAgent(this.agentId(role), { state });\n\t\tconst taskId = this.taskIds.get(role);\n\t\tif (taskId === undefined) return;\n\t\tthis.store.update(taskId, { status: taskStatusFromState(state), ...(title !== undefined ? { title } : {}) });\n\t}\n}\n\nexport interface TeamViewOptions {\n\t/** Warning sink; defaults to console.error. */\n\twarn?: (message: string) => void;\n\t/** Store override for tests. */\n\tstore?: typeof taskStore;\n\t/** Delay between reconnect attempts in ms (default 5000). */\n\tretryDelayMs?: number;\n}\n\nexport interface TeamViewConnection {\n\t/** Close the SSE connection and stop reconnecting. */\n\tstop(): void;\n\t/** POST /steer { role, message }. Rejects on network or HTTP error. */\n\tsteer(role: string, message: string): Promise<void>;\n\t/**\n\t * Subscribe to every TeamEvent delivered by the shared /events stream.\n\t * Returns an unsubscribe function. Listeners receive events for all roles;\n\t * per-role filtering is the subscriber's job (the attach panel filters).\n\t */\n\tsubscribe(listener: (event: TeamViewEvent) => void): () => void;\n\t/** Number of live event subscribers. Exposed for leak tests. */\n\tsubscriberCount(): number;\n}\n\nconst STATUS_TIMEOUT_MS = 5000;\nconst STEER_TIMEOUT_MS = 5000;\n\n/**\n * Start the read-only team view against a hooteams server base URL.\n *\n * Returns immediately; all network work happens in the background and any\n * failure is reported through `warn` without ever throwing.\n */\nexport function connectTeamView(url: string, options: TeamViewOptions = {}): TeamViewConnection {\n\tconst base = url.replace(/\\/+$/, \"\");\n\tconst warn = options.warn ?? ((message: string) => console.error(message));\n\tconst retryDelayMs = options.retryDelayMs ?? 5000;\n\tconst mapper = new TeamViewMapper(options.store);\n\tconst controller = new AbortController();\n\tconst listeners = new Set<(event: TeamViewEvent) => void>();\n\tlet stopped = false;\n\n\tconst deliver = (event: TeamViewEvent): void => {\n\t\tmapper.applyEvent(event);\n\t\tfor (const listener of listeners) {\n\t\t\ttry {\n\t\t\t\tlistener(event);\n\t\t\t} catch {\n\t\t\t\t// A broken subscriber must not take down the stream or its peers.\n\t\t\t}\n\t\t}\n\t};\n\n\tconst run = async (): Promise<void> => {\n\t\t// 1. Status snapshot: register the current roles.\n\t\ttry {\n\t\t\tconst response = await fetch(`${base}/status`, {\n\t\t\t\tsignal: AbortSignal.any([controller.signal, AbortSignal.timeout(STATUS_TIMEOUT_MS)]),\n\t\t\t});\n\t\t\tif (!response.ok) throw new Error(`HTTP ${response.status}`);\n\t\t\tmapper.applyStatus((await response.json()) as TeamStatusSnapshot);\n\t\t} catch (error) {\n\t\t\tif (stopped) return;\n\t\t\twarn(`team view: failed to fetch ${base}/status (${String(error)}); continuing without the team view`);\n\t\t}\n\n\t\t// 2. Single SSE subscription, reconnecting on drops.\n\t\tlet announcedDrop = false;\n\t\twhile (!stopped) {\n\t\t\ttry {\n\t\t\t\tconst response = await fetch(`${base}/events`, { signal: controller.signal });\n\t\t\t\tif (!response.ok || !response.body) throw new Error(`HTTP ${response.status}`);\n\t\t\t\tconst reader = response.body.getReader();\n\t\t\t\tconst decoder = new TextDecoder();\n\t\t\t\tlet buffer = \"\";\n\t\t\t\twhile (true) {\n\t\t\t\t\tconst { value, done } = await reader.read();\n\t\t\t\t\tif (done) break;\n\t\t\t\t\t// Only a stream that actually delivers data counts as recovered. A 200\n\t\t\t\t\t// that closes immediately (e.g. a server that answers /events without\n\t\t\t\t\t// streaming) used to re-arm the warning and repeat it every retry.\n\t\t\t\t\tannouncedDrop = false;\n\t\t\t\t\tbuffer += decoder.decode(value, { stream: true });\n\t\t\t\t\tlet index = buffer.indexOf(\"\\n\\n\");\n\t\t\t\t\twhile (index !== -1) {\n\t\t\t\t\t\tconst frame = buffer.slice(0, index);\n\t\t\t\t\t\tbuffer = buffer.slice(index + 2);\n\t\t\t\t\t\tfor (const line of frame.split(\"\\n\")) {\n\t\t\t\t\t\t\tif (!line.startsWith(\"data:\")) continue;\n\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\tdeliver(JSON.parse(line.slice(5).trim()) as TeamViewEvent);\n\t\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\t\t// Malformed frames are dropped; the stream stays up.\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tindex = buffer.indexOf(\"\\n\\n\");\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (stopped) return;\n\t\t\t\tthrow new Error(\"stream ended\");\n\t\t\t} catch (error) {\n\t\t\t\tif (stopped || controller.signal.aborted) return;\n\t\t\t\tif (!announcedDrop) {\n\t\t\t\t\tannouncedDrop = true;\n\t\t\t\t\twarn(`team view: lost connection to ${base}/events (${String(error)}); retrying in background`);\n\t\t\t\t}\n\t\t\t\tawait new Promise((resolve) => setTimeout(resolve, retryDelayMs));\n\t\t\t}\n\t\t}\n\t};\n\n\tvoid run().catch((error) => {\n\t\tif (!stopped) warn(`team view: unexpected error (${String(error)})`);\n\t});\n\n\treturn {\n\t\tstop() {\n\t\t\tstopped = true;\n\t\t\tlisteners.clear();\n\t\t\tcontroller.abort();\n\t\t},\n\t\tasync steer(role: string, message: string): Promise<void> {\n\t\t\tconst response = await fetch(`${base}/steer`, {\n\t\t\t\tmethod: \"POST\",\n\t\t\t\theaders: { \"content-type\": \"application/json\" },\n\t\t\t\tbody: JSON.stringify({ role, message }),\n\t\t\t\tsignal: AbortSignal.any([controller.signal, AbortSignal.timeout(STEER_TIMEOUT_MS)]),\n\t\t\t});\n\t\t\tif (!response.ok) throw new Error(`HTTP ${response.status}`);\n\t\t},\n\t\tsubscribe(listener: (event: TeamViewEvent) => void): () => void {\n\t\t\tlisteners.add(listener);\n\t\t\treturn () => {\n\t\t\t\tlisteners.delete(listener);\n\t\t\t};\n\t\t},\n\t\tsubscriberCount(): number {\n\t\t\treturn listeners.size;\n\t\t},\n\t};\n}\n"]}
|
package/dist/core/team-view.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* hooteams team client (`--team <url>`).
|
|
3
3
|
*
|
|
4
4
|
* Connects to a running hooteams server, registers every role as a
|
|
5
5
|
* kind="role" agent in the task store, and maps the server's TeamEvent SSE
|
|
6
6
|
* stream onto task-store patches so the task panel's existing "teams" view
|
|
7
|
-
* shows live role state.
|
|
7
|
+
* shows live role state. On top of that mirror the connection exposes
|
|
8
|
+
* steering (POST /steer) and an event subscription used by the attach
|
|
9
|
+
* side-panel — both share the single /events stream; no second SSE
|
|
10
|
+
* connection is ever opened.
|
|
8
11
|
*
|
|
9
12
|
* The connection is best-effort by design — a connect failure or a later
|
|
10
13
|
* drop logs a warning and never blocks (or crashes) the main agent. At most
|
|
@@ -147,6 +150,7 @@ export class TeamViewMapper {
|
|
|
147
150
|
}
|
|
148
151
|
}
|
|
149
152
|
const STATUS_TIMEOUT_MS = 5000;
|
|
153
|
+
const STEER_TIMEOUT_MS = 5000;
|
|
150
154
|
/**
|
|
151
155
|
* Start the read-only team view against a hooteams server base URL.
|
|
152
156
|
*
|
|
@@ -159,7 +163,19 @@ export function connectTeamView(url, options = {}) {
|
|
|
159
163
|
const retryDelayMs = options.retryDelayMs ?? 5000;
|
|
160
164
|
const mapper = new TeamViewMapper(options.store);
|
|
161
165
|
const controller = new AbortController();
|
|
166
|
+
const listeners = new Set();
|
|
162
167
|
let stopped = false;
|
|
168
|
+
const deliver = (event) => {
|
|
169
|
+
mapper.applyEvent(event);
|
|
170
|
+
for (const listener of listeners) {
|
|
171
|
+
try {
|
|
172
|
+
listener(event);
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
// A broken subscriber must not take down the stream or its peers.
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
};
|
|
163
179
|
const run = async () => {
|
|
164
180
|
// 1. Status snapshot: register the current roles.
|
|
165
181
|
try {
|
|
@@ -202,7 +218,7 @@ export function connectTeamView(url, options = {}) {
|
|
|
202
218
|
if (!line.startsWith("data:"))
|
|
203
219
|
continue;
|
|
204
220
|
try {
|
|
205
|
-
|
|
221
|
+
deliver(JSON.parse(line.slice(5).trim()));
|
|
206
222
|
}
|
|
207
223
|
catch {
|
|
208
224
|
// Malformed frames are dropped; the stream stays up.
|
|
@@ -233,8 +249,28 @@ export function connectTeamView(url, options = {}) {
|
|
|
233
249
|
return {
|
|
234
250
|
stop() {
|
|
235
251
|
stopped = true;
|
|
252
|
+
listeners.clear();
|
|
236
253
|
controller.abort();
|
|
237
254
|
},
|
|
255
|
+
async steer(role, message) {
|
|
256
|
+
const response = await fetch(`${base}/steer`, {
|
|
257
|
+
method: "POST",
|
|
258
|
+
headers: { "content-type": "application/json" },
|
|
259
|
+
body: JSON.stringify({ role, message }),
|
|
260
|
+
signal: AbortSignal.any([controller.signal, AbortSignal.timeout(STEER_TIMEOUT_MS)]),
|
|
261
|
+
});
|
|
262
|
+
if (!response.ok)
|
|
263
|
+
throw new Error(`HTTP ${response.status}`);
|
|
264
|
+
},
|
|
265
|
+
subscribe(listener) {
|
|
266
|
+
listeners.add(listener);
|
|
267
|
+
return () => {
|
|
268
|
+
listeners.delete(listener);
|
|
269
|
+
};
|
|
270
|
+
},
|
|
271
|
+
subscriberCount() {
|
|
272
|
+
return listeners.size;
|
|
273
|
+
},
|
|
238
274
|
};
|
|
239
275
|
}
|
|
240
276
|
//# sourceMappingURL=team-view.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"team-view.js","sourceRoot":"","sources":["../../src/core/team-view.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Read-only hooteams team view (`--team <url>`).\n *\n * Connects to a running hooteams server, registers every role as a\n * kind=\"role\" agent in the task store, and maps the server's TeamEvent SSE\n * stream onto task-store patches so the task panel's existing \"teams\" view\n * shows live role state. Strictly observational: no steering, no attach.\n *\n * The connection is best-effort by design — a connect failure or a later\n * drop logs a warning and never blocks (or crashes) the main agent. At most\n * one SSE connection (to /events) is open at any time.\n */\n\nimport { type TaskAgentState, type TaskStatus, taskStore } from \"./task-store.js\";\n\n/** Shape of GET /status: coarse per-role status keyed by role name. */\nexport type TeamStatusSnapshot = Record<string, { status?: string; lastEventType?: string }>;\n\n/** One frame of GET /events: a hoocode AgentEvent tagged with its producer. */\nexport interface TeamViewEvent {\n\ttype: string;\n\trole: string;\n\tagentId?: string;\n\tts?: number;\n\ttoolName?: string;\n\tmessage?: { role?: string; errorMessage?: string };\n}\n\n/** hooteams AgentStatus word → task panel agent state. */\nfunction stateFromStatus(status: string | undefined): TaskAgentState {\n\tswitch (status) {\n\t\tcase \"idle\":\n\t\t\treturn \"idle\";\n\t\tcase \"thinking\":\n\t\tcase \"streaming\":\n\t\t\treturn \"active\";\n\t\tcase \"tool\":\n\t\t\treturn \"running\";\n\t\tcase \"done\":\n\t\t\treturn \"done\";\n\t\tcase \"error\":\n\t\t\treturn \"failed\";\n\t\tdefault:\n\t\t\treturn \"idle\";\n\t}\n}\n\nfunction taskStatusFromState(state: TaskAgentState): TaskStatus {\n\tswitch (state) {\n\t\tcase \"done\":\n\t\t\treturn \"done\";\n\t\tcase \"failed\":\n\t\t\treturn \"failed\";\n\t\tcase \"idle\":\n\t\t\t// Idle is settled, not queued: a \"pending\" task here would survive every\n\t\t\t// taskStore.reset() and pin the pane at \"working\" for the whole session.\n\t\t\treturn \"done\";\n\t\tdefault:\n\t\t\treturn \"in_progress\";\n\t}\n}\n\n/** Only these states represent activity worth a task row of its own. */\nfunction stateWarrantsTask(state: TaskAgentState): boolean {\n\treturn state === \"active\" || state === \"running\" || state === \"failed\";\n}\n\n/**\n * Maps team status snapshots and TeamEvents onto task-store patches.\n *\n * Each role owns one roster entry (id `team:<role>`) and at most one task whose\n * title tracks the role's latest activity. Tasks exist only while a role is\n * actually doing something (active/running, or failed so the error is visible);\n * idle roles keep their roster entry but no task — the panel's teams lens\n * renders them as placeholder groups, so a quiet team reads as an idle roster\n * instead of pinning the pane at \"working\". Entries are re-created on demand\n * because taskStore.reset() wipes finished tasks between user turns.\n */\nexport class TeamViewMapper {\n\tprivate readonly store: typeof taskStore;\n\tprivate readonly taskIds = new Map<string, number>();\n\n\tconstructor(store: typeof taskStore = taskStore) {\n\t\tthis.store = store;\n\t}\n\n\t/** Register roles from a GET /status snapshot. */\n\tapplyStatus(snapshot: TeamStatusSnapshot): void {\n\t\tfor (const [role, info] of Object.entries(snapshot)) {\n\t\t\tconst state = stateFromStatus(info?.status);\n\t\t\tthis.ensureRole(role, state, info?.lastEventType ?? \"connected\");\n\t\t\tthis.patchRole(role, state);\n\t\t}\n\t}\n\n\t/** Map one TeamEvent from GET /events onto the store. */\n\tapplyEvent(event: TeamViewEvent): void {\n\t\tif (!event || typeof event.role !== \"string\" || event.role.length === 0) return;\n\t\tconst role = event.role;\n\t\tswitch (event.type) {\n\t\t\tcase \"agent_start\":\n\t\t\tcase \"turn_start\":\n\t\t\t\tthis.ensureRole(role, \"active\", \"thinking\");\n\t\t\t\tthis.patchRole(role, \"active\", \"thinking\");\n\t\t\t\tbreak;\n\t\t\tcase \"message_start\":\n\t\t\tcase \"message_update\":\n\t\t\tcase \"message_end\":\n\t\t\t\tthis.ensureRole(role, \"active\", \"responding\");\n\t\t\t\tthis.patchRole(role, \"active\");\n\t\t\t\tbreak;\n\t\t\tcase \"tool_execution_start\":\n\t\t\t\tthis.ensureRole(role, \"running\", `tool: ${event.toolName ?? \"?\"}`);\n\t\t\t\tthis.patchRole(role, \"running\", `tool: ${event.toolName ?? \"?\"}`);\n\t\t\t\tbreak;\n\t\t\tcase \"tool_execution_end\":\n\t\t\t\tthis.ensureRole(role, \"active\", \"thinking\");\n\t\t\t\tthis.patchRole(role, \"active\", \"thinking\");\n\t\t\t\tbreak;\n\t\t\tcase \"turn_end\":\n\t\t\t\tif (event.message?.role === \"assistant\" && event.message.errorMessage) {\n\t\t\t\t\tthis.ensureRole(role, \"failed\", \"error\");\n\t\t\t\t\tthis.patchRole(role, \"failed\", \"error\");\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\tcase \"agent_end\": {\n\t\t\t\t// A failed run stays failed; agent_end only marks clean completions\n\t\t\t\t// (mirrors hooteams' own status tracking).\n\t\t\t\tconst failed = this.store.agents().find((a) => a.id === this.agentId(role))?.state === \"failed\";\n\t\t\t\tif (!failed) {\n\t\t\t\t\tthis.ensureRole(role, \"done\", \"idle\");\n\t\t\t\t\tthis.patchRole(role, \"done\", \"idle\");\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tdefault:\n\t\t\t\t// Unknown event types still prove the role exists.\n\t\t\t\tthis.ensureRole(role, \"idle\", event.type);\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\tprivate agentId(role: string): string {\n\t\treturn `team:${role}`;\n\t}\n\n\t/**\n\t * Make sure the role's roster entry exists, plus its task when the state\n\t * warrants one (reset() may have dropped both). Idle/done states never\n\t * create a task — only patch one that live activity already opened.\n\t */\n\tprivate ensureRole(role: string, state: TaskAgentState, title: string): void {\n\t\tconst id = this.agentId(role);\n\t\tthis.store.upsertAgent({ id, name: role, kind: \"role\", state });\n\t\tconst taskId = this.taskIds.get(role);\n\t\tconst existing = taskId !== undefined ? this.store.list().find((task) => task.id === taskId) : undefined;\n\t\tif (!existing && stateWarrantsTask(state)) {\n\t\t\tconst task = this.store.create(title, { agent: id });\n\t\t\tthis.store.update(task.id, { status: taskStatusFromState(state) });\n\t\t\tthis.taskIds.set(role, task.id);\n\t\t}\n\t}\n\n\tprivate patchRole(role: string, state: TaskAgentState, title?: string): void {\n\t\tthis.store.patchAgent(this.agentId(role), { state });\n\t\tconst taskId = this.taskIds.get(role);\n\t\tif (taskId === undefined) return;\n\t\tthis.store.update(taskId, { status: taskStatusFromState(state), ...(title !== undefined ? { title } : {}) });\n\t}\n}\n\nexport interface TeamViewOptions {\n\t/** Warning sink; defaults to console.error. */\n\twarn?: (message: string) => void;\n\t/** Store override for tests. */\n\tstore?: typeof taskStore;\n\t/** Delay between reconnect attempts in ms (default 5000). */\n\tretryDelayMs?: number;\n}\n\nexport interface TeamViewConnection {\n\t/** Close the SSE connection and stop reconnecting. */\n\tstop(): void;\n}\n\nconst STATUS_TIMEOUT_MS = 5000;\n\n/**\n * Start the read-only team view against a hooteams server base URL.\n *\n * Returns immediately; all network work happens in the background and any\n * failure is reported through `warn` without ever throwing.\n */\nexport function connectTeamView(url: string, options: TeamViewOptions = {}): TeamViewConnection {\n\tconst base = url.replace(/\\/+$/, \"\");\n\tconst warn = options.warn ?? ((message: string) => console.error(message));\n\tconst retryDelayMs = options.retryDelayMs ?? 5000;\n\tconst mapper = new TeamViewMapper(options.store);\n\tconst controller = new AbortController();\n\tlet stopped = false;\n\n\tconst run = async (): Promise<void> => {\n\t\t// 1. Status snapshot: register the current roles.\n\t\ttry {\n\t\t\tconst response = await fetch(`${base}/status`, {\n\t\t\t\tsignal: AbortSignal.any([controller.signal, AbortSignal.timeout(STATUS_TIMEOUT_MS)]),\n\t\t\t});\n\t\t\tif (!response.ok) throw new Error(`HTTP ${response.status}`);\n\t\t\tmapper.applyStatus((await response.json()) as TeamStatusSnapshot);\n\t\t} catch (error) {\n\t\t\tif (stopped) return;\n\t\t\twarn(`team view: failed to fetch ${base}/status (${String(error)}); continuing without the team view`);\n\t\t}\n\n\t\t// 2. Single SSE subscription, reconnecting on drops.\n\t\tlet announcedDrop = false;\n\t\twhile (!stopped) {\n\t\t\ttry {\n\t\t\t\tconst response = await fetch(`${base}/events`, { signal: controller.signal });\n\t\t\t\tif (!response.ok || !response.body) throw new Error(`HTTP ${response.status}`);\n\t\t\t\tconst reader = response.body.getReader();\n\t\t\t\tconst decoder = new TextDecoder();\n\t\t\t\tlet buffer = \"\";\n\t\t\t\twhile (true) {\n\t\t\t\t\tconst { value, done } = await reader.read();\n\t\t\t\t\tif (done) break;\n\t\t\t\t\t// Only a stream that actually delivers data counts as recovered. A 200\n\t\t\t\t\t// that closes immediately (e.g. a server that answers /events without\n\t\t\t\t\t// streaming) used to re-arm the warning and repeat it every retry.\n\t\t\t\t\tannouncedDrop = false;\n\t\t\t\t\tbuffer += decoder.decode(value, { stream: true });\n\t\t\t\t\tlet index = buffer.indexOf(\"\\n\\n\");\n\t\t\t\t\twhile (index !== -1) {\n\t\t\t\t\t\tconst frame = buffer.slice(0, index);\n\t\t\t\t\t\tbuffer = buffer.slice(index + 2);\n\t\t\t\t\t\tfor (const line of frame.split(\"\\n\")) {\n\t\t\t\t\t\t\tif (!line.startsWith(\"data:\")) continue;\n\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\tmapper.applyEvent(JSON.parse(line.slice(5).trim()) as TeamViewEvent);\n\t\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\t\t// Malformed frames are dropped; the stream stays up.\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tindex = buffer.indexOf(\"\\n\\n\");\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (stopped) return;\n\t\t\t\tthrow new Error(\"stream ended\");\n\t\t\t} catch (error) {\n\t\t\t\tif (stopped || controller.signal.aborted) return;\n\t\t\t\tif (!announcedDrop) {\n\t\t\t\t\tannouncedDrop = true;\n\t\t\t\t\twarn(`team view: lost connection to ${base}/events (${String(error)}); retrying in background`);\n\t\t\t\t}\n\t\t\t\tawait new Promise((resolve) => setTimeout(resolve, retryDelayMs));\n\t\t\t}\n\t\t}\n\t};\n\n\tvoid run().catch((error) => {\n\t\tif (!stopped) warn(`team view: unexpected error (${String(error)})`);\n\t});\n\n\treturn {\n\t\tstop() {\n\t\t\tstopped = true;\n\t\t\tcontroller.abort();\n\t\t},\n\t};\n}\n"]}
|
|
1
|
+
{"version":3,"file":"team-view.js","sourceRoot":"","sources":["../../src/core/team-view.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAwC,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAuBlF,4DAA0D;AAC1D,SAAS,eAAe,CAAC,MAA0B,EAAkB;IACpE,QAAQ,MAAM,EAAE,CAAC;QAChB,KAAK,MAAM;YACV,OAAO,MAAM,CAAC;QACf,KAAK,UAAU,CAAC;QAChB,KAAK,WAAW;YACf,OAAO,QAAQ,CAAC;QACjB,KAAK,MAAM;YACV,OAAO,SAAS,CAAC;QAClB,KAAK,MAAM;YACV,OAAO,MAAM,CAAC;QACf,KAAK,OAAO;YACX,OAAO,QAAQ,CAAC;QACjB;YACC,OAAO,MAAM,CAAC;IAChB,CAAC;AAAA,CACD;AAED,SAAS,mBAAmB,CAAC,KAAqB,EAAc;IAC/D,QAAQ,KAAK,EAAE,CAAC;QACf,KAAK,MAAM;YACV,OAAO,MAAM,CAAC;QACf,KAAK,QAAQ;YACZ,OAAO,QAAQ,CAAC;QACjB,KAAK,MAAM;YACV,yEAAyE;YACzE,yEAAyE;YACzE,OAAO,MAAM,CAAC;QACf;YACC,OAAO,aAAa,CAAC;IACvB,CAAC;AAAA,CACD;AAED,wEAAwE;AACxE,SAAS,iBAAiB,CAAC,KAAqB,EAAW;IAC1D,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,QAAQ,CAAC;AAAA,CACvE;AAED;;;;;;;;;;GAUG;AACH,MAAM,OAAO,cAAc;IACT,KAAK,CAAmB;IACxB,OAAO,GAAG,IAAI,GAAG,EAAkB,CAAC;IAErD,YAAY,KAAK,GAAqB,SAAS,EAAE;QAChD,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;IAAA,CACnB;IAED,kDAAkD;IAClD,WAAW,CAAC,QAA4B,EAAQ;QAC/C,KAAK,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;YACrD,MAAM,KAAK,GAAG,eAAe,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;YAC5C,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,aAAa,IAAI,WAAW,CAAC,CAAC;YACjE,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;QAC7B,CAAC;IAAA,CACD;IAED,yDAAyD;IACzD,UAAU,CAAC,KAAoB,EAAQ;QACtC,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,CAAC,IAAI,KAAK,QAAQ,IAAI,KAAK,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO;QAChF,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC;QACxB,QAAQ,KAAK,CAAC,IAAI,EAAE,CAAC;YACpB,KAAK,aAAa,CAAC;YACnB,KAAK,YAAY;gBAChB,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,QAAQ,EAAE,UAAU,CAAC,CAAC;gBAC5C,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,QAAQ,EAAE,UAAU,CAAC,CAAC;gBAC3C,MAAM;YACP,KAAK,eAAe,CAAC;YACrB,KAAK,gBAAgB,CAAC;YACtB,KAAK,aAAa;gBACjB,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,QAAQ,EAAE,YAAY,CAAC,CAAC;gBAC9C,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;gBAC/B,MAAM;YACP,KAAK,sBAAsB;gBAC1B,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,SAAS,EAAE,SAAS,KAAK,CAAC,QAAQ,IAAI,GAAG,EAAE,CAAC,CAAC;gBACnE,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,SAAS,EAAE,SAAS,KAAK,CAAC,QAAQ,IAAI,GAAG,EAAE,CAAC,CAAC;gBAClE,MAAM;YACP,KAAK,oBAAoB;gBACxB,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,QAAQ,EAAE,UAAU,CAAC,CAAC;gBAC5C,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,QAAQ,EAAE,UAAU,CAAC,CAAC;gBAC3C,MAAM;YACP,KAAK,UAAU;gBACd,IAAI,KAAK,CAAC,OAAO,EAAE,IAAI,KAAK,WAAW,IAAI,KAAK,CAAC,OAAO,CAAC,YAAY,EAAE,CAAC;oBACvE,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAC;oBACzC,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAC;gBACzC,CAAC;gBACD,MAAM;YACP,KAAK,WAAW,EAAE,CAAC;gBAClB,oEAAoE;gBACpE,2CAA2C;gBAC3C,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,EAAE,KAAK,KAAK,QAAQ,CAAC;gBAChG,IAAI,CAAC,MAAM,EAAE,CAAC;oBACb,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;oBACtC,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;gBACtC,CAAC;gBACD,MAAM;YACP,CAAC;YACD;gBACC,mDAAmD;gBACnD,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;gBAC1C,MAAM;QACR,CAAC;IAAA,CACD;IAEO,OAAO,CAAC,IAAY,EAAU;QACrC,OAAO,QAAQ,IAAI,EAAE,CAAC;IAAA,CACtB;IAED;;;;OAIG;IACK,UAAU,CAAC,IAAY,EAAE,KAAqB,EAAE,KAAa,EAAQ;QAC5E,MAAM,EAAE,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QAC9B,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC;QAChE,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACtC,MAAM,QAAQ,GAAG,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,EAAE,KAAK,MAAM,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QACzG,IAAI,CAAC,QAAQ,IAAI,iBAAiB,CAAC,KAAK,CAAC,EAAE,CAAC;YAC3C,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,CAAC;YACrD,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,mBAAmB,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;YACnE,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,CAAC,CAAC;QACjC,CAAC;IAAA,CACD;IAEO,SAAS,CAAC,IAAY,EAAE,KAAqB,EAAE,KAAc,EAAQ;QAC5E,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC;QACrD,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACtC,IAAI,MAAM,KAAK,SAAS;YAAE,OAAO;QACjC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,EAAE,MAAM,EAAE,mBAAmB,CAAC,KAAK,CAAC,EAAE,GAAG,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;IAAA,CAC7G;CACD;AA0BD,MAAM,iBAAiB,GAAG,IAAI,CAAC;AAC/B,MAAM,gBAAgB,GAAG,IAAI,CAAC;AAE9B;;;;;GAKG;AACH,MAAM,UAAU,eAAe,CAAC,GAAW,EAAE,OAAO,GAAoB,EAAE,EAAsB;IAC/F,MAAM,IAAI,GAAG,GAAG,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IACrC,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,IAAI,CAAC,CAAC,OAAe,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC;IAC3E,MAAM,YAAY,GAAG,OAAO,CAAC,YAAY,IAAI,IAAI,CAAC;IAClD,MAAM,MAAM,GAAG,IAAI,cAAc,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;IACjD,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;IACzC,MAAM,SAAS,GAAG,IAAI,GAAG,EAAkC,CAAC;IAC5D,IAAI,OAAO,GAAG,KAAK,CAAC;IAEpB,MAAM,OAAO,GAAG,CAAC,KAAoB,EAAQ,EAAE,CAAC;QAC/C,MAAM,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;QACzB,KAAK,MAAM,QAAQ,IAAI,SAAS,EAAE,CAAC;YAClC,IAAI,CAAC;gBACJ,QAAQ,CAAC,KAAK,CAAC,CAAC;YACjB,CAAC;YAAC,MAAM,CAAC;gBACR,kEAAkE;YACnE,CAAC;QACF,CAAC;IAAA,CACD,CAAC;IAEF,MAAM,GAAG,GAAG,KAAK,IAAmB,EAAE,CAAC;QACtC,kDAAkD;QAClD,IAAI,CAAC;YACJ,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,IAAI,SAAS,EAAE;gBAC9C,MAAM,EAAE,WAAW,CAAC,GAAG,CAAC,CAAC,UAAU,CAAC,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,iBAAiB,CAAC,CAAC,CAAC;aACpF,CAAC,CAAC;YACH,IAAI,CAAC,QAAQ,CAAC,EAAE;gBAAE,MAAM,IAAI,KAAK,CAAC,QAAQ,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;YAC7D,MAAM,CAAC,WAAW,CAAC,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAuB,CAAC,CAAC;QACnE,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YAChB,IAAI,OAAO;gBAAE,OAAO;YACpB,IAAI,CAAC,8BAA8B,IAAI,YAAY,MAAM,CAAC,KAAK,CAAC,qCAAqC,CAAC,CAAC;QACxG,CAAC;QAED,qDAAqD;QACrD,IAAI,aAAa,GAAG,KAAK,CAAC;QAC1B,OAAO,CAAC,OAAO,EAAE,CAAC;YACjB,IAAI,CAAC;gBACJ,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,IAAI,SAAS,EAAE,EAAE,MAAM,EAAE,UAAU,CAAC,MAAM,EAAE,CAAC,CAAC;gBAC9E,IAAI,CAAC,QAAQ,CAAC,EAAE,IAAI,CAAC,QAAQ,CAAC,IAAI;oBAAE,MAAM,IAAI,KAAK,CAAC,QAAQ,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;gBAC/E,MAAM,MAAM,GAAG,QAAQ,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC;gBACzC,MAAM,OAAO,GAAG,IAAI,WAAW,EAAE,CAAC;gBAClC,IAAI,MAAM,GAAG,EAAE,CAAC;gBAChB,OAAO,IAAI,EAAE,CAAC;oBACb,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;oBAC5C,IAAI,IAAI;wBAAE,MAAM;oBAChB,uEAAuE;oBACvE,sEAAsE;oBACtE,mEAAmE;oBACnE,aAAa,GAAG,KAAK,CAAC;oBACtB,MAAM,IAAI,OAAO,CAAC,MAAM,CAAC,KAAK,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;oBAClD,IAAI,KAAK,GAAG,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;oBACnC,OAAO,KAAK,KAAK,CAAC,CAAC,EAAE,CAAC;wBACrB,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC;wBACrC,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC;wBACjC,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;4BACtC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC;gCAAE,SAAS;4BACxC,IAAI,CAAC;gCACJ,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAkB,CAAC,CAAC;4BAC5D,CAAC;4BAAC,MAAM,CAAC;gCACR,qDAAqD;4BACtD,CAAC;wBACF,CAAC;wBACD,KAAK,GAAG,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;oBAChC,CAAC;gBACF,CAAC;gBACD,IAAI,OAAO;oBAAE,OAAO;gBACpB,MAAM,IAAI,KAAK,CAAC,cAAc,CAAC,CAAC;YACjC,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBAChB,IAAI,OAAO,IAAI,UAAU,CAAC,MAAM,CAAC,OAAO;oBAAE,OAAO;gBACjD,IAAI,CAAC,aAAa,EAAE,CAAC;oBACpB,aAAa,GAAG,IAAI,CAAC;oBACrB,IAAI,CAAC,iCAAiC,IAAI,YAAY,MAAM,CAAC,KAAK,CAAC,2BAA2B,CAAC,CAAC;gBACjG,CAAC;gBACD,MAAM,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC,CAAC;YACnE,CAAC;QACF,CAAC;IAAA,CACD,CAAC;IAEF,KAAK,GAAG,EAAE,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC;QAC3B,IAAI,CAAC,OAAO;YAAE,IAAI,CAAC,gCAAgC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAAA,CACrE,CAAC,CAAC;IAEH,OAAO;QACN,IAAI,GAAG;YACN,OAAO,GAAG,IAAI,CAAC;YACf,SAAS,CAAC,KAAK,EAAE,CAAC;YAClB,UAAU,CAAC,KAAK,EAAE,CAAC;QAAA,CACnB;QACD,KAAK,CAAC,KAAK,CAAC,IAAY,EAAE,OAAe,EAAiB;YACzD,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,IAAI,QAAQ,EAAE;gBAC7C,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;gBAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;gBACvC,MAAM,EAAE,WAAW,CAAC,GAAG,CAAC,CAAC,UAAU,CAAC,MAAM,EAAE,WAAW,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAC,CAAC;aACnF,CAAC,CAAC;YACH,IAAI,CAAC,QAAQ,CAAC,EAAE;gBAAE,MAAM,IAAI,KAAK,CAAC,QAAQ,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;QAAA,CAC7D;QACD,SAAS,CAAC,QAAwC,EAAc;YAC/D,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;YACxB,OAAO,GAAG,EAAE,CAAC;gBACZ,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;YAAA,CAC3B,CAAC;QAAA,CACF;QACD,eAAe,GAAW;YACzB,OAAO,SAAS,CAAC,IAAI,CAAC;QAAA,CACtB;KACD,CAAC;AAAA,CACF","sourcesContent":["/**\n * hooteams team client (`--team <url>`).\n *\n * Connects to a running hooteams server, registers every role as a\n * kind=\"role\" agent in the task store, and maps the server's TeamEvent SSE\n * stream onto task-store patches so the task panel's existing \"teams\" view\n * shows live role state. On top of that mirror the connection exposes\n * steering (POST /steer) and an event subscription used by the attach\n * side-panel — both share the single /events stream; no second SSE\n * connection is ever opened.\n *\n * The connection is best-effort by design — a connect failure or a later\n * drop logs a warning and never blocks (or crashes) the main agent. At most\n * one SSE connection (to /events) is open at any time.\n */\n\nimport { type TaskAgentState, type TaskStatus, taskStore } from \"./task-store.js\";\n\n/** Shape of GET /status: coarse per-role status keyed by role name. */\nexport type TeamStatusSnapshot = Record<string, { status?: string; lastEventType?: string }>;\n\n/** One frame of GET /events: a hoocode AgentEvent tagged with its producer. */\nexport interface TeamViewEvent {\n\ttype: string;\n\trole: string;\n\tagentId?: string;\n\tts?: number;\n\ttoolName?: string;\n\targs?: unknown;\n\tisError?: boolean;\n\t/** Streaming assistant-message delta carried by message_update events. */\n\tassistantMessageEvent?: { type?: string; delta?: string };\n\tmessage?: {\n\t\trole?: string;\n\t\terrorMessage?: string;\n\t\tusage?: { input?: number; output?: number; cost?: { total?: number } };\n\t};\n}\n\n/** hooteams AgentStatus word → task panel agent state. */\nfunction stateFromStatus(status: string | undefined): TaskAgentState {\n\tswitch (status) {\n\t\tcase \"idle\":\n\t\t\treturn \"idle\";\n\t\tcase \"thinking\":\n\t\tcase \"streaming\":\n\t\t\treturn \"active\";\n\t\tcase \"tool\":\n\t\t\treturn \"running\";\n\t\tcase \"done\":\n\t\t\treturn \"done\";\n\t\tcase \"error\":\n\t\t\treturn \"failed\";\n\t\tdefault:\n\t\t\treturn \"idle\";\n\t}\n}\n\nfunction taskStatusFromState(state: TaskAgentState): TaskStatus {\n\tswitch (state) {\n\t\tcase \"done\":\n\t\t\treturn \"done\";\n\t\tcase \"failed\":\n\t\t\treturn \"failed\";\n\t\tcase \"idle\":\n\t\t\t// Idle is settled, not queued: a \"pending\" task here would survive every\n\t\t\t// taskStore.reset() and pin the pane at \"working\" for the whole session.\n\t\t\treturn \"done\";\n\t\tdefault:\n\t\t\treturn \"in_progress\";\n\t}\n}\n\n/** Only these states represent activity worth a task row of its own. */\nfunction stateWarrantsTask(state: TaskAgentState): boolean {\n\treturn state === \"active\" || state === \"running\" || state === \"failed\";\n}\n\n/**\n * Maps team status snapshots and TeamEvents onto task-store patches.\n *\n * Each role owns one roster entry (id `team:<role>`) and at most one task whose\n * title tracks the role's latest activity. Tasks exist only while a role is\n * actually doing something (active/running, or failed so the error is visible);\n * idle roles keep their roster entry but no task — the panel's teams lens\n * renders them as placeholder groups, so a quiet team reads as an idle roster\n * instead of pinning the pane at \"working\". Entries are re-created on demand\n * because taskStore.reset() wipes finished tasks between user turns.\n */\nexport class TeamViewMapper {\n\tprivate readonly store: typeof taskStore;\n\tprivate readonly taskIds = new Map<string, number>();\n\n\tconstructor(store: typeof taskStore = taskStore) {\n\t\tthis.store = store;\n\t}\n\n\t/** Register roles from a GET /status snapshot. */\n\tapplyStatus(snapshot: TeamStatusSnapshot): void {\n\t\tfor (const [role, info] of Object.entries(snapshot)) {\n\t\t\tconst state = stateFromStatus(info?.status);\n\t\t\tthis.ensureRole(role, state, info?.lastEventType ?? \"connected\");\n\t\t\tthis.patchRole(role, state);\n\t\t}\n\t}\n\n\t/** Map one TeamEvent from GET /events onto the store. */\n\tapplyEvent(event: TeamViewEvent): void {\n\t\tif (!event || typeof event.role !== \"string\" || event.role.length === 0) return;\n\t\tconst role = event.role;\n\t\tswitch (event.type) {\n\t\t\tcase \"agent_start\":\n\t\t\tcase \"turn_start\":\n\t\t\t\tthis.ensureRole(role, \"active\", \"thinking\");\n\t\t\t\tthis.patchRole(role, \"active\", \"thinking\");\n\t\t\t\tbreak;\n\t\t\tcase \"message_start\":\n\t\t\tcase \"message_update\":\n\t\t\tcase \"message_end\":\n\t\t\t\tthis.ensureRole(role, \"active\", \"responding\");\n\t\t\t\tthis.patchRole(role, \"active\");\n\t\t\t\tbreak;\n\t\t\tcase \"tool_execution_start\":\n\t\t\t\tthis.ensureRole(role, \"running\", `tool: ${event.toolName ?? \"?\"}`);\n\t\t\t\tthis.patchRole(role, \"running\", `tool: ${event.toolName ?? \"?\"}`);\n\t\t\t\tbreak;\n\t\t\tcase \"tool_execution_end\":\n\t\t\t\tthis.ensureRole(role, \"active\", \"thinking\");\n\t\t\t\tthis.patchRole(role, \"active\", \"thinking\");\n\t\t\t\tbreak;\n\t\t\tcase \"turn_end\":\n\t\t\t\tif (event.message?.role === \"assistant\" && event.message.errorMessage) {\n\t\t\t\t\tthis.ensureRole(role, \"failed\", \"error\");\n\t\t\t\t\tthis.patchRole(role, \"failed\", \"error\");\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\tcase \"agent_end\": {\n\t\t\t\t// A failed run stays failed; agent_end only marks clean completions\n\t\t\t\t// (mirrors hooteams' own status tracking).\n\t\t\t\tconst failed = this.store.agents().find((a) => a.id === this.agentId(role))?.state === \"failed\";\n\t\t\t\tif (!failed) {\n\t\t\t\t\tthis.ensureRole(role, \"done\", \"idle\");\n\t\t\t\t\tthis.patchRole(role, \"done\", \"idle\");\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tdefault:\n\t\t\t\t// Unknown event types still prove the role exists.\n\t\t\t\tthis.ensureRole(role, \"idle\", event.type);\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\tprivate agentId(role: string): string {\n\t\treturn `team:${role}`;\n\t}\n\n\t/**\n\t * Make sure the role's roster entry exists, plus its task when the state\n\t * warrants one (reset() may have dropped both). Idle/done states never\n\t * create a task — only patch one that live activity already opened.\n\t */\n\tprivate ensureRole(role: string, state: TaskAgentState, title: string): void {\n\t\tconst id = this.agentId(role);\n\t\tthis.store.upsertAgent({ id, name: role, kind: \"role\", state });\n\t\tconst taskId = this.taskIds.get(role);\n\t\tconst existing = taskId !== undefined ? this.store.list().find((task) => task.id === taskId) : undefined;\n\t\tif (!existing && stateWarrantsTask(state)) {\n\t\t\tconst task = this.store.create(title, { agent: id });\n\t\t\tthis.store.update(task.id, { status: taskStatusFromState(state) });\n\t\t\tthis.taskIds.set(role, task.id);\n\t\t}\n\t}\n\n\tprivate patchRole(role: string, state: TaskAgentState, title?: string): void {\n\t\tthis.store.patchAgent(this.agentId(role), { state });\n\t\tconst taskId = this.taskIds.get(role);\n\t\tif (taskId === undefined) return;\n\t\tthis.store.update(taskId, { status: taskStatusFromState(state), ...(title !== undefined ? { title } : {}) });\n\t}\n}\n\nexport interface TeamViewOptions {\n\t/** Warning sink; defaults to console.error. */\n\twarn?: (message: string) => void;\n\t/** Store override for tests. */\n\tstore?: typeof taskStore;\n\t/** Delay between reconnect attempts in ms (default 5000). */\n\tretryDelayMs?: number;\n}\n\nexport interface TeamViewConnection {\n\t/** Close the SSE connection and stop reconnecting. */\n\tstop(): void;\n\t/** POST /steer { role, message }. Rejects on network or HTTP error. */\n\tsteer(role: string, message: string): Promise<void>;\n\t/**\n\t * Subscribe to every TeamEvent delivered by the shared /events stream.\n\t * Returns an unsubscribe function. Listeners receive events for all roles;\n\t * per-role filtering is the subscriber's job (the attach panel filters).\n\t */\n\tsubscribe(listener: (event: TeamViewEvent) => void): () => void;\n\t/** Number of live event subscribers. Exposed for leak tests. */\n\tsubscriberCount(): number;\n}\n\nconst STATUS_TIMEOUT_MS = 5000;\nconst STEER_TIMEOUT_MS = 5000;\n\n/**\n * Start the read-only team view against a hooteams server base URL.\n *\n * Returns immediately; all network work happens in the background and any\n * failure is reported through `warn` without ever throwing.\n */\nexport function connectTeamView(url: string, options: TeamViewOptions = {}): TeamViewConnection {\n\tconst base = url.replace(/\\/+$/, \"\");\n\tconst warn = options.warn ?? ((message: string) => console.error(message));\n\tconst retryDelayMs = options.retryDelayMs ?? 5000;\n\tconst mapper = new TeamViewMapper(options.store);\n\tconst controller = new AbortController();\n\tconst listeners = new Set<(event: TeamViewEvent) => void>();\n\tlet stopped = false;\n\n\tconst deliver = (event: TeamViewEvent): void => {\n\t\tmapper.applyEvent(event);\n\t\tfor (const listener of listeners) {\n\t\t\ttry {\n\t\t\t\tlistener(event);\n\t\t\t} catch {\n\t\t\t\t// A broken subscriber must not take down the stream or its peers.\n\t\t\t}\n\t\t}\n\t};\n\n\tconst run = async (): Promise<void> => {\n\t\t// 1. Status snapshot: register the current roles.\n\t\ttry {\n\t\t\tconst response = await fetch(`${base}/status`, {\n\t\t\t\tsignal: AbortSignal.any([controller.signal, AbortSignal.timeout(STATUS_TIMEOUT_MS)]),\n\t\t\t});\n\t\t\tif (!response.ok) throw new Error(`HTTP ${response.status}`);\n\t\t\tmapper.applyStatus((await response.json()) as TeamStatusSnapshot);\n\t\t} catch (error) {\n\t\t\tif (stopped) return;\n\t\t\twarn(`team view: failed to fetch ${base}/status (${String(error)}); continuing without the team view`);\n\t\t}\n\n\t\t// 2. Single SSE subscription, reconnecting on drops.\n\t\tlet announcedDrop = false;\n\t\twhile (!stopped) {\n\t\t\ttry {\n\t\t\t\tconst response = await fetch(`${base}/events`, { signal: controller.signal });\n\t\t\t\tif (!response.ok || !response.body) throw new Error(`HTTP ${response.status}`);\n\t\t\t\tconst reader = response.body.getReader();\n\t\t\t\tconst decoder = new TextDecoder();\n\t\t\t\tlet buffer = \"\";\n\t\t\t\twhile (true) {\n\t\t\t\t\tconst { value, done } = await reader.read();\n\t\t\t\t\tif (done) break;\n\t\t\t\t\t// Only a stream that actually delivers data counts as recovered. A 200\n\t\t\t\t\t// that closes immediately (e.g. a server that answers /events without\n\t\t\t\t\t// streaming) used to re-arm the warning and repeat it every retry.\n\t\t\t\t\tannouncedDrop = false;\n\t\t\t\t\tbuffer += decoder.decode(value, { stream: true });\n\t\t\t\t\tlet index = buffer.indexOf(\"\\n\\n\");\n\t\t\t\t\twhile (index !== -1) {\n\t\t\t\t\t\tconst frame = buffer.slice(0, index);\n\t\t\t\t\t\tbuffer = buffer.slice(index + 2);\n\t\t\t\t\t\tfor (const line of frame.split(\"\\n\")) {\n\t\t\t\t\t\t\tif (!line.startsWith(\"data:\")) continue;\n\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\tdeliver(JSON.parse(line.slice(5).trim()) as TeamViewEvent);\n\t\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\t\t// Malformed frames are dropped; the stream stays up.\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tindex = buffer.indexOf(\"\\n\\n\");\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (stopped) return;\n\t\t\t\tthrow new Error(\"stream ended\");\n\t\t\t} catch (error) {\n\t\t\t\tif (stopped || controller.signal.aborted) return;\n\t\t\t\tif (!announcedDrop) {\n\t\t\t\t\tannouncedDrop = true;\n\t\t\t\t\twarn(`team view: lost connection to ${base}/events (${String(error)}); retrying in background`);\n\t\t\t\t}\n\t\t\t\tawait new Promise((resolve) => setTimeout(resolve, retryDelayMs));\n\t\t\t}\n\t\t}\n\t};\n\n\tvoid run().catch((error) => {\n\t\tif (!stopped) warn(`team view: unexpected error (${String(error)})`);\n\t});\n\n\treturn {\n\t\tstop() {\n\t\t\tstopped = true;\n\t\t\tlisteners.clear();\n\t\t\tcontroller.abort();\n\t\t},\n\t\tasync steer(role: string, message: string): Promise<void> {\n\t\t\tconst response = await fetch(`${base}/steer`, {\n\t\t\t\tmethod: \"POST\",\n\t\t\t\theaders: { \"content-type\": \"application/json\" },\n\t\t\t\tbody: JSON.stringify({ role, message }),\n\t\t\t\tsignal: AbortSignal.any([controller.signal, AbortSignal.timeout(STEER_TIMEOUT_MS)]),\n\t\t\t});\n\t\t\tif (!response.ok) throw new Error(`HTTP ${response.status}`);\n\t\t},\n\t\tsubscribe(listener: (event: TeamViewEvent) => void): () => void {\n\t\t\tlisteners.add(listener);\n\t\t\treturn () => {\n\t\t\t\tlisteners.delete(listener);\n\t\t\t};\n\t\t},\n\t\tsubscriberCount(): number {\n\t\t\treturn listeners.size;\n\t\t},\n\t};\n}\n"]}
|