zoe-agent 0.3.1

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 (267) hide show
  1. package/CHANGELOG.md +154 -0
  2. package/LICENSE +96 -0
  3. package/README.md +568 -0
  4. package/dist/adapters/cli/agent.d.ts +59 -0
  5. package/dist/adapters/cli/agent.js +232 -0
  6. package/dist/adapters/cli/bootstrap.d.ts +25 -0
  7. package/dist/adapters/cli/bootstrap.js +204 -0
  8. package/dist/adapters/cli/commands/build-registry.d.ts +14 -0
  9. package/dist/adapters/cli/commands/build-registry.js +88 -0
  10. package/dist/adapters/cli/commands/clear.d.ts +7 -0
  11. package/dist/adapters/cli/commands/clear.js +10 -0
  12. package/dist/adapters/cli/commands/compact.d.ts +13 -0
  13. package/dist/adapters/cli/commands/compact.js +96 -0
  14. package/dist/adapters/cli/commands/exit.d.ts +7 -0
  15. package/dist/adapters/cli/commands/exit.js +9 -0
  16. package/dist/adapters/cli/commands/gateway.d.ts +7 -0
  17. package/dist/adapters/cli/commands/gateway.js +152 -0
  18. package/dist/adapters/cli/commands/help.d.ts +9 -0
  19. package/dist/adapters/cli/commands/help.js +12 -0
  20. package/dist/adapters/cli/commands/models.d.ts +10 -0
  21. package/dist/adapters/cli/commands/models.js +32 -0
  22. package/dist/adapters/cli/commands/registry.d.ts +70 -0
  23. package/dist/adapters/cli/commands/registry.js +111 -0
  24. package/dist/adapters/cli/commands/settings-utils.d.ts +38 -0
  25. package/dist/adapters/cli/commands/settings-utils.js +182 -0
  26. package/dist/adapters/cli/commands/settings.d.ts +9 -0
  27. package/dist/adapters/cli/commands/settings.js +395 -0
  28. package/dist/adapters/cli/commands/skills.d.ts +7 -0
  29. package/dist/adapters/cli/commands/skills.js +21 -0
  30. package/dist/adapters/cli/config-loader.d.ts +27 -0
  31. package/dist/adapters/cli/config-loader.js +48 -0
  32. package/dist/adapters/cli/docker-utils.d.ts +37 -0
  33. package/dist/adapters/cli/docker-utils.js +90 -0
  34. package/dist/adapters/cli/index.d.ts +2 -0
  35. package/dist/adapters/cli/index.js +88 -0
  36. package/dist/adapters/cli/repl.d.ts +22 -0
  37. package/dist/adapters/cli/repl.js +256 -0
  38. package/dist/adapters/cli/setup.d.ts +19 -0
  39. package/dist/adapters/cli/setup.js +613 -0
  40. package/dist/adapters/cli/system-prompts.d.ts +56 -0
  41. package/dist/adapters/cli/system-prompts.js +131 -0
  42. package/dist/adapters/cli/tui/app.d.ts +58 -0
  43. package/dist/adapters/cli/tui/app.js +314 -0
  44. package/dist/adapters/cli/tui/components/assistant-message.d.ts +5 -0
  45. package/dist/adapters/cli/tui/components/assistant-message.js +9 -0
  46. package/dist/adapters/cli/tui/components/autocomplete.d.ts +19 -0
  47. package/dist/adapters/cli/tui/components/autocomplete.js +75 -0
  48. package/dist/adapters/cli/tui/components/command-palette.d.ts +15 -0
  49. package/dist/adapters/cli/tui/components/command-palette.js +50 -0
  50. package/dist/adapters/cli/tui/components/diff-viewer.d.ts +5 -0
  51. package/dist/adapters/cli/tui/components/diff-viewer.js +109 -0
  52. package/dist/adapters/cli/tui/components/error-message.d.ts +5 -0
  53. package/dist/adapters/cli/tui/components/error-message.js +8 -0
  54. package/dist/adapters/cli/tui/components/footer.d.ts +20 -0
  55. package/dist/adapters/cli/tui/components/footer.js +19 -0
  56. package/dist/adapters/cli/tui/components/goal-status.d.ts +12 -0
  57. package/dist/adapters/cli/tui/components/goal-status.js +22 -0
  58. package/dist/adapters/cli/tui/components/info-message.d.ts +5 -0
  59. package/dist/adapters/cli/tui/components/info-message.js +8 -0
  60. package/dist/adapters/cli/tui/components/logo-banner.d.ts +7 -0
  61. package/dist/adapters/cli/tui/components/logo-banner.js +33 -0
  62. package/dist/adapters/cli/tui/components/markdown.d.ts +9 -0
  63. package/dist/adapters/cli/tui/components/markdown.js +92 -0
  64. package/dist/adapters/cli/tui/components/message-area.d.ts +19 -0
  65. package/dist/adapters/cli/tui/components/message-area.js +55 -0
  66. package/dist/adapters/cli/tui/components/permission-prompt.d.ts +13 -0
  67. package/dist/adapters/cli/tui/components/permission-prompt.js +32 -0
  68. package/dist/adapters/cli/tui/components/prompt-area.d.ts +22 -0
  69. package/dist/adapters/cli/tui/components/prompt-area.js +68 -0
  70. package/dist/adapters/cli/tui/components/text-input.d.ts +27 -0
  71. package/dist/adapters/cli/tui/components/text-input.js +142 -0
  72. package/dist/adapters/cli/tui/components/tool-call-block.d.ts +11 -0
  73. package/dist/adapters/cli/tui/components/tool-call-block.js +68 -0
  74. package/dist/adapters/cli/tui/components/user-message.d.ts +5 -0
  75. package/dist/adapters/cli/tui/components/user-message.js +8 -0
  76. package/dist/adapters/cli/tui/diff/file-write-meta.d.ts +11 -0
  77. package/dist/adapters/cli/tui/diff/file-write-meta.js +11 -0
  78. package/dist/adapters/cli/tui/diff/line-diff.d.ts +17 -0
  79. package/dist/adapters/cli/tui/diff/line-diff.js +44 -0
  80. package/dist/adapters/cli/tui/feed-serializer.d.ts +29 -0
  81. package/dist/adapters/cli/tui/feed-serializer.js +70 -0
  82. package/dist/adapters/cli/tui/file-index.d.ts +8 -0
  83. package/dist/adapters/cli/tui/file-index.js +41 -0
  84. package/dist/adapters/cli/tui/hooks/use-agent.d.ts +54 -0
  85. package/dist/adapters/cli/tui/hooks/use-agent.js +177 -0
  86. package/dist/adapters/cli/tui/hooks/use-feed.d.ts +16 -0
  87. package/dist/adapters/cli/tui/hooks/use-feed.js +25 -0
  88. package/dist/adapters/cli/tui/hooks/use-file-watcher.d.ts +10 -0
  89. package/dist/adapters/cli/tui/hooks/use-file-watcher.js +43 -0
  90. package/dist/adapters/cli/tui/hooks/use-keybindings.d.ts +16 -0
  91. package/dist/adapters/cli/tui/hooks/use-keybindings.js +25 -0
  92. package/dist/adapters/cli/tui/hooks/use-theme.d.ts +8 -0
  93. package/dist/adapters/cli/tui/hooks/use-theme.js +12 -0
  94. package/dist/adapters/cli/tui/index.d.ts +19 -0
  95. package/dist/adapters/cli/tui/index.js +206 -0
  96. package/dist/adapters/cli/tui/ink-reset.d.ts +29 -0
  97. package/dist/adapters/cli/tui/ink-reset.js +57 -0
  98. package/dist/adapters/cli/tui/layout.d.ts +15 -0
  99. package/dist/adapters/cli/tui/layout.js +15 -0
  100. package/dist/adapters/cli/tui/logo/gradient.d.ts +11 -0
  101. package/dist/adapters/cli/tui/logo/gradient.js +31 -0
  102. package/dist/adapters/cli/tui/overlays/help-dialog.d.ts +4 -0
  103. package/dist/adapters/cli/tui/overlays/help-dialog.js +26 -0
  104. package/dist/adapters/cli/tui/overlays/model-selector.d.ts +14 -0
  105. package/dist/adapters/cli/tui/overlays/model-selector.js +43 -0
  106. package/dist/adapters/cli/tui/overlays/session-selector.d.ts +35 -0
  107. package/dist/adapters/cli/tui/overlays/session-selector.js +162 -0
  108. package/dist/adapters/cli/tui/overlays/settings-overlay.d.ts +24 -0
  109. package/dist/adapters/cli/tui/overlays/settings-overlay.js +126 -0
  110. package/dist/adapters/cli/tui/session-export.d.ts +21 -0
  111. package/dist/adapters/cli/tui/session-export.js +63 -0
  112. package/dist/adapters/cli/tui/theme.d.ts +23 -0
  113. package/dist/adapters/cli/tui/theme.js +22 -0
  114. package/dist/adapters/cli/tui/types.d.ts +52 -0
  115. package/dist/adapters/cli/tui/types.js +12 -0
  116. package/dist/adapters/sdk/agent.d.ts +20 -0
  117. package/dist/adapters/sdk/agent.js +356 -0
  118. package/dist/adapters/sdk/http.d.ts +43 -0
  119. package/dist/adapters/sdk/http.js +61 -0
  120. package/dist/adapters/sdk/index.d.ts +58 -0
  121. package/dist/adapters/sdk/index.js +209 -0
  122. package/dist/adapters/sdk/settings.d.ts +18 -0
  123. package/dist/adapters/sdk/settings.js +57 -0
  124. package/dist/adapters/sdk/tools.d.ts +7 -0
  125. package/dist/adapters/sdk/tools.js +13 -0
  126. package/dist/adapters/server/auth.d.ts +53 -0
  127. package/dist/adapters/server/auth.js +168 -0
  128. package/dist/adapters/server/index.d.ts +40 -0
  129. package/dist/adapters/server/index.js +255 -0
  130. package/dist/adapters/server/rest-gateway.d.ts +13 -0
  131. package/dist/adapters/server/rest-gateway.js +218 -0
  132. package/dist/adapters/server/rest.d.ts +37 -0
  133. package/dist/adapters/server/rest.js +341 -0
  134. package/dist/adapters/server/server-core.d.ts +55 -0
  135. package/dist/adapters/server/server-core.js +121 -0
  136. package/dist/adapters/server/session-store.d.ts +81 -0
  137. package/dist/adapters/server/session-store.js +272 -0
  138. package/dist/adapters/server/settings-handlers.d.ts +24 -0
  139. package/dist/adapters/server/settings-handlers.js +360 -0
  140. package/dist/adapters/server/standalone.d.ts +19 -0
  141. package/dist/adapters/server/standalone.js +113 -0
  142. package/dist/adapters/server/websocket.d.ts +26 -0
  143. package/dist/adapters/server/websocket.js +68 -0
  144. package/dist/adapters/server/ws-handlers.d.ts +32 -0
  145. package/dist/adapters/server/ws-handlers.js +523 -0
  146. package/dist/adapters/server/ws-types.d.ts +304 -0
  147. package/dist/adapters/server/ws-types.js +7 -0
  148. package/dist/core/agent-loop.d.ts +68 -0
  149. package/dist/core/agent-loop.js +423 -0
  150. package/dist/core/config.d.ts +115 -0
  151. package/dist/core/config.js +189 -0
  152. package/dist/core/errors.d.ts +58 -0
  153. package/dist/core/errors.js +88 -0
  154. package/dist/core/hooks.d.ts +35 -0
  155. package/dist/core/hooks.js +49 -0
  156. package/dist/core/index.d.ts +23 -0
  157. package/dist/core/index.js +29 -0
  158. package/dist/core/message-convert.d.ts +41 -0
  159. package/dist/core/message-convert.js +94 -0
  160. package/dist/core/middleware/auth.d.ts +24 -0
  161. package/dist/core/middleware/auth.js +28 -0
  162. package/dist/core/middleware/logging.d.ts +23 -0
  163. package/dist/core/middleware/logging.js +28 -0
  164. package/dist/core/middleware/rate-limit.d.ts +27 -0
  165. package/dist/core/middleware/rate-limit.js +38 -0
  166. package/dist/core/middleware/semantic-tools.d.ts +10 -0
  167. package/dist/core/middleware/semantic-tools.js +43 -0
  168. package/dist/core/middleware.d.ts +48 -0
  169. package/dist/core/middleware.js +38 -0
  170. package/dist/core/permission.d.ts +25 -0
  171. package/dist/core/permission.js +50 -0
  172. package/dist/core/provider-config.d.ts +129 -0
  173. package/dist/core/provider-config.js +273 -0
  174. package/dist/core/provider-env.d.ts +39 -0
  175. package/dist/core/provider-env.js +142 -0
  176. package/dist/core/provider-resolver.d.ts +12 -0
  177. package/dist/core/provider-resolver.js +12 -0
  178. package/dist/core/session-store.d.ts +75 -0
  179. package/dist/core/session-store.js +245 -0
  180. package/dist/core/settings-manager.d.ts +57 -0
  181. package/dist/core/settings-manager.js +359 -0
  182. package/dist/core/settings-schema.d.ts +38 -0
  183. package/dist/core/settings-schema.js +171 -0
  184. package/dist/core/skill-catalog.d.ts +6 -0
  185. package/dist/core/skill-catalog.js +17 -0
  186. package/dist/core/skill-invoker.d.ts +127 -0
  187. package/dist/core/skill-invoker.js +182 -0
  188. package/dist/core/stream-accumulator.d.ts +21 -0
  189. package/dist/core/stream-accumulator.js +51 -0
  190. package/dist/core/stream-manager.d.ts +58 -0
  191. package/dist/core/stream-manager.js +212 -0
  192. package/dist/core/tool-executor.d.ts +84 -0
  193. package/dist/core/tool-executor.js +256 -0
  194. package/dist/core/types.d.ts +259 -0
  195. package/dist/core/types.js +11 -0
  196. package/dist/gateway/gateway.d.ts +52 -0
  197. package/dist/gateway/gateway.js +537 -0
  198. package/dist/gateway/index.d.ts +21 -0
  199. package/dist/gateway/index.js +31 -0
  200. package/dist/gateway/openapi-importer.d.ts +15 -0
  201. package/dist/gateway/openapi-importer.js +66 -0
  202. package/dist/gateway/semantic-scorer.d.ts +7 -0
  203. package/dist/gateway/semantic-scorer.js +24 -0
  204. package/dist/gateway/settings-adapter.d.ts +49 -0
  205. package/dist/gateway/settings-adapter.js +137 -0
  206. package/dist/gateway/tool-factory.d.ts +9 -0
  207. package/dist/gateway/tool-factory.js +414 -0
  208. package/dist/gateway/types.d.ts +68 -0
  209. package/dist/gateway/types.js +7 -0
  210. package/dist/models-catalog.js +46 -0
  211. package/dist/providers/anthropic.d.ts +22 -0
  212. package/dist/providers/anthropic.js +148 -0
  213. package/dist/providers/factory.d.ts +10 -0
  214. package/dist/providers/factory.js +25 -0
  215. package/dist/providers/openai.d.ts +15 -0
  216. package/dist/providers/openai.js +71 -0
  217. package/dist/providers/types.d.ts +48 -0
  218. package/dist/providers/types.js +1 -0
  219. package/dist/skills/args.d.ts +37 -0
  220. package/dist/skills/args.js +99 -0
  221. package/dist/skills/index.d.ts +11 -0
  222. package/dist/skills/index.js +23 -0
  223. package/dist/skills/loader.d.ts +3 -0
  224. package/dist/skills/loader.js +59 -0
  225. package/dist/skills/parser.d.ts +7 -0
  226. package/dist/skills/parser.js +152 -0
  227. package/dist/skills/registry.d.ts +13 -0
  228. package/dist/skills/registry.js +74 -0
  229. package/dist/skills/resolver.d.ts +19 -0
  230. package/dist/skills/resolver.js +116 -0
  231. package/dist/skills/types.d.ts +74 -0
  232. package/dist/skills/types.js +50 -0
  233. package/dist/tools/browser.d.ts +2 -0
  234. package/dist/tools/browser.js +68 -0
  235. package/dist/tools/core.d.ts +20 -0
  236. package/dist/tools/core.js +244 -0
  237. package/dist/tools/email.d.ts +2 -0
  238. package/dist/tools/email.js +61 -0
  239. package/dist/tools/image.d.ts +2 -0
  240. package/dist/tools/image.js +257 -0
  241. package/dist/tools/index.d.ts +2 -0
  242. package/dist/tools/index.js +88 -0
  243. package/dist/tools/interface.d.ts +22 -0
  244. package/dist/tools/interface.js +1 -0
  245. package/dist/tools/notify.d.ts +2 -0
  246. package/dist/tools/notify.js +100 -0
  247. package/dist/tools/prompt-optimizer.d.ts +2 -0
  248. package/dist/tools/prompt-optimizer.js +65 -0
  249. package/dist/tools/screenshot.d.ts +2 -0
  250. package/dist/tools/screenshot.js +184 -0
  251. package/dist/tools/search.d.ts +2 -0
  252. package/dist/tools/search.js +78 -0
  253. package/dist/tools/todos.d.ts +10 -0
  254. package/dist/tools/todos.js +50 -0
  255. package/package.json +119 -0
  256. package/skills/docker-ops/SKILL.md +329 -0
  257. package/skills/k8s-deploy/SKILL.md +397 -0
  258. package/skills/log-analyzer/SKILL.md +331 -0
  259. package/skills/speckit-analyze/SKILL.md +260 -0
  260. package/skills/speckit-checklist/SKILL.md +374 -0
  261. package/skills/speckit-clarify/SKILL.md +286 -0
  262. package/skills/speckit-constitution/SKILL.md +157 -0
  263. package/skills/speckit-implement/SKILL.md +224 -0
  264. package/skills/speckit-plan/SKILL.md +171 -0
  265. package/skills/speckit-specify/SKILL.md +346 -0
  266. package/skills/speckit-tasks/SKILL.md +215 -0
  267. package/skills/speckit-taskstoissues/SKILL.md +107 -0
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Zoe CLI — System Prompts
3
+ *
4
+ * Two system prompts, selected by launch mode:
5
+ * - non-interactive (headless / piped / docker / --no-interactive):
6
+ * the Docker-native "worker unit" prompt — byte-identical to the
7
+ * historical CLI system prompt.
8
+ * - interactive (TTY + interactive flag): a general-purpose agent
9
+ * prompt tuned for a live terminal session (the TUI, or
10
+ * interactive readline).
11
+ *
12
+ * Mode detection reuses the CLI's existing signals — Commander's
13
+ * `options.interactive` (`--no-interactive`) and `isNonInteractive()`
14
+ * (TTY / docker / env). Core's `runAgentLoop` stays mode-agnostic: it
15
+ * only receives the selected prompt string.
16
+ */
17
+ import * as os from 'os';
18
+ import { isNonInteractive } from './docker-utils.js';
19
+ /**
20
+ * Shared, runtime-derived environment block embedded in every prompt.
21
+ * Leading and trailing newlines are intentional — callers interpolate it
22
+ * between section headers.
23
+ */
24
+ export function buildSystemInfoBlock() {
25
+ return `
26
+ System Information:
27
+ - OS: ${os.type()} ${os.release()} (${os.platform()})
28
+ - Architecture: ${os.arch()}
29
+ - Node.js Version: ${process.version}
30
+ - Current Working Directory: ${process.cwd()}
31
+ - User: ${os.userInfo().username}
32
+ - Home Directory: ${os.homedir()}
33
+ - Current Date: ${new Date().toLocaleString()}
34
+ `;
35
+ }
36
+ /**
37
+ * Non-interactive / Docker / headless prompt.
38
+ * Byte-identical to the historical CLI system prompt.
39
+ */
40
+ export function buildSystemPrompt() {
41
+ return `You are Zoe, a Docker-Native Autonomous Agent designed for massive scale automation.
42
+ You are likely running inside a container or headless server, possibly as one of thousands of parallel units in a swarm.
43
+
44
+ CONTEXT:
45
+ ${buildSystemInfoBlock()}
46
+
47
+ ENVIRONMENT CONSTRAINTS:
48
+ 1. HEADLESS: No GUI available. Do not try to open browsers or apps.
49
+ 2. CONTAINER-OPTIMIZED: Assume you are in a sandbox. You can be aggressive with file creation but robust with errors.
50
+ 3. NON-INTERACTIVE: Always use flags to suppress prompts (e.g., 'apt-get -y', 'rm -rf').
51
+
52
+ GUIDELINES:
53
+ 1. EFFICIENCY: Your goal is speed and success. Write scripts that just work.
54
+ 2. ROBUSTNESS: Use standard Linux/Unix tools found in minimal images (Alpine/Debian).
55
+ 3. TOOLS: Use 'execute_shell_command' for actions, 'write_file' for code generation.
56
+ 4. CLARITY: Output concise logs. You are a worker unit, not a chat bot.
57
+ 5. OPTIMIZATION: When asked to generate creative content (images, stories, complex code), use 'optimize_prompt' first to ensure the best possible output quality.`;
58
+ }
59
+ /**
60
+ * Interactive prompt for terminal sessions (TUI or interactive readline).
61
+ *
62
+ * Role, tool list, numbered process, and output format follow the
63
+ * interactive-agent conventions shared by tools like Command Code; the
64
+ * working principles mirror this project's own engineering standards
65
+ * (think before acting, surgical changes, simplicity, goal-driven).
66
+ */
67
+ export function buildInteractiveSystemPrompt() {
68
+ return `You are Zoe — the user's AI person. You're a general-purpose assistant in a terminal who gains new capabilities through skills. Coding is one of the things you do, not the whole of it: you also research, write, automate, communicate, and generate media, and each loaded skill adds more. You work through conversation, tool calls, and verified results.
69
+
70
+ CONTEXT:
71
+ ${buildSystemInfoBlock()}
72
+
73
+ TOOLS AVAILABLE:
74
+ - execute_shell_command: Run shell commands
75
+ - read_file / write_file: Read and write files
76
+ - get_current_datetime: Current date and time
77
+ - web_search, send_email, send_notification: Look things up and communicate
78
+ - read_website, take_screenshot, generate_image, optimize_prompt: Advanced tools
79
+ - use_skill: Invoke a domain skill (loaded skills are listed at startup)
80
+ - manage_todos: Maintain a visible task list (pending / in_progress / completed / blocked). Replace the full list each call.
81
+
82
+ TOOL RULES:
83
+ - Non-interactive flags always: shell commands must never prompt — pass -y/--yes (e.g. apt-get -y, rm -f) so they don't hang waiting on stdin.
84
+ - Optimize first for creative work: when asked for creative output (images via generate_image, stories, or complex code), call optimize_prompt on the request before generating, to maximize quality.
85
+ - Track multi-step work with manage_todos: for any task with 2 or more steps, call manage_todos FIRST with the full plan (every item status "pending"), mark one item "in_progress" when you start it, and mark items "completed" (or "blocked") as you finish. Replace the ENTIRE list on every call — do not append. This keeps the user informed of progress in the task panel. Treat "add N items to the todo/task list", "make a plan", and similar as an explicit request to use manage_todos.
86
+
87
+ WORKING PRINCIPLES:
88
+ 1. Think before acting. State assumptions. If a request is ambiguous or a simpler approach exists, say so before implementing.
89
+ 2. Surgical changes. Touch only what the task requires. Match existing code style. Don't refactor working code unprompted.
90
+ 3. Simplicity first. Write the minimum code that solves the problem. No speculative features.
91
+ 4. Goal-driven. Know what "done" means, then verify it — run the tests, re-read the changed code, show the evidence.
92
+
93
+ PROCESS:
94
+ 1. Understand: read the relevant files before editing. Don't guess at structure.
95
+ 2. Plan: for non-trivial changes, outline the approach in a few lines first.
96
+ 3. Act: make focused edits; prefer targeted edits over full rewrites.
97
+ 4. Verify: run a build or tests, or re-read the result, to confirm the change works.
98
+
99
+ OUTPUT:
100
+ - Be concise. Lead with what you did and what to check, not preamble.
101
+ - Use short fenced code blocks for commands and code.
102
+ - When a tool changes files, name the files and summarize the diff in one line.
103
+ - Stop when the task is verified complete, or state precisely what is blocking you.
104
+
105
+ The user is present and interactive. You may ask a clarifying question when truly blocked, but prefer to make a reasonable choice, proceed, and note the assumption.`;
106
+ }
107
+ /**
108
+ * Resolve launch mode from the CLI's two existing interactive signals.
109
+ *
110
+ * A session is interactive only when the Commander interactive flag is on
111
+ * (i.e. not `--no-interactive`) AND the process is in an interactive
112
+ * context (TTY, not docker, no non-interactive env). This matches every
113
+ * documented launch path:
114
+ * - plain `zoe` in a TTY -> interactive
115
+ * - `zoe -n` / `--no-interactive` -> non-interactive
116
+ * - piped stdin -> non-interactive
117
+ * - `zoe --docker` -> non-interactive
118
+ */
119
+ export function resolveLaunchMode(options) {
120
+ if (options.interactive === false)
121
+ return 'non-interactive';
122
+ if (isNonInteractive())
123
+ return 'non-interactive';
124
+ return 'interactive';
125
+ }
126
+ /**
127
+ * Select the system prompt for a launch mode.
128
+ */
129
+ export function selectSystemPrompt(mode) {
130
+ return mode === 'interactive' ? buildInteractiveSystemPrompt() : buildSystemPrompt();
131
+ }
@@ -0,0 +1,58 @@
1
+ import { type ModelOption } from './overlays/model-selector.js';
2
+ import { type SessionListItem } from './overlays/session-selector.js';
3
+ import { type SettingItem } from './overlays/settings-overlay.js';
4
+ import type { Suggestion } from './components/autocomplete.js';
5
+ import type { Agent } from '../agent.js';
6
+ import type { PermissionLevel } from '../../../core/types.js';
7
+ /** Outcome of dispatching a slash command in the TUI (built in startTui). */
8
+ export interface TuiCommandOutcome {
9
+ status: 'handled' | 'fallthrough';
10
+ /** Command owns stdin/stdout — the TUI can't run it (deferred to a later phase). */
11
+ deferred?: boolean;
12
+ /** ANSI-styled text; the TUI strips ANSI before rendering. */
13
+ output?: string;
14
+ /** Session should terminate. */
15
+ exit?: boolean;
16
+ }
17
+ interface TuiAppProps {
18
+ agent: Agent;
19
+ permissionLevel?: PermissionLevel;
20
+ initialQuery?: string;
21
+ onExit: () => void;
22
+ dispatchCommand: (input: string) => Promise<TuiCommandOutcome>;
23
+ commands: Suggestion[];
24
+ skills: Suggestion[];
25
+ /** Reset Ink's accumulated Static output + clear screen before a `<Static>`
26
+ * remount (resize / expand / session-resume) so history repaints cleanly. */
27
+ resetView: () => void;
28
+ /** Footer status info from the session. */
29
+ providerType: string;
30
+ gatewayOn: boolean;
31
+ skillCount: number;
32
+ mcpCount: number;
33
+ modelOptions: ModelOption[];
34
+ onSwitchModel: (providerType: string, modelId: string) => Promise<void>;
35
+ getSettingsList: () => SettingItem[];
36
+ onSetSetting: (dotKey: string, value: string) => Promise<void>;
37
+ listSessions: () => Promise<SessionListItem[]>;
38
+ onSwitchSession: (sessionId: string) => Promise<{
39
+ preview: string;
40
+ userMessageCount: number;
41
+ toolCallCount: number;
42
+ } | null>;
43
+ onDeleteSession: (sessionId: string) => Promise<void>;
44
+ onExportSession: (sessionId: string) => Promise<string | null>;
45
+ onTranscriptSession: (sessionId: string) => Promise<string | null>;
46
+ onRenameSession: (sessionId: string, title: string) => Promise<boolean>;
47
+ getSessionId: () => string;
48
+ }
49
+ /**
50
+ * TuiApp — Ink `<Static>` + native terminal scroll (like Command Code: the wheel
51
+ * scrolls the terminal's own scrollback, so no mouse capture / no gibberish).
52
+ * `<MessageArea>` grows the scrollback; the live region swaps between modal
53
+ * overlays, the inline permission prompt, a "working" indicator, and the input
54
+ * prompt. A status footer is always at the bottom of the written content.
55
+ * `ink-reset.ts` (`resetView`) keeps resize/expand repaints artifact-free.
56
+ */
57
+ export declare function TuiApp({ agent, permissionLevel, initialQuery, onExit, dispatchCommand, commands, skills, resetView, providerType, gatewayOn, skillCount, mcpCount, modelOptions, onSwitchModel, getSettingsList, onSetSetting, listSessions, onSwitchSession, onDeleteSession, onExportSession, onTranscriptSession, onRenameSession, getSessionId, }: TuiAppProps): import("react").JSX.Element;
58
+ export {};
@@ -0,0 +1,314 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useRef, useState } from 'react';
3
+ import { Box, Text } from 'ink';
4
+ import { useTheme } from './hooks/use-theme.js';
5
+ import { useFeed } from './hooks/use-feed.js';
6
+ import { useAgent } from './hooks/use-agent.js';
7
+ import { useKeybindings } from './hooks/use-keybindings.js';
8
+ import { useFileWatcher } from './hooks/use-file-watcher.js';
9
+ import { MessageArea } from './components/message-area.js';
10
+ import { PromptArea } from './components/prompt-area.js';
11
+ import { PermissionPrompt } from './components/permission-prompt.js';
12
+ import { AssistantMessage } from './components/assistant-message.js';
13
+ import { ToolCallBlock } from './components/tool-call-block.js';
14
+ import { GoalStatus } from './components/goal-status.js';
15
+ import { Footer } from './components/footer.js';
16
+ import Spinner from 'ink-spinner';
17
+ import { CommandPalette } from './components/command-palette.js';
18
+ import { HelpDialog } from './overlays/help-dialog.js';
19
+ import { ModelSelector } from './overlays/model-selector.js';
20
+ import { SessionSelector } from './overlays/session-selector.js';
21
+ import { SettingsEditor } from './overlays/settings-overlay.js';
22
+ import { messagesToFeedEntries } from './feed-serializer.js';
23
+ import { getModelMeta } from '../../../models-catalog.js';
24
+ import { HORIZONTAL_PADDING } from './layout.js';
25
+ /** Strip ANSI escapes — handler output is chalk-styled for the readline path. */
26
+ function stripAnsi(text) {
27
+ return text.replace(/\x1b\[[0-9;?]*[a-zA-Z]/g, '');
28
+ }
29
+ /**
30
+ * TuiApp — Ink `<Static>` + native terminal scroll (like Command Code: the wheel
31
+ * scrolls the terminal's own scrollback, so no mouse capture / no gibberish).
32
+ * `<MessageArea>` grows the scrollback; the live region swaps between modal
33
+ * overlays, the inline permission prompt, a "working" indicator, and the input
34
+ * prompt. A status footer is always at the bottom of the written content.
35
+ * `ink-reset.ts` (`resetView`) keeps resize/expand repaints artifact-free.
36
+ */
37
+ export function TuiApp({ agent, permissionLevel, initialQuery, onExit, dispatchCommand, commands, skills, resetView, providerType, gatewayOn, skillCount, mcpCount, modelOptions, onSwitchModel, getSettingsList, onSetSetting, listSessions, onSwitchSession, onDeleteSession, onExportSession, onTranscriptSession, onRenameSession, getSessionId, }) {
38
+ const theme = useTheme();
39
+ const feed = useFeed();
40
+ const { isRunning, pendingPermission, streamingText, streamingTool, usage, contextTokens, latestTodos, submit, resolvePermission, abort, resetTodos, restoreTodos } = useAgent({
41
+ agent,
42
+ feed,
43
+ permissionLevel,
44
+ });
45
+ const [input, setInput] = useState('');
46
+ const [overlay, setOverlay] = useState(null);
47
+ const [settingsList, setSettingsList] = useState([]);
48
+ const [sessionsList, setSessionsList] = useState([]);
49
+ // File-watcher: notifies when project files change externally while idle.
50
+ const { changedFile, clear: clearFileChange } = useFileWatcher(!isRunning);
51
+ // Input history lives here (not in PromptArea) so it survives PromptArea
52
+ // unmounting during a run — otherwise every turn wiped the history.
53
+ const historyRef = useRef([]);
54
+ const historyIndexRef = useRef(-1);
55
+ // The in-progress prompt, saved on first ↑ so ↓ back to the present restores it.
56
+ const draftRef = useRef('');
57
+ // Queue: chat messages typed during a run are buffered and drained one per
58
+ // run completion (like other AI coding TUIs). isRunningRef mirrors state for
59
+ // synchronous reads inside the handleUserInput event handler.
60
+ const isRunningRef = useRef(false);
61
+ isRunningRef.current = isRunning;
62
+ const queueRef = useRef([]);
63
+ const [queuedCount, setQueuedCount] = useState(0);
64
+ const [staticKey, setStaticKey] = useState(0);
65
+ const [expanded, setExpanded] = useState(false);
66
+ // Resize → reset Ink's Static buffer + remount <Static> for a clean repaint.
67
+ useEffect(() => {
68
+ const onResize = () => {
69
+ resetView();
70
+ setStaticKey((k) => k + 1);
71
+ };
72
+ process.stdout.on('resize', onResize);
73
+ return () => {
74
+ process.stdout.off('resize', onResize);
75
+ };
76
+ }, [resetView]);
77
+ const didInit = useRef(false);
78
+ useEffect(() => {
79
+ if (didInit.current)
80
+ return;
81
+ didInit.current = true;
82
+ feed.appendEntry({ kind: 'logo' });
83
+ if (initialQuery && initialQuery.trim()) {
84
+ void submit(initialQuery);
85
+ }
86
+ }, [initialQuery, submit, feed]);
87
+ // Drain the queue when a run finishes — submit the next queued message.
88
+ // Each submission's completion (isRunning→false) re-fires this effect,
89
+ // draining one message at a time until the queue is empty.
90
+ useEffect(() => {
91
+ if (!isRunning && queueRef.current.length > 0) {
92
+ const next = queueRef.current.shift();
93
+ setQueuedCount(queueRef.current.length);
94
+ void submit(next);
95
+ }
96
+ }, [isRunning, submit]);
97
+ // Full clear — start a new session (the agent rotates the session id) + a
98
+ // fresh TUI: empty the feed, reset todos, re-seed the logo, clear the screen
99
+ // and remount <Static>. Shared by the `/clear` command and the Ctrl+L binding.
100
+ const clearAll = () => {
101
+ agent.clearConversation();
102
+ feed.clear();
103
+ resetTodos();
104
+ feed.appendEntry({ kind: 'logo' });
105
+ resetView();
106
+ setStaticKey((k) => k + 1);
107
+ };
108
+ // Run a /command via the shared registry; surface its output in the feed.
109
+ const runSlash = async (raw) => {
110
+ const name = raw.split(/\s+/)[0];
111
+ const result = await dispatchCommand(raw);
112
+ if (result.deferred) {
113
+ feed.appendEntry({ kind: 'assistant', content: `${name} is interactive — run it in the readline REPL (zoe), or wait for the TUI overlay.` });
114
+ }
115
+ else if (result.exit) {
116
+ onExit();
117
+ }
118
+ else if (result.output) {
119
+ feed.appendEntry({ kind: 'assistant', content: stripAnsi(result.output) });
120
+ }
121
+ else if (result.status === 'fallthrough') {
122
+ feed.appendEntry({ kind: 'assistant', content: `${name} skill launch from the TUI arrives in US2 — ask in chat, or run it in the readline REPL.` });
123
+ }
124
+ };
125
+ const handleUserInput = async (value) => {
126
+ const trimmed = value.trim();
127
+ if (trimmed) {
128
+ historyRef.current.push(trimmed);
129
+ historyIndexRef.current = -1;
130
+ }
131
+ setInput('');
132
+ clearFileChange();
133
+ // /steer <message> — interrupt the current run and send a new message.
134
+ if (trimmed === '/steer' || trimmed.startsWith('/steer ')) {
135
+ const steerMsg = trimmed.slice('/steer'.length).trim();
136
+ if (!steerMsg) {
137
+ feed.appendEntry({ kind: 'info', content: 'Usage: /steer <message> — interrupts the current run and sends a new message.' });
138
+ return;
139
+ }
140
+ if (isRunningRef.current) {
141
+ abort();
142
+ queueRef.current.unshift(steerMsg);
143
+ setQueuedCount(queueRef.current.length);
144
+ feed.appendEntry({ kind: 'info', content: 'Steering — current run aborted, sending next.' });
145
+ }
146
+ else {
147
+ void submit(steerMsg);
148
+ }
149
+ return;
150
+ }
151
+ // During an active run, queue chat messages and block all other commands.
152
+ if (isRunningRef.current) {
153
+ if (trimmed.startsWith('/')) {
154
+ feed.appendEntry({ kind: 'info', content: 'Command unavailable during a run — use /steer <message> to interrupt, or wait for the run to finish.' });
155
+ }
156
+ else if (trimmed) {
157
+ queueRef.current.push(trimmed);
158
+ setQueuedCount(queueRef.current.length);
159
+ feed.appendEntry({ kind: 'info', content: `Queued (${queueRef.current.length}) — will submit when the run finishes. /steer to send now.` });
160
+ }
161
+ return;
162
+ }
163
+ if (trimmed === '/?') {
164
+ setOverlay('help');
165
+ return;
166
+ }
167
+ if (trimmed === '/models' || trimmed === '/model') {
168
+ setOverlay('model');
169
+ return;
170
+ }
171
+ if (trimmed === '/sessions' || trimmed === '/session') {
172
+ setSessionsList(await listSessions());
173
+ setOverlay('sessions');
174
+ return;
175
+ }
176
+ // `/clear` starts a fresh session + TUI (logo, empty feed) — handled here,
177
+ // not via the registry (which only clears the agent, not the visible feed).
178
+ if (trimmed === '/clear') {
179
+ clearAll();
180
+ return;
181
+ }
182
+ {
183
+ const parts = trimmed.split(/\s+/);
184
+ const cmd = parts[0]?.toLowerCase();
185
+ const sub = parts[1]?.toLowerCase();
186
+ if (['/settings', '/setting', '/config'].includes(cmd)) {
187
+ if (sub === undefined) {
188
+ setSettingsList(getSettingsList());
189
+ setOverlay('settings');
190
+ return;
191
+ }
192
+ if (sub === 'set' && parts.length <= 3) {
193
+ feed.appendEntry({ kind: 'info', content: 'Provide a value: /settings set <key> <value> (e.g. /settings set gateway.enabled true)' });
194
+ return;
195
+ }
196
+ }
197
+ }
198
+ if (trimmed.startsWith('/')) {
199
+ await runSlash(trimmed);
200
+ }
201
+ else {
202
+ void submit(value);
203
+ }
204
+ };
205
+ const onHistoryUp = () => {
206
+ const h = historyRef.current;
207
+ if (h.length === 0)
208
+ return;
209
+ if (historyIndexRef.current === -1)
210
+ draftRef.current = input; // save in-progress prompt
211
+ const next = historyIndexRef.current === -1 ? h.length - 1 : Math.max(0, historyIndexRef.current - 1);
212
+ historyIndexRef.current = next;
213
+ setInput(h[next]);
214
+ };
215
+ const onHistoryDown = () => {
216
+ const h = historyRef.current;
217
+ if (h.length === 0 || historyIndexRef.current === -1)
218
+ return;
219
+ const next = historyIndexRef.current + 1;
220
+ if (next >= h.length) {
221
+ historyIndexRef.current = -1;
222
+ setInput(draftRef.current); // restore in-progress prompt
223
+ }
224
+ else {
225
+ historyIndexRef.current = next;
226
+ setInput(h[next]);
227
+ }
228
+ };
229
+ // Palette includes synthetic entries that open overlays.
230
+ const paletteCommands = [
231
+ ...commands,
232
+ { name: 'shortcuts', description: 'Keyboard reference' },
233
+ { name: 'model', description: 'Switch model' },
234
+ { name: 'settings', description: 'View settings' },
235
+ { name: 'sessions', description: 'Resume / delete a session' },
236
+ ];
237
+ const onPaletteRun = (name) => {
238
+ setOverlay(null);
239
+ if (name === 'shortcuts') {
240
+ setOverlay('help');
241
+ }
242
+ else if (name === 'model') {
243
+ setOverlay('model');
244
+ }
245
+ else if (name === 'settings') {
246
+ setSettingsList(getSettingsList());
247
+ setOverlay('settings');
248
+ }
249
+ else if (name === 'sessions') {
250
+ void (async () => {
251
+ setSessionsList(await listSessions());
252
+ setOverlay('sessions');
253
+ })();
254
+ }
255
+ else {
256
+ void runSlash('/' + name);
257
+ }
258
+ };
259
+ const handleSetSetting = async (dotKey, value) => {
260
+ await onSetSetting(dotKey, value);
261
+ setSettingsList(getSettingsList());
262
+ };
263
+ // Resume a session: load messages into the agent, rebuild the visual feed,
264
+ // and remount <Static> so history repaints without phantom duplicates.
265
+ const handleSelectSession = async (sessionId) => {
266
+ const summary = await onSwitchSession(sessionId);
267
+ setOverlay(null);
268
+ if (!summary) {
269
+ feed.appendEntry({ kind: 'info', content: `Session ${sessionId.slice(0, 8)} could not be loaded.` });
270
+ return;
271
+ }
272
+ feed.clear();
273
+ resetTodos();
274
+ const { entries, latestTodos: sessionTodos } = messagesToFeedEntries(agent.getMessages());
275
+ for (const entry of entries)
276
+ feed.appendEntry(entry);
277
+ restoreTodos(sessionTodos);
278
+ resetView();
279
+ setStaticKey((k) => k + 1);
280
+ feed.appendEntry({ kind: 'info', content: `Resumed session: ${summary.preview} (${summary.userMessageCount} turns, ${summary.toolCallCount} tool calls)` });
281
+ };
282
+ const handleDeleteSession = async (sessionId) => {
283
+ await onDeleteSession(sessionId);
284
+ setSessionsList(await listSessions());
285
+ };
286
+ const handleExportSession = async (sessionId) => {
287
+ const outPath = await onExportSession(sessionId);
288
+ feed.appendEntry({ kind: 'info', content: outPath ? `Exported session to ${outPath}` : `Session ${sessionId.slice(0, 8)} could not be exported.` });
289
+ };
290
+ const handleTranscriptSession = async (sessionId) => {
291
+ const outPath = await onTranscriptSession(sessionId);
292
+ feed.appendEntry({ kind: 'info', content: outPath ? `Transcript written to ${outPath}` : `Session ${sessionId.slice(0, 8)} could not be exported.` });
293
+ };
294
+ const handleRenameSession = async (sessionId, title) => {
295
+ const ok = await onRenameSession(sessionId, title);
296
+ if (ok)
297
+ setSessionsList(await listSessions());
298
+ return ok;
299
+ };
300
+ useKeybindings({
301
+ onAbort: abort,
302
+ onExit,
303
+ onExpandToggle: () => { resetView(); setExpanded((e) => !e); setStaticKey((k) => k + 1); },
304
+ onPalette: () => setOverlay('palette'),
305
+ onClear: clearAll,
306
+ }, { enabled: overlay === null, isRunning });
307
+ const showSpinner = isRunning && !streamingText && !pendingPermission;
308
+ // The bordered input is always visible (003 US1). While a tool needs approval
309
+ // the inline prompt replaces it; otherwise the spinner (with queued count)
310
+ // renders ABOVE the input — the input stays active so queued/steered messages
311
+ // can be typed during a run.
312
+ const inputAreaSlot = pendingPermission ? (_jsx(PermissionPrompt, { toolName: pendingPermission.toolName, args: pendingPermission.args, onResolve: resolvePermission })) : (_jsxs(Box, { flexDirection: "column", children: [showSpinner ? (_jsxs(Box, { children: [_jsxs(Text, { color: theme.yellow, children: [_jsx(Spinner, { type: "dots" }), " Zoe is working "] }), _jsxs(Text, { color: theme.fgDim, children: ["(Esc to abort)", queuedCount > 0 ? ` · ${queuedCount} queued` : ''] })] })) : null, _jsx(PromptArea, { value: input, onChange: setInput, onSubmit: (v) => { void handleUserInput(v); }, onHistoryUp: onHistoryUp, onHistoryDown: onHistoryDown, commands: commands, skills: skills })] }));
313
+ return (_jsxs(Box, { flexDirection: "column", paddingLeft: HORIZONTAL_PADDING, paddingRight: HORIZONTAL_PADDING, children: [_jsx(MessageArea, { entries: feed.entries, staticKey: staticKey, expanded: expanded }), latestTodos ? _jsx(GoalStatus, { todos: latestTodos }) : null, streamingText ? (_jsx(AssistantMessage, { entry: { id: '__streaming', kind: 'assistant', content: streamingText } })) : null, streamingTool ? (_jsx(ToolCallBlock, { entry: { id: '__running-tool', kind: 'tool', name: streamingTool.name, args: streamingTool.args, status: 'running', output: streamingTool.output }, expanded: true })) : null, _jsx(Box, { flexDirection: "column", children: overlay === 'palette' ? (_jsx(CommandPalette, { commands: paletteCommands, skills: skills, onRun: onPaletteRun, onClose: () => setOverlay(null) })) : overlay === 'help' ? (_jsx(HelpDialog, { onClose: () => setOverlay(null) })) : overlay === 'model' ? (_jsx(ModelSelector, { options: modelOptions, currentModel: agent.getModel(), onSwitch: (pt, m) => { setOverlay(null); void onSwitchModel(pt, m); }, onClose: () => setOverlay(null) })) : overlay === 'settings' ? (_jsx(SettingsEditor, { settings: settingsList, onSet: handleSetSetting, onClose: () => setOverlay(null) })) : overlay === 'sessions' ? (_jsx(SessionSelector, { sessions: sessionsList, currentSessionId: getSessionId(), onSelect: (id) => { void handleSelectSession(id); }, onDelete: (id) => { void handleDeleteSession(id); }, onExport: (id) => { void handleExportSession(id); }, onTranscript: (id) => { void handleTranscriptSession(id); }, onRename: (id, title) => handleRenameSession(id, title), onClose: () => setOverlay(null) })) : (inputAreaSlot) }), changedFile ? (_jsxs(Text, { color: theme.yellow, children: ["~ ", changedFile, " changed externally"] })) : null, _jsx(Footer, { providerType: providerType, model: agent.getModel(), usage: usage, permissionLevel: permissionLevel, skillCount: skillCount, gatewayOn: gatewayOn, mcpCount: mcpCount, contextTokens: contextTokens, contextWindow: getModelMeta(agent.getModel())?.contextWindow })] }));
314
+ }
@@ -0,0 +1,5 @@
1
+ import type { AssistantMessageEntry } from '../types.js';
2
+ /** An LLM text response entry — blue speaker token + markdown-rendered content. */
3
+ export declare function AssistantMessage({ entry }: {
4
+ entry: AssistantMessageEntry;
5
+ }): import("react").JSX.Element;
@@ -0,0 +1,9 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ import { useTheme } from '../hooks/use-theme.js';
4
+ import { Markdown } from './markdown.js';
5
+ /** An LLM text response entry — blue speaker token + markdown-rendered content. */
6
+ export function AssistantMessage({ entry }) {
7
+ const theme = useTheme();
8
+ return (_jsxs(Box, { children: [_jsx(Text, { color: theme.blue, bold: true, children: "Zoe \u203A " }), _jsx(Markdown, { content: entry.content })] }));
9
+ }
@@ -0,0 +1,19 @@
1
+ export interface Suggestion {
2
+ name: string;
3
+ description?: string;
4
+ }
5
+ /** Filter to matches and rank by relevance score (desc). */
6
+ export declare function fuzzyFilter(items: Suggestion[], query: string): Suggestion[];
7
+ /**
8
+ * Fuzzy suggestion dropdown rendered above the prompt. Caller owns selection
9
+ * state and key handling (Tab/arrows/Esc in `prompt-area`); this is pure view.
10
+ *
11
+ * The list is windowed: only MAX_VISIBLE rows render, and the window follows
12
+ * `selectedIndex` (clamped) so ↑/↓ scroll through every match — not just the
13
+ * first page. Count hints show how many are hidden above/below.
14
+ */
15
+ export declare function Autocomplete({ suggestions, selectedIndex, prefix, }: {
16
+ suggestions: Suggestion[];
17
+ selectedIndex: number;
18
+ prefix: '/' | '@';
19
+ }): import("react").JSX.Element | null;
@@ -0,0 +1,75 @@
1
+ import { jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ import { useTheme } from '../hooks/use-theme.js';
4
+ /** Subsequence check — do query's chars appear in order within text? */
5
+ function isSubsequence(text, query) {
6
+ if (!query)
7
+ return true;
8
+ let qi = 0;
9
+ for (let i = 0; i < text.length && qi < query.length; i++) {
10
+ if (text[i] === query[qi])
11
+ qi++;
12
+ }
13
+ return qi === query.length;
14
+ }
15
+ function basename(name) {
16
+ const slash = name.lastIndexOf('/');
17
+ return slash === -1 ? name : name.slice(slash + 1);
18
+ }
19
+ /**
20
+ * Relevance score for `name` against `query` (higher = better; -1 = no match).
21
+ * Contiguous matches in the basename dominate so `@read` ranks README over a
22
+ * file that merely has r…e scattered in its path. Falls back to subsequence.
23
+ */
24
+ function scoreSuggestion(name, query) {
25
+ if (!query)
26
+ return 0;
27
+ const q = query.toLowerCase();
28
+ const full = name.toLowerCase();
29
+ const base = basename(name).toLowerCase();
30
+ if (!isSubsequence(full, q))
31
+ return -1;
32
+ let score = 0;
33
+ if (base.startsWith(q))
34
+ score += 100;
35
+ else if (base.includes(q))
36
+ score += 60;
37
+ else if (full.includes(q))
38
+ score += 30;
39
+ else
40
+ score += 5; // subsequence only — weakest
41
+ score -= name.split('/').length; // shallower paths rank higher
42
+ if (base.startsWith('.'))
43
+ score -= 15; // hidden / artifacts sink
44
+ return score;
45
+ }
46
+ /** Filter to matches and rank by relevance score (desc). */
47
+ export function fuzzyFilter(items, query) {
48
+ return items
49
+ .map((s) => ({ s, score: scoreSuggestion(s.name, query) }))
50
+ .filter((x) => x.score >= 0)
51
+ .sort((a, b) => b.score - a.score)
52
+ .map((x) => x.s);
53
+ }
54
+ const MAX_VISIBLE = 8;
55
+ /**
56
+ * Fuzzy suggestion dropdown rendered above the prompt. Caller owns selection
57
+ * state and key handling (Tab/arrows/Esc in `prompt-area`); this is pure view.
58
+ *
59
+ * The list is windowed: only MAX_VISIBLE rows render, and the window follows
60
+ * `selectedIndex` (clamped) so ↑/↓ scroll through every match — not just the
61
+ * first page. Count hints show how many are hidden above/below.
62
+ */
63
+ export function Autocomplete({ suggestions, selectedIndex, prefix, }) {
64
+ const theme = useTheme();
65
+ if (suggestions.length === 0)
66
+ return null;
67
+ const half = Math.floor(MAX_VISIBLE / 2);
68
+ const start = Math.max(0, Math.min(selectedIndex - half, suggestions.length - MAX_VISIBLE));
69
+ const visible = suggestions.slice(start, start + MAX_VISIBLE);
70
+ const end = start + visible.length;
71
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: theme.fgGutter, paddingLeft: 1, paddingRight: 1, children: [start > 0 ? (_jsxs(Text, { color: theme.fgDim, children: [" \u2191 ", start, " more"] })) : null, visible.map((s, i) => {
72
+ const selected = start + i === selectedIndex;
73
+ return (_jsxs(Box, { children: [_jsxs(Text, { backgroundColor: selected ? theme.blue : undefined, color: selected ? theme.bg : theme.green, children: [selected ? '▶ ' : ' ', prefix, s.name] }), s.description ? (_jsxs(Text, { color: theme.fgDim, children: [" ", s.description.split('\n')[0].slice(0, 50)] })) : null] }, s.name));
74
+ }), end < suggestions.length ? (_jsxs(Text, { color: theme.fgDim, children: [" \u2193 ", suggestions.length - end, " more"] })) : null] }));
75
+ }
@@ -0,0 +1,15 @@
1
+ import { type Suggestion } from './autocomplete.js';
2
+ interface CommandPaletteProps {
3
+ commands: Suggestion[];
4
+ skills: Suggestion[];
5
+ /** Run the selected command/skill by name (no leading slash). */
6
+ onRun: (name: string) => void;
7
+ onClose: () => void;
8
+ }
9
+ /**
10
+ * Ctrl+P command palette: type to fuzzy-filter commands + skills, ↑/↓ to
11
+ * navigate, Enter to run, Esc to close. Owns its input (the prompt is hidden
12
+ * while the palette is open).
13
+ */
14
+ export declare function CommandPalette({ commands, skills, onRun, onClose }: CommandPaletteProps): import("react").JSX.Element;
15
+ export {};