@greppleai/grip 0.1.3 → 0.1.5

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 (62) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/README.md +1 -1
  3. package/dist/core/auth-storage.d.ts.map +1 -1
  4. package/dist/core/auth-storage.js +3 -1
  5. package/dist/core/auth-storage.js.map +1 -1
  6. package/dist/core/extensions/types.d.ts +5 -1
  7. package/dist/core/extensions/types.d.ts.map +1 -1
  8. package/dist/core/extensions/types.js.map +1 -1
  9. package/dist/core/grip-agent.d.ts.map +1 -1
  10. package/dist/core/grip-agent.js +4 -2
  11. package/dist/core/grip-agent.js.map +1 -1
  12. package/dist/core/sandbox/lima.d.ts.map +1 -1
  13. package/dist/core/sandbox/lima.js +6 -6
  14. package/dist/core/sandbox/lima.js.map +1 -1
  15. package/dist/core/tools/find.d.ts.map +1 -1
  16. package/dist/core/tools/find.js +2 -3
  17. package/dist/core/tools/find.js.map +1 -1
  18. package/dist/core/tools/grep.d.ts.map +1 -1
  19. package/dist/core/tools/grep.js +2 -3
  20. package/dist/core/tools/grep.js.map +1 -1
  21. package/dist/core/tools/ls.d.ts.map +1 -1
  22. package/dist/core/tools/ls.js +2 -3
  23. package/dist/core/tools/ls.js.map +1 -1
  24. package/dist/core/tools/read.d.ts.map +1 -1
  25. package/dist/core/tools/read.js +3 -3
  26. package/dist/core/tools/read.js.map +1 -1
  27. package/dist/core/tools/render-utils.d.ts +1 -0
  28. package/dist/core/tools/render-utils.d.ts.map +1 -1
  29. package/dist/core/tools/render-utils.js +5 -0
  30. package/dist/core/tools/render-utils.js.map +1 -1
  31. package/dist/core/tools/write.d.ts.map +1 -1
  32. package/dist/core/tools/write.js +2 -3
  33. package/dist/core/tools/write.js.map +1 -1
  34. package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  35. package/dist/modes/interactive/components/assistant-message.js +18 -7
  36. package/dist/modes/interactive/components/assistant-message.js.map +1 -1
  37. package/dist/modes/interactive/components/hook-execution.d.ts +3 -0
  38. package/dist/modes/interactive/components/hook-execution.d.ts.map +1 -1
  39. package/dist/modes/interactive/components/hook-execution.js +17 -0
  40. package/dist/modes/interactive/components/hook-execution.js.map +1 -1
  41. package/dist/modes/interactive/components/model-selector.d.ts +26 -18
  42. package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  43. package/dist/modes/interactive/components/model-selector.js +220 -192
  44. package/dist/modes/interactive/components/model-selector.js.map +1 -1
  45. package/dist/modes/interactive/interactive-mode.d.ts +1 -0
  46. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  47. package/dist/modes/interactive/interactive-mode.js +118 -8
  48. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  49. package/dist/utils/grip-user-agent.d.ts.map +1 -1
  50. package/dist/utils/grip-user-agent.js +1 -1
  51. package/dist/utils/grip-user-agent.js.map +1 -1
  52. package/docs/extensions.md +3 -0
  53. package/docs/keybindings.md +3 -1
  54. package/docs/providers.md +3 -1
  55. package/docs/sandbox.md +1 -1
  56. package/docs/usage.md +37 -1
  57. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  58. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  59. package/examples/extensions/sandbox/package.json +1 -1
  60. package/examples/extensions/with-deps/package.json +1 -1
  61. package/npm-shrinkwrap.json +12 -12
  62. package/package.json +4 -4
@@ -3,17 +3,20 @@ import type { HookExecutionNotice } from "../../../core/hooks/types.ts";
3
3
  /** TUI-only card for completed hook calls and their model-visible returned prompts. */
4
4
  export declare class HookExecutionComponent extends Container {
5
5
  private readonly notices;
6
+ private readonly annotations;
6
7
  private readonly markdownTheme;
7
8
  private readonly box;
8
9
  private expanded;
9
10
  constructor(notice: HookExecutionNotice, markdownTheme?: MarkdownTheme);
10
11
  appendNotice(notice: HookExecutionNotice): void;
12
+ appendAnnotation(text: string): void;
11
13
  setExpanded(expanded: boolean): void;
12
14
  invalidate(): void;
13
15
  private rebuild;
14
16
  private renderCollapsedNotices;
15
17
  private renderSingleNotice;
16
18
  private renderGroupedNotices;
19
+ private renderAnnotations;
17
20
  private renderPrompts;
18
21
  }
19
22
  //# sourceMappingURL=hook-execution.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"hook-execution.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/hook-execution.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAY,KAAK,aAAa,EAAgB,MAAM,qBAAqB,CAAC;AAC5F,OAAO,KAAK,EAA2B,mBAAmB,EAAE,MAAM,8BAA8B,CAAC;AAmGjG,uFAAuF;AACvF,qBAAa,sBAAuB,SAAQ,SAAS;IACpD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAwB;IAChD,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAgB;IAC9C,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAc;IAClC,OAAO,CAAC,QAAQ,CAAS;IAEzB,YAAY,MAAM,EAAE,mBAAmB,EAAE,aAAa,GAAE,aAAkC,EAYzF;IAED,YAAY,CAAC,MAAM,EAAE,mBAAmB,GAAG,IAAI,CAM9C;IAED,WAAW,CAAC,QAAQ,EAAE,OAAO,GAAG,IAAI,CAInC;IAEQ,UAAU,IAAI,IAAI,CAG1B;IAED,OAAO,CAAC,OAAO;IAYf,OAAO,CAAC,sBAAsB;IAuB9B,OAAO,CAAC,kBAAkB;IAa1B,OAAO,CAAC,oBAAoB;IAoB5B,OAAO,CAAC,aAAa;CAuBrB","sourcesContent":["import { Container, Markdown, type MarkdownTheme, Spacer, Text } from \"@greppleai/grip-tui\";\nimport type { HookExecutionCallNotice, HookExecutionNotice } from \"../../../core/hooks/types.ts\";\nimport { getMarkdownTheme, type ThemeColor, theme } from \"../theme/theme.ts\";\nimport { keyHint, keyText } from \"./keybinding-hints.ts\";\nimport { MessageCard } from \"./message-card.ts\";\n\nconst MAX_CALL_LABEL_CHARS = 300;\n\ninterface HookCallView {\n\tlabel: string;\n\tsource: string;\n\tstatus: string;\n\tstatusColor: ThemeColor;\n}\n\ninterface CountedValue {\n\tvalue: string;\n\tcount: number;\n}\n\ninterface GroupedHookCall {\n\tcall: HookExecutionCallNotice;\n\tinvocationCount: number;\n\ttotalDurationMs: number;\n\tstatuses: CountedValue[];\n}\n\nfunction truncateCallLabel(label: string): string {\n\treturn label.length <= MAX_CALL_LABEL_CHARS ? label : `${label.slice(0, MAX_CALL_LABEL_CHARS - 1)}…`;\n}\n\nfunction hookCallView(call: HookExecutionCallNotice): HookCallView {\n\tconst successful = call.status === \"completed\" && (call.exitCode === 0 || call.exitCode === null);\n\tconst exit = call.exitCode === null ? \"\" : `, exit ${call.exitCode}`;\n\treturn {\n\t\tlabel: `${call.type} ${truncateCallLabel(call.label)}`,\n\t\tsource: `${call.source.kind}: ${call.source.path}`,\n\t\tstatus: `${call.status}${exit}, ${call.durationMs}ms`,\n\t\tstatusColor: successful ? \"success\" : call.status === \"cancelled\" ? \"warning\" : \"error\",\n\t};\n}\n\nfunction callIdentity(call: HookExecutionCallNotice): string {\n\treturn JSON.stringify([call.type, truncateCallLabel(call.label), call.source.kind, call.source.path]);\n}\n\nfunction statusIdentity(call: HookExecutionCallNotice): string {\n\treturn call.exitCode === null ? call.status : `${call.status} (exit ${call.exitCode})`;\n}\n\nfunction countValues(values: Iterable<string>): CountedValue[] {\n\tconst counts = new Map<string, CountedValue>();\n\tfor (const value of values) {\n\t\tconst existing = counts.get(value);\n\t\tif (existing) {\n\t\t\texisting.count += 1;\n\t\t} else {\n\t\t\tcounts.set(value, { value, count: 1 });\n\t\t}\n\t}\n\treturn [...counts.values()];\n}\n\nfunction groupedHookCalls(notices: readonly HookExecutionNotice[]): GroupedHookCall[] {\n\tconst groups = new Map<string, GroupedHookCall>();\n\tfor (const notice of notices) {\n\t\tfor (const call of notice.calls) {\n\t\t\tconst identity = callIdentity(call);\n\t\t\tconst status = statusIdentity(call);\n\t\t\tconst existing = groups.get(identity);\n\t\t\tif (existing) {\n\t\t\t\texisting.invocationCount += 1;\n\t\t\t\texisting.totalDurationMs += call.durationMs;\n\t\t\t\tconst existingStatus = existing.statuses.find((entry) => entry.value === status);\n\t\t\t\tif (existingStatus) existingStatus.count += 1;\n\t\t\t\telse existing.statuses.push({ value: status, count: 1 });\n\t\t\t} else {\n\t\t\t\tgroups.set(identity, {\n\t\t\t\t\tcall,\n\t\t\t\t\tinvocationCount: 1,\n\t\t\t\t\ttotalDurationMs: call.durationMs,\n\t\t\t\t\tstatuses: [{ value: status, count: 1 }],\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n\treturn [...groups.values()];\n}\n\nfunction groupedCallStatus(group: GroupedHookCall): string {\n\tconst statuses = group.statuses.map(({ value, count }) => `${value}${count > 1 ? ` ×${count}` : \"\"}`).join(\"; \");\n\treturn `${statuses}, ~${(group.totalDurationMs / group.invocationCount).toFixed(1)}ms`;\n}\n\nfunction groupedCallStatusColor(group: GroupedHookCall): ThemeColor {\n\tif (group.statuses.every(({ value }) => value === \"completed\" || value === \"completed (exit 0)\")) return \"success\";\n\tif (group.statuses.every(({ value }) => value.startsWith(\"cancelled\"))) return \"warning\";\n\treturn \"error\";\n}\n\n/** TUI-only card for completed hook calls and their model-visible returned prompts. */\nexport class HookExecutionComponent extends Container {\n\tprivate readonly notices: HookExecutionNotice[];\n\tprivate readonly markdownTheme: MarkdownTheme;\n\tprivate readonly box: MessageCard;\n\tprivate expanded = false;\n\n\tconstructor(notice: HookExecutionNotice, markdownTheme: MarkdownTheme = getMarkdownTheme()) {\n\t\tsuper();\n\t\tthis.notices = [notice];\n\t\tthis.markdownTheme = markdownTheme;\n\t\tthis.box = new MessageCard(\n\t\t\t1,\n\t\t\t1,\n\t\t\tnotice.event === \"Stop\" || notice.event === \"StopFailure\" ? \"warning\" : undefined,\n\t\t);\n\t\tthis.addChild(new Spacer(1));\n\t\tthis.addChild(this.box);\n\t\tthis.rebuild();\n\t}\n\n\tappendNotice(notice: HookExecutionNotice): void {\n\t\tif (notice.event !== this.notices[0].event) {\n\t\t\tthrow new Error(`Cannot group ${notice.event} notice with ${this.notices[0].event}`);\n\t\t}\n\t\tthis.notices.push(notice);\n\t\tthis.rebuild();\n\t}\n\n\tsetExpanded(expanded: boolean): void {\n\t\tif (this.expanded === expanded) return;\n\t\tthis.expanded = expanded;\n\t\tthis.rebuild();\n\t}\n\n\toverride invalidate(): void {\n\t\tsuper.invalidate();\n\t\tthis.rebuild();\n\t}\n\n\tprivate rebuild(): void {\n\t\tthis.box.clear();\n\t\tconst firstNotice = this.notices[0];\n\t\tif (!this.expanded) {\n\t\t\tthis.renderCollapsedNotices(firstNotice);\n\t\t} else if (this.notices.length === 1) {\n\t\t\tthis.renderSingleNotice(firstNotice);\n\t\t} else {\n\t\t\tthis.renderGroupedNotices(firstNotice);\n\t\t}\n\t}\n\n\tprivate renderCollapsedNotices(firstNotice: HookExecutionNotice): void {\n\t\tconst subjects = countValues(this.notices.flatMap((notice) => (notice.subject ? [notice.subject] : [])));\n\t\tconst subject = subjects.map(({ value, count }) => `${value}${count > 1 ? ` ×${count}` : \"\"}`).join(\", \");\n\t\tconst title = theme.fg(\"warning\", theme.bold(`Hook · ${firstNotice.event}`));\n\t\tthis.box.addChild(new Text(title + (subject ? theme.fg(\"muted\", ` · ${subject}`) : \"\"), 0, 0));\n\t\tconst statuses = countValues(\n\t\t\tthis.notices.flatMap((notice) =>\n\t\t\t\tnotice.calls.map((call) => {\n\t\t\t\t\tconst successful = call.status === \"completed\" && (call.exitCode === 0 || call.exitCode === null);\n\t\t\t\t\treturn successful ? \"completed\" : call.status === \"cancelled\" ? \"cancelled\" : \"failed\";\n\t\t\t\t}),\n\t\t\t),\n\t\t);\n\t\tconst summary = statuses\n\t\t\t.map(({ value, count }) => {\n\t\t\t\tconst color = value === \"completed\" ? \"success\" : value === \"cancelled\" ? \"warning\" : \"error\";\n\t\t\t\treturn theme.fg(color, `${count} ${value}`);\n\t\t\t})\n\t\t\t.join(theme.fg(\"muted\", \", \"));\n\t\tthis.box.addChild(new Text(summary || theme.fg(\"muted\", \"0 calls\"), 0, 0));\n\t\tif (keyText(\"app.tools.expand\")) this.box.addChild(new Text(keyHint(\"app.tools.expand\", \"to expand\"), 0, 0));\n\t}\n\n\tprivate renderSingleNotice(notice: HookExecutionNotice): void {\n\t\tconst subject = notice.subject ? theme.fg(\"muted\", ` · ${notice.subject}`) : \"\";\n\t\tthis.box.addChild(new Text(theme.fg(\"warning\", theme.bold(`Hook · ${notice.event}`)) + subject, 0, 0));\n\t\tthis.box.addChild(new Spacer(1));\n\n\t\tfor (const call of notice.calls.map(hookCallView)) {\n\t\t\tconst status = theme.fg(call.statusColor, call.status);\n\t\t\tthis.box.addChild(new Text(`${theme.fg(\"text\", call.label)} ${status}`, 0, 0));\n\t\t\tthis.box.addChild(new Text(theme.fg(\"dim\", call.source), 0, 0));\n\t\t}\n\t\tthis.renderPrompts(countValues(notice.returnedPrompts), false);\n\t}\n\n\tprivate renderGroupedNotices(firstNotice: HookExecutionNotice): void {\n\t\tthis.box.addChild(new Text(theme.fg(\"warning\", theme.bold(`Hook · ${firstNotice.event}`)), 0, 0));\n\t\tconst subjects = countValues(\n\t\t\tthis.notices.flatMap((notice) => (notice.subject === undefined ? [] : [notice.subject])),\n\t\t);\n\t\tif (subjects.length > 0) {\n\t\t\tconst summary = subjects.map(({ value, count }) => `${value}${count > 1 ? ` ×${count}` : \"\"}`).join(\", \");\n\t\t\tthis.box.addChild(new Text(theme.fg(\"muted\", `Tools: ${summary}`), 0, 0));\n\t\t}\n\t\tthis.box.addChild(new Spacer(1));\n\n\t\tfor (const group of groupedHookCalls(this.notices)) {\n\t\t\tconst call = hookCallView(group.call);\n\t\t\tconst status = theme.fg(groupedCallStatusColor(group), groupedCallStatus(group));\n\t\t\tthis.box.addChild(new Text(`${theme.fg(\"text\", call.label)} ${status}`, 0, 0));\n\t\t\tthis.box.addChild(new Text(theme.fg(\"dim\", call.source), 0, 0));\n\t\t}\n\t\tthis.renderPrompts(countValues(this.notices.flatMap((notice) => notice.returnedPrompts)), true);\n\t}\n\n\tprivate renderPrompts(prompts: readonly CountedValue[], grouped: boolean): void {\n\t\tif (prompts.length === 0) return;\n\t\tthis.box.addChild(new Spacer(1));\n\t\tthis.box.addChild(\n\t\t\tnew Text(\n\t\t\t\ttheme.fg(\"warning\", theme.bold(prompts.length === 1 && !grouped ? \"Returned prompt\" : \"Returned prompts\")),\n\t\t\t\t0,\n\t\t\t\t0,\n\t\t\t),\n\t\t);\n\t\tfor (let index = 0; index < prompts.length; index++) {\n\t\t\tconst prompt = prompts[index];\n\t\t\tif (prompts.length > 1 || grouped) {\n\t\t\t\tconst count = prompt.count > 1 ? ` ×${prompt.count}` : \"\";\n\t\t\t\tthis.box.addChild(new Text(theme.fg(\"muted\", `Prompt ${index + 1}${count}`), 0, 0));\n\t\t\t}\n\t\t\tthis.box.addChild(\n\t\t\t\tnew Markdown(prompt.value, 0, 0, this.markdownTheme, {\n\t\t\t\t\tcolor: (text: string) => theme.fg(\"text\", text),\n\t\t\t\t}),\n\t\t\t);\n\t\t}\n\t}\n}\n"]}
1
+ {"version":3,"file":"hook-execution.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/hook-execution.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAY,KAAK,aAAa,EAAgB,MAAM,qBAAqB,CAAC;AAC5F,OAAO,KAAK,EAA2B,mBAAmB,EAAE,MAAM,8BAA8B,CAAC;AAmGjG,uFAAuF;AACvF,qBAAa,sBAAuB,SAAQ,SAAS;IACpD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAwB;IAChD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAgB;IAC5C,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAgB;IAC9C,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAc;IAClC,OAAO,CAAC,QAAQ,CAAS;IAEzB,YAAY,MAAM,EAAE,mBAAmB,EAAE,aAAa,GAAE,aAAkC,EAYzF;IAED,YAAY,CAAC,MAAM,EAAE,mBAAmB,GAAG,IAAI,CAM9C;IAED,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAGnC;IAED,WAAW,CAAC,QAAQ,EAAE,OAAO,GAAG,IAAI,CAInC;IAEQ,UAAU,IAAI,IAAI,CAG1B;IAED,OAAO,CAAC,OAAO;IAYf,OAAO,CAAC,sBAAsB;IAyB9B,OAAO,CAAC,kBAAkB;IAc1B,OAAO,CAAC,oBAAoB;IAoB5B,OAAO,CAAC,iBAAiB;IAMzB,OAAO,CAAC,aAAa;CAuBrB","sourcesContent":["import { Container, Markdown, type MarkdownTheme, Spacer, Text } from \"@greppleai/grip-tui\";\nimport type { HookExecutionCallNotice, HookExecutionNotice } from \"../../../core/hooks/types.ts\";\nimport { getMarkdownTheme, type ThemeColor, theme } from \"../theme/theme.ts\";\nimport { keyHint, keyText } from \"./keybinding-hints.ts\";\nimport { MessageCard } from \"./message-card.ts\";\n\nconst MAX_CALL_LABEL_CHARS = 300;\n\ninterface HookCallView {\n\tlabel: string;\n\tsource: string;\n\tstatus: string;\n\tstatusColor: ThemeColor;\n}\n\ninterface CountedValue {\n\tvalue: string;\n\tcount: number;\n}\n\ninterface GroupedHookCall {\n\tcall: HookExecutionCallNotice;\n\tinvocationCount: number;\n\ttotalDurationMs: number;\n\tstatuses: CountedValue[];\n}\n\nfunction truncateCallLabel(label: string): string {\n\treturn label.length <= MAX_CALL_LABEL_CHARS ? label : `${label.slice(0, MAX_CALL_LABEL_CHARS - 1)}…`;\n}\n\nfunction hookCallView(call: HookExecutionCallNotice): HookCallView {\n\tconst successful = call.status === \"completed\" && (call.exitCode === 0 || call.exitCode === null);\n\tconst exit = call.exitCode === null ? \"\" : `, exit ${call.exitCode}`;\n\treturn {\n\t\tlabel: `${call.type} ${truncateCallLabel(call.label)}`,\n\t\tsource: `${call.source.kind}: ${call.source.path}`,\n\t\tstatus: `${call.status}${exit}, ${call.durationMs}ms`,\n\t\tstatusColor: successful ? \"success\" : call.status === \"cancelled\" ? \"warning\" : \"error\",\n\t};\n}\n\nfunction callIdentity(call: HookExecutionCallNotice): string {\n\treturn JSON.stringify([call.type, truncateCallLabel(call.label), call.source.kind, call.source.path]);\n}\n\nfunction statusIdentity(call: HookExecutionCallNotice): string {\n\treturn call.exitCode === null ? call.status : `${call.status} (exit ${call.exitCode})`;\n}\n\nfunction countValues(values: Iterable<string>): CountedValue[] {\n\tconst counts = new Map<string, CountedValue>();\n\tfor (const value of values) {\n\t\tconst existing = counts.get(value);\n\t\tif (existing) {\n\t\t\texisting.count += 1;\n\t\t} else {\n\t\t\tcounts.set(value, { value, count: 1 });\n\t\t}\n\t}\n\treturn [...counts.values()];\n}\n\nfunction groupedHookCalls(notices: readonly HookExecutionNotice[]): GroupedHookCall[] {\n\tconst groups = new Map<string, GroupedHookCall>();\n\tfor (const notice of notices) {\n\t\tfor (const call of notice.calls) {\n\t\t\tconst identity = callIdentity(call);\n\t\t\tconst status = statusIdentity(call);\n\t\t\tconst existing = groups.get(identity);\n\t\t\tif (existing) {\n\t\t\t\texisting.invocationCount += 1;\n\t\t\t\texisting.totalDurationMs += call.durationMs;\n\t\t\t\tconst existingStatus = existing.statuses.find((entry) => entry.value === status);\n\t\t\t\tif (existingStatus) existingStatus.count += 1;\n\t\t\t\telse existing.statuses.push({ value: status, count: 1 });\n\t\t\t} else {\n\t\t\t\tgroups.set(identity, {\n\t\t\t\t\tcall,\n\t\t\t\t\tinvocationCount: 1,\n\t\t\t\t\ttotalDurationMs: call.durationMs,\n\t\t\t\t\tstatuses: [{ value: status, count: 1 }],\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n\treturn [...groups.values()];\n}\n\nfunction groupedCallStatus(group: GroupedHookCall): string {\n\tconst statuses = group.statuses.map(({ value, count }) => `${value}${count > 1 ? ` ×${count}` : \"\"}`).join(\"; \");\n\treturn `${statuses}, ~${(group.totalDurationMs / group.invocationCount).toFixed(1)}ms`;\n}\n\nfunction groupedCallStatusColor(group: GroupedHookCall): ThemeColor {\n\tif (group.statuses.every(({ value }) => value === \"completed\" || value === \"completed (exit 0)\")) return \"success\";\n\tif (group.statuses.every(({ value }) => value.startsWith(\"cancelled\"))) return \"warning\";\n\treturn \"error\";\n}\n\n/** TUI-only card for completed hook calls and their model-visible returned prompts. */\nexport class HookExecutionComponent extends Container {\n\tprivate readonly notices: HookExecutionNotice[];\n\tprivate readonly annotations: string[] = [];\n\tprivate readonly markdownTheme: MarkdownTheme;\n\tprivate readonly box: MessageCard;\n\tprivate expanded = false;\n\n\tconstructor(notice: HookExecutionNotice, markdownTheme: MarkdownTheme = getMarkdownTheme()) {\n\t\tsuper();\n\t\tthis.notices = [notice];\n\t\tthis.markdownTheme = markdownTheme;\n\t\tthis.box = new MessageCard(\n\t\t\t1,\n\t\t\t1,\n\t\t\tnotice.event === \"Stop\" || notice.event === \"StopFailure\" ? \"warning\" : undefined,\n\t\t);\n\t\tthis.addChild(new Spacer(1));\n\t\tthis.addChild(this.box);\n\t\tthis.rebuild();\n\t}\n\n\tappendNotice(notice: HookExecutionNotice): void {\n\t\tif (notice.event !== this.notices[0].event) {\n\t\t\tthrow new Error(`Cannot group ${notice.event} notice with ${this.notices[0].event}`);\n\t\t}\n\t\tthis.notices.push(notice);\n\t\tthis.rebuild();\n\t}\n\n\tappendAnnotation(text: string): void {\n\t\tthis.annotations.push(text);\n\t\tthis.rebuild();\n\t}\n\n\tsetExpanded(expanded: boolean): void {\n\t\tif (this.expanded === expanded) return;\n\t\tthis.expanded = expanded;\n\t\tthis.rebuild();\n\t}\n\n\toverride invalidate(): void {\n\t\tsuper.invalidate();\n\t\tthis.rebuild();\n\t}\n\n\tprivate rebuild(): void {\n\t\tthis.box.clear();\n\t\tconst firstNotice = this.notices[0];\n\t\tif (!this.expanded) {\n\t\t\tthis.renderCollapsedNotices(firstNotice);\n\t\t} else if (this.notices.length === 1) {\n\t\t\tthis.renderSingleNotice(firstNotice);\n\t\t} else {\n\t\t\tthis.renderGroupedNotices(firstNotice);\n\t\t}\n\t}\n\n\tprivate renderCollapsedNotices(firstNotice: HookExecutionNotice): void {\n\t\tconst subjects = countValues(this.notices.flatMap((notice) => (notice.subject ? [notice.subject] : [])));\n\t\tconst subject = subjects.map(({ value, count }) => `${value}${count > 1 ? ` ×${count}` : \"\"}`).join(\", \");\n\t\tconst title = theme.fg(\"warning\", theme.bold(`Hook · ${firstNotice.event}`));\n\t\tthis.box.addChild(new Text(title + (subject ? theme.fg(\"muted\", ` · ${subject}`) : \"\"), 0, 0));\n\t\tif (firstNotice.event === \"Stop\" || firstNotice.event === \"StopFailure\") this.box.addChild(new Spacer(1));\n\t\tconst statuses = countValues(\n\t\t\tthis.notices.flatMap((notice) =>\n\t\t\t\tnotice.calls.map((call) => {\n\t\t\t\t\tconst successful = call.status === \"completed\" && (call.exitCode === 0 || call.exitCode === null);\n\t\t\t\t\treturn successful ? \"completed\" : call.status === \"cancelled\" ? \"cancelled\" : \"failed\";\n\t\t\t\t}),\n\t\t\t),\n\t\t);\n\t\tconst summary = statuses\n\t\t\t.map(({ value, count }) => {\n\t\t\t\tconst color = value === \"completed\" ? \"success\" : value === \"cancelled\" ? \"warning\" : \"error\";\n\t\t\t\treturn theme.fg(color, `${count} ${value}`);\n\t\t\t})\n\t\t\t.join(theme.fg(\"muted\", \", \"));\n\t\tthis.box.addChild(new Text(summary || theme.fg(\"muted\", \"0 calls\"), 0, 0));\n\t\tthis.renderAnnotations();\n\t\tif (keyText(\"app.tools.expand\")) this.box.addChild(new Text(keyHint(\"app.tools.expand\", \"to expand\"), 0, 0));\n\t}\n\n\tprivate renderSingleNotice(notice: HookExecutionNotice): void {\n\t\tconst subject = notice.subject ? theme.fg(\"muted\", ` · ${notice.subject}`) : \"\";\n\t\tthis.box.addChild(new Text(theme.fg(\"warning\", theme.bold(`Hook · ${notice.event}`)) + subject, 0, 0));\n\t\tthis.box.addChild(new Spacer(1));\n\n\t\tfor (const call of notice.calls.map(hookCallView)) {\n\t\t\tconst status = theme.fg(call.statusColor, call.status);\n\t\t\tthis.box.addChild(new Text(`${theme.fg(\"text\", call.label)} ${status}`, 0, 0));\n\t\t\tthis.box.addChild(new Text(theme.fg(\"dim\", call.source), 0, 0));\n\t\t}\n\t\tthis.renderPrompts(countValues(notice.returnedPrompts), false);\n\t\tthis.renderAnnotations();\n\t}\n\n\tprivate renderGroupedNotices(firstNotice: HookExecutionNotice): void {\n\t\tthis.box.addChild(new Text(theme.fg(\"warning\", theme.bold(`Hook · ${firstNotice.event}`)), 0, 0));\n\t\tconst subjects = countValues(\n\t\t\tthis.notices.flatMap((notice) => (notice.subject === undefined ? [] : [notice.subject])),\n\t\t);\n\t\tif (subjects.length > 0) {\n\t\t\tconst summary = subjects.map(({ value, count }) => `${value}${count > 1 ? ` ×${count}` : \"\"}`).join(\", \");\n\t\t\tthis.box.addChild(new Text(theme.fg(\"muted\", `Tools: ${summary}`), 0, 0));\n\t\t}\n\t\tthis.box.addChild(new Spacer(1));\n\n\t\tfor (const group of groupedHookCalls(this.notices)) {\n\t\t\tconst call = hookCallView(group.call);\n\t\t\tconst status = theme.fg(groupedCallStatusColor(group), groupedCallStatus(group));\n\t\t\tthis.box.addChild(new Text(`${theme.fg(\"text\", call.label)} ${status}`, 0, 0));\n\t\t\tthis.box.addChild(new Text(theme.fg(\"dim\", call.source), 0, 0));\n\t\t}\n\t\tthis.renderPrompts(countValues(this.notices.flatMap((notice) => notice.returnedPrompts)), true);\n\t\tthis.renderAnnotations();\n\t}\n\tprivate renderAnnotations(): void {\n\t\tif (this.annotations.length === 0) return;\n\t\tthis.box.addChild(new Spacer(1));\n\t\tfor (const annotation of this.annotations) this.box.addChild(new Text(theme.fg(\"dim\", annotation), 0, 0));\n\t}\n\n\tprivate renderPrompts(prompts: readonly CountedValue[], grouped: boolean): void {\n\t\tif (prompts.length === 0) return;\n\t\tthis.box.addChild(new Spacer(1));\n\t\tthis.box.addChild(\n\t\t\tnew Text(\n\t\t\t\ttheme.fg(\"warning\", theme.bold(prompts.length === 1 && !grouped ? \"Returned prompt\" : \"Returned prompts\")),\n\t\t\t\t0,\n\t\t\t\t0,\n\t\t\t),\n\t\t);\n\t\tfor (let index = 0; index < prompts.length; index++) {\n\t\t\tconst prompt = prompts[index];\n\t\t\tif (prompts.length > 1 || grouped) {\n\t\t\t\tconst count = prompt.count > 1 ? ` ×${prompt.count}` : \"\";\n\t\t\t\tthis.box.addChild(new Text(theme.fg(\"muted\", `Prompt ${index + 1}${count}`), 0, 0));\n\t\t\t}\n\t\t\tthis.box.addChild(\n\t\t\t\tnew Markdown(prompt.value, 0, 0, this.markdownTheme, {\n\t\t\t\t\tcolor: (text: string) => theme.fg(\"text\", text),\n\t\t\t\t}),\n\t\t\t);\n\t\t}\n\t}\n}\n"]}
@@ -77,6 +77,7 @@ function groupedCallStatusColor(group) {
77
77
  /** TUI-only card for completed hook calls and their model-visible returned prompts. */
78
78
  export class HookExecutionComponent extends Container {
79
79
  notices;
80
+ annotations = [];
80
81
  markdownTheme;
81
82
  box;
82
83
  expanded = false;
@@ -96,6 +97,10 @@ export class HookExecutionComponent extends Container {
96
97
  this.notices.push(notice);
97
98
  this.rebuild();
98
99
  }
100
+ appendAnnotation(text) {
101
+ this.annotations.push(text);
102
+ this.rebuild();
103
+ }
99
104
  setExpanded(expanded) {
100
105
  if (this.expanded === expanded)
101
106
  return;
@@ -124,6 +129,8 @@ export class HookExecutionComponent extends Container {
124
129
  const subject = subjects.map(({ value, count }) => `${value}${count > 1 ? ` ×${count}` : ""}`).join(", ");
125
130
  const title = theme.fg("warning", theme.bold(`Hook · ${firstNotice.event}`));
126
131
  this.box.addChild(new Text(title + (subject ? theme.fg("muted", ` · ${subject}`) : ""), 0, 0));
132
+ if (firstNotice.event === "Stop" || firstNotice.event === "StopFailure")
133
+ this.box.addChild(new Spacer(1));
127
134
  const statuses = countValues(this.notices.flatMap((notice) => notice.calls.map((call) => {
128
135
  const successful = call.status === "completed" && (call.exitCode === 0 || call.exitCode === null);
129
136
  return successful ? "completed" : call.status === "cancelled" ? "cancelled" : "failed";
@@ -135,6 +142,7 @@ export class HookExecutionComponent extends Container {
135
142
  })
136
143
  .join(theme.fg("muted", ", "));
137
144
  this.box.addChild(new Text(summary || theme.fg("muted", "0 calls"), 0, 0));
145
+ this.renderAnnotations();
138
146
  if (keyText("app.tools.expand"))
139
147
  this.box.addChild(new Text(keyHint("app.tools.expand", "to expand"), 0, 0));
140
148
  }
@@ -148,6 +156,7 @@ export class HookExecutionComponent extends Container {
148
156
  this.box.addChild(new Text(theme.fg("dim", call.source), 0, 0));
149
157
  }
150
158
  this.renderPrompts(countValues(notice.returnedPrompts), false);
159
+ this.renderAnnotations();
151
160
  }
152
161
  renderGroupedNotices(firstNotice) {
153
162
  this.box.addChild(new Text(theme.fg("warning", theme.bold(`Hook · ${firstNotice.event}`)), 0, 0));
@@ -164,6 +173,14 @@ export class HookExecutionComponent extends Container {
164
173
  this.box.addChild(new Text(theme.fg("dim", call.source), 0, 0));
165
174
  }
166
175
  this.renderPrompts(countValues(this.notices.flatMap((notice) => notice.returnedPrompts)), true);
176
+ this.renderAnnotations();
177
+ }
178
+ renderAnnotations() {
179
+ if (this.annotations.length === 0)
180
+ return;
181
+ this.box.addChild(new Spacer(1));
182
+ for (const annotation of this.annotations)
183
+ this.box.addChild(new Text(theme.fg("dim", annotation), 0, 0));
167
184
  }
168
185
  renderPrompts(prompts, grouped) {
169
186
  if (prompts.length === 0)
@@ -1 +1 @@
1
- {"version":3,"file":"hook-execution.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/hook-execution.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAsB,MAAM,EAAE,IAAI,EAAE,MAAM,qBAAqB,CAAC;AAE5F,OAAO,EAAE,gBAAgB,EAAmB,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAC7E,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAEhD,MAAM,oBAAoB,GAAG,GAAG,CAAC;AAqBjC,SAAS,iBAAiB,CAAC,KAAa,EAAU;IACjD,OAAO,KAAK,CAAC,MAAM,IAAI,oBAAoB,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,oBAAoB,GAAG,CAAC,CAAC,KAAG,CAAC;AAAA,CACrG;AAED,SAAS,YAAY,CAAC,IAA6B,EAAgB;IAClE,MAAM,UAAU,GAAG,IAAI,CAAC,MAAM,KAAK,WAAW,IAAI,CAAC,IAAI,CAAC,QAAQ,KAAK,CAAC,IAAI,IAAI,CAAC,QAAQ,KAAK,IAAI,CAAC,CAAC;IAClG,MAAM,IAAI,GAAG,IAAI,CAAC,QAAQ,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,UAAU,IAAI,CAAC,QAAQ,EAAE,CAAC;IACrE,OAAO;QACN,KAAK,EAAE,GAAG,IAAI,CAAC,IAAI,IAAI,iBAAiB,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE;QACtD,MAAM,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,KAAK,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE;QAClD,MAAM,EAAE,GAAG,IAAI,CAAC,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC,UAAU,IAAI;QACrD,WAAW,EAAE,UAAU,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,KAAK,WAAW,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO;KACvF,CAAC;AAAA,CACF;AAED,SAAS,YAAY,CAAC,IAA6B,EAAU;IAC5D,OAAO,IAAI,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,iBAAiB,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC;AAAA,CACtG;AAED,SAAS,cAAc,CAAC,IAA6B,EAAU;IAC9D,OAAO,IAAI,CAAC,QAAQ,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,MAAM,UAAU,IAAI,CAAC,QAAQ,GAAG,CAAC;AAAA,CACvF;AAED,SAAS,WAAW,CAAC,MAAwB,EAAkB;IAC9D,MAAM,MAAM,GAAG,IAAI,GAAG,EAAwB,CAAC;IAC/C,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC5B,MAAM,QAAQ,GAAG,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QACnC,IAAI,QAAQ,EAAE,CAAC;YACd,QAAQ,CAAC,KAAK,IAAI,CAAC,CAAC;QACrB,CAAC;aAAM,CAAC;YACP,MAAM,CAAC,GAAG,CAAC,KAAK,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC;QACxC,CAAC;IACF,CAAC;IACD,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC;AAAA,CAC5B;AAED,SAAS,gBAAgB,CAAC,OAAuC,EAAqB;IACrF,MAAM,MAAM,GAAG,IAAI,GAAG,EAA2B,CAAC;IAClD,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC9B,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;YACjC,MAAM,QAAQ,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;YACpC,MAAM,MAAM,GAAG,cAAc,CAAC,IAAI,CAAC,CAAC;YACpC,MAAM,QAAQ,GAAG,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;YACtC,IAAI,QAAQ,EAAE,CAAC;gBACd,QAAQ,CAAC,eAAe,IAAI,CAAC,CAAC;gBAC9B,QAAQ,CAAC,eAAe,IAAI,IAAI,CAAC,UAAU,CAAC;gBAC5C,MAAM,cAAc,GAAG,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,KAAK,KAAK,MAAM,CAAC,CAAC;gBACjF,IAAI,cAAc;oBAAE,cAAc,CAAC,KAAK,IAAI,CAAC,CAAC;;oBACzC,QAAQ,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC;YAC1D,CAAC;iBAAM,CAAC;gBACP,MAAM,CAAC,GAAG,CAAC,QAAQ,EAAE;oBACpB,IAAI;oBACJ,eAAe,EAAE,CAAC;oBAClB,eAAe,EAAE,IAAI,CAAC,UAAU;oBAChC,QAAQ,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC;iBACvC,CAAC,CAAC;YACJ,CAAC;QACF,CAAC;IACF,CAAC;IACD,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC;AAAA,CAC5B;AAED,SAAS,iBAAiB,CAAC,KAAsB,EAAU;IAC1D,MAAM,QAAQ,GAAG,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,GAAG,KAAK,GAAG,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,MAAK,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACjH,OAAO,GAAG,QAAQ,MAAM,CAAC,KAAK,CAAC,eAAe,GAAG,KAAK,CAAC,eAAe,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC;AAAA,CACvF;AAED,SAAS,sBAAsB,CAAC,KAAsB,EAAc;IACnE,IAAI,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,KAAK,KAAK,WAAW,IAAI,KAAK,KAAK,oBAAoB,CAAC;QAAE,OAAO,SAAS,CAAC;IACnH,IAAI,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,KAAK,CAAC,UAAU,CAAC,WAAW,CAAC,CAAC;QAAE,OAAO,SAAS,CAAC;IACzF,OAAO,OAAO,CAAC;AAAA,CACf;AAED,uFAAuF;AACvF,MAAM,OAAO,sBAAuB,SAAQ,SAAS;IACnC,OAAO,CAAwB;IAC/B,aAAa,CAAgB;IAC7B,GAAG,CAAc;IAC1B,QAAQ,GAAG,KAAK,CAAC;IAEzB,YAAY,MAA2B,EAAE,aAAa,GAAkB,gBAAgB,EAAE,EAAE;QAC3F,KAAK,EAAE,CAAC;QACR,IAAI,CAAC,OAAO,GAAG,CAAC,MAAM,CAAC,CAAC;QACxB,IAAI,CAAC,aAAa,GAAG,aAAa,CAAC;QACnC,IAAI,CAAC,GAAG,GAAG,IAAI,WAAW,CACzB,CAAC,EACD,CAAC,EACD,MAAM,CAAC,KAAK,KAAK,MAAM,IAAI,MAAM,CAAC,KAAK,KAAK,aAAa,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CACjF,CAAC;QACF,IAAI,CAAC,QAAQ,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;QAC7B,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACxB,IAAI,CAAC,OAAO,EAAE,CAAC;IAAA,CACf;IAED,YAAY,CAAC,MAA2B,EAAQ;QAC/C,IAAI,MAAM,CAAC,KAAK,KAAK,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC;YAC5C,MAAM,IAAI,KAAK,CAAC,gBAAgB,MAAM,CAAC,KAAK,gBAAgB,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC;QACtF,CAAC;QACD,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAC1B,IAAI,CAAC,OAAO,EAAE,CAAC;IAAA,CACf;IAED,WAAW,CAAC,QAAiB,EAAQ;QACpC,IAAI,IAAI,CAAC,QAAQ,KAAK,QAAQ;YAAE,OAAO;QACvC,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,OAAO,EAAE,CAAC;IAAA,CACf;IAEQ,UAAU,GAAS;QAC3B,KAAK,CAAC,UAAU,EAAE,CAAC;QACnB,IAAI,CAAC,OAAO,EAAE,CAAC;IAAA,CACf;IAEO,OAAO,GAAS;QACvB,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC;QACjB,MAAM,WAAW,GAAG,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;QACpC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC;YACpB,IAAI,CAAC,sBAAsB,CAAC,WAAW,CAAC,CAAC;QAC1C,CAAC;aAAM,IAAI,IAAI,CAAC,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACtC,IAAI,CAAC,kBAAkB,CAAC,WAAW,CAAC,CAAC;QACtC,CAAC;aAAM,CAAC;YACP,IAAI,CAAC,oBAAoB,CAAC,WAAW,CAAC,CAAC;QACxC,CAAC;IAAA,CACD;IAEO,sBAAsB,CAAC,WAAgC,EAAQ;QACtE,MAAM,QAAQ,GAAG,WAAW,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QACzG,MAAM,OAAO,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,GAAG,KAAK,GAAG,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,MAAK,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC1G,MAAM,KAAK,GAAG,KAAK,CAAC,EAAE,CAAC,SAAS,EAAE,KAAK,CAAC,IAAI,CAAC,WAAU,WAAW,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;QAC7E,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,KAAK,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,OAAM,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QAC/F,MAAM,QAAQ,GAAG,WAAW,CAC3B,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE,EAAE,CAC/B,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC;YAC1B,MAAM,UAAU,GAAG,IAAI,CAAC,MAAM,KAAK,WAAW,IAAI,CAAC,IAAI,CAAC,QAAQ,KAAK,CAAC,IAAI,IAAI,CAAC,QAAQ,KAAK,IAAI,CAAC,CAAC;YAClG,OAAO,UAAU,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,KAAK,WAAW,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,QAAQ,CAAC;QAAA,CACvF,CAAC,CACF,CACD,CAAC;QACF,MAAM,OAAO,GAAG,QAAQ;aACtB,GAAG,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC;YAC1B,MAAM,KAAK,GAAG,KAAK,KAAK,WAAW,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,KAAK,WAAW,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC;YAC9F,OAAO,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,KAAK,IAAI,KAAK,EAAE,CAAC,CAAC;QAAA,CAC5C,CAAC;aACD,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,CAAC;QAChC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,OAAO,IAAI,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,SAAS,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QAC3E,IAAI,OAAO,CAAC,kBAAkB,CAAC;YAAE,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,OAAO,CAAC,kBAAkB,EAAE,WAAW,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IAAA,CAC7G;IAEO,kBAAkB,CAAC,MAA2B,EAAQ;QAC7D,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,OAAM,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAChF,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,SAAS,EAAE,KAAK,CAAC,IAAI,CAAC,WAAU,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,GAAG,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QACvG,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;QAEjC,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,YAAY,CAAC,EAAE,CAAC;YACnD,MAAM,MAAM,GAAG,KAAK,CAAC,EAAE,CAAC,IAAI,CAAC,WAAW,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;YACvD,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,GAAG,KAAK,CAAC,EAAE,CAAC,MAAM,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,MAAM,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;YAChF,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QACjE,CAAC;QACD,IAAI,CAAC,aAAa,CAAC,WAAW,CAAC,MAAM,CAAC,eAAe,CAAC,EAAE,KAAK,CAAC,CAAC;IAAA,CAC/D;IAEO,oBAAoB,CAAC,WAAgC,EAAQ;QACpE,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,SAAS,EAAE,KAAK,CAAC,IAAI,CAAC,WAAU,WAAW,CAAC,KAAK,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QAClG,MAAM,QAAQ,GAAG,WAAW,CAC3B,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CACxF,CAAC;QACF,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACzB,MAAM,OAAO,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,GAAG,KAAK,GAAG,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,MAAK,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAC1G,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,UAAU,OAAO,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QAC3E,CAAC;QACD,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;QAEjC,KAAK,MAAM,KAAK,IAAI,gBAAgB,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;YACpD,MAAM,IAAI,GAAG,YAAY,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;YACtC,MAAM,MAAM,GAAG,KAAK,CAAC,EAAE,CAAC,sBAAsB,CAAC,KAAK,CAAC,EAAE,iBAAiB,CAAC,KAAK,CAAC,CAAC,CAAC;YACjF,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,GAAG,KAAK,CAAC,EAAE,CAAC,MAAM,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,MAAM,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;YAChF,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QACjE,CAAC;QACD,IAAI,CAAC,aAAa,CAAC,WAAW,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,eAAe,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;IAAA,CAChG;IAEO,aAAa,CAAC,OAAgC,EAAE,OAAgB,EAAQ;QAC/E,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO;QACjC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;QACjC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAChB,IAAI,IAAI,CACP,KAAK,CAAC,EAAE,CAAC,SAAS,EAAE,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,iBAAiB,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAC,EAC1G,CAAC,EACD,CAAC,CACD,CACD,CAAC;QACF,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,OAAO,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,CAAC;YACrD,MAAM,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC;YAC9B,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,IAAI,OAAO,EAAE,CAAC;gBACnC,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,MAAK,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBAC1D,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,UAAU,KAAK,GAAG,CAAC,GAAG,KAAK,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;YACrF,CAAC;YACD,IAAI,CAAC,GAAG,CAAC,QAAQ,CAChB,IAAI,QAAQ,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,EAAE,CAAC,EAAE,IAAI,CAAC,aAAa,EAAE;gBACpD,KAAK,EAAE,CAAC,IAAY,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,MAAM,EAAE,IAAI,CAAC;aAC/C,CAAC,CACF,CAAC;QACH,CAAC;IAAA,CACD;CACD","sourcesContent":["import { Container, Markdown, type MarkdownTheme, Spacer, Text } from \"@greppleai/grip-tui\";\nimport type { HookExecutionCallNotice, HookExecutionNotice } from \"../../../core/hooks/types.ts\";\nimport { getMarkdownTheme, type ThemeColor, theme } from \"../theme/theme.ts\";\nimport { keyHint, keyText } from \"./keybinding-hints.ts\";\nimport { MessageCard } from \"./message-card.ts\";\n\nconst MAX_CALL_LABEL_CHARS = 300;\n\ninterface HookCallView {\n\tlabel: string;\n\tsource: string;\n\tstatus: string;\n\tstatusColor: ThemeColor;\n}\n\ninterface CountedValue {\n\tvalue: string;\n\tcount: number;\n}\n\ninterface GroupedHookCall {\n\tcall: HookExecutionCallNotice;\n\tinvocationCount: number;\n\ttotalDurationMs: number;\n\tstatuses: CountedValue[];\n}\n\nfunction truncateCallLabel(label: string): string {\n\treturn label.length <= MAX_CALL_LABEL_CHARS ? label : `${label.slice(0, MAX_CALL_LABEL_CHARS - 1)}…`;\n}\n\nfunction hookCallView(call: HookExecutionCallNotice): HookCallView {\n\tconst successful = call.status === \"completed\" && (call.exitCode === 0 || call.exitCode === null);\n\tconst exit = call.exitCode === null ? \"\" : `, exit ${call.exitCode}`;\n\treturn {\n\t\tlabel: `${call.type} ${truncateCallLabel(call.label)}`,\n\t\tsource: `${call.source.kind}: ${call.source.path}`,\n\t\tstatus: `${call.status}${exit}, ${call.durationMs}ms`,\n\t\tstatusColor: successful ? \"success\" : call.status === \"cancelled\" ? \"warning\" : \"error\",\n\t};\n}\n\nfunction callIdentity(call: HookExecutionCallNotice): string {\n\treturn JSON.stringify([call.type, truncateCallLabel(call.label), call.source.kind, call.source.path]);\n}\n\nfunction statusIdentity(call: HookExecutionCallNotice): string {\n\treturn call.exitCode === null ? call.status : `${call.status} (exit ${call.exitCode})`;\n}\n\nfunction countValues(values: Iterable<string>): CountedValue[] {\n\tconst counts = new Map<string, CountedValue>();\n\tfor (const value of values) {\n\t\tconst existing = counts.get(value);\n\t\tif (existing) {\n\t\t\texisting.count += 1;\n\t\t} else {\n\t\t\tcounts.set(value, { value, count: 1 });\n\t\t}\n\t}\n\treturn [...counts.values()];\n}\n\nfunction groupedHookCalls(notices: readonly HookExecutionNotice[]): GroupedHookCall[] {\n\tconst groups = new Map<string, GroupedHookCall>();\n\tfor (const notice of notices) {\n\t\tfor (const call of notice.calls) {\n\t\t\tconst identity = callIdentity(call);\n\t\t\tconst status = statusIdentity(call);\n\t\t\tconst existing = groups.get(identity);\n\t\t\tif (existing) {\n\t\t\t\texisting.invocationCount += 1;\n\t\t\t\texisting.totalDurationMs += call.durationMs;\n\t\t\t\tconst existingStatus = existing.statuses.find((entry) => entry.value === status);\n\t\t\t\tif (existingStatus) existingStatus.count += 1;\n\t\t\t\telse existing.statuses.push({ value: status, count: 1 });\n\t\t\t} else {\n\t\t\t\tgroups.set(identity, {\n\t\t\t\t\tcall,\n\t\t\t\t\tinvocationCount: 1,\n\t\t\t\t\ttotalDurationMs: call.durationMs,\n\t\t\t\t\tstatuses: [{ value: status, count: 1 }],\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n\treturn [...groups.values()];\n}\n\nfunction groupedCallStatus(group: GroupedHookCall): string {\n\tconst statuses = group.statuses.map(({ value, count }) => `${value}${count > 1 ? ` ×${count}` : \"\"}`).join(\"; \");\n\treturn `${statuses}, ~${(group.totalDurationMs / group.invocationCount).toFixed(1)}ms`;\n}\n\nfunction groupedCallStatusColor(group: GroupedHookCall): ThemeColor {\n\tif (group.statuses.every(({ value }) => value === \"completed\" || value === \"completed (exit 0)\")) return \"success\";\n\tif (group.statuses.every(({ value }) => value.startsWith(\"cancelled\"))) return \"warning\";\n\treturn \"error\";\n}\n\n/** TUI-only card for completed hook calls and their model-visible returned prompts. */\nexport class HookExecutionComponent extends Container {\n\tprivate readonly notices: HookExecutionNotice[];\n\tprivate readonly markdownTheme: MarkdownTheme;\n\tprivate readonly box: MessageCard;\n\tprivate expanded = false;\n\n\tconstructor(notice: HookExecutionNotice, markdownTheme: MarkdownTheme = getMarkdownTheme()) {\n\t\tsuper();\n\t\tthis.notices = [notice];\n\t\tthis.markdownTheme = markdownTheme;\n\t\tthis.box = new MessageCard(\n\t\t\t1,\n\t\t\t1,\n\t\t\tnotice.event === \"Stop\" || notice.event === \"StopFailure\" ? \"warning\" : undefined,\n\t\t);\n\t\tthis.addChild(new Spacer(1));\n\t\tthis.addChild(this.box);\n\t\tthis.rebuild();\n\t}\n\n\tappendNotice(notice: HookExecutionNotice): void {\n\t\tif (notice.event !== this.notices[0].event) {\n\t\t\tthrow new Error(`Cannot group ${notice.event} notice with ${this.notices[0].event}`);\n\t\t}\n\t\tthis.notices.push(notice);\n\t\tthis.rebuild();\n\t}\n\n\tsetExpanded(expanded: boolean): void {\n\t\tif (this.expanded === expanded) return;\n\t\tthis.expanded = expanded;\n\t\tthis.rebuild();\n\t}\n\n\toverride invalidate(): void {\n\t\tsuper.invalidate();\n\t\tthis.rebuild();\n\t}\n\n\tprivate rebuild(): void {\n\t\tthis.box.clear();\n\t\tconst firstNotice = this.notices[0];\n\t\tif (!this.expanded) {\n\t\t\tthis.renderCollapsedNotices(firstNotice);\n\t\t} else if (this.notices.length === 1) {\n\t\t\tthis.renderSingleNotice(firstNotice);\n\t\t} else {\n\t\t\tthis.renderGroupedNotices(firstNotice);\n\t\t}\n\t}\n\n\tprivate renderCollapsedNotices(firstNotice: HookExecutionNotice): void {\n\t\tconst subjects = countValues(this.notices.flatMap((notice) => (notice.subject ? [notice.subject] : [])));\n\t\tconst subject = subjects.map(({ value, count }) => `${value}${count > 1 ? ` ×${count}` : \"\"}`).join(\", \");\n\t\tconst title = theme.fg(\"warning\", theme.bold(`Hook · ${firstNotice.event}`));\n\t\tthis.box.addChild(new Text(title + (subject ? theme.fg(\"muted\", ` · ${subject}`) : \"\"), 0, 0));\n\t\tconst statuses = countValues(\n\t\t\tthis.notices.flatMap((notice) =>\n\t\t\t\tnotice.calls.map((call) => {\n\t\t\t\t\tconst successful = call.status === \"completed\" && (call.exitCode === 0 || call.exitCode === null);\n\t\t\t\t\treturn successful ? \"completed\" : call.status === \"cancelled\" ? \"cancelled\" : \"failed\";\n\t\t\t\t}),\n\t\t\t),\n\t\t);\n\t\tconst summary = statuses\n\t\t\t.map(({ value, count }) => {\n\t\t\t\tconst color = value === \"completed\" ? \"success\" : value === \"cancelled\" ? \"warning\" : \"error\";\n\t\t\t\treturn theme.fg(color, `${count} ${value}`);\n\t\t\t})\n\t\t\t.join(theme.fg(\"muted\", \", \"));\n\t\tthis.box.addChild(new Text(summary || theme.fg(\"muted\", \"0 calls\"), 0, 0));\n\t\tif (keyText(\"app.tools.expand\")) this.box.addChild(new Text(keyHint(\"app.tools.expand\", \"to expand\"), 0, 0));\n\t}\n\n\tprivate renderSingleNotice(notice: HookExecutionNotice): void {\n\t\tconst subject = notice.subject ? theme.fg(\"muted\", ` · ${notice.subject}`) : \"\";\n\t\tthis.box.addChild(new Text(theme.fg(\"warning\", theme.bold(`Hook · ${notice.event}`)) + subject, 0, 0));\n\t\tthis.box.addChild(new Spacer(1));\n\n\t\tfor (const call of notice.calls.map(hookCallView)) {\n\t\t\tconst status = theme.fg(call.statusColor, call.status);\n\t\t\tthis.box.addChild(new Text(`${theme.fg(\"text\", call.label)} ${status}`, 0, 0));\n\t\t\tthis.box.addChild(new Text(theme.fg(\"dim\", call.source), 0, 0));\n\t\t}\n\t\tthis.renderPrompts(countValues(notice.returnedPrompts), false);\n\t}\n\n\tprivate renderGroupedNotices(firstNotice: HookExecutionNotice): void {\n\t\tthis.box.addChild(new Text(theme.fg(\"warning\", theme.bold(`Hook · ${firstNotice.event}`)), 0, 0));\n\t\tconst subjects = countValues(\n\t\t\tthis.notices.flatMap((notice) => (notice.subject === undefined ? [] : [notice.subject])),\n\t\t);\n\t\tif (subjects.length > 0) {\n\t\t\tconst summary = subjects.map(({ value, count }) => `${value}${count > 1 ? ` ×${count}` : \"\"}`).join(\", \");\n\t\t\tthis.box.addChild(new Text(theme.fg(\"muted\", `Tools: ${summary}`), 0, 0));\n\t\t}\n\t\tthis.box.addChild(new Spacer(1));\n\n\t\tfor (const group of groupedHookCalls(this.notices)) {\n\t\t\tconst call = hookCallView(group.call);\n\t\t\tconst status = theme.fg(groupedCallStatusColor(group), groupedCallStatus(group));\n\t\t\tthis.box.addChild(new Text(`${theme.fg(\"text\", call.label)} ${status}`, 0, 0));\n\t\t\tthis.box.addChild(new Text(theme.fg(\"dim\", call.source), 0, 0));\n\t\t}\n\t\tthis.renderPrompts(countValues(this.notices.flatMap((notice) => notice.returnedPrompts)), true);\n\t}\n\n\tprivate renderPrompts(prompts: readonly CountedValue[], grouped: boolean): void {\n\t\tif (prompts.length === 0) return;\n\t\tthis.box.addChild(new Spacer(1));\n\t\tthis.box.addChild(\n\t\t\tnew Text(\n\t\t\t\ttheme.fg(\"warning\", theme.bold(prompts.length === 1 && !grouped ? \"Returned prompt\" : \"Returned prompts\")),\n\t\t\t\t0,\n\t\t\t\t0,\n\t\t\t),\n\t\t);\n\t\tfor (let index = 0; index < prompts.length; index++) {\n\t\t\tconst prompt = prompts[index];\n\t\t\tif (prompts.length > 1 || grouped) {\n\t\t\t\tconst count = prompt.count > 1 ? ` ×${prompt.count}` : \"\";\n\t\t\t\tthis.box.addChild(new Text(theme.fg(\"muted\", `Prompt ${index + 1}${count}`), 0, 0));\n\t\t\t}\n\t\t\tthis.box.addChild(\n\t\t\t\tnew Markdown(prompt.value, 0, 0, this.markdownTheme, {\n\t\t\t\t\tcolor: (text: string) => theme.fg(\"text\", text),\n\t\t\t\t}),\n\t\t\t);\n\t\t}\n\t}\n}\n"]}
1
+ {"version":3,"file":"hook-execution.js","sourceRoot":"","sources":["../../../../src/modes/interactive/components/hook-execution.ts"],"names":[],"mappings":"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","sourcesContent":["import { Container, Markdown, type MarkdownTheme, Spacer, Text } from \"@greppleai/grip-tui\";\nimport type { HookExecutionCallNotice, HookExecutionNotice } from \"../../../core/hooks/types.ts\";\nimport { getMarkdownTheme, type ThemeColor, theme } from \"../theme/theme.ts\";\nimport { keyHint, keyText } from \"./keybinding-hints.ts\";\nimport { MessageCard } from \"./message-card.ts\";\n\nconst MAX_CALL_LABEL_CHARS = 300;\n\ninterface HookCallView {\n\tlabel: string;\n\tsource: string;\n\tstatus: string;\n\tstatusColor: ThemeColor;\n}\n\ninterface CountedValue {\n\tvalue: string;\n\tcount: number;\n}\n\ninterface GroupedHookCall {\n\tcall: HookExecutionCallNotice;\n\tinvocationCount: number;\n\ttotalDurationMs: number;\n\tstatuses: CountedValue[];\n}\n\nfunction truncateCallLabel(label: string): string {\n\treturn label.length <= MAX_CALL_LABEL_CHARS ? label : `${label.slice(0, MAX_CALL_LABEL_CHARS - 1)}…`;\n}\n\nfunction hookCallView(call: HookExecutionCallNotice): HookCallView {\n\tconst successful = call.status === \"completed\" && (call.exitCode === 0 || call.exitCode === null);\n\tconst exit = call.exitCode === null ? \"\" : `, exit ${call.exitCode}`;\n\treturn {\n\t\tlabel: `${call.type} ${truncateCallLabel(call.label)}`,\n\t\tsource: `${call.source.kind}: ${call.source.path}`,\n\t\tstatus: `${call.status}${exit}, ${call.durationMs}ms`,\n\t\tstatusColor: successful ? \"success\" : call.status === \"cancelled\" ? \"warning\" : \"error\",\n\t};\n}\n\nfunction callIdentity(call: HookExecutionCallNotice): string {\n\treturn JSON.stringify([call.type, truncateCallLabel(call.label), call.source.kind, call.source.path]);\n}\n\nfunction statusIdentity(call: HookExecutionCallNotice): string {\n\treturn call.exitCode === null ? call.status : `${call.status} (exit ${call.exitCode})`;\n}\n\nfunction countValues(values: Iterable<string>): CountedValue[] {\n\tconst counts = new Map<string, CountedValue>();\n\tfor (const value of values) {\n\t\tconst existing = counts.get(value);\n\t\tif (existing) {\n\t\t\texisting.count += 1;\n\t\t} else {\n\t\t\tcounts.set(value, { value, count: 1 });\n\t\t}\n\t}\n\treturn [...counts.values()];\n}\n\nfunction groupedHookCalls(notices: readonly HookExecutionNotice[]): GroupedHookCall[] {\n\tconst groups = new Map<string, GroupedHookCall>();\n\tfor (const notice of notices) {\n\t\tfor (const call of notice.calls) {\n\t\t\tconst identity = callIdentity(call);\n\t\t\tconst status = statusIdentity(call);\n\t\t\tconst existing = groups.get(identity);\n\t\t\tif (existing) {\n\t\t\t\texisting.invocationCount += 1;\n\t\t\t\texisting.totalDurationMs += call.durationMs;\n\t\t\t\tconst existingStatus = existing.statuses.find((entry) => entry.value === status);\n\t\t\t\tif (existingStatus) existingStatus.count += 1;\n\t\t\t\telse existing.statuses.push({ value: status, count: 1 });\n\t\t\t} else {\n\t\t\t\tgroups.set(identity, {\n\t\t\t\t\tcall,\n\t\t\t\t\tinvocationCount: 1,\n\t\t\t\t\ttotalDurationMs: call.durationMs,\n\t\t\t\t\tstatuses: [{ value: status, count: 1 }],\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n\treturn [...groups.values()];\n}\n\nfunction groupedCallStatus(group: GroupedHookCall): string {\n\tconst statuses = group.statuses.map(({ value, count }) => `${value}${count > 1 ? ` ×${count}` : \"\"}`).join(\"; \");\n\treturn `${statuses}, ~${(group.totalDurationMs / group.invocationCount).toFixed(1)}ms`;\n}\n\nfunction groupedCallStatusColor(group: GroupedHookCall): ThemeColor {\n\tif (group.statuses.every(({ value }) => value === \"completed\" || value === \"completed (exit 0)\")) return \"success\";\n\tif (group.statuses.every(({ value }) => value.startsWith(\"cancelled\"))) return \"warning\";\n\treturn \"error\";\n}\n\n/** TUI-only card for completed hook calls and their model-visible returned prompts. */\nexport class HookExecutionComponent extends Container {\n\tprivate readonly notices: HookExecutionNotice[];\n\tprivate readonly annotations: string[] = [];\n\tprivate readonly markdownTheme: MarkdownTheme;\n\tprivate readonly box: MessageCard;\n\tprivate expanded = false;\n\n\tconstructor(notice: HookExecutionNotice, markdownTheme: MarkdownTheme = getMarkdownTheme()) {\n\t\tsuper();\n\t\tthis.notices = [notice];\n\t\tthis.markdownTheme = markdownTheme;\n\t\tthis.box = new MessageCard(\n\t\t\t1,\n\t\t\t1,\n\t\t\tnotice.event === \"Stop\" || notice.event === \"StopFailure\" ? \"warning\" : undefined,\n\t\t);\n\t\tthis.addChild(new Spacer(1));\n\t\tthis.addChild(this.box);\n\t\tthis.rebuild();\n\t}\n\n\tappendNotice(notice: HookExecutionNotice): void {\n\t\tif (notice.event !== this.notices[0].event) {\n\t\t\tthrow new Error(`Cannot group ${notice.event} notice with ${this.notices[0].event}`);\n\t\t}\n\t\tthis.notices.push(notice);\n\t\tthis.rebuild();\n\t}\n\n\tappendAnnotation(text: string): void {\n\t\tthis.annotations.push(text);\n\t\tthis.rebuild();\n\t}\n\n\tsetExpanded(expanded: boolean): void {\n\t\tif (this.expanded === expanded) return;\n\t\tthis.expanded = expanded;\n\t\tthis.rebuild();\n\t}\n\n\toverride invalidate(): void {\n\t\tsuper.invalidate();\n\t\tthis.rebuild();\n\t}\n\n\tprivate rebuild(): void {\n\t\tthis.box.clear();\n\t\tconst firstNotice = this.notices[0];\n\t\tif (!this.expanded) {\n\t\t\tthis.renderCollapsedNotices(firstNotice);\n\t\t} else if (this.notices.length === 1) {\n\t\t\tthis.renderSingleNotice(firstNotice);\n\t\t} else {\n\t\t\tthis.renderGroupedNotices(firstNotice);\n\t\t}\n\t}\n\n\tprivate renderCollapsedNotices(firstNotice: HookExecutionNotice): void {\n\t\tconst subjects = countValues(this.notices.flatMap((notice) => (notice.subject ? [notice.subject] : [])));\n\t\tconst subject = subjects.map(({ value, count }) => `${value}${count > 1 ? ` ×${count}` : \"\"}`).join(\", \");\n\t\tconst title = theme.fg(\"warning\", theme.bold(`Hook · ${firstNotice.event}`));\n\t\tthis.box.addChild(new Text(title + (subject ? theme.fg(\"muted\", ` · ${subject}`) : \"\"), 0, 0));\n\t\tif (firstNotice.event === \"Stop\" || firstNotice.event === \"StopFailure\") this.box.addChild(new Spacer(1));\n\t\tconst statuses = countValues(\n\t\t\tthis.notices.flatMap((notice) =>\n\t\t\t\tnotice.calls.map((call) => {\n\t\t\t\t\tconst successful = call.status === \"completed\" && (call.exitCode === 0 || call.exitCode === null);\n\t\t\t\t\treturn successful ? \"completed\" : call.status === \"cancelled\" ? \"cancelled\" : \"failed\";\n\t\t\t\t}),\n\t\t\t),\n\t\t);\n\t\tconst summary = statuses\n\t\t\t.map(({ value, count }) => {\n\t\t\t\tconst color = value === \"completed\" ? \"success\" : value === \"cancelled\" ? \"warning\" : \"error\";\n\t\t\t\treturn theme.fg(color, `${count} ${value}`);\n\t\t\t})\n\t\t\t.join(theme.fg(\"muted\", \", \"));\n\t\tthis.box.addChild(new Text(summary || theme.fg(\"muted\", \"0 calls\"), 0, 0));\n\t\tthis.renderAnnotations();\n\t\tif (keyText(\"app.tools.expand\")) this.box.addChild(new Text(keyHint(\"app.tools.expand\", \"to expand\"), 0, 0));\n\t}\n\n\tprivate renderSingleNotice(notice: HookExecutionNotice): void {\n\t\tconst subject = notice.subject ? theme.fg(\"muted\", ` · ${notice.subject}`) : \"\";\n\t\tthis.box.addChild(new Text(theme.fg(\"warning\", theme.bold(`Hook · ${notice.event}`)) + subject, 0, 0));\n\t\tthis.box.addChild(new Spacer(1));\n\n\t\tfor (const call of notice.calls.map(hookCallView)) {\n\t\t\tconst status = theme.fg(call.statusColor, call.status);\n\t\t\tthis.box.addChild(new Text(`${theme.fg(\"text\", call.label)} ${status}`, 0, 0));\n\t\t\tthis.box.addChild(new Text(theme.fg(\"dim\", call.source), 0, 0));\n\t\t}\n\t\tthis.renderPrompts(countValues(notice.returnedPrompts), false);\n\t\tthis.renderAnnotations();\n\t}\n\n\tprivate renderGroupedNotices(firstNotice: HookExecutionNotice): void {\n\t\tthis.box.addChild(new Text(theme.fg(\"warning\", theme.bold(`Hook · ${firstNotice.event}`)), 0, 0));\n\t\tconst subjects = countValues(\n\t\t\tthis.notices.flatMap((notice) => (notice.subject === undefined ? [] : [notice.subject])),\n\t\t);\n\t\tif (subjects.length > 0) {\n\t\t\tconst summary = subjects.map(({ value, count }) => `${value}${count > 1 ? ` ×${count}` : \"\"}`).join(\", \");\n\t\t\tthis.box.addChild(new Text(theme.fg(\"muted\", `Tools: ${summary}`), 0, 0));\n\t\t}\n\t\tthis.box.addChild(new Spacer(1));\n\n\t\tfor (const group of groupedHookCalls(this.notices)) {\n\t\t\tconst call = hookCallView(group.call);\n\t\t\tconst status = theme.fg(groupedCallStatusColor(group), groupedCallStatus(group));\n\t\t\tthis.box.addChild(new Text(`${theme.fg(\"text\", call.label)} ${status}`, 0, 0));\n\t\t\tthis.box.addChild(new Text(theme.fg(\"dim\", call.source), 0, 0));\n\t\t}\n\t\tthis.renderPrompts(countValues(this.notices.flatMap((notice) => notice.returnedPrompts)), true);\n\t\tthis.renderAnnotations();\n\t}\n\tprivate renderAnnotations(): void {\n\t\tif (this.annotations.length === 0) return;\n\t\tthis.box.addChild(new Spacer(1));\n\t\tfor (const annotation of this.annotations) this.box.addChild(new Text(theme.fg(\"dim\", annotation), 0, 0));\n\t}\n\n\tprivate renderPrompts(prompts: readonly CountedValue[], grouped: boolean): void {\n\t\tif (prompts.length === 0) return;\n\t\tthis.box.addChild(new Spacer(1));\n\t\tthis.box.addChild(\n\t\t\tnew Text(\n\t\t\t\ttheme.fg(\"warning\", theme.bold(prompts.length === 1 && !grouped ? \"Returned prompt\" : \"Returned prompts\")),\n\t\t\t\t0,\n\t\t\t\t0,\n\t\t\t),\n\t\t);\n\t\tfor (let index = 0; index < prompts.length; index++) {\n\t\t\tconst prompt = prompts[index];\n\t\t\tif (prompts.length > 1 || grouped) {\n\t\t\t\tconst count = prompt.count > 1 ? ` ×${prompt.count}` : \"\";\n\t\t\t\tthis.box.addChild(new Text(theme.fg(\"muted\", `Prompt ${index + 1}${count}`), 0, 0));\n\t\t\t}\n\t\t\tthis.box.addChild(\n\t\t\t\tnew Markdown(prompt.value, 0, 0, this.markdownTheme, {\n\t\t\t\t\tcolor: (text: string) => theme.fg(\"text\", text),\n\t\t\t\t}),\n\t\t\t);\n\t\t}\n\t}\n}\n"]}
@@ -1,14 +1,12 @@
1
- import { type Model } from "@greppleai/grip-ai";
1
+ import { type Api, type Model } from "@greppleai/grip-ai";
2
2
  import { Container, type Focusable, Input, type TUI } from "@greppleai/grip-tui";
3
3
  import type { ModelRegistry } from "../../../core/model-registry.ts";
4
4
  import type { SettingsManager } from "../../../core/settings-manager.ts";
5
5
  interface ScopedModelItem {
6
- model: Model<any>;
6
+ model: Model<Api>;
7
7
  thinkingLevel?: string;
8
8
  }
9
- /**
10
- * Component that renders a model selector with search
11
- */
9
+ /** Native provider/model navigation, with a global model-search entry point. */
12
10
  export declare class ModelSelectorComponent extends Container implements Focusable {
13
11
  private searchInput;
14
12
  private _focused;
@@ -19,33 +17,43 @@ export declare class ModelSelectorComponent extends Container implements Focusab
19
17
  private scopedModelItems;
20
18
  private activeModels;
21
19
  private filteredModels;
20
+ private providers;
21
+ private filteredProviders;
22
+ private selectedProvider;
23
+ private providerQuery;
24
+ private providerIndex;
25
+ private view;
22
26
  private selectedIndex;
23
- private currentModel?;
27
+ private currentModel;
24
28
  private settingsManager;
25
29
  private modelRegistry;
26
30
  private onSelectCallback;
27
31
  private onCancelCallback;
28
- private errorMessage?;
32
+ private errorMessage;
33
+ private loading;
34
+ private listWidth;
29
35
  private tui;
30
36
  private scopedModels;
31
37
  private scope;
32
- private scopeText?;
33
- private scopeHintText?;
38
+ private scopeText;
39
+ private navigationText;
34
40
  private readonly persistDefault;
35
- constructor(tui: TUI, currentModel: Model<any> | undefined, settingsManager: SettingsManager, modelRegistry: ModelRegistry, scopedModels: ReadonlyArray<ScopedModelItem>, onSelect: (model: Model<any>) => void, onCancel: () => void, initialSearchInput?: string, persistDefault?: boolean);
41
+ constructor(tui: TUI, currentModel: Model<Api> | undefined, settingsManager: SettingsManager, modelRegistry: ModelRegistry, scopedModels: ReadonlyArray<ScopedModelItem>, onSelect: (model: Model<Api>) => void, onCancel: () => void, initialSearchInput?: string, persistDefault?: boolean);
36
42
  private loadModels;
37
- private sortModels;
38
- private getScopeText;
39
- private getScopeHintText;
40
- private setScope;
41
- private filterModels;
42
- private appendModelListStatus;
43
+ private rebuildProviders;
44
+ private selectCurrent;
45
+ private getModelItems;
46
+ private filterItems;
47
+ private itemCount;
48
+ private updateHeader;
43
49
  private updateList;
50
+ private appendListStatus;
51
+ private addListLine;
52
+ render(width: number): string[];
44
53
  private toggleModelScope;
45
54
  private moveSelection;
46
- private selectHighlightedModel;
55
+ private confirmSelection;
47
56
  handleInput(keyData: string): void;
48
- private handleSelect;
49
57
  getSearchInput(): Input;
50
58
  }
51
59
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"model-selector.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/model-selector.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,KAAK,EAAkB,MAAM,oBAAoB,CAAC;AAChE,OAAO,EACN,SAAS,EACT,KAAK,SAAS,EAGd,KAAK,EAGL,KAAK,GAAG,EACR,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AACrE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAC;AAWzE,UAAU,eAAe;IACxB,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;IAClB,aAAa,CAAC,EAAE,MAAM,CAAC;CACvB;AAID;;GAEG;AACH,qBAAa,sBAAuB,SAAQ,SAAU,YAAW,SAAS;IACzE,OAAO,CAAC,WAAW,CAAQ;IAG3B,OAAO,CAAC,QAAQ,CAAS;IACzB,IAAI,OAAO,IAAI,OAAO,CAErB;IACD,IAAI,OAAO,CAAC,KAAK,EAAE,OAAO,EAGzB;IACD,OAAO,CAAC,aAAa,CAAY;IACjC,OAAO,CAAC,SAAS,CAAmB;IACpC,OAAO,CAAC,gBAAgB,CAAmB;IAC3C,OAAO,CAAC,YAAY,CAAmB;IACvC,OAAO,CAAC,cAAc,CAAmB;IACzC,OAAO,CAAC,aAAa,CAAa;IAClC,OAAO,CAAC,YAAY,CAAC,CAAa;IAClC,OAAO,CAAC,eAAe,CAAkB;IACzC,OAAO,CAAC,aAAa,CAAgB;IACrC,OAAO,CAAC,gBAAgB,CAA8B;IACtD,OAAO,CAAC,gBAAgB,CAAa;IACrC,OAAO,CAAC,YAAY,CAAC,CAAS;IAC9B,OAAO,CAAC,GAAG,CAAM;IACjB,OAAO,CAAC,YAAY,CAAiC;IACrD,OAAO,CAAC,KAAK,CAAqB;IAClC,OAAO,CAAC,SAAS,CAAC,CAAO;IACzB,OAAO,CAAC,aAAa,CAAC,CAAO;IAC7B,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAU;IAEzC,YACC,GAAG,EAAE,GAAG,EACR,YAAY,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,SAAS,EACpC,eAAe,EAAE,eAAe,EAChC,aAAa,EAAE,aAAa,EAC5B,YAAY,EAAE,aAAa,CAAC,eAAe,CAAC,EAC5C,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,KAAK,IAAI,EACrC,QAAQ,EAAE,MAAM,IAAI,EACpB,kBAAkB,CAAC,EAAE,MAAM,EAC3B,cAAc,UAAO,EA+DrB;YAEa,UAAU;IAoDxB,OAAO,CAAC,UAAU;IAalB,OAAO,CAAC,YAAY;IAMpB,OAAO,CAAC,gBAAgB;IAIxB,OAAO,CAAC,QAAQ;IAYhB,OAAO,CAAC,YAAY;IAcpB,OAAO,CAAC,qBAAqB;IAe7B,OAAO,CAAC,UAAU;IA6ClB,OAAO,CAAC,gBAAgB;IAOxB,OAAO,CAAC,aAAa;IAUrB,OAAO,CAAC,sBAAsB;IAK9B,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAwBjC;IAED,OAAO,CAAC,YAAY;IAMpB,cAAc,IAAI,KAAK,CAEtB;CACD","sourcesContent":["import { type Model, modelsAreEqual } from \"@greppleai/grip-ai\";\nimport {\n\tContainer,\n\ttype Focusable,\n\tfuzzyFilter,\n\tgetKeybindings,\n\tInput,\n\tSpacer,\n\tText,\n\ttype TUI,\n} from \"@greppleai/grip-tui\";\nimport type { ModelRegistry } from \"../../../core/model-registry.ts\";\nimport type { SettingsManager } from \"../../../core/settings-manager.ts\";\nimport { theme } from \"../theme/theme.ts\";\nimport { DynamicBorder } from \"./dynamic-border.ts\";\nimport { keyHint } from \"./keybinding-hints.ts\";\n\ninterface ModelItem {\n\tprovider: string;\n\tid: string;\n\tmodel: Model<any>;\n}\n\ninterface ScopedModelItem {\n\tmodel: Model<any>;\n\tthinkingLevel?: string;\n}\n\ntype ModelScope = \"all\" | \"scoped\";\n\n/**\n * Component that renders a model selector with search\n */\nexport class ModelSelectorComponent extends Container implements Focusable {\n\tprivate searchInput: Input;\n\n\t// Focusable implementation - propagate to searchInput for IME cursor positioning\n\tprivate _focused = false;\n\tget focused(): boolean {\n\t\treturn this._focused;\n\t}\n\tset focused(value: boolean) {\n\t\tthis._focused = value;\n\t\tthis.searchInput.focused = value;\n\t}\n\tprivate listContainer: Container;\n\tprivate allModels: ModelItem[] = [];\n\tprivate scopedModelItems: ModelItem[] = [];\n\tprivate activeModels: ModelItem[] = [];\n\tprivate filteredModels: ModelItem[] = [];\n\tprivate selectedIndex: number = 0;\n\tprivate currentModel?: Model<any>;\n\tprivate settingsManager: SettingsManager;\n\tprivate modelRegistry: ModelRegistry;\n\tprivate onSelectCallback: (model: Model<any>) => void;\n\tprivate onCancelCallback: () => void;\n\tprivate errorMessage?: string;\n\tprivate tui: TUI;\n\tprivate scopedModels: ReadonlyArray<ScopedModelItem>;\n\tprivate scope: ModelScope = \"all\";\n\tprivate scopeText?: Text;\n\tprivate scopeHintText?: Text;\n\tprivate readonly persistDefault: boolean;\n\n\tconstructor(\n\t\ttui: TUI,\n\t\tcurrentModel: Model<any> | undefined,\n\t\tsettingsManager: SettingsManager,\n\t\tmodelRegistry: ModelRegistry,\n\t\tscopedModels: ReadonlyArray<ScopedModelItem>,\n\t\tonSelect: (model: Model<any>) => void,\n\t\tonCancel: () => void,\n\t\tinitialSearchInput?: string,\n\t\tpersistDefault = true,\n\t) {\n\t\tsuper();\n\n\t\tthis.tui = tui;\n\t\tthis.currentModel = currentModel;\n\t\tthis.settingsManager = settingsManager;\n\t\tthis.modelRegistry = modelRegistry;\n\t\tthis.scopedModels = scopedModels;\n\t\tthis.scope = scopedModels.length > 0 ? \"scoped\" : \"all\";\n\t\tthis.onSelectCallback = onSelect;\n\t\tthis.onCancelCallback = onCancel;\n\t\tthis.persistDefault = persistDefault;\n\t\t// Add top border\n\t\tthis.addChild(new DynamicBorder());\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Add hint about model filtering\n\t\tif (scopedModels.length > 0) {\n\t\t\tthis.scopeText = new Text(this.getScopeText(), 0, 0);\n\t\t\tthis.addChild(this.scopeText);\n\t\t\tthis.scopeHintText = new Text(this.getScopeHintText(), 0, 0);\n\t\t\tthis.addChild(this.scopeHintText);\n\t\t} else {\n\t\t\tconst hintText = \"Only showing models from configured providers. Use /login to add providers.\";\n\t\t\tthis.addChild(new Text(theme.fg(\"warning\", hintText), 0, 0));\n\t\t}\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Create search input\n\t\tthis.searchInput = new Input();\n\t\tif (initialSearchInput) {\n\t\t\tthis.searchInput.setValue(initialSearchInput);\n\t\t}\n\t\tthis.searchInput.onSubmit = () => {\n\t\t\t// Enter on search input selects the first filtered item\n\t\t\tif (this.filteredModels[this.selectedIndex]) {\n\t\t\t\tthis.handleSelect(this.filteredModels[this.selectedIndex].model);\n\t\t\t}\n\t\t};\n\t\tthis.addChild(this.searchInput);\n\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Create list container\n\t\tthis.listContainer = new Container();\n\t\tthis.addChild(this.listContainer);\n\n\t\tthis.addChild(new Spacer(1));\n\n\t\t// Add bottom border\n\t\tthis.addChild(new DynamicBorder());\n\n\t\t// Load models and do initial render\n\t\tthis.loadModels().then(() => {\n\t\t\tif (initialSearchInput) {\n\t\t\t\tthis.filterModels(initialSearchInput);\n\t\t\t} else {\n\t\t\t\tthis.updateList();\n\t\t\t}\n\t\t\t// Request re-render after models are loaded\n\t\t\tthis.tui.requestRender();\n\t\t});\n\t}\n\n\tprivate async loadModels(): Promise<void> {\n\t\tlet models: ModelItem[];\n\n\t\t// Refresh to pick up any changes to models.json\n\t\tthis.modelRegistry.refresh();\n\n\t\t// Check for models.json errors\n\t\tconst loadError = this.modelRegistry.getError();\n\t\tif (loadError) {\n\t\t\tthis.errorMessage = loadError;\n\t\t}\n\n\t\t// Load available models (built-in models still work even if models.json failed)\n\t\ttry {\n\t\t\tconst availableModels = await this.modelRegistry.getAvailable();\n\t\t\tmodels = availableModels.map((model: Model<any>) => ({\n\t\t\t\tprovider: model.provider,\n\t\t\t\tid: model.id,\n\t\t\t\tmodel,\n\t\t\t}));\n\t\t} catch (error) {\n\t\t\tthis.allModels = [];\n\t\t\tthis.scopedModelItems = [];\n\t\t\tthis.activeModels = [];\n\t\t\tthis.filteredModels = [];\n\t\t\tthis.errorMessage = error instanceof Error ? error.message : String(error);\n\t\t\treturn;\n\t\t}\n\n\t\tthis.allModels = this.sortModels(models);\n\t\tconst registeredModels = new Map<string, Map<string, Model<any>>>();\n\t\tfor (const model of this.modelRegistry.getAll()) {\n\t\t\tconst providerModels = registeredModels.get(model.provider) ?? new Map<string, Model<any>>();\n\t\t\tif (!providerModels.has(model.id)) providerModels.set(model.id, model);\n\t\t\tregisteredModels.set(model.provider, providerModels);\n\t\t}\n\t\tthis.scopedModels = this.scopedModels.map((scoped) => {\n\t\t\tconst refreshed = registeredModels.get(scoped.model.provider)?.get(scoped.model.id);\n\t\t\treturn refreshed ? { ...scoped, model: refreshed } : scoped;\n\t\t});\n\t\tthis.scopedModelItems = this.scopedModels.map((scoped) => ({\n\t\t\tprovider: scoped.model.provider,\n\t\t\tid: scoped.model.id,\n\t\t\tmodel: scoped.model,\n\t\t}));\n\t\tthis.activeModels = this.scope === \"scoped\" ? this.scopedModelItems : this.allModels;\n\t\tthis.filteredModels = this.activeModels;\n\t\tconst currentIndex = this.filteredModels.findIndex((item) => modelsAreEqual(this.currentModel, item.model));\n\t\tthis.selectedIndex =\n\t\t\tcurrentIndex >= 0 ? currentIndex : Math.min(this.selectedIndex, Math.max(0, this.filteredModels.length - 1));\n\t}\n\n\tprivate sortModels(models: ModelItem[]): ModelItem[] {\n\t\tconst sorted = [...models];\n\t\t// Sort: current model first, then by provider\n\t\tsorted.sort((a, b) => {\n\t\t\tconst aIsCurrent = modelsAreEqual(this.currentModel, a.model);\n\t\t\tconst bIsCurrent = modelsAreEqual(this.currentModel, b.model);\n\t\t\tif (aIsCurrent && !bIsCurrent) return -1;\n\t\t\tif (!aIsCurrent && bIsCurrent) return 1;\n\t\t\treturn a.provider.localeCompare(b.provider);\n\t\t});\n\t\treturn sorted;\n\t}\n\n\tprivate getScopeText(): string {\n\t\tconst allText = this.scope === \"all\" ? theme.fg(\"accent\", \"all\") : theme.fg(\"muted\", \"all\");\n\t\tconst scopedText = this.scope === \"scoped\" ? theme.fg(\"accent\", \"scoped\") : theme.fg(\"muted\", \"scoped\");\n\t\treturn `${theme.fg(\"muted\", \"Scope: \")}${allText}${theme.fg(\"muted\", \" | \")}${scopedText}`;\n\t}\n\n\tprivate getScopeHintText(): string {\n\t\treturn keyHint(\"tui.input.tab\", \"scope\") + theme.fg(\"muted\", \" (all/scoped)\");\n\t}\n\n\tprivate setScope(scope: ModelScope): void {\n\t\tif (this.scope === scope) return;\n\t\tthis.scope = scope;\n\t\tthis.activeModels = this.scope === \"scoped\" ? this.scopedModelItems : this.allModels;\n\t\tconst currentIndex = this.activeModels.findIndex((item) => modelsAreEqual(this.currentModel, item.model));\n\t\tthis.selectedIndex = currentIndex >= 0 ? currentIndex : 0;\n\t\tthis.filterModels(this.searchInput.getValue());\n\t\tif (this.scopeText) {\n\t\t\tthis.scopeText.setText(this.getScopeText());\n\t\t}\n\t}\n\n\tprivate filterModels(query: string): void {\n\t\tthis.filteredModels = query\n\t\t\t? fuzzyFilter(\n\t\t\t\t\tthis.activeModels,\n\t\t\t\t\tquery,\n\t\t\t\t\t({ id, provider }) => `${id} ${provider} ${provider}/${id} ${provider} ${id}`,\n\t\t\t\t)\n\t\t\t: this.activeModels;\n\t\t// A query changes fuzzy-match ranking, so select the best match. When the query\n\t\t// is cleared, preserve the current position and only clamp it to the restored list.\n\t\tthis.selectedIndex = query ? 0 : Math.min(this.selectedIndex, Math.max(0, this.filteredModels.length - 1));\n\t\tthis.updateList();\n\t}\n\n\tprivate appendModelListStatus(): void {\n\t\tif (this.errorMessage) {\n\t\t\tfor (const line of this.errorMessage.split(\"\\n\")) {\n\t\t\t\tthis.listContainer.addChild(new Text(theme.fg(\"error\", line), 0, 0));\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\t\tif (this.filteredModels.length === 0) {\n\t\t\tthis.listContainer.addChild(new Text(theme.fg(\"muted\", \" No matching models\"), 0, 0));\n\t\t\treturn;\n\t\t}\n\t\tconst selected = this.filteredModels[this.selectedIndex];\n\t\tthis.listContainer.addChild(new Spacer(1));\n\t\tthis.listContainer.addChild(new Text(theme.fg(\"muted\", ` Model Name: ${selected.model.name}`), 0, 0));\n\t}\n\tprivate updateList(): void {\n\t\tthis.listContainer.clear();\n\n\t\tconst maxVisible = 10;\n\t\tconst startIndex = Math.max(\n\t\t\t0,\n\t\t\tMath.min(this.selectedIndex - Math.floor(maxVisible / 2), this.filteredModels.length - maxVisible),\n\t\t);\n\t\tconst endIndex = Math.min(startIndex + maxVisible, this.filteredModels.length);\n\n\t\t// Show visible slice of filtered models\n\t\tfor (let i = startIndex; i < endIndex; i++) {\n\t\t\tconst item = this.filteredModels[i];\n\t\t\tif (!item) continue;\n\n\t\t\tconst isSelected = i === this.selectedIndex;\n\t\t\tconst isCurrent = modelsAreEqual(this.currentModel, item.model);\n\n\t\t\tlet line = \"\";\n\t\t\tif (isSelected) {\n\t\t\t\tconst prefix = theme.fg(\"accent\", \"→ \");\n\t\t\t\tconst modelText = `${item.id}`;\n\t\t\t\tconst providerBadge = theme.fg(\"muted\", `[${item.provider}]`);\n\t\t\t\tconst checkmark = isCurrent ? theme.fg(\"success\", \" ✓\") : \"\";\n\t\t\t\tline = `${prefix + theme.fg(\"accent\", modelText)} ${providerBadge}${checkmark}`;\n\t\t\t} else {\n\t\t\t\tconst modelText = ` ${item.id}`;\n\t\t\t\tconst providerBadge = theme.fg(\"muted\", `[${item.provider}]`);\n\t\t\t\tconst checkmark = isCurrent ? theme.fg(\"success\", \" ✓\") : \"\";\n\t\t\t\tline = `${modelText} ${providerBadge}${checkmark}`;\n\t\t\t}\n\n\t\t\tthis.listContainer.addChild(new Text(line, 0, 0));\n\t\t}\n\n\t\t// Add scroll indicator if needed\n\t\tif (startIndex > 0 || endIndex < this.filteredModels.length) {\n\t\t\tconst scrollInfo = theme.fg(\"muted\", ` (${this.selectedIndex + 1}/${this.filteredModels.length})`);\n\t\t\tthis.listContainer.addChild(new Text(scrollInfo, 0, 0));\n\t\t}\n\n\t\t// Show error message or \"no results\" if empty\n\t\tthis.appendModelListStatus();\n\t}\n\n\tprivate toggleModelScope(): void {\n\t\tif (this.scopedModelItems.length === 0) return;\n\t\tconst nextScope: ModelScope = this.scope === \"all\" ? \"scoped\" : \"all\";\n\t\tthis.setScope(nextScope);\n\t\tthis.scopeHintText?.setText(this.getScopeHintText());\n\t}\n\n\tprivate moveSelection(direction: -1 | 1): void {\n\t\tif (this.filteredModels.length === 0) return;\n\t\tif (direction < 0) {\n\t\t\tthis.selectedIndex = this.selectedIndex === 0 ? this.filteredModels.length - 1 : this.selectedIndex - 1;\n\t\t} else {\n\t\t\tthis.selectedIndex = this.selectedIndex === this.filteredModels.length - 1 ? 0 : this.selectedIndex + 1;\n\t\t}\n\t\tthis.updateList();\n\t}\n\n\tprivate selectHighlightedModel(): void {\n\t\tconst selectedModel = this.filteredModels[this.selectedIndex];\n\t\tif (selectedModel) this.handleSelect(selectedModel.model);\n\t}\n\n\thandleInput(keyData: string): void {\n\t\tconst kb = getKeybindings();\n\t\tif (kb.matches(keyData, \"tui.input.tab\")) {\n\t\t\tthis.toggleModelScope();\n\t\t\treturn;\n\t\t}\n\t\tif (kb.matches(keyData, \"tui.select.up\")) {\n\t\t\tthis.moveSelection(-1);\n\t\t\treturn;\n\t\t}\n\t\tif (kb.matches(keyData, \"tui.select.down\")) {\n\t\t\tthis.moveSelection(1);\n\t\t\treturn;\n\t\t}\n\t\tif (kb.matches(keyData, \"tui.select.confirm\")) {\n\t\t\tthis.selectHighlightedModel();\n\t\t\treturn;\n\t\t}\n\t\tif (kb.matches(keyData, \"tui.select.cancel\")) {\n\t\t\tthis.onCancelCallback();\n\t\t\treturn;\n\t\t}\n\t\tthis.searchInput.handleInput(keyData);\n\t\tthis.filterModels(this.searchInput.getValue());\n\t}\n\n\tprivate handleSelect(model: Model<any>): void {\n\t\t// Save as new default\n\t\tif (this.persistDefault) this.settingsManager.setDefaultModelAndProvider(model.provider, model.id);\n\t\tthis.onSelectCallback(model);\n\t}\n\n\tgetSearchInput(): Input {\n\t\treturn this.searchInput;\n\t}\n}\n"]}
1
+ {"version":3,"file":"model-selector.d.ts","sourceRoot":"","sources":["../../../../src/modes/interactive/components/model-selector.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,GAAG,EAAE,KAAK,KAAK,EAAkB,MAAM,oBAAoB,CAAC;AAC1E,OAAO,EACN,SAAS,EACT,KAAK,SAAS,EAGd,KAAK,EAGL,KAAK,GAAG,EAER,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AACrE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAC;AAWzE,UAAU,eAAe;IACxB,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;IAClB,aAAa,CAAC,EAAE,MAAM,CAAC;CACvB;AAWD,gFAAgF;AAChF,qBAAa,sBAAuB,SAAQ,SAAU,YAAW,SAAS;IACzE,OAAO,CAAC,WAAW,CAAQ;IAC3B,OAAO,CAAC,QAAQ,CAAS;IACzB,IAAI,OAAO,IAAI,OAAO,CAErB;IACD,IAAI,OAAO,CAAC,KAAK,EAAE,OAAO,EAGzB;IACD,OAAO,CAAC,aAAa,CAAY;IACjC,OAAO,CAAC,SAAS,CAAmB;IACpC,OAAO,CAAC,gBAAgB,CAAmB;IAC3C,OAAO,CAAC,YAAY,CAAmB;IACvC,OAAO,CAAC,cAAc,CAAmB;IACzC,OAAO,CAAC,SAAS,CAAsB;IACvC,OAAO,CAAC,iBAAiB,CAAsB;IAC/C,OAAO,CAAC,gBAAgB,CAA6B;IACrD,OAAO,CAAC,aAAa,CAAM;IAC3B,OAAO,CAAC,aAAa,CAAK;IAC1B,OAAO,CAAC,IAAI,CAAa;IACzB,OAAO,CAAC,aAAa,CAAK;IAC1B,OAAO,CAAC,YAAY,CAAyB;IAC7C,OAAO,CAAC,eAAe,CAAkB;IACzC,OAAO,CAAC,aAAa,CAAgB;IACrC,OAAO,CAAC,gBAAgB,CAA8B;IACtD,OAAO,CAAC,gBAAgB,CAAa;IACrC,OAAO,CAAC,YAAY,CAAqB;IACzC,OAAO,CAAC,OAAO,CAAQ;IACvB,OAAO,CAAC,SAAS,CAAM;IACvB,OAAO,CAAC,GAAG,CAAM;IACjB,OAAO,CAAC,YAAY,CAAiC;IACrD,OAAO,CAAC,KAAK,CAAa;IAC1B,OAAO,CAAC,SAAS,CAAO;IACxB,OAAO,CAAC,cAAc,CAAO;IAC7B,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAU;IAEzC,YACC,GAAG,EAAE,GAAG,EACR,YAAY,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,SAAS,EACpC,eAAe,EAAE,eAAe,EAChC,aAAa,EAAE,aAAa,EAC5B,YAAY,EAAE,aAAa,CAAC,eAAe,CAAC,EAC5C,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,KAAK,IAAI,EACrC,QAAQ,EAAE,MAAM,IAAI,EACpB,kBAAkB,CAAC,EAAE,MAAM,EAC3B,cAAc,UAAO,EA+BrB;YAEa,UAAU;IAwCxB,OAAO,CAAC,gBAAgB;IAkBxB,OAAO,CAAC,aAAa;IAQrB,OAAO,CAAC,aAAa;IAIrB,OAAO,CAAC,WAAW;IAoBnB,OAAO,CAAC,SAAS;IAIjB,OAAO,CAAC,YAAY;IAuBpB,OAAO,CAAC,UAAU;IA4BlB,OAAO,CAAC,gBAAgB;IAgBxB,OAAO,CAAC,WAAW;IAIV,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAMvC;IAED,OAAO,CAAC,gBAAgB;IAexB,OAAO,CAAC,aAAa;IAOrB,OAAO,CAAC,gBAAgB;IAoBxB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CA8BjC;IAED,cAAc,IAAI,KAAK,CAEtB;CACD","sourcesContent":["import { type Api, type Model, modelsAreEqual } from \"@greppleai/grip-ai\";\nimport {\n\tContainer,\n\ttype Focusable,\n\tfuzzyFilter,\n\tgetKeybindings,\n\tInput,\n\tSpacer,\n\tText,\n\ttype TUI,\n\ttruncateToWidth,\n} from \"@greppleai/grip-tui\";\nimport type { ModelRegistry } from \"../../../core/model-registry.ts\";\nimport type { SettingsManager } from \"../../../core/settings-manager.ts\";\nimport { theme } from \"../theme/theme.ts\";\nimport { DynamicBorder } from \"./dynamic-border.ts\";\nimport { keyHint } from \"./keybinding-hints.ts\";\n\ninterface ModelItem {\n\tprovider: string;\n\tid: string;\n\tmodel: Model<Api>;\n}\n\ninterface ScopedModelItem {\n\tmodel: Model<Api>;\n\tthinkingLevel?: string;\n}\n\ninterface ProviderItem {\n\tid: string;\n\tname: string;\n\tmodels: ModelItem[];\n}\n\ntype ModelScope = \"all\" | \"scoped\";\ntype PickerView = \"providers\" | \"models\" | \"search\";\n\n/** Native provider/model navigation, with a global model-search entry point. */\nexport class ModelSelectorComponent extends Container implements Focusable {\n\tprivate searchInput: Input;\n\tprivate _focused = false;\n\tget focused(): boolean {\n\t\treturn this._focused;\n\t}\n\tset focused(value: boolean) {\n\t\tthis._focused = value;\n\t\tthis.searchInput.focused = value;\n\t}\n\tprivate listContainer: Container;\n\tprivate allModels: ModelItem[] = [];\n\tprivate scopedModelItems: ModelItem[] = [];\n\tprivate activeModels: ModelItem[] = [];\n\tprivate filteredModels: ModelItem[] = [];\n\tprivate providers: ProviderItem[] = [];\n\tprivate filteredProviders: ProviderItem[] = [];\n\tprivate selectedProvider: ProviderItem | null = null;\n\tprivate providerQuery = \"\";\n\tprivate providerIndex = 0;\n\tprivate view: PickerView;\n\tprivate selectedIndex = 0;\n\tprivate currentModel: Model<Api> | undefined;\n\tprivate settingsManager: SettingsManager;\n\tprivate modelRegistry: ModelRegistry;\n\tprivate onSelectCallback: (model: Model<Api>) => void;\n\tprivate onCancelCallback: () => void;\n\tprivate errorMessage: string | undefined;\n\tprivate loading = true;\n\tprivate listWidth = 80;\n\tprivate tui: TUI;\n\tprivate scopedModels: ReadonlyArray<ScopedModelItem>;\n\tprivate scope: ModelScope;\n\tprivate scopeText: Text;\n\tprivate navigationText: Text;\n\tprivate readonly persistDefault: boolean;\n\n\tconstructor(\n\t\ttui: TUI,\n\t\tcurrentModel: Model<Api> | undefined,\n\t\tsettingsManager: SettingsManager,\n\t\tmodelRegistry: ModelRegistry,\n\t\tscopedModels: ReadonlyArray<ScopedModelItem>,\n\t\tonSelect: (model: Model<Api>) => void,\n\t\tonCancel: () => void,\n\t\tinitialSearchInput?: string,\n\t\tpersistDefault = true,\n\t) {\n\t\tsuper();\n\t\tthis.tui = tui;\n\t\tthis.currentModel = currentModel;\n\t\tthis.settingsManager = settingsManager;\n\t\tthis.modelRegistry = modelRegistry;\n\t\tthis.scopedModels = scopedModels;\n\t\tthis.scope = scopedModels.length > 0 ? \"scoped\" : \"all\";\n\t\tthis.view = initialSearchInput ? \"search\" : \"providers\";\n\t\tthis.onSelectCallback = onSelect;\n\t\tthis.onCancelCallback = onCancel;\n\t\tthis.persistDefault = persistDefault;\n\t\tthis.addChild(new DynamicBorder());\n\t\tthis.addChild(new Spacer(1));\n\t\tthis.scopeText = new Text(\"\", 0, 0);\n\t\tthis.addChild(this.scopeText);\n\t\tthis.navigationText = new Text(\"\", 0, 0);\n\t\tthis.addChild(this.navigationText);\n\t\tthis.addChild(new Spacer(1));\n\t\tthis.searchInput = new Input();\n\t\tthis.searchInput.setValue(initialSearchInput ?? \"\");\n\t\tthis.searchInput.onSubmit = () => this.confirmSelection();\n\t\tthis.addChild(this.searchInput);\n\t\tthis.addChild(new Spacer(1));\n\t\tthis.listContainer = new Container();\n\t\tthis.addChild(this.listContainer);\n\t\tthis.addChild(new Spacer(1));\n\t\tthis.addChild(new DynamicBorder());\n\t\tthis.updateList();\n\t\tvoid this.loadModels();\n\t}\n\n\tprivate async loadModels(): Promise<void> {\n\t\ttry {\n\t\t\t// Refresh without introducing provider/network validation.\n\t\t\tthis.modelRegistry.refresh();\n\t\t\tthis.errorMessage = this.modelRegistry.getError();\n\t\t\tconst available = await this.modelRegistry.getAvailable();\n\t\t\tthis.allModels = available.map((model) => ({ provider: model.provider, id: model.id, model }));\n\t\t\tthis.allModels.sort((a, b) => {\n\t\t\t\tconst aCurrent = modelsAreEqual(this.currentModel, a.model);\n\t\t\t\tconst bCurrent = modelsAreEqual(this.currentModel, b.model);\n\t\t\t\tif (aCurrent !== bCurrent) return aCurrent ? -1 : 1;\n\t\t\t\treturn a.provider.localeCompare(b.provider) || a.id.localeCompare(b.id);\n\t\t\t});\n\t\t\tconst registered = new Map<string, Map<string, Model<Api>>>();\n\t\t\tfor (const model of this.modelRegistry.getAll()) {\n\t\t\t\tlet models = registered.get(model.provider);\n\t\t\t\tif (!models) {\n\t\t\t\t\tmodels = new Map();\n\t\t\t\t\tregistered.set(model.provider, models);\n\t\t\t\t}\n\t\t\t\tif (!models.has(model.id)) models.set(model.id, model);\n\t\t\t}\n\t\t\t// Keep scoped order, reconciling stale instances after registry refresh.\n\t\t\tthis.scopedModelItems = this.scopedModels.map(({ model }) => ({\n\t\t\t\tprovider: model.provider,\n\t\t\t\tid: model.id,\n\t\t\t\tmodel: registered.get(model.provider)?.get(model.id) ?? model,\n\t\t\t}));\n\t\t} catch (error) {\n\t\t\tthis.allModels = [];\n\t\t\tthis.scopedModelItems = [];\n\t\t\tthis.errorMessage = error instanceof Error ? error.message : String(error);\n\t\t}\n\t\tthis.loading = false;\n\t\tthis.rebuildProviders();\n\t\tthis.selectCurrent();\n\t\tthis.filterItems(this.searchInput.getValue());\n\t\tthis.tui.requestRender();\n\t}\n\n\tprivate rebuildProviders(): void {\n\t\tthis.activeModels = this.scope === \"scoped\" ? this.scopedModelItems : this.allModels;\n\t\tconst groups = new Map<string, ProviderItem>();\n\t\tfor (const item of this.activeModels) {\n\t\t\tlet provider = groups.get(item.provider);\n\t\t\tif (!provider) {\n\t\t\t\tprovider = {\n\t\t\t\t\tid: item.provider,\n\t\t\t\t\tname: this.modelRegistry.getProviderDisplayName(item.provider),\n\t\t\t\t\tmodels: [],\n\t\t\t\t};\n\t\t\t\tgroups.set(item.provider, provider);\n\t\t\t}\n\t\t\tprovider.models.push(item);\n\t\t}\n\t\tthis.providers = [...groups.values()].sort((a, b) => a.name.localeCompare(b.name) || a.id.localeCompare(b.id));\n\t}\n\n\tprivate selectCurrent(): void {\n\t\tconst index =\n\t\t\tthis.view === \"providers\"\n\t\t\t\t? this.providers.findIndex((item) => item.id === this.currentModel?.provider)\n\t\t\t\t: this.getModelItems().findIndex((item) => modelsAreEqual(this.currentModel, item.model));\n\t\tthis.selectedIndex = Math.max(0, index);\n\t}\n\n\tprivate getModelItems(): ModelItem[] {\n\t\treturn this.view === \"models\" ? (this.selectedProvider?.models ?? []) : this.activeModels;\n\t}\n\n\tprivate filterItems(query: string): void {\n\t\tif (this.view === \"providers\") {\n\t\t\tthis.filteredProviders = query\n\t\t\t\t? fuzzyFilter(this.providers, query, ({ id, name }) => `${name} ${id}`)\n\t\t\t\t: this.providers;\n\t\t} else {\n\t\t\tconst items = this.getModelItems();\n\t\t\tthis.filteredModels = query\n\t\t\t\t? fuzzyFilter(\n\t\t\t\t\t\titems,\n\t\t\t\t\t\tquery,\n\t\t\t\t\t\t({ id, provider, model }) => `${id} ${provider} ${provider}/${id} ${model.name}`,\n\t\t\t\t\t)\n\t\t\t\t: items;\n\t\t}\n\t\t// Query edits select the best match; clearing only clamps the position (7209).\n\t\tthis.selectedIndex = query ? 0 : Math.min(this.selectedIndex, Math.max(0, this.itemCount() - 1));\n\t\tthis.updateList();\n\t}\n\n\tprivate itemCount(): number {\n\t\treturn this.view === \"providers\" ? this.filteredProviders.length : this.filteredModels.length;\n\t}\n\n\tprivate updateHeader(): void {\n\t\tconst all = this.scope === \"all\" ? theme.fg(\"accent\", \"all\") : theme.fg(\"muted\", \"all\");\n\t\tconst scoped = this.scope === \"scoped\" ? theme.fg(\"accent\", \"scoped\") : theme.fg(\"muted\", \"scoped\");\n\t\tconst scopeHint =\n\t\t\tthis.scopedModels.length > 0 ? ` · ${keyHint(\"tui.input.tab\", \"scope\")} (${all}/${scoped})` : \"\";\n\t\tconst title =\n\t\t\tthis.view === \"providers\"\n\t\t\t\t? \"Providers\"\n\t\t\t\t: this.view === \"search\"\n\t\t\t\t\t? \"Search all providers\"\n\t\t\t\t\t: `${this.selectedProvider?.name} [${this.selectedProvider?.id}]`;\n\t\tthis.scopeText.setText(theme.fg(\"muted\", title) + scopeHint);\n\t\tthis.navigationText.setText(\n\t\t\tkeyHint(\"tui.select.up\", \"up\") +\n\t\t\t\t\" · \" +\n\t\t\t\tkeyHint(\"tui.select.down\", \"down\") +\n\t\t\t\t\" · \" +\n\t\t\t\tkeyHint(\"tui.select.confirm\", this.view === \"providers\" ? \"models\" : \"select\") +\n\t\t\t\t\" · \" +\n\t\t\t\tkeyHint(\"tui.select.cancel\", this.view === \"models\" ? \"providers\" : \"cancel\"),\n\t\t);\n\t}\n\n\tprivate updateList(): void {\n\t\tthis.updateHeader();\n\t\tthis.listContainer.clear();\n\t\tconst count = this.itemCount();\n\t\tconst maxVisible = 10;\n\t\tconst start = Math.max(0, Math.min(this.selectedIndex - Math.floor(maxVisible / 2), count - maxVisible));\n\t\tconst end = Math.min(start + maxVisible, count);\n\t\tfor (let i = start; i < end; i++) {\n\t\t\tlet label: string;\n\t\t\tlet current: boolean;\n\t\t\tif (this.view === \"providers\") {\n\t\t\t\tconst item = this.filteredProviders[i];\n\t\t\t\tlabel = `${item.name} [${item.id}] (${item.models.length} models)`;\n\t\t\t\tcurrent = item.id === this.currentModel?.provider;\n\t\t\t} else {\n\t\t\t\tconst item = this.filteredModels[i];\n\t\t\t\tlabel = `${item.id} [${item.provider}]`;\n\t\t\t\tcurrent = modelsAreEqual(this.currentModel, item.model);\n\t\t\t}\n\t\t\tconst selected = i === this.selectedIndex;\n\t\t\tconst prefix = selected ? theme.fg(\"accent\", \"→ \") : \" \";\n\t\t\tconst marker = current ? theme.fg(\"success\", \" ✓\") : \"\";\n\t\t\tthis.addListLine(prefix + (selected ? theme.fg(\"accent\", label) : label) + marker);\n\t\t}\n\t\tif (start > 0 || end < count) this.addListLine(theme.fg(\"muted\", ` (${this.selectedIndex + 1}/${count})`));\n\t\tthis.appendListStatus(count);\n\t}\n\n\tprivate appendListStatus(count: number): void {\n\t\tif (this.loading) {\n\t\t\tthis.addListLine(theme.fg(\"muted\", \" Loading models…\"));\n\t\t} else if (count === 0) {\n\t\t\tconst empty =\n\t\t\t\tthis.view === \"providers\" ? \"No matching providers. Use /login to add providers.\" : \"No matching models\";\n\t\t\tthis.addListLine(theme.fg(\"muted\", ` ${empty}`));\n\t\t} else if (this.view !== \"providers\") {\n\t\t\tthis.listContainer.addChild(new Spacer(1));\n\t\t\tthis.addListLine(theme.fg(\"muted\", ` Model Name: ${this.filteredModels[this.selectedIndex].model.name}`));\n\t\t}\n\t\tif (this.errorMessage) {\n\t\t\tfor (const line of this.errorMessage.split(\"\\n\")) this.addListLine(theme.fg(\"error\", line));\n\t\t}\n\t}\n\n\tprivate addListLine(line: string): void {\n\t\tthis.listContainer.addChild(new Text(truncateToWidth(line, this.listWidth), 0, 0));\n\t}\n\n\toverride render(width: number): string[] {\n\t\tif (this.listWidth !== width) {\n\t\t\tthis.listWidth = width;\n\t\t\tthis.updateList();\n\t\t}\n\t\treturn super.render(width);\n\t}\n\n\tprivate toggleModelScope(): void {\n\t\tif (this.scopedModels.length === 0) return;\n\t\tthis.scope = this.scope === \"all\" ? \"scoped\" : \"all\";\n\t\tif (this.view !== \"search\") {\n\t\t\tthis.view = \"providers\";\n\t\t\tthis.selectedProvider = null;\n\t\t\tthis.providerQuery = \"\";\n\t\t\tthis.providerIndex = 0;\n\t\t\tthis.searchInput.setValue(\"\");\n\t\t}\n\t\tthis.rebuildProviders();\n\t\tthis.selectCurrent();\n\t\tthis.filterItems(this.searchInput.getValue());\n\t}\n\n\tprivate moveSelection(delta: number): void {\n\t\tconst count = this.itemCount();\n\t\tif (count === 0) return;\n\t\tthis.selectedIndex = (this.selectedIndex + delta + count) % count;\n\t\tthis.updateList();\n\t}\n\n\tprivate confirmSelection(): void {\n\t\tif (this.loading) return;\n\t\tif (this.view === \"providers\") {\n\t\t\tconst provider = this.filteredProviders[this.selectedIndex];\n\t\t\tif (!provider) return;\n\t\t\tthis.providerQuery = this.searchInput.getValue();\n\t\t\tthis.providerIndex = this.selectedIndex;\n\t\t\tthis.selectedProvider = provider;\n\t\t\tthis.view = \"models\";\n\t\t\tthis.searchInput.setValue(\"\");\n\t\t\tthis.selectCurrent();\n\t\t\tthis.filterItems(\"\");\n\t\t\treturn;\n\t\t}\n\t\tconst item = this.filteredModels[this.selectedIndex];\n\t\tif (!item) return;\n\t\tif (this.persistDefault) this.settingsManager.setDefaultModelAndProvider(item.provider, item.id);\n\t\tthis.onSelectCallback(item.model);\n\t}\n\n\thandleInput(keyData: string): void {\n\t\tconst kb = getKeybindings();\n\t\tif (kb.matches(keyData, \"tui.input.tab\")) {\n\t\t\tthis.toggleModelScope();\n\t\t} else if (kb.matches(keyData, \"tui.select.up\")) {\n\t\t\tthis.moveSelection(-1);\n\t\t} else if (kb.matches(keyData, \"tui.select.down\")) {\n\t\t\tthis.moveSelection(1);\n\t\t} else if (kb.matches(keyData, \"tui.select.pageUp\")) {\n\t\t\tthis.moveSelection(-Math.min(10, Math.max(0, this.itemCount() - 1)));\n\t\t} else if (kb.matches(keyData, \"tui.select.pageDown\")) {\n\t\t\tthis.moveSelection(Math.min(10, Math.max(0, this.itemCount() - 1)));\n\t\t} else if (kb.matches(keyData, \"tui.select.confirm\")) {\n\t\t\tthis.confirmSelection();\n\t\t} else if (kb.matches(keyData, \"tui.select.cancel\")) {\n\t\t\tif (this.view === \"models\") {\n\t\t\t\tthis.view = \"providers\";\n\t\t\t\tthis.selectedProvider = null;\n\t\t\t\tthis.searchInput.setValue(this.providerQuery);\n\t\t\t\tthis.filterItems(this.providerQuery);\n\t\t\t\tthis.selectedIndex = Math.min(this.providerIndex, Math.max(0, this.itemCount() - 1));\n\t\t\t\tthis.updateList();\n\t\t\t} else {\n\t\t\t\tthis.onCancelCallback();\n\t\t\t}\n\t\t} else {\n\t\t\tconst query = this.searchInput.getValue();\n\t\t\tthis.searchInput.handleInput(keyData);\n\t\t\tif (query !== this.searchInput.getValue()) this.filterItems(this.searchInput.getValue());\n\t\t}\n\t}\n\n\tgetSearchInput(): Input {\n\t\treturn this.searchInput;\n\t}\n}\n"]}