@armadra/agent 0.0.0-stage → 0.2.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 +22 -0
- package/LICENSE +21 -0
- package/README.md +426 -2
- package/dist/agent/agent.d.ts +56 -0
- package/dist/agent/agent.js +129 -0
- package/dist/agent/loop.d.ts +51 -0
- package/dist/agent/loop.js +158 -0
- package/dist/agent/queue.d.ts +25 -0
- package/dist/agent/queue.js +55 -0
- package/dist/agent/retry.d.ts +22 -0
- package/dist/agent/retry.js +92 -0
- package/dist/agent/schema.d.ts +27 -0
- package/dist/agent/schema.js +186 -0
- package/dist/agent/session-cache.d.ts +100 -0
- package/dist/agent/session-cache.js +511 -0
- package/dist/agent/session-compaction.d.ts +49 -0
- package/dist/agent/session-compaction.js +251 -0
- package/dist/agent/session-core.d.ts +103 -0
- package/dist/agent/session-core.js +7 -0
- package/dist/agent/session-run.d.ts +55 -0
- package/dist/agent/session-run.js +246 -0
- package/dist/agent/session-state.d.ts +39 -0
- package/dist/agent/session-state.js +118 -0
- package/dist/agent/session-subagent.d.ts +54 -0
- package/dist/agent/session-subagent.js +157 -0
- package/dist/agent/session-sync.d.ts +20 -0
- package/dist/agent/session-sync.js +104 -0
- package/dist/agent/session-tools.d.ts +24 -0
- package/dist/agent/session-tools.js +277 -0
- package/dist/agent/session.d.ts +117 -0
- package/dist/agent/session.js +484 -0
- package/dist/agent/system-prompt.d.ts +49 -0
- package/dist/agent/system-prompt.js +121 -0
- package/dist/agent/tool-runner.d.ts +64 -0
- package/dist/agent/tool-runner.js +309 -0
- package/dist/agent/transform.d.ts +26 -0
- package/dist/agent/transform.js +171 -0
- package/dist/agent/types.d.ts +378 -0
- package/dist/agent/types.js +21 -0
- package/dist/ai/apis/anthropic-messages.d.ts +26 -0
- package/dist/ai/apis/anthropic-messages.js +214 -0
- package/dist/ai/apis/anthropic-request.d.ts +42 -0
- package/dist/ai/apis/anthropic-request.js +282 -0
- package/dist/ai/apis/api.d.ts +28 -0
- package/dist/ai/apis/api.js +102 -0
- package/dist/ai/apis/cache-params.d.ts +48 -0
- package/dist/ai/apis/cache-params.js +163 -0
- package/dist/ai/apis/google-generative-ai.d.ts +32 -0
- package/dist/ai/apis/google-generative-ai.js +249 -0
- package/dist/ai/apis/google-request.d.ts +40 -0
- package/dist/ai/apis/google-request.js +255 -0
- package/dist/ai/apis/openai-compat.d.ts +35 -0
- package/dist/ai/apis/openai-compat.js +142 -0
- package/dist/ai/apis/openai-completions.d.ts +28 -0
- package/dist/ai/apis/openai-completions.js +246 -0
- package/dist/ai/apis/openai-request.d.ts +35 -0
- package/dist/ai/apis/openai-request.js +303 -0
- package/dist/ai/apis/openai-responses-request.d.ts +52 -0
- package/dist/ai/apis/openai-responses-request.js +266 -0
- package/dist/ai/apis/openai-responses.d.ts +28 -0
- package/dist/ai/apis/openai-responses.js +303 -0
- package/dist/ai/apis/shared.d.ts +56 -0
- package/dist/ai/apis/shared.js +179 -0
- package/dist/ai/cache/economics.d.ts +24 -0
- package/dist/ai/cache/economics.js +69 -0
- package/dist/ai/cache/fingerprint.d.ts +17 -0
- package/dist/ai/cache/fingerprint.js +39 -0
- package/dist/ai/cache/miss.d.ts +49 -0
- package/dist/ai/cache/miss.js +96 -0
- package/dist/ai/cache/reporting.d.ts +39 -0
- package/dist/ai/cache/reporting.js +102 -0
- package/dist/ai/cache/types.d.ts +94 -0
- package/dist/ai/cache/types.js +19 -0
- package/dist/ai/cache/warmer.d.ts +94 -0
- package/dist/ai/cache/warmer.js +224 -0
- package/dist/ai/context.d.ts +35 -0
- package/dist/ai/context.js +114 -0
- package/dist/ai/cost.d.ts +21 -0
- package/dist/ai/cost.js +60 -0
- package/dist/ai/event-stream.d.ts +33 -0
- package/dist/ai/event-stream.js +101 -0
- package/dist/ai/fake/fake-provider.d.ts +68 -0
- package/dist/ai/fake/fake-provider.js +278 -0
- package/dist/ai/fake/fake-script.d.ts +69 -0
- package/dist/ai/fake/fake-script.js +142 -0
- package/dist/ai/http.d.ts +54 -0
- package/dist/ai/http.js +212 -0
- package/dist/ai/json-partial.d.ts +17 -0
- package/dist/ai/json-partial.js +243 -0
- package/dist/ai/overflow.d.ts +22 -0
- package/dist/ai/overflow.js +61 -0
- package/dist/ai/providers/auth.d.ts +71 -0
- package/dist/ai/providers/auth.js +202 -0
- package/dist/ai/providers/builtin.d.ts +23 -0
- package/dist/ai/providers/builtin.js +148 -0
- package/dist/ai/providers/catalog-data.d.ts +5 -0
- package/dist/ai/providers/catalog-data.js +19 -0
- package/dist/ai/providers/catalog.d.ts +28 -0
- package/dist/ai/providers/catalog.js +166 -0
- package/dist/ai/providers/registry.d.ts +83 -0
- package/dist/ai/providers/registry.js +305 -0
- package/dist/ai/sse.d.ts +32 -0
- package/dist/ai/sse.js +140 -0
- package/dist/ai/thinking.d.ts +42 -0
- package/dist/ai/thinking.js +101 -0
- package/dist/ai/types.d.ts +428 -0
- package/dist/ai/types.js +19 -0
- package/dist/bundle/ama-sandbox.cjs +324 -0
- package/dist/bundle/ama.cjs +25770 -0
- package/dist/bundle.d.ts +9 -0
- package/dist/bundle.js +15 -0
- package/dist/cli/args.d.ts +82 -0
- package/dist/cli/args.js +381 -0
- package/dist/cli/bootstrap.d.ts +18 -0
- package/dist/cli/bootstrap.js +398 -0
- package/dist/cli/compose-providers.d.ts +37 -0
- package/dist/cli/compose-providers.js +104 -0
- package/dist/cli/compose-session.d.ts +69 -0
- package/dist/cli/compose-session.js +370 -0
- package/dist/cli/compose-store.d.ts +24 -0
- package/dist/cli/compose-store.js +125 -0
- package/dist/cli/compose.d.ts +106 -0
- package/dist/cli/compose.js +206 -0
- package/dist/cli/default-model.d.ts +16 -0
- package/dist/cli/default-model.js +24 -0
- package/dist/cli/deps.d.ts +205 -0
- package/dist/cli/deps.js +12 -0
- package/dist/cli/exit-codes.d.ts +29 -0
- package/dist/cli/exit-codes.js +40 -0
- package/dist/cli/main.d.ts +26 -0
- package/dist/cli/main.js +165 -0
- package/dist/cli/runtime.d.ts +90 -0
- package/dist/cli/runtime.js +8 -0
- package/dist/cli/startup-screen.d.ts +15 -0
- package/dist/cli/startup-screen.js +56 -0
- package/dist/cli/startup-steps.d.ts +35 -0
- package/dist/cli/startup-steps.js +197 -0
- package/dist/cli/subcommands/auth.d.ts +12 -0
- package/dist/cli/subcommands/auth.js +91 -0
- package/dist/cli/subcommands/config.d.ts +44 -0
- package/dist/cli/subcommands/config.js +201 -0
- package/dist/cli/subcommands/context.d.ts +21 -0
- package/dist/cli/subcommands/context.js +41 -0
- package/dist/cli/subcommands/doctor.d.ts +11 -0
- package/dist/cli/subcommands/doctor.js +246 -0
- package/dist/cli/subcommands/models-cache-probe.d.ts +34 -0
- package/dist/cli/subcommands/models-cache-probe.js +190 -0
- package/dist/cli/subcommands/models-discover.d.ts +40 -0
- package/dist/cli/subcommands/models-discover.js +204 -0
- package/dist/cli/subcommands/models.d.ts +40 -0
- package/dist/cli/subcommands/models.js +144 -0
- package/dist/cli/subcommands/sessions.d.ts +11 -0
- package/dist/cli/subcommands/sessions.js +100 -0
- package/dist/codemode/capability.d.ts +37 -0
- package/dist/codemode/capability.js +55 -0
- package/dist/codemode/declarations.d.ts +44 -0
- package/dist/codemode/declarations.js +111 -0
- package/dist/codemode/host-side.d.ts +59 -0
- package/dist/codemode/host-side.js +253 -0
- package/dist/codemode/modes.d.ts +23 -0
- package/dist/codemode/modes.js +40 -0
- package/dist/codemode/protocol.d.ts +91 -0
- package/dist/codemode/protocol.js +114 -0
- package/dist/codemode/sandbox-entry.d.ts +25 -0
- package/dist/codemode/sandbox-entry.js +323 -0
- package/dist/codemode/store.d.ts +26 -0
- package/dist/codemode/store.js +53 -0
- package/dist/codemode/tool.d.ts +105 -0
- package/dist/codemode/tool.js +283 -0
- package/dist/compaction/branch-summary.d.ts +30 -0
- package/dist/compaction/branch-summary.js +79 -0
- package/dist/compaction/breaker.d.ts +32 -0
- package/dist/compaction/breaker.js +65 -0
- package/dist/compaction/cut-point.d.ts +23 -0
- package/dist/compaction/cut-point.js +70 -0
- package/dist/compaction/estimate.d.ts +30 -0
- package/dist/compaction/estimate.js +114 -0
- package/dist/compaction/prune-tier.d.ts +31 -0
- package/dist/compaction/prune-tier.js +83 -0
- package/dist/compaction/serialize.d.ts +20 -0
- package/dist/compaction/serialize.js +115 -0
- package/dist/compaction/summarize-tier.d.ts +111 -0
- package/dist/compaction/summarize-tier.js +305 -0
- package/dist/config/auth-file.d.ts +39 -0
- package/dist/config/auth-file.js +92 -0
- package/dist/config/context-files.d.ts +30 -0
- package/dist/config/context-files.js +77 -0
- package/dist/config/load.d.ts +45 -0
- package/dist/config/load.js +97 -0
- package/dist/config/merge.d.ts +73 -0
- package/dist/config/merge.js +248 -0
- package/dist/config/paths.d.ts +38 -0
- package/dist/config/paths.js +108 -0
- package/dist/config/profile.d.ts +27 -0
- package/dist/config/profile.js +65 -0
- package/dist/config/schema.d.ts +34 -0
- package/dist/config/schema.js +413 -0
- package/dist/config/trust.d.ts +42 -0
- package/dist/config/trust.js +122 -0
- package/dist/config/types.d.ts +172 -0
- package/dist/config/types.js +27 -0
- package/dist/config/write.d.ts +10 -0
- package/dist/config/write.js +25 -0
- package/dist/errors.d.ts +28 -0
- package/dist/errors.js +30 -0
- package/dist/hooks/config.d.ts +49 -0
- package/dist/hooks/config.js +102 -0
- package/dist/hooks/dispatcher.d.ts +41 -0
- package/dist/hooks/dispatcher.js +104 -0
- package/dist/hooks/matcher.d.ts +22 -0
- package/dist/hooks/matcher.js +108 -0
- package/dist/hooks/protocol.d.ts +33 -0
- package/dist/hooks/protocol.js +240 -0
- package/dist/hooks/runner.d.ts +30 -0
- package/dist/hooks/runner.js +143 -0
- package/dist/hooks/types.d.ts +147 -0
- package/dist/hooks/types.js +20 -0
- package/dist/host/api-impl.d.ts +75 -0
- package/dist/host/api-impl.js +173 -0
- package/dist/host/loader.d.ts +31 -0
- package/dist/host/loader.js +147 -0
- package/dist/host/types.d.ts +169 -0
- package/dist/host/types.js +14 -0
- package/dist/host.d.ts +5 -0
- package/dist/host.js +4 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.js +28 -0
- package/dist/modes/commands-core.d.ts +44 -0
- package/dist/modes/commands-core.js +171 -0
- package/dist/modes/interactive/approval-dialog.d.ts +48 -0
- package/dist/modes/interactive/approval-dialog.js +202 -0
- package/dist/modes/interactive/commands.d.ts +41 -0
- package/dist/modes/interactive/commands.js +202 -0
- package/dist/modes/interactive/completion.d.ts +53 -0
- package/dist/modes/interactive/completion.js +157 -0
- package/dist/modes/interactive/interactive-mode.d.ts +46 -0
- package/dist/modes/interactive/interactive-mode.js +487 -0
- package/dist/modes/interactive/key-dispatch.d.ts +29 -0
- package/dist/modes/interactive/key-dispatch.js +107 -0
- package/dist/modes/interactive/line/line-editor.d.ts +50 -0
- package/dist/modes/interactive/line/line-editor.js +232 -0
- package/dist/modes/interactive/line/line-mode.d.ts +20 -0
- package/dist/modes/interactive/line/line-mode.js +216 -0
- package/dist/modes/interactive/line/line-render.d.ts +35 -0
- package/dist/modes/interactive/line/line-render.js +171 -0
- package/dist/modes/interactive/line/paste-state.d.ts +26 -0
- package/dist/modes/interactive/line/paste-state.js +72 -0
- package/dist/modes/interactive/message-view.d.ts +77 -0
- package/dist/modes/interactive/message-view.js +269 -0
- package/dist/modes/interactive/pickers.d.ts +42 -0
- package/dist/modes/interactive/pickers.js +115 -0
- package/dist/modes/interactive/startup-ui.d.ts +28 -0
- package/dist/modes/interactive/startup-ui.js +238 -0
- package/dist/modes/interactive/status-bar.d.ts +48 -0
- package/dist/modes/interactive/status-bar.js +139 -0
- package/dist/modes/interactive/tool-view.d.ts +95 -0
- package/dist/modes/interactive/tool-view.js +344 -0
- package/dist/modes/print/json-event.d.ts +15 -0
- package/dist/modes/print/json-event.js +39 -0
- package/dist/modes/print/print-mode.d.ts +15 -0
- package/dist/modes/print/print-mode.js +96 -0
- package/dist/modes/rpc/commands.d.ts +40 -0
- package/dist/modes/rpc/commands.js +258 -0
- package/dist/modes/rpc/jsonl.d.ts +16 -0
- package/dist/modes/rpc/jsonl.js +55 -0
- package/dist/modes/rpc/rpc-mode.d.ts +22 -0
- package/dist/modes/rpc/rpc-mode.js +153 -0
- package/dist/modes/session-report.d.ts +55 -0
- package/dist/modes/session-report.js +278 -0
- package/dist/modes/shared.d.ts +13 -0
- package/dist/modes/shared.js +52 -0
- package/dist/modes/startup-ui-text.d.ts +16 -0
- package/dist/modes/startup-ui-text.js +103 -0
- package/dist/permissions/broker.d.ts +47 -0
- package/dist/permissions/broker.js +107 -0
- package/dist/permissions/dangerous.d.ts +47 -0
- package/dist/permissions/dangerous.js +370 -0
- package/dist/permissions/pipeline.d.ts +51 -0
- package/dist/permissions/pipeline.js +181 -0
- package/dist/permissions/preview.d.ts +43 -0
- package/dist/permissions/preview.js +463 -0
- package/dist/permissions/rules.d.ts +58 -0
- package/dist/permissions/rules.js +249 -0
- package/dist/permissions/types.d.ts +100 -0
- package/dist/permissions/types.js +18 -0
- package/dist/rpc.d.ts +171 -0
- package/dist/rpc.js +13 -0
- package/dist/sdk.d.ts +98 -0
- package/dist/sdk.js +195 -0
- package/dist/session/manager.d.ts +70 -0
- package/dist/session/manager.js +278 -0
- package/dist/session/migrate.d.ts +19 -0
- package/dist/session/migrate.js +53 -0
- package/dist/session/projection.d.ts +50 -0
- package/dist/session/projection.js +180 -0
- package/dist/session/store.d.ts +47 -0
- package/dist/session/store.js +210 -0
- package/dist/session/tree.d.ts +21 -0
- package/dist/session/tree.js +88 -0
- package/dist/session/types.d.ts +199 -0
- package/dist/session/types.js +18 -0
- package/dist/skills/discover.d.ts +60 -0
- package/dist/skills/discover.js +160 -0
- package/dist/skills/expand.d.ts +35 -0
- package/dist/skills/expand.js +46 -0
- package/dist/skills/frontmatter.d.ts +27 -0
- package/dist/skills/frontmatter.js +157 -0
- package/dist/skills/index-prompt.d.ts +13 -0
- package/dist/skills/index-prompt.js +42 -0
- package/dist/skills/templates.d.ts +52 -0
- package/dist/skills/templates.js +177 -0
- package/dist/tools/bash.d.ts +49 -0
- package/dist/tools/bash.js +222 -0
- package/dist/tools/edit-fuzzy.d.ts +36 -0
- package/dist/tools/edit-fuzzy.js +129 -0
- package/dist/tools/edit.d.ts +38 -0
- package/dist/tools/edit.js +241 -0
- package/dist/tools/file-mutex.d.ts +8 -0
- package/dist/tools/file-mutex.js +29 -0
- package/dist/tools/glob.d.ts +44 -0
- package/dist/tools/glob.js +217 -0
- package/dist/tools/grep.d.ts +39 -0
- package/dist/tools/grep.js +179 -0
- package/dist/tools/ignore.d.ts +40 -0
- package/dist/tools/ignore.js +174 -0
- package/dist/tools/ls.d.ts +21 -0
- package/dist/tools/ls.js +96 -0
- package/dist/tools/output-accumulator.d.ts +48 -0
- package/dist/tools/output-accumulator.js +106 -0
- package/dist/tools/paths.d.ts +20 -0
- package/dist/tools/paths.js +58 -0
- package/dist/tools/presets.d.ts +71 -0
- package/dist/tools/presets.js +128 -0
- package/dist/tools/process-tree.d.ts +35 -0
- package/dist/tools/process-tree.js +125 -0
- package/dist/tools/read.d.ts +28 -0
- package/dist/tools/read.js +181 -0
- package/dist/tools/registry.d.ts +43 -0
- package/dist/tools/registry.js +106 -0
- package/dist/tools/shell.d.ts +38 -0
- package/dist/tools/shell.js +91 -0
- package/dist/tools/task.d.ts +29 -0
- package/dist/tools/task.js +107 -0
- package/dist/tools/todo.d.ts +28 -0
- package/dist/tools/todo.js +107 -0
- package/dist/tools/truncate.d.ts +44 -0
- package/dist/tools/truncate.js +175 -0
- package/dist/tools/types.d.ts +135 -0
- package/dist/tools/types.js +19 -0
- package/dist/tools/write.d.ts +20 -0
- package/dist/tools/write.js +84 -0
- package/dist/tui/ansi.d.ts +29 -0
- package/dist/tui/ansi.js +420 -0
- package/dist/tui/component.d.ts +38 -0
- package/dist/tui/component.js +13 -0
- package/dist/tui/components/box.d.ts +26 -0
- package/dist/tui/components/box.js +65 -0
- package/dist/tui/components/container.d.ts +14 -0
- package/dist/tui/components/container.js +39 -0
- package/dist/tui/components/editor-buffer.d.ts +67 -0
- package/dist/tui/components/editor-buffer.js +300 -0
- package/dist/tui/components/editor-paste.d.ts +29 -0
- package/dist/tui/components/editor-paste.js +64 -0
- package/dist/tui/components/editor.d.ts +114 -0
- package/dist/tui/components/editor.js +530 -0
- package/dist/tui/components/key-value.d.ts +29 -0
- package/dist/tui/components/key-value.js +48 -0
- package/dist/tui/components/loader.d.ts +35 -0
- package/dist/tui/components/loader.js +71 -0
- package/dist/tui/components/markdown.d.ts +90 -0
- package/dist/tui/components/markdown.js +462 -0
- package/dist/tui/components/meter.d.ts +29 -0
- package/dist/tui/components/meter.js +47 -0
- package/dist/tui/components/overlay.d.ts +24 -0
- package/dist/tui/components/overlay.js +52 -0
- package/dist/tui/components/select-list.d.ts +57 -0
- package/dist/tui/components/select-list.js +179 -0
- package/dist/tui/components/spacer.d.ts +11 -0
- package/dist/tui/components/spacer.js +16 -0
- package/dist/tui/components/text.d.ts +30 -0
- package/dist/tui/components/text.js +65 -0
- package/dist/tui/keybindings.d.ts +66 -0
- package/dist/tui/keybindings.js +120 -0
- package/dist/tui/keys.d.ts +38 -0
- package/dist/tui/keys.js +275 -0
- package/dist/tui/stdin-buffer.d.ts +41 -0
- package/dist/tui/stdin-buffer.js +206 -0
- package/dist/tui/terminal.d.ts +79 -0
- package/dist/tui/terminal.js +153 -0
- package/dist/tui/theme.d.ts +31 -0
- package/dist/tui/theme.js +199 -0
- package/dist/tui/tui.d.ts +103 -0
- package/dist/tui/tui.js +363 -0
- package/dist/tui/vt-screen.d.ts +46 -0
- package/dist/tui/vt-screen.js +251 -0
- package/dist/tui.d.ts +30 -0
- package/dist/tui.js +31 -0
- package/dist/version.d.ts +8 -0
- package/dist/version.js +24 -0
- package/docs/codemode.md +102 -0
- package/docs/hooks.md +196 -0
- package/docs/host-api.md +161 -0
- package/docs/providers.md +316 -0
- package/docs/rpc.md +253 -0
- package/docs/session-format.md +119 -0
- package/docs/tui.md +168 -0
- package/package.json +101 -4
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 审批对话框(设计 §12.6):模式层的 UI `ApprovalBroker`,底部覆盖层。[B7]
|
|
3
|
+
*
|
|
4
|
+
* - 标题行:`[task]`(`request.context.depth > 0`,子 Agent 发起)+ 工具名 + 原因标签;
|
|
5
|
+
* - 输入:bash 显示命令全文,write 显示路径与行数,edit 显示路径与每处修改的 −/+ 摘要,其它显示
|
|
6
|
+
* 一行摘要;`v` 展开 / 收起完整输入(JSON);
|
|
7
|
+
* - 执行前预览(W3-B9a-2,`request.preview`):输入摘要之后列出会碰到的路径与规模,danger 红、
|
|
8
|
+
* warn 黄、info 暗色;`other` 类预览与输入摘要重复,不显示;
|
|
9
|
+
* - 原因:mode(当前权限模式需要确认)/ dangerous / hook + Hook 给的文本;
|
|
10
|
+
* - 按键:`y` 允许、`n` / Esc / Ctrl+C 拒绝、`a` 本会话允许同类;
|
|
11
|
+
* - 超时与中断:broker 链在超时(缺省 10 分钟)或运行被中断时 abort `signal`,对话框关闭并返回
|
|
12
|
+
* undefined(链按 deny 处理),消息区留一行说明。
|
|
13
|
+
*
|
|
14
|
+
* broker 链本身串行化审批,所以同一时刻最多一个对话框。
|
|
15
|
+
*/
|
|
16
|
+
import { previewDisplayLines } from "../../permissions/preview.js";
|
|
17
|
+
import { Box, Container, Text, defaultKeybindings, matchesKey, truncateToWidth, } from "../../tui.js";
|
|
18
|
+
import { toolSummary } from "./tool-view.js";
|
|
19
|
+
const COMMAND_LINES = 8;
|
|
20
|
+
const EDIT_LINES = 3;
|
|
21
|
+
const FULL_INPUT_LINES = 40;
|
|
22
|
+
function lines(text, max) {
|
|
23
|
+
const all = text.replace(/\r\n?/g, "\n").replace(/\t/g, " ").split("\n");
|
|
24
|
+
while (all.length > 1 && all[all.length - 1].trim() === "")
|
|
25
|
+
all.pop();
|
|
26
|
+
return { shown: all.slice(0, max), hidden: Math.max(0, all.length - max) };
|
|
27
|
+
}
|
|
28
|
+
function record(input) {
|
|
29
|
+
return typeof input === "object" && input !== null ? input : {};
|
|
30
|
+
}
|
|
31
|
+
/** 对话框正文(不含按键行)。 */
|
|
32
|
+
export function describeRequest(request, theme, options = {}) {
|
|
33
|
+
const out = [];
|
|
34
|
+
const task = (request.context?.depth ?? 0) > 0 ? theme.fg("warning", "[task] ") : "";
|
|
35
|
+
const tag = request.reason === "dangerous"
|
|
36
|
+
? theme.fg("error", "危险命令")
|
|
37
|
+
: request.reason === "hook"
|
|
38
|
+
? theme.fg("warning", "Hook 要求确认")
|
|
39
|
+
: theme.fg("dim", "需要确认");
|
|
40
|
+
out.push(`${task}${theme.bold(theme.fg("tool", request.toolName))} ${tag}`);
|
|
41
|
+
const input = record(request.input);
|
|
42
|
+
const more = (n) => theme.fg("dim", `… 另 ${n} 行(v 查看)`);
|
|
43
|
+
if (options.expanded === true) {
|
|
44
|
+
const json = JSON.stringify(request.input, null, 2) ?? String(request.input);
|
|
45
|
+
const { shown, hidden } = lines(json, FULL_INPUT_LINES);
|
|
46
|
+
out.push(...shown.map((l) => theme.fg("code", l)));
|
|
47
|
+
if (hidden > 0)
|
|
48
|
+
out.push(theme.fg("dim", `… 另 ${hidden} 行`));
|
|
49
|
+
}
|
|
50
|
+
else if (request.toolName === "bash" && typeof input["command"] === "string") {
|
|
51
|
+
const { shown, hidden } = lines(input["command"], COMMAND_LINES);
|
|
52
|
+
out.push(...shown.map((l, i) => theme.fg("code", (i === 0 ? "$ " : " ") + l)));
|
|
53
|
+
if (hidden > 0)
|
|
54
|
+
out.push(more(hidden));
|
|
55
|
+
}
|
|
56
|
+
else if (request.toolName === "write" && typeof input["content"] === "string") {
|
|
57
|
+
const count = lines(input["content"], Number.MAX_SAFE_INTEGER).shown.length;
|
|
58
|
+
out.push(`${toolSummary("write", input, options.cwd)} ${theme.fg("dim", `写入 ${count} 行`)}`);
|
|
59
|
+
}
|
|
60
|
+
else if (request.toolName === "edit" && Array.isArray(input["edits"])) {
|
|
61
|
+
const edits = input["edits"];
|
|
62
|
+
out.push(`${toolSummary("edit", input, options.cwd)} ${theme.fg("dim", `${edits.length} 处修改`)}`);
|
|
63
|
+
for (const edit of edits.slice(0, 2)) {
|
|
64
|
+
for (const [sign, text, color] of [
|
|
65
|
+
["-", edit.oldText, "error"],
|
|
66
|
+
["+", edit.newText, "success"],
|
|
67
|
+
]) {
|
|
68
|
+
if (typeof text !== "string")
|
|
69
|
+
continue;
|
|
70
|
+
const { shown, hidden } = lines(text, EDIT_LINES);
|
|
71
|
+
out.push(...shown.map((l) => theme.fg(color, `${sign} ${l}`)));
|
|
72
|
+
if (hidden > 0)
|
|
73
|
+
out.push(more(hidden));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (edits.length > 2)
|
|
77
|
+
out.push(theme.fg("dim", `… 另 ${edits.length - 2} 处`));
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
const summary = toolSummary(request.toolName, input, options.cwd);
|
|
81
|
+
if (summary !== "")
|
|
82
|
+
out.push(summary);
|
|
83
|
+
}
|
|
84
|
+
const severity = request.preview?.severity;
|
|
85
|
+
const color = severity === "danger" ? "error" : severity === "warn" ? "warning" : "dim";
|
|
86
|
+
out.push(...previewDisplayLines(request.preview).map((l) => theme.fg(color, l)));
|
|
87
|
+
if (request.reason === "hook") {
|
|
88
|
+
out.push(theme.fg("warning", `Hook:${request.hookReason ?? "(无说明)"}`));
|
|
89
|
+
}
|
|
90
|
+
else if (request.reason === "dangerous") {
|
|
91
|
+
out.push(theme.fg("error", "这条命令可能有破坏性,请确认"));
|
|
92
|
+
}
|
|
93
|
+
else if (options.permissionMode !== undefined) {
|
|
94
|
+
out.push(theme.fg("dim", `权限模式 ${options.permissionMode} 下需要确认`));
|
|
95
|
+
}
|
|
96
|
+
return out;
|
|
97
|
+
}
|
|
98
|
+
export const APPROVAL_KEYS_HINT = "[y] 允许 [n] 拒绝 [a] 本会话允许同类 [v] 完整输入";
|
|
99
|
+
class ApprovalDialog {
|
|
100
|
+
request;
|
|
101
|
+
host;
|
|
102
|
+
decide;
|
|
103
|
+
focused = false;
|
|
104
|
+
expanded = false;
|
|
105
|
+
body = new Container();
|
|
106
|
+
constructor(request, host, decide) {
|
|
107
|
+
this.request = request;
|
|
108
|
+
this.host = host;
|
|
109
|
+
this.decide = decide;
|
|
110
|
+
this.rebuild();
|
|
111
|
+
}
|
|
112
|
+
rebuild() {
|
|
113
|
+
const { theme } = this.host;
|
|
114
|
+
this.body.clear();
|
|
115
|
+
const options = {
|
|
116
|
+
permissionMode: this.host.permissionMode(),
|
|
117
|
+
expanded: this.expanded,
|
|
118
|
+
};
|
|
119
|
+
if (this.host.cwd !== undefined)
|
|
120
|
+
options.cwd = this.host.cwd;
|
|
121
|
+
this.body.addChild(new Text(describeRequest(this.request, theme, options).join("\n")));
|
|
122
|
+
this.body.addChild(new Text(theme.fg("accent", APPROVAL_KEYS_HINT)));
|
|
123
|
+
}
|
|
124
|
+
handleInput(data) {
|
|
125
|
+
const keys = this.host.keybindings ?? defaultKeybindings;
|
|
126
|
+
const key = data.length === 1 ? data.toLowerCase() : "";
|
|
127
|
+
if (key === "y")
|
|
128
|
+
this.decide("allow");
|
|
129
|
+
else if (key === "a")
|
|
130
|
+
this.decide("allow_session");
|
|
131
|
+
else if (key === "n" ||
|
|
132
|
+
matchesKey(data, "escape") ||
|
|
133
|
+
keys.matches(data, "app.clear") ||
|
|
134
|
+
keys.matches(data, "tui.select.cancel")) {
|
|
135
|
+
this.decide("deny");
|
|
136
|
+
}
|
|
137
|
+
else if (key === "v") {
|
|
138
|
+
this.expanded = !this.expanded;
|
|
139
|
+
this.rebuild();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
render(width) {
|
|
143
|
+
return this.body.render(width).map((l) => truncateToWidth(l, width));
|
|
144
|
+
}
|
|
145
|
+
invalidate() {
|
|
146
|
+
this.body.invalidate();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/** UI broker:每个请求开一个底部覆盖层,等用户按键或 signal abort。 */
|
|
150
|
+
export class ApprovalDialogBroker {
|
|
151
|
+
host;
|
|
152
|
+
open = 0;
|
|
153
|
+
constructor(host) {
|
|
154
|
+
this.host = host;
|
|
155
|
+
}
|
|
156
|
+
get isOpen() {
|
|
157
|
+
return this.open > 0;
|
|
158
|
+
}
|
|
159
|
+
ask(request, signal) {
|
|
160
|
+
if (signal.aborted)
|
|
161
|
+
return Promise.resolve(undefined);
|
|
162
|
+
return new Promise((resolve) => {
|
|
163
|
+
let handle;
|
|
164
|
+
let settled = false;
|
|
165
|
+
const settle = (decision) => {
|
|
166
|
+
if (settled)
|
|
167
|
+
return;
|
|
168
|
+
settled = true;
|
|
169
|
+
signal.removeEventListener("abort", onAbort);
|
|
170
|
+
handle?.hide();
|
|
171
|
+
this.open--;
|
|
172
|
+
this.host.onClose?.();
|
|
173
|
+
this.host.report?.(request, decision ?? "cancelled");
|
|
174
|
+
resolve(decision);
|
|
175
|
+
};
|
|
176
|
+
const onAbort = () => settle(undefined);
|
|
177
|
+
const dialog = new ApprovalDialog(request, this.host, settle);
|
|
178
|
+
this.open++;
|
|
179
|
+
this.host.onOpen?.();
|
|
180
|
+
handle = this.host.showOverlay(new Box(dialog, {
|
|
181
|
+
title: (request.context?.depth ?? 0) > 0 ? "子任务审批" : "审批",
|
|
182
|
+
theme: this.host.theme,
|
|
183
|
+
}));
|
|
184
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
/** 结果的一行说明(消息区)。 */
|
|
189
|
+
export function approvalOutcomeText(request, outcome) {
|
|
190
|
+
const task = (request.context?.depth ?? 0) > 0 ? "[task] " : "";
|
|
191
|
+
const what = `${task}${request.toolName}`;
|
|
192
|
+
switch (outcome) {
|
|
193
|
+
case "allow":
|
|
194
|
+
return `已允许 ${what}`;
|
|
195
|
+
case "allow_session":
|
|
196
|
+
return `已允许 ${what}(本会话同类不再询问)`;
|
|
197
|
+
case "deny":
|
|
198
|
+
return `已拒绝 ${what}`;
|
|
199
|
+
case "cancelled":
|
|
200
|
+
return `审批已取消(超时或中断):${what}`;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 交互模式的斜杠命令:把 commands-core 的 `CommandResult` 变成界面动作。[B7]
|
|
3
|
+
*
|
|
4
|
+
* - `handled` → 消息区显示文本;`prompt` → 发提示;`exit` → 退出;
|
|
5
|
+
* - `pick` → 打开对应选择器:模型(setModel)、会话(resume)、树(/fork 无参数:从选中的用户消息
|
|
6
|
+
* 之前分叉,消息文本回填编辑器)、权限模式、思考级别;
|
|
7
|
+
* - 交互模式自有命令:`/tree`(同一文件内换叶子到选中消息之前,文本回填编辑器,可改后重发形成新分支)、
|
|
8
|
+
* `/permissions`(当前模式、判定顺序与已加载规则);`/help` 追加这两条与按键说明。
|
|
9
|
+
* - 不是命令(含模板与 `/skill:`)返回 false,调用方把整行当提示发出。
|
|
10
|
+
*/
|
|
11
|
+
import type { AgentSession } from "../../agent/types.js";
|
|
12
|
+
import type { SwitchRequest } from "../../cli/compose-session.js";
|
|
13
|
+
import type { Runtime } from "../../cli/runtime.js";
|
|
14
|
+
import type { SelectItem } from "../../tui.js";
|
|
15
|
+
import { type CommandInfo } from "../commands-core.js";
|
|
16
|
+
import { type NoticeLevel } from "./message-view.js";
|
|
17
|
+
import { type PickerSpec } from "./pickers.js";
|
|
18
|
+
export declare const INTERACTIVE_COMMANDS: readonly CommandInfo[];
|
|
19
|
+
/** 补全与 /help 用的完整命令表。 */
|
|
20
|
+
export declare const ALL_COMMANDS: readonly CommandInfo[];
|
|
21
|
+
export declare const KEY_HINTS: string;
|
|
22
|
+
export interface CommandUi {
|
|
23
|
+
readonly runtime: Runtime;
|
|
24
|
+
session(): AgentSession;
|
|
25
|
+
/** 切换会话(调用方负责重新订阅、重画消息区、announceStart)。 */
|
|
26
|
+
switchSession(request: SwitchRequest): Promise<AgentSession>;
|
|
27
|
+
pick(spec: PickerSpec): Promise<SelectItem | undefined>;
|
|
28
|
+
notice(level: NoticeLevel, text: string): void;
|
|
29
|
+
setEditorText(text: string): void;
|
|
30
|
+
/** 发一条提示(不等运行结束)。 */
|
|
31
|
+
prompt(text: string): void;
|
|
32
|
+
/** /tree 换叶子后重画消息区。 */
|
|
33
|
+
reload(): void;
|
|
34
|
+
exit(code: number): void;
|
|
35
|
+
now(): number;
|
|
36
|
+
}
|
|
37
|
+
/** 当前分支上的条目 id(树选择器标 ●)。 */
|
|
38
|
+
export declare function activeBranchIds(session: AgentSession): Set<string>;
|
|
39
|
+
export declare function permissionsText(runtime: Runtime, session: AgentSession): string;
|
|
40
|
+
/** 返回 false:不是命令,调用方当作提示发出。 */
|
|
41
|
+
export declare function runInteractiveCommand(line: string, ui: CommandUi): Promise<boolean>;
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 交互模式的斜杠命令:把 commands-core 的 `CommandResult` 变成界面动作。[B7]
|
|
3
|
+
*
|
|
4
|
+
* - `handled` → 消息区显示文本;`prompt` → 发提示;`exit` → 退出;
|
|
5
|
+
* - `pick` → 打开对应选择器:模型(setModel)、会话(resume)、树(/fork 无参数:从选中的用户消息
|
|
6
|
+
* 之前分叉,消息文本回填编辑器)、权限模式、思考级别;
|
|
7
|
+
* - 交互模式自有命令:`/tree`(同一文件内换叶子到选中消息之前,文本回填编辑器,可改后重发形成新分支)、
|
|
8
|
+
* `/permissions`(当前模式、判定顺序与已加载规则);`/help` 追加这两条与按键说明。
|
|
9
|
+
* - 不是命令(含模板与 `/skill:`)返回 false,调用方把整行当提示发出。
|
|
10
|
+
*/
|
|
11
|
+
import { AgentSessionImpl } from "../../agent/session.js";
|
|
12
|
+
import { listSessions } from "../../cli/compose-store.js";
|
|
13
|
+
import { BUILTIN_COMMANDS, parseSlash, runSlashCommand, } from "../commands-core.js";
|
|
14
|
+
import { contentText } from "./message-view.js";
|
|
15
|
+
import { modelItems, permissionItems, sessionItems, thinkingItems, treeItems, } from "./pickers.js";
|
|
16
|
+
export const INTERACTIVE_COMMANDS = [
|
|
17
|
+
{ name: "tree", description: "浏览会话树,回到某条消息之前重写" },
|
|
18
|
+
{ name: "permissions", description: "权限模式、判定顺序与规则" },
|
|
19
|
+
];
|
|
20
|
+
/** 补全与 /help 用的完整命令表。 */
|
|
21
|
+
export const ALL_COMMANDS = [...BUILTIN_COMMANDS, ...INTERACTIVE_COMMANDS];
|
|
22
|
+
export const KEY_HINTS = [
|
|
23
|
+
"Enter 发送(运行中 = steer) Alt+Enter 排到本轮之后 Shift+Enter / Ctrl+J 换行",
|
|
24
|
+
"Esc 中断(排队消息回填编辑器) Alt+↑ 取回最后一条排队消息",
|
|
25
|
+
"Shift+Tab 切换权限模式 Ctrl+L 模型 Ctrl+T 思考级别 Ctrl+O 展开工具输出",
|
|
26
|
+
"Ctrl+C 清空输入(再按退出) Ctrl+D 空输入时退出 Tab 补全 @ 引用文件",
|
|
27
|
+
].join("\n");
|
|
28
|
+
function userEntryText(entry) {
|
|
29
|
+
if (entry?.type !== "message" || entry.message.role !== "user")
|
|
30
|
+
return undefined;
|
|
31
|
+
return contentText(entry.message.content);
|
|
32
|
+
}
|
|
33
|
+
/** 当前分支上的条目 id(树选择器标 ●)。 */
|
|
34
|
+
export function activeBranchIds(session) {
|
|
35
|
+
if (session instanceof AgentSessionImpl)
|
|
36
|
+
return new Set(session.manager.branch().map((e) => e.id));
|
|
37
|
+
return new Set(session.entries.map((e) => e.id));
|
|
38
|
+
}
|
|
39
|
+
export function permissionsText(runtime, session) {
|
|
40
|
+
const rules = runtime.permission.rules;
|
|
41
|
+
const lines = [
|
|
42
|
+
`权限模式:${session.state.permissionMode}`,
|
|
43
|
+
"判定顺序:deny 规则 → Hook deny → 危险命令确认 → 权限模式 → allow 规则 / Hook allow / 本会话记忆 → 询问",
|
|
44
|
+
rules.length === 0 ? "规则:(无)" : `规则(${rules.length}):`,
|
|
45
|
+
...rules.map((r) => ` ${r.effect === "deny" ? "deny " : "allow"} ${r.raw} [${r.source}]`),
|
|
46
|
+
];
|
|
47
|
+
return lines.join("\n");
|
|
48
|
+
}
|
|
49
|
+
async function pickTree(ui, title) {
|
|
50
|
+
const session = ui.session();
|
|
51
|
+
const items = treeItems(session.entries, activeBranchIds(session), ui.now());
|
|
52
|
+
if (items.length === 0) {
|
|
53
|
+
ui.notice("info", "还没有用户消息");
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
const active = [...items].reverse().find((i) => i.label.includes("● "));
|
|
57
|
+
const picked = await ui.pick({
|
|
58
|
+
title,
|
|
59
|
+
items,
|
|
60
|
+
filterable: true,
|
|
61
|
+
...(active !== undefined ? { selected: active.value } : {}),
|
|
62
|
+
});
|
|
63
|
+
return picked === undefined ? undefined : session.entries.find((e) => e.id === picked.value);
|
|
64
|
+
}
|
|
65
|
+
/** /tree:换叶子到选中用户消息之前,文本回填编辑器。 */
|
|
66
|
+
async function treeCommand(ui) {
|
|
67
|
+
const session = ui.session();
|
|
68
|
+
if (!(session instanceof AgentSessionImpl)) {
|
|
69
|
+
ui.notice("warn", "当前会话不支持 /tree");
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (session.state.isStreaming) {
|
|
73
|
+
ui.notice("warn", "运行中不能切换分支(先 Esc 中断)");
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const entry = await pickTree(ui, "会话树");
|
|
77
|
+
if (entry === undefined)
|
|
78
|
+
return;
|
|
79
|
+
await session.navigate(entry.parentId);
|
|
80
|
+
ui.reload();
|
|
81
|
+
ui.setEditorText(userEntryText(entry) ?? "");
|
|
82
|
+
}
|
|
83
|
+
async function handlePick(what, ui) {
|
|
84
|
+
const session = ui.session();
|
|
85
|
+
switch (what) {
|
|
86
|
+
case "model": {
|
|
87
|
+
const current = session.state.model;
|
|
88
|
+
const picked = await ui.pick({
|
|
89
|
+
title: "选择模型",
|
|
90
|
+
items: await modelItems(ui.runtime.providers),
|
|
91
|
+
filterable: true,
|
|
92
|
+
...(current !== undefined ? { selected: `${current.provider}/${current.id}` } : {}),
|
|
93
|
+
});
|
|
94
|
+
if (picked === undefined)
|
|
95
|
+
return;
|
|
96
|
+
await session.setModel(picked.value);
|
|
97
|
+
ui.notice("info", `模型:${picked.value}`);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
case "session": {
|
|
101
|
+
const items = listSessions({
|
|
102
|
+
sessionDir: ui.runtime.paths.sessionDir,
|
|
103
|
+
cwd: session.state.cwd,
|
|
104
|
+
});
|
|
105
|
+
const others = items.filter((i) => i.id !== session.state.sessionId);
|
|
106
|
+
if (others.length === 0) {
|
|
107
|
+
ui.notice("info", "本目录没有其它会话");
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const picked = await ui.pick({
|
|
111
|
+
title: "恢复会话",
|
|
112
|
+
items: sessionItems(others, ui.now()),
|
|
113
|
+
filterable: true,
|
|
114
|
+
});
|
|
115
|
+
if (picked === undefined)
|
|
116
|
+
return;
|
|
117
|
+
const next = await ui.switchSession({ kind: "resume", id: picked.value });
|
|
118
|
+
ui.notice("info", `已恢复会话 ${next.state.sessionId.slice(0, 8)}`);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
case "tree": {
|
|
122
|
+
const entry = await pickTree(ui, "从哪条消息之前分叉");
|
|
123
|
+
if (entry === undefined)
|
|
124
|
+
return;
|
|
125
|
+
const next = await ui.switchSession(entry.parentId === null ? { kind: "new" } : { kind: "fork", entryId: entry.parentId });
|
|
126
|
+
ui.notice("info", `已分叉到新会话 ${next.state.sessionId.slice(0, 8)}`);
|
|
127
|
+
ui.setEditorText(userEntryText(entry) ?? "");
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
case "permission": {
|
|
131
|
+
const picked = await ui.pick({
|
|
132
|
+
title: "权限模式",
|
|
133
|
+
items: permissionItems(),
|
|
134
|
+
selected: session.state.permissionMode,
|
|
135
|
+
});
|
|
136
|
+
if (picked !== undefined)
|
|
137
|
+
session.setPermissionMode(picked.value);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
case "thinking": {
|
|
141
|
+
const model = session.state.model;
|
|
142
|
+
const found = model === undefined
|
|
143
|
+
? undefined
|
|
144
|
+
: ui.runtime.providers.findModel(`${model.provider}/${model.id}`);
|
|
145
|
+
const reasoning = found?.ok === true ? found.model.reasoning : true;
|
|
146
|
+
const picked = await ui.pick({
|
|
147
|
+
title: "思考级别",
|
|
148
|
+
items: thinkingItems(reasoning),
|
|
149
|
+
selected: session.state.thinkingLevel,
|
|
150
|
+
});
|
|
151
|
+
if (picked !== undefined)
|
|
152
|
+
session.setThinkingLevel(picked.value);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/** 返回 false:不是命令,调用方当作提示发出。 */
|
|
158
|
+
export async function runInteractiveCommand(line, ui) {
|
|
159
|
+
const parsed = parseSlash(line);
|
|
160
|
+
if (parsed === undefined)
|
|
161
|
+
return false;
|
|
162
|
+
try {
|
|
163
|
+
if (parsed.name === "tree") {
|
|
164
|
+
await treeCommand(ui);
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
if (parsed.name === "permissions") {
|
|
168
|
+
ui.notice("info", permissionsText(ui.runtime, ui.session()));
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
const result = await runSlashCommand(line, {
|
|
172
|
+
runtime: ui.runtime,
|
|
173
|
+
session: () => ui.session(),
|
|
174
|
+
switchSession: (request) => ui.switchSession(request),
|
|
175
|
+
});
|
|
176
|
+
if (result === undefined)
|
|
177
|
+
return false;
|
|
178
|
+
switch (result.kind) {
|
|
179
|
+
case "exit":
|
|
180
|
+
ui.exit(0);
|
|
181
|
+
break;
|
|
182
|
+
case "prompt":
|
|
183
|
+
ui.prompt(result.text);
|
|
184
|
+
break;
|
|
185
|
+
case "pick":
|
|
186
|
+
await handlePick(result.what, ui);
|
|
187
|
+
break;
|
|
188
|
+
case "handled":
|
|
189
|
+
if (parsed.name === "help") {
|
|
190
|
+
const extra = INTERACTIVE_COMMANDS.map((c) => `/${c.name} ${c.description}`);
|
|
191
|
+
ui.notice("info", [result.message ?? "", ...extra, "", KEY_HINTS].join("\n"));
|
|
192
|
+
}
|
|
193
|
+
else if (result.message !== undefined)
|
|
194
|
+
ui.notice("info", result.message);
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
ui.notice("error", error instanceof Error ? error.message : String(error));
|
|
200
|
+
}
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 编辑器补全(设计 §12.4)。[B7]
|
|
3
|
+
*
|
|
4
|
+
* - 首行行首 `/`:内置命令(commands-core 的表 + 交互模式自有命令)、提示模板 `/<名字>`、
|
|
5
|
+
* Skill `/skill:<名字>`;名字前缀匹配排前,其次子串匹配。
|
|
6
|
+
* - `@` 文件(行首或空白之后):遍历会话 cwd(尊重 .gitignore,复用 tools/ignore.ts 的 walk),
|
|
7
|
+
* 最多 50 项;查询含 glob 字符时按 glob(tools/glob.ts)匹配相对路径,否则按「文件名前缀 → 路径前缀 →
|
|
8
|
+
* 文件名子串 → 路径子串」排序。目录以 `/` 结尾,可继续补全。文件表按 cwd 缓存 15 秒、最多 20 000 项。
|
|
9
|
+
*/
|
|
10
|
+
import type { AutocompleteContext, AutocompleteItem, AutocompleteProvider, AutocompleteResult } from "../../tui.js";
|
|
11
|
+
export declare const MAX_FILE_ITEMS = 50;
|
|
12
|
+
export declare const MAX_FILE_INDEX = 20000;
|
|
13
|
+
export declare const FILE_INDEX_TTL_MS = 15000;
|
|
14
|
+
export interface CompletionCommand {
|
|
15
|
+
name: string;
|
|
16
|
+
args?: string;
|
|
17
|
+
description: string;
|
|
18
|
+
}
|
|
19
|
+
export interface CompletionSource {
|
|
20
|
+
commands(): readonly CompletionCommand[];
|
|
21
|
+
prompts(): readonly {
|
|
22
|
+
name: string;
|
|
23
|
+
}[];
|
|
24
|
+
skills(): readonly {
|
|
25
|
+
name: string;
|
|
26
|
+
description: string;
|
|
27
|
+
}[];
|
|
28
|
+
cwd(): string;
|
|
29
|
+
now?(): number;
|
|
30
|
+
}
|
|
31
|
+
interface FileIndex {
|
|
32
|
+
cwd: string;
|
|
33
|
+
at: number;
|
|
34
|
+
entries: {
|
|
35
|
+
rel: string;
|
|
36
|
+
isDir: boolean;
|
|
37
|
+
}[];
|
|
38
|
+
}
|
|
39
|
+
/** 文件匹配排序:0 文件名前缀 / 1 路径前缀 / 2 文件名子串 / 3 路径子串。 */
|
|
40
|
+
export declare function fileScore(rel: string, query: string): number | undefined;
|
|
41
|
+
export declare class InteractiveCompletion implements AutocompleteProvider {
|
|
42
|
+
private readonly source;
|
|
43
|
+
private index;
|
|
44
|
+
private loading;
|
|
45
|
+
constructor(source: CompletionSource);
|
|
46
|
+
getSuggestions(context: AutocompleteContext): AutocompleteResult | null | Promise<AutocompleteResult | null>;
|
|
47
|
+
private slash;
|
|
48
|
+
private now;
|
|
49
|
+
/** 文件表(缓存);并发请求共用一次遍历。 */
|
|
50
|
+
fileIndex(): Promise<FileIndex>;
|
|
51
|
+
files(query: string): Promise<AutocompleteItem[]>;
|
|
52
|
+
}
|
|
53
|
+
export {};
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 编辑器补全(设计 §12.4)。[B7]
|
|
3
|
+
*
|
|
4
|
+
* - 首行行首 `/`:内置命令(commands-core 的表 + 交互模式自有命令)、提示模板 `/<名字>`、
|
|
5
|
+
* Skill `/skill:<名字>`;名字前缀匹配排前,其次子串匹配。
|
|
6
|
+
* - `@` 文件(行首或空白之后):遍历会话 cwd(尊重 .gitignore,复用 tools/ignore.ts 的 walk),
|
|
7
|
+
* 最多 50 项;查询含 glob 字符时按 glob(tools/glob.ts)匹配相对路径,否则按「文件名前缀 → 路径前缀 →
|
|
8
|
+
* 文件名子串 → 路径子串」排序。目录以 `/` 结尾,可继续补全。文件表按 cwd 缓存 15 秒、最多 20 000 项。
|
|
9
|
+
*/
|
|
10
|
+
import { GlobMatcher } from "../../tools/glob.js";
|
|
11
|
+
import { walk } from "../../tools/ignore.js";
|
|
12
|
+
export const MAX_FILE_ITEMS = 50;
|
|
13
|
+
export const MAX_FILE_INDEX = 20_000;
|
|
14
|
+
export const FILE_INDEX_TTL_MS = 15_000;
|
|
15
|
+
const GLOB_CHARS = /[*?[{]/;
|
|
16
|
+
function rank(name, query) {
|
|
17
|
+
const lower = name.toLowerCase();
|
|
18
|
+
const q = query.toLowerCase();
|
|
19
|
+
if (lower.startsWith(q))
|
|
20
|
+
return 0;
|
|
21
|
+
if (lower.includes(q))
|
|
22
|
+
return 1;
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
function basename(rel) {
|
|
26
|
+
const trimmed = rel.endsWith("/") ? rel.slice(0, -1) : rel;
|
|
27
|
+
const at = trimmed.lastIndexOf("/");
|
|
28
|
+
return at === -1 ? trimmed : trimmed.slice(at + 1);
|
|
29
|
+
}
|
|
30
|
+
/** 文件匹配排序:0 文件名前缀 / 1 路径前缀 / 2 文件名子串 / 3 路径子串。 */
|
|
31
|
+
export function fileScore(rel, query) {
|
|
32
|
+
if (query === "")
|
|
33
|
+
return 0;
|
|
34
|
+
const q = query.toLowerCase();
|
|
35
|
+
const base = basename(rel).toLowerCase();
|
|
36
|
+
const path = rel.toLowerCase();
|
|
37
|
+
if (base.startsWith(q))
|
|
38
|
+
return 0;
|
|
39
|
+
if (path.startsWith(q))
|
|
40
|
+
return 1;
|
|
41
|
+
if (base.includes(q))
|
|
42
|
+
return 2;
|
|
43
|
+
if (path.includes(q))
|
|
44
|
+
return 3;
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
export class InteractiveCompletion {
|
|
48
|
+
source;
|
|
49
|
+
index;
|
|
50
|
+
loading;
|
|
51
|
+
constructor(source) {
|
|
52
|
+
this.source = source;
|
|
53
|
+
}
|
|
54
|
+
getSuggestions(context) {
|
|
55
|
+
const before = context.textBeforeCursor;
|
|
56
|
+
const firstLine = context.text.startsWith(context.line);
|
|
57
|
+
const slash = /^\/(\S*)$/.exec(before);
|
|
58
|
+
if (slash !== null && firstLine)
|
|
59
|
+
return this.slash(slash[1] ?? "");
|
|
60
|
+
const at = /(?:^|\s)@(\S*)$/.exec(before);
|
|
61
|
+
if (at !== null) {
|
|
62
|
+
const query = at[1] ?? "";
|
|
63
|
+
return this.files(query).then((items) => items.length === 0 ? null : { items, from: before.length - query.length - 1 });
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
slash(query) {
|
|
68
|
+
const ranked = [];
|
|
69
|
+
const push = (value, description, key) => {
|
|
70
|
+
const score = rank(key, query);
|
|
71
|
+
if (score === undefined)
|
|
72
|
+
return;
|
|
73
|
+
ranked.push({
|
|
74
|
+
item: { value: `/${value}`, label: `/${value}`, description },
|
|
75
|
+
score,
|
|
76
|
+
order: ranked.length,
|
|
77
|
+
});
|
|
78
|
+
};
|
|
79
|
+
for (const command of this.source.commands()) {
|
|
80
|
+
push(command.name, command.args !== undefined
|
|
81
|
+
? `${command.args} ${command.description}`
|
|
82
|
+
: command.description, command.name);
|
|
83
|
+
}
|
|
84
|
+
for (const prompt of this.source.prompts())
|
|
85
|
+
push(prompt.name, "提示模板", prompt.name);
|
|
86
|
+
for (const skill of this.source.skills()) {
|
|
87
|
+
push(`skill:${skill.name}`, skill.description, `skill:${skill.name}`);
|
|
88
|
+
}
|
|
89
|
+
if (ranked.length === 0)
|
|
90
|
+
return null;
|
|
91
|
+
ranked.sort((a, b) => a.score - b.score || a.order - b.order);
|
|
92
|
+
return { items: ranked.slice(0, MAX_FILE_ITEMS).map((r) => r.item), from: 0 };
|
|
93
|
+
}
|
|
94
|
+
now() {
|
|
95
|
+
return this.source.now?.() ?? Date.now();
|
|
96
|
+
}
|
|
97
|
+
/** 文件表(缓存);并发请求共用一次遍历。 */
|
|
98
|
+
async fileIndex() {
|
|
99
|
+
const cwd = this.source.cwd();
|
|
100
|
+
const cached = this.index;
|
|
101
|
+
if (cached !== undefined && cached.cwd === cwd && this.now() - cached.at < FILE_INDEX_TTL_MS) {
|
|
102
|
+
return cached;
|
|
103
|
+
}
|
|
104
|
+
this.loading ??= (async () => {
|
|
105
|
+
const entries = [];
|
|
106
|
+
const controller = new AbortController();
|
|
107
|
+
try {
|
|
108
|
+
for await (const entry of walk(cwd, { signal: controller.signal })) {
|
|
109
|
+
entries.push({ rel: entry.isDir ? `${entry.rel}/` : entry.rel, isDir: entry.isDir });
|
|
110
|
+
if (entries.length >= MAX_FILE_INDEX) {
|
|
111
|
+
controller.abort();
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
// 遍历出错(权限等):用已收集到的部分
|
|
118
|
+
}
|
|
119
|
+
const index = { cwd, at: this.now(), entries };
|
|
120
|
+
this.index = index;
|
|
121
|
+
return index;
|
|
122
|
+
})().finally(() => {
|
|
123
|
+
this.loading = undefined;
|
|
124
|
+
});
|
|
125
|
+
return this.loading;
|
|
126
|
+
}
|
|
127
|
+
async files(query) {
|
|
128
|
+
const { entries } = await this.fileIndex();
|
|
129
|
+
let matched;
|
|
130
|
+
if (GLOB_CHARS.test(query)) {
|
|
131
|
+
let matcher;
|
|
132
|
+
try {
|
|
133
|
+
matcher = new GlobMatcher(query);
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return [];
|
|
137
|
+
}
|
|
138
|
+
matched = entries
|
|
139
|
+
.filter((e) => !e.isDir && matcher.matches(e.rel))
|
|
140
|
+
.map((e) => ({ rel: e.rel, score: 0 }));
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
matched = [];
|
|
144
|
+
for (const entry of entries) {
|
|
145
|
+
const score = fileScore(entry.rel, query);
|
|
146
|
+
if (score !== undefined)
|
|
147
|
+
matched.push({ rel: entry.rel, score });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const depth = (rel) => rel.split("/").filter(Boolean).length;
|
|
151
|
+
matched.sort((a, b) => a.score - b.score ||
|
|
152
|
+
depth(a.rel) - depth(b.rel) ||
|
|
153
|
+
a.rel.length - b.rel.length ||
|
|
154
|
+
a.rel.localeCompare(b.rel));
|
|
155
|
+
return matched.slice(0, MAX_FILE_ITEMS).map((m) => ({ value: `@${m.rel}`, label: m.rel }));
|
|
156
|
+
}
|
|
157
|
+
}
|