@kolisachint/hoocode-agent 0.4.54 → 0.4.55
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 +14 -0
- package/dist/modes/interactive/components/team-attach-panel.d.ts +18 -1
- package/dist/modes/interactive/components/team-attach-panel.d.ts.map +1 -1
- package/dist/modes/interactive/components/team-attach-panel.js +96 -5
- package/dist/modes/interactive/components/team-attach-panel.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +2 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +18 -5
- 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
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.4.55] - 2026-06-12
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Approval gates inline in the attach panel: when the attached role pauses
|
|
8
|
+
(`task_paused`), the question and its options render inside the side panel —
|
|
9
|
+
right where the stream stopped — instead of the editor's INPUT NEEDED pane.
|
|
10
|
+
While a gate is open it owns the panel's keyboard (`q`/`n` type into the
|
|
11
|
+
custom row, esc skips); answering stamps "✓ answered: …" into the stream and
|
|
12
|
+
resumes it. Detaching mid-gate falls back to the options pane so the
|
|
13
|
+
question isn't lost, and a gate answered elsewhere still dismisses itself.
|
|
14
|
+
The panel also renders orchestrator task lifecycle lines now
|
|
15
|
+
(`task_started`/`task_paused`/`task_resumed`/`task_finished`).
|
|
16
|
+
|
|
3
17
|
## [0.4.54] - 2026-06-12
|
|
4
18
|
|
|
5
19
|
### Added
|
|
@@ -11,8 +11,15 @@
|
|
|
11
11
|
* The panel filters the team connection's single shared /events subscription —
|
|
12
12
|
* it never opens its own SSE connection — and unsubscribes on dispose(), so
|
|
13
13
|
* attach/detach cycles leave no leaked subscribers.
|
|
14
|
+
*
|
|
15
|
+
* Approval gates: task_* lifecycle events render as stream lines, and when the
|
|
16
|
+
* attached role pauses, presentApproval() embeds the AskOptions pane right
|
|
17
|
+
* where the stream stopped — pick an option (or type a free-form answer) and
|
|
18
|
+
* the caller answers the server; the stream then carries on under a
|
|
19
|
+
* "✓ answered: …" stamp.
|
|
14
20
|
*/
|
|
15
21
|
import type { Component, Focusable, TUI } from "@kolisachint/hoocode-tui";
|
|
22
|
+
import type { TeamApproval } from "../../../core/team-approvals.js";
|
|
16
23
|
import type { TeamViewConnection } from "../../../core/team-view.js";
|
|
17
24
|
/** Fixed-capacity FIFO over a circular array: push evicts the oldest entry. */
|
|
18
25
|
export declare class RingBuffer<T> {
|
|
@@ -41,10 +48,20 @@ export declare class TeamAttachPanelComponent implements Component, Focusable {
|
|
|
41
48
|
private partial;
|
|
42
49
|
private partialKind;
|
|
43
50
|
private unsubscribe;
|
|
51
|
+
/** Gate currently embedded in the panel; input is delegated to it. */
|
|
52
|
+
private approval;
|
|
44
53
|
constructor(role: string, connection: TeamViewConnection, callbacks: TeamAttachPanelCallbacks, ui?: TUI | undefined, bufferLines?: number);
|
|
45
|
-
/** Detach from the shared event stream. Idempotent. */
|
|
54
|
+
/** Detach from the shared event stream; settles any open gate as skipped. Idempotent. */
|
|
46
55
|
dispose(): void;
|
|
47
56
|
invalidate(): void;
|
|
57
|
+
/**
|
|
58
|
+
* Embed one approval gate in the panel (the attached role paused). Resolves
|
|
59
|
+
* with the chosen or free-form answer; undefined when skipped (esc), when
|
|
60
|
+
* the signal aborts (answered elsewhere), or when the panel is disposed.
|
|
61
|
+
* Answering the server is the caller's job — on an answer the panel just
|
|
62
|
+
* stamps "✓ answered: …" into the stream.
|
|
63
|
+
*/
|
|
64
|
+
presentApproval(approval: TeamApproval, signal: AbortSignal): Promise<string | undefined>;
|
|
48
65
|
handleInput(data: string): void;
|
|
49
66
|
private styledPartial;
|
|
50
67
|
/** Flush the streaming tail into the buffer as a finished line. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"team-attach-panel.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/team-attach-panel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,GAAG,EAAE,MAAM,0BAA0B,CAAC;AAE1E,OAAO,KAAK,EAAE,kBAAkB,EAAiB,MAAM,4BAA4B,CAAC;AAGpF,+EAA+E;AAC/E,qBAAa,UAAU,CAAC,CAAC;IAKZ,QAAQ,CAAC,QAAQ,EAAE,MAAM;IAJrC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAoB;IAC1C,OAAO,CAAC,KAAK,CAAK;IAClB,OAAO,CAAC,KAAK,CAAK;IAElB,YAAqB,QAAQ,EAAE,MAAM,EAGpC;IAED,IAAI,CAAC,IAAI,EAAE,CAAC,GAAG,IAAI,CAKlB;IAED,IAAI,MAAM,IAAI,MAAM,CAEnB;IAED,OAAO,IAAI,CAAC,EAAE,CAMb;CACD;AAOD,MAAM,WAAW,wBAAwB;IACxC,wDAAwD;IACxD,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,oCAAoC;IACpC,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CAChC;AAKD,qBAAa,wBAAyB,YAAW,SAAS,EAAE,SAAS;IAUnE,QAAQ,CAAC,IAAI,EAAE,MAAM;IAErB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;IAZrB,OAAO,UAAS;IAEhB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAqB;IAC3C,gFAAgF;IAChF,OAAO,CAAC,OAAO,CAAM;IACrB,OAAO,CAAC,WAAW,CAA+B;IAClD,OAAO,CAAC,WAAW,CAA2B;IAE9C,YACU,IAAI,EAAE,MAAM,EACrB,UAAU,EAAE,kBAAkB,EACb,SAAS,EAAE,wBAAwB,EACnC,EAAE,CAAC,iBAAK,EACzB,WAAW,SAAuB,EASlC;IAED,uDAAuD;IACvD,OAAO,IAAI,IAAI,CAGd;IAED,UAAU,IAAI,IAAI,CAEjB;IAED,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAQ9B;IAED,OAAO,CAAC,aAAa;IAIrB,mEAAmE;IACnE,OAAO,CAAC,SAAS;IAMjB,iFAAiF;IACjF,OAAO,CAAC,WAAW;IAYnB,4EAA4E;IAC5E,OAAO,CAAC,UAAU;IA8DlB,6EAA6E;IAC7E,iBAAiB,IAAI,MAAM,CAE1B;IAED,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAyC9B;CACD","sourcesContent":["/**\n * Attach side-panel for a hooteams role (`a` in team focus).\n *\n * Renders one role's live TeamEvents in the style of hooteams' StreamRenderer\n * (`hooteams attach <role>`): ◉ lifecycle lines, dim italic thinking, inline\n * streaming text, ✓/✗ tool results, dim per-turn usage stamps. Differences\n * from the CLI renderer are dictated by the host: colors go through hoocode's\n * theme helpers (no raw ANSI) and output lands in a bounded ring buffer\n * instead of an unbounded stdout stream.\n *\n * The panel filters the team connection's single shared /events subscription —\n * it never opens its own SSE connection — and unsubscribes on dispose(), so\n * attach/detach cycles leave no leaked subscribers.\n */\n\nimport type { Component, Focusable, TUI } from \"@kolisachint/hoocode-tui\";\nimport { getKeybindings, matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi } from \"@kolisachint/hoocode-tui\";\nimport type { TeamViewConnection, TeamViewEvent } from \"../../../core/team-view.js\";\nimport { theme } from \"../theme/theme.js\";\n\n/** Fixed-capacity FIFO over a circular array: push evicts the oldest entry. */\nexport class RingBuffer<T> {\n\tprivate readonly slots: (T | undefined)[];\n\tprivate start = 0;\n\tprivate count = 0;\n\n\tconstructor(readonly capacity: number) {\n\t\tif (!Number.isInteger(capacity) || capacity <= 0) throw new Error(`invalid ring buffer capacity ${capacity}`);\n\t\tthis.slots = new Array<T | undefined>(capacity);\n\t}\n\n\tpush(item: T): void {\n\t\tconst end = (this.start + this.count) % this.capacity;\n\t\tthis.slots[end] = item;\n\t\tif (this.count < this.capacity) this.count++;\n\t\telse this.start = (this.start + 1) % this.capacity;\n\t}\n\n\tget length(): number {\n\t\treturn this.count;\n\t}\n\n\ttoArray(): T[] {\n\t\tconst out: T[] = [];\n\t\tfor (let i = 0; i < this.count; i++) {\n\t\t\tout.push(this.slots[(this.start + i) % this.capacity] as T);\n\t\t}\n\t\treturn out;\n\t}\n}\n\nfunction argsPreview(args: unknown, max = 60): string {\n\tconst text = JSON.stringify(args) ?? \"\";\n\treturn text.length > max ? `${text.slice(0, max)}…` : text;\n}\n\nexport interface TeamAttachPanelCallbacks {\n\t/** `q`/esc: close the panel; the role keeps running. */\n\tonDetach: () => void;\n\t/** `n`: nudge the attached role. */\n\tonNudge: (role: string) => void;\n}\n\n/** Logical event lines kept in the buffer (wrapped to width at render time). */\nconst DEFAULT_BUFFER_LINES = 200;\n\nexport class TeamAttachPanelComponent implements Component, Focusable {\n\tfocused = false;\n\n\tprivate readonly lines: RingBuffer<string>;\n\t/** Streaming tail (text/thinking deltas) not yet terminated by a line break. */\n\tprivate partial = \"\";\n\tprivate partialKind: \"text\" | \"thinking\" = \"text\";\n\tprivate unsubscribe: (() => void) | undefined;\n\n\tconstructor(\n\t\treadonly role: string,\n\t\tconnection: TeamViewConnection,\n\t\tprivate readonly callbacks: TeamAttachPanelCallbacks,\n\t\tprivate readonly ui?: TUI,\n\t\tbufferLines = DEFAULT_BUFFER_LINES,\n\t) {\n\t\tthis.lines = new RingBuffer(bufferLines);\n\t\t// Filter the shared stream down to this role; no second SSE connection.\n\t\tthis.unsubscribe = connection.subscribe((event) => {\n\t\t\tif (event.role !== this.role) return;\n\t\t\tthis.applyEvent(event);\n\t\t\tthis.ui?.requestRender();\n\t\t});\n\t}\n\n\t/** Detach from the shared event stream. Idempotent. */\n\tdispose(): void {\n\t\tthis.unsubscribe?.();\n\t\tthis.unsubscribe = undefined;\n\t}\n\n\tinvalidate(): void {\n\t\t// No cached rendering state.\n\t}\n\n\thandleInput(data: string): void {\n\t\tif (matchesKey(data, \"q\") || getKeybindings().matches(data, \"tui.select.cancel\")) {\n\t\t\tthis.callbacks.onDetach();\n\t\t\treturn;\n\t\t}\n\t\tif (matchesKey(data, \"n\")) {\n\t\t\tthis.callbacks.onNudge(this.role);\n\t\t}\n\t}\n\n\tprivate styledPartial(): string {\n\t\treturn this.partialKind === \"thinking\" ? theme.italic(theme.fg(\"dim\", this.partial)) : this.partial;\n\t}\n\n\t/** Flush the streaming tail into the buffer as a finished line. */\n\tprivate breakLine(): void {\n\t\tif (this.partial.length === 0) return;\n\t\tthis.lines.push(this.styledPartial());\n\t\tthis.partial = \"\";\n\t}\n\n\t/** Append streaming delta text, completing buffer lines at embedded newlines. */\n\tprivate appendDelta(delta: string, kind: \"text\" | \"thinking\"): void {\n\t\tif (this.partialKind !== kind) this.breakLine();\n\t\tthis.partialKind = kind;\n\t\tconst parts = delta.split(\"\\n\");\n\t\tfor (let i = 0; i < parts.length - 1; i++) {\n\t\t\tthis.partial += parts[i];\n\t\t\tthis.breakLine();\n\t\t\tthis.partialKind = kind;\n\t\t}\n\t\tthis.partial += parts[parts.length - 1];\n\t}\n\n\t/** Mirrors hooteams' StreamRenderer event handling, themed and buffered. */\n\tprivate applyEvent(event: TeamViewEvent): void {\n\t\tswitch (event.type) {\n\t\t\tcase \"agent_start\":\n\t\t\t\tthis.breakLine();\n\t\t\t\tthis.lines.push(theme.fg(\"accent\", `◉ ${event.role} started`));\n\t\t\t\tbreak;\n\t\t\tcase \"message_update\": {\n\t\t\t\tconst delta = event.assistantMessageEvent;\n\t\t\t\tif (!delta) break;\n\t\t\t\tif (delta.type === \"thinking_start\") {\n\t\t\t\t\tthis.breakLine();\n\t\t\t\t\tthis.lines.push(theme.fg(\"dim\", \"◉ thinking…\"));\n\t\t\t\t} else if (delta.type === \"thinking_delta\") {\n\t\t\t\t\tthis.appendDelta(delta.delta ?? \"\", \"thinking\");\n\t\t\t\t} else if (delta.type === \"thinking_end\") {\n\t\t\t\t\tthis.breakLine();\n\t\t\t\t} else if (delta.type === \"text_delta\") {\n\t\t\t\t\tthis.appendDelta(delta.delta ?? \"\", \"text\");\n\t\t\t\t} else if (delta.type === \"text_end\") {\n\t\t\t\t\tthis.breakLine();\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"tool_execution_start\":\n\t\t\t\tthis.breakLine();\n\t\t\t\tthis.lines.push(\n\t\t\t\t\ttheme.fg(\"accent\", \"◉ tool: \") +\n\t\t\t\t\t\ttheme.bold(theme.fg(\"accent\", event.toolName ?? \"?\")) +\n\t\t\t\t\t\ttheme.fg(\"accent\", `(${argsPreview(event.args)})`) +\n\t\t\t\t\t\t\" \" +\n\t\t\t\t\t\ttheme.fg(\"warning\", \"running…\"),\n\t\t\t\t);\n\t\t\t\tbreak;\n\t\t\tcase \"tool_execution_end\":\n\t\t\t\tthis.breakLine();\n\t\t\t\tthis.lines.push(\n\t\t\t\t\tevent.isError\n\t\t\t\t\t\t? theme.fg(\"error\", ` ✗ ${event.toolName ?? \"?\"} failed`)\n\t\t\t\t\t\t: theme.fg(\"success\", ` ✓ ${event.toolName ?? \"?\"} done`),\n\t\t\t\t);\n\t\t\t\tbreak;\n\t\t\tcase \"turn_end\": {\n\t\t\t\tthis.breakLine();\n\t\t\t\tconst usage = event.message?.usage;\n\t\t\t\tif (usage) {\n\t\t\t\t\tconst cost = usage.cost?.total ? ` $${usage.cost.total.toFixed(4)}` : \"\";\n\t\t\t\t\tthis.lines.push(\n\t\t\t\t\t\ttheme.fg(\"dim\", `— turn: ${usage.input ?? 0} in / ${usage.output ?? 0} out tokens${cost}`),\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tif (event.message?.errorMessage) {\n\t\t\t\t\tthis.lines.push(theme.fg(\"error\", `error: ${event.message.errorMessage}`));\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"agent_end\":\n\t\t\t\tthis.breakLine();\n\t\t\t\tthis.lines.push(theme.fg(\"accent\", `◉ ${event.role} idle`));\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\t/** Buffered logical line count (completed lines only). Exposed for tests. */\n\tbufferedLineCount(): number {\n\t\treturn this.lines.length;\n\t}\n\n\trender(width: number): string[] {\n\t\tconst inner = Math.max(1, width);\n\n\t\t// Header: role identity left, key hints right.\n\t\tconst titlePlain = `◉ ${this.role} — attached`;\n\t\tconst title =\n\t\t\ttheme.fg(\"accent\", \"◉ \") + theme.bold(theme.fg(\"accent\", this.role)) + theme.fg(\"muted\", \" — attached\");\n\t\tconst hintsPlain = \"n nudge · q detach\";\n\t\tconst hints = theme.fg(\"dim\", hintsPlain);\n\t\tlet header: string;\n\t\tif (visibleWidth(titlePlain) + 2 + visibleWidth(hintsPlain) <= inner) {\n\t\t\theader = title + \" \".repeat(inner - visibleWidth(titlePlain) - visibleWidth(hintsPlain)) + hints;\n\t\t} else {\n\t\t\theader = truncateToWidth(title, inner, \"…\");\n\t\t}\n\t\tconst rule = theme.fg(\"borderMuted\", \"─\".repeat(inner));\n\n\t\t// Body: wrap each buffered line, then keep the newest rows that fit the\n\t\t// panel's height budget (the freshest output hugs the bottom, like a\n\t\t// terminal tail). Budget derives from the live terminal height so the\n\t\t// panel never asks the overlay compositor to clip it (clipping drops the\n\t\t// bottom — exactly the rows we care about).\n\t\tconst rows = this.ui?.terminal.rows ?? 24;\n\t\tconst bodyBudget = Math.max(5, Math.floor(rows * 0.6) - 3);\n\t\tconst wrapped: string[] = [];\n\t\tfor (const line of this.lines.toArray()) {\n\t\t\tif (line.length === 0) {\n\t\t\t\twrapped.push(\"\");\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\twrapped.push(...wrapTextWithAnsi(line, inner));\n\t\t}\n\t\tif (this.partial.length > 0) {\n\t\t\twrapped.push(...wrapTextWithAnsi(this.styledPartial(), inner));\n\t\t}\n\t\tconst body = wrapped.slice(-bodyBudget);\n\t\tif (body.length === 0) {\n\t\t\tbody.push(theme.fg(\"dim\", \"waiting for events…\"));\n\t\t}\n\n\t\treturn [header, rule, ...body, rule];\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"team-attach-panel.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/team-attach-panel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,GAAG,EAAE,MAAM,0BAA0B,CAAC;AAG1E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iCAAiC,CAAC;AACpE,OAAO,KAAK,EAAE,kBAAkB,EAAiB,MAAM,4BAA4B,CAAC;AAIpF,+EAA+E;AAC/E,qBAAa,UAAU,CAAC,CAAC;IAKZ,QAAQ,CAAC,QAAQ,EAAE,MAAM;IAJrC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAoB;IAC1C,OAAO,CAAC,KAAK,CAAK;IAClB,OAAO,CAAC,KAAK,CAAK;IAElB,YAAqB,QAAQ,EAAE,MAAM,EAGpC;IAED,IAAI,CAAC,IAAI,EAAE,CAAC,GAAG,IAAI,CAKlB;IAED,IAAI,MAAM,IAAI,MAAM,CAEnB;IAED,OAAO,IAAI,CAAC,EAAE,CAMb;CACD;AAOD,MAAM,WAAW,wBAAwB;IACxC,wDAAwD;IACxD,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,oCAAoC;IACpC,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CAChC;AAKD,qBAAa,wBAAyB,YAAW,SAAS,EAAE,SAAS;IAYnE,QAAQ,CAAC,IAAI,EAAE,MAAM;IAErB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;IAdrB,OAAO,UAAS;IAEhB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAqB;IAC3C,gFAAgF;IAChF,OAAO,CAAC,OAAO,CAAM;IACrB,OAAO,CAAC,WAAW,CAA+B;IAClD,OAAO,CAAC,WAAW,CAA2B;IAC9C,sEAAsE;IACtE,OAAO,CAAC,QAAQ,CAA+F;IAE/G,YACU,IAAI,EAAE,MAAM,EACrB,UAAU,EAAE,kBAAkB,EACb,SAAS,EAAE,wBAAwB,EACnC,EAAE,CAAC,iBAAK,EACzB,WAAW,SAAuB,EASlC;IAED,yFAAyF;IACzF,OAAO,IAAI,IAAI,CAId;IAED,UAAU,IAAI,IAAI,CAEjB;IAED;;;;;;OAMG;IACH,eAAe,CAAC,QAAQ,EAAE,YAAY,EAAE,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CA0CxF;IAED,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAc9B;IAED,OAAO,CAAC,aAAa;IAIrB,mEAAmE;IACnE,OAAO,CAAC,SAAS;IAMjB,iFAAiF;IACjF,OAAO,CAAC,WAAW;IAYnB,4EAA4E;IAC5E,OAAO,CAAC,UAAU;IAuFlB,6EAA6E;IAC7E,iBAAiB,IAAI,MAAM,CAE1B;IAED,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAsD9B;CACD","sourcesContent":["/**\n * Attach side-panel for a hooteams role (`a` in team focus).\n *\n * Renders one role's live TeamEvents in the style of hooteams' StreamRenderer\n * (`hooteams attach <role>`): ◉ lifecycle lines, dim italic thinking, inline\n * streaming text, ✓/✗ tool results, dim per-turn usage stamps. Differences\n * from the CLI renderer are dictated by the host: colors go through hoocode's\n * theme helpers (no raw ANSI) and output lands in a bounded ring buffer\n * instead of an unbounded stdout stream.\n *\n * The panel filters the team connection's single shared /events subscription —\n * it never opens its own SSE connection — and unsubscribes on dispose(), so\n * attach/detach cycles leave no leaked subscribers.\n *\n * Approval gates: task_* lifecycle events render as stream lines, and when the\n * attached role pauses, presentApproval() embeds the AskOptions pane right\n * where the stream stopped — pick an option (or type a free-form answer) and\n * the caller answers the server; the stream then carries on under a\n * \"✓ answered: …\" stamp.\n */\n\nimport type { Component, Focusable, TUI } from \"@kolisachint/hoocode-tui\";\nimport { getKeybindings, matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi } from \"@kolisachint/hoocode-tui\";\nimport type { AskQuestion } from \"../../../core/extensions/types.js\";\nimport type { TeamApproval } from \"../../../core/team-approvals.js\";\nimport type { TeamViewConnection, TeamViewEvent } from \"../../../core/team-view.js\";\nimport { theme } from \"../theme/theme.js\";\nimport { AskOptionsComponent } from \"./ask-options.js\";\n\n/** Fixed-capacity FIFO over a circular array: push evicts the oldest entry. */\nexport class RingBuffer<T> {\n\tprivate readonly slots: (T | undefined)[];\n\tprivate start = 0;\n\tprivate count = 0;\n\n\tconstructor(readonly capacity: number) {\n\t\tif (!Number.isInteger(capacity) || capacity <= 0) throw new Error(`invalid ring buffer capacity ${capacity}`);\n\t\tthis.slots = new Array<T | undefined>(capacity);\n\t}\n\n\tpush(item: T): void {\n\t\tconst end = (this.start + this.count) % this.capacity;\n\t\tthis.slots[end] = item;\n\t\tif (this.count < this.capacity) this.count++;\n\t\telse this.start = (this.start + 1) % this.capacity;\n\t}\n\n\tget length(): number {\n\t\treturn this.count;\n\t}\n\n\ttoArray(): T[] {\n\t\tconst out: T[] = [];\n\t\tfor (let i = 0; i < this.count; i++) {\n\t\t\tout.push(this.slots[(this.start + i) % this.capacity] as T);\n\t\t}\n\t\treturn out;\n\t}\n}\n\nfunction argsPreview(args: unknown, max = 60): string {\n\tconst text = JSON.stringify(args) ?? \"\";\n\treturn text.length > max ? `${text.slice(0, max)}…` : text;\n}\n\nexport interface TeamAttachPanelCallbacks {\n\t/** `q`/esc: close the panel; the role keeps running. */\n\tonDetach: () => void;\n\t/** `n`: nudge the attached role. */\n\tonNudge: (role: string) => void;\n}\n\n/** Logical event lines kept in the buffer (wrapped to width at render time). */\nconst DEFAULT_BUFFER_LINES = 200;\n\nexport class TeamAttachPanelComponent implements Component, Focusable {\n\tfocused = false;\n\n\tprivate readonly lines: RingBuffer<string>;\n\t/** Streaming tail (text/thinking deltas) not yet terminated by a line break. */\n\tprivate partial = \"\";\n\tprivate partialKind: \"text\" | \"thinking\" = \"text\";\n\tprivate unsubscribe: (() => void) | undefined;\n\t/** Gate currently embedded in the panel; input is delegated to it. */\n\tprivate approval: { component: AskOptionsComponent; settle: (answer: string | undefined) => void } | undefined;\n\n\tconstructor(\n\t\treadonly role: string,\n\t\tconnection: TeamViewConnection,\n\t\tprivate readonly callbacks: TeamAttachPanelCallbacks,\n\t\tprivate readonly ui?: TUI,\n\t\tbufferLines = DEFAULT_BUFFER_LINES,\n\t) {\n\t\tthis.lines = new RingBuffer(bufferLines);\n\t\t// Filter the shared stream down to this role; no second SSE connection.\n\t\tthis.unsubscribe = connection.subscribe((event) => {\n\t\t\tif (event.role !== this.role) return;\n\t\t\tthis.applyEvent(event);\n\t\t\tthis.ui?.requestRender();\n\t\t});\n\t}\n\n\t/** Detach from the shared event stream; settles any open gate as skipped. Idempotent. */\n\tdispose(): void {\n\t\tthis.unsubscribe?.();\n\t\tthis.unsubscribe = undefined;\n\t\tthis.approval?.settle(undefined);\n\t}\n\n\tinvalidate(): void {\n\t\t// No cached rendering state.\n\t}\n\n\t/**\n\t * Embed one approval gate in the panel (the attached role paused). Resolves\n\t * with the chosen or free-form answer; undefined when skipped (esc), when\n\t * the signal aborts (answered elsewhere), or when the panel is disposed.\n\t * Answering the server is the caller's job — on an answer the panel just\n\t * stamps \"✓ answered: …\" into the stream.\n\t */\n\tpresentApproval(approval: TeamApproval, signal: AbortSignal): Promise<string | undefined> {\n\t\treturn new Promise((resolve) => {\n\t\t\tif (signal.aborted || this.unsubscribe === undefined) {\n\t\t\t\tresolve(undefined);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// The coordinator shows one gate at a time; a stray second call\n\t\t\t// settles the first as skipped instead of stacking panes.\n\t\t\tthis.approval?.settle(undefined);\n\n\t\t\tlet settled = false;\n\t\t\tconst settle = (answer: string | undefined): void => {\n\t\t\t\tif (settled) return;\n\t\t\t\tsettled = true;\n\t\t\t\tsignal.removeEventListener(\"abort\", onAbort);\n\t\t\t\tthis.approval = undefined;\n\t\t\t\tif (answer !== undefined) {\n\t\t\t\t\tthis.breakLine();\n\t\t\t\t\tthis.lines.push(theme.fg(\"success\", ` ✓ answered: ${answer}`));\n\t\t\t\t}\n\t\t\t\tthis.ui?.requestRender();\n\t\t\t\tresolve(answer);\n\t\t\t};\n\t\t\tconst onAbort = (): void => settle(undefined);\n\t\t\tsignal.addEventListener(\"abort\", onAbort, { once: true });\n\n\t\t\tconst question: AskQuestion = {\n\t\t\t\tquestion: approval.question,\n\t\t\t\tshort: approval.taskId,\n\t\t\t\tdetail: `team task \"${approval.taskId}\" is paused until answered`,\n\t\t\t\toptions: approval.options.map((label) => ({ label })),\n\t\t\t\tallowCustom: true,\n\t\t\t};\n\t\t\tconst component = new AskOptionsComponent(\n\t\t\t\t[question],\n\t\t\t\t(answers) => settle(answers[0]),\n\t\t\t\t() => settle(undefined),\n\t\t\t);\n\t\t\tcomponent.focused = this.focused;\n\t\t\tthis.approval = { component, settle };\n\t\t\tthis.ui?.requestRender();\n\t\t});\n\t}\n\n\thandleInput(data: string): void {\n\t\t// An open gate owns the keyboard: q/n must type into the custom row, not\n\t\t// detach or nudge. esc skips the gate (AskOptions cancel), not the panel.\n\t\tif (this.approval) {\n\t\t\tthis.approval.component.handleInput(data);\n\t\t\treturn;\n\t\t}\n\t\tif (matchesKey(data, \"q\") || getKeybindings().matches(data, \"tui.select.cancel\")) {\n\t\t\tthis.callbacks.onDetach();\n\t\t\treturn;\n\t\t}\n\t\tif (matchesKey(data, \"n\")) {\n\t\t\tthis.callbacks.onNudge(this.role);\n\t\t}\n\t}\n\n\tprivate styledPartial(): string {\n\t\treturn this.partialKind === \"thinking\" ? theme.italic(theme.fg(\"dim\", this.partial)) : this.partial;\n\t}\n\n\t/** Flush the streaming tail into the buffer as a finished line. */\n\tprivate breakLine(): void {\n\t\tif (this.partial.length === 0) return;\n\t\tthis.lines.push(this.styledPartial());\n\t\tthis.partial = \"\";\n\t}\n\n\t/** Append streaming delta text, completing buffer lines at embedded newlines. */\n\tprivate appendDelta(delta: string, kind: \"text\" | \"thinking\"): void {\n\t\tif (this.partialKind !== kind) this.breakLine();\n\t\tthis.partialKind = kind;\n\t\tconst parts = delta.split(\"\\n\");\n\t\tfor (let i = 0; i < parts.length - 1; i++) {\n\t\t\tthis.partial += parts[i];\n\t\t\tthis.breakLine();\n\t\t\tthis.partialKind = kind;\n\t\t}\n\t\tthis.partial += parts[parts.length - 1];\n\t}\n\n\t/** Mirrors hooteams' StreamRenderer event handling, themed and buffered. */\n\tprivate applyEvent(event: TeamViewEvent): void {\n\t\tswitch (event.type) {\n\t\t\tcase \"agent_start\":\n\t\t\t\tthis.breakLine();\n\t\t\t\tthis.lines.push(theme.fg(\"accent\", `◉ ${event.role} started`));\n\t\t\t\tbreak;\n\t\t\tcase \"message_update\": {\n\t\t\t\tconst delta = event.assistantMessageEvent;\n\t\t\t\tif (!delta) break;\n\t\t\t\tif (delta.type === \"thinking_start\") {\n\t\t\t\t\tthis.breakLine();\n\t\t\t\t\tthis.lines.push(theme.fg(\"dim\", \"◉ thinking…\"));\n\t\t\t\t} else if (delta.type === \"thinking_delta\") {\n\t\t\t\t\tthis.appendDelta(delta.delta ?? \"\", \"thinking\");\n\t\t\t\t} else if (delta.type === \"thinking_end\") {\n\t\t\t\t\tthis.breakLine();\n\t\t\t\t} else if (delta.type === \"text_delta\") {\n\t\t\t\t\tthis.appendDelta(delta.delta ?? \"\", \"text\");\n\t\t\t\t} else if (delta.type === \"text_end\") {\n\t\t\t\t\tthis.breakLine();\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"tool_execution_start\":\n\t\t\t\tthis.breakLine();\n\t\t\t\tthis.lines.push(\n\t\t\t\t\ttheme.fg(\"accent\", \"◉ tool: \") +\n\t\t\t\t\t\ttheme.bold(theme.fg(\"accent\", event.toolName ?? \"?\")) +\n\t\t\t\t\t\ttheme.fg(\"accent\", `(${argsPreview(event.args)})`) +\n\t\t\t\t\t\t\" \" +\n\t\t\t\t\t\ttheme.fg(\"warning\", \"running…\"),\n\t\t\t\t);\n\t\t\t\tbreak;\n\t\t\tcase \"tool_execution_end\":\n\t\t\t\tthis.breakLine();\n\t\t\t\tthis.lines.push(\n\t\t\t\t\tevent.isError\n\t\t\t\t\t\t? theme.fg(\"error\", ` ✗ ${event.toolName ?? \"?\"} failed`)\n\t\t\t\t\t\t: theme.fg(\"success\", ` ✓ ${event.toolName ?? \"?\"} done`),\n\t\t\t\t);\n\t\t\t\tbreak;\n\t\t\tcase \"turn_end\": {\n\t\t\t\tthis.breakLine();\n\t\t\t\tconst usage = event.message?.usage;\n\t\t\t\tif (usage) {\n\t\t\t\t\tconst cost = usage.cost?.total ? ` $${usage.cost.total.toFixed(4)}` : \"\";\n\t\t\t\t\tthis.lines.push(\n\t\t\t\t\t\ttheme.fg(\"dim\", `— turn: ${usage.input ?? 0} in / ${usage.output ?? 0} out tokens${cost}`),\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tif (event.message?.errorMessage) {\n\t\t\t\t\tthis.lines.push(theme.fg(\"error\", `error: ${event.message.errorMessage}`));\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"agent_end\":\n\t\t\t\tthis.breakLine();\n\t\t\t\tthis.lines.push(theme.fg(\"accent\", `◉ ${event.role} idle`));\n\t\t\t\tbreak;\n\t\t\tcase \"task_started\":\n\t\t\t\tthis.breakLine();\n\t\t\t\tthis.lines.push(theme.fg(\"accent\", `◉ task ${event.taskId ?? \"?\"} started`));\n\t\t\t\tbreak;\n\t\t\tcase \"task_paused\":\n\t\t\t\tthis.breakLine();\n\t\t\t\tthis.lines.push(theme.fg(\"warning\", `⏸ awaiting approval: ${event.question ?? \"?\"}`));\n\t\t\t\tbreak;\n\t\t\tcase \"task_resumed\":\n\t\t\t\tthis.breakLine();\n\t\t\t\tthis.lines.push(\n\t\t\t\t\ttheme.fg(\n\t\t\t\t\t\t\"accent\",\n\t\t\t\t\t\t`▶ task ${event.taskId ?? \"?\"} resumed${event.chosenOption ? `: ${event.chosenOption}` : \"\"}`,\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t\tbreak;\n\t\t\tcase \"task_finished\":\n\t\t\t\tthis.breakLine();\n\t\t\t\tthis.lines.push(\n\t\t\t\t\tevent.status === \"error\"\n\t\t\t\t\t\t? theme.fg(\"error\", `✗ task ${event.taskId ?? \"?\"} failed`)\n\t\t\t\t\t\t: theme.fg(\"success\", `✓ task ${event.taskId ?? \"?\"} done`),\n\t\t\t\t);\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\t/** Buffered logical line count (completed lines only). Exposed for tests. */\n\tbufferedLineCount(): number {\n\t\treturn this.lines.length;\n\t}\n\n\trender(width: number): string[] {\n\t\tconst inner = Math.max(1, width);\n\n\t\t// Header: role identity left, key hints right. An open gate owns the\n\t\t// keyboard and renders its own hints, so the panel's would lie.\n\t\tconst titlePlain = `◉ ${this.role} — attached`;\n\t\tconst title =\n\t\t\ttheme.fg(\"accent\", \"◉ \") + theme.bold(theme.fg(\"accent\", this.role)) + theme.fg(\"muted\", \" — attached\");\n\t\tconst hintsPlain = this.approval ? \"\" : \"n nudge · q detach\";\n\t\tconst hints = theme.fg(\"dim\", hintsPlain);\n\t\tlet header: string;\n\t\tif (visibleWidth(titlePlain) + 2 + visibleWidth(hintsPlain) <= inner) {\n\t\t\theader = title + \" \".repeat(inner - visibleWidth(titlePlain) - visibleWidth(hintsPlain)) + hints;\n\t\t} else {\n\t\t\theader = truncateToWidth(title, inner, \"…\");\n\t\t}\n\t\tconst rule = theme.fg(\"borderMuted\", \"─\".repeat(inner));\n\n\t\t// Body: wrap each buffered line, then keep the newest rows that fit the\n\t\t// panel's height budget (the freshest output hugs the bottom, like a\n\t\t// terminal tail). Budget derives from the live terminal height so the\n\t\t// panel never asks the overlay compositor to clip it (clipping drops the\n\t\t// bottom — exactly the rows we care about).\n\t\tconst rows = this.ui?.terminal.rows ?? 24;\n\t\tconst bodyBudget = Math.max(5, Math.floor(rows * 0.6) - 3);\n\n\t\t// An open gate takes its rows out of the stream's budget: the freshest\n\t\t// output stays visible above the question for context.\n\t\tlet approvalLines: string[] = [];\n\t\tif (this.approval) {\n\t\t\tthis.approval.component.focused = this.focused;\n\t\t\tapprovalLines = this.approval.component.render(inner);\n\t\t}\n\n\t\tconst wrapped: string[] = [];\n\t\tfor (const line of this.lines.toArray()) {\n\t\t\tif (line.length === 0) {\n\t\t\t\twrapped.push(\"\");\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\twrapped.push(...wrapTextWithAnsi(line, inner));\n\t\t}\n\t\tif (this.partial.length > 0) {\n\t\t\twrapped.push(...wrapTextWithAnsi(this.styledPartial(), inner));\n\t\t}\n\t\tconst keep = Math.max(0, bodyBudget - approvalLines.length);\n\t\tconst body = keep > 0 ? wrapped.slice(-keep) : [];\n\t\tif (body.length === 0 && approvalLines.length === 0) {\n\t\t\tbody.push(theme.fg(\"dim\", \"waiting for events…\"));\n\t\t}\n\n\t\t// The gate brings its own accent rules, so it closes the panel itself.\n\t\tif (approvalLines.length > 0) return [header, rule, ...body, ...approvalLines];\n\t\treturn [header, rule, ...body, rule];\n\t}\n}\n"]}
|
|
@@ -11,9 +11,16 @@
|
|
|
11
11
|
* The panel filters the team connection's single shared /events subscription —
|
|
12
12
|
* it never opens its own SSE connection — and unsubscribes on dispose(), so
|
|
13
13
|
* attach/detach cycles leave no leaked subscribers.
|
|
14
|
+
*
|
|
15
|
+
* Approval gates: task_* lifecycle events render as stream lines, and when the
|
|
16
|
+
* attached role pauses, presentApproval() embeds the AskOptions pane right
|
|
17
|
+
* where the stream stopped — pick an option (or type a free-form answer) and
|
|
18
|
+
* the caller answers the server; the stream then carries on under a
|
|
19
|
+
* "✓ answered: …" stamp.
|
|
14
20
|
*/
|
|
15
21
|
import { getKeybindings, matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@kolisachint/hoocode-tui";
|
|
16
22
|
import { theme } from "../theme/theme.js";
|
|
23
|
+
import { AskOptionsComponent } from "./ask-options.js";
|
|
17
24
|
/** Fixed-capacity FIFO over a circular array: push evicts the oldest entry. */
|
|
18
25
|
export class RingBuffer {
|
|
19
26
|
capacity;
|
|
@@ -61,6 +68,8 @@ export class TeamAttachPanelComponent {
|
|
|
61
68
|
partial = "";
|
|
62
69
|
partialKind = "text";
|
|
63
70
|
unsubscribe;
|
|
71
|
+
/** Gate currently embedded in the panel; input is delegated to it. */
|
|
72
|
+
approval;
|
|
64
73
|
constructor(role, connection, callbacks, ui, bufferLines = DEFAULT_BUFFER_LINES) {
|
|
65
74
|
this.role = role;
|
|
66
75
|
this.callbacks = callbacks;
|
|
@@ -74,15 +83,67 @@ export class TeamAttachPanelComponent {
|
|
|
74
83
|
this.ui?.requestRender();
|
|
75
84
|
});
|
|
76
85
|
}
|
|
77
|
-
/** Detach from the shared event stream. Idempotent. */
|
|
86
|
+
/** Detach from the shared event stream; settles any open gate as skipped. Idempotent. */
|
|
78
87
|
dispose() {
|
|
79
88
|
this.unsubscribe?.();
|
|
80
89
|
this.unsubscribe = undefined;
|
|
90
|
+
this.approval?.settle(undefined);
|
|
81
91
|
}
|
|
82
92
|
invalidate() {
|
|
83
93
|
// No cached rendering state.
|
|
84
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* Embed one approval gate in the panel (the attached role paused). Resolves
|
|
97
|
+
* with the chosen or free-form answer; undefined when skipped (esc), when
|
|
98
|
+
* the signal aborts (answered elsewhere), or when the panel is disposed.
|
|
99
|
+
* Answering the server is the caller's job — on an answer the panel just
|
|
100
|
+
* stamps "✓ answered: …" into the stream.
|
|
101
|
+
*/
|
|
102
|
+
presentApproval(approval, signal) {
|
|
103
|
+
return new Promise((resolve) => {
|
|
104
|
+
if (signal.aborted || this.unsubscribe === undefined) {
|
|
105
|
+
resolve(undefined);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
// The coordinator shows one gate at a time; a stray second call
|
|
109
|
+
// settles the first as skipped instead of stacking panes.
|
|
110
|
+
this.approval?.settle(undefined);
|
|
111
|
+
let settled = false;
|
|
112
|
+
const settle = (answer) => {
|
|
113
|
+
if (settled)
|
|
114
|
+
return;
|
|
115
|
+
settled = true;
|
|
116
|
+
signal.removeEventListener("abort", onAbort);
|
|
117
|
+
this.approval = undefined;
|
|
118
|
+
if (answer !== undefined) {
|
|
119
|
+
this.breakLine();
|
|
120
|
+
this.lines.push(theme.fg("success", ` ✓ answered: ${answer}`));
|
|
121
|
+
}
|
|
122
|
+
this.ui?.requestRender();
|
|
123
|
+
resolve(answer);
|
|
124
|
+
};
|
|
125
|
+
const onAbort = () => settle(undefined);
|
|
126
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
127
|
+
const question = {
|
|
128
|
+
question: approval.question,
|
|
129
|
+
short: approval.taskId,
|
|
130
|
+
detail: `team task "${approval.taskId}" is paused until answered`,
|
|
131
|
+
options: approval.options.map((label) => ({ label })),
|
|
132
|
+
allowCustom: true,
|
|
133
|
+
};
|
|
134
|
+
const component = new AskOptionsComponent([question], (answers) => settle(answers[0]), () => settle(undefined));
|
|
135
|
+
component.focused = this.focused;
|
|
136
|
+
this.approval = { component, settle };
|
|
137
|
+
this.ui?.requestRender();
|
|
138
|
+
});
|
|
139
|
+
}
|
|
85
140
|
handleInput(data) {
|
|
141
|
+
// An open gate owns the keyboard: q/n must type into the custom row, not
|
|
142
|
+
// detach or nudge. esc skips the gate (AskOptions cancel), not the panel.
|
|
143
|
+
if (this.approval) {
|
|
144
|
+
this.approval.component.handleInput(data);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
86
147
|
if (matchesKey(data, "q") || getKeybindings().matches(data, "tui.select.cancel")) {
|
|
87
148
|
this.callbacks.onDetach();
|
|
88
149
|
return;
|
|
@@ -173,6 +234,24 @@ export class TeamAttachPanelComponent {
|
|
|
173
234
|
this.breakLine();
|
|
174
235
|
this.lines.push(theme.fg("accent", `◉ ${event.role} idle`));
|
|
175
236
|
break;
|
|
237
|
+
case "task_started":
|
|
238
|
+
this.breakLine();
|
|
239
|
+
this.lines.push(theme.fg("accent", `◉ task ${event.taskId ?? "?"} started`));
|
|
240
|
+
break;
|
|
241
|
+
case "task_paused":
|
|
242
|
+
this.breakLine();
|
|
243
|
+
this.lines.push(theme.fg("warning", `⏸ awaiting approval: ${event.question ?? "?"}`));
|
|
244
|
+
break;
|
|
245
|
+
case "task_resumed":
|
|
246
|
+
this.breakLine();
|
|
247
|
+
this.lines.push(theme.fg("accent", `▶ task ${event.taskId ?? "?"} resumed${event.chosenOption ? `: ${event.chosenOption}` : ""}`));
|
|
248
|
+
break;
|
|
249
|
+
case "task_finished":
|
|
250
|
+
this.breakLine();
|
|
251
|
+
this.lines.push(event.status === "error"
|
|
252
|
+
? theme.fg("error", `✗ task ${event.taskId ?? "?"} failed`)
|
|
253
|
+
: theme.fg("success", `✓ task ${event.taskId ?? "?"} done`));
|
|
254
|
+
break;
|
|
176
255
|
}
|
|
177
256
|
}
|
|
178
257
|
/** Buffered logical line count (completed lines only). Exposed for tests. */
|
|
@@ -181,10 +260,11 @@ export class TeamAttachPanelComponent {
|
|
|
181
260
|
}
|
|
182
261
|
render(width) {
|
|
183
262
|
const inner = Math.max(1, width);
|
|
184
|
-
// Header: role identity left, key hints right.
|
|
263
|
+
// Header: role identity left, key hints right. An open gate owns the
|
|
264
|
+
// keyboard and renders its own hints, so the panel's would lie.
|
|
185
265
|
const titlePlain = `◉ ${this.role} — attached`;
|
|
186
266
|
const title = theme.fg("accent", "◉ ") + theme.bold(theme.fg("accent", this.role)) + theme.fg("muted", " — attached");
|
|
187
|
-
const hintsPlain = "n nudge · q detach";
|
|
267
|
+
const hintsPlain = this.approval ? "" : "n nudge · q detach";
|
|
188
268
|
const hints = theme.fg("dim", hintsPlain);
|
|
189
269
|
let header;
|
|
190
270
|
if (visibleWidth(titlePlain) + 2 + visibleWidth(hintsPlain) <= inner) {
|
|
@@ -201,6 +281,13 @@ export class TeamAttachPanelComponent {
|
|
|
201
281
|
// bottom — exactly the rows we care about).
|
|
202
282
|
const rows = this.ui?.terminal.rows ?? 24;
|
|
203
283
|
const bodyBudget = Math.max(5, Math.floor(rows * 0.6) - 3);
|
|
284
|
+
// An open gate takes its rows out of the stream's budget: the freshest
|
|
285
|
+
// output stays visible above the question for context.
|
|
286
|
+
let approvalLines = [];
|
|
287
|
+
if (this.approval) {
|
|
288
|
+
this.approval.component.focused = this.focused;
|
|
289
|
+
approvalLines = this.approval.component.render(inner);
|
|
290
|
+
}
|
|
204
291
|
const wrapped = [];
|
|
205
292
|
for (const line of this.lines.toArray()) {
|
|
206
293
|
if (line.length === 0) {
|
|
@@ -212,10 +299,14 @@ export class TeamAttachPanelComponent {
|
|
|
212
299
|
if (this.partial.length > 0) {
|
|
213
300
|
wrapped.push(...wrapTextWithAnsi(this.styledPartial(), inner));
|
|
214
301
|
}
|
|
215
|
-
const
|
|
216
|
-
|
|
302
|
+
const keep = Math.max(0, bodyBudget - approvalLines.length);
|
|
303
|
+
const body = keep > 0 ? wrapped.slice(-keep) : [];
|
|
304
|
+
if (body.length === 0 && approvalLines.length === 0) {
|
|
217
305
|
body.push(theme.fg("dim", "waiting for events…"));
|
|
218
306
|
}
|
|
307
|
+
// The gate brings its own accent rules, so it closes the panel itself.
|
|
308
|
+
if (approvalLines.length > 0)
|
|
309
|
+
return [header, rule, ...body, ...approvalLines];
|
|
219
310
|
return [header, rule, ...body, rule];
|
|
220
311
|
}
|
|
221
312
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"team-attach-panel.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/team-attach-panel.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Attach side-panel for a hooteams role (`a` in team focus).\n *\n * Renders one role's live TeamEvents in the style of hooteams' StreamRenderer\n * (`hooteams attach <role>`): ◉ lifecycle lines, dim italic thinking, inline\n * streaming text, ✓/✗ tool results, dim per-turn usage stamps. Differences\n * from the CLI renderer are dictated by the host: colors go through hoocode's\n * theme helpers (no raw ANSI) and output lands in a bounded ring buffer\n * instead of an unbounded stdout stream.\n *\n * The panel filters the team connection's single shared /events subscription —\n * it never opens its own SSE connection — and unsubscribes on dispose(), so\n * attach/detach cycles leave no leaked subscribers.\n */\n\nimport type { Component, Focusable, TUI } from \"@kolisachint/hoocode-tui\";\nimport { getKeybindings, matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi } from \"@kolisachint/hoocode-tui\";\nimport type { TeamViewConnection, TeamViewEvent } from \"../../../core/team-view.js\";\nimport { theme } from \"../theme/theme.js\";\n\n/** Fixed-capacity FIFO over a circular array: push evicts the oldest entry. */\nexport class RingBuffer<T> {\n\tprivate readonly slots: (T | undefined)[];\n\tprivate start = 0;\n\tprivate count = 0;\n\n\tconstructor(readonly capacity: number) {\n\t\tif (!Number.isInteger(capacity) || capacity <= 0) throw new Error(`invalid ring buffer capacity ${capacity}`);\n\t\tthis.slots = new Array<T | undefined>(capacity);\n\t}\n\n\tpush(item: T): void {\n\t\tconst end = (this.start + this.count) % this.capacity;\n\t\tthis.slots[end] = item;\n\t\tif (this.count < this.capacity) this.count++;\n\t\telse this.start = (this.start + 1) % this.capacity;\n\t}\n\n\tget length(): number {\n\t\treturn this.count;\n\t}\n\n\ttoArray(): T[] {\n\t\tconst out: T[] = [];\n\t\tfor (let i = 0; i < this.count; i++) {\n\t\t\tout.push(this.slots[(this.start + i) % this.capacity] as T);\n\t\t}\n\t\treturn out;\n\t}\n}\n\nfunction argsPreview(args: unknown, max = 60): string {\n\tconst text = JSON.stringify(args) ?? \"\";\n\treturn text.length > max ? `${text.slice(0, max)}…` : text;\n}\n\nexport interface TeamAttachPanelCallbacks {\n\t/** `q`/esc: close the panel; the role keeps running. */\n\tonDetach: () => void;\n\t/** `n`: nudge the attached role. */\n\tonNudge: (role: string) => void;\n}\n\n/** Logical event lines kept in the buffer (wrapped to width at render time). */\nconst DEFAULT_BUFFER_LINES = 200;\n\nexport class TeamAttachPanelComponent implements Component, Focusable {\n\tfocused = false;\n\n\tprivate readonly lines: RingBuffer<string>;\n\t/** Streaming tail (text/thinking deltas) not yet terminated by a line break. */\n\tprivate partial = \"\";\n\tprivate partialKind: \"text\" | \"thinking\" = \"text\";\n\tprivate unsubscribe: (() => void) | undefined;\n\n\tconstructor(\n\t\treadonly role: string,\n\t\tconnection: TeamViewConnection,\n\t\tprivate readonly callbacks: TeamAttachPanelCallbacks,\n\t\tprivate readonly ui?: TUI,\n\t\tbufferLines = DEFAULT_BUFFER_LINES,\n\t) {\n\t\tthis.lines = new RingBuffer(bufferLines);\n\t\t// Filter the shared stream down to this role; no second SSE connection.\n\t\tthis.unsubscribe = connection.subscribe((event) => {\n\t\t\tif (event.role !== this.role) return;\n\t\t\tthis.applyEvent(event);\n\t\t\tthis.ui?.requestRender();\n\t\t});\n\t}\n\n\t/** Detach from the shared event stream. Idempotent. */\n\tdispose(): void {\n\t\tthis.unsubscribe?.();\n\t\tthis.unsubscribe = undefined;\n\t}\n\n\tinvalidate(): void {\n\t\t// No cached rendering state.\n\t}\n\n\thandleInput(data: string): void {\n\t\tif (matchesKey(data, \"q\") || getKeybindings().matches(data, \"tui.select.cancel\")) {\n\t\t\tthis.callbacks.onDetach();\n\t\t\treturn;\n\t\t}\n\t\tif (matchesKey(data, \"n\")) {\n\t\t\tthis.callbacks.onNudge(this.role);\n\t\t}\n\t}\n\n\tprivate styledPartial(): string {\n\t\treturn this.partialKind === \"thinking\" ? theme.italic(theme.fg(\"dim\", this.partial)) : this.partial;\n\t}\n\n\t/** Flush the streaming tail into the buffer as a finished line. */\n\tprivate breakLine(): void {\n\t\tif (this.partial.length === 0) return;\n\t\tthis.lines.push(this.styledPartial());\n\t\tthis.partial = \"\";\n\t}\n\n\t/** Append streaming delta text, completing buffer lines at embedded newlines. */\n\tprivate appendDelta(delta: string, kind: \"text\" | \"thinking\"): void {\n\t\tif (this.partialKind !== kind) this.breakLine();\n\t\tthis.partialKind = kind;\n\t\tconst parts = delta.split(\"\\n\");\n\t\tfor (let i = 0; i < parts.length - 1; i++) {\n\t\t\tthis.partial += parts[i];\n\t\t\tthis.breakLine();\n\t\t\tthis.partialKind = kind;\n\t\t}\n\t\tthis.partial += parts[parts.length - 1];\n\t}\n\n\t/** Mirrors hooteams' StreamRenderer event handling, themed and buffered. */\n\tprivate applyEvent(event: TeamViewEvent): void {\n\t\tswitch (event.type) {\n\t\t\tcase \"agent_start\":\n\t\t\t\tthis.breakLine();\n\t\t\t\tthis.lines.push(theme.fg(\"accent\", `◉ ${event.role} started`));\n\t\t\t\tbreak;\n\t\t\tcase \"message_update\": {\n\t\t\t\tconst delta = event.assistantMessageEvent;\n\t\t\t\tif (!delta) break;\n\t\t\t\tif (delta.type === \"thinking_start\") {\n\t\t\t\t\tthis.breakLine();\n\t\t\t\t\tthis.lines.push(theme.fg(\"dim\", \"◉ thinking…\"));\n\t\t\t\t} else if (delta.type === \"thinking_delta\") {\n\t\t\t\t\tthis.appendDelta(delta.delta ?? \"\", \"thinking\");\n\t\t\t\t} else if (delta.type === \"thinking_end\") {\n\t\t\t\t\tthis.breakLine();\n\t\t\t\t} else if (delta.type === \"text_delta\") {\n\t\t\t\t\tthis.appendDelta(delta.delta ?? \"\", \"text\");\n\t\t\t\t} else if (delta.type === \"text_end\") {\n\t\t\t\t\tthis.breakLine();\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"tool_execution_start\":\n\t\t\t\tthis.breakLine();\n\t\t\t\tthis.lines.push(\n\t\t\t\t\ttheme.fg(\"accent\", \"◉ tool: \") +\n\t\t\t\t\t\ttheme.bold(theme.fg(\"accent\", event.toolName ?? \"?\")) +\n\t\t\t\t\t\ttheme.fg(\"accent\", `(${argsPreview(event.args)})`) +\n\t\t\t\t\t\t\" \" +\n\t\t\t\t\t\ttheme.fg(\"warning\", \"running…\"),\n\t\t\t\t);\n\t\t\t\tbreak;\n\t\t\tcase \"tool_execution_end\":\n\t\t\t\tthis.breakLine();\n\t\t\t\tthis.lines.push(\n\t\t\t\t\tevent.isError\n\t\t\t\t\t\t? theme.fg(\"error\", ` ✗ ${event.toolName ?? \"?\"} failed`)\n\t\t\t\t\t\t: theme.fg(\"success\", ` ✓ ${event.toolName ?? \"?\"} done`),\n\t\t\t\t);\n\t\t\t\tbreak;\n\t\t\tcase \"turn_end\": {\n\t\t\t\tthis.breakLine();\n\t\t\t\tconst usage = event.message?.usage;\n\t\t\t\tif (usage) {\n\t\t\t\t\tconst cost = usage.cost?.total ? ` $${usage.cost.total.toFixed(4)}` : \"\";\n\t\t\t\t\tthis.lines.push(\n\t\t\t\t\t\ttheme.fg(\"dim\", `— turn: ${usage.input ?? 0} in / ${usage.output ?? 0} out tokens${cost}`),\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tif (event.message?.errorMessage) {\n\t\t\t\t\tthis.lines.push(theme.fg(\"error\", `error: ${event.message.errorMessage}`));\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"agent_end\":\n\t\t\t\tthis.breakLine();\n\t\t\t\tthis.lines.push(theme.fg(\"accent\", `◉ ${event.role} idle`));\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\t/** Buffered logical line count (completed lines only). Exposed for tests. */\n\tbufferedLineCount(): number {\n\t\treturn this.lines.length;\n\t}\n\n\trender(width: number): string[] {\n\t\tconst inner = Math.max(1, width);\n\n\t\t// Header: role identity left, key hints right.\n\t\tconst titlePlain = `◉ ${this.role} — attached`;\n\t\tconst title =\n\t\t\ttheme.fg(\"accent\", \"◉ \") + theme.bold(theme.fg(\"accent\", this.role)) + theme.fg(\"muted\", \" — attached\");\n\t\tconst hintsPlain = \"n nudge · q detach\";\n\t\tconst hints = theme.fg(\"dim\", hintsPlain);\n\t\tlet header: string;\n\t\tif (visibleWidth(titlePlain) + 2 + visibleWidth(hintsPlain) <= inner) {\n\t\t\theader = title + \" \".repeat(inner - visibleWidth(titlePlain) - visibleWidth(hintsPlain)) + hints;\n\t\t} else {\n\t\t\theader = truncateToWidth(title, inner, \"…\");\n\t\t}\n\t\tconst rule = theme.fg(\"borderMuted\", \"─\".repeat(inner));\n\n\t\t// Body: wrap each buffered line, then keep the newest rows that fit the\n\t\t// panel's height budget (the freshest output hugs the bottom, like a\n\t\t// terminal tail). Budget derives from the live terminal height so the\n\t\t// panel never asks the overlay compositor to clip it (clipping drops the\n\t\t// bottom — exactly the rows we care about).\n\t\tconst rows = this.ui?.terminal.rows ?? 24;\n\t\tconst bodyBudget = Math.max(5, Math.floor(rows * 0.6) - 3);\n\t\tconst wrapped: string[] = [];\n\t\tfor (const line of this.lines.toArray()) {\n\t\t\tif (line.length === 0) {\n\t\t\t\twrapped.push(\"\");\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\twrapped.push(...wrapTextWithAnsi(line, inner));\n\t\t}\n\t\tif (this.partial.length > 0) {\n\t\t\twrapped.push(...wrapTextWithAnsi(this.styledPartial(), inner));\n\t\t}\n\t\tconst body = wrapped.slice(-bodyBudget);\n\t\tif (body.length === 0) {\n\t\t\tbody.push(theme.fg(\"dim\", \"waiting for events…\"));\n\t\t}\n\n\t\treturn [header, rule, ...body, rule];\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"team-attach-panel.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/team-attach-panel.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Attach side-panel for a hooteams role (`a` in team focus).\n *\n * Renders one role's live TeamEvents in the style of hooteams' StreamRenderer\n * (`hooteams attach <role>`): ◉ lifecycle lines, dim italic thinking, inline\n * streaming text, ✓/✗ tool results, dim per-turn usage stamps. Differences\n * from the CLI renderer are dictated by the host: colors go through hoocode's\n * theme helpers (no raw ANSI) and output lands in a bounded ring buffer\n * instead of an unbounded stdout stream.\n *\n * The panel filters the team connection's single shared /events subscription —\n * it never opens its own SSE connection — and unsubscribes on dispose(), so\n * attach/detach cycles leave no leaked subscribers.\n *\n * Approval gates: task_* lifecycle events render as stream lines, and when the\n * attached role pauses, presentApproval() embeds the AskOptions pane right\n * where the stream stopped — pick an option (or type a free-form answer) and\n * the caller answers the server; the stream then carries on under a\n * \"✓ answered: …\" stamp.\n */\n\nimport type { Component, Focusable, TUI } from \"@kolisachint/hoocode-tui\";\nimport { getKeybindings, matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi } from \"@kolisachint/hoocode-tui\";\nimport type { AskQuestion } from \"../../../core/extensions/types.js\";\nimport type { TeamApproval } from \"../../../core/team-approvals.js\";\nimport type { TeamViewConnection, TeamViewEvent } from \"../../../core/team-view.js\";\nimport { theme } from \"../theme/theme.js\";\nimport { AskOptionsComponent } from \"./ask-options.js\";\n\n/** Fixed-capacity FIFO over a circular array: push evicts the oldest entry. */\nexport class RingBuffer<T> {\n\tprivate readonly slots: (T | undefined)[];\n\tprivate start = 0;\n\tprivate count = 0;\n\n\tconstructor(readonly capacity: number) {\n\t\tif (!Number.isInteger(capacity) || capacity <= 0) throw new Error(`invalid ring buffer capacity ${capacity}`);\n\t\tthis.slots = new Array<T | undefined>(capacity);\n\t}\n\n\tpush(item: T): void {\n\t\tconst end = (this.start + this.count) % this.capacity;\n\t\tthis.slots[end] = item;\n\t\tif (this.count < this.capacity) this.count++;\n\t\telse this.start = (this.start + 1) % this.capacity;\n\t}\n\n\tget length(): number {\n\t\treturn this.count;\n\t}\n\n\ttoArray(): T[] {\n\t\tconst out: T[] = [];\n\t\tfor (let i = 0; i < this.count; i++) {\n\t\t\tout.push(this.slots[(this.start + i) % this.capacity] as T);\n\t\t}\n\t\treturn out;\n\t}\n}\n\nfunction argsPreview(args: unknown, max = 60): string {\n\tconst text = JSON.stringify(args) ?? \"\";\n\treturn text.length > max ? `${text.slice(0, max)}…` : text;\n}\n\nexport interface TeamAttachPanelCallbacks {\n\t/** `q`/esc: close the panel; the role keeps running. */\n\tonDetach: () => void;\n\t/** `n`: nudge the attached role. */\n\tonNudge: (role: string) => void;\n}\n\n/** Logical event lines kept in the buffer (wrapped to width at render time). */\nconst DEFAULT_BUFFER_LINES = 200;\n\nexport class TeamAttachPanelComponent implements Component, Focusable {\n\tfocused = false;\n\n\tprivate readonly lines: RingBuffer<string>;\n\t/** Streaming tail (text/thinking deltas) not yet terminated by a line break. */\n\tprivate partial = \"\";\n\tprivate partialKind: \"text\" | \"thinking\" = \"text\";\n\tprivate unsubscribe: (() => void) | undefined;\n\t/** Gate currently embedded in the panel; input is delegated to it. */\n\tprivate approval: { component: AskOptionsComponent; settle: (answer: string | undefined) => void } | undefined;\n\n\tconstructor(\n\t\treadonly role: string,\n\t\tconnection: TeamViewConnection,\n\t\tprivate readonly callbacks: TeamAttachPanelCallbacks,\n\t\tprivate readonly ui?: TUI,\n\t\tbufferLines = DEFAULT_BUFFER_LINES,\n\t) {\n\t\tthis.lines = new RingBuffer(bufferLines);\n\t\t// Filter the shared stream down to this role; no second SSE connection.\n\t\tthis.unsubscribe = connection.subscribe((event) => {\n\t\t\tif (event.role !== this.role) return;\n\t\t\tthis.applyEvent(event);\n\t\t\tthis.ui?.requestRender();\n\t\t});\n\t}\n\n\t/** Detach from the shared event stream; settles any open gate as skipped. Idempotent. */\n\tdispose(): void {\n\t\tthis.unsubscribe?.();\n\t\tthis.unsubscribe = undefined;\n\t\tthis.approval?.settle(undefined);\n\t}\n\n\tinvalidate(): void {\n\t\t// No cached rendering state.\n\t}\n\n\t/**\n\t * Embed one approval gate in the panel (the attached role paused). Resolves\n\t * with the chosen or free-form answer; undefined when skipped (esc), when\n\t * the signal aborts (answered elsewhere), or when the panel is disposed.\n\t * Answering the server is the caller's job — on an answer the panel just\n\t * stamps \"✓ answered: …\" into the stream.\n\t */\n\tpresentApproval(approval: TeamApproval, signal: AbortSignal): Promise<string | undefined> {\n\t\treturn new Promise((resolve) => {\n\t\t\tif (signal.aborted || this.unsubscribe === undefined) {\n\t\t\t\tresolve(undefined);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// The coordinator shows one gate at a time; a stray second call\n\t\t\t// settles the first as skipped instead of stacking panes.\n\t\t\tthis.approval?.settle(undefined);\n\n\t\t\tlet settled = false;\n\t\t\tconst settle = (answer: string | undefined): void => {\n\t\t\t\tif (settled) return;\n\t\t\t\tsettled = true;\n\t\t\t\tsignal.removeEventListener(\"abort\", onAbort);\n\t\t\t\tthis.approval = undefined;\n\t\t\t\tif (answer !== undefined) {\n\t\t\t\t\tthis.breakLine();\n\t\t\t\t\tthis.lines.push(theme.fg(\"success\", ` ✓ answered: ${answer}`));\n\t\t\t\t}\n\t\t\t\tthis.ui?.requestRender();\n\t\t\t\tresolve(answer);\n\t\t\t};\n\t\t\tconst onAbort = (): void => settle(undefined);\n\t\t\tsignal.addEventListener(\"abort\", onAbort, { once: true });\n\n\t\t\tconst question: AskQuestion = {\n\t\t\t\tquestion: approval.question,\n\t\t\t\tshort: approval.taskId,\n\t\t\t\tdetail: `team task \"${approval.taskId}\" is paused until answered`,\n\t\t\t\toptions: approval.options.map((label) => ({ label })),\n\t\t\t\tallowCustom: true,\n\t\t\t};\n\t\t\tconst component = new AskOptionsComponent(\n\t\t\t\t[question],\n\t\t\t\t(answers) => settle(answers[0]),\n\t\t\t\t() => settle(undefined),\n\t\t\t);\n\t\t\tcomponent.focused = this.focused;\n\t\t\tthis.approval = { component, settle };\n\t\t\tthis.ui?.requestRender();\n\t\t});\n\t}\n\n\thandleInput(data: string): void {\n\t\t// An open gate owns the keyboard: q/n must type into the custom row, not\n\t\t// detach or nudge. esc skips the gate (AskOptions cancel), not the panel.\n\t\tif (this.approval) {\n\t\t\tthis.approval.component.handleInput(data);\n\t\t\treturn;\n\t\t}\n\t\tif (matchesKey(data, \"q\") || getKeybindings().matches(data, \"tui.select.cancel\")) {\n\t\t\tthis.callbacks.onDetach();\n\t\t\treturn;\n\t\t}\n\t\tif (matchesKey(data, \"n\")) {\n\t\t\tthis.callbacks.onNudge(this.role);\n\t\t}\n\t}\n\n\tprivate styledPartial(): string {\n\t\treturn this.partialKind === \"thinking\" ? theme.italic(theme.fg(\"dim\", this.partial)) : this.partial;\n\t}\n\n\t/** Flush the streaming tail into the buffer as a finished line. */\n\tprivate breakLine(): void {\n\t\tif (this.partial.length === 0) return;\n\t\tthis.lines.push(this.styledPartial());\n\t\tthis.partial = \"\";\n\t}\n\n\t/** Append streaming delta text, completing buffer lines at embedded newlines. */\n\tprivate appendDelta(delta: string, kind: \"text\" | \"thinking\"): void {\n\t\tif (this.partialKind !== kind) this.breakLine();\n\t\tthis.partialKind = kind;\n\t\tconst parts = delta.split(\"\\n\");\n\t\tfor (let i = 0; i < parts.length - 1; i++) {\n\t\t\tthis.partial += parts[i];\n\t\t\tthis.breakLine();\n\t\t\tthis.partialKind = kind;\n\t\t}\n\t\tthis.partial += parts[parts.length - 1];\n\t}\n\n\t/** Mirrors hooteams' StreamRenderer event handling, themed and buffered. */\n\tprivate applyEvent(event: TeamViewEvent): void {\n\t\tswitch (event.type) {\n\t\t\tcase \"agent_start\":\n\t\t\t\tthis.breakLine();\n\t\t\t\tthis.lines.push(theme.fg(\"accent\", `◉ ${event.role} started`));\n\t\t\t\tbreak;\n\t\t\tcase \"message_update\": {\n\t\t\t\tconst delta = event.assistantMessageEvent;\n\t\t\t\tif (!delta) break;\n\t\t\t\tif (delta.type === \"thinking_start\") {\n\t\t\t\t\tthis.breakLine();\n\t\t\t\t\tthis.lines.push(theme.fg(\"dim\", \"◉ thinking…\"));\n\t\t\t\t} else if (delta.type === \"thinking_delta\") {\n\t\t\t\t\tthis.appendDelta(delta.delta ?? \"\", \"thinking\");\n\t\t\t\t} else if (delta.type === \"thinking_end\") {\n\t\t\t\t\tthis.breakLine();\n\t\t\t\t} else if (delta.type === \"text_delta\") {\n\t\t\t\t\tthis.appendDelta(delta.delta ?? \"\", \"text\");\n\t\t\t\t} else if (delta.type === \"text_end\") {\n\t\t\t\t\tthis.breakLine();\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"tool_execution_start\":\n\t\t\t\tthis.breakLine();\n\t\t\t\tthis.lines.push(\n\t\t\t\t\ttheme.fg(\"accent\", \"◉ tool: \") +\n\t\t\t\t\t\ttheme.bold(theme.fg(\"accent\", event.toolName ?? \"?\")) +\n\t\t\t\t\t\ttheme.fg(\"accent\", `(${argsPreview(event.args)})`) +\n\t\t\t\t\t\t\" \" +\n\t\t\t\t\t\ttheme.fg(\"warning\", \"running…\"),\n\t\t\t\t);\n\t\t\t\tbreak;\n\t\t\tcase \"tool_execution_end\":\n\t\t\t\tthis.breakLine();\n\t\t\t\tthis.lines.push(\n\t\t\t\t\tevent.isError\n\t\t\t\t\t\t? theme.fg(\"error\", ` ✗ ${event.toolName ?? \"?\"} failed`)\n\t\t\t\t\t\t: theme.fg(\"success\", ` ✓ ${event.toolName ?? \"?\"} done`),\n\t\t\t\t);\n\t\t\t\tbreak;\n\t\t\tcase \"turn_end\": {\n\t\t\t\tthis.breakLine();\n\t\t\t\tconst usage = event.message?.usage;\n\t\t\t\tif (usage) {\n\t\t\t\t\tconst cost = usage.cost?.total ? ` $${usage.cost.total.toFixed(4)}` : \"\";\n\t\t\t\t\tthis.lines.push(\n\t\t\t\t\t\ttheme.fg(\"dim\", `— turn: ${usage.input ?? 0} in / ${usage.output ?? 0} out tokens${cost}`),\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tif (event.message?.errorMessage) {\n\t\t\t\t\tthis.lines.push(theme.fg(\"error\", `error: ${event.message.errorMessage}`));\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"agent_end\":\n\t\t\t\tthis.breakLine();\n\t\t\t\tthis.lines.push(theme.fg(\"accent\", `◉ ${event.role} idle`));\n\t\t\t\tbreak;\n\t\t\tcase \"task_started\":\n\t\t\t\tthis.breakLine();\n\t\t\t\tthis.lines.push(theme.fg(\"accent\", `◉ task ${event.taskId ?? \"?\"} started`));\n\t\t\t\tbreak;\n\t\t\tcase \"task_paused\":\n\t\t\t\tthis.breakLine();\n\t\t\t\tthis.lines.push(theme.fg(\"warning\", `⏸ awaiting approval: ${event.question ?? \"?\"}`));\n\t\t\t\tbreak;\n\t\t\tcase \"task_resumed\":\n\t\t\t\tthis.breakLine();\n\t\t\t\tthis.lines.push(\n\t\t\t\t\ttheme.fg(\n\t\t\t\t\t\t\"accent\",\n\t\t\t\t\t\t`▶ task ${event.taskId ?? \"?\"} resumed${event.chosenOption ? `: ${event.chosenOption}` : \"\"}`,\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t\tbreak;\n\t\t\tcase \"task_finished\":\n\t\t\t\tthis.breakLine();\n\t\t\t\tthis.lines.push(\n\t\t\t\t\tevent.status === \"error\"\n\t\t\t\t\t\t? theme.fg(\"error\", `✗ task ${event.taskId ?? \"?\"} failed`)\n\t\t\t\t\t\t: theme.fg(\"success\", `✓ task ${event.taskId ?? \"?\"} done`),\n\t\t\t\t);\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\t/** Buffered logical line count (completed lines only). Exposed for tests. */\n\tbufferedLineCount(): number {\n\t\treturn this.lines.length;\n\t}\n\n\trender(width: number): string[] {\n\t\tconst inner = Math.max(1, width);\n\n\t\t// Header: role identity left, key hints right. An open gate owns the\n\t\t// keyboard and renders its own hints, so the panel's would lie.\n\t\tconst titlePlain = `◉ ${this.role} — attached`;\n\t\tconst title =\n\t\t\ttheme.fg(\"accent\", \"◉ \") + theme.bold(theme.fg(\"accent\", this.role)) + theme.fg(\"muted\", \" — attached\");\n\t\tconst hintsPlain = this.approval ? \"\" : \"n nudge · q detach\";\n\t\tconst hints = theme.fg(\"dim\", hintsPlain);\n\t\tlet header: string;\n\t\tif (visibleWidth(titlePlain) + 2 + visibleWidth(hintsPlain) <= inner) {\n\t\t\theader = title + \" \".repeat(inner - visibleWidth(titlePlain) - visibleWidth(hintsPlain)) + hints;\n\t\t} else {\n\t\t\theader = truncateToWidth(title, inner, \"…\");\n\t\t}\n\t\tconst rule = theme.fg(\"borderMuted\", \"─\".repeat(inner));\n\n\t\t// Body: wrap each buffered line, then keep the newest rows that fit the\n\t\t// panel's height budget (the freshest output hugs the bottom, like a\n\t\t// terminal tail). Budget derives from the live terminal height so the\n\t\t// panel never asks the overlay compositor to clip it (clipping drops the\n\t\t// bottom — exactly the rows we care about).\n\t\tconst rows = this.ui?.terminal.rows ?? 24;\n\t\tconst bodyBudget = Math.max(5, Math.floor(rows * 0.6) - 3);\n\n\t\t// An open gate takes its rows out of the stream's budget: the freshest\n\t\t// output stays visible above the question for context.\n\t\tlet approvalLines: string[] = [];\n\t\tif (this.approval) {\n\t\t\tthis.approval.component.focused = this.focused;\n\t\t\tapprovalLines = this.approval.component.render(inner);\n\t\t}\n\n\t\tconst wrapped: string[] = [];\n\t\tfor (const line of this.lines.toArray()) {\n\t\t\tif (line.length === 0) {\n\t\t\t\twrapped.push(\"\");\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\twrapped.push(...wrapTextWithAnsi(line, inner));\n\t\t}\n\t\tif (this.partial.length > 0) {\n\t\t\twrapped.push(...wrapTextWithAnsi(this.styledPartial(), inner));\n\t\t}\n\t\tconst keep = Math.max(0, bodyBudget - approvalLines.length);\n\t\tconst body = keep > 0 ? wrapped.slice(-keep) : [];\n\t\tif (body.length === 0 && approvalLines.length === 0) {\n\t\t\tbody.push(theme.fg(\"dim\", \"waiting for events…\"));\n\t\t}\n\n\t\t// The gate brings its own accent rules, so it closes the panel itself.\n\t\tif (approvalLines.length > 0) return [header, rule, ...body, ...approvalLines];\n\t\treturn [header, rule, ...body, rule];\n\t}\n}\n"]}
|
|
@@ -259,7 +259,8 @@ export declare class InteractiveMode {
|
|
|
259
259
|
* is set, before run(). Enables team focus (app.team.focus), nudging (n),
|
|
260
260
|
* the attach side panel (a) on the task panel's teams view, and approval
|
|
261
261
|
* gates: task_paused events (and gates already pending on the server)
|
|
262
|
-
* surface in the
|
|
262
|
+
* surface inline in the attach panel when it shows the paused role,
|
|
263
|
+
* otherwise in the options pane; the answer goes back over
|
|
263
264
|
* POST /tasks/:id/resume.
|
|
264
265
|
*/
|
|
265
266
|
attachTeamClient(client: TeamViewConnection): void;
|