@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.
Files changed (37) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/cli/args.d.ts +1 -1
  3. package/dist/cli/args.d.ts.map +1 -1
  4. package/dist/cli/args.js +3 -1
  5. package/dist/cli/args.js.map +1 -1
  6. package/dist/core/keybindings.d.ts +5 -0
  7. package/dist/core/keybindings.d.ts.map +1 -1
  8. package/dist/core/keybindings.js +8 -0
  9. package/dist/core/keybindings.js.map +1 -1
  10. package/dist/core/team-auto.d.ts +46 -0
  11. package/dist/core/team-auto.d.ts.map +1 -0
  12. package/dist/core/team-auto.js +163 -0
  13. package/dist/core/team-auto.js.map +1 -0
  14. package/dist/core/team-view.d.ts +29 -2
  15. package/dist/core/team-view.d.ts.map +1 -1
  16. package/dist/core/team-view.js +39 -3
  17. package/dist/core/team-view.js.map +1 -1
  18. package/dist/main.d.ts.map +1 -1
  19. package/dist/main.js +28 -7
  20. package/dist/main.js.map +1 -1
  21. package/dist/modes/interactive/components/task-panel.d.ts +14 -2
  22. package/dist/modes/interactive/components/task-panel.d.ts.map +1 -1
  23. package/dist/modes/interactive/components/task-panel.js +69 -5
  24. package/dist/modes/interactive/components/task-panel.js.map +1 -1
  25. package/dist/modes/interactive/components/team-attach-panel.d.ts +60 -0
  26. package/dist/modes/interactive/components/team-attach-panel.d.ts.map +1 -0
  27. package/dist/modes/interactive/components/team-attach-panel.js +222 -0
  28. package/dist/modes/interactive/components/team-attach-panel.js.map +1 -0
  29. package/dist/modes/interactive/interactive-mode.d.ts +24 -0
  30. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  31. package/dist/modes/interactive/interactive-mode.js +106 -0
  32. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  33. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  34. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  35. package/examples/extensions/sandbox/package.json +1 -1
  36. package/examples/extensions/with-deps/package.json +1 -1
  37. package/package.json +4 -4
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Attach side-panel for a hooteams role (`a` in team focus).
3
+ *
4
+ * Renders one role's live TeamEvents in the style of hooteams' StreamRenderer
5
+ * (`hooteams attach <role>`): ◉ lifecycle lines, dim italic thinking, inline
6
+ * streaming text, ✓/✗ tool results, dim per-turn usage stamps. Differences
7
+ * from the CLI renderer are dictated by the host: colors go through hoocode's
8
+ * theme helpers (no raw ANSI) and output lands in a bounded ring buffer
9
+ * instead of an unbounded stdout stream.
10
+ *
11
+ * The panel filters the team connection's single shared /events subscription —
12
+ * it never opens its own SSE connection — and unsubscribes on dispose(), so
13
+ * attach/detach cycles leave no leaked subscribers.
14
+ */
15
+ import type { Component, Focusable, TUI } from "@kolisachint/hoocode-tui";
16
+ import type { TeamViewConnection } from "../../../core/team-view.js";
17
+ /** Fixed-capacity FIFO over a circular array: push evicts the oldest entry. */
18
+ export declare class RingBuffer<T> {
19
+ readonly capacity: number;
20
+ private readonly slots;
21
+ private start;
22
+ private count;
23
+ constructor(capacity: number);
24
+ push(item: T): void;
25
+ get length(): number;
26
+ toArray(): T[];
27
+ }
28
+ export interface TeamAttachPanelCallbacks {
29
+ /** `q`/esc: close the panel; the role keeps running. */
30
+ onDetach: () => void;
31
+ /** `n`: nudge the attached role. */
32
+ onNudge: (role: string) => void;
33
+ }
34
+ export declare class TeamAttachPanelComponent implements Component, Focusable {
35
+ readonly role: string;
36
+ private readonly callbacks;
37
+ private readonly ui?;
38
+ focused: boolean;
39
+ private readonly lines;
40
+ /** Streaming tail (text/thinking deltas) not yet terminated by a line break. */
41
+ private partial;
42
+ private partialKind;
43
+ private unsubscribe;
44
+ constructor(role: string, connection: TeamViewConnection, callbacks: TeamAttachPanelCallbacks, ui?: TUI | undefined, bufferLines?: number);
45
+ /** Detach from the shared event stream. Idempotent. */
46
+ dispose(): void;
47
+ invalidate(): void;
48
+ handleInput(data: string): void;
49
+ private styledPartial;
50
+ /** Flush the streaming tail into the buffer as a finished line. */
51
+ private breakLine;
52
+ /** Append streaming delta text, completing buffer lines at embedded newlines. */
53
+ private appendDelta;
54
+ /** Mirrors hooteams' StreamRenderer event handling, themed and buffered. */
55
+ private applyEvent;
56
+ /** Buffered logical line count (completed lines only). Exposed for tests. */
57
+ bufferedLineCount(): number;
58
+ render(width: number): string[];
59
+ }
60
+ //# sourceMappingURL=team-attach-panel.d.ts.map
@@ -0,0 +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"]}
@@ -0,0 +1,222 @@
1
+ /**
2
+ * Attach side-panel for a hooteams role (`a` in team focus).
3
+ *
4
+ * Renders one role's live TeamEvents in the style of hooteams' StreamRenderer
5
+ * (`hooteams attach <role>`): ◉ lifecycle lines, dim italic thinking, inline
6
+ * streaming text, ✓/✗ tool results, dim per-turn usage stamps. Differences
7
+ * from the CLI renderer are dictated by the host: colors go through hoocode's
8
+ * theme helpers (no raw ANSI) and output lands in a bounded ring buffer
9
+ * instead of an unbounded stdout stream.
10
+ *
11
+ * The panel filters the team connection's single shared /events subscription —
12
+ * it never opens its own SSE connection — and unsubscribes on dispose(), so
13
+ * attach/detach cycles leave no leaked subscribers.
14
+ */
15
+ import { getKeybindings, matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@kolisachint/hoocode-tui";
16
+ import { theme } from "../theme/theme.js";
17
+ /** Fixed-capacity FIFO over a circular array: push evicts the oldest entry. */
18
+ export class RingBuffer {
19
+ capacity;
20
+ slots;
21
+ start = 0;
22
+ count = 0;
23
+ constructor(capacity) {
24
+ this.capacity = capacity;
25
+ if (!Number.isInteger(capacity) || capacity <= 0)
26
+ throw new Error(`invalid ring buffer capacity ${capacity}`);
27
+ this.slots = new Array(capacity);
28
+ }
29
+ push(item) {
30
+ const end = (this.start + this.count) % this.capacity;
31
+ this.slots[end] = item;
32
+ if (this.count < this.capacity)
33
+ this.count++;
34
+ else
35
+ this.start = (this.start + 1) % this.capacity;
36
+ }
37
+ get length() {
38
+ return this.count;
39
+ }
40
+ toArray() {
41
+ const out = [];
42
+ for (let i = 0; i < this.count; i++) {
43
+ out.push(this.slots[(this.start + i) % this.capacity]);
44
+ }
45
+ return out;
46
+ }
47
+ }
48
+ function argsPreview(args, max = 60) {
49
+ const text = JSON.stringify(args) ?? "";
50
+ return text.length > max ? `${text.slice(0, max)}…` : text;
51
+ }
52
+ /** Logical event lines kept in the buffer (wrapped to width at render time). */
53
+ const DEFAULT_BUFFER_LINES = 200;
54
+ export class TeamAttachPanelComponent {
55
+ role;
56
+ callbacks;
57
+ ui;
58
+ focused = false;
59
+ lines;
60
+ /** Streaming tail (text/thinking deltas) not yet terminated by a line break. */
61
+ partial = "";
62
+ partialKind = "text";
63
+ unsubscribe;
64
+ constructor(role, connection, callbacks, ui, bufferLines = DEFAULT_BUFFER_LINES) {
65
+ this.role = role;
66
+ this.callbacks = callbacks;
67
+ this.ui = ui;
68
+ this.lines = new RingBuffer(bufferLines);
69
+ // Filter the shared stream down to this role; no second SSE connection.
70
+ this.unsubscribe = connection.subscribe((event) => {
71
+ if (event.role !== this.role)
72
+ return;
73
+ this.applyEvent(event);
74
+ this.ui?.requestRender();
75
+ });
76
+ }
77
+ /** Detach from the shared event stream. Idempotent. */
78
+ dispose() {
79
+ this.unsubscribe?.();
80
+ this.unsubscribe = undefined;
81
+ }
82
+ invalidate() {
83
+ // No cached rendering state.
84
+ }
85
+ handleInput(data) {
86
+ if (matchesKey(data, "q") || getKeybindings().matches(data, "tui.select.cancel")) {
87
+ this.callbacks.onDetach();
88
+ return;
89
+ }
90
+ if (matchesKey(data, "n")) {
91
+ this.callbacks.onNudge(this.role);
92
+ }
93
+ }
94
+ styledPartial() {
95
+ return this.partialKind === "thinking" ? theme.italic(theme.fg("dim", this.partial)) : this.partial;
96
+ }
97
+ /** Flush the streaming tail into the buffer as a finished line. */
98
+ breakLine() {
99
+ if (this.partial.length === 0)
100
+ return;
101
+ this.lines.push(this.styledPartial());
102
+ this.partial = "";
103
+ }
104
+ /** Append streaming delta text, completing buffer lines at embedded newlines. */
105
+ appendDelta(delta, kind) {
106
+ if (this.partialKind !== kind)
107
+ this.breakLine();
108
+ this.partialKind = kind;
109
+ const parts = delta.split("\n");
110
+ for (let i = 0; i < parts.length - 1; i++) {
111
+ this.partial += parts[i];
112
+ this.breakLine();
113
+ this.partialKind = kind;
114
+ }
115
+ this.partial += parts[parts.length - 1];
116
+ }
117
+ /** Mirrors hooteams' StreamRenderer event handling, themed and buffered. */
118
+ applyEvent(event) {
119
+ switch (event.type) {
120
+ case "agent_start":
121
+ this.breakLine();
122
+ this.lines.push(theme.fg("accent", `◉ ${event.role} started`));
123
+ break;
124
+ case "message_update": {
125
+ const delta = event.assistantMessageEvent;
126
+ if (!delta)
127
+ break;
128
+ if (delta.type === "thinking_start") {
129
+ this.breakLine();
130
+ this.lines.push(theme.fg("dim", "◉ thinking…"));
131
+ }
132
+ else if (delta.type === "thinking_delta") {
133
+ this.appendDelta(delta.delta ?? "", "thinking");
134
+ }
135
+ else if (delta.type === "thinking_end") {
136
+ this.breakLine();
137
+ }
138
+ else if (delta.type === "text_delta") {
139
+ this.appendDelta(delta.delta ?? "", "text");
140
+ }
141
+ else if (delta.type === "text_end") {
142
+ this.breakLine();
143
+ }
144
+ break;
145
+ }
146
+ case "tool_execution_start":
147
+ this.breakLine();
148
+ this.lines.push(theme.fg("accent", "◉ tool: ") +
149
+ theme.bold(theme.fg("accent", event.toolName ?? "?")) +
150
+ theme.fg("accent", `(${argsPreview(event.args)})`) +
151
+ " " +
152
+ theme.fg("warning", "running…"));
153
+ break;
154
+ case "tool_execution_end":
155
+ this.breakLine();
156
+ this.lines.push(event.isError
157
+ ? theme.fg("error", ` ✗ ${event.toolName ?? "?"} failed`)
158
+ : theme.fg("success", ` ✓ ${event.toolName ?? "?"} done`));
159
+ break;
160
+ case "turn_end": {
161
+ this.breakLine();
162
+ const usage = event.message?.usage;
163
+ if (usage) {
164
+ const cost = usage.cost?.total ? ` $${usage.cost.total.toFixed(4)}` : "";
165
+ this.lines.push(theme.fg("dim", `— turn: ${usage.input ?? 0} in / ${usage.output ?? 0} out tokens${cost}`));
166
+ }
167
+ if (event.message?.errorMessage) {
168
+ this.lines.push(theme.fg("error", `error: ${event.message.errorMessage}`));
169
+ }
170
+ break;
171
+ }
172
+ case "agent_end":
173
+ this.breakLine();
174
+ this.lines.push(theme.fg("accent", `◉ ${event.role} idle`));
175
+ break;
176
+ }
177
+ }
178
+ /** Buffered logical line count (completed lines only). Exposed for tests. */
179
+ bufferedLineCount() {
180
+ return this.lines.length;
181
+ }
182
+ render(width) {
183
+ const inner = Math.max(1, width);
184
+ // Header: role identity left, key hints right.
185
+ const titlePlain = `◉ ${this.role} — attached`;
186
+ const title = theme.fg("accent", "◉ ") + theme.bold(theme.fg("accent", this.role)) + theme.fg("muted", " — attached");
187
+ const hintsPlain = "n nudge · q detach";
188
+ const hints = theme.fg("dim", hintsPlain);
189
+ let header;
190
+ if (visibleWidth(titlePlain) + 2 + visibleWidth(hintsPlain) <= inner) {
191
+ header = title + " ".repeat(inner - visibleWidth(titlePlain) - visibleWidth(hintsPlain)) + hints;
192
+ }
193
+ else {
194
+ header = truncateToWidth(title, inner, "…");
195
+ }
196
+ const rule = theme.fg("borderMuted", "─".repeat(inner));
197
+ // Body: wrap each buffered line, then keep the newest rows that fit the
198
+ // panel's height budget (the freshest output hugs the bottom, like a
199
+ // terminal tail). Budget derives from the live terminal height so the
200
+ // panel never asks the overlay compositor to clip it (clipping drops the
201
+ // bottom — exactly the rows we care about).
202
+ const rows = this.ui?.terminal.rows ?? 24;
203
+ const bodyBudget = Math.max(5, Math.floor(rows * 0.6) - 3);
204
+ const wrapped = [];
205
+ for (const line of this.lines.toArray()) {
206
+ if (line.length === 0) {
207
+ wrapped.push("");
208
+ continue;
209
+ }
210
+ wrapped.push(...wrapTextWithAnsi(line, inner));
211
+ }
212
+ if (this.partial.length > 0) {
213
+ wrapped.push(...wrapTextWithAnsi(this.styledPartial(), inner));
214
+ }
215
+ const body = wrapped.slice(-bodyBudget);
216
+ if (body.length === 0) {
217
+ body.push(theme.fg("dim", "waiting for events…"));
218
+ }
219
+ return [header, rule, ...body, rule];
220
+ }
221
+ }
222
+ //# sourceMappingURL=team-attach-panel.js.map
@@ -0,0 +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"]}
@@ -4,6 +4,7 @@
4
4
  */
5
5
  import { type ImageContent } from "@kolisachint/hoocode-ai";
6
6
  import type { AgentSessionRuntime } from "../../core/agent-session-runtime.js";
7
+ import type { TeamViewConnection } from "../../core/team-view.js";
7
8
  export declare function isApiKeyLoginProvider(providerId: string, oauthProviderIds: ReadonlySet<string>, builtInProviderIds?: ReadonlySet<string>): boolean;
8
9
  /**
9
10
  * Options for InteractiveMode initialization.
@@ -85,6 +86,9 @@ export declare class InteractiveMode {
85
86
  private widgetContainerAbove;
86
87
  private widgetContainerBelow;
87
88
  private taskPanel;
89
+ private teamClient;
90
+ private teamAttachPanel;
91
+ private teamAttachHandle;
88
92
  private customFooter;
89
93
  private headerContainer;
90
94
  private builtInHeader;
@@ -250,6 +254,26 @@ export declare class InteractiveMode {
250
254
  private showExtensionError;
251
255
  private setupKeyHandlers;
252
256
  private handleClipboardImagePaste;
257
+ /**
258
+ * Wire a hooteams connection into the TUI. Called by main.ts when `--team`
259
+ * is set, before run(). Enables team focus (app.team.focus), nudging (n)
260
+ * and the attach side panel (a) on the task panel's teams view.
261
+ */
262
+ attachTeamClient(client: TeamViewConnection): void;
263
+ /** Move keyboard focus to the task panel's team roster. */
264
+ private enterTeamFocus;
265
+ /** Leave team focus: detach any side panel and return focus to the editor. */
266
+ private exitTeamFocus;
267
+ /**
268
+ * Inline nudge editor for a role (n in team focus or while attached).
269
+ * Swaps the prompt editor for a one-line input; submit fires POST /steer in
270
+ * the background (the REPL and team focus are never blocked on the network).
271
+ */
272
+ private showTeamNudgeInput;
273
+ /** Open the attach side panel for a role (a in team focus). */
274
+ private showTeamAttach;
275
+ /** Detach the side panel; the role keeps running. Safe to call when closed. */
276
+ private closeTeamAttach;
253
277
  private setupEditorSubmitHandler;
254
278
  private subscribeToAgent;
255
279
  private handleEvent;