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.
- package/CHANGELOG.md +154 -0
- package/LICENSE +96 -0
- package/README.md +568 -0
- package/dist/adapters/cli/agent.d.ts +59 -0
- package/dist/adapters/cli/agent.js +232 -0
- package/dist/adapters/cli/bootstrap.d.ts +25 -0
- package/dist/adapters/cli/bootstrap.js +204 -0
- package/dist/adapters/cli/commands/build-registry.d.ts +14 -0
- package/dist/adapters/cli/commands/build-registry.js +88 -0
- package/dist/adapters/cli/commands/clear.d.ts +7 -0
- package/dist/adapters/cli/commands/clear.js +10 -0
- package/dist/adapters/cli/commands/compact.d.ts +13 -0
- package/dist/adapters/cli/commands/compact.js +96 -0
- package/dist/adapters/cli/commands/exit.d.ts +7 -0
- package/dist/adapters/cli/commands/exit.js +9 -0
- package/dist/adapters/cli/commands/gateway.d.ts +7 -0
- package/dist/adapters/cli/commands/gateway.js +152 -0
- package/dist/adapters/cli/commands/help.d.ts +9 -0
- package/dist/adapters/cli/commands/help.js +12 -0
- package/dist/adapters/cli/commands/models.d.ts +10 -0
- package/dist/adapters/cli/commands/models.js +32 -0
- package/dist/adapters/cli/commands/registry.d.ts +70 -0
- package/dist/adapters/cli/commands/registry.js +111 -0
- package/dist/adapters/cli/commands/settings-utils.d.ts +38 -0
- package/dist/adapters/cli/commands/settings-utils.js +182 -0
- package/dist/adapters/cli/commands/settings.d.ts +9 -0
- package/dist/adapters/cli/commands/settings.js +395 -0
- package/dist/adapters/cli/commands/skills.d.ts +7 -0
- package/dist/adapters/cli/commands/skills.js +21 -0
- package/dist/adapters/cli/config-loader.d.ts +27 -0
- package/dist/adapters/cli/config-loader.js +48 -0
- package/dist/adapters/cli/docker-utils.d.ts +37 -0
- package/dist/adapters/cli/docker-utils.js +90 -0
- package/dist/adapters/cli/index.d.ts +2 -0
- package/dist/adapters/cli/index.js +88 -0
- package/dist/adapters/cli/repl.d.ts +22 -0
- package/dist/adapters/cli/repl.js +256 -0
- package/dist/adapters/cli/setup.d.ts +19 -0
- package/dist/adapters/cli/setup.js +613 -0
- package/dist/adapters/cli/system-prompts.d.ts +56 -0
- package/dist/adapters/cli/system-prompts.js +131 -0
- package/dist/adapters/cli/tui/app.d.ts +58 -0
- package/dist/adapters/cli/tui/app.js +314 -0
- package/dist/adapters/cli/tui/components/assistant-message.d.ts +5 -0
- package/dist/adapters/cli/tui/components/assistant-message.js +9 -0
- package/dist/adapters/cli/tui/components/autocomplete.d.ts +19 -0
- package/dist/adapters/cli/tui/components/autocomplete.js +75 -0
- package/dist/adapters/cli/tui/components/command-palette.d.ts +15 -0
- package/dist/adapters/cli/tui/components/command-palette.js +50 -0
- package/dist/adapters/cli/tui/components/diff-viewer.d.ts +5 -0
- package/dist/adapters/cli/tui/components/diff-viewer.js +109 -0
- package/dist/adapters/cli/tui/components/error-message.d.ts +5 -0
- package/dist/adapters/cli/tui/components/error-message.js +8 -0
- package/dist/adapters/cli/tui/components/footer.d.ts +20 -0
- package/dist/adapters/cli/tui/components/footer.js +19 -0
- package/dist/adapters/cli/tui/components/goal-status.d.ts +12 -0
- package/dist/adapters/cli/tui/components/goal-status.js +22 -0
- package/dist/adapters/cli/tui/components/info-message.d.ts +5 -0
- package/dist/adapters/cli/tui/components/info-message.js +8 -0
- package/dist/adapters/cli/tui/components/logo-banner.d.ts +7 -0
- package/dist/adapters/cli/tui/components/logo-banner.js +33 -0
- package/dist/adapters/cli/tui/components/markdown.d.ts +9 -0
- package/dist/adapters/cli/tui/components/markdown.js +92 -0
- package/dist/adapters/cli/tui/components/message-area.d.ts +19 -0
- package/dist/adapters/cli/tui/components/message-area.js +55 -0
- package/dist/adapters/cli/tui/components/permission-prompt.d.ts +13 -0
- package/dist/adapters/cli/tui/components/permission-prompt.js +32 -0
- package/dist/adapters/cli/tui/components/prompt-area.d.ts +22 -0
- package/dist/adapters/cli/tui/components/prompt-area.js +68 -0
- package/dist/adapters/cli/tui/components/text-input.d.ts +27 -0
- package/dist/adapters/cli/tui/components/text-input.js +142 -0
- package/dist/adapters/cli/tui/components/tool-call-block.d.ts +11 -0
- package/dist/adapters/cli/tui/components/tool-call-block.js +68 -0
- package/dist/adapters/cli/tui/components/user-message.d.ts +5 -0
- package/dist/adapters/cli/tui/components/user-message.js +8 -0
- package/dist/adapters/cli/tui/diff/file-write-meta.d.ts +11 -0
- package/dist/adapters/cli/tui/diff/file-write-meta.js +11 -0
- package/dist/adapters/cli/tui/diff/line-diff.d.ts +17 -0
- package/dist/adapters/cli/tui/diff/line-diff.js +44 -0
- package/dist/adapters/cli/tui/feed-serializer.d.ts +29 -0
- package/dist/adapters/cli/tui/feed-serializer.js +70 -0
- package/dist/adapters/cli/tui/file-index.d.ts +8 -0
- package/dist/adapters/cli/tui/file-index.js +41 -0
- package/dist/adapters/cli/tui/hooks/use-agent.d.ts +54 -0
- package/dist/adapters/cli/tui/hooks/use-agent.js +177 -0
- package/dist/adapters/cli/tui/hooks/use-feed.d.ts +16 -0
- package/dist/adapters/cli/tui/hooks/use-feed.js +25 -0
- package/dist/adapters/cli/tui/hooks/use-file-watcher.d.ts +10 -0
- package/dist/adapters/cli/tui/hooks/use-file-watcher.js +43 -0
- package/dist/adapters/cli/tui/hooks/use-keybindings.d.ts +16 -0
- package/dist/adapters/cli/tui/hooks/use-keybindings.js +25 -0
- package/dist/adapters/cli/tui/hooks/use-theme.d.ts +8 -0
- package/dist/adapters/cli/tui/hooks/use-theme.js +12 -0
- package/dist/adapters/cli/tui/index.d.ts +19 -0
- package/dist/adapters/cli/tui/index.js +206 -0
- package/dist/adapters/cli/tui/ink-reset.d.ts +29 -0
- package/dist/adapters/cli/tui/ink-reset.js +57 -0
- package/dist/adapters/cli/tui/layout.d.ts +15 -0
- package/dist/adapters/cli/tui/layout.js +15 -0
- package/dist/adapters/cli/tui/logo/gradient.d.ts +11 -0
- package/dist/adapters/cli/tui/logo/gradient.js +31 -0
- package/dist/adapters/cli/tui/overlays/help-dialog.d.ts +4 -0
- package/dist/adapters/cli/tui/overlays/help-dialog.js +26 -0
- package/dist/adapters/cli/tui/overlays/model-selector.d.ts +14 -0
- package/dist/adapters/cli/tui/overlays/model-selector.js +43 -0
- package/dist/adapters/cli/tui/overlays/session-selector.d.ts +35 -0
- package/dist/adapters/cli/tui/overlays/session-selector.js +162 -0
- package/dist/adapters/cli/tui/overlays/settings-overlay.d.ts +24 -0
- package/dist/adapters/cli/tui/overlays/settings-overlay.js +126 -0
- package/dist/adapters/cli/tui/session-export.d.ts +21 -0
- package/dist/adapters/cli/tui/session-export.js +63 -0
- package/dist/adapters/cli/tui/theme.d.ts +23 -0
- package/dist/adapters/cli/tui/theme.js +22 -0
- package/dist/adapters/cli/tui/types.d.ts +52 -0
- package/dist/adapters/cli/tui/types.js +12 -0
- package/dist/adapters/sdk/agent.d.ts +20 -0
- package/dist/adapters/sdk/agent.js +356 -0
- package/dist/adapters/sdk/http.d.ts +43 -0
- package/dist/adapters/sdk/http.js +61 -0
- package/dist/adapters/sdk/index.d.ts +58 -0
- package/dist/adapters/sdk/index.js +209 -0
- package/dist/adapters/sdk/settings.d.ts +18 -0
- package/dist/adapters/sdk/settings.js +57 -0
- package/dist/adapters/sdk/tools.d.ts +7 -0
- package/dist/adapters/sdk/tools.js +13 -0
- package/dist/adapters/server/auth.d.ts +53 -0
- package/dist/adapters/server/auth.js +168 -0
- package/dist/adapters/server/index.d.ts +40 -0
- package/dist/adapters/server/index.js +255 -0
- package/dist/adapters/server/rest-gateway.d.ts +13 -0
- package/dist/adapters/server/rest-gateway.js +218 -0
- package/dist/adapters/server/rest.d.ts +37 -0
- package/dist/adapters/server/rest.js +341 -0
- package/dist/adapters/server/server-core.d.ts +55 -0
- package/dist/adapters/server/server-core.js +121 -0
- package/dist/adapters/server/session-store.d.ts +81 -0
- package/dist/adapters/server/session-store.js +272 -0
- package/dist/adapters/server/settings-handlers.d.ts +24 -0
- package/dist/adapters/server/settings-handlers.js +360 -0
- package/dist/adapters/server/standalone.d.ts +19 -0
- package/dist/adapters/server/standalone.js +113 -0
- package/dist/adapters/server/websocket.d.ts +26 -0
- package/dist/adapters/server/websocket.js +68 -0
- package/dist/adapters/server/ws-handlers.d.ts +32 -0
- package/dist/adapters/server/ws-handlers.js +523 -0
- package/dist/adapters/server/ws-types.d.ts +304 -0
- package/dist/adapters/server/ws-types.js +7 -0
- package/dist/core/agent-loop.d.ts +68 -0
- package/dist/core/agent-loop.js +423 -0
- package/dist/core/config.d.ts +115 -0
- package/dist/core/config.js +189 -0
- package/dist/core/errors.d.ts +58 -0
- package/dist/core/errors.js +88 -0
- package/dist/core/hooks.d.ts +35 -0
- package/dist/core/hooks.js +49 -0
- package/dist/core/index.d.ts +23 -0
- package/dist/core/index.js +29 -0
- package/dist/core/message-convert.d.ts +41 -0
- package/dist/core/message-convert.js +94 -0
- package/dist/core/middleware/auth.d.ts +24 -0
- package/dist/core/middleware/auth.js +28 -0
- package/dist/core/middleware/logging.d.ts +23 -0
- package/dist/core/middleware/logging.js +28 -0
- package/dist/core/middleware/rate-limit.d.ts +27 -0
- package/dist/core/middleware/rate-limit.js +38 -0
- package/dist/core/middleware/semantic-tools.d.ts +10 -0
- package/dist/core/middleware/semantic-tools.js +43 -0
- package/dist/core/middleware.d.ts +48 -0
- package/dist/core/middleware.js +38 -0
- package/dist/core/permission.d.ts +25 -0
- package/dist/core/permission.js +50 -0
- package/dist/core/provider-config.d.ts +129 -0
- package/dist/core/provider-config.js +273 -0
- package/dist/core/provider-env.d.ts +39 -0
- package/dist/core/provider-env.js +142 -0
- package/dist/core/provider-resolver.d.ts +12 -0
- package/dist/core/provider-resolver.js +12 -0
- package/dist/core/session-store.d.ts +75 -0
- package/dist/core/session-store.js +245 -0
- package/dist/core/settings-manager.d.ts +57 -0
- package/dist/core/settings-manager.js +359 -0
- package/dist/core/settings-schema.d.ts +38 -0
- package/dist/core/settings-schema.js +171 -0
- package/dist/core/skill-catalog.d.ts +6 -0
- package/dist/core/skill-catalog.js +17 -0
- package/dist/core/skill-invoker.d.ts +127 -0
- package/dist/core/skill-invoker.js +182 -0
- package/dist/core/stream-accumulator.d.ts +21 -0
- package/dist/core/stream-accumulator.js +51 -0
- package/dist/core/stream-manager.d.ts +58 -0
- package/dist/core/stream-manager.js +212 -0
- package/dist/core/tool-executor.d.ts +84 -0
- package/dist/core/tool-executor.js +256 -0
- package/dist/core/types.d.ts +259 -0
- package/dist/core/types.js +11 -0
- package/dist/gateway/gateway.d.ts +52 -0
- package/dist/gateway/gateway.js +537 -0
- package/dist/gateway/index.d.ts +21 -0
- package/dist/gateway/index.js +31 -0
- package/dist/gateway/openapi-importer.d.ts +15 -0
- package/dist/gateway/openapi-importer.js +66 -0
- package/dist/gateway/semantic-scorer.d.ts +7 -0
- package/dist/gateway/semantic-scorer.js +24 -0
- package/dist/gateway/settings-adapter.d.ts +49 -0
- package/dist/gateway/settings-adapter.js +137 -0
- package/dist/gateway/tool-factory.d.ts +9 -0
- package/dist/gateway/tool-factory.js +414 -0
- package/dist/gateway/types.d.ts +68 -0
- package/dist/gateway/types.js +7 -0
- package/dist/models-catalog.js +46 -0
- package/dist/providers/anthropic.d.ts +22 -0
- package/dist/providers/anthropic.js +148 -0
- package/dist/providers/factory.d.ts +10 -0
- package/dist/providers/factory.js +25 -0
- package/dist/providers/openai.d.ts +15 -0
- package/dist/providers/openai.js +71 -0
- package/dist/providers/types.d.ts +48 -0
- package/dist/providers/types.js +1 -0
- package/dist/skills/args.d.ts +37 -0
- package/dist/skills/args.js +99 -0
- package/dist/skills/index.d.ts +11 -0
- package/dist/skills/index.js +23 -0
- package/dist/skills/loader.d.ts +3 -0
- package/dist/skills/loader.js +59 -0
- package/dist/skills/parser.d.ts +7 -0
- package/dist/skills/parser.js +152 -0
- package/dist/skills/registry.d.ts +13 -0
- package/dist/skills/registry.js +74 -0
- package/dist/skills/resolver.d.ts +19 -0
- package/dist/skills/resolver.js +116 -0
- package/dist/skills/types.d.ts +74 -0
- package/dist/skills/types.js +50 -0
- package/dist/tools/browser.d.ts +2 -0
- package/dist/tools/browser.js +68 -0
- package/dist/tools/core.d.ts +20 -0
- package/dist/tools/core.js +244 -0
- package/dist/tools/email.d.ts +2 -0
- package/dist/tools/email.js +61 -0
- package/dist/tools/image.d.ts +2 -0
- package/dist/tools/image.js +257 -0
- package/dist/tools/index.d.ts +2 -0
- package/dist/tools/index.js +88 -0
- package/dist/tools/interface.d.ts +22 -0
- package/dist/tools/interface.js +1 -0
- package/dist/tools/notify.d.ts +2 -0
- package/dist/tools/notify.js +100 -0
- package/dist/tools/prompt-optimizer.d.ts +2 -0
- package/dist/tools/prompt-optimizer.js +65 -0
- package/dist/tools/screenshot.d.ts +2 -0
- package/dist/tools/screenshot.js +184 -0
- package/dist/tools/search.d.ts +2 -0
- package/dist/tools/search.js +78 -0
- package/dist/tools/todos.d.ts +10 -0
- package/dist/tools/todos.js +50 -0
- package/package.json +119 -0
- package/skills/docker-ops/SKILL.md +329 -0
- package/skills/k8s-deploy/SKILL.md +397 -0
- package/skills/log-analyzer/SKILL.md +331 -0
- package/skills/speckit-analyze/SKILL.md +260 -0
- package/skills/speckit-checklist/SKILL.md +374 -0
- package/skills/speckit-clarify/SKILL.md +286 -0
- package/skills/speckit-constitution/SKILL.md +157 -0
- package/skills/speckit-implement/SKILL.md +224 -0
- package/skills/speckit-plan/SKILL.md +171 -0
- package/skills/speckit-specify/SKILL.md +346 -0
- package/skills/speckit-tasks/SKILL.md +215 -0
- 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,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 {};
|