From 842e699a643d8a60647bd824d28255c56ad61a42 Mon Sep 17 00:00:00 2001 From: Kaiyi Date: Fri, 22 May 2026 15:54:50 +0800 Subject: [PATCH] Kimi For Coding --- .agents/skills/gen-changesets/SKILL.md | 109 + .agents/skills/gen-docs/SKILL.md | 87 + .agents/skills/sync-changelog/SKILL.md | 247 + .agents/skills/translate-docs/SKILL.md | 65 + .changeset/README.md | 152 + .changeset/config.json | 18 + .editorconfig | 12 + .github/ISSUE_TEMPLATE/1-bug-report.yml | 65 + .github/ISSUE_TEMPLATE/2-feature-request.yml | 25 + .../actions/macos-keychain-cleanup/action.yml | 12 + .../actions/macos-keychain-setup/action.yml | 82 + .github/actions/macos-notarize/action.yml | 95 + .github/pr-title-checker-config.json | 12 + .github/pull_request_template.md | 25 + .github/workflows/_native-build.yml | 115 + .github/workflows/ci.yml | 84 + .github/workflows/docs-deploy.yml | 60 + .github/workflows/native-bundle.yml | 21 + .github/workflows/pr-title-checker.yml | 15 + .github/workflows/release.yml | 112 + .gitignore | 13 + .npmrc | 3 + .nvmrc | 1 + .oxfmtrc.json | 19 + .oxlintrc.json | 155 + AGENTS.md | 60 + CONTRIBUTING.md | 96 + LICENSE | 21 + Makefile | 67 + README.md | 100 + README.zh-CN.md | 104 + SECURITY.md | 33 + apps/kimi-code/.gitignore | 2 + apps/kimi-code/AGENTS.md | 128 + apps/kimi-code/README.md | 88 + apps/kimi-code/package.json | 88 + apps/kimi-code/scripts/native/01-bundle.mjs | 17 + apps/kimi-code/scripts/native/02-sea-blob.mjs | 69 + apps/kimi-code/scripts/native/03-inject.mjs | 65 + apps/kimi-code/scripts/native/04-sign.mjs | 68 + apps/kimi-code/scripts/native/05-verify.mjs | 30 + apps/kimi-code/scripts/native/assets.mjs | 276 + apps/kimi-code/scripts/native/build.mjs | 47 + .../kimi-code/scripts/native/check-bundle.mjs | 90 + .../scripts/native/entitlements.plist | 24 + apps/kimi-code/scripts/native/exec.mjs | 61 + apps/kimi-code/scripts/native/manifest.mjs | 13 + apps/kimi-code/scripts/native/native-deps.mjs | 103 + apps/kimi-code/scripts/native/package.mjs | 53 + apps/kimi-code/scripts/native/paths.mjs | 57 + .../scripts/native/produce-manifest.mjs | 55 + .../scripts/native/resolve-release.mjs | 55 + apps/kimi-code/scripts/native/smoke.mjs | 82 + apps/kimi-code/scripts/postinstall.mjs | 269 + .../kimi-code/scripts/postinstall/migrate.mjs | 351 + apps/kimi-code/scripts/postinstall/reach.mjs | 457 + apps/kimi-code/scripts/postinstall/ui.mjs | 458 + apps/kimi-code/scripts/smoke.mjs | 58 + apps/kimi-code/src/cli/build-info.ts | 34 + apps/kimi-code/src/cli/commands.ts | 98 + apps/kimi-code/src/cli/options.ts | 58 + apps/kimi-code/src/cli/run-prompt.ts | 673 ++ apps/kimi-code/src/cli/run-shell.ts | 180 + apps/kimi-code/src/cli/startup-error.ts | 35 + apps/kimi-code/src/cli/sub/export.ts | 224 + apps/kimi-code/src/cli/update/cache.ts | 31 + apps/kimi-code/src/cli/update/cdn.ts | 27 + apps/kimi-code/src/cli/update/preflight.ts | 246 + apps/kimi-code/src/cli/update/prompt.ts | 184 + apps/kimi-code/src/cli/update/refresh.ts | 32 + apps/kimi-code/src/cli/update/select.ts | 13 + apps/kimi-code/src/cli/update/source.ts | 155 + apps/kimi-code/src/cli/update/types.ts | 33 + apps/kimi-code/src/cli/version.ts | 63 + apps/kimi-code/src/constant/app.ts | 49 + apps/kimi-code/src/constant/index.ts | 4 + apps/kimi-code/src/constant/startup-error.ts | 2 + apps/kimi-code/src/constant/terminal.ts | 8 + apps/kimi-code/src/constant/update.ts | 7 + apps/kimi-code/src/main.ts | 128 + apps/kimi-code/src/migration/badge.ts | 27 + apps/kimi-code/src/migration/command.ts | 19 + .../kimi-code/src/migration/detect-pending.ts | 69 + apps/kimi-code/src/migration/index.ts | 12 + .../src/migration/migration-screen.ts | 532 ++ apps/kimi-code/src/native/module-hook.ts | 40 + apps/kimi-code/src/native/native-assets.ts | 371 + apps/kimi-code/src/native/native-require.ts | 30 + apps/kimi-code/src/native/smoke.ts | 26 + apps/kimi-code/src/tui/actions/replay-ops.ts | 699 ++ apps/kimi-code/src/tui/commands/index.ts | 5 + apps/kimi-code/src/tui/commands/parse.ts | 12 + apps/kimi-code/src/tui/commands/registry.ts | 181 + apps/kimi-code/src/tui/commands/resolve.ts | 115 + apps/kimi-code/src/tui/commands/skills.ts | 33 + apps/kimi-code/src/tui/commands/types.ts | 20 + .../tui/components/chrome/device-code-box.ts | 76 + .../src/tui/components/chrome/footer.ts | 266 + .../tui/components/chrome/gutter-container.ts | 33 + .../src/tui/components/chrome/moon-loader.ts | 68 + .../src/tui/components/chrome/todo-panel.ts | 96 + .../src/tui/components/chrome/welcome.ts | 86 + .../dialogs/api-key-input-dialog.ts | 148 + .../tui/components/dialogs/approval-panel.ts | 406 + .../tui/components/dialogs/choice-picker.ts | 133 + .../src/tui/components/dialogs/compaction.ts | 109 + .../tui/components/dialogs/editor-selector.ts | 31 + .../dialogs/feedback-input-dialog.ts | 126 + .../src/tui/components/dialogs/help-panel.ts | 148 + .../tui/components/dialogs/model-selector.ts | 184 + .../components/dialogs/permission-selector.ts | 52 + .../components/dialogs/platform-selector.ts | 27 + .../tui/components/dialogs/question-dialog.ts | 755 ++ .../tui/components/dialogs/session-picker.ts | 268 + .../components/dialogs/settings-selector.ts | 63 + .../components/dialogs/task-output-viewer.ts | 261 + .../tui/components/dialogs/tasks-browser.ts | 603 ++ .../tui/components/dialogs/theme-selector.ts | 36 + .../tui/components/editor/custom-editor.ts | 362 + .../editor/file-mention-provider.ts | 272 + apps/kimi-code/src/tui/components/index.ts | 35 + .../tui/components/media/code-highlight.ts | 52 + .../src/tui/components/media/diff-preview.ts | 310 + .../tui/components/media/image-thumbnail.ts | 55 + .../tui/components/messages/agent-group.ts | 240 + .../components/messages/assistant-message.ts | 63 + .../messages/background-agent-status.ts | 42 + .../components/messages/mcp-status-panel.ts | 143 + .../src/tui/components/messages/plan-box.ts | 124 + .../src/tui/components/messages/read-group.ts | 188 + .../components/messages/shell-execution.ts | 88 + .../components/messages/skill-activation.ts | 36 + .../tui/components/messages/status-message.ts | 23 + .../tui/components/messages/status-panel.ts | 148 + .../src/tui/components/messages/thinking.ts | 122 + .../src/tui/components/messages/tool-call.ts | 1675 ++++ .../messages/tool-renderers/chip.ts | 126 + .../messages/tool-renderers/media.ts | 152 + .../messages/tool-renderers/registry.ts | 55 + .../messages/tool-renderers/summary.ts | 91 + .../messages/tool-renderers/truncated.ts | 22 + .../messages/tool-renderers/types.ts | 26 + .../tui/components/messages/usage-panel.ts | 246 + .../tui/components/messages/user-message.ts | 63 + .../src/tui/components/panes/activity-pane.ts | 29 + .../src/tui/components/panes/queue-pane.ts | 36 + apps/kimi-code/src/tui/config.ts | 153 + apps/kimi-code/src/tui/constant/feedback.ts | 42 + apps/kimi-code/src/tui/constant/kimi-tui.ts | 17 + apps/kimi-code/src/tui/constant/rendering.ts | 19 + apps/kimi-code/src/tui/constant/streaming.ts | 4 + apps/kimi-code/src/tui/constant/symbols.ts | 7 + apps/kimi-code/src/tui/constant/terminal.ts | 40 + apps/kimi-code/src/tui/index.ts | 3 + apps/kimi-code/src/tui/kimi-tui.ts | 5221 +++++++++++ .../src/tui/reverse-rpc/approval/adapter.ts | 339 + .../tui/reverse-rpc/approval/controller.ts | 27 + .../src/tui/reverse-rpc/approval/handler.ts | 24 + .../src/tui/reverse-rpc/base-controller.ts | 111 + apps/kimi-code/src/tui/reverse-rpc/index.ts | 44 + .../src/tui/reverse-rpc/modal-coordinator.ts | 97 + .../tui/reverse-rpc/question/controller.ts | 11 + .../src/tui/reverse-rpc/question/handler.ts | 57 + apps/kimi-code/src/tui/reverse-rpc/types.ts | 151 + apps/kimi-code/src/tui/theme/bundle.ts | 27 + apps/kimi-code/src/tui/theme/colors.ts | 148 + apps/kimi-code/src/tui/theme/detect.ts | 121 + apps/kimi-code/src/tui/theme/index.ts | 46 + apps/kimi-code/src/tui/theme/pi-tui-theme.ts | 71 + apps/kimi-code/src/tui/theme/styles.ts | 66 + .../src/tui/theme/terminal-background.ts | 28 + apps/kimi-code/src/tui/types.ts | 149 + .../src/tui/utils/background-agent-status.ts | 40 + .../src/tui/utils/background-task-status.ts | 104 + .../src/tui/utils/component-capabilities.ts | 40 + apps/kimi-code/src/tui/utils/errors.ts | 14 + apps/kimi-code/src/tui/utils/event-payload.ts | 81 + .../src/tui/utils/image-attachment-store.ts | 120 + .../src/tui/utils/image-placeholder.ts | 126 + apps/kimi-code/src/tui/utils/mcp-oauth.ts | 51 + .../src/tui/utils/mcp-server-status.ts | 73 + apps/kimi-code/src/tui/utils/mcp-tool-name.ts | 16 + apps/kimi-code/src/tui/utils/media-url.ts | 51 + apps/kimi-code/src/tui/utils/open-url.ts | 11 + apps/kimi-code/src/tui/utils/printable-key.ts | 27 + apps/kimi-code/src/tui/utils/proctitle.ts | 29 + .../kimi-code/src/tui/utils/terminal-focus.ts | 44 + .../src/tui/utils/terminal-notification.ts | 129 + .../kimi-code/src/tui/utils/terminal-state.ts | 19 + .../kimi-code/src/tui/utils/terminal-theme.ts | 153 + apps/kimi-code/src/tui/utils/transcript-id.ts | 6 + .../src/utils/clipboard/clipboard-image.ts | 516 ++ .../src/utils/clipboard/clipboard-native.ts | 51 + apps/kimi-code/src/utils/git/git-ls-files.ts | 189 + apps/kimi-code/src/utils/git/git-status.ts | 351 + .../src/utils/history/input-history.ts | 42 + apps/kimi-code/src/utils/image/image-mime.ts | 189 + apps/kimi-code/src/utils/paths.ts | 55 + apps/kimi-code/src/utils/persistence.ts | 105 + .../src/utils/process/external-editor.ts | 60 + apps/kimi-code/src/utils/process/fd-detect.ts | 28 + apps/kimi-code/src/utils/process/proctitle.ts | 31 + apps/kimi-code/src/utils/process/stdin.ts | 18 + .../kimi-code/src/utils/usage/usage-format.ts | 37 + apps/kimi-code/test/cli/export.test.ts | 446 + apps/kimi-code/test/cli/main.test.ts | 211 + apps/kimi-code/test/cli/options.test.ts | 256 + apps/kimi-code/test/cli/run-prompt.test.ts | 689 ++ apps/kimi-code/test/cli/run-shell.test.ts | 535 ++ .../test/cli/session-flag-picker.test.ts | 78 + apps/kimi-code/test/cli/update/cache.test.ts | 62 + apps/kimi-code/test/cli/update/cdn.test.ts | 49 + .../test/cli/update/preflight.test.ts | 269 + apps/kimi-code/test/cli/update/prompt.test.ts | 30 + .../kimi-code/test/cli/update/refresh.test.ts | 40 + apps/kimi-code/test/cli/update/select.test.ts | 34 + apps/kimi-code/test/cli/update/source.test.ts | 149 + apps/kimi-code/test/cli/version.test.ts | 28 + .../test/e2e/local-logging-export.e2e.test.ts | 143 + .../test/e2e/real-llm-smoke.e2e.test.ts | 81 + apps/kimi-code/test/helpers/process.ts | 32 + apps/kimi-code/test/migration/badge.test.ts | 47 + apps/kimi-code/test/migration/command.test.ts | 29 + .../test/migration/detect-pending.test.ts | 79 + .../test/migration/migration-screen.test.ts | 617 ++ apps/kimi-code/test/native/cache-base.test.ts | 51 + apps/kimi-code/test/native/cache-gc.test.ts | 103 + .../test/native/native-assets.test.ts | 128 + .../test/scripts/native/exec.test.ts | 46 + .../test/scripts/native/manifest.test.ts | 32 + .../test/scripts/native/native-deps.test.ts | 90 + .../test/scripts/native/paths.test.ts | 101 + .../scripts/native/release-artifacts.test.ts | 150 + .../test/scripts/native/sign-args.test.ts | 48 + .../test/scripts/nix/update-pnpm-deps.test.ts | 152 + .../replay-hydrate-agent-group.test.ts | 352 + apps/kimi-code/test/tui/activity-pane.test.ts | 112 + .../test/tui/background-task-status.test.ts | 104 + .../test/tui/commands/registry.test.ts | 105 + .../test/tui/commands/resolve.test.ts | 117 + .../test/tui/commands/skills.test.ts | 57 + .../components/chrome/device-code-box.test.ts | 66 + .../chrome/gutter-container.test.ts | 57 + .../components/dialogs/approval-panel.test.ts | 414 + .../components/dialogs/choice-picker.test.ts | 202 + .../tui/components/dialogs/compaction.test.ts | 39 + .../dialogs/feedback-input-dialog.test.ts | 108 + .../dialogs/question-dialog.test.ts | 439 + .../components/dialogs/session-picker.test.ts | 264 + .../components/editor/custom-editor.test.ts | 102 + .../editor/file-mention-provider.test.ts | 215 + .../editor/normalize-caps-locked-ctrl.test.ts | 76 + .../components/editor/prompt-symbol.test.ts | 37 + .../components/editor/side-borders.test.ts | 70 + .../components/editor/slash-highlight.test.ts | 66 + .../components/media/code-highlight.test.ts | 26 + .../tui/components/media/diff-preview.test.ts | 180 + .../messages/assistant-message.test.ts | 53 + .../messages/background-agent-status.test.ts | 56 + .../tui/components/messages/notice.test.ts | 23 + .../messages/shell-execution.test.ts | 55 + .../components/messages/status-panel.test.ts | 97 + .../tui/components/messages/thinking.test.ts | 82 + .../tui/components/messages/tool-call.test.ts | 1068 +++ .../messages/tool-renderers/chip.test.ts | 124 + .../messages/tool-renderers/media.test.ts | 150 + .../messages/tool-renderers/registry.test.ts | 165 + .../components/messages/usage-panel.test.ts | 67 + .../components/messages/user-message.test.ts | 24 + .../panels/footer-bg-agents.test.ts | 108 + .../components/panels/footer-context.test.ts | 150 + .../tui/components/panels/help-panel.test.ts | 102 + .../tui/components/panels/plan-box.test.ts | 106 + .../tui/components/panels/todo-panel.test.ts | 58 + .../components/panes/activity-pane.test.ts | 29 + .../tui/components/panes/queue-pane.test.ts | 58 + apps/kimi-code/test/tui/config.test.ts | 111 + .../test/tui/create-tui-state.test.ts | 103 + .../tui/input/image-attachment-store.test.ts | 62 + .../test/tui/input/image-placeholder.test.ts | 104 + .../test/tui/kimi-tui-message-flow.test.ts | 1172 +++ .../test/tui/kimi-tui-startup.test.ts | 678 ++ apps/kimi-code/test/tui/media-url.test.ts | 37 + .../test/tui/printable-key-guard.test.ts | 69 + apps/kimi-code/test/tui/replay-ops.test.ts | 566 ++ .../tui/reverse-rpc/approval-adapter.test.ts | 162 + .../test/tui/reverse-rpc/approval.test.ts | 142 + .../tui/reverse-rpc/base-controller.test.ts | 124 + .../test/tui/reverse-rpc/index.test.ts | 214 + .../test/tui/reverse-rpc/question.test.ts | 134 + .../test/tui/task-output-viewer.test.ts | 260 + apps/kimi-code/test/tui/tasks-browser.test.ts | 434 + .../kimi-code/test/tui/terminal-focus.test.ts | 59 + .../test/tui/terminal-notification.test.ts | 227 + .../kimi-code/test/tui/terminal-theme.test.ts | 160 + .../test/tui/utils/mcp-oauth.test.ts | 106 + .../utils/clipboard/clipboard-image.test.ts | 161 + .../test/utils/git/git-ls-files.test.ts | 113 + .../test/utils/git/git-status.test.ts | 200 + .../test/utils/history/input-history.test.ts | 84 + .../test/utils/image/image-mime.test.ts | 98 + apps/kimi-code/test/utils/paths.test.ts | 73 + apps/kimi-code/test/utils/persistence.test.ts | 116 + .../utils/process/external-editor.test.ts | 79 + .../test/utils/process/stdin.test.ts | 59 + .../test/utils/usage/usage-format.test.ts | 77 + apps/kimi-code/tsconfig.json | 10 + apps/kimi-code/tsdown.config.ts | 31 + apps/kimi-code/tsdown.native.config.ts | 64 + apps/kimi-code/vitest.config.ts | 20 + apps/vis/package.json | 19 + apps/vis/scripts/copy-web-dist.mjs | 26 + apps/vis/server/package.json | 27 + apps/vis/server/src/app.ts | 138 + apps/vis/server/src/config.ts | 156 + apps/vis/server/src/index.ts | 32 + apps/vis/server/src/lib/context-builder.ts | 500 ++ apps/vis/server/src/lib/session-delete.ts | 50 + apps/vis/server/src/lib/session-lister.ts | 196 + apps/vis/server/src/lib/session-title.ts | 25 + apps/vis/server/src/lib/subagent-loader.ts | 213 + apps/vis/server/src/lib/types.ts | 725 ++ apps/vis/server/src/lib/wire-loader.ts | 179 + apps/vis/server/src/lib/wire-replay.ts | 488 + apps/vis/server/src/routes/context.ts | 57 + apps/vis/server/src/routes/session-detail.ts | 155 + apps/vis/server/src/routes/sessions.ts | 28 + apps/vis/server/src/routes/subagents.ts | 226 + apps/vis/server/src/routes/tool-results.ts | 48 + apps/vis/server/src/routes/wire.ts | 84 + apps/vis/server/src/startup-banner.ts | 19 + apps/vis/server/test/_fixture.ts | 251 + apps/vis/server/test/app.test.ts | 74 + apps/vis/server/test/context-builder.test.ts | 242 + apps/vis/server/test/session-lister.test.ts | 116 + apps/vis/server/test/subagent-loader.test.ts | 47 + apps/vis/server/test/wire-loader.test.ts | 558 ++ apps/vis/server/tsconfig.json | 4 + apps/vis/server/tsdown.config.ts | 9 + apps/vis/server/vitest.config.ts | 8 + apps/vis/web/index.html | 39 + apps/vis/web/package.json | 39 + apps/vis/web/src/App.tsx | 20 + apps/vis/web/src/api.ts | 119 + .../components/context/CompactionRibbon.tsx | 28 + .../web/src/components/context/ContextTab.tsx | 281 + .../components/context/EphemeralBubble.tsx | 101 + .../src/components/context/MessageBubble.tsx | 193 + .../context/PersistedOutputLink.tsx | 53 + .../vis/web/src/components/files/FilesTab.tsx | 202 + .../web/src/components/layout/AppShell.tsx | 138 + apps/vis/web/src/components/layout/TabBar.tsx | 56 + .../src/components/sessions/SessionCard.tsx | 124 + .../src/components/sessions/SessionFilter.tsx | 127 + .../src/components/sessions/SessionRail.tsx | 188 + .../web/src/components/shared/CopyButton.tsx | 28 + apps/vis/web/src/components/shared/Drawer.tsx | 50 + .../web/src/components/shared/JsonViewer.tsx | 155 + apps/vis/web/src/components/shared/Pill.tsx | 78 + .../web/src/components/shared/SizePreview.tsx | 55 + .../vis/web/src/components/state/StateTab.tsx | 217 + .../src/components/subagents/SubagentNode.tsx | 92 + .../src/components/subagents/SubagentTree.tsx | 24 + .../src/components/subagents/SubagentsTab.tsx | 28 + .../web/src/components/wire/IssuesDrawer.tsx | 144 + .../vis/web/src/components/wire/TypeBadge.tsx | 20 + .../web/src/components/wire/WireHeadline.tsx | 733 ++ apps/vis/web/src/components/wire/WireRow.tsx | 134 + .../web/src/components/wire/WireRowDetail.tsx | 253 + apps/vis/web/src/components/wire/WireTab.tsx | 524 ++ apps/vis/web/src/components/wire/typeMeta.ts | 67 + apps/vis/web/src/hooks/useContext.ts | 34 + apps/vis/web/src/hooks/useSession.ts | 46 + apps/vis/web/src/hooks/useSubagents.ts | 21 + apps/vis/web/src/hooks/useTheme.ts | 82 + apps/vis/web/src/hooks/useWire.ts | 33 + apps/vis/web/src/lib/focus-context.tsx | 96 + apps/vis/web/src/lib/issues.ts | 182 + apps/vis/web/src/lib/query-parser.ts | 295 + apps/vis/web/src/main.tsx | 30 + apps/vis/web/src/pages/SessionDetailPage.tsx | 158 + apps/vis/web/src/pages/SessionListPage.tsx | 35 + apps/vis/web/src/pages/SubagentDetailPage.tsx | 206 + apps/vis/web/src/theme.css | 239 + apps/vis/web/src/types.ts | 359 + apps/vis/web/src/util/time.ts | 33 + apps/vis/web/test/api.test.ts | 39 + apps/vis/web/tsconfig.json | 24 + apps/vis/web/vite.config.ts | 22 + build/nix/update-pnpm-deps.sh | 201 + build/raw-text-loader.mjs | 23 + build/raw-text-plugin.mjs | 23 + build/register-raw-text-loader.mjs | 7 + docs/.gitignore | 3 + docs/.npmrc | 1 + docs/.vitepress/config.ts | 184 + docs/.vitepress/theme/Kimi.png | Bin 0 -> 82192 bytes .../theme/components/HomeFeatures.vue | 319 + docs/.vitepress/theme/components/HomeHero.vue | 147 + .../theme/components/HomeLayout.vue | 38 + .../theme/components/HomeQuickStart.vue | 210 + docs/.vitepress/theme/components/KimiLogo.vue | 22 + docs/.vitepress/theme/index.ts | 12 + docs/.vitepress/theme/styles/base.css | 231 + docs/.vitepress/theme/styles/home.css | 85 + docs/.vitepress/theme/styles/vars.css | 120 + docs/AGENTS.md | 179 + docs/en/configuration/config-files.md | 230 + docs/en/configuration/data-locations.md | 131 + docs/en/configuration/env-vars.md | 124 + docs/en/configuration/overrides.md | 110 + docs/en/configuration/providers.md | 136 + docs/en/customization/agents.md | 45 + docs/en/customization/hooks.md | 150 + docs/en/customization/mcp.md | 83 + docs/en/customization/skills.md | 127 + docs/en/faq.md | 3 + docs/en/guides/getting-started.md | 135 + docs/en/guides/interaction.md | 67 + docs/en/guides/migration.md | 37 + docs/en/guides/sessions.md | 110 + docs/en/guides/use-cases.md | 118 + docs/en/index.md | 13 + docs/en/reference/keyboard.md | 97 + docs/en/reference/kimi-command.md | 154 + docs/en/reference/slash-commands.md | 86 + docs/en/reference/tools.md | 104 + docs/en/release-notes/changelog.md | 5 + docs/index.md | 29 + docs/media/intro.gif | Bin 0 -> 3517259 bytes docs/package.json | 19 + docs/pnpm-lock.yaml | 3179 +++++++ docs/public/favicon.ico | Bin 0 -> 16958 bytes docs/zh/configuration/config-files.md | 228 + docs/zh/configuration/data-locations.md | 131 + docs/zh/configuration/env-vars.md | 125 + docs/zh/configuration/overrides.md | 110 + docs/zh/configuration/providers.md | 135 + docs/zh/customization/agents.md | 45 + docs/zh/customization/hooks.md | 150 + docs/zh/customization/mcp.md | 83 + docs/zh/customization/skills.md | 127 + docs/zh/faq.md | 3 + docs/zh/guides/getting-started.md | 135 + docs/zh/guides/interaction.md | 67 + docs/zh/guides/migration.md | 37 + docs/zh/guides/sessions.md | 110 + docs/zh/guides/use-cases.md | 118 + docs/zh/index.md | 13 + docs/zh/reference/keyboard.md | 112 + docs/zh/reference/kimi-command.md | 154 + docs/zh/reference/slash-commands.md | 86 + docs/zh/reference/tools.md | 104 + docs/zh/release-notes/changelog.md | 5 + flake.lock | 27 + flake.nix | 302 + package.json | 57 + packages/agent-core/README.md | 11 + packages/agent-core/package.json | 85 + .../agent-core/src/agent/background/index.ts | 191 + .../compaction/compaction-instruction.md | 67 + .../agent-core/src/agent/compaction/config.ts | 19 + .../agent-core/src/agent/compaction/full.ts | 557 ++ .../agent-core/src/agent/compaction/index.ts | 3 + .../src/agent/compaction/render-messages.ts | 111 + .../agent-core/src/agent/compaction/types.ts | 11 + packages/agent-core/src/agent/config/index.ts | 134 + .../agent-core/src/agent/config/thinking.ts | 31 + packages/agent-core/src/agent/config/types.ts | 19 + .../src/agent/context/complete-slice.ts | 55 + .../agent-core/src/agent/context/index.ts | 268 + .../src/agent/context/notification-xml.ts | 72 + .../agent-core/src/agent/context/projector.ts | 173 + .../agent-core/src/agent/context/types.ts | 78 + packages/agent-core/src/agent/hooks/engine.ts | 186 + packages/agent-core/src/agent/hooks/index.ts | 4 + packages/agent-core/src/agent/hooks/runner.ts | 228 + packages/agent-core/src/agent/hooks/types.ts | 67 + .../agent-core/src/agent/hooks/user-prompt.ts | 66 + packages/agent-core/src/agent/index.ts | 518 ++ .../src/agent/injection/injector.ts | 33 + .../agent-core/src/agent/injection/manager.ts | 34 + .../src/agent/injection/permission-mode.ts | 30 + .../src/agent/injection/plan-mode.ts | 181 + .../src/agent/permission/action-label.ts | 142 + .../src/agent/permission/check-rules.ts | 59 + .../agent-core/src/agent/permission/index.ts | 340 + .../src/agent/permission/matches-rule.ts | 125 + .../src/agent/permission/parse-pattern.ts | 50 + .../src/agent/permission/path-glob-match.ts | 156 + .../permission/policies/ask-user-question.ts | 17 + .../policies/default-git-cwd-write.ts | 127 + .../src/agent/permission/policies/index.ts | 19 + .../src/agent/permission/policies/plan.ts | 324 + .../policies/yolo-workspace-access.ts | 74 + .../agent-core/src/agent/permission/policy.ts | 42 + .../agent-core/src/agent/permission/types.ts | 60 + packages/agent-core/src/agent/plan/index.ts | 146 + .../agent-core/src/agent/records/index.ts | 139 + .../agent-core/src/agent/records/types.ts | 91 + .../agent-core/src/agent/records/wire-file.ts | 193 + packages/agent-core/src/agent/replay/index.ts | 18 + packages/agent-core/src/agent/skill/index.ts | 72 + packages/agent-core/src/agent/tool/index.ts | 421 + packages/agent-core/src/agent/tool/types.ts | 31 + .../src/agent/turn/canonical-args.ts | 28 + packages/agent-core/src/agent/turn/index.ts | 858 ++ .../agent-core/src/agent/turn/kosong-llm.ts | 254 + .../agent-core/src/agent/turn/tool-dedup.ts | 195 + packages/agent-core/src/agent/usage/index.ts | 79 + packages/agent-core/src/config/index.ts | 5 + packages/agent-core/src/config/merge.ts | 62 + packages/agent-core/src/config/path.ts | 18 + packages/agent-core/src/config/resolve.ts | 26 + packages/agent-core/src/config/schema.ts | 279 + packages/agent-core/src/config/toml.ts | 508 ++ packages/agent-core/src/errors.ts | 5 + packages/agent-core/src/errors/classes.ts | 28 + packages/agent-core/src/errors/codes.ts | 377 + packages/agent-core/src/errors/index.ts | 17 + packages/agent-core/src/errors/serialize.ts | 149 + packages/agent-core/src/index.ts | 45 + packages/agent-core/src/logging/formatter.ts | 200 + packages/agent-core/src/logging/index.ts | 32 + packages/agent-core/src/logging/logger.ts | 437 + .../agent-core/src/logging/resolve-config.ts | 51 + packages/agent-core/src/logging/sinks.ts | 224 + packages/agent-core/src/logging/types.ts | 91 + packages/agent-core/src/loop/README.md | 57 + packages/agent-core/src/loop/errors.ts | 27 + packages/agent-core/src/loop/events.ts | 182 + packages/agent-core/src/loop/index.ts | 70 + packages/agent-core/src/loop/llm.ts | 71 + packages/agent-core/src/loop/retry.ts | 137 + packages/agent-core/src/loop/run-turn.ts | 141 + packages/agent-core/src/loop/tool-access.ts | 150 + packages/agent-core/src/loop/tool-call.ts | 626 ++ .../agent-core/src/loop/tool-scheduler.ts | 100 + packages/agent-core/src/loop/turn-step.ts | 249 + packages/agent-core/src/loop/types.ts | 208 + packages/agent-core/src/mcp/auth-tool.ts | 181 + packages/agent-core/src/mcp/client-http.ts | 231 + packages/agent-core/src/mcp/client-shared.ts | 91 + packages/agent-core/src/mcp/client-stdio.ts | 228 + packages/agent-core/src/mcp/config-loader.ts | 95 + .../agent-core/src/mcp/connection-manager.ts | 462 + packages/agent-core/src/mcp/index.ts | 5 + .../src/mcp/oauth/callback-server.ts | 164 + packages/agent-core/src/mcp/oauth/index.ts | 4 + packages/agent-core/src/mcp/oauth/provider.ts | 188 + packages/agent-core/src/mcp/oauth/service.ts | 218 + packages/agent-core/src/mcp/oauth/store.ts | 130 + packages/agent-core/src/mcp/output.ts | 258 + packages/agent-core/src/mcp/session-config.ts | 23 + packages/agent-core/src/mcp/tool-naming.ts | 49 + packages/agent-core/src/mcp/types.ts | 105 + packages/agent-core/src/profile/context.ts | 192 + packages/agent-core/src/profile/default.ts | 26 + .../agent-core/src/profile/default/agent.yaml | 35 + .../agent-core/src/profile/default/coder.yaml | 18 + .../src/profile/default/explore.yaml | 36 + .../agent-core/src/profile/default/init.md | 21 + .../agent-core/src/profile/default/plan.yaml | 19 + .../agent-core/src/profile/default/system.md | 165 + packages/agent-core/src/profile/index.ts | 5 + packages/agent-core/src/profile/load.ts | 125 + packages/agent-core/src/profile/resolve.ts | 222 + packages/agent-core/src/profile/types.ts | 56 + packages/agent-core/src/prompt-modules.d.ts | 12 + packages/agent-core/src/providers/index.ts | 2 + .../src/providers/provider-manager.ts | 139 + .../agent-core/src/providers/request-auth.ts | 46 + .../src/providers/runtime-provider.ts | 409 + packages/agent-core/src/rpc/client.ts | 108 + packages/agent-core/src/rpc/core-api.ts | 273 + packages/agent-core/src/rpc/core-impl.ts | 739 ++ packages/agent-core/src/rpc/events.ts | 297 + packages/agent-core/src/rpc/index.ts | 7 + packages/agent-core/src/rpc/resumed.ts | 39 + packages/agent-core/src/rpc/sdk-api.ts | 79 + packages/agent-core/src/rpc/types.ts | 29 + packages/agent-core/src/runtime-types.ts | 11 + .../agent-core/src/session/export/index.ts | 4 + .../agent-core/src/session/export/manifest.ts | 36 + .../src/session/export/session-export.ts | 86 + .../src/session/export/wire-scan.ts | 69 + packages/agent-core/src/session/export/zip.ts | 69 + .../agent-core/src/session/git-context.ts | 197 + packages/agent-core/src/session/index.ts | 511 ++ .../agent-core/src/session/prompt-metadata.ts | 62 + packages/agent-core/src/session/rpc.ts | 261 + .../agent-core/src/session/store/index.ts | 8 + .../src/session/store/session-index.ts | 79 + .../src/session/store/session-store.ts | 366 + .../src/session/store/workdir-key.ts | 18 + .../agent-core/src/session/subagent-host.ts | 345 + .../src/session/summary-continuation.md | 5 + .../agent-core/src/skill/builtin/index.ts | 8 + .../src/skill/builtin/mcp-config.md | 96 + .../src/skill/builtin/mcp-config.ts | 23 + packages/agent-core/src/skill/index.ts | 5 + packages/agent-core/src/skill/parser.ts | 295 + packages/agent-core/src/skill/registry.ts | 150 + packages/agent-core/src/skill/scanner.ts | 366 + packages/agent-core/src/skill/types.ts | 77 + packages/agent-core/src/telemetry.ts | 26 + .../agent-core/src/tools/args-validator.ts | 93 + .../agent-core/src/tools/background/index.ts | 19 + .../src/tools/background/manager.ts | 1205 +++ .../src/tools/background/persist.ts | 269 + .../src/tools/background/task-list.md | 24 + .../src/tools/background/task-list.ts | 83 + .../src/tools/background/task-output.md | 12 + .../src/tools/background/task-output.ts | 179 + .../src/tools/background/task-stop.md | 13 + .../src/tools/background/task-stop.ts | 87 + .../agent-background-disabled.md | 1 + .../collaboration/agent-background-enabled.md | 3 + .../src/tools/builtin/collaboration/agent.md | 15 + .../src/tools/builtin/collaboration/agent.ts | 334 + .../tools/builtin/collaboration/ask-user.md | 19 + .../tools/builtin/collaboration/ask-user.ts | 187 + .../tools/builtin/collaboration/skill-tool.md | 1 + .../tools/builtin/collaboration/skill-tool.ts | 168 + .../agent-core/src/tools/builtin/file/edit.md | 10 + .../agent-core/src/tools/builtin/file/edit.ts | 149 + .../agent-core/src/tools/builtin/file/glob.md | 16 + .../agent-core/src/tools/builtin/file/glob.ts | 376 + .../agent-core/src/tools/builtin/file/grep.md | 9 + .../agent-core/src/tools/builtin/file/grep.ts | 955 ++ .../src/tools/builtin/file/line-endings.ts | 51 + .../src/tools/builtin/file/read-media.md | 13 + .../src/tools/builtin/file/read-media.ts | 264 + .../agent-core/src/tools/builtin/file/read.md | 17 + .../agent-core/src/tools/builtin/file/read.ts | 444 + .../src/tools/builtin/file/write.md | 1 + .../src/tools/builtin/file/write.ts | 144 + .../agent-core/src/tools/builtin/index.ts | 19 + .../tools/builtin/planning/enter-plan-mode.md | 32 + .../tools/builtin/planning/enter-plan-mode.ts | 82 + .../tools/builtin/planning/exit-plan-mode.md | 29 + .../tools/builtin/planning/exit-plan-mode.ts | 242 + .../src/tools/builtin/shell/bash.md | 43 + .../src/tools/builtin/shell/bash.ts | 473 + .../src/tools/builtin/state/todo-list.md | 22 + .../src/tools/builtin/state/todo-list.ts | 133 + .../src/tools/builtin/web/fetch-url.md | 3 + .../src/tools/builtin/web/fetch-url.ts | 122 + .../src/tools/builtin/web/web-search.md | 3 + .../src/tools/builtin/web/web-search.ts | 152 + .../agent-core/src/tools/display/index.ts | 1 + .../agent-core/src/tools/display/schemas.ts | 158 + .../src/tools/policies/default-permissions.ts | 69 + .../src/tools/policies/path-access.ts | 334 + .../src/tools/policies/sensitive.ts | 93 + .../src/tools/providers/local-fetch-url.ts | 229 + .../src/tools/providers/moonshot-fetch-url.ts | 128 + .../tools/providers/moonshot-web-search.ts | 143 + packages/agent-core/src/tools/store.ts | 13 + .../agent-core/src/tools/support/file-type.ts | 383 + .../src/tools/support/git-worktree.ts | 98 + .../src/tools/support/input-schema.ts | 62 + .../src/tools/support/list-directory.ts | 127 + .../src/tools/support/result-builder.ts | 149 + .../src/tools/support/rg-locator.ts | 363 + .../agent-core/src/tools/support/workspace.ts | 17 + packages/agent-core/src/utils/abort.ts | 61 + .../agent-core/src/utils/completion-budget.ts | 153 + packages/agent-core/src/utils/environment.ts | 186 + packages/agent-core/src/utils/fs.ts | 187 + packages/agent-core/src/utils/hero-slug.ts | 267 + .../agent-core/src/utils/render-prompt.ts | 19 + packages/agent-core/src/utils/tokens.ts | 63 + packages/agent-core/src/utils/types.ts | 13 + packages/agent-core/src/utils/workdir-slug.ts | 11 + packages/agent-core/src/version.ts | 13 + .../test/agent/background-manager.test.ts | 628 ++ packages/agent-core/test/agent/basic.test.ts | 138 + .../agent-core/test/agent/compaction.test.ts | 1651 ++++ .../test/agent/config-state.test.ts | 122 + packages/agent-core/test/agent/config.test.ts | 210 + .../test/agent/config/thinking.test.ts | 69 + .../agent-core/test/agent/context.test.ts | 758 ++ .../file-system-record-persistence.test.ts | 119 + .../agent-core/test/agent/harness/agent.ts | 637 ++ .../agent-core/test/agent/harness/index.ts | 16 + .../test/agent/harness/scripted-generate.ts | 147 + .../test/agent/harness/snapshots.ts | 314 + .../test/agent/injection/manager.test.ts | 102 + .../test/agent/injection/plan-mode.test.ts | 150 + .../agent-core/test/agent/kosong-llm.test.ts | 133 + .../agent-core/test/agent/permission.test.ts | 1946 ++++ packages/agent-core/test/agent/plan.test.ts | 673 ++ .../agent-core/test/agent/question.test.ts | 57 + packages/agent-core/test/agent/resume.test.ts | 543 ++ .../test/agent/skill-tool-manager.test.ts | 216 + packages/agent-core/test/agent/tool.test.ts | 404 + packages/agent-core/test/agent/turn.test.ts | 1508 ++++ .../test/agent/turn/tool-dedup.test.ts | 290 + packages/agent-core/test/agent/usage.test.ts | 140 + .../agent-core/test/config/configs.test.ts | 538 ++ .../agent-core/test/fixtures/telemetry.ts | 32 + .../test/harness/model-alias-session.test.ts | 412 + .../test/harness/plan-mode-session.test.ts | 99 + .../test/harness/runtime-provider.test.ts | 610 ++ .../agent-core/test/harness/runtime.test.ts | 89 + .../test/harness/skill-session.test.ts | 556 ++ packages/agent-core/test/hooks/engine.test.ts | 352 + .../agent-core/test/hooks/integration.test.ts | 339 + packages/agent-core/test/hooks/runner.test.ts | 100 + .../agent-core/test/logging/formatter.test.ts | 231 + .../agent-core/test/logging/logger.test.ts | 459 + .../agent-core/test/logging/sinks.test.ts | 156 + .../agent-core/test/loop/abort.e2e.test.ts | 253 + .../test/loop/api-shape.e2e.test.ts | 589 ++ .../test/loop/error-paths.e2e.test.ts | 183 + .../agent-core/test/loop/events.e2e.test.ts | 176 + .../test/loop/fixtures/collecting-sink.ts | 82 + .../agent-core/test/loop/fixtures/fake-llm.ts | 207 + .../agent-core/test/loop/fixtures/helpers.ts | 141 + .../test/loop/fixtures/recording-context.ts | 99 + .../agent-core/test/loop/fixtures/tools.ts | 299 + .../agent-core/test/loop/hooks.e2e.test.ts | 492 + .../test/loop/streaming.e2e.test.ts | 200 + .../test/loop/tool-call.e2e.test.ts | 633 ++ .../test/loop/tool-scheduler.test.ts | 328 + .../test/loop/transcript.e2e.test.ts | 221 + .../test/loop/turn-lifecycle.e2e.test.ts | 227 + .../agent-core/test/mcp/auth-tool.test.ts | 143 + .../agent-core/test/mcp/client-http.test.ts | 271 + .../agent-core/test/mcp/client-stdio.test.ts | 248 + .../agent-core/test/mcp/config-loader.test.ts | 167 + .../test/mcp/connection-manager.test.ts | 898 ++ .../crash-after-connect-stdio-server.mjs | 53 + .../fixtures/hanging-list-stdio-server.mjs | 38 + .../test/mcp/fixtures/mock-stdio-server.mjs | 55 + .../test/mcp/fixtures/slow-stdio-server.mjs | 26 + .../stderr-then-exit-stdio-server.mjs | 7 + .../agent-core/test/mcp/oauth-store.test.ts | 155 + packages/agent-core/test/mcp/output.test.ts | 324 + .../test/mcp/tool-manager-mcp.test.ts | 610 ++ .../agent-core/test/mcp/tool-naming.test.ts | 64 + .../test/profile/agent-profile-loader.test.ts | 245 + .../agent-core/test/profile/context.test.ts | 68 + .../profile/default-agent-profiles.test.ts | 33 + .../test/prompt-placeholders.test.ts | 76 + .../agent-core/test/rpc/create-rpc.test.ts | 203 + .../test/session/git-context.test.ts | 178 + packages/agent-core/test/session/init.test.ts | 229 + .../test/session/lifecycle-hooks.test.ts | 251 + .../test/session/subagent-host.test.ts | 1074 +++ .../test/skill/parser-frontmatter.test.ts | 27 + packages/agent-core/test/skill/parser.test.ts | 270 + .../agent-core/test/skill/registry.test.ts | 128 + .../agent-core/test/skill/scanner.test.ts | 1177 +++ packages/agent-core/test/tools/agent.test.ts | 718 ++ .../agent-core/test/tools/ask-user.test.ts | 263 + .../tools/background/agent-timeout.test.ts | 137 + .../tools/background/heartbeat-stale.test.ts | 88 + .../test/tools/background/ids.test.ts | 75 + .../test/tools/background/lifecycle.test.ts | 257 + .../test/tools/background/manager.test.ts | 758 ++ .../tools/background/output-access.test.ts | 141 + .../test/tools/background/persist.test.ts | 276 + .../test/tools/background/reconcile.test.ts | 317 + .../background/state-transitions.test.ts | 190 + .../test/tools/background/task-tools.test.ts | 1222 +++ .../agent-core/test/tools/bash-env.test.ts | 68 + .../test/tools/bash-windows-kill.test.ts | 10 + packages/agent-core/test/tools/bash.test.ts | 908 ++ .../test/tools/builtin-current.test.ts | 304 + .../test/tools/default-permissions.test.ts | 25 + packages/agent-core/test/tools/edit.test.ts | 413 + .../test/tools/enter-plan-mode.test.ts | 164 + .../test/tools/exit-plan-mode-options.test.ts | 279 + .../test/tools/exit-plan-mode.test.ts | 216 + .../agent-core/test/tools/fetch-url.test.ts | 288 + .../agent-core/test/tools/file-type.test.ts | 481 + .../test/tools/fixtures/execute-tool.ts | 27 + .../test/tools/fixtures/fake-kaos.ts | 62 + packages/agent-core/test/tools/glob.test.ts | 588 ++ packages/agent-core/test/tools/grep.test.ts | 1890 ++++ .../test/tools/input-schema-io.test.ts | 90 + .../test/tools/list-directory.test.ts | 246 + .../agent-core/test/tools/path-guard.test.ts | 411 + .../test/tools/plan-mode-hard-block.test.ts | 177 + .../enter-plan-mode-telemetry.test.ts | 55 + .../planning/exit-plan-mode-telemetry.test.ts | 195 + .../test/tools/policies/sensitive.test.ts | 54 + .../tools/providers/local-fetch-url.test.ts | 58 + .../providers/moonshot-fetch-url.test.ts | 94 + .../agent-core/test/tools/read-file.test.ts | 71 + .../test/tools/read-media-desc.test.ts | 46 + .../agent-core/test/tools/read-media.test.ts | 564 ++ packages/agent-core/test/tools/read.test.ts | 826 ++ .../test/tools/result-builder.test.ts | 162 + .../agent-core/test/tools/rg-locator.test.ts | 478 + .../test/tools/shell-cancel.test.ts | 57 + .../test/tools/shell-quoting.test.ts | 135 + .../test/tools/skill-tool-dispatch.test.ts | 108 + .../agent-core/test/tools/skill-tool.test.ts | 247 + .../agent-core/test/tools/todo-list.test.ts | 169 + .../agent-core/test/tools/web-search.test.ts | 289 + packages/agent-core/test/tools/write.test.ts | 328 + .../test/utils/completion-budget.test.ts | 339 + .../agent-core/test/utils/environment.test.ts | 234 + .../agent-core/test/utils/hero-slug.test.ts | 25 + packages/agent-core/tsconfig.json | 5 + packages/agent-core/tsdown.config.ts | 16 + packages/agent-core/vitest.config.ts | 11 + packages/kaos/README.md | 11 + packages/kaos/package.json | 56 + packages/kaos/src/current.ts | 50 + packages/kaos/src/errors.ts | 29 + packages/kaos/src/index.ts | 96 + packages/kaos/src/internal.ts | 279 + packages/kaos/src/kaos.ts | 80 + packages/kaos/src/local.ts | 560 ++ packages/kaos/src/path.ts | 433 + packages/kaos/src/process.ts | 25 + packages/kaos/src/ssh.ts | 902 ++ packages/kaos/src/types.ts | 15 + packages/kaos/test/cmd.test.ts | 116 + packages/kaos/test/current.test.ts | 396 + .../kaos/test/e2e/async-isolation.test.ts | 202 + .../test/e2e/concurrent-operations.test.ts | 234 + packages/kaos/test/e2e/cwd-isolation.test.ts | 195 + .../kaos/test/e2e/exec-edge-cases.test.ts | 225 + .../test/e2e/glob-boundaries-parity.test.ts | 66 + .../kaos/test/e2e/path-cross-platform.test.ts | 297 + .../kaos/test/e2e/process-lifecycle.test.ts | 342 + packages/kaos/test/e2e/ssh-mock.test.ts | 139 + .../kaos/test/e2e/ssh-resolve-path.test.ts | 450 + .../kaos/test/e2e/symlink-stat-parity.test.ts | 67 + packages/kaos/test/internal.test.ts | 257 + packages/kaos/test/local.test.ts | 852 ++ packages/kaos/test/path.test.ts | 481 + packages/kaos/test/shell.test.ts | 246 + packages/kaos/test/ssh-create.test.ts | 375 + packages/kaos/test/ssh-process.test.ts | 223 + packages/kaos/test/ssh.test.ts | 1473 +++ packages/kaos/tsconfig.json | 4 + packages/kaos/tsdown.config.ts | 21 + packages/kaos/vitest.config.ts | 8 + packages/kosong/README.md | 11 + packages/kosong/package.json | 59 + packages/kosong/src/capability.ts | 55 + packages/kosong/src/errors.ts | 92 + packages/kosong/src/generate.ts | 284 + packages/kosong/src/index.ts | 62 + packages/kosong/src/message.ts | 226 + packages/kosong/src/provider.ts | 170 + packages/kosong/src/providers/anthropic.ts | 1075 +++ .../src/providers/capability-registry.ts | 203 + .../src/providers/chat-completions-stream.ts | 110 + packages/kosong/src/providers/google-genai.ts | 901 ++ packages/kosong/src/providers/index.ts | 35 + packages/kosong/src/providers/kimi-files.ts | 191 + packages/kosong/src/providers/kimi-schema.ts | 424 + packages/kosong/src/providers/kimi.ts | 582 ++ .../kosong/src/providers/openai-common.ts | 293 + .../kosong/src/providers/openai-legacy.ts | 480 + .../kosong/src/providers/openai-responses.ts | 979 ++ packages/kosong/src/providers/request-auth.ts | 69 + packages/kosong/src/tool.ts | 15 + packages/kosong/src/usage.ts | 54 + packages/kosong/test/anthropic-errors.test.ts | 356 + packages/kosong/test/anthropic.test.ts | 2046 +++++ .../kosong/test/capability-providers.test.ts | 184 + packages/kosong/test/capability.test.ts | 63 + .../kosong/test/e2e/abort-cleanup.test.ts | 265 + packages/kosong/test/e2e/abort-signal.test.ts | 239 + .../kosong/test/e2e/anthropic-adapter.test.ts | 216 + .../e2e/cross-provider-consistency.test.ts | 173 + .../kosong/test/e2e/error-recovery.test.ts | 416 + .../kosong/test/e2e/extreme-streams.test.ts | 182 + .../test/e2e/fake-provider-harness.test.ts | 94 + .../kosong/test/e2e/fake-provider-harness.ts | 265 + .../e2e/fixtures/python-jsonl-compat.jsonl | 6 + .../test/e2e/google-genai-adapter.test.ts | 201 + .../e2e/google-genai-error-mapping.test.ts | 88 + .../kosong/test/e2e/kimi-adapter-e2e.test.ts | 235 + .../test/e2e/multi-step-agent-loop.test.ts | 358 + .../e2e/openai-legacy-adapter-e2e.test.ts | 229 + .../e2e/openai-responses-adapter-e2e.test.ts | 212 + .../test/e2e/parallel-tool-calls.test.ts | 450 + .../test/e2e/provider-error-handling.test.ts | 333 + .../test/e2e/provider-switching.test.ts | 377 + .../test/e2e/streaming-fidelity.test.ts | 419 + .../test/e2e/streaming-providers.test.ts | 387 + .../kosong/test/e2e/toolchain-bridges.test.ts | 224 + .../kosong/test/e2e/toolset-advanced.test.ts | 419 + packages/kosong/test/echo-provider.test.ts | 353 + packages/kosong/test/errors.test.ts | 152 + packages/kosong/test/finish-reason.test.ts | 946 ++ .../kosong/test/fixtures/args-validator.ts | 39 + .../kosong/test/fixtures/echo-provider.ts | 440 + .../kosong/test/fixtures/mock-provider.ts | 121 + .../kosong/test/fixtures/simple-toolset.ts | 189 + packages/kosong/test/fixtures/step.ts | 154 + packages/kosong/test/fixtures/typed-tool.ts | 67 + packages/kosong/test/generate.test.ts | 907 ++ packages/kosong/test/google-genai.test.ts | 1688 ++++ packages/kosong/test/kimi-files.test.ts | 200 + packages/kosong/test/kimi.test.ts | 1469 +++ packages/kosong/test/message.test.ts | 424 + .../kosong/test/openai-common-errors.test.ts | 319 + packages/kosong/test/openai-legacy.test.ts | 1218 +++ packages/kosong/test/openai-responses.test.ts | 1709 ++++ packages/kosong/test/provider.test.ts | 103 + .../kosong/test/providers/kimi-schema.test.ts | 721 ++ packages/kosong/test/regression.test.ts | 106 + .../test/scripted-echo-provider.test.ts | 183 + packages/kosong/test/simple-toolset.test.ts | 278 + packages/kosong/test/step.test.ts | 516 ++ packages/kosong/test/stress.test.ts | 364 + .../test/tool-message-conversion.test.ts | 82 + packages/kosong/test/type-safety-negative.ts | 79 + packages/kosong/test/type-safety.test.ts | 164 + packages/kosong/test/typed-tool.test.ts | 154 + packages/kosong/test/usage.test.ts | 78 + packages/kosong/tsconfig.json | 6 + packages/kosong/tsconfig.type-negative.json | 4 + packages/kosong/tsdown.config.ts | 17 + packages/kosong/vitest.config.ts | 8 + packages/migration-legacy/package.json | 37 + packages/migration-legacy/src/atomic-write.ts | 19 + packages/migration-legacy/src/detect.ts | 132 + packages/migration-legacy/src/index.ts | 12 + .../migration-legacy/src/kimi-cli-schema.ts | 45 + packages/migration-legacy/src/marker.ts | 82 + .../src/migration-errors-log.ts | 98 + packages/migration-legacy/src/paths.ts | 31 + packages/migration-legacy/src/prompt.ts | 48 + packages/migration-legacy/src/report.ts | 17 + .../migration-legacy/src/run-migration.ts | 161 + .../migration-legacy/src/session-index.ts | 62 + .../migration-legacy/src/sessions/classify.ts | 44 + .../src/sessions/close-tool-calls.ts | 47 + .../src/sessions/content-part.ts | 81 + .../migration-legacy/src/sessions/index.ts | 268 + .../src/sessions/migrate-one.ts | 215 + .../src/sessions/state-writer.ts | 59 + .../src/sessions/translator.ts | 171 + .../src/sessions/wire-writer.ts | 26 + .../src/sessions/workdir-bucket.ts | 51 + packages/migration-legacy/src/steps/config.ts | 435 + packages/migration-legacy/src/steps/mcp.ts | 102 + .../src/steps/user-history.ts | 62 + packages/migration-legacy/src/stub-detect.ts | 68 + packages/migration-legacy/src/types.ts | 133 + packages/migration-legacy/test/.gitkeep | 0 .../test/atomic-write.test.ts | 24 + packages/migration-legacy/test/detect.test.ts | 42 + .../test/fixtures/archived/context.jsonl | 10 + .../test/fixtures/archived/state.json | 19 + .../test/fixtures/archived/wire.jsonl | 10 + .../fixtures/broken-state-json/context.jsonl | 5 + .../fixtures/broken-state-json/state.json | 1 + .../fixtures/broken-state-json/wire.jsonl | 1 + .../test/fixtures/large-100msgs/context.jsonl | 130 + .../test/fixtures/large-100msgs/state.json | 12 + .../test/fixtures/large-100msgs/wire.jsonl | 130 + .../legacy-protocol-1.3/context.jsonl | 10 + .../fixtures/legacy-protocol-1.3/state.json | 20 + .../fixtures/legacy-protocol-1.3/wire.jsonl | 10 + .../fixtures/multi-workdir/.kimi/kimi.json | 7 + .../uuid-a1/context.jsonl | 3 + .../uuid-a1/state.json | 10 + .../uuid-a2/context.jsonl | 3 + .../uuid-a2/state.json | 10 + .../uuid-b1/test | 0 .../uuid-c1/context.jsonl | 3 + .../uuid-c1/state.json | 10 + .../recent-protocol-1.10/context.jsonl | 10 + .../fixtures/recent-protocol-1.10/state.json | 22 + .../fixtures/recent-protocol-1.10/wire.jsonl | 10 + .../fixtures/tiny-hello-world/context.jsonl | 5 + .../test/fixtures/tiny-hello-world/state.json | 10 + .../test/fixtures/tiny-hello-world/wire.jsonl | 1 + .../test/fixtures/with-image/context.jsonl | 10 + .../test/fixtures/with-image/wire.jsonl | 10 + .../with-subagent-collapsed/context.jsonl | 10 + .../with-subagent-collapsed/state.json | 18 + .../with-subagent-collapsed/wire.jsonl | 10 + .../test/fixtures/with-thinking/context.jsonl | 10 + .../test/fixtures/with-thinking/state.json | 19 + .../test/fixtures/with-thinking/wire.jsonl | 10 + .../fixtures/with-tool-calls/context.jsonl | 10 + .../test/fixtures/with-tool-calls/state.json | 14 + .../test/fixtures/with-tool-calls/wire.jsonl | 10 + .../migration-legacy/test/integration.test.ts | 122 + .../test/kimi-cli-schema.test.ts | 48 + packages/migration-legacy/test/marker.test.ts | 79 + .../test/migration-errors-log.test.ts | 118 + packages/migration-legacy/test/paths.test.ts | 36 + .../migration-legacy/test/prompt-modules.d.ts | 14 + packages/migration-legacy/test/prompt.test.ts | 39 + packages/migration-legacy/test/report.test.ts | 68 + .../test/resume.integration.test.ts | 168 + .../test/session-index.test.ts | 95 + .../fixtures.snapshot.test.ts.snap | 355 + .../test/sessions/classify.test.ts | 100 + .../test/sessions/close-tool-calls.test.ts | 65 + .../test/sessions/content-part.test.ts | 60 + .../test/sessions/fixtures.snapshot.test.ts | 64 + .../test/sessions/migrate-one.test.ts | 225 + .../test/sessions/sessions-step.test.ts | 178 + .../test/sessions/state-writer.test.ts | 75 + .../test/sessions/translator.test.ts | 173 + .../test/sessions/wire-writer.test.ts | 50 + .../test/sessions/workdir-bucket.test.ts | 66 + .../test/steps/config.test.ts | 447 + .../migration-legacy/test/steps/mcp.test.ts | 91 + .../test/steps/user-history.test.ts | 52 + .../migration-legacy/test/stub-detect.test.ts | 92 + packages/migration-legacy/tsconfig.json | 4 + packages/migration-legacy/tsdown.config.ts | 9 + packages/migration-legacy/vitest.config.ts | 14 + packages/node-sdk/README.md | 11 + packages/node-sdk/api-extractor.json | 52 + .../examples/kimi-harness-auth-smoke.ts | 119 + .../examples/kimi-harness-cancel-smoke.ts | 36 + .../examples/kimi-harness-config-smoke.ts | 93 + .../examples/kimi-harness-export-smoke.ts | 62 + .../examples/kimi-harness-list-smoke.ts | 77 + .../examples/kimi-harness-log-marker.ts | 166 + .../examples/kimi-harness-logging-smoke.ts | 225 + .../examples/kimi-harness-prompt-demo.ts | 133 + .../examples/kimi-harness-rename-smoke.ts | 80 + .../examples/kimi-harness-set-model-smoke.ts | 41 + .../kimi-harness-set-permission-smoke.ts | 36 + .../kimi-harness-set-thinking-smoke.ts | 35 + .../node-sdk/examples/kimi-harness-smoke.ts | 71 + .../examples/kimi-harness-steer-smoke.ts | 33 + .../examples/runtime-smoke-helpers.ts | 242 + .../examples/t8-race-create-single.ts | 20 + packages/node-sdk/examples/t8-race-create.ts | 23 + packages/node-sdk/examples/t8-race-direct.ts | 60 + packages/node-sdk/package.json | 71 + packages/node-sdk/scripts/build-dts.mjs | 211 + packages/node-sdk/src/auth.ts | 133 + packages/node-sdk/src/events.ts | 108 + packages/node-sdk/src/index.ts | 36 + packages/node-sdk/src/kimi-harness.ts | 240 + packages/node-sdk/src/rpc.ts | 556 ++ packages/node-sdk/src/session.ts | 411 + packages/node-sdk/src/types.ts | 164 + packages/node-sdk/test/auth-facade.test.ts | 309 + packages/node-sdk/test/config.test.ts | 307 + .../test/create-session-transport.test.ts | 510 ++ packages/node-sdk/test/export-session.test.ts | 310 + packages/node-sdk/test/list-sessions.test.ts | 295 + packages/node-sdk/test/local-logging.test.ts | 387 + packages/node-sdk/test/rename-session.test.ts | 228 + .../test/runtime-provider-identity.test.ts | 138 + .../test/runtime-provider-oauth.test.ts | 148 + .../test/session-approval-handler.test.ts | 223 + .../test/session-background-tasks.test.ts | 116 + packages/node-sdk/test/session-cancel.test.ts | 171 + .../node-sdk/test/session-event-types.test.ts | 83 + .../session-plan-compact-usage-resume.test.ts | 340 + .../test/session-prompt-events.test.ts | 370 + .../test/session-prompt-input.test.ts | 27 + .../test/session-question-handler.test.ts | 148 + .../node-sdk/test/session-runtime-helpers.ts | 159 + .../node-sdk/test/session-set-model.test.ts | 177 + .../test/session-set-permission.test.ts | 80 + .../test/session-set-thinking.test.ts | 75 + packages/node-sdk/test/session-skills.test.ts | 300 + packages/node-sdk/test/session-steer.test.ts | 114 + packages/node-sdk/test/telemetry.ts | 20 + packages/node-sdk/test/test-identity.ts | 6 + packages/node-sdk/tsconfig.api-extractor.json | 8 + packages/node-sdk/tsconfig.dts.json | 21 + packages/node-sdk/tsconfig.json | 7 + packages/node-sdk/tsdown.config.ts | 32 + packages/node-sdk/vitest.config.ts | 21 + packages/oauth/README.md | 11 + packages/oauth/examples/kimi-oauth-smoke.ts | 128 + packages/oauth/package.json | 51 + packages/oauth/src/constants.ts | 10 + packages/oauth/src/errors.ts | 49 + packages/oauth/src/identity.ts | 130 + packages/oauth/src/index.ts | 111 + packages/oauth/src/managed-feedback.ts | 73 + packages/oauth/src/managed-kimi-code.ts | 395 + packages/oauth/src/managed-usage.ts | 235 + packages/oauth/src/oauth-manager.ts | 478 + packages/oauth/src/oauth.ts | 276 + packages/oauth/src/open-platform.ts | 189 + packages/oauth/src/storage.ts | 138 + packages/oauth/src/token-state.ts | 45 + packages/oauth/src/toolkit.ts | 300 + packages/oauth/src/types.ts | 87 + packages/oauth/src/utils.ts | 3 + packages/oauth/test/helpers.ts | 129 + packages/oauth/test/identity.test.ts | 150 + packages/oauth/test/managed-feedback.test.ts | 152 + packages/oauth/test/managed-kimi-code.test.ts | 531 ++ packages/oauth/test/managed-usage.test.ts | 133 + .../test/oauth-manager-lock-failure.test.ts | 95 + .../test/oauth-manager-multi-process.test.ts | 268 + packages/oauth/test/oauth-manager.test.ts | 902 ++ packages/oauth/test/oauth.test.ts | 636 ++ packages/oauth/test/open-platform.test.ts | 279 + packages/oauth/test/refresh-threshold.test.ts | 30 + packages/oauth/test/storage.test.ts | 214 + packages/oauth/test/toolkit.test.ts | 307 + packages/oauth/tsconfig.json | 4 + packages/oauth/tsdown.config.ts | 9 + packages/oauth/vitest.config.ts | 8 + packages/telemetry/README.md | 11 + packages/telemetry/package.json | 46 + packages/telemetry/src/bootstrap.ts | 69 + packages/telemetry/src/client.ts | 275 + packages/telemetry/src/crash.ts | 64 + packages/telemetry/src/index.ts | 42 + packages/telemetry/src/remote.ts | 42 + packages/telemetry/src/sink.ts | 127 + packages/telemetry/src/transport.ts | 350 + packages/telemetry/src/types.ts | 31 + .../telemetry/test/normalize-remote.test.ts | 26 + packages/telemetry/test/telemetry.test.ts | 889 ++ packages/telemetry/tsconfig.json | 5 + packages/telemetry/tsdown.config.ts | 9 + packages/telemetry/vitest.config.ts | 8 + pnpm-lock.yaml | 7992 +++++++++++++++++ pnpm-workspace.yaml | 12 + tsconfig.json | 44 + vitest.config.ts | 13 + 1131 files changed, 206253 insertions(+) create mode 100644 .agents/skills/gen-changesets/SKILL.md create mode 100644 .agents/skills/gen-docs/SKILL.md create mode 100644 .agents/skills/sync-changelog/SKILL.md create mode 100644 .agents/skills/translate-docs/SKILL.md create mode 100644 .changeset/README.md create mode 100644 .changeset/config.json create mode 100644 .editorconfig create mode 100644 .github/ISSUE_TEMPLATE/1-bug-report.yml create mode 100644 .github/ISSUE_TEMPLATE/2-feature-request.yml create mode 100644 .github/actions/macos-keychain-cleanup/action.yml create mode 100644 .github/actions/macos-keychain-setup/action.yml create mode 100644 .github/actions/macos-notarize/action.yml create mode 100644 .github/pr-title-checker-config.json create mode 100644 .github/pull_request_template.md create mode 100644 .github/workflows/_native-build.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/docs-deploy.yml create mode 100644 .github/workflows/native-bundle.yml create mode 100644 .github/workflows/pr-title-checker.yml create mode 100644 .github/workflows/release.yml create mode 100644 .gitignore create mode 100644 .npmrc create mode 100644 .nvmrc create mode 100644 .oxfmtrc.json create mode 100644 .oxlintrc.json create mode 100644 AGENTS.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 README.zh-CN.md create mode 100644 SECURITY.md create mode 100644 apps/kimi-code/.gitignore create mode 100644 apps/kimi-code/AGENTS.md create mode 100644 apps/kimi-code/README.md create mode 100644 apps/kimi-code/package.json create mode 100644 apps/kimi-code/scripts/native/01-bundle.mjs create mode 100644 apps/kimi-code/scripts/native/02-sea-blob.mjs create mode 100644 apps/kimi-code/scripts/native/03-inject.mjs create mode 100644 apps/kimi-code/scripts/native/04-sign.mjs create mode 100644 apps/kimi-code/scripts/native/05-verify.mjs create mode 100644 apps/kimi-code/scripts/native/assets.mjs create mode 100644 apps/kimi-code/scripts/native/build.mjs create mode 100644 apps/kimi-code/scripts/native/check-bundle.mjs create mode 100644 apps/kimi-code/scripts/native/entitlements.plist create mode 100644 apps/kimi-code/scripts/native/exec.mjs create mode 100644 apps/kimi-code/scripts/native/manifest.mjs create mode 100644 apps/kimi-code/scripts/native/native-deps.mjs create mode 100644 apps/kimi-code/scripts/native/package.mjs create mode 100644 apps/kimi-code/scripts/native/paths.mjs create mode 100644 apps/kimi-code/scripts/native/produce-manifest.mjs create mode 100644 apps/kimi-code/scripts/native/resolve-release.mjs create mode 100644 apps/kimi-code/scripts/native/smoke.mjs create mode 100644 apps/kimi-code/scripts/postinstall.mjs create mode 100644 apps/kimi-code/scripts/postinstall/migrate.mjs create mode 100644 apps/kimi-code/scripts/postinstall/reach.mjs create mode 100644 apps/kimi-code/scripts/postinstall/ui.mjs create mode 100644 apps/kimi-code/scripts/smoke.mjs create mode 100644 apps/kimi-code/src/cli/build-info.ts create mode 100644 apps/kimi-code/src/cli/commands.ts create mode 100644 apps/kimi-code/src/cli/options.ts create mode 100644 apps/kimi-code/src/cli/run-prompt.ts create mode 100644 apps/kimi-code/src/cli/run-shell.ts create mode 100644 apps/kimi-code/src/cli/startup-error.ts create mode 100644 apps/kimi-code/src/cli/sub/export.ts create mode 100644 apps/kimi-code/src/cli/update/cache.ts create mode 100644 apps/kimi-code/src/cli/update/cdn.ts create mode 100644 apps/kimi-code/src/cli/update/preflight.ts create mode 100644 apps/kimi-code/src/cli/update/prompt.ts create mode 100644 apps/kimi-code/src/cli/update/refresh.ts create mode 100644 apps/kimi-code/src/cli/update/select.ts create mode 100644 apps/kimi-code/src/cli/update/source.ts create mode 100644 apps/kimi-code/src/cli/update/types.ts create mode 100644 apps/kimi-code/src/cli/version.ts create mode 100644 apps/kimi-code/src/constant/app.ts create mode 100644 apps/kimi-code/src/constant/index.ts create mode 100644 apps/kimi-code/src/constant/startup-error.ts create mode 100644 apps/kimi-code/src/constant/terminal.ts create mode 100644 apps/kimi-code/src/constant/update.ts create mode 100644 apps/kimi-code/src/main.ts create mode 100644 apps/kimi-code/src/migration/badge.ts create mode 100644 apps/kimi-code/src/migration/command.ts create mode 100644 apps/kimi-code/src/migration/detect-pending.ts create mode 100644 apps/kimi-code/src/migration/index.ts create mode 100644 apps/kimi-code/src/migration/migration-screen.ts create mode 100644 apps/kimi-code/src/native/module-hook.ts create mode 100644 apps/kimi-code/src/native/native-assets.ts create mode 100644 apps/kimi-code/src/native/native-require.ts create mode 100644 apps/kimi-code/src/native/smoke.ts create mode 100644 apps/kimi-code/src/tui/actions/replay-ops.ts create mode 100644 apps/kimi-code/src/tui/commands/index.ts create mode 100644 apps/kimi-code/src/tui/commands/parse.ts create mode 100644 apps/kimi-code/src/tui/commands/registry.ts create mode 100644 apps/kimi-code/src/tui/commands/resolve.ts create mode 100644 apps/kimi-code/src/tui/commands/skills.ts create mode 100644 apps/kimi-code/src/tui/commands/types.ts create mode 100644 apps/kimi-code/src/tui/components/chrome/device-code-box.ts create mode 100644 apps/kimi-code/src/tui/components/chrome/footer.ts create mode 100644 apps/kimi-code/src/tui/components/chrome/gutter-container.ts create mode 100644 apps/kimi-code/src/tui/components/chrome/moon-loader.ts create mode 100644 apps/kimi-code/src/tui/components/chrome/todo-panel.ts create mode 100644 apps/kimi-code/src/tui/components/chrome/welcome.ts create mode 100644 apps/kimi-code/src/tui/components/dialogs/api-key-input-dialog.ts create mode 100644 apps/kimi-code/src/tui/components/dialogs/approval-panel.ts create mode 100644 apps/kimi-code/src/tui/components/dialogs/choice-picker.ts create mode 100644 apps/kimi-code/src/tui/components/dialogs/compaction.ts create mode 100644 apps/kimi-code/src/tui/components/dialogs/editor-selector.ts create mode 100644 apps/kimi-code/src/tui/components/dialogs/feedback-input-dialog.ts create mode 100644 apps/kimi-code/src/tui/components/dialogs/help-panel.ts create mode 100644 apps/kimi-code/src/tui/components/dialogs/model-selector.ts create mode 100644 apps/kimi-code/src/tui/components/dialogs/permission-selector.ts create mode 100644 apps/kimi-code/src/tui/components/dialogs/platform-selector.ts create mode 100644 apps/kimi-code/src/tui/components/dialogs/question-dialog.ts create mode 100644 apps/kimi-code/src/tui/components/dialogs/session-picker.ts create mode 100644 apps/kimi-code/src/tui/components/dialogs/settings-selector.ts create mode 100644 apps/kimi-code/src/tui/components/dialogs/task-output-viewer.ts create mode 100644 apps/kimi-code/src/tui/components/dialogs/tasks-browser.ts create mode 100644 apps/kimi-code/src/tui/components/dialogs/theme-selector.ts create mode 100644 apps/kimi-code/src/tui/components/editor/custom-editor.ts create mode 100644 apps/kimi-code/src/tui/components/editor/file-mention-provider.ts create mode 100644 apps/kimi-code/src/tui/components/index.ts create mode 100644 apps/kimi-code/src/tui/components/media/code-highlight.ts create mode 100644 apps/kimi-code/src/tui/components/media/diff-preview.ts create mode 100644 apps/kimi-code/src/tui/components/media/image-thumbnail.ts create mode 100644 apps/kimi-code/src/tui/components/messages/agent-group.ts create mode 100644 apps/kimi-code/src/tui/components/messages/assistant-message.ts create mode 100644 apps/kimi-code/src/tui/components/messages/background-agent-status.ts create mode 100644 apps/kimi-code/src/tui/components/messages/mcp-status-panel.ts create mode 100644 apps/kimi-code/src/tui/components/messages/plan-box.ts create mode 100644 apps/kimi-code/src/tui/components/messages/read-group.ts create mode 100644 apps/kimi-code/src/tui/components/messages/shell-execution.ts create mode 100644 apps/kimi-code/src/tui/components/messages/skill-activation.ts create mode 100644 apps/kimi-code/src/tui/components/messages/status-message.ts create mode 100644 apps/kimi-code/src/tui/components/messages/status-panel.ts create mode 100644 apps/kimi-code/src/tui/components/messages/thinking.ts create mode 100644 apps/kimi-code/src/tui/components/messages/tool-call.ts create mode 100644 apps/kimi-code/src/tui/components/messages/tool-renderers/chip.ts create mode 100644 apps/kimi-code/src/tui/components/messages/tool-renderers/media.ts create mode 100644 apps/kimi-code/src/tui/components/messages/tool-renderers/registry.ts create mode 100644 apps/kimi-code/src/tui/components/messages/tool-renderers/summary.ts create mode 100644 apps/kimi-code/src/tui/components/messages/tool-renderers/truncated.ts create mode 100644 apps/kimi-code/src/tui/components/messages/tool-renderers/types.ts create mode 100644 apps/kimi-code/src/tui/components/messages/usage-panel.ts create mode 100644 apps/kimi-code/src/tui/components/messages/user-message.ts create mode 100644 apps/kimi-code/src/tui/components/panes/activity-pane.ts create mode 100644 apps/kimi-code/src/tui/components/panes/queue-pane.ts create mode 100644 apps/kimi-code/src/tui/config.ts create mode 100644 apps/kimi-code/src/tui/constant/feedback.ts create mode 100644 apps/kimi-code/src/tui/constant/kimi-tui.ts create mode 100644 apps/kimi-code/src/tui/constant/rendering.ts create mode 100644 apps/kimi-code/src/tui/constant/streaming.ts create mode 100644 apps/kimi-code/src/tui/constant/symbols.ts create mode 100644 apps/kimi-code/src/tui/constant/terminal.ts create mode 100644 apps/kimi-code/src/tui/index.ts create mode 100644 apps/kimi-code/src/tui/kimi-tui.ts create mode 100644 apps/kimi-code/src/tui/reverse-rpc/approval/adapter.ts create mode 100644 apps/kimi-code/src/tui/reverse-rpc/approval/controller.ts create mode 100644 apps/kimi-code/src/tui/reverse-rpc/approval/handler.ts create mode 100644 apps/kimi-code/src/tui/reverse-rpc/base-controller.ts create mode 100644 apps/kimi-code/src/tui/reverse-rpc/index.ts create mode 100644 apps/kimi-code/src/tui/reverse-rpc/modal-coordinator.ts create mode 100644 apps/kimi-code/src/tui/reverse-rpc/question/controller.ts create mode 100644 apps/kimi-code/src/tui/reverse-rpc/question/handler.ts create mode 100644 apps/kimi-code/src/tui/reverse-rpc/types.ts create mode 100644 apps/kimi-code/src/tui/theme/bundle.ts create mode 100644 apps/kimi-code/src/tui/theme/colors.ts create mode 100644 apps/kimi-code/src/tui/theme/detect.ts create mode 100644 apps/kimi-code/src/tui/theme/index.ts create mode 100644 apps/kimi-code/src/tui/theme/pi-tui-theme.ts create mode 100644 apps/kimi-code/src/tui/theme/styles.ts create mode 100644 apps/kimi-code/src/tui/theme/terminal-background.ts create mode 100644 apps/kimi-code/src/tui/types.ts create mode 100644 apps/kimi-code/src/tui/utils/background-agent-status.ts create mode 100644 apps/kimi-code/src/tui/utils/background-task-status.ts create mode 100644 apps/kimi-code/src/tui/utils/component-capabilities.ts create mode 100644 apps/kimi-code/src/tui/utils/errors.ts create mode 100644 apps/kimi-code/src/tui/utils/event-payload.ts create mode 100644 apps/kimi-code/src/tui/utils/image-attachment-store.ts create mode 100644 apps/kimi-code/src/tui/utils/image-placeholder.ts create mode 100644 apps/kimi-code/src/tui/utils/mcp-oauth.ts create mode 100644 apps/kimi-code/src/tui/utils/mcp-server-status.ts create mode 100644 apps/kimi-code/src/tui/utils/mcp-tool-name.ts create mode 100644 apps/kimi-code/src/tui/utils/media-url.ts create mode 100644 apps/kimi-code/src/tui/utils/open-url.ts create mode 100644 apps/kimi-code/src/tui/utils/printable-key.ts create mode 100644 apps/kimi-code/src/tui/utils/proctitle.ts create mode 100644 apps/kimi-code/src/tui/utils/terminal-focus.ts create mode 100644 apps/kimi-code/src/tui/utils/terminal-notification.ts create mode 100644 apps/kimi-code/src/tui/utils/terminal-state.ts create mode 100644 apps/kimi-code/src/tui/utils/terminal-theme.ts create mode 100644 apps/kimi-code/src/tui/utils/transcript-id.ts create mode 100644 apps/kimi-code/src/utils/clipboard/clipboard-image.ts create mode 100644 apps/kimi-code/src/utils/clipboard/clipboard-native.ts create mode 100644 apps/kimi-code/src/utils/git/git-ls-files.ts create mode 100644 apps/kimi-code/src/utils/git/git-status.ts create mode 100644 apps/kimi-code/src/utils/history/input-history.ts create mode 100644 apps/kimi-code/src/utils/image/image-mime.ts create mode 100644 apps/kimi-code/src/utils/paths.ts create mode 100644 apps/kimi-code/src/utils/persistence.ts create mode 100644 apps/kimi-code/src/utils/process/external-editor.ts create mode 100644 apps/kimi-code/src/utils/process/fd-detect.ts create mode 100644 apps/kimi-code/src/utils/process/proctitle.ts create mode 100644 apps/kimi-code/src/utils/process/stdin.ts create mode 100644 apps/kimi-code/src/utils/usage/usage-format.ts create mode 100644 apps/kimi-code/test/cli/export.test.ts create mode 100644 apps/kimi-code/test/cli/main.test.ts create mode 100644 apps/kimi-code/test/cli/options.test.ts create mode 100644 apps/kimi-code/test/cli/run-prompt.test.ts create mode 100644 apps/kimi-code/test/cli/run-shell.test.ts create mode 100644 apps/kimi-code/test/cli/session-flag-picker.test.ts create mode 100644 apps/kimi-code/test/cli/update/cache.test.ts create mode 100644 apps/kimi-code/test/cli/update/cdn.test.ts create mode 100644 apps/kimi-code/test/cli/update/preflight.test.ts create mode 100644 apps/kimi-code/test/cli/update/prompt.test.ts create mode 100644 apps/kimi-code/test/cli/update/refresh.test.ts create mode 100644 apps/kimi-code/test/cli/update/select.test.ts create mode 100644 apps/kimi-code/test/cli/update/source.test.ts create mode 100644 apps/kimi-code/test/cli/version.test.ts create mode 100644 apps/kimi-code/test/e2e/local-logging-export.e2e.test.ts create mode 100644 apps/kimi-code/test/e2e/real-llm-smoke.e2e.test.ts create mode 100644 apps/kimi-code/test/helpers/process.ts create mode 100644 apps/kimi-code/test/migration/badge.test.ts create mode 100644 apps/kimi-code/test/migration/command.test.ts create mode 100644 apps/kimi-code/test/migration/detect-pending.test.ts create mode 100644 apps/kimi-code/test/migration/migration-screen.test.ts create mode 100644 apps/kimi-code/test/native/cache-base.test.ts create mode 100644 apps/kimi-code/test/native/cache-gc.test.ts create mode 100644 apps/kimi-code/test/native/native-assets.test.ts create mode 100644 apps/kimi-code/test/scripts/native/exec.test.ts create mode 100644 apps/kimi-code/test/scripts/native/manifest.test.ts create mode 100644 apps/kimi-code/test/scripts/native/native-deps.test.ts create mode 100644 apps/kimi-code/test/scripts/native/paths.test.ts create mode 100644 apps/kimi-code/test/scripts/native/release-artifacts.test.ts create mode 100644 apps/kimi-code/test/scripts/native/sign-args.test.ts create mode 100644 apps/kimi-code/test/scripts/nix/update-pnpm-deps.test.ts create mode 100644 apps/kimi-code/test/tui/actions/replay-hydrate-agent-group.test.ts create mode 100644 apps/kimi-code/test/tui/activity-pane.test.ts create mode 100644 apps/kimi-code/test/tui/background-task-status.test.ts create mode 100644 apps/kimi-code/test/tui/commands/registry.test.ts create mode 100644 apps/kimi-code/test/tui/commands/resolve.test.ts create mode 100644 apps/kimi-code/test/tui/commands/skills.test.ts create mode 100644 apps/kimi-code/test/tui/components/chrome/device-code-box.test.ts create mode 100644 apps/kimi-code/test/tui/components/chrome/gutter-container.test.ts create mode 100644 apps/kimi-code/test/tui/components/dialogs/approval-panel.test.ts create mode 100644 apps/kimi-code/test/tui/components/dialogs/choice-picker.test.ts create mode 100644 apps/kimi-code/test/tui/components/dialogs/compaction.test.ts create mode 100644 apps/kimi-code/test/tui/components/dialogs/feedback-input-dialog.test.ts create mode 100644 apps/kimi-code/test/tui/components/dialogs/question-dialog.test.ts create mode 100644 apps/kimi-code/test/tui/components/dialogs/session-picker.test.ts create mode 100644 apps/kimi-code/test/tui/components/editor/custom-editor.test.ts create mode 100644 apps/kimi-code/test/tui/components/editor/file-mention-provider.test.ts create mode 100644 apps/kimi-code/test/tui/components/editor/normalize-caps-locked-ctrl.test.ts create mode 100644 apps/kimi-code/test/tui/components/editor/prompt-symbol.test.ts create mode 100644 apps/kimi-code/test/tui/components/editor/side-borders.test.ts create mode 100644 apps/kimi-code/test/tui/components/editor/slash-highlight.test.ts create mode 100644 apps/kimi-code/test/tui/components/media/code-highlight.test.ts create mode 100644 apps/kimi-code/test/tui/components/media/diff-preview.test.ts create mode 100644 apps/kimi-code/test/tui/components/messages/assistant-message.test.ts create mode 100644 apps/kimi-code/test/tui/components/messages/background-agent-status.test.ts create mode 100644 apps/kimi-code/test/tui/components/messages/notice.test.ts create mode 100644 apps/kimi-code/test/tui/components/messages/shell-execution.test.ts create mode 100644 apps/kimi-code/test/tui/components/messages/status-panel.test.ts create mode 100644 apps/kimi-code/test/tui/components/messages/thinking.test.ts create mode 100644 apps/kimi-code/test/tui/components/messages/tool-call.test.ts create mode 100644 apps/kimi-code/test/tui/components/messages/tool-renderers/chip.test.ts create mode 100644 apps/kimi-code/test/tui/components/messages/tool-renderers/media.test.ts create mode 100644 apps/kimi-code/test/tui/components/messages/tool-renderers/registry.test.ts create mode 100644 apps/kimi-code/test/tui/components/messages/usage-panel.test.ts create mode 100644 apps/kimi-code/test/tui/components/messages/user-message.test.ts create mode 100644 apps/kimi-code/test/tui/components/panels/footer-bg-agents.test.ts create mode 100644 apps/kimi-code/test/tui/components/panels/footer-context.test.ts create mode 100644 apps/kimi-code/test/tui/components/panels/help-panel.test.ts create mode 100644 apps/kimi-code/test/tui/components/panels/plan-box.test.ts create mode 100644 apps/kimi-code/test/tui/components/panels/todo-panel.test.ts create mode 100644 apps/kimi-code/test/tui/components/panes/activity-pane.test.ts create mode 100644 apps/kimi-code/test/tui/components/panes/queue-pane.test.ts create mode 100644 apps/kimi-code/test/tui/config.test.ts create mode 100644 apps/kimi-code/test/tui/create-tui-state.test.ts create mode 100644 apps/kimi-code/test/tui/input/image-attachment-store.test.ts create mode 100644 apps/kimi-code/test/tui/input/image-placeholder.test.ts create mode 100644 apps/kimi-code/test/tui/kimi-tui-message-flow.test.ts create mode 100644 apps/kimi-code/test/tui/kimi-tui-startup.test.ts create mode 100644 apps/kimi-code/test/tui/media-url.test.ts create mode 100644 apps/kimi-code/test/tui/printable-key-guard.test.ts create mode 100644 apps/kimi-code/test/tui/replay-ops.test.ts create mode 100644 apps/kimi-code/test/tui/reverse-rpc/approval-adapter.test.ts create mode 100644 apps/kimi-code/test/tui/reverse-rpc/approval.test.ts create mode 100644 apps/kimi-code/test/tui/reverse-rpc/base-controller.test.ts create mode 100644 apps/kimi-code/test/tui/reverse-rpc/index.test.ts create mode 100644 apps/kimi-code/test/tui/reverse-rpc/question.test.ts create mode 100644 apps/kimi-code/test/tui/task-output-viewer.test.ts create mode 100644 apps/kimi-code/test/tui/tasks-browser.test.ts create mode 100644 apps/kimi-code/test/tui/terminal-focus.test.ts create mode 100644 apps/kimi-code/test/tui/terminal-notification.test.ts create mode 100644 apps/kimi-code/test/tui/terminal-theme.test.ts create mode 100644 apps/kimi-code/test/tui/utils/mcp-oauth.test.ts create mode 100644 apps/kimi-code/test/utils/clipboard/clipboard-image.test.ts create mode 100644 apps/kimi-code/test/utils/git/git-ls-files.test.ts create mode 100644 apps/kimi-code/test/utils/git/git-status.test.ts create mode 100644 apps/kimi-code/test/utils/history/input-history.test.ts create mode 100644 apps/kimi-code/test/utils/image/image-mime.test.ts create mode 100644 apps/kimi-code/test/utils/paths.test.ts create mode 100644 apps/kimi-code/test/utils/persistence.test.ts create mode 100644 apps/kimi-code/test/utils/process/external-editor.test.ts create mode 100644 apps/kimi-code/test/utils/process/stdin.test.ts create mode 100644 apps/kimi-code/test/utils/usage/usage-format.test.ts create mode 100644 apps/kimi-code/tsconfig.json create mode 100644 apps/kimi-code/tsdown.config.ts create mode 100644 apps/kimi-code/tsdown.native.config.ts create mode 100644 apps/kimi-code/vitest.config.ts create mode 100644 apps/vis/package.json create mode 100644 apps/vis/scripts/copy-web-dist.mjs create mode 100644 apps/vis/server/package.json create mode 100644 apps/vis/server/src/app.ts create mode 100644 apps/vis/server/src/config.ts create mode 100644 apps/vis/server/src/index.ts create mode 100644 apps/vis/server/src/lib/context-builder.ts create mode 100644 apps/vis/server/src/lib/session-delete.ts create mode 100644 apps/vis/server/src/lib/session-lister.ts create mode 100644 apps/vis/server/src/lib/session-title.ts create mode 100644 apps/vis/server/src/lib/subagent-loader.ts create mode 100644 apps/vis/server/src/lib/types.ts create mode 100644 apps/vis/server/src/lib/wire-loader.ts create mode 100644 apps/vis/server/src/lib/wire-replay.ts create mode 100644 apps/vis/server/src/routes/context.ts create mode 100644 apps/vis/server/src/routes/session-detail.ts create mode 100644 apps/vis/server/src/routes/sessions.ts create mode 100644 apps/vis/server/src/routes/subagents.ts create mode 100644 apps/vis/server/src/routes/tool-results.ts create mode 100644 apps/vis/server/src/routes/wire.ts create mode 100644 apps/vis/server/src/startup-banner.ts create mode 100644 apps/vis/server/test/_fixture.ts create mode 100644 apps/vis/server/test/app.test.ts create mode 100644 apps/vis/server/test/context-builder.test.ts create mode 100644 apps/vis/server/test/session-lister.test.ts create mode 100644 apps/vis/server/test/subagent-loader.test.ts create mode 100644 apps/vis/server/test/wire-loader.test.ts create mode 100644 apps/vis/server/tsconfig.json create mode 100644 apps/vis/server/tsdown.config.ts create mode 100644 apps/vis/server/vitest.config.ts create mode 100644 apps/vis/web/index.html create mode 100644 apps/vis/web/package.json create mode 100644 apps/vis/web/src/App.tsx create mode 100644 apps/vis/web/src/api.ts create mode 100644 apps/vis/web/src/components/context/CompactionRibbon.tsx create mode 100644 apps/vis/web/src/components/context/ContextTab.tsx create mode 100644 apps/vis/web/src/components/context/EphemeralBubble.tsx create mode 100644 apps/vis/web/src/components/context/MessageBubble.tsx create mode 100644 apps/vis/web/src/components/context/PersistedOutputLink.tsx create mode 100644 apps/vis/web/src/components/files/FilesTab.tsx create mode 100644 apps/vis/web/src/components/layout/AppShell.tsx create mode 100644 apps/vis/web/src/components/layout/TabBar.tsx create mode 100644 apps/vis/web/src/components/sessions/SessionCard.tsx create mode 100644 apps/vis/web/src/components/sessions/SessionFilter.tsx create mode 100644 apps/vis/web/src/components/sessions/SessionRail.tsx create mode 100644 apps/vis/web/src/components/shared/CopyButton.tsx create mode 100644 apps/vis/web/src/components/shared/Drawer.tsx create mode 100644 apps/vis/web/src/components/shared/JsonViewer.tsx create mode 100644 apps/vis/web/src/components/shared/Pill.tsx create mode 100644 apps/vis/web/src/components/shared/SizePreview.tsx create mode 100644 apps/vis/web/src/components/state/StateTab.tsx create mode 100644 apps/vis/web/src/components/subagents/SubagentNode.tsx create mode 100644 apps/vis/web/src/components/subagents/SubagentTree.tsx create mode 100644 apps/vis/web/src/components/subagents/SubagentsTab.tsx create mode 100644 apps/vis/web/src/components/wire/IssuesDrawer.tsx create mode 100644 apps/vis/web/src/components/wire/TypeBadge.tsx create mode 100644 apps/vis/web/src/components/wire/WireHeadline.tsx create mode 100644 apps/vis/web/src/components/wire/WireRow.tsx create mode 100644 apps/vis/web/src/components/wire/WireRowDetail.tsx create mode 100644 apps/vis/web/src/components/wire/WireTab.tsx create mode 100644 apps/vis/web/src/components/wire/typeMeta.ts create mode 100644 apps/vis/web/src/hooks/useContext.ts create mode 100644 apps/vis/web/src/hooks/useSession.ts create mode 100644 apps/vis/web/src/hooks/useSubagents.ts create mode 100644 apps/vis/web/src/hooks/useTheme.ts create mode 100644 apps/vis/web/src/hooks/useWire.ts create mode 100644 apps/vis/web/src/lib/focus-context.tsx create mode 100644 apps/vis/web/src/lib/issues.ts create mode 100644 apps/vis/web/src/lib/query-parser.ts create mode 100644 apps/vis/web/src/main.tsx create mode 100644 apps/vis/web/src/pages/SessionDetailPage.tsx create mode 100644 apps/vis/web/src/pages/SessionListPage.tsx create mode 100644 apps/vis/web/src/pages/SubagentDetailPage.tsx create mode 100644 apps/vis/web/src/theme.css create mode 100644 apps/vis/web/src/types.ts create mode 100644 apps/vis/web/src/util/time.ts create mode 100644 apps/vis/web/test/api.test.ts create mode 100644 apps/vis/web/tsconfig.json create mode 100644 apps/vis/web/vite.config.ts create mode 100644 build/nix/update-pnpm-deps.sh create mode 100644 build/raw-text-loader.mjs create mode 100644 build/raw-text-plugin.mjs create mode 100644 build/register-raw-text-loader.mjs create mode 100644 docs/.gitignore create mode 100644 docs/.npmrc create mode 100644 docs/.vitepress/config.ts create mode 100644 docs/.vitepress/theme/Kimi.png create mode 100644 docs/.vitepress/theme/components/HomeFeatures.vue create mode 100644 docs/.vitepress/theme/components/HomeHero.vue create mode 100644 docs/.vitepress/theme/components/HomeLayout.vue create mode 100644 docs/.vitepress/theme/components/HomeQuickStart.vue create mode 100644 docs/.vitepress/theme/components/KimiLogo.vue create mode 100644 docs/.vitepress/theme/index.ts create mode 100644 docs/.vitepress/theme/styles/base.css create mode 100644 docs/.vitepress/theme/styles/home.css create mode 100644 docs/.vitepress/theme/styles/vars.css create mode 100644 docs/AGENTS.md create mode 100644 docs/en/configuration/config-files.md create mode 100644 docs/en/configuration/data-locations.md create mode 100644 docs/en/configuration/env-vars.md create mode 100644 docs/en/configuration/overrides.md create mode 100644 docs/en/configuration/providers.md create mode 100644 docs/en/customization/agents.md create mode 100644 docs/en/customization/hooks.md create mode 100644 docs/en/customization/mcp.md create mode 100644 docs/en/customization/skills.md create mode 100644 docs/en/faq.md create mode 100644 docs/en/guides/getting-started.md create mode 100644 docs/en/guides/interaction.md create mode 100644 docs/en/guides/migration.md create mode 100644 docs/en/guides/sessions.md create mode 100644 docs/en/guides/use-cases.md create mode 100644 docs/en/index.md create mode 100644 docs/en/reference/keyboard.md create mode 100644 docs/en/reference/kimi-command.md create mode 100644 docs/en/reference/slash-commands.md create mode 100644 docs/en/reference/tools.md create mode 100644 docs/en/release-notes/changelog.md create mode 100644 docs/index.md create mode 100644 docs/media/intro.gif create mode 100644 docs/package.json create mode 100644 docs/pnpm-lock.yaml create mode 100644 docs/public/favicon.ico create mode 100644 docs/zh/configuration/config-files.md create mode 100644 docs/zh/configuration/data-locations.md create mode 100644 docs/zh/configuration/env-vars.md create mode 100644 docs/zh/configuration/overrides.md create mode 100644 docs/zh/configuration/providers.md create mode 100644 docs/zh/customization/agents.md create mode 100644 docs/zh/customization/hooks.md create mode 100644 docs/zh/customization/mcp.md create mode 100644 docs/zh/customization/skills.md create mode 100644 docs/zh/faq.md create mode 100644 docs/zh/guides/getting-started.md create mode 100644 docs/zh/guides/interaction.md create mode 100644 docs/zh/guides/migration.md create mode 100644 docs/zh/guides/sessions.md create mode 100644 docs/zh/guides/use-cases.md create mode 100644 docs/zh/index.md create mode 100644 docs/zh/reference/keyboard.md create mode 100644 docs/zh/reference/kimi-command.md create mode 100644 docs/zh/reference/slash-commands.md create mode 100644 docs/zh/reference/tools.md create mode 100644 docs/zh/release-notes/changelog.md create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 package.json create mode 100644 packages/agent-core/README.md create mode 100644 packages/agent-core/package.json create mode 100644 packages/agent-core/src/agent/background/index.ts create mode 100644 packages/agent-core/src/agent/compaction/compaction-instruction.md create mode 100644 packages/agent-core/src/agent/compaction/config.ts create mode 100644 packages/agent-core/src/agent/compaction/full.ts create mode 100644 packages/agent-core/src/agent/compaction/index.ts create mode 100644 packages/agent-core/src/agent/compaction/render-messages.ts create mode 100644 packages/agent-core/src/agent/compaction/types.ts create mode 100644 packages/agent-core/src/agent/config/index.ts create mode 100644 packages/agent-core/src/agent/config/thinking.ts create mode 100644 packages/agent-core/src/agent/config/types.ts create mode 100644 packages/agent-core/src/agent/context/complete-slice.ts create mode 100644 packages/agent-core/src/agent/context/index.ts create mode 100644 packages/agent-core/src/agent/context/notification-xml.ts create mode 100644 packages/agent-core/src/agent/context/projector.ts create mode 100644 packages/agent-core/src/agent/context/types.ts create mode 100644 packages/agent-core/src/agent/hooks/engine.ts create mode 100644 packages/agent-core/src/agent/hooks/index.ts create mode 100644 packages/agent-core/src/agent/hooks/runner.ts create mode 100644 packages/agent-core/src/agent/hooks/types.ts create mode 100644 packages/agent-core/src/agent/hooks/user-prompt.ts create mode 100644 packages/agent-core/src/agent/index.ts create mode 100644 packages/agent-core/src/agent/injection/injector.ts create mode 100644 packages/agent-core/src/agent/injection/manager.ts create mode 100644 packages/agent-core/src/agent/injection/permission-mode.ts create mode 100644 packages/agent-core/src/agent/injection/plan-mode.ts create mode 100644 packages/agent-core/src/agent/permission/action-label.ts create mode 100644 packages/agent-core/src/agent/permission/check-rules.ts create mode 100644 packages/agent-core/src/agent/permission/index.ts create mode 100644 packages/agent-core/src/agent/permission/matches-rule.ts create mode 100644 packages/agent-core/src/agent/permission/parse-pattern.ts create mode 100644 packages/agent-core/src/agent/permission/path-glob-match.ts create mode 100644 packages/agent-core/src/agent/permission/policies/ask-user-question.ts create mode 100644 packages/agent-core/src/agent/permission/policies/default-git-cwd-write.ts create mode 100644 packages/agent-core/src/agent/permission/policies/index.ts create mode 100644 packages/agent-core/src/agent/permission/policies/plan.ts create mode 100644 packages/agent-core/src/agent/permission/policies/yolo-workspace-access.ts create mode 100644 packages/agent-core/src/agent/permission/policy.ts create mode 100644 packages/agent-core/src/agent/permission/types.ts create mode 100644 packages/agent-core/src/agent/plan/index.ts create mode 100644 packages/agent-core/src/agent/records/index.ts create mode 100644 packages/agent-core/src/agent/records/types.ts create mode 100644 packages/agent-core/src/agent/records/wire-file.ts create mode 100644 packages/agent-core/src/agent/replay/index.ts create mode 100644 packages/agent-core/src/agent/skill/index.ts create mode 100644 packages/agent-core/src/agent/tool/index.ts create mode 100644 packages/agent-core/src/agent/tool/types.ts create mode 100644 packages/agent-core/src/agent/turn/canonical-args.ts create mode 100644 packages/agent-core/src/agent/turn/index.ts create mode 100644 packages/agent-core/src/agent/turn/kosong-llm.ts create mode 100644 packages/agent-core/src/agent/turn/tool-dedup.ts create mode 100644 packages/agent-core/src/agent/usage/index.ts create mode 100644 packages/agent-core/src/config/index.ts create mode 100644 packages/agent-core/src/config/merge.ts create mode 100644 packages/agent-core/src/config/path.ts create mode 100644 packages/agent-core/src/config/resolve.ts create mode 100644 packages/agent-core/src/config/schema.ts create mode 100644 packages/agent-core/src/config/toml.ts create mode 100644 packages/agent-core/src/errors.ts create mode 100644 packages/agent-core/src/errors/classes.ts create mode 100644 packages/agent-core/src/errors/codes.ts create mode 100644 packages/agent-core/src/errors/index.ts create mode 100644 packages/agent-core/src/errors/serialize.ts create mode 100644 packages/agent-core/src/index.ts create mode 100644 packages/agent-core/src/logging/formatter.ts create mode 100644 packages/agent-core/src/logging/index.ts create mode 100644 packages/agent-core/src/logging/logger.ts create mode 100644 packages/agent-core/src/logging/resolve-config.ts create mode 100644 packages/agent-core/src/logging/sinks.ts create mode 100644 packages/agent-core/src/logging/types.ts create mode 100644 packages/agent-core/src/loop/README.md create mode 100644 packages/agent-core/src/loop/errors.ts create mode 100644 packages/agent-core/src/loop/events.ts create mode 100644 packages/agent-core/src/loop/index.ts create mode 100644 packages/agent-core/src/loop/llm.ts create mode 100644 packages/agent-core/src/loop/retry.ts create mode 100644 packages/agent-core/src/loop/run-turn.ts create mode 100644 packages/agent-core/src/loop/tool-access.ts create mode 100644 packages/agent-core/src/loop/tool-call.ts create mode 100644 packages/agent-core/src/loop/tool-scheduler.ts create mode 100644 packages/agent-core/src/loop/turn-step.ts create mode 100644 packages/agent-core/src/loop/types.ts create mode 100644 packages/agent-core/src/mcp/auth-tool.ts create mode 100644 packages/agent-core/src/mcp/client-http.ts create mode 100644 packages/agent-core/src/mcp/client-shared.ts create mode 100644 packages/agent-core/src/mcp/client-stdio.ts create mode 100644 packages/agent-core/src/mcp/config-loader.ts create mode 100644 packages/agent-core/src/mcp/connection-manager.ts create mode 100644 packages/agent-core/src/mcp/index.ts create mode 100644 packages/agent-core/src/mcp/oauth/callback-server.ts create mode 100644 packages/agent-core/src/mcp/oauth/index.ts create mode 100644 packages/agent-core/src/mcp/oauth/provider.ts create mode 100644 packages/agent-core/src/mcp/oauth/service.ts create mode 100644 packages/agent-core/src/mcp/oauth/store.ts create mode 100644 packages/agent-core/src/mcp/output.ts create mode 100644 packages/agent-core/src/mcp/session-config.ts create mode 100644 packages/agent-core/src/mcp/tool-naming.ts create mode 100644 packages/agent-core/src/mcp/types.ts create mode 100644 packages/agent-core/src/profile/context.ts create mode 100644 packages/agent-core/src/profile/default.ts create mode 100644 packages/agent-core/src/profile/default/agent.yaml create mode 100644 packages/agent-core/src/profile/default/coder.yaml create mode 100644 packages/agent-core/src/profile/default/explore.yaml create mode 100644 packages/agent-core/src/profile/default/init.md create mode 100644 packages/agent-core/src/profile/default/plan.yaml create mode 100644 packages/agent-core/src/profile/default/system.md create mode 100644 packages/agent-core/src/profile/index.ts create mode 100644 packages/agent-core/src/profile/load.ts create mode 100644 packages/agent-core/src/profile/resolve.ts create mode 100644 packages/agent-core/src/profile/types.ts create mode 100644 packages/agent-core/src/prompt-modules.d.ts create mode 100644 packages/agent-core/src/providers/index.ts create mode 100644 packages/agent-core/src/providers/provider-manager.ts create mode 100644 packages/agent-core/src/providers/request-auth.ts create mode 100644 packages/agent-core/src/providers/runtime-provider.ts create mode 100644 packages/agent-core/src/rpc/client.ts create mode 100644 packages/agent-core/src/rpc/core-api.ts create mode 100644 packages/agent-core/src/rpc/core-impl.ts create mode 100644 packages/agent-core/src/rpc/events.ts create mode 100644 packages/agent-core/src/rpc/index.ts create mode 100644 packages/agent-core/src/rpc/resumed.ts create mode 100644 packages/agent-core/src/rpc/sdk-api.ts create mode 100644 packages/agent-core/src/rpc/types.ts create mode 100644 packages/agent-core/src/runtime-types.ts create mode 100644 packages/agent-core/src/session/export/index.ts create mode 100644 packages/agent-core/src/session/export/manifest.ts create mode 100644 packages/agent-core/src/session/export/session-export.ts create mode 100644 packages/agent-core/src/session/export/wire-scan.ts create mode 100644 packages/agent-core/src/session/export/zip.ts create mode 100644 packages/agent-core/src/session/git-context.ts create mode 100644 packages/agent-core/src/session/index.ts create mode 100644 packages/agent-core/src/session/prompt-metadata.ts create mode 100644 packages/agent-core/src/session/rpc.ts create mode 100644 packages/agent-core/src/session/store/index.ts create mode 100644 packages/agent-core/src/session/store/session-index.ts create mode 100644 packages/agent-core/src/session/store/session-store.ts create mode 100644 packages/agent-core/src/session/store/workdir-key.ts create mode 100644 packages/agent-core/src/session/subagent-host.ts create mode 100644 packages/agent-core/src/session/summary-continuation.md create mode 100644 packages/agent-core/src/skill/builtin/index.ts create mode 100644 packages/agent-core/src/skill/builtin/mcp-config.md create mode 100644 packages/agent-core/src/skill/builtin/mcp-config.ts create mode 100644 packages/agent-core/src/skill/index.ts create mode 100644 packages/agent-core/src/skill/parser.ts create mode 100644 packages/agent-core/src/skill/registry.ts create mode 100644 packages/agent-core/src/skill/scanner.ts create mode 100644 packages/agent-core/src/skill/types.ts create mode 100644 packages/agent-core/src/telemetry.ts create mode 100644 packages/agent-core/src/tools/args-validator.ts create mode 100644 packages/agent-core/src/tools/background/index.ts create mode 100644 packages/agent-core/src/tools/background/manager.ts create mode 100644 packages/agent-core/src/tools/background/persist.ts create mode 100644 packages/agent-core/src/tools/background/task-list.md create mode 100644 packages/agent-core/src/tools/background/task-list.ts create mode 100644 packages/agent-core/src/tools/background/task-output.md create mode 100644 packages/agent-core/src/tools/background/task-output.ts create mode 100644 packages/agent-core/src/tools/background/task-stop.md create mode 100644 packages/agent-core/src/tools/background/task-stop.ts create mode 100644 packages/agent-core/src/tools/builtin/collaboration/agent-background-disabled.md create mode 100644 packages/agent-core/src/tools/builtin/collaboration/agent-background-enabled.md create mode 100644 packages/agent-core/src/tools/builtin/collaboration/agent.md create mode 100644 packages/agent-core/src/tools/builtin/collaboration/agent.ts create mode 100644 packages/agent-core/src/tools/builtin/collaboration/ask-user.md create mode 100644 packages/agent-core/src/tools/builtin/collaboration/ask-user.ts create mode 100644 packages/agent-core/src/tools/builtin/collaboration/skill-tool.md create mode 100644 packages/agent-core/src/tools/builtin/collaboration/skill-tool.ts create mode 100644 packages/agent-core/src/tools/builtin/file/edit.md create mode 100644 packages/agent-core/src/tools/builtin/file/edit.ts create mode 100644 packages/agent-core/src/tools/builtin/file/glob.md create mode 100644 packages/agent-core/src/tools/builtin/file/glob.ts create mode 100644 packages/agent-core/src/tools/builtin/file/grep.md create mode 100644 packages/agent-core/src/tools/builtin/file/grep.ts create mode 100644 packages/agent-core/src/tools/builtin/file/line-endings.ts create mode 100644 packages/agent-core/src/tools/builtin/file/read-media.md create mode 100644 packages/agent-core/src/tools/builtin/file/read-media.ts create mode 100644 packages/agent-core/src/tools/builtin/file/read.md create mode 100644 packages/agent-core/src/tools/builtin/file/read.ts create mode 100644 packages/agent-core/src/tools/builtin/file/write.md create mode 100644 packages/agent-core/src/tools/builtin/file/write.ts create mode 100644 packages/agent-core/src/tools/builtin/index.ts create mode 100644 packages/agent-core/src/tools/builtin/planning/enter-plan-mode.md create mode 100644 packages/agent-core/src/tools/builtin/planning/enter-plan-mode.ts create mode 100644 packages/agent-core/src/tools/builtin/planning/exit-plan-mode.md create mode 100644 packages/agent-core/src/tools/builtin/planning/exit-plan-mode.ts create mode 100644 packages/agent-core/src/tools/builtin/shell/bash.md create mode 100644 packages/agent-core/src/tools/builtin/shell/bash.ts create mode 100644 packages/agent-core/src/tools/builtin/state/todo-list.md create mode 100644 packages/agent-core/src/tools/builtin/state/todo-list.ts create mode 100644 packages/agent-core/src/tools/builtin/web/fetch-url.md create mode 100644 packages/agent-core/src/tools/builtin/web/fetch-url.ts create mode 100644 packages/agent-core/src/tools/builtin/web/web-search.md create mode 100644 packages/agent-core/src/tools/builtin/web/web-search.ts create mode 100644 packages/agent-core/src/tools/display/index.ts create mode 100644 packages/agent-core/src/tools/display/schemas.ts create mode 100644 packages/agent-core/src/tools/policies/default-permissions.ts create mode 100644 packages/agent-core/src/tools/policies/path-access.ts create mode 100644 packages/agent-core/src/tools/policies/sensitive.ts create mode 100644 packages/agent-core/src/tools/providers/local-fetch-url.ts create mode 100644 packages/agent-core/src/tools/providers/moonshot-fetch-url.ts create mode 100644 packages/agent-core/src/tools/providers/moonshot-web-search.ts create mode 100644 packages/agent-core/src/tools/store.ts create mode 100644 packages/agent-core/src/tools/support/file-type.ts create mode 100644 packages/agent-core/src/tools/support/git-worktree.ts create mode 100644 packages/agent-core/src/tools/support/input-schema.ts create mode 100644 packages/agent-core/src/tools/support/list-directory.ts create mode 100644 packages/agent-core/src/tools/support/result-builder.ts create mode 100644 packages/agent-core/src/tools/support/rg-locator.ts create mode 100644 packages/agent-core/src/tools/support/workspace.ts create mode 100644 packages/agent-core/src/utils/abort.ts create mode 100644 packages/agent-core/src/utils/completion-budget.ts create mode 100644 packages/agent-core/src/utils/environment.ts create mode 100644 packages/agent-core/src/utils/fs.ts create mode 100644 packages/agent-core/src/utils/hero-slug.ts create mode 100644 packages/agent-core/src/utils/render-prompt.ts create mode 100644 packages/agent-core/src/utils/tokens.ts create mode 100644 packages/agent-core/src/utils/types.ts create mode 100644 packages/agent-core/src/utils/workdir-slug.ts create mode 100644 packages/agent-core/src/version.ts create mode 100644 packages/agent-core/test/agent/background-manager.test.ts create mode 100644 packages/agent-core/test/agent/basic.test.ts create mode 100644 packages/agent-core/test/agent/compaction.test.ts create mode 100644 packages/agent-core/test/agent/config-state.test.ts create mode 100644 packages/agent-core/test/agent/config.test.ts create mode 100644 packages/agent-core/test/agent/config/thinking.test.ts create mode 100644 packages/agent-core/test/agent/context.test.ts create mode 100644 packages/agent-core/test/agent/file-system-record-persistence.test.ts create mode 100644 packages/agent-core/test/agent/harness/agent.ts create mode 100644 packages/agent-core/test/agent/harness/index.ts create mode 100644 packages/agent-core/test/agent/harness/scripted-generate.ts create mode 100644 packages/agent-core/test/agent/harness/snapshots.ts create mode 100644 packages/agent-core/test/agent/injection/manager.test.ts create mode 100644 packages/agent-core/test/agent/injection/plan-mode.test.ts create mode 100644 packages/agent-core/test/agent/kosong-llm.test.ts create mode 100644 packages/agent-core/test/agent/permission.test.ts create mode 100644 packages/agent-core/test/agent/plan.test.ts create mode 100644 packages/agent-core/test/agent/question.test.ts create mode 100644 packages/agent-core/test/agent/resume.test.ts create mode 100644 packages/agent-core/test/agent/skill-tool-manager.test.ts create mode 100644 packages/agent-core/test/agent/tool.test.ts create mode 100644 packages/agent-core/test/agent/turn.test.ts create mode 100644 packages/agent-core/test/agent/turn/tool-dedup.test.ts create mode 100644 packages/agent-core/test/agent/usage.test.ts create mode 100644 packages/agent-core/test/config/configs.test.ts create mode 100644 packages/agent-core/test/fixtures/telemetry.ts create mode 100644 packages/agent-core/test/harness/model-alias-session.test.ts create mode 100644 packages/agent-core/test/harness/plan-mode-session.test.ts create mode 100644 packages/agent-core/test/harness/runtime-provider.test.ts create mode 100644 packages/agent-core/test/harness/runtime.test.ts create mode 100644 packages/agent-core/test/harness/skill-session.test.ts create mode 100644 packages/agent-core/test/hooks/engine.test.ts create mode 100644 packages/agent-core/test/hooks/integration.test.ts create mode 100644 packages/agent-core/test/hooks/runner.test.ts create mode 100644 packages/agent-core/test/logging/formatter.test.ts create mode 100644 packages/agent-core/test/logging/logger.test.ts create mode 100644 packages/agent-core/test/logging/sinks.test.ts create mode 100644 packages/agent-core/test/loop/abort.e2e.test.ts create mode 100644 packages/agent-core/test/loop/api-shape.e2e.test.ts create mode 100644 packages/agent-core/test/loop/error-paths.e2e.test.ts create mode 100644 packages/agent-core/test/loop/events.e2e.test.ts create mode 100644 packages/agent-core/test/loop/fixtures/collecting-sink.ts create mode 100644 packages/agent-core/test/loop/fixtures/fake-llm.ts create mode 100644 packages/agent-core/test/loop/fixtures/helpers.ts create mode 100644 packages/agent-core/test/loop/fixtures/recording-context.ts create mode 100644 packages/agent-core/test/loop/fixtures/tools.ts create mode 100644 packages/agent-core/test/loop/hooks.e2e.test.ts create mode 100644 packages/agent-core/test/loop/streaming.e2e.test.ts create mode 100644 packages/agent-core/test/loop/tool-call.e2e.test.ts create mode 100644 packages/agent-core/test/loop/tool-scheduler.test.ts create mode 100644 packages/agent-core/test/loop/transcript.e2e.test.ts create mode 100644 packages/agent-core/test/loop/turn-lifecycle.e2e.test.ts create mode 100644 packages/agent-core/test/mcp/auth-tool.test.ts create mode 100644 packages/agent-core/test/mcp/client-http.test.ts create mode 100644 packages/agent-core/test/mcp/client-stdio.test.ts create mode 100644 packages/agent-core/test/mcp/config-loader.test.ts create mode 100644 packages/agent-core/test/mcp/connection-manager.test.ts create mode 100644 packages/agent-core/test/mcp/fixtures/crash-after-connect-stdio-server.mjs create mode 100644 packages/agent-core/test/mcp/fixtures/hanging-list-stdio-server.mjs create mode 100644 packages/agent-core/test/mcp/fixtures/mock-stdio-server.mjs create mode 100644 packages/agent-core/test/mcp/fixtures/slow-stdio-server.mjs create mode 100644 packages/agent-core/test/mcp/fixtures/stderr-then-exit-stdio-server.mjs create mode 100644 packages/agent-core/test/mcp/oauth-store.test.ts create mode 100644 packages/agent-core/test/mcp/output.test.ts create mode 100644 packages/agent-core/test/mcp/tool-manager-mcp.test.ts create mode 100644 packages/agent-core/test/mcp/tool-naming.test.ts create mode 100644 packages/agent-core/test/profile/agent-profile-loader.test.ts create mode 100644 packages/agent-core/test/profile/context.test.ts create mode 100644 packages/agent-core/test/profile/default-agent-profiles.test.ts create mode 100644 packages/agent-core/test/prompt-placeholders.test.ts create mode 100644 packages/agent-core/test/rpc/create-rpc.test.ts create mode 100644 packages/agent-core/test/session/git-context.test.ts create mode 100644 packages/agent-core/test/session/init.test.ts create mode 100644 packages/agent-core/test/session/lifecycle-hooks.test.ts create mode 100644 packages/agent-core/test/session/subagent-host.test.ts create mode 100644 packages/agent-core/test/skill/parser-frontmatter.test.ts create mode 100644 packages/agent-core/test/skill/parser.test.ts create mode 100644 packages/agent-core/test/skill/registry.test.ts create mode 100644 packages/agent-core/test/skill/scanner.test.ts create mode 100644 packages/agent-core/test/tools/agent.test.ts create mode 100644 packages/agent-core/test/tools/ask-user.test.ts create mode 100644 packages/agent-core/test/tools/background/agent-timeout.test.ts create mode 100644 packages/agent-core/test/tools/background/heartbeat-stale.test.ts create mode 100644 packages/agent-core/test/tools/background/ids.test.ts create mode 100644 packages/agent-core/test/tools/background/lifecycle.test.ts create mode 100644 packages/agent-core/test/tools/background/manager.test.ts create mode 100644 packages/agent-core/test/tools/background/output-access.test.ts create mode 100644 packages/agent-core/test/tools/background/persist.test.ts create mode 100644 packages/agent-core/test/tools/background/reconcile.test.ts create mode 100644 packages/agent-core/test/tools/background/state-transitions.test.ts create mode 100644 packages/agent-core/test/tools/background/task-tools.test.ts create mode 100644 packages/agent-core/test/tools/bash-env.test.ts create mode 100644 packages/agent-core/test/tools/bash-windows-kill.test.ts create mode 100644 packages/agent-core/test/tools/bash.test.ts create mode 100644 packages/agent-core/test/tools/builtin-current.test.ts create mode 100644 packages/agent-core/test/tools/default-permissions.test.ts create mode 100644 packages/agent-core/test/tools/edit.test.ts create mode 100644 packages/agent-core/test/tools/enter-plan-mode.test.ts create mode 100644 packages/agent-core/test/tools/exit-plan-mode-options.test.ts create mode 100644 packages/agent-core/test/tools/exit-plan-mode.test.ts create mode 100644 packages/agent-core/test/tools/fetch-url.test.ts create mode 100644 packages/agent-core/test/tools/file-type.test.ts create mode 100644 packages/agent-core/test/tools/fixtures/execute-tool.ts create mode 100644 packages/agent-core/test/tools/fixtures/fake-kaos.ts create mode 100644 packages/agent-core/test/tools/glob.test.ts create mode 100644 packages/agent-core/test/tools/grep.test.ts create mode 100644 packages/agent-core/test/tools/input-schema-io.test.ts create mode 100644 packages/agent-core/test/tools/list-directory.test.ts create mode 100644 packages/agent-core/test/tools/path-guard.test.ts create mode 100644 packages/agent-core/test/tools/plan-mode-hard-block.test.ts create mode 100644 packages/agent-core/test/tools/planning/enter-plan-mode-telemetry.test.ts create mode 100644 packages/agent-core/test/tools/planning/exit-plan-mode-telemetry.test.ts create mode 100644 packages/agent-core/test/tools/policies/sensitive.test.ts create mode 100644 packages/agent-core/test/tools/providers/local-fetch-url.test.ts create mode 100644 packages/agent-core/test/tools/providers/moonshot-fetch-url.test.ts create mode 100644 packages/agent-core/test/tools/read-file.test.ts create mode 100644 packages/agent-core/test/tools/read-media-desc.test.ts create mode 100644 packages/agent-core/test/tools/read-media.test.ts create mode 100644 packages/agent-core/test/tools/read.test.ts create mode 100644 packages/agent-core/test/tools/result-builder.test.ts create mode 100644 packages/agent-core/test/tools/rg-locator.test.ts create mode 100644 packages/agent-core/test/tools/shell-cancel.test.ts create mode 100644 packages/agent-core/test/tools/shell-quoting.test.ts create mode 100644 packages/agent-core/test/tools/skill-tool-dispatch.test.ts create mode 100644 packages/agent-core/test/tools/skill-tool.test.ts create mode 100644 packages/agent-core/test/tools/todo-list.test.ts create mode 100644 packages/agent-core/test/tools/web-search.test.ts create mode 100644 packages/agent-core/test/tools/write.test.ts create mode 100644 packages/agent-core/test/utils/completion-budget.test.ts create mode 100644 packages/agent-core/test/utils/environment.test.ts create mode 100644 packages/agent-core/test/utils/hero-slug.test.ts create mode 100644 packages/agent-core/tsconfig.json create mode 100644 packages/agent-core/tsdown.config.ts create mode 100644 packages/agent-core/vitest.config.ts create mode 100644 packages/kaos/README.md create mode 100644 packages/kaos/package.json create mode 100644 packages/kaos/src/current.ts create mode 100644 packages/kaos/src/errors.ts create mode 100644 packages/kaos/src/index.ts create mode 100644 packages/kaos/src/internal.ts create mode 100644 packages/kaos/src/kaos.ts create mode 100644 packages/kaos/src/local.ts create mode 100644 packages/kaos/src/path.ts create mode 100644 packages/kaos/src/process.ts create mode 100644 packages/kaos/src/ssh.ts create mode 100644 packages/kaos/src/types.ts create mode 100644 packages/kaos/test/cmd.test.ts create mode 100644 packages/kaos/test/current.test.ts create mode 100644 packages/kaos/test/e2e/async-isolation.test.ts create mode 100644 packages/kaos/test/e2e/concurrent-operations.test.ts create mode 100644 packages/kaos/test/e2e/cwd-isolation.test.ts create mode 100644 packages/kaos/test/e2e/exec-edge-cases.test.ts create mode 100644 packages/kaos/test/e2e/glob-boundaries-parity.test.ts create mode 100644 packages/kaos/test/e2e/path-cross-platform.test.ts create mode 100644 packages/kaos/test/e2e/process-lifecycle.test.ts create mode 100644 packages/kaos/test/e2e/ssh-mock.test.ts create mode 100644 packages/kaos/test/e2e/ssh-resolve-path.test.ts create mode 100644 packages/kaos/test/e2e/symlink-stat-parity.test.ts create mode 100644 packages/kaos/test/internal.test.ts create mode 100644 packages/kaos/test/local.test.ts create mode 100644 packages/kaos/test/path.test.ts create mode 100644 packages/kaos/test/shell.test.ts create mode 100644 packages/kaos/test/ssh-create.test.ts create mode 100644 packages/kaos/test/ssh-process.test.ts create mode 100644 packages/kaos/test/ssh.test.ts create mode 100644 packages/kaos/tsconfig.json create mode 100644 packages/kaos/tsdown.config.ts create mode 100644 packages/kaos/vitest.config.ts create mode 100644 packages/kosong/README.md create mode 100644 packages/kosong/package.json create mode 100644 packages/kosong/src/capability.ts create mode 100644 packages/kosong/src/errors.ts create mode 100644 packages/kosong/src/generate.ts create mode 100644 packages/kosong/src/index.ts create mode 100644 packages/kosong/src/message.ts create mode 100644 packages/kosong/src/provider.ts create mode 100644 packages/kosong/src/providers/anthropic.ts create mode 100644 packages/kosong/src/providers/capability-registry.ts create mode 100644 packages/kosong/src/providers/chat-completions-stream.ts create mode 100644 packages/kosong/src/providers/google-genai.ts create mode 100644 packages/kosong/src/providers/index.ts create mode 100644 packages/kosong/src/providers/kimi-files.ts create mode 100644 packages/kosong/src/providers/kimi-schema.ts create mode 100644 packages/kosong/src/providers/kimi.ts create mode 100644 packages/kosong/src/providers/openai-common.ts create mode 100644 packages/kosong/src/providers/openai-legacy.ts create mode 100644 packages/kosong/src/providers/openai-responses.ts create mode 100644 packages/kosong/src/providers/request-auth.ts create mode 100644 packages/kosong/src/tool.ts create mode 100644 packages/kosong/src/usage.ts create mode 100644 packages/kosong/test/anthropic-errors.test.ts create mode 100644 packages/kosong/test/anthropic.test.ts create mode 100644 packages/kosong/test/capability-providers.test.ts create mode 100644 packages/kosong/test/capability.test.ts create mode 100644 packages/kosong/test/e2e/abort-cleanup.test.ts create mode 100644 packages/kosong/test/e2e/abort-signal.test.ts create mode 100644 packages/kosong/test/e2e/anthropic-adapter.test.ts create mode 100644 packages/kosong/test/e2e/cross-provider-consistency.test.ts create mode 100644 packages/kosong/test/e2e/error-recovery.test.ts create mode 100644 packages/kosong/test/e2e/extreme-streams.test.ts create mode 100644 packages/kosong/test/e2e/fake-provider-harness.test.ts create mode 100644 packages/kosong/test/e2e/fake-provider-harness.ts create mode 100644 packages/kosong/test/e2e/fixtures/python-jsonl-compat.jsonl create mode 100644 packages/kosong/test/e2e/google-genai-adapter.test.ts create mode 100644 packages/kosong/test/e2e/google-genai-error-mapping.test.ts create mode 100644 packages/kosong/test/e2e/kimi-adapter-e2e.test.ts create mode 100644 packages/kosong/test/e2e/multi-step-agent-loop.test.ts create mode 100644 packages/kosong/test/e2e/openai-legacy-adapter-e2e.test.ts create mode 100644 packages/kosong/test/e2e/openai-responses-adapter-e2e.test.ts create mode 100644 packages/kosong/test/e2e/parallel-tool-calls.test.ts create mode 100644 packages/kosong/test/e2e/provider-error-handling.test.ts create mode 100644 packages/kosong/test/e2e/provider-switching.test.ts create mode 100644 packages/kosong/test/e2e/streaming-fidelity.test.ts create mode 100644 packages/kosong/test/e2e/streaming-providers.test.ts create mode 100644 packages/kosong/test/e2e/toolchain-bridges.test.ts create mode 100644 packages/kosong/test/e2e/toolset-advanced.test.ts create mode 100644 packages/kosong/test/echo-provider.test.ts create mode 100644 packages/kosong/test/errors.test.ts create mode 100644 packages/kosong/test/finish-reason.test.ts create mode 100644 packages/kosong/test/fixtures/args-validator.ts create mode 100644 packages/kosong/test/fixtures/echo-provider.ts create mode 100644 packages/kosong/test/fixtures/mock-provider.ts create mode 100644 packages/kosong/test/fixtures/simple-toolset.ts create mode 100644 packages/kosong/test/fixtures/step.ts create mode 100644 packages/kosong/test/fixtures/typed-tool.ts create mode 100644 packages/kosong/test/generate.test.ts create mode 100644 packages/kosong/test/google-genai.test.ts create mode 100644 packages/kosong/test/kimi-files.test.ts create mode 100644 packages/kosong/test/kimi.test.ts create mode 100644 packages/kosong/test/message.test.ts create mode 100644 packages/kosong/test/openai-common-errors.test.ts create mode 100644 packages/kosong/test/openai-legacy.test.ts create mode 100644 packages/kosong/test/openai-responses.test.ts create mode 100644 packages/kosong/test/provider.test.ts create mode 100644 packages/kosong/test/providers/kimi-schema.test.ts create mode 100644 packages/kosong/test/regression.test.ts create mode 100644 packages/kosong/test/scripted-echo-provider.test.ts create mode 100644 packages/kosong/test/simple-toolset.test.ts create mode 100644 packages/kosong/test/step.test.ts create mode 100644 packages/kosong/test/stress.test.ts create mode 100644 packages/kosong/test/tool-message-conversion.test.ts create mode 100644 packages/kosong/test/type-safety-negative.ts create mode 100644 packages/kosong/test/type-safety.test.ts create mode 100644 packages/kosong/test/typed-tool.test.ts create mode 100644 packages/kosong/test/usage.test.ts create mode 100644 packages/kosong/tsconfig.json create mode 100644 packages/kosong/tsconfig.type-negative.json create mode 100644 packages/kosong/tsdown.config.ts create mode 100644 packages/kosong/vitest.config.ts create mode 100644 packages/migration-legacy/package.json create mode 100644 packages/migration-legacy/src/atomic-write.ts create mode 100644 packages/migration-legacy/src/detect.ts create mode 100644 packages/migration-legacy/src/index.ts create mode 100644 packages/migration-legacy/src/kimi-cli-schema.ts create mode 100644 packages/migration-legacy/src/marker.ts create mode 100644 packages/migration-legacy/src/migration-errors-log.ts create mode 100644 packages/migration-legacy/src/paths.ts create mode 100644 packages/migration-legacy/src/prompt.ts create mode 100644 packages/migration-legacy/src/report.ts create mode 100644 packages/migration-legacy/src/run-migration.ts create mode 100644 packages/migration-legacy/src/session-index.ts create mode 100644 packages/migration-legacy/src/sessions/classify.ts create mode 100644 packages/migration-legacy/src/sessions/close-tool-calls.ts create mode 100644 packages/migration-legacy/src/sessions/content-part.ts create mode 100644 packages/migration-legacy/src/sessions/index.ts create mode 100644 packages/migration-legacy/src/sessions/migrate-one.ts create mode 100644 packages/migration-legacy/src/sessions/state-writer.ts create mode 100644 packages/migration-legacy/src/sessions/translator.ts create mode 100644 packages/migration-legacy/src/sessions/wire-writer.ts create mode 100644 packages/migration-legacy/src/sessions/workdir-bucket.ts create mode 100644 packages/migration-legacy/src/steps/config.ts create mode 100644 packages/migration-legacy/src/steps/mcp.ts create mode 100644 packages/migration-legacy/src/steps/user-history.ts create mode 100644 packages/migration-legacy/src/stub-detect.ts create mode 100644 packages/migration-legacy/src/types.ts create mode 100644 packages/migration-legacy/test/.gitkeep create mode 100644 packages/migration-legacy/test/atomic-write.test.ts create mode 100644 packages/migration-legacy/test/detect.test.ts create mode 100644 packages/migration-legacy/test/fixtures/archived/context.jsonl create mode 100644 packages/migration-legacy/test/fixtures/archived/state.json create mode 100644 packages/migration-legacy/test/fixtures/archived/wire.jsonl create mode 100644 packages/migration-legacy/test/fixtures/broken-state-json/context.jsonl create mode 100644 packages/migration-legacy/test/fixtures/broken-state-json/state.json create mode 100644 packages/migration-legacy/test/fixtures/broken-state-json/wire.jsonl create mode 100644 packages/migration-legacy/test/fixtures/large-100msgs/context.jsonl create mode 100644 packages/migration-legacy/test/fixtures/large-100msgs/state.json create mode 100644 packages/migration-legacy/test/fixtures/large-100msgs/wire.jsonl create mode 100644 packages/migration-legacy/test/fixtures/legacy-protocol-1.3/context.jsonl create mode 100644 packages/migration-legacy/test/fixtures/legacy-protocol-1.3/state.json create mode 100644 packages/migration-legacy/test/fixtures/legacy-protocol-1.3/wire.jsonl create mode 100644 packages/migration-legacy/test/fixtures/multi-workdir/.kimi/kimi.json create mode 100644 packages/migration-legacy/test/fixtures/multi-workdir/.kimi/sessions/73d3cf4d1cf337adf57d6732b8be2205/uuid-a1/context.jsonl create mode 100644 packages/migration-legacy/test/fixtures/multi-workdir/.kimi/sessions/73d3cf4d1cf337adf57d6732b8be2205/uuid-a1/state.json create mode 100644 packages/migration-legacy/test/fixtures/multi-workdir/.kimi/sessions/73d3cf4d1cf337adf57d6732b8be2205/uuid-a2/context.jsonl create mode 100644 packages/migration-legacy/test/fixtures/multi-workdir/.kimi/sessions/73d3cf4d1cf337adf57d6732b8be2205/uuid-a2/state.json create mode 100644 packages/migration-legacy/test/fixtures/multi-workdir/.kimi/sessions/dbf62706c1b976e79a5e7cfcc3491a1f/uuid-b1/test create mode 100644 packages/migration-legacy/test/fixtures/multi-workdir/.kimi/sessions/kaos-foo_caaf674614e26c6365945408f85c9c9a/uuid-c1/context.jsonl create mode 100644 packages/migration-legacy/test/fixtures/multi-workdir/.kimi/sessions/kaos-foo_caaf674614e26c6365945408f85c9c9a/uuid-c1/state.json create mode 100644 packages/migration-legacy/test/fixtures/recent-protocol-1.10/context.jsonl create mode 100644 packages/migration-legacy/test/fixtures/recent-protocol-1.10/state.json create mode 100644 packages/migration-legacy/test/fixtures/recent-protocol-1.10/wire.jsonl create mode 100644 packages/migration-legacy/test/fixtures/tiny-hello-world/context.jsonl create mode 100644 packages/migration-legacy/test/fixtures/tiny-hello-world/state.json create mode 100644 packages/migration-legacy/test/fixtures/tiny-hello-world/wire.jsonl create mode 100644 packages/migration-legacy/test/fixtures/with-image/context.jsonl create mode 100644 packages/migration-legacy/test/fixtures/with-image/wire.jsonl create mode 100644 packages/migration-legacy/test/fixtures/with-subagent-collapsed/context.jsonl create mode 100644 packages/migration-legacy/test/fixtures/with-subagent-collapsed/state.json create mode 100644 packages/migration-legacy/test/fixtures/with-subagent-collapsed/wire.jsonl create mode 100644 packages/migration-legacy/test/fixtures/with-thinking/context.jsonl create mode 100644 packages/migration-legacy/test/fixtures/with-thinking/state.json create mode 100644 packages/migration-legacy/test/fixtures/with-thinking/wire.jsonl create mode 100644 packages/migration-legacy/test/fixtures/with-tool-calls/context.jsonl create mode 100644 packages/migration-legacy/test/fixtures/with-tool-calls/state.json create mode 100644 packages/migration-legacy/test/fixtures/with-tool-calls/wire.jsonl create mode 100644 packages/migration-legacy/test/integration.test.ts create mode 100644 packages/migration-legacy/test/kimi-cli-schema.test.ts create mode 100644 packages/migration-legacy/test/marker.test.ts create mode 100644 packages/migration-legacy/test/migration-errors-log.test.ts create mode 100644 packages/migration-legacy/test/paths.test.ts create mode 100644 packages/migration-legacy/test/prompt-modules.d.ts create mode 100644 packages/migration-legacy/test/prompt.test.ts create mode 100644 packages/migration-legacy/test/report.test.ts create mode 100644 packages/migration-legacy/test/resume.integration.test.ts create mode 100644 packages/migration-legacy/test/session-index.test.ts create mode 100644 packages/migration-legacy/test/sessions/__snapshots__/fixtures.snapshot.test.ts.snap create mode 100644 packages/migration-legacy/test/sessions/classify.test.ts create mode 100644 packages/migration-legacy/test/sessions/close-tool-calls.test.ts create mode 100644 packages/migration-legacy/test/sessions/content-part.test.ts create mode 100644 packages/migration-legacy/test/sessions/fixtures.snapshot.test.ts create mode 100644 packages/migration-legacy/test/sessions/migrate-one.test.ts create mode 100644 packages/migration-legacy/test/sessions/sessions-step.test.ts create mode 100644 packages/migration-legacy/test/sessions/state-writer.test.ts create mode 100644 packages/migration-legacy/test/sessions/translator.test.ts create mode 100644 packages/migration-legacy/test/sessions/wire-writer.test.ts create mode 100644 packages/migration-legacy/test/sessions/workdir-bucket.test.ts create mode 100644 packages/migration-legacy/test/steps/config.test.ts create mode 100644 packages/migration-legacy/test/steps/mcp.test.ts create mode 100644 packages/migration-legacy/test/steps/user-history.test.ts create mode 100644 packages/migration-legacy/test/stub-detect.test.ts create mode 100644 packages/migration-legacy/tsconfig.json create mode 100644 packages/migration-legacy/tsdown.config.ts create mode 100644 packages/migration-legacy/vitest.config.ts create mode 100644 packages/node-sdk/README.md create mode 100644 packages/node-sdk/api-extractor.json create mode 100644 packages/node-sdk/examples/kimi-harness-auth-smoke.ts create mode 100644 packages/node-sdk/examples/kimi-harness-cancel-smoke.ts create mode 100644 packages/node-sdk/examples/kimi-harness-config-smoke.ts create mode 100644 packages/node-sdk/examples/kimi-harness-export-smoke.ts create mode 100644 packages/node-sdk/examples/kimi-harness-list-smoke.ts create mode 100644 packages/node-sdk/examples/kimi-harness-log-marker.ts create mode 100644 packages/node-sdk/examples/kimi-harness-logging-smoke.ts create mode 100644 packages/node-sdk/examples/kimi-harness-prompt-demo.ts create mode 100644 packages/node-sdk/examples/kimi-harness-rename-smoke.ts create mode 100644 packages/node-sdk/examples/kimi-harness-set-model-smoke.ts create mode 100644 packages/node-sdk/examples/kimi-harness-set-permission-smoke.ts create mode 100644 packages/node-sdk/examples/kimi-harness-set-thinking-smoke.ts create mode 100644 packages/node-sdk/examples/kimi-harness-smoke.ts create mode 100644 packages/node-sdk/examples/kimi-harness-steer-smoke.ts create mode 100644 packages/node-sdk/examples/runtime-smoke-helpers.ts create mode 100644 packages/node-sdk/examples/t8-race-create-single.ts create mode 100644 packages/node-sdk/examples/t8-race-create.ts create mode 100644 packages/node-sdk/examples/t8-race-direct.ts create mode 100644 packages/node-sdk/package.json create mode 100644 packages/node-sdk/scripts/build-dts.mjs create mode 100644 packages/node-sdk/src/auth.ts create mode 100644 packages/node-sdk/src/events.ts create mode 100644 packages/node-sdk/src/index.ts create mode 100644 packages/node-sdk/src/kimi-harness.ts create mode 100644 packages/node-sdk/src/rpc.ts create mode 100644 packages/node-sdk/src/session.ts create mode 100644 packages/node-sdk/src/types.ts create mode 100644 packages/node-sdk/test/auth-facade.test.ts create mode 100644 packages/node-sdk/test/config.test.ts create mode 100644 packages/node-sdk/test/create-session-transport.test.ts create mode 100644 packages/node-sdk/test/export-session.test.ts create mode 100644 packages/node-sdk/test/list-sessions.test.ts create mode 100644 packages/node-sdk/test/local-logging.test.ts create mode 100644 packages/node-sdk/test/rename-session.test.ts create mode 100644 packages/node-sdk/test/runtime-provider-identity.test.ts create mode 100644 packages/node-sdk/test/runtime-provider-oauth.test.ts create mode 100644 packages/node-sdk/test/session-approval-handler.test.ts create mode 100644 packages/node-sdk/test/session-background-tasks.test.ts create mode 100644 packages/node-sdk/test/session-cancel.test.ts create mode 100644 packages/node-sdk/test/session-event-types.test.ts create mode 100644 packages/node-sdk/test/session-plan-compact-usage-resume.test.ts create mode 100644 packages/node-sdk/test/session-prompt-events.test.ts create mode 100644 packages/node-sdk/test/session-prompt-input.test.ts create mode 100644 packages/node-sdk/test/session-question-handler.test.ts create mode 100644 packages/node-sdk/test/session-runtime-helpers.ts create mode 100644 packages/node-sdk/test/session-set-model.test.ts create mode 100644 packages/node-sdk/test/session-set-permission.test.ts create mode 100644 packages/node-sdk/test/session-set-thinking.test.ts create mode 100644 packages/node-sdk/test/session-skills.test.ts create mode 100644 packages/node-sdk/test/session-steer.test.ts create mode 100644 packages/node-sdk/test/telemetry.ts create mode 100644 packages/node-sdk/test/test-identity.ts create mode 100644 packages/node-sdk/tsconfig.api-extractor.json create mode 100644 packages/node-sdk/tsconfig.dts.json create mode 100644 packages/node-sdk/tsconfig.json create mode 100644 packages/node-sdk/tsdown.config.ts create mode 100644 packages/node-sdk/vitest.config.ts create mode 100644 packages/oauth/README.md create mode 100644 packages/oauth/examples/kimi-oauth-smoke.ts create mode 100644 packages/oauth/package.json create mode 100644 packages/oauth/src/constants.ts create mode 100644 packages/oauth/src/errors.ts create mode 100644 packages/oauth/src/identity.ts create mode 100644 packages/oauth/src/index.ts create mode 100644 packages/oauth/src/managed-feedback.ts create mode 100644 packages/oauth/src/managed-kimi-code.ts create mode 100644 packages/oauth/src/managed-usage.ts create mode 100644 packages/oauth/src/oauth-manager.ts create mode 100644 packages/oauth/src/oauth.ts create mode 100644 packages/oauth/src/open-platform.ts create mode 100644 packages/oauth/src/storage.ts create mode 100644 packages/oauth/src/token-state.ts create mode 100644 packages/oauth/src/toolkit.ts create mode 100644 packages/oauth/src/types.ts create mode 100644 packages/oauth/src/utils.ts create mode 100644 packages/oauth/test/helpers.ts create mode 100644 packages/oauth/test/identity.test.ts create mode 100644 packages/oauth/test/managed-feedback.test.ts create mode 100644 packages/oauth/test/managed-kimi-code.test.ts create mode 100644 packages/oauth/test/managed-usage.test.ts create mode 100644 packages/oauth/test/oauth-manager-lock-failure.test.ts create mode 100644 packages/oauth/test/oauth-manager-multi-process.test.ts create mode 100644 packages/oauth/test/oauth-manager.test.ts create mode 100644 packages/oauth/test/oauth.test.ts create mode 100644 packages/oauth/test/open-platform.test.ts create mode 100644 packages/oauth/test/refresh-threshold.test.ts create mode 100644 packages/oauth/test/storage.test.ts create mode 100644 packages/oauth/test/toolkit.test.ts create mode 100644 packages/oauth/tsconfig.json create mode 100644 packages/oauth/tsdown.config.ts create mode 100644 packages/oauth/vitest.config.ts create mode 100644 packages/telemetry/README.md create mode 100644 packages/telemetry/package.json create mode 100644 packages/telemetry/src/bootstrap.ts create mode 100644 packages/telemetry/src/client.ts create mode 100644 packages/telemetry/src/crash.ts create mode 100644 packages/telemetry/src/index.ts create mode 100644 packages/telemetry/src/remote.ts create mode 100644 packages/telemetry/src/sink.ts create mode 100644 packages/telemetry/src/transport.ts create mode 100644 packages/telemetry/src/types.ts create mode 100644 packages/telemetry/test/normalize-remote.test.ts create mode 100644 packages/telemetry/test/telemetry.test.ts create mode 100644 packages/telemetry/tsconfig.json create mode 100644 packages/telemetry/tsdown.config.ts create mode 100644 packages/telemetry/vitest.config.ts create mode 100644 pnpm-lock.yaml create mode 100644 pnpm-workspace.yaml create mode 100644 tsconfig.json create mode 100644 vitest.config.ts diff --git a/.agents/skills/gen-changesets/SKILL.md b/.agents/skills/gen-changesets/SKILL.md new file mode 100644 index 000000000..6a4d91e66 --- /dev/null +++ b/.agents/skills/gen-changesets/SKILL.md @@ -0,0 +1,109 @@ +--- +name: gen-changesets +description: Use when generating changesets in the kimi-code repository, including package bump selection, internal package and CLI bundle handling, bump levels, major confirmation, and English changelog wording. +--- + +# Generate Changesets + +`kimi-code` uses changesets to manage versions and changelogs. The current user-facing published package is: + +- `@moonshot-ai/kimi-code`: the CLI + +All other `@moonshot-ai/*` packages are treated as internal packages, including `@moonshot-ai/kimi-code-sdk`, `agent-core`, `kosong`, `kaos`, `kimi-code-oauth`, `kimi-telemetry`, and `migration-legacy`. + +## Core Rules + +1. **Inspect the actual changes first.** Use `git status` / `git diff --name-only` to identify which packages were actually changed. +2. **List packages that were actually changed.** Source code, build config, package metadata, and other changes that affect a package's output or behavior need a changeset entry for that package. +3. **Do not list unchanged internal packages.** For example, if `packages/node-sdk` was not changed, do not list `@moonshot-ai/kimi-code-sdk` just because another internal package changed. The SDK follows the same rule as other internal packages: list it only when it was actually changed. +4. **Internal package source changes that enter the CLI bundle must manually list the CLI.** `@moonshot-ai/kimi-code` inline-bundles `@moonshot-ai/*` source, but those internal packages are devDependencies from the CLI's perspective, so changesets will not automatically propagate bumps. If a change enters the CLI output, also list `@moonshot-ai/kimi-code`. +5. **Docs-only and tests-only changes usually do not need a changeset.** README, internal docs, and `test/` changes that do not enter package output do not trigger a CLI bump. +6. `@moonshot-ai/vis` / `vis-server` / `vis-web` are ignored by changesets and should not be handled. + +## Workflow + +1. If the change includes `pnpm-lock.yaml`, `pnpm-workspace.yaml`, root or workspace `package.json`, `.npmrc`, `flake.nix`, or `flake.lock`: + - First check `command -v nix >/dev/null 2>&1`. + - If `nix` exists, run `nix run .#update-pnpm-deps`. + - If `nix` is unavailable, skip this step and do not block the changeset; mention in the final response that the command was not run. +2. List the packages that were actually changed. +3. Choose a bump level for each package. +4. If an internal package change enters the CLI bundle, add `@moonshot-ai/kimi-code`. +5. Create a short kebab-case file under `.changeset/`. +6. Split unrelated changes into separate changesets; keep one logical change in one file. + +Format: + +```markdown +--- +"": patch +"": minor +--- + + +``` + +## Bump Levels + +| Level | When to use | +|---|---| +| `patch` | Bug fixes; build/package fixes; internal refactors that do not change behavior; wording tweaks; small dependency upgrades | +| `minor` | New backwards-compatible features or capabilities | +| `major` | Breaking changes: incompatible config changes, renamed or removed commands/arguments, behavior semantics changes, and similar | + +### Major Rule + +Never write `major` on your own. + +If you believe a change qualifies as major, stop first, explain why, and ask the user for confirmation. Only write `major` after the user explicitly agrees. If the user does not reply, replies ambiguously, or disagrees, fall back to `minor`; if `minor` is also unclear, fall back to `patch`. + +## Wording Rules + +- Changelog entries **must be written in English**. +- User-facing CLI wording should only be used when CLI users can perceive the change. +- Internal changes that do not affect CLI users can still share a changeset with the CLI, but the wording must describe the real change honestly and must not present it as a user-facing feature. +- Do not mention file names, class names, function names, PR numbers, or commit hashes. +- Avoid vague words such as `refactor`, `optimize`, and `improve`. Describe the actual change, or use more specific wording. + +## Common Examples + +An internal package fixes a bug visible to CLI users: + +```markdown +--- +"@moonshot-ai/agent-core": patch +"@moonshot-ai/kimi-code": patch +--- + +Fix occasional loss of tool call results in long conversations. +``` + +An internal package has an internal-only change, but it enters the CLI bundle: + +```markdown +--- +"@moonshot-ai/agent-core": patch +"@moonshot-ai/kimi-code": patch +--- + +Unify tool execution metadata handling. +``` + +Only SDK source changed, and the CLI does not use it: + +```markdown +--- +"@moonshot-ai/kimi-code-sdk": patch +--- + +Clarify session status typing for internal SDK callers. +``` + +## Red Flags + +- You are about to write `major` without asking the user. +- Internal package source enters the CLI bundle, but `@moonshot-ai/kimi-code` is missing. +- `packages/node-sdk` was not changed, but `@moonshot-ai/kimi-code-sdk` was listed for "internal package sync". +- The changelog entry is in Chinese. +- The wording claims more than the diff actually did. +- The CLI wording mentions internal package names, class names, or PR numbers. diff --git a/.agents/skills/gen-docs/SKILL.md b/.agents/skills/gen-docs/SKILL.md new file mode 100644 index 000000000..62830a652 --- /dev/null +++ b/.agents/skills/gen-docs/SKILL.md @@ -0,0 +1,87 @@ +--- +name: gen-docs +description: Update Kimi Code CLI user documentation after meaningful code changes that affect product behavior or user experience. +--- + +# Gen Docs + +## Overview + +This repository maintains bilingual user documentation under `docs/`. `docs/en/` and `docs/zh/` are mirrored pairs for most pages; update both in the same change. **Changelog is the exception** — English is the source, and Chinese is translated from English. + +Use this skill to update the corresponding documentation whenever the codebase has changes that affect product behavior or user experience. + +For a **full pre-release audit** of all pages (detecting hallucinations and coverage gaps), use the `audit-docs` skill instead. + +## Prerequisites + +This skill depends on the following being in place. If any are missing, stop and report to the user before continuing: + +- `docs/` directory with `docs/zh/`, `docs/en/`, and `docs/.vitepress/config.ts` set up (VitePress site). +- `docs/AGENTS.md` style guide — defines source-of-truth rules, terminology table, typography, and writing style. +- `docs/scripts/sync-changelog.mjs` — auto-syncs root `CHANGELOG.md` to `docs/en/release-notes/changelog.md`. +- `translate-docs` skill in `.agents/skills/` — handles bilingual synchronization. + +## Workflow + +1. **Inspect changes** + + - `git log main..HEAD --oneline` — commits on the current branch + - `git diff main..HEAD --stat` — file-level scope + - `ls .changeset/*.md` (excluding `README.md`) — pending changeset entries + - Read `CHANGELOG.md` and any subpackage `packages/*/CHANGELOG.md` for already-recorded entries. + +2. **Understand user-facing impact** + + For each change, read the actual implementation when needed; **do not infer behavior from commit messages or PR titles alone**. Skip: + + - Internal refactors with no externally visible behavior change + - Tests, CI, type-only changes + - Tooling / build-system changes that do not change how users invoke the CLI + + If after the scan you conclude there is no user-facing impact, say so and stop. + +3. **Sync English changelog** + + Run: + + ```bash + node docs/scripts/sync-changelog.mjs + ``` + + This updates `docs/en/release-notes/changelog.md` from the root `CHANGELOG.md`. Never edit the docs changelog by hand. + +4. **Update user docs** + + Following the rules in `docs/AGENTS.md`, edit the affected pages in whichever locale you are working in, then sync the mirror. Match terminology with the term table in `docs/AGENTS.md` and the existing wording in surrounding pages. + + Cover all relevant sections: + + - Guides (getting-started, use cases, interaction, sessions, IDE integration) + - Customization (skills, agents, MCP, hooks, plugins, etc.) + - Configuration (config files, env vars, providers, data locations) + - Reference (CLI subcommands, slash commands, keyboard shortcuts) + - Release notes (`docs/zh/release-notes/breaking-changes.md` if a breaking change is involved) + +5. **Sync bilingual content** + + Invoke the `translate-docs` skill. It will: + + - Sync updated non-changelog pages between `docs/en/` and `docs/zh/` + - Translate the English changelog → Chinese under `docs/zh/release-notes/changelog.md` + +## Rules and conventions + +- **Locale sync**: Non-changelog pages stay mirrored between `docs/en/` and `docs/zh/`. Changelog flows English → Chinese. +- **Terminology**: Use the term table in `docs/AGENTS.md` exactly. Do not invent new translations or use synonyms. +- **Scope discipline**: Only update sections affected by the recent changes. Do not opportunistically rewrite unrelated docs. +- **Breaking changes**: If any change is breaking, also update `docs/en/release-notes/breaking-changes.md` (under `## Unreleased`) with `**Affected**` + `**Migration**` subsections, and mirror it in `docs/zh/release-notes/breaking-changes.md`. +- **Do not edit auto-synced files**: `docs/en/release-notes/changelog.md` is regenerated by the sync script; any manual edit will be overwritten. + +## Common mistakes + +- Describing what code changed instead of what the user can now do (or can no longer do). +- Adding a new section heading per feature instead of weaving the change into existing prose. +- Updating only one locale and leaving its mirror stale. +- Editing only the mirror to fix wording that should be corrected in the locale you changed first. +- Inventing new terminology that drifts from the `docs/AGENTS.md` term table. diff --git a/.agents/skills/sync-changelog/SKILL.md b/.agents/skills/sync-changelog/SKILL.md new file mode 100644 index 000000000..13b734771 --- /dev/null +++ b/.agents/skills/sync-changelog/SKILL.md @@ -0,0 +1,247 @@ +--- +name: sync-changelog +description: Use after a release succeeds, when maintainers need to sync apps/kimi-code/CHANGELOG.md into docs/en/release-notes/changelog.md and docs/zh/release-notes/changelog.md. +--- + +# Sync Changelog + +## Overview + +`kimi-code` uses changesets for versioning. Each package gets its own `CHANGELOG.md`. The user-facing CLI package, `@moonshot-ai/kimi-code`, writes its changelog here: + +```text +apps/kimi-code/CHANGELOG.md +``` + +This file is the **only upstream source** for the documentation-site changelog. Internal package changelogs such as `packages/*/CHANGELOG.md` do not go into the documentation site. + +After the release flow finishes (Release PR merged → `Version Packages` completed → npm publish succeeded), maintainers manually run this skill to copy the new CLI changelog entries into the docs site and translate the English increment into Chinese. + +## When To Use + +- A new version has been published to npm. +- The top of `apps/kimi-code/CHANGELOG.md` contains version blocks that are not yet in `docs/en/release-notes/changelog.md`. +- The `gen-docs` flow does not run this automatically; maintainers must explicitly do it after release. + +Do **not** run this before the Release PR is merged. At that point, changesets has not yet written the new version into `apps/kimi-code/CHANGELOG.md`. + +## Source And Targets + +| File | Role | Edited by | +|---|---|---| +| `apps/kimi-code/CHANGELOG.md` | **Only upstream source**, generated by changesets | Never edit manually | +| `docs/en/release-notes/changelog.md` | English docs changelog; source of truth for docs | This skill | +| `docs/zh/release-notes/changelog.md` | Chinese docs changelog, translated from English | This skill, following `translate-docs` | + +Core rule: the English docs changelog is the source of truth, and Chinese is translated from English. This matches `translate-docs`. + +## Preconditions + +Before editing, confirm: + +- The released version exists on npm (`npm view @moonshot-ai/kimi-code versions --json`) or has a matching GitHub Release tag. +- The top of `apps/kimi-code/CHANGELOG.md` is that new version. +- The current branch is clean, or you are on a dedicated docs-sync branch. + +If any condition is not true, stop and confirm with the user. + +## Workflow + +### 1. Find The Version Range + +```bash +# Upstream versions +rg '^## ' apps/kimi-code/CHANGELOG.md | head -20 + +# Latest version already synced into the English docs page +rg '^## ' docs/en/release-notes/changelog.md | head -5 +``` + +- First sync: copy all upstream version blocks into the English page. +- Incremental sync: copy every upstream version block above the latest version already present in the English page. + +Use upstream order: newest version first. + +### 2. Strip Decorations And Extract Entry Text + +Upstream entries look like this: + +```markdown +- [#317](https://github.com/...) [`2f51db4`](https://github.com/...) - Clean up lint warnings ... +``` + +Keep: + +- Version headings such as `## 0.2.0`. +- Only the body text of each entry, after the PR/hash decoration. + +Remove: + +- The upstream H1 `# @moonshot-ai/kimi-code` because the docs page already has `# Changelog`. +- Changesets subheadings such as `### Patch Changes`, `### Minor Changes`, and `### Major Changes`. +- PR links such as `[#317](...)`. +- Commit hash links such as ``[`2f51db4`](...)``. + +After stripping, each entry should be only: + +```markdown +- +``` + +Upstream language rule: `gen-changesets` requires changelog entries to be English. If the upstream CLI changelog contains a non-English entry, stop and report it to the user. Do not silently rewrite it while syncing docs. + +### 3. Classify Entries + +The docs changelog uses four section types: + +| English section | Chinese section | Meaning | +|---|---|---| +| `### Features` | `### 新功能` | New user-facing functionality | +| `### Bug Fixes` | `### 修复` | Fixes for behavior that was broken | +| `### Refactors` | `### 重构` | Internal changes with no user-visible behavior change, including build, CI, tests, dependency cleanup, and internal renames | +| `### Other` | `### 其他` | Anything that does not fit above, such as user-visible adjustments that are not fixes or features, CDN/endpoint swaps, and docs-related artifacts | + +Classification process: + +1. Classify from the stripped entry text first. +2. If unclear, inspect the related commit or PR: + - Use the stripped commit hash with `git show `. + - Or use the PR number with `gh pr view `. +3. If it is still unclear, put it in `Other`. Do not guess or force entries into `Features`. + +Keyword hints: + +- **Features**: `Add`, `Introduce`, `Support`, `Allow`, `Enable`, `Implement`, `New ... command/flag/option` +- **Bug Fixes**: `Fix`, `Resolve`, `Correct`, `Address`, `Prevent ... from`, `Stop ... from`, `... no longer ...` +- **Refactors**: `Refactor`, `Rename`, `Clean up`, `Simplify`, `Remove unused`, `Migrate to`, `Unify`, `Restructure`, `Internal`, dependency bumps, pure CI/build/test changes +- **Other**: docs artifacts, CDN/endpoint switches, or small user-visible adjustments that are not fixes or new features + +Within each version, section order is: + +```text +Features → Bug Fixes → Refactors → Other +``` + +Omit empty sections. Preserve upstream entry order within each section. + +### 4. Write The English Page + +Never change the English page header: + +```markdown +# Changelog + +This page documents the changes in each Kimi Code CLI release. +``` + +Insert new version blocks immediately after the header paragraph and before the previous latest version. + +Example: + +```markdown +## 0.2.0 + +### Bug Fixes + +- Fix the TUI not restoring the current todo list after resuming a session. + +### Refactors + +- Clean up lint warnings across the CLI, SDK examples, and bundled runtime code without changing product behavior. +- Update the native release workflow to use current GitHub artifact actions. +``` + +### 5. Translate The Increment Into Chinese + +After updating the English page, translate only the newly added English content into `docs/zh/release-notes/changelog.md`. + +Follow `translate-docs`, direction `en → zh`. Changelog direction is English-to-Chinese even though many other docs flows use Chinese-to-English. + +Chinese page requirements: + +- Header: + + ```markdown + # 变更记录 + + 本页记录 Kimi Code CLI 每个版本的变更内容。 + ``` + +- Preserve version headings exactly, such as `## 0.2.0`. +- Translate section headings exactly: + - `### Features` → `### 新功能` + - `### Bug Fixes` → `### 修复` + - `### Refactors` → `### 重构` + - `### Other` → `### 其他` +- The Chinese page must mirror the English page 1:1 for versions, sections, section order, and entry counts. +- Keep the classification from the English page. Do not reclassify while translating. +- Translate only entry body text. Do not add entries that are not present in English. +- Follow `docs/AGENTS.md` for Chinese typography: full-width punctuation, spaces between Chinese and English, and the glossary. + +### 6. Verify + +Review: + +```bash +git diff docs/en/release-notes/changelog.md docs/zh/release-notes/changelog.md +``` + +Check: + +- Versions and version counts match between English and Chinese. +- Each version has the same section set and order on both pages. +- Each section has the same number of entries on both pages. +- PR links and commit hashes were stripped. +- There are no empty sections. +- Markdown indentation and blank lines are intact. + +Then run the docs build: + +```bash +pnpm --filter docs run build +``` + +### 7. Commit + +Use a neutral docs-sync commit message: + +```text +docs(changelog): sync from apps/kimi-code/CHANGELOG.md +``` + +Do **not** create a changeset for changelog docs sync. Docs sync does not enter the bundle. + +## Rules + +- The English docs changelog is the source of truth. +- Never edit upstream `apps/kimi-code/CHANGELOG.md`. +- Do not backfill unreleased `.changeset/*.md` drafts into the docs site. +- If upstream wording is wrong, leave upstream alone and fix it in a future changeset. + +## Common Mistakes + +| Mistake | Fix | +|---|---| +| Adding entries directly to the English docs page without reading upstream | Use `apps/kimi-code/CHANGELOG.md` as the source | +| Copying PR links or commit hashes into docs | Strip them; keep only body text | +| Rewording upstream English entries | Upstream is frozen; copy the body text unless the user explicitly asks otherwise | +| Leaving English text untranslated in the Chinese page | The Chinese page must be fully Chinese except preserved technical terms | +| Editing upstream changelog text | Do not edit upstream | +| Losing two-space indentation in multi-line list items | Restore indentation so Markdown lists stay valid | +| Copying `### Patch Changes` into docs | Remove changesets headings and classify under Features / Bug Fixes / Refactors / Other | +| Guessing unclear entries as Features | Inspect commit/PR; if still unclear, use Other | +| Reclassifying entries while translating | Chinese classification must mirror English | +| Leaving empty sections | Delete sections with no entries | +| Putting everything under Other for convenience | Classify what can be classified first | +| Translating tool names, command names, or config keys | Keep them as written | +| Creating a changeset for docs sync | Do not create one | +| Using curly quotes or half-width Chinese punctuation | Follow `docs/AGENTS.md` | + +## Stop Signals + +- The top version in `apps/kimi-code/CHANGELOG.md` is not published on npm or GitHub Releases. +- You are about to edit `apps/kimi-code/CHANGELOG.md`. +- You are about to add docs sync to a changeset. +- English and Chinese versions, entry counts, or section sets do not match. +- A section is empty. +- A Chinese term is uncertain and `docs/AGENTS.md` does not answer it. diff --git a/.agents/skills/translate-docs/SKILL.md b/.agents/skills/translate-docs/SKILL.md new file mode 100644 index 000000000..d34370225 --- /dev/null +++ b/.agents/skills/translate-docs/SKILL.md @@ -0,0 +1,65 @@ +--- +name: translate-docs +description: Translate and sync bilingual user documentation between docs/zh/ and docs/en/ following the source-of-truth rules in docs/AGENTS.md. +--- + +# Translate Docs + +## Overview + +This repository keeps bilingual user documentation under `docs/zh/` and `docs/en/`. This skill synchronizes the two locales, page by page, after either side has been updated. + +This skill is invoked by both `gen-docs` (incremental updates) and `audit-docs` (full pre-release audit) to keep locale mirrors in sync. + +## Prerequisites + +If any of the following are missing, stop and report to the user before continuing: + +- `docs/zh/` and `docs/en/` mirrored directory structure. +- `docs/AGENTS.md` — terminology table, typography rules, and source-of-truth rules. + +## Locale sync rules + +- **Changelog** (`release-notes/changelog.md`): English is the source. Translate to Chinese. +- **Breaking changes** (`release-notes/breaking-changes.md`): English is the source. Translate to Chinese. +- **All other pages**: `docs/en/` and `docs/zh/` are mirrored pairs. After either side changes, update the other locale in the same change. + +When non-changelog pages change in either locale, sync the mirror before release. When the English changelog changes, sync the Chinese changelog. + +## Workflow + +1. **Detect what needs syncing** + + - `git diff main..HEAD --stat docs/` — see which files changed + - For each changed file under `docs/en/` or `docs/zh/`, locate its mirror in the other locale (same relative path). + +2. **Translate page by page, section by section** + + - Keep heading hierarchy, list structure, code blocks, callout blocks, and link targets identical between the two versions. + - When in doubt about a technical term, **read the actual code** to confirm behavior rather than guessing. + +3. **Apply terminology and typography rules from `docs/AGENTS.md`** + + - Use the term table exactly. Do not invent translations or use synonyms. + - English H2+ uses sentence case (proper nouns excepted, per the term table). + - Chinese typography: full-width punctuation (`,。;:?!()`), space between Chinese and ASCII (letters / numbers / inline code / links). + - Callout titles (`::: tip` / `::: warning` / `::: info` / `::: danger`) use the short Chinese labels from `docs/AGENTS.md`. + +4. **Verify** + + - `git diff docs/` — scan for terminology drift or punctuation regressions. + - Run the docs build if available (`pnpm --filter docs run build` or equivalent) to catch broken links and Markdown errors. + +## Rules and conventions + +- **Do not one-sided fixes**: if the changed locale has an unclear or incorrect statement, fix it there first; do not patch only the mirror. +- **Match style, not just words**: Chinese docs use a narrative tone (see `docs/AGENTS.md` writing-style examples); preserve that tone in Chinese; preserve sentence-case headings and concise English style in English. +- **Code blocks and identifiers stay as-is**: do not translate code, command names, flag names, or file paths. + +## Common mistakes + +- Rewriting only the mirror because a phrase feels awkward in the target language — fix the changed locale first, then sync. +- Letting English headings slip into Title Case (only sentence case is allowed for H2+). +- Forgetting to add spaces between Chinese characters and inline code or English words. +- Translating proper nouns listed in the term table (`Wire`, `MCP`, `ACP`, `JSON`, `OAuth`, `macOS`, `uv`, etc.). +- Updating only one direction and leaving the other locale stale — always finish all pages flagged by the diff. diff --git a/.changeset/README.md b/.changeset/README.md new file mode 100644 index 000000000..aed0865b2 --- /dev/null +++ b/.changeset/README.md @@ -0,0 +1,152 @@ +# Changesets + +This repository uses [changesets](https://github.com/changesets/changesets) to manage npm package versions and releases. + +## Package Publishing Strategy + +This repository uses an **independent, manually-selected publishing** strategy. When generating a changeset, only select the publishable packages that this change actually affects. The repository's `.changeset/config.json` already filters out internal workspace packages via `ignore`, so only the publishable packages listed below should appear in the `pnpm changeset` prompt. + +Current publishable packages: + +| Package | Directory | Description | +| --- | --- | --- | +| `@moonshot-ai/kimi-code` | `apps/kimi-code` | CLI / TUI application — provides the `kimi` command after install | +| `@moonshot-ai/kimi-code-sdk` | `packages/node-sdk` | Public TypeScript SDK | + +All other workspace packages are private internal packages, are not published to npm, and are excluded via `ignore` in `.changeset/config.json`: + +- `@moonshot-ai/agent-core` +- `@moonshot-ai/kimi-code-oauth` +- `@moonshot-ai/kimi-telemetry` +- `@moonshot-ai/kaos` +- `@moonshot-ai/kosong` +- `@moonshot-ai/vis` +- `@moonshot-ai/vis-server` +- `@moonshot-ai/vis-web` + +Version impact from internal dependencies must be judged manually. The published artifacts for CLI and SDK bundle internal workspace packages into the artifact itself; runtime `dependencies` of published packages must not include any `@moonshot-ai/*` internal workspace packages. + +The repository's `.changeset/config.json` sets `updateInternalDependencies: "patch"`. Because internal packages are not published, you still need to manually select all affected publishable packages in the changeset — do not rely solely on automatic dependency bumps to express user-visible changes. + +Example scenarios: + +| Change | Changeset selection | +| --- | --- | +| Only modifies TUI behavior in `@moonshot-ai/kimi-code` | Add `patch` / `minor` / `major` to `@moonshot-ai/kimi-code` | +| Only modifies internal packages, no user-visible change in SDK / CLI | Usually no changeset needed | +| Internal package fix changes the CLI user experience | Add a changeset to `@moonshot-ai/kimi-code` describing the user-visible fix | +| Internal package adds a new capability exposed by the SDK | Add a changeset to `@moonshot-ai/kimi-code-sdk` | +| SDK behavior change affects CLI user experience | Add changesets to both `@moonshot-ai/kimi-code-sdk` and `@moonshot-ai/kimi-code` | +| Provider abstraction change affects SDK / CLI | Add changesets to the affected `@moonshot-ai/kimi-code-sdk` and/or `@moonshot-ai/kimi-code` | +| Test-only, internal refactor, docs, or private debug tooling changes | Usually no changeset needed | + +## Prerequisite: NPM Trusted Publishing (OIDC) + +This repository uses npm's **Trusted Publishing** (OIDC-based) for publishing — no `NPM_TOKEN` is required. + +### Configuration steps + +1. Open each publishable package's page on the npm website, e.g. `https://www.npmjs.com/package/@moonshot-ai/kimi-code`. +2. Go to **Settings** -> **Publishing access**. +3. Find **Automate publishing with GitHub Actions** or **Add trusted publisher**. +4. Click **Add a new trusted publisher**. + +Fill in the following: + +| Field | Value | +| --- | --- | +| GitHub Organization | `MoonshotAI` | +| GitHub Repository | `kimi-code` | +| GitHub Workflow | `release.yml` | +| Environment | leave empty | + +Each publishable package needs its Trusted Publisher configured once. The current GitHub Actions workflow lives at `.github/workflows/release.yml` and already has `id-token: write` configured. + +## Development Workflow + +### 1. Implement the feature or fix + +Complete code, tests, and documentation changes as usual. A changeset is required when the change affects user-visible behavior, public API, dependency ranges, or release artifacts of a publishable package. + +### 2. Generate a changeset + +From the repository root: + +```sh +pnpm changeset +``` + +Follow the prompts to choose: + +- Which publishable packages this change affects; +- The version bump level: + - `patch`: bug fixes, small changes, follow-up dependency updates; + - `minor`: backward-compatible new features; + - `major`: breaking changes; +- A user-facing description of the change. + +The command creates a `.changeset/*.md` file that must be committed alongside the code. + +### 3. Commit the changeset + +```sh +git add .changeset/ +git commit -m "chore: add changeset for package release" +git push +``` + +Commit messages must follow Conventional Commit style. Do not include any author/agent identity in the commit message. + +### 4. CI generates the release PR + +Once the changeset file is merged into `main`, `.github/workflows/release.yml` uses `changesets/action@v1` to create or update a release PR. + +The release PR runs: + +- `pnpm changeset version`: bumps publishable package versions and updates changelogs; +- Deletes the consumed `.changeset/*.md` files; +- Uses the title `[CI]: Release packages`. + +### 5. Merge the release PR + +Once the release PR is merged into `main`, the same workflow runs: + +- `pnpm install --frozen-lockfile` +- `pnpm build` +- `pnpm changeset publish` + +The packages are then published via npm Trusted Publishing, and a GitHub Release is created. + +## Manual Publishing (Not Recommended) + +Only publish manually when CI is unavailable. Before publishing manually, make sure you are logged into npm locally and using the Node.js and pnpm versions required by the repository. + +```sh +pnpm run version +pnpm run publish +``` + +The underlying changesets commands are: + +```sh +pnpm changeset version +pnpm changeset publish +``` + +The root-level `pnpm run publish` first runs typecheck, lint, sherif, test, build, and package lint, then runs `changeset publish`. + +## Notes + +- Every PR that affects publishable-package behavior or public API should include a corresponding changeset. +- Changeset files must be committed to the repository — release PRs are only triggered after they're merged. +- Release PRs require human review and merge; they will not publish automatically. +- Do not add release changesets for private internal packages; only select `@moonshot-ai/kimi-code` and `@moonshot-ai/kimi-code-sdk`. +- If a change in an underlying internal package alters user-visible behavior or public API of a publishable package, add a changeset to the affected publishable package. For example, when a bug fixed in `@moonshot-ai/agent-core` resolves an issue CLI users encounter, add a changeset to `@moonshot-ai/kimi-code` describing the user-visible fix. +- `@moonshot-ai/kimi-code` is the official CLI package name; after a global install it provides the `kimi` command. +- Make sure each publishable package on npm has a Trusted Publisher configured. + +## References + +- [Changesets documentation](https://github.com/changesets/changesets) +- [Changesets GitHub Action](https://github.com/changesets/action) +- [npm Trusted Publishing documentation](https://docs.npmjs.com/trusted-publishers) diff --git a/.changeset/config.json b/.changeset/config.json new file mode 100644 index 000000000..af6143de9 --- /dev/null +++ b/.changeset/config.json @@ -0,0 +1,18 @@ +{ + "changelog": ["@changesets/changelog-github", { "repo": "MoonshotAI/kimi-code", "disableThanks": true }], + "commit": false, + "fixed": [], + "linked": [], + "access": "public", + "baseBranch": "main", + "updateInternalDependencies": "patch", + "ignore": [ + "@moonshot-ai/vis", + "@moonshot-ai/vis-server", + "@moonshot-ai/vis-web" + ], + "snapshot": { + "useCalculatedVersion": true, + "prereleaseTemplate": "{tag}-{commit}" + } +} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..4039ff111 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 2 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false diff --git a/.github/ISSUE_TEMPLATE/1-bug-report.yml b/.github/ISSUE_TEMPLATE/1-bug-report.yml new file mode 100644 index 000000000..d38aab205 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/1-bug-report.yml @@ -0,0 +1,65 @@ +name: Bug Report +description: Report an issue that should be fixed +labels: + - bug + - needs triage +body: + - type: markdown + attributes: + value: | + Thank you for submitting a bug report! It helps make Kimi Code better for everyone. + + Make sure you are running the latest version of Kimi Code CLI. The bug you are experiencing may already have been fixed. + + Please try to include as much information as possible. + + - type: input + id: version + attributes: + label: What version of Kimi Code is running? + description: Copy the output of `kimi --version` or `/version` + validations: + required: true + - type: input + id: plan + attributes: + label: Which open platform/subscription were you using? + description: The one you selected when running `/login` + validations: + required: true + - type: input + id: model + attributes: + label: Which model were you using? + description: The one you can see on the bottom status line, like `kimi-k2.6`, `kimi-for-coding`, etc. + - type: input + id: platform + attributes: + label: What platform is your computer? + description: | + For MacOS and Linux: copy the output of `uname -mprs` + For Windows: copy the output of `"$([Environment]::OSVersion | ForEach-Object VersionString) $(if ([Environment]::Is64BitOperatingSystem) { "x64" } else { "x86" })"` in the PowerShell console + - type: textarea + id: actual + attributes: + label: What issue are you seeing? + description: Please include the full error messages and prompts with any private information redacted. If possible, please provide text instead of a screenshot. + validations: + required: true + - type: textarea + id: steps + attributes: + label: What steps can reproduce the bug? + description: Explain the bug and provide a code snippet that can reproduce it. Please include session id and context usage if applicable. + validations: + required: true + - type: textarea + id: expected + attributes: + label: What is the expected behavior? + description: If possible, please provide text instead of a screenshot. + - type: textarea + id: notes + attributes: + label: Additional information + description: Is there anything else you think we should know? diff --git a/.github/ISSUE_TEMPLATE/2-feature-request.yml b/.github/ISSUE_TEMPLATE/2-feature-request.yml new file mode 100644 index 000000000..cbc055334 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/2-feature-request.yml @@ -0,0 +1,25 @@ +name: Feature Request +description: Propose a new feature for Kimi Code +labels: + - enhancement +body: + - type: markdown + attributes: + value: | + Is Kimi Code missing a feature that you'd like to see? Feel free to propose it here. + + Before you submit a feature: + 1. Search existing issues for similar features. If you find one, 👍 it rather than opening a new one. + 2. The Kimi Code team will try to balance the varying needs of the community when prioritizing or rejecting new features. Please understand that not all features will be accepted. + + - type: textarea + id: feature + attributes: + label: What feature would you like to see? + validations: + required: true + - type: textarea + id: notes + attributes: + label: Additional information + description: Is there anything else you think we should know? diff --git a/.github/actions/macos-keychain-cleanup/action.yml b/.github/actions/macos-keychain-cleanup/action.yml new file mode 100644 index 000000000..1cc377a1c --- /dev/null +++ b/.github/actions/macos-keychain-cleanup/action.yml @@ -0,0 +1,12 @@ +name: macOS keychain cleanup +description: Delete the temporary keychain created by macos-keychain-setup. + +runs: + using: composite + steps: + - name: Cleanup keychain + shell: bash + run: | + if [[ -n "${APPLE_KEYCHAIN_PATH:-}" && -f "${APPLE_KEYCHAIN_PATH}" ]]; then + security delete-keychain "$APPLE_KEYCHAIN_PATH" || true + fi diff --git a/.github/actions/macos-keychain-setup/action.yml b/.github/actions/macos-keychain-setup/action.yml new file mode 100644 index 000000000..199f8ed7f --- /dev/null +++ b/.github/actions/macos-keychain-setup/action.yml @@ -0,0 +1,82 @@ +name: macOS keychain setup +description: | + Create a temporary keychain, import a Developer ID Application certificate, + discover the signing identity, and export APPLE_SIGNING_IDENTITY + + APPLE_KEYCHAIN_PATH to GITHUB_ENV for subsequent steps. Must be paired + with macos-keychain-cleanup in an if: always() step at job end. + +inputs: + certificate-p12: + description: Base64-encoded P12 certificate + required: true + certificate-password: + description: P12 password + required: true + +runs: + using: composite + steps: + - name: Import signing certificate + shell: bash + env: + APPLE_CERTIFICATE_P12: ${{ inputs.certificate-p12 }} + APPLE_CERTIFICATE_PASSWORD: ${{ inputs.certificate-password }} + KEYCHAIN_PASSWORD: actions + run: | + set -euo pipefail + + # 0. Validate inputs early — base64 decoding "" gives 0-byte .p12 → security import reports + # "SecKeychainItemImport: One or more parameters passed to a function were not valid" + # which is cryptic. Fail loudly here instead. + if [ -z "$APPLE_CERTIFICATE_P12" ]; then + echo "::error::APPLE_CERTIFICATE_P12 secret is empty. Configure it in repo settings, or pass sign-macos=false." + exit 1 + fi + if [ -z "$APPLE_CERTIFICATE_PASSWORD" ]; then + echo "::error::APPLE_CERTIFICATE_PASSWORD secret is empty." + exit 1 + fi + + # 1. Decode certificate + cert_path="${RUNNER_TEMP}/certificate.p12" + printf '%s' "$APPLE_CERTIFICATE_P12" | base64 -d > "$cert_path" + if [ ! -s "$cert_path" ]; then + echo "::error::Decoded certificate is empty. APPLE_CERTIFICATE_P12 secret value may not be valid base64." + exit 1 + fi + + # 2. Create temporary keychain (don't pollute runner default keychain) + keychain_path="${RUNNER_TEMP}/signing.keychain-db" + security create-keychain -p "$KEYCHAIN_PASSWORD" "$keychain_path" + security set-keychain-settings -lut 21600 "$keychain_path" + security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$keychain_path" + + # 3. Add to user keychain search list and set default + security list-keychains -d user -s "$keychain_path" $(security list-keychains -d user | tr -d '"') + security default-keychain -s "$keychain_path" + + # 4. Import cert, authorize codesign + security tools + security import "$cert_path" -k "$keychain_path" \ + -P "$APPLE_CERTIFICATE_PASSWORD" \ + -T /usr/bin/codesign -T /usr/bin/security + + # 5. Non-interactive private key access + security set-key-partition-list -S apple-tool:,apple: \ + -s -k "$KEYCHAIN_PASSWORD" "$keychain_path" > /dev/null + + # 6. Discover identity (don't hardcode team ID) + IDENTITY=$(security find-identity -v -p codesigning "$keychain_path" \ + | grep "Developer ID Application" | head -1 \ + | sed -n 's/.*"\(Developer ID Application[^"]*\)".*/\1/p') + + if [[ -z "$IDENTITY" ]]; then + echo "::error::No Developer ID Application identity found in keychain" + security find-identity -v -p codesigning "$keychain_path" + exit 1 + fi + + echo "Found signing identity: $IDENTITY" + echo "APPLE_SIGNING_IDENTITY=$IDENTITY" >> "$GITHUB_ENV" + echo "APPLE_KEYCHAIN_PATH=$keychain_path" >> "$GITHUB_ENV" + + rm -f "$cert_path" diff --git a/.github/actions/macos-notarize/action.yml b/.github/actions/macos-notarize/action.yml new file mode 100644 index 000000000..9a919d810 --- /dev/null +++ b/.github/actions/macos-notarize/action.yml @@ -0,0 +1,95 @@ +name: macOS notarize +description: | + Submit a signed binary to Apple notary service via notarytool, wait for + result, and run Gatekeeper online check (spctl). Fails the job with the + notarytool log on rejection. + +inputs: + binary-path: + description: Absolute path to the signed binary + required: true + notarization-key-p8: + description: Base64-encoded App Store Connect API Key (.p8) + required: true + notarization-key-id: + description: API Key ID + required: true + notarization-issuer-id: + description: Issuer ID + required: true + timeout: + description: notarytool wait timeout (e.g. 15m) + required: false + default: '15m' + +runs: + using: composite + steps: + - name: Submit to notary service + shell: bash + env: + BINARY_PATH: ${{ inputs.binary-path }} + APPLE_NOTARIZATION_KEY_P8: ${{ inputs.notarization-key-p8 }} + APPLE_NOTARIZATION_KEY_ID: ${{ inputs.notarization-key-id }} + APPLE_NOTARIZATION_ISSUER_ID: ${{ inputs.notarization-issuer-id }} + NOTARY_TIMEOUT: ${{ inputs.timeout }} + run: | + set -euo pipefail + + if [[ ! -f "$BINARY_PATH" ]]; then + echo "::error::Binary not found at $BINARY_PATH" + exit 1 + fi + + # 1. Decode API key + key_path="${RUNNER_TEMP}/AuthKey.p8" + printf '%s' "$APPLE_NOTARIZATION_KEY_P8" | base64 -d > "$key_path" + + # 2. Pack with ditto (--norsrc avoids ._AppleDouble files) + binary_name=$(basename "$BINARY_PATH") + zip_path="${RUNNER_TEMP}/${binary_name}.notarize.zip" + ditto -c -k --norsrc --keepParent "$BINARY_PATH" "$zip_path" + + echo "==> Submitting $binary_name for notarization..." + + # 3. Submit and capture log + log_path="${RUNNER_TEMP}/notarize-${binary_name}.log" + set +e + xcrun notarytool submit "$zip_path" \ + --key "$key_path" \ + --key-id "$APPLE_NOTARIZATION_KEY_ID" \ + --issuer "$APPLE_NOTARIZATION_ISSUER_ID" \ + --wait --timeout "$NOTARY_TIMEOUT" 2>&1 | tee "$log_path" + notarize_rc=${PIPESTATUS[0]} + set -e + + # 4. Inspect status + if ! grep -q "status: Accepted" "$log_path"; then + echo "::error::Notarization failed (exit=$notarize_rc)" + submission_id=$(grep -m1 "id:" "$log_path" | awk '{print $2}' || true) + if [[ -n "$submission_id" ]]; then + echo "Fetching detailed log for submission $submission_id..." + xcrun notarytool log "$submission_id" \ + --key "$key_path" \ + --key-id "$APPLE_NOTARIZATION_KEY_ID" \ + --issuer "$APPLE_NOTARIZATION_ISSUER_ID" || true + fi + rm -f "$key_path" "$zip_path" + exit 1 + fi + + echo "==> Notarization accepted" + + # 5. Cleanup + rm -f "$key_path" "$zip_path" + + - name: Verify with Gatekeeper online check + shell: bash + env: + BINARY_PATH: ${{ inputs.binary-path }} + run: | + set -euo pipefail + echo "==> codesign -dv $BINARY_PATH" + codesign -dv --verbose=2 "$BINARY_PATH" + echo "==> spctl -a -vvv -t install $BINARY_PATH" + spctl -a -vvv -t install "$BINARY_PATH" diff --git a/.github/pr-title-checker-config.json b/.github/pr-title-checker-config.json new file mode 100644 index 000000000..c1000ebb4 --- /dev/null +++ b/.github/pr-title-checker-config.json @@ -0,0 +1,12 @@ +{ + "LABEL": { + "name": "Invalid PR Title", + "color": "B60205" + }, + "CHECKS": { + "regexp": "^(feat|fix|test|refactor|chore|style|docs|perf|build|ci|revert)(\\(.*\\))?:.*" + }, + "MESSAGES": { + "failure": "The PR title is invalid. Please refer to https://www.conventionalcommits.org/en/v1.0.0/ for the convention." + } +} diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..020e01f04 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,25 @@ + + +## Related Issue + + + +Resolve #(issue_number) + +## Description + + + +## Checklist + +- [ ] I have read the [CONTRIBUTING](https://github.com/MoonshotAI/kimi-code/blob/main/CONTRIBUTING.md) document. +- [ ] I have linked the related issue, if any. +- [ ] I have added tests that prove my fix is effective or that my feature works. +- [ ] Ran `gen-changesets` skill, or this PR needs no changeset. +- [ ] Ran `gen-docs` skill, or this PR needs no doc update. diff --git a/.github/workflows/_native-build.yml b/.github/workflows/_native-build.yml new file mode 100644 index 000000000..439d23544 --- /dev/null +++ b/.github/workflows/_native-build.yml @@ -0,0 +1,115 @@ +name: _native-build + +on: + workflow_call: + inputs: + upload-artifact-prefix: + description: 'Prefix for uploaded artifact name' + required: false + type: string + default: 'kimi-code-native' + retention-days: + description: 'Artifact retention in days' + required: false + type: number + default: 3 + sign-macos: + description: 'Whether to sign + notarize macOS builds (requires Apple secrets)' + required: false + type: boolean + default: false + secrets: + APPLE_CERTIFICATE_P12: + required: false + APPLE_CERTIFICATE_PASSWORD: + required: false + APPLE_NOTARIZATION_KEY_P8: + required: false + APPLE_NOTARIZATION_KEY_ID: + required: false + APPLE_NOTARIZATION_ISSUER_ID: + required: false + +permissions: + contents: read + +jobs: + native-bundle: + name: Native bundle (${{ matrix.target }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-24.04 + target: linux-x64 + - os: ubuntu-24.04-arm + target: linux-arm64 + - os: macos-15-intel + target: darwin-x64 + - os: macos-15 + target: darwin-arm64 + - os: windows-2025-vs2026 + target: win32-x64 + - os: windows-11-arm + target: win32-arm64 + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup pnpm + uses: pnpm/action-setup@v6 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version-file: .nvmrc + cache: 'pnpm' + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Setup macOS keychain (release only) + if: runner.os == 'macOS' && inputs.sign-macos + uses: ./.github/actions/macos-keychain-setup + with: + certificate-p12: ${{ secrets.APPLE_CERTIFICATE_P12 }} + certificate-password: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + + - name: Build native executable (release profile, macOS signed) + if: runner.os == 'macOS' && inputs.sign-macos + run: pnpm --filter @moonshot-ai/kimi-code run build:native:release + + - name: Build native executable (local profile) + if: '!(runner.os == ''macOS'' && inputs.sign-macos)' + run: pnpm --filter @moonshot-ai/kimi-code run build:native:sea + + - name: Notarize macOS binary + if: runner.os == 'macOS' && inputs.sign-macos + uses: ./.github/actions/macos-notarize + with: + binary-path: ${{ github.workspace }}/apps/kimi-code/dist-native/bin/${{ matrix.target }}/kimi + notarization-key-p8: ${{ secrets.APPLE_NOTARIZATION_KEY_P8 }} + notarization-key-id: ${{ secrets.APPLE_NOTARIZATION_KEY_ID }} + notarization-issuer-id: ${{ secrets.APPLE_NOTARIZATION_ISSUER_ID }} + + - name: Cleanup macOS keychain + if: always() && runner.os == 'macOS' && inputs.sign-macos + uses: ./.github/actions/macos-keychain-cleanup + + - name: Smoke test native executable + run: pnpm --filter @moonshot-ai/kimi-code run test:native:smoke + + - name: Package native artifact + run: pnpm --filter @moonshot-ai/kimi-code run package:native + + - name: Upload native artifact + uses: actions/upload-artifact@v7 + with: + name: ${{ inputs.upload-artifact-prefix }}-${{ matrix.target }} + retention-days: ${{ inputs.retention-days }} + path: | + apps/kimi-code/dist-native/artifacts/kimi-code-${{ matrix.target }}.zip + apps/kimi-code/dist-native/artifacts/kimi-code-${{ matrix.target }}.zip.sha256 + if-no-files-found: ignore diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..c7ceac2ad --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,84 @@ +name: CI + +on: + pull_request: + push: + branches: + - main + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v6 + + - uses: actions/setup-node@v6 + with: + node-version-file: .nvmrc + cache: pnpm + + - run: pnpm install --frozen-lockfile + - run: pnpm run build + - name: Smoke test CLI bundle + run: pnpm -C apps/kimi-code run smoke + + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v6 + + - uses: actions/setup-node@v6 + with: + node-version-file: .nvmrc + cache: pnpm + + - run: pnpm install --frozen-lockfile + - run: pnpm run test + + lint: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v6 + + - uses: actions/setup-node@v6 + with: + node-version-file: .nvmrc + cache: pnpm + + - run: pnpm install --frozen-lockfile + - run: pnpm run lint + - run: pnpm run sherif + + typecheck: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v6 + + - uses: actions/setup-node@v6 + with: + node-version-file: .nvmrc + cache: pnpm + + - run: pnpm install --frozen-lockfile + - name: Typecheck + run: | + pnpm dlx --package @typescript/native-preview@beta tsgo --version + for config in packages/*/tsconfig.json apps/kimi-code/tsconfig.json; do + echo "Typechecking ${config}" + pnpm dlx --package @typescript/native-preview@beta tsgo -p "${config}" --noEmit + done diff --git a/.github/workflows/docs-deploy.yml b/.github/workflows/docs-deploy.yml new file mode 100644 index 000000000..1d54f305b --- /dev/null +++ b/.github/workflows/docs-deploy.yml @@ -0,0 +1,60 @@ +name: Deploy Docs to GitHub Pages + +on: + push: + branches: + - main + paths: + - 'docs/**' + - '.github/workflows/docs-deploy.yml' + + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - uses: pnpm/action-setup@v6 + + - uses: actions/setup-node@v6 + with: + node-version-file: .nvmrc + cache: pnpm + + - run: pnpm install --frozen-lockfile + + - name: Install docs dependencies + run: pnpm --ignore-workspace -C docs install + + - name: Build docs + run: pnpm -C docs run build + env: + VITEPRESS_BASE: /${{ github.event.repository.name }}/ + + - name: Upload artifact + uses: actions/upload-pages-artifact@v5 + with: + path: docs/.vitepress/dist + + deploy: + if: github.ref == 'refs/heads/main' + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v5 diff --git a/.github/workflows/native-bundle.yml b/.github/workflows/native-bundle.yml new file mode 100644 index 000000000..c9aa010c5 --- /dev/null +++ b/.github/workflows/native-bundle.yml @@ -0,0 +1,21 @@ +name: Native Bundle + +on: + workflow_dispatch: + +permissions: + contents: read + +jobs: + build: + uses: ./.github/workflows/_native-build.yml + with: + upload-artifact-prefix: kimi-code-native + retention-days: 3 + sign-macos: true + secrets: + APPLE_CERTIFICATE_P12: ${{ secrets.APPLE_CERTIFICATE_P12 }} + APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + APPLE_NOTARIZATION_KEY_P8: ${{ secrets.APPLE_NOTARIZATION_KEY_P8 }} + APPLE_NOTARIZATION_KEY_ID: ${{ secrets.APPLE_NOTARIZATION_KEY_ID }} + APPLE_NOTARIZATION_ISSUER_ID: ${{ secrets.APPLE_NOTARIZATION_ISSUER_ID }} diff --git a/.github/workflows/pr-title-checker.yml b/.github/workflows/pr-title-checker.yml new file mode 100644 index 000000000..00bfdf20f --- /dev/null +++ b/.github/workflows/pr-title-checker.yml @@ -0,0 +1,15 @@ +name: PR Title Checker + +on: + pull_request: + types: [opened, edited, labeled] + +jobs: + check: + runs-on: ubuntu-latest + name: pr-title-checker + steps: + - uses: thehanimo/pr-title-checker@v1.4.3 + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + configuration_path: ".github/pr-title-checker-config.json" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..fc0b9a8bb --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,112 @@ +name: Release + +on: + push: + branches: + - main + +concurrency: ${{ github.workflow }}-${{ github.ref }} + +jobs: + release: + name: Release + runs-on: ubuntu-latest + if: github.repository_owner == 'MoonshotAI' + outputs: + kimi_native_release: ${{ steps.kimi-release.outputs.should_publish }} + kimi_release_tag: ${{ steps.kimi-release.outputs.tag }} + permissions: + contents: write + pull-requests: write + id-token: write # Required for NPM Trusted Publishing (OIDC) + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Setup pnpm + uses: pnpm/action-setup@v6 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version-file: .nvmrc + cache: 'pnpm' + registry-url: 'https://registry.npmjs.org' + + - name: Upgrade npm for Trusted Publishing + run: npm install -g npm@latest + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build packages + run: pnpm build + + - name: Create Release Pull Request or Publish to npm + id: changesets + uses: changesets/action@v1 + with: + publish: pnpm changeset publish + version: pnpm changeset version + commit: 'ci: release packages' + title: 'ci: release packages' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # NPM_TOKEN is not needed with Trusted Publishing (OIDC) + + - name: Resolve Kimi Code native release + if: steps.changesets.outputs.published == 'true' + id: kimi-release + run: node apps/kimi-code/scripts/native/resolve-release.mjs + env: + CHANGESETS_PUBLISHED_PACKAGES: ${{ steps.changesets.outputs.publishedPackages }} + + native-artifacts: + name: Native release artifact + needs: release + if: needs.release.outputs.kimi_native_release == 'true' + uses: ./.github/workflows/_native-build.yml + with: + upload-artifact-prefix: kimi-code-native + retention-days: 7 + sign-macos: true + secrets: + APPLE_CERTIFICATE_P12: ${{ secrets.APPLE_CERTIFICATE_P12 }} + APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + APPLE_NOTARIZATION_KEY_P8: ${{ secrets.APPLE_NOTARIZATION_KEY_P8 }} + APPLE_NOTARIZATION_KEY_ID: ${{ secrets.APPLE_NOTARIZATION_KEY_ID }} + APPLE_NOTARIZATION_ISSUER_ID: ${{ secrets.APPLE_NOTARIZATION_ISSUER_ID }} + + publish-native-assets: + name: Publish native release assets + needs: + - release + - native-artifacts + if: needs.release.outputs.kimi_native_release == 'true' + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Download native artifacts + uses: actions/download-artifact@v8 + with: + pattern: kimi-code-native-* + path: dist-native-release + merge-multiple: true + + - name: Produce manifest.json + env: + RELEASE_TAG: ${{ needs.release.outputs.kimi_release_tag }} + run: node apps/kimi-code/scripts/native/produce-manifest.mjs dist-native-release "$RELEASE_TAG" + + - name: Upload assets to GitHub Release + env: + GH_TOKEN: ${{ github.token }} + RELEASE_TAG: ${{ needs.release.outputs.kimi_release_tag }} + run: gh release upload "$RELEASE_TAG" dist-native-release/* --clobber diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..13b316428 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +node_modules/ +dist/ +dist-native/ +.tmp-api-extractor/ +coverage/ +*.tsbuildinfo +.vitest-results/ +.DS_Store +.playwright-mcp/ +.claude +.conductor +.kimi-stash-dir +superpowers/ diff --git a/.npmrc b/.npmrc new file mode 100644 index 000000000..fd6158e56 --- /dev/null +++ b/.npmrc @@ -0,0 +1,3 @@ +auto-install-peers=true +engine-strict=true +strict-peer-dependencies=false diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 000000000..5bf4400f2 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +24.15.0 diff --git a/.oxfmtrc.json b/.oxfmtrc.json new file mode 100644 index 000000000..493d050e2 --- /dev/null +++ b/.oxfmtrc.json @@ -0,0 +1,19 @@ +{ + "$schema": "./node_modules/oxfmt/configuration_schema.json", + "printWidth": 100, + "tabWidth": 2, + "useTabs": false, + "semi": true, + "singleQuote": true, + "trailingComma": "all", + "bracketSpacing": true, + "arrowParens": "always", + "endOfLine": "lf", + "sortImports": { + "groups": ["builtin", "external", "internal", ["parent", "sibling", "index"], "unknown"], + "newlinesBetween": true, + "order": "asc" + }, + "sortPackageJson": true, + "ignorePatterns": ["dist/", "coverage/", "pnpm-lock.yaml", "*.generated.ts"] +} diff --git a/.oxlintrc.json b/.oxlintrc.json new file mode 100644 index 000000000..877553f49 --- /dev/null +++ b/.oxlintrc.json @@ -0,0 +1,155 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "categories": { + "correctness": "error", + "suspicious": "warn", + "pedantic": "warn", + "perf": "warn" + }, + "plugins": ["typescript", "import", "unicorn", "promise", "node"], + "rules": { + "eslint/no-useless-return": "off", + "eslint/no-shadow": "off", + "eslint/eqeqeq": "error", + "eslint/no-throw-literal": "off", + "eslint/no-control-regex": "off", + "typescript/no-misused-promises": "error", + "typescript/return-await": "error", + "typescript/only-throw-error": "error", + "import/no-cycle": "error", + "import/no-self-import": "error", + "import/no-unassigned-import": "off", + "unicorn/prefer-node-protocol": "error", + "unicorn/no-useless-undefined": "off", + "unicorn/no-lonely-if": "off", + "unicorn/no-array-callback-reference": "off", + "typescript/prefer-promise-reject-errors": "off", + "typescript/no-unsafe-function-type": "off", + "typescript/no-unsafe-argument": "off", + "typescript/no-unsafe-assignment": "off", + "typescript/no-unsafe-call": "off", + "typescript/no-unsafe-member-access": "off", + "typescript/no-unsafe-return": "off", + + "eslint/no-console": "warn", + "typescript/no-explicit-any": "off", + "typescript/no-non-null-assertion": "off", + "typescript/use-unknown-in-catch-callback-variable": "off", + "typescript/consistent-type-imports": "off", + "typescript/ban-types": "off", + "import/first": "warn", + "import/extensions": [ + "error", + "ignorePackages", + { + "js": "never", + "ts": "never", + "tsx": "never" + } + ], + "import/no-duplicates": "warn", + "import/no-mutable-exports": "warn", + "unicorn/error-message": "warn", + "unicorn/throw-new-error": "warn", + "unicorn/catch-error-name": "warn", + "unicorn/prefer-add-event-listener": "off", + "node/no-new-require": "warn", + "node/no-path-concat": "warn", + + "promise/no-callback-in-promise": "warn", + "promise/valid-params": "warn", + "promise/no-return-in-finally": "warn", + "promise/always-return": "off", + + "eslint/max-classes-per-file": "off", + "eslint/max-depth": "off", + "eslint/max-lines": "off", + "eslint/max-lines-per-function": "off", + "eslint/max-nested-callbacks": "off", + "eslint/no-warning-comments": "off", + "eslint/no-inline-comments": "off", + "eslint/no-negated-condition": "off", + "eslint/no-await-in-loop": "off", + "eslint/require-await": "off", + "typescript/require-await": "off", + "eslint/sort-vars": "off", + "eslint/radix": "off", + "eslint/symbol-description": "off", + "typescript/consistent-return": "off", + "unicorn/consistent-function-scoping": "off", + "typescript/no-deprecated": "off", + "typescript/no-unsafe-type-assertion": "off", + "typescript/strict-boolean-expressions": "off", + "typescript/no-duplicate-type-constituents": "off", + "import/max-dependencies": "off" + }, + "overrides": [ + { + "files": ["**/examples/**/*.ts", "**/examples/**/*.tsx"], + "rules": { + "eslint/no-console": "off" + } + }, + { + "files": ["packages/kosong/src/providers/**/*.ts"], + "rules": { + "typescript/no-unsafe-argument": "off", + "typescript/no-unsafe-assignment": "off", + "typescript/no-unsafe-call": "off", + "typescript/no-unsafe-member-access": "off", + "typescript/no-unsafe-return": "off" + } + }, + { + "files": [ + "**/*.test.ts", + "**/*.test.tsx", + "**/*.spec.ts", + "**/*.spec.tsx", + "**/test/**/*.ts", + "**/test/**/*.tsx" + ], + "plugins": ["vitest"], + "rules": { + "typescript/no-explicit-any": "off", + "typescript/no-unsafe-assignment": "off", + "typescript/no-unsafe-argument": "off", + "typescript/no-non-null-assertion": "off", + "typescript/unbound-method": "off", + "typescript/no-redundant-type-constituents": "off", + "eslint/no-console": "off", + "eslint/no-promise-executor-return": "off", + "typescript/no-unsafe-member-access": "off", + "vitest/require-mock-type-parameters": "off", + "eslint/no-shadow": "off", + "unicorn/prefer-event-target": "off", + "jest/no-disabled-tests": "off", + "vitest/no-disabled-tests": "off", + "eslint/no-control-regex": "off", + "eslint/no-unused-vars": "off", + "eslint/no-unsafe-optional-chaining": "off", + "jest/valid-expect": "off", + "unicorn/no-useless-spread": "off", + "import/no-cycle": "off", + "typescript/no-meaningless-void-operator": "off", + "typescript/require-array-sort-compare": "off", + "vitest/expect-expect": "error", + "vitest/no-conditional-tests": "error", + "vitest/no-focused-tests": "error", + "vitest/no-identical-title": "error", + "vitest/valid-expect": "off", + "vitest/valid-describe-callback": "error", + "vitest/no-standalone-expect": "error", + "vitest/warn-todo": "off" + } + } + ], + "ignorePatterns": [ + "dist/", + "coverage/", + "node_modules/", + "apps/*/scripts/", + "docs/smoke-archive/", + "*.generated.ts" + ] +} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..ddb2cea34 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,60 @@ +# Repository-level Agent Guide + +Reply in the same language as the user. + +This is a TypeScript monorepo built for agent-assisted development. Keep the root `AGENTS.md` limited to hot-path rules: the project map, hard constraints, and workflow requirements — things every task needs to know. + +## Working Principles + +- Think from first principles. Start from real requirements, code facts, and verification results; if the goal is unclear, discuss it with the user first. +- Treat code, not documentation, as the source of truth. Unless the user explicitly says otherwise, do not read ordinary Markdown just to understand the implementation. +- Before making code changes, read the relevant code and the most recent constraints, and follow the nearest `AGENTS.md` in the directory tree. +- Keep changes focused. Do not slip in unrelated refactors along the way. +- When committing, do not add any co-author attribution, and do not reveal the identity of the agent in commit messages, PR descriptions, or any explanatory text. + +## Project Map + +- `apps/kimi-code`: the CLI / TUI application. It consumes core capabilities through `@moonshot-ai/kimi-code-sdk` and must not depend directly on `@moonshot-ai/agent-core`. +- `apps/vis`, `apps/vis/server`, `apps/vis/web`: visual debugging tools for sessions and replays. +- `packages/agent-core`: the unified agent engine, including Agent, Session, profile, skills, tools, plan, permission, background, records, and other core capabilities. +- `packages/node-sdk`: the public TypeScript SDK and harness. +- `packages/kosong`: the LLM / provider abstraction layer. +- `packages/kaos`: the execution environment and file/process abstractions. +- `packages/oauth`: Kimi OAuth and managed auth utilities. +- `packages/telemetry`: shared client-side telemetry infrastructure. + +## Environment Requirements + +- **Node.js**: `>=24.15.0` (from the root `package.json` `engines`; `.nvmrc` is `24.15.0`, used by nvm / fnm / mise to pick the minimum recommended version). +- **pnpm**: `10.33.0` (from the root `package.json` `packageManager`). +- `pnpm install` will fail when the Node version is not satisfied, because `.npmrc` sets `engine-strict=true`. + +## General Coding Rules + +- For optional object properties, pass `undefined` directly instead of using conditional spread. + - YES: `{ user }` + - NO: `{ ...(user ? { user } : undefined) }` +- Optional object properties do not need to additionally allow `undefined` in the type. + - YES: `interface Options { user?: User }` + - NO: `interface Options { user?: User | undefined }` +- Internal methods with only a single parameter should not be turned into options objects just for stylistic uniformity. +- Except for a package's `index.ts`, other `index.ts` files should prefer `export * from './module';`. +- The `Agent` class in `packages/agent-core/src/agent` must be usable on its own. The constructor must not force the caller to create a `Session` instance, nor require an `agentId` or `session`. It may accept an optional `sessionId` as a request-config hint — for example mapped to the provider's `prompt_cache_key` — but the instance must not hold `sessionId`, and must not depend on the Session lifecycle, metadata, or parent/child relationship logic. +- Do not add too many new test files. Prefer adding tests to the existing test file of the corresponding component or module. +- When a test fails because of a user modification, default to fixing the test first; do not change the implementation to satisfy an old test unless the implementation truly has a bug. +- Do not sacrifice code quality for external compatibility unless the user explicitly asks for it. Breaking changes go through changesets and a `major` bump, gated by the rule below. + +## Where to Update Instructions + +- Hard rules that affect almost every task: update the root `AGENTS.md`. +- Rules that only affect a specific directory: update the nearest sub-directory `AGENTS.md`. +- Keep instruction updates focused and supported by code facts. + +## Workflow Requirements + +- Prefer `rg` / `rg --files` when reading code. +- When designing changes, follow existing boundaries and local patterns first. +- When creating a PR, the PR title must follow Conventional Commit style, e.g. `chore: remove legacy format commands`. +- After finishing a task and before submitting a PR, you must run the `gen-changesets` skill (see `.agents/skills/gen-changesets/SKILL.md`) and generate a changeset under `.changeset/` according to its rules. +- When generating a changeset, **never** decide on a `major` bump on your own. When you judge a change to meet the major criteria (breaking changes, incompatible user configuration, renamed or removed commands/arguments, changed behavior semantics, etc.), you must stop and explain it to the user and ask for confirmation. **Only write `major` after the user has explicitly agreed.** Otherwise default to `minor` (and fall back to `patch` if `minor` is unclear). See the "Hard rule: confirm with the user before writing `major`" section in `.agents/skills/gen-changesets/SKILL.md` for details. +- Prefer importing via `import ... from '#/...'`, which serves the same purpose as `import ... from '@/...'`. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..ac49526de --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,96 @@ +# Contributing to kimi-code + +Thanks for taking the time to contribute! This project moves quickly, and thoughtful contributions from the community are what keep it sharp. The guide below walks you through how we work so your PR has the best chance of landing smoothly. + +## Before You Start + +- Open an issue or discussion before making changes larger than ~100 lines so we can align on direction early. +- We only merge PRs that are aligned with the roadmap — drive-by refactors without context are unlikely to land. +- Code quality bar: as good as code written by a strong human engineer or a competent coding agent. We hold AI-assisted contributions to the same standard as hand-written ones. + +## Project Layout + +This is a pnpm monorepo. The most relevant entry points are: + +- `apps/kimi-code` — CLI / TUI +- `apps/vis` — session replay & debugging visualizer +- `packages/node-sdk` — public TypeScript SDK (`@moonshot-ai/kimi-code-sdk`) +- `packages/agent-core`, `kosong`, `kaos`, `oauth`, `telemetry` — internal engine packages +- `docs/` — VitePress bilingual docs site + +For the full project map, see [AGENTS.md](AGENTS.md). + +## Development Setup + +Prerequisites: Node.js >= 24.15.0, pnpm 10.33.0, Git. + +```sh +git clone https://github.com/MoonshotAI/kimi-code.git +cd kimi-code +pnpm install +``` + +Useful scripts: + +- `pnpm dev:cli` — run the CLI in dev mode +- `pnpm test` — run tests (vitest) +- `pnpm typecheck` — TypeScript check (note: builds packages first) +- `pnpm lint` — oxlint +- `pnpm lint:fix` — oxlint with auto-fix +- `pnpm build` — build all packages + +## Commit Convention + +All commits and PR titles must follow [Conventional Commits](https://www.conventionalcommits.org/). + +| Type | Use for | Example | +|----------|---------------------------------------------|-------------------------------------------| +| feat | A new feature | feat(agent-core): add tool dedup | +| fix | A bug fix | fix(tui): correct status bar alignment | +| docs | Documentation only | docs: clarify install instructions | +| chore | Tooling / housekeeping | chore: bump dependencies | +| refactor | Internal refactor without behavior change | refactor(kosong): extract retry helper | +| test | Adding or improving tests | test(agent-core): cover skill resolver | +| ci | CI / build pipeline changes | ci: cache pnpm store | +| build | Build system / artifact changes | build(native): add win32-arm64 target | +| perf | Performance improvement | perf(session): batch event flushes | +| style | Formatting only (no logic) | style: apply oxlint --fix | + +PR titles are enforced by the `pr-title-checker` workflow — a non-conforming title will block merge. + +## Changesets + +This repo uses [changesets](https://github.com/changesets/changesets) to manage versioning and releases. + +- Every PR that affects release artifacts (code, behavior, public API) **must** include a changeset. +- Docs-only, test-only, or CI-only PRs may skip changesets. +- Generate one with `pnpm changeset` and follow the prompts (which packages are touched, which bump level). +- For repo-specific conventions, see `.changeset/README.md`. + +## Pull Request Checklist + +Before requesting review, make sure your PR ticks the following: + +- [ ] Linked an issue (for non-trivial changes) +- [ ] PR title follows Conventional Commits +- [ ] Added or updated tests +- [ ] Added a changeset if the PR affects release artifacts +- [ ] Ran `pnpm lint && pnpm typecheck && pnpm test` locally +- [ ] Updated user-facing docs in `docs/` if behavior changed + +The `.github/pull_request_template.md` checklist is a shorter subset of this — both must pass. + +## Code Style + +- TypeScript across the codebase. +- Linting via `oxlint` (config in `.oxlintrc.json`). +- Auto-formatting via `pnpm lint:fix`. +- Follow existing local patterns when the lint rules do not cover a style choice. + +## Reporting Security Issues + +Found a security issue? Please see [SECURITY.md](SECURITY.md) instead of opening a public issue. + +## License + +By contributing to this repository, you agree that your contributions will be licensed under the [MIT License](LICENSE). diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..6102a1bc7 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Moonshot AI + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 000000000..d5889efa2 --- /dev/null +++ b/Makefile @@ -0,0 +1,67 @@ +.PHONY: prepare build typecheck lint lint-fix lint-pkg sherif test test-watch test-coverage clean changeset version publish release dev vis + +## Setup + +prepare: + pnpm install + +## Build + +build: + pnpm run build + +## Quality + +typecheck: + pnpm run typecheck + +lint: + pnpm run lint + +lint-fix: + pnpm run lint:fix + +sherif: + pnpm run sherif + +lint-pkg: + pnpm run lint:pkg + +## Test + +test: + pnpm run test + +test-watch: + pnpm run test:watch + +test-coverage: + pnpm run test:coverage + +## Clean + +clean: + pnpm run clean + +## Release + +changeset: + pnpm run changeset + +version: + pnpm run version + +publish: + pnpm run publish + +release: version publish + +## Development + +dev: + pnpm run dev:cli + +## vis + +vis: + pnpm run vis diff --git a/README.md b/README.md new file mode 100644 index 000000000..501bf8199 --- /dev/null +++ b/README.md @@ -0,0 +1,100 @@ +# Kimi Code CLI + +[![License](https://img.shields.io/badge/license-MIT-blue)](LICENSE) [![Docs](https://img.shields.io/badge/docs-online-blue)](https://moonshotai.github.io/kimi-code/en/)
+[Documentation](https://moonshotai.github.io/kimi-code/en/) · [Issues](https://github.com/MoonshotAI/kimi-code/issues) · [中文](README.zh-CN.md) + +![Demo of using Kimi Code](./docs/media/intro.gif) + +## What is Kimi Code CLI + +Kimi Code CLI is an AI coding agent that runs in your terminal — it can read and edit code, run shell commands, search files, fetch web pages, and choose the next step based on the feedback it receives. It works out of the box with Moonshot AI’s Kimi models and can also be configured to use other compatible providers. + +## Install + +Install with the official script. No Node.js required. + +- **macOS or Linux**: + +```sh +curl -fsSL https://code.kimi.com/kimi-code/install.sh | bash +``` + +- **Windows (PowerShell)**: + +```powershell +irm https://code.kimi.com/kimi-code/install.ps1 | iex +``` + +Then, run it with a new shell session: + +```sh +kimi --version +``` + +For npm install, upgrade, uninstall, see [Getting Started](https://moonshotai.github.io/kimi-code/en/guides/getting-started). + +## Quick Start + +Open a project and start the interactive UI: + +```sh +cd your-project +kimi +``` + +On first launch, run `/login` inside Kimi Code CLI and choose either Kimi Code OAuth or a Moonshot AI Open Platform API key. After login, try your first task: + +``` +Take a look at this project and explain its main directories. +``` + +## Key Features + +- **Single-binary distribution.** Install with one command: no Node.js setup, PATH gymnastics, or global module conflicts. +- **Blazing-fast startup.** The TUI is ready in milliseconds, so starting a session never feels heavy. +- **Purpose-built TUI.** A carefully tuned interface for long, focused agent sessions. +- **Video input.** Drop a screen recording or demo clip into the chat, and let the agent watch what is hard to describe in words. +- **AI-native MCP configuration.** Add, edit, and authenticate Model Context Protocol servers conversationally with `/mcp-config`, without hand-editing JSON. +- **Subagents for focused, parallel work.** Dispatch built-in `coder`, `explore`, and `plan` subagents in isolated contexts while keeping the main conversation clean. +- **Lifecycle hooks.** Run local commands at key points to gate risky tool calls, audit decisions, trigger desktop notifications, or connect to your own automation. + +## Docs + +- [Getting Started](https://moonshotai.github.io/kimi-code/en/guides/getting-started) +- [Interaction and approvals](https://moonshotai.github.io/kimi-code/en/guides/interaction) +- [Sessions](https://moonshotai.github.io/kimi-code/en/guides/sessions) +- [Configuration](https://moonshotai.github.io/kimi-code/en/configuration/config-files) +- [Command reference](https://moonshotai.github.io/kimi-code/en/reference/kimi-command) + +## Develop + +Requirements: Node.js ≥ 24.15.0, pnpm 10.33.0. + +```sh +git clone https://github.com/MoonshotAI/kimi-code.git +cd kimi-code +pnpm install +``` + +```sh +pnpm dev:cli # run the CLI in dev mode +pnpm test # run tests +pnpm typecheck # TypeScript check +pnpm lint # oxlint +pnpm build # build all packages +``` + +See [CONTRIBUTING.md](CONTRIBUTING.md) for the full contribution guide. + +## Community + +- [Issues](https://github.com/MoonshotAI/kimi-code/issues) +- For security vulnerabilities, see [SECURITY.md](SECURITY.md). + +## Acknowledgements + +Our TUI is built on top of [`pi-tui`](https://github.com/earendil-works/pi-mono/tree/main/packages/tui). We thank the authors of `pi-tui` for their valuable work. + +## License + +Released under the [MIT License](LICENSE). diff --git a/README.zh-CN.md b/README.zh-CN.md new file mode 100644 index 000000000..ace6cb425 --- /dev/null +++ b/README.zh-CN.md @@ -0,0 +1,104 @@ +# Kimi Code CLI + +[![License](https://img.shields.io/badge/license-MIT-blue)](LICENSE) [![Docs](https://img.shields.io/badge/docs-online-blue)](https://moonshotai.github.io/kimi-code/zh/) + +[Documentation](https://moonshotai.github.io/kimi-code/zh/) · [Issues](https://github.com/MoonshotAI/kimi-code/issues) · [English](README.md) + + +![Kimi Code 的使用演示](./docs/media/intro.gif) + + +## 什么是 Kimi Code CLI + +Kimi Code CLI 是一个运行在终端里的 AI 编程 agent,可以帮你读写代码、执行 shell 命令、检索文件、抓取网页,并根据反馈自主决定下一步动作。开箱即用对接 Moonshot AI 的 Kimi 模型,也可指向其他兼容厂商。 + +## 安装 + +推荐使用官方安装脚本,不需要提前安装 Node.js。 + +- **macOS / Linux**: + +```sh +curl -fsSL https://code.kimi.com/kimi-code/install.sh | bash +``` + +- **Windows(PowerShell)**: + +```powershell +irm https://code.kimi.com/kimi-code/install.ps1 | iex +``` + +随后在新的终端会话中运行: + +```sh +kimi --version +``` + +npm 安装、升级、卸载方式,见[快速上手](https://moonshotai.github.io/kimi-code/zh/guides/getting-started)。 + +## 快速开始 + +进入项目目录并启动交互界面: + +```sh +cd your-project +kimi +``` + +首次启动时,在 Kimi Code CLI 里输入 `/login`,选择 Kimi Code OAuth 或 Moonshot AI Open Platform API 密钥登录。登录完成后,可以先让它熟悉项目: + +``` +帮我看一下这个项目的目录结构,简单介绍一下每个目录是做什么的 +``` + +## 核心特性 + +- **二进制发行,零环境依赖** 一行命令安装,不需要预装 Node.js,不用折腾 PATH,也不会和全局模块冲突。 +- **极速启动** TUI 在毫秒级就绪,开一个新会话没有任何心智负担。 +- **精致的 TUI 体验** 为长时间、专注的 Agent 会话精心打磨的交互界面。 +- **视频也能输入** 屏幕录像、演示视频也能拖进对话。 +- **AI-native 的 MCP 配置** 通过 `/mcp-config` 对话式添加、编辑、认证 MCP 服务器,无需手写 JSON。 +- **子 Agent 聚焦并行工作** 内置 `coder`、`explore`、`plan` 子 Agent 在隔离上下文中处理子任务,主对话保持清爽。 +- **生命周期 hooks** 在关键节点执行本地命令:拦截高风险工具调用、审计决策、发送桌面通知,或对接你自己的自动化脚本。 + + +## 文档 + +- [快速上手](https://moonshotai.github.io/kimi-code/zh/guides/getting-started) +- [交互与审批](https://moonshotai.github.io/kimi-code/zh/guides/interaction) +- [会话](https://moonshotai.github.io/kimi-code/zh/guides/sessions) +- [配置](https://moonshotai.github.io/kimi-code/zh/configuration/config-files) +- [命令参考](https://moonshotai.github.io/kimi-code/zh/reference/kimi-command) + +## 本地开发 + +环境要求:Node.js ≥ 24.15.0,pnpm 10.33.0。 + +```sh +git clone https://github.com/MoonshotAI/kimi-code.git +cd kimi-code +pnpm install +``` + +```sh +pnpm dev:cli # 以开发模式运行 CLI +pnpm test # 运行测试 +pnpm typecheck # TypeScript 检查 +pnpm lint # 运行 oxlint +pnpm build # 构建所有包 +``` + +完整贡献流程见 [CONTRIBUTING.md](CONTRIBUTING.md)。 + +## 社区 + +- [Issues](https://github.com/MoonshotAI/kimi-code/issues) +- 安全漏洞反馈,请见 [SECURITY.md](SECURITY.md)。 + +## 致谢 + +我们的 TUI 构建在 [`pi-tui`](https://github.com/earendil-works/pi-mono/tree/main/packages/tui) 之上。我们忠心感谢 `pi-tui` 作者的工作。 + +## 许可证 + +基于 [MIT](LICENSE) 协议发布。 diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000..982c6df63 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,33 @@ +# Security Policy + +## Supported Versions + +Currently, Kimi Code only provides security support for the latest released version. + +## Reporting a Vulnerability + +We take security seriously. **Please do not open a public issue for security vulnerabilities.** + +Preferred channel: + +- GitHub Security Advisories — https://github.com/MoonshotAI/kimi-code/security/advisories/new + (private disclosure, tracked with the codebase) + +Alternative channel: + +- Email: code@moonshot.ai (please include "[security]" in the subject) + +## What to Include + +- Affected version (output of `kimi --version`) +- Reproduction steps +- Impact assessment +- Any suggested mitigation + +## Our Response + +We will acknowledge your report and provide an initial assessment as soon as we can. + +## Public Disclosure + +We will coordinate with you on disclosure timing once a fix is ready. diff --git a/apps/kimi-code/.gitignore b/apps/kimi-code/.gitignore new file mode 100644 index 000000000..66d6a21a6 --- /dev/null +++ b/apps/kimi-code/.gitignore @@ -0,0 +1,2 @@ +# Copied from packages/kimi-core at build time +agents/ diff --git a/apps/kimi-code/AGENTS.md b/apps/kimi-code/AGENTS.md new file mode 100644 index 000000000..9c8f422cf --- /dev/null +++ b/apps/kimi-code/AGENTS.md @@ -0,0 +1,128 @@ +# apps/kimi-code Development Guide + +This file only contains rules local to `apps/kimi-code`. For cross-repo rules, see the root `AGENTS.md`. + +## TUI File Layout + +`apps/kimi-code` is the terminal UI / CLI app. The entry chain is: + +`src/main.ts` -> `src/cli/commands.ts` -> `src/cli/run-shell.ts` -> SDK `KimiHarness` -> `src/tui/kimi-tui.ts` + +Main directories: + +- `src/constant/`: non-copy constants shared by CLI/TUI — product, protocol, paths, terminal control, updates, and so on. +- `src/cli/`: command-line arguments, subcommands, and CLI startup. +- `src/tui/`: the interactive terminal UI. +- `src/tui/kimi-tui.ts`: the TUI master assembler, responsible for wiring state, layout, editor, session, SDK events, and dialogs together. +- `src/tui/actions/`: reusable TUI state/replay/projection logic. Pure logic that can be split out of `KimiTUI` should land here first. +- `src/tui/commands/`: slash command definitions, parsing, ordering, and dynamic skill command generation. +- `src/tui/components/`: pi-tui components, organized by UI type. +- `src/tui/constant/`: non-copy constants reused across TUI modules — symbols, terminal sequences, render sizing, streaming-arg match rules, and so on. +- `src/tui/components/chrome/`: persistent UI chrome — footer, todo panel, welcome, loader, device code. +- `src/tui/components/dialogs/`: selectors, approval panels, question popups, and settings popups that temporarily replace the editor. +- `src/tui/components/editor/`: the custom input box and the file mention provider. +- `src/tui/components/media/`: image, diff, code highlight, and other media displays. +- `src/tui/components/messages/`: message blocks in the transcript — assistant, user, tool call, thinking, usage, subagent, and so on. +- `src/tui/components/panes/`: right-side / activity-area panes such as the activity pane and queue pane. +- `src/tui/reverse-rpc/`: the adapter layer that bridges SDK approval/question callbacks to the UI. +- `src/tui/theme/`: themes, color tokens, style helpers, and the pi-tui markdown theme. +- `src/tui/utils/`: TUI-only utility functions. +- `src/utils/`: app-wide utilities — clipboard, git, history, image, process, usage, and so on. + +## Module Responsibilities + +- `cli` only interprets command-line input, assembles startup arguments, and invokes the TUI. Do not put TUI interaction logic into the CLI. +- `KimiTUI` coordinates; it does not accumulate complex business rules. New logic that can be tested independently should be split into `actions`, `commands`, `components`, `reverse-rpc`, or `utils` first. +- `commands` only owns slash-command declaration, parsing, and the parsed-result types. The actual execution can be dispatched from `KimiTUI`, but complex logic should continue to sink downward. +- `components` only handle presentation and local interaction; they must not call the SDK directly, and must not read or write session state directly. +- `reverse-rpc` converts SDK approval/question requests into the data shape a UI panel/dialog needs, and converts the user's choice back into an SDK response. +- `theme` is the single source of truth for colors and styles. Components must not bypass the theme system and use chalk named colors directly. +- `utils` holds utility functions with no UI-state dependency. Logic that needs `TUIState` or a component instance must not live under app-level `src/utils`. +- `apps/kimi-code` may only use core capabilities through `@moonshot-ai/kimi-code-sdk`. Do not import `@moonshot-ai/agent-core` directly in app code. + +## KimiTUI Internal Sections + +`src/tui/kimi-tui.ts` is large. When you modify it, place code into the existing responsibility section — do not just drop it where it happens to be convenient. + +- Types and state creation: `KimiTUIStartupInput`, `TUIState`, `createInitialAppState`, `createTUIState`. Before adding new global UI state, decide whether it really belongs in `TUIState`. +- Startup helpers: slash commands, autocomplete, skill commands, input history. +- Lifecycle: `start`, `init`, `stop`. They only handle startup/shutdown order — do not stuff feature implementations into them. +- Layout and editor: `buildLayout`, `setupEditorHandlers`, external editor, clipboard image, exit shortcuts. +- User input: `handleUserInput`, `executeSlashCommand`, `handleBuiltInSlashCommand`, `sendNormalUserInput`. +- Sending and queueing: `enqueueMessage`, `sendMessageInternal`, `sendMessage`, `steerMessage`, `finalizeTurn`. +- Session management: create, restore, switch, close, sync runtime state, subscribe to session events. +- Event routing: `handleEvent` only dispatches; concrete events go into the corresponding `handleXxx`. +- Streaming rendering: assistant delta, thinking, tool call, tool result, compaction, subagent, background agent. +- Transcript: `createTranscriptComponent`, `appendTranscriptEntry`, read/tool/agent group aggregation. +- Activity / queue / footer: `updateActivityPane`, `resolveActivityPaneMode`, `updateQueueDisplay`, terminal progress. +- Dialogs / selectors: help, session picker, editor/model/thinking/theme/permission/settings selectors, approval / question panels. +- Slash command handlers: `handleThemeCommand`, `handleModelCommand`, `handlePlanCommand`, `handleCompactCommand`, `handleLoginCommand`, and so on. + +If a section keeps growing, split pure functions, state projections, presentation components, and handler logic into the corresponding directories rather than continuing to expand `KimiTUI`. + +## Where New Features Go + +The feature type decides where it lands: + +- New CLI arguments: change `src/cli/commands.ts` / `src/cli/options.ts`, then pass them into the TUI via `src/cli/run-shell.ts`. Do not let the CLI operate on the session directly. +- New CLI subcommands: put them under `src/cli/sub/`, with non-interactive command logic only; when SDK access is needed, go through `@moonshot-ai/kimi-code-sdk`. +- New slash commands: first change definition, parsing, and types under `src/tui/commands/`; put the execution entry into the slash-command handler section of `KimiTUI`; split complex execution logic into `actions` or `utils`. +- New skill-derived commands: hook into `buildSkillSlashCommands` / the skill command map — do not hard-code a single skill. +- New transcript message types: define the data shape in `src/tui/types.ts`, add or extend a component under `components/messages/`, and register the renderer in `createTranscriptComponent`. +- New tool-result display: prefer extending `components/messages/tool-renderers/registry.ts` and the corresponding renderer; do not stack branches inside `ToolCallComponent`. +- New popup / selector: put it under `components/dialogs/` and mount it via `mountEditorReplacement`; if the trigger comes from an SDK callback, also check whether `reverse-rpc/` needs an adapter/controller/handler. +- New SDK event handling: add the dispatch in `handleEvent`, then add the corresponding `handleXxx`. If the event simply maps to a transcript entry. +- New session start / resume behavior: put it in the session management section, keeping `init` focused only on startup orchestration. +- New status bar, activity area, or queue display: change `chrome/footer`, `panes/activity`, `panes/queue`, and the corresponding `updateXxx` method. +- New configuration option: first change the read/write and schema in `src/tui/config.ts`, then wire the settings UI; when persistence is needed, go through `saveTuiConfig`. +- New constants: constants shared by CLI/TUI and not copy belong in `src/constant/`; non-copy constants reused only within the TUI belong in `src/tui/constant/`. Component-local copy, option labels, help descriptions, dialog title/footer text — keep these next to the corresponding component or command, do not centralize them into a global copy constants module. +- New general-purpose capability: if it does not depend on TUI state, put it under `src/utils/`; if it depends on TUI state or a component, put it under `src/tui/utils/`. + +Test placement rules: + +- Component behavior tests live next to the corresponding component's tests. +- Command parsing tests go under `test/tui/commands/`. +- reverse-rpc tests go under `test/tui/reverse-rpc/`. +- Pure utility tests go next to the corresponding utils tests. +- Do not create a generic `some-feature.test.ts` just to land a small feature. + +## TUI Coding Conventions + +- Do not over-encapsulate, especially for one- or two-line functions — do not introduce a two-layer wrapper, just inline. +- Functions with no state / UI side effects do not belong as private methods on the `KimiTUI` class; put them in external utils. +- Constants must live in the corresponding `constant` directory; they must not be scattered through component or logic code. +- Inside `handleInput(data)`, when comparing a printable character (letter, digit, space, punctuation), it is **forbidden** to write literal comparisons such as `data === 'q'`. With the Kitty keyboard protocol enabled in terminals like VSCode, these keys are sent as CSI-u sequences (e.g. `\x1b[113u`), and a bare comparison will never match. Decode with `printableChar(data)` from `src/tui/utils/printable-key.ts` first, then compare; function keys continue to use `matchesKey(data, Key.*)`; control characters (codepoint < 32) may still be compared against the raw `data`. `test/tui/printable-key-guard.test.ts` enforces this in CI. + +## How to Set Themes + +Themes are managed centrally under `src/tui/theme/`: + +- `colors.ts` defines semantic tokens: `ColorPalette`, `darkColors`, `lightColors`. +- `styles.ts` builds common chalk helpers on top of `ColorPalette`. +- `pi-tui-theme.ts` produces the theme configuration markdown / pi-tui requires. +- `bundle.ts` packs `colors`, `styles`, and `markdownTheme` into a `KimiTUIThemeBundle`. +- `index.ts` / `detect.ts` handle the theme type and auto/dark/light resolution. + +When setting or switching themes: + +- The UI entry goes through `ThemeSelectorComponent`, `handleThemeCommand`, and `applyThemeChoice`. +- The real apply step goes through `KimiTUI.applyTheme`, which should update `state.theme`, `state.appState.theme`, and notify the relevant components to refresh their palette. +- Persisting the user's choice goes through `saveTuiConfig`. Do not let a component write the config file itself. + +When writing color: + +- Do not use chalk named colors such as `chalk.red`, `chalk.cyan`, `chalk.white`, `chalk.gray`, `chalk.dim`, or `chalk.yellow` directly. +- If a component already has `colors`, use `chalk.hex(colors.)(text)`. +- If a component already has `state.theme.styles` or styles passed in, prefer helpers such as `styles.error(text)`, `styles.dim(text)`. +- When new visual semantics have no token, first add a semantic field to `ColorPalette`, and fill in both `darkColors` and `lightColors`. +- In light themes, text tokens against a white background must be at least 4.5:1; borders and large chrome must be at least 3:1. +- Do not cache styled chalk functions at module top level. Theme switching must take effect within a single render, so styles must be generated on the render path from the current palette. + +After a theme change, non-comment code must not contain chalk named colors such as `chalk.white`, `chalk.cyan`, `chalk.red`, `chalk.green`, `chalk.gray`, `chalk.yellow`, `chalk.blue`, `chalk.magenta`, `chalk.whiteBright`, or `chalk.blackBright`. + +## General Coding Requirements + +- For optional object properties, pass `undefined` directly — do not use conditional spread. +- Optional object properties do not need to additionally allow `undefined` in the type. +- Internal methods with only a single parameter should not be turned into options objects just for stylistic uniformity. +- Except for a package's own `index.ts`, other `index.ts` files should prefer `export * from './module'`. diff --git a/apps/kimi-code/README.md b/apps/kimi-code/README.md new file mode 100644 index 000000000..9c0bd4e27 --- /dev/null +++ b/apps/kimi-code/README.md @@ -0,0 +1,88 @@ +# @moonshot-ai/kimi-code + +> The Starting Point for Next-Gen Agents + +[![npm](https://img.shields.io/npm/v/@moonshot-ai/kimi-code)](https://www.npmjs.com/package/@moonshot-ai/kimi-code) [![License](https://img.shields.io/badge/license-MIT-blue)](LICENSE) [![Docs](https://img.shields.io/badge/docs-online-blue)](https://moonshotai.github.io/kimi-code/en/) + +## What is Kimi Code CLI + +Kimi Code CLI is an AI coding agent that runs in your terminal. It can read and edit code, run shell commands, search files, fetch web pages, and choose the next step based on the feedback it receives. It works out of the box with Moonshot AI's Kimi models and can also be configured to use other compatible providers. + +## Install + +The recommended install path is the official script. It does not require Node.js to be installed first. + +- **macOS / Linux**: + +```sh +curl -fsSL https://code.kimi.com/kimi-code/install.sh | bash +``` + +- **Windows (PowerShell)**: + +```powershell +irm https://code.kimi.com/kimi-code/install.ps1 | iex +``` + +Then run it with a new Terminal session: + +```sh +kimi --version +``` + +### Alternative: npm + +If you prefer npm, use Node.js 22.19.0 or later: + +```sh +npm install -g @moonshot-ai/kimi-code +``` + +Or with pnpm: + +```sh +pnpm add -g @moonshot-ai/kimi-code +``` + +For upgrade and uninstall instructions, see the [Getting Started guide](https://moonshotai.github.io/kimi-code/en/guides/getting-started). + +## Quick Start + +Open a project and start the interactive UI: + +```sh +cd your-project +kimi +``` + +On first launch, run `/login` inside Kimi Code CLI and choose either Kimi Code OAuth or a Moonshot AI Open Platform API key. After login, try a first task: + +``` +Take a look at this project and explain the main directories. +``` + +## Key Features + +- **Single-binary distribution.** Install with one command — no Node.js setup, no PATH gymnastics, no global module conflicts. +- **Blazing-fast startup.** The TUI is ready in milliseconds, so opening a session never feels heavy. +- **Polished TUI.** A carefully tuned interface designed for long, focused agent sessions. +- **Video input.** Drop a screen recording or demo clip into the chat — let the agent watch instead of typing out what's hard to describe in words. +- **AI-native MCP configuration.** Add, edit, and authenticate Model Context Protocol servers conversationally via `/mcp-config` — no hand-editing JSON. +- **Subagents for focused, parallel work.** Dispatch built-in `coder`, `explore`, and `plan` subagents in isolated context windows; the main conversation stays clean. +- **Lifecycle hooks.** Run local commands at key points — gate risky tool calls, audit decisions, fire desktop notifications, wire into your own automation. + +## Documentation + +- Full docs: https://moonshotai.github.io/kimi-code/en/ +- 中文文档: https://moonshotai.github.io/kimi-code/zh/ +- Getting Started: https://moonshotai.github.io/kimi-code/en/guides/getting-started + +## Repository & Issues + +- Source: https://github.com/MoonshotAI/kimi-code +- Issues: https://github.com/MoonshotAI/kimi-code/issues +- Security: see SECURITY.md in the main repository + +## License + +MIT diff --git a/apps/kimi-code/package.json b/apps/kimi-code/package.json new file mode 100644 index 000000000..01f448325 --- /dev/null +++ b/apps/kimi-code/package.json @@ -0,0 +1,88 @@ +{ + "name": "@moonshot-ai/kimi-code", + "version": "0.1.1", + "description": "The Starting Point for Next-Gen Agents", + "license": "MIT", + "author": "Moonshot AI", + "homepage": "https://github.com/MoonshotAI/kimi-code/tree/main/apps/kimi-code#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/MoonshotAI/kimi-code.git", + "directory": "apps/kimi-code" + }, + "bugs": { + "url": "https://github.com/MoonshotAI/kimi-code/issues" + }, + "keywords": [ + "kimi", + "kimi-code", + "cli", + "agent", + "coding-agent", + "ai", + "tui" + ], + "bin": { + "kimi": "dist/main.mjs" + }, + "files": [ + "dist", + "scripts/postinstall.mjs", + "scripts/postinstall", + "README.md" + ], + "type": "module", + "imports": { + "#/*": [ + "./src/*.ts", + "./src/*/index.ts" + ] + }, + "publishConfig": { + "access": "public", + "provenance": true + }, + "scripts": { + "build": "tsdown", + "smoke": "node scripts/smoke.mjs", + "build:native:js": "node scripts/native/01-bundle.mjs", + "build:native:sea": "node scripts/native/build.mjs --profile=local", + "build:native:release": "node scripts/native/build.mjs --profile=release", + "package:native": "node scripts/native/package.mjs", + "produce:native:manifest": "node scripts/native/produce-manifest.mjs", + "release:native:resolve": "node scripts/native/resolve-release.mjs", + "test:native:smoke": "node scripts/native/smoke.mjs", + "dev": "tsx --import ../../build/register-raw-text-loader.mjs ./src/main.ts", + "dev:prod": "node dist/main.mjs", + "clean": "rm -rf dist", + "typecheck": "tsc -p tsconfig.json --noEmit", + "test": "pnpm -w run build:packages && vitest run", + "e2e": "pnpm -w run build:packages && KIMI_E2E=1 vitest run test/e2e", + "e2e:real": "pnpm -w run build:packages && KIMI_E2E_REAL=1 vitest run test/e2e/real-llm-smoke.e2e.test.ts", + "postinstall": "node scripts/postinstall.mjs" + }, + "dependencies": { + "@earendil-works/pi-tui": "^0.74.0", + "@mariozechner/clipboard": "^0.3.2", + "chalk": "^5.4.1", + "cli-highlight": "^2.1.11", + "commander": "^13.1.0", + "semver": "^7.7.4", + "smol-toml": "^1.6.1", + "zod": "^4.3.6" + }, + "devDependencies": { + "@moonshot-ai/kimi-code-oauth": "workspace:^", + "@moonshot-ai/kimi-code-sdk": "workspace:^", + "@moonshot-ai/kimi-telemetry": "workspace:^", + "@moonshot-ai/migration-legacy": "workspace:^", + "@types/semver": "^7.7.0", + "@types/yazl": "^2.4.6", + "postject": "1.0.0-alpha.6", + "tsx": "^4.21.0", + "yazl": "^3.3.1" + }, + "engines": { + "node": ">=22.19.0" + } +} diff --git a/apps/kimi-code/scripts/native/01-bundle.mjs b/apps/kimi-code/scripts/native/01-bundle.mjs new file mode 100644 index 000000000..46193d3e7 --- /dev/null +++ b/apps/kimi-code/scripts/native/01-bundle.mjs @@ -0,0 +1,17 @@ +import { createRequire } from 'node:module'; +import { resolve } from 'node:path'; + +import { run } from './exec.mjs'; + +const requireFromScript = createRequire(import.meta.url); +const tsdownCliPath = requireFromScript.resolve('tsdown/run'); +const checkBundlePath = resolve(import.meta.dirname, 'check-bundle.mjs'); + +export async function runBundleStep() { + await run(process.execPath, [tsdownCliPath, '--config', 'tsdown.native.config.ts']); + await run(process.execPath, [checkBundlePath]); +} + +if (import.meta.url === `file://${process.argv[1]}`) { + await runBundleStep(); +} diff --git a/apps/kimi-code/scripts/native/02-sea-blob.mjs b/apps/kimi-code/scripts/native/02-sea-blob.mjs new file mode 100644 index 000000000..8bac05a84 --- /dev/null +++ b/apps/kimi-code/scripts/native/02-sea-blob.mjs @@ -0,0 +1,69 @@ +import { mkdir, stat, writeFile } from 'node:fs/promises'; +import { dirname, resolve } from 'node:path'; + +import { + collectNativeAssets, + nativeAssetManifestKey, + nativeAssetSummary, +} from './assets.mjs'; +import { fail, run } from './exec.mjs'; +import { + appRoot, + nativeBlobPath, + nativeIntermediatesDir, + nativeJsBundlePath, + nativeManifestDir, + nativeSeaConfigPath, + targetTriple, +} from './paths.mjs'; + +async function ensureBundleExists() { + try { + await stat(nativeJsBundlePath()); + } catch { + fail(`Native JS bundle not found at ${nativeJsBundlePath()}. Run 01-bundle.mjs first.`); + } +} + +async function writeSeaConfig(target) { + await mkdir(nativeIntermediatesDir(), { recursive: true }); + const { manifest, manifestJson, assets } = await collectNativeAssets({ + appRoot, + target, + }); + const manifestPath = resolve(nativeManifestDir(target), 'manifest.json'); + await mkdir(dirname(manifestPath), { recursive: true }); + await writeFile(manifestPath, manifestJson); + + const seaAssets = { + [nativeAssetManifestKey(target)]: manifestPath, + ...assets, + }; + const config = { + main: nativeJsBundlePath(), + output: nativeBlobPath(), + assets: Object.fromEntries( + Object.entries(seaAssets).sort(([a], [b]) => a.localeCompare(b)), + ), + disableExperimentalSEAWarning: true, + useCodeCache: false, + useSnapshot: false, + }; + await writeFile(nativeSeaConfigPath(), `${JSON.stringify(config, null, 2)}\n`); + + console.log(`Collected native assets for ${manifest.target}:`); + for (const line of nativeAssetSummary(manifest)) { + console.log(`- ${line}`); + } +} + +export async function runSeaBlobStep() { + await ensureBundleExists(); + const target = targetTriple(); + await writeSeaConfig(target); + await run(process.execPath, ['--experimental-sea-config', nativeSeaConfigPath()]); +} + +if (import.meta.url === `file://${process.argv[1]}`) { + await runSeaBlobStep(); +} diff --git a/apps/kimi-code/scripts/native/03-inject.mjs b/apps/kimi-code/scripts/native/03-inject.mjs new file mode 100644 index 000000000..24cd0ad32 --- /dev/null +++ b/apps/kimi-code/scripts/native/03-inject.mjs @@ -0,0 +1,65 @@ +import { copyFile, mkdir, stat } from 'node:fs/promises'; +import { resolve } from 'node:path'; + +import { fail, run, tryRun } from './exec.mjs'; +import { + appRoot, + nativeBinDir, + nativeBinPath, + nativeBlobPath, + SEA_SENTINEL_FUSE, + targetTriple, +} from './paths.mjs'; + +function postjectPath() { + const command = process.platform === 'win32' ? 'postject.cmd' : 'postject'; + return resolve(appRoot, 'node_modules/.bin', command); +} + +async function ensureBlobExists() { + try { + await stat(nativeBlobPath()); + } catch { + fail(`SEA blob not found at ${nativeBlobPath()}. Run 02-sea-blob.mjs first.`); + } +} + +async function copyNodeExecutable(target) { + await mkdir(nativeBinDir(target), { recursive: true }); + const out = nativeBinPath(target); + await copyFile(process.execPath, out); + if (process.platform !== 'win32') { + await run('chmod', ['755', out]); + } +} + +async function removeSignatureIfNeeded(target) { + const out = nativeBinPath(target); + if (process.platform === 'darwin') { + await tryRun('codesign', ['--remove-signature', out]); + } + if (process.platform === 'win32') { + await tryRun('signtool', ['remove', '/s', out]); + } +} + +async function injectSeaBlob(target) { + const out = nativeBinPath(target); + const args = [out, 'NODE_SEA_BLOB', nativeBlobPath(), '--sentinel-fuse', SEA_SENTINEL_FUSE]; + if (process.platform === 'darwin') { + args.push('--macho-segment-name', 'NODE_SEA'); + } + await run(postjectPath(), args); +} + +export async function runInjectStep() { + const target = targetTriple(); + await ensureBlobExists(); + await copyNodeExecutable(target); + await removeSignatureIfNeeded(target); + await injectSeaBlob(target); +} + +if (import.meta.url === `file://${process.argv[1]}`) { + await runInjectStep(); +} diff --git a/apps/kimi-code/scripts/native/04-sign.mjs b/apps/kimi-code/scripts/native/04-sign.mjs new file mode 100644 index 000000000..2930b5e63 --- /dev/null +++ b/apps/kimi-code/scripts/native/04-sign.mjs @@ -0,0 +1,68 @@ +import { createHash } from 'node:crypto'; +import { createReadStream } from 'node:fs'; +import { writeFile } from 'node:fs/promises'; +import { basename, resolve } from 'node:path'; + +import { run } from './exec.mjs'; +import { nativeBinPath, targetTriple } from './paths.mjs'; + +const ENTITLEMENTS_PATH = resolve(import.meta.dirname, 'entitlements.plist'); + +export function buildCodesignArgs({ identity, executable, entitlementsPath, keychainPath }) { + if (identity === '-') { + return ['--sign', '-', executable]; + } + const args = [ + '--sign', + identity, + '--options', + 'runtime', + '--entitlements', + entitlementsPath, + '--timestamp', + ]; + if (keychainPath) { + args.push('--keychain', keychainPath); + } + args.push('--force', executable); + return args; +} + +async function sha256(path) { + return await new Promise((resolveHash, reject) => { + const hash = createHash('sha256'); + const stream = createReadStream(path); + stream.on('error', reject); + stream.on('data', (chunk) => hash.update(chunk)); + stream.on('end', () => resolveHash(hash.digest('hex'))); + }); +} + +async function writeChecksum(executable) { + const digest = await sha256(executable); + await writeFile(`${executable}.sha256`, `${digest} ${basename(executable)}\n`); +} + +export async function runSignStep({ identity = '-', keychainPath = null } = {}) { + const target = targetTriple(); + const executable = nativeBinPath(target); + + if (process.platform === 'darwin') { + const args = buildCodesignArgs({ + identity, + executable, + entitlementsPath: ENTITLEMENTS_PATH, + keychainPath, + }); + await run('codesign', args); + } + + await writeChecksum(executable); + console.log(`Signed and hashed: ${executable}`); +} + +if (import.meta.url === `file://${process.argv[1]}`) { + const identity = process.env.APPLE_SIGNING_IDENTITY ?? '-'; + const keychainPath = process.env.APPLE_KEYCHAIN_PATH ?? null; + await runSignStep({ identity, keychainPath }); +} diff --git a/apps/kimi-code/scripts/native/05-verify.mjs b/apps/kimi-code/scripts/native/05-verify.mjs new file mode 100644 index 000000000..5c47da6bc --- /dev/null +++ b/apps/kimi-code/scripts/native/05-verify.mjs @@ -0,0 +1,30 @@ +import { run } from './exec.mjs'; +import { nativeBinPath, targetTriple } from './paths.mjs'; + +export async function runVerifyStep({ requireGatekeeper = false } = {}) { + if (process.platform !== 'darwin') { + console.log('Verify step skipped (not macOS)'); + return; + } + + const target = targetTriple(); + const executable = nativeBinPath(target); + + console.log(`==> codesign -dv ${executable}`); + await run('codesign', ['-dv', '--verbose=2', executable]); + + if (requireGatekeeper) { + // spctl in 'install' mode simulates the Gatekeeper online check — only a + // fully notarized binary passes. Ad-hoc signed binaries fail, so this is + // only run under the release profile. + console.log(`==> spctl -a -vvv -t install ${executable}`); + await run('spctl', ['-a', '-vvv', '-t', 'install', executable]); + } else { + console.log('Skipping spctl check (requireGatekeeper=false)'); + } +} + +if (import.meta.url === `file://${process.argv[1]}`) { + const requireGatekeeper = process.env.KIMI_VERIFY_GATEKEEPER === '1'; + await runVerifyStep({ requireGatekeeper }); +} diff --git a/apps/kimi-code/scripts/native/assets.mjs b/apps/kimi-code/scripts/native/assets.mjs new file mode 100644 index 000000000..7b0560b69 --- /dev/null +++ b/apps/kimi-code/scripts/native/assets.mjs @@ -0,0 +1,276 @@ +import { createHash } from 'node:crypto'; +import { existsSync, realpathSync } from 'node:fs'; +import { readdir, readFile } from 'node:fs/promises'; +import { createRequire } from 'node:module'; +import { dirname, extname, isAbsolute, join, relative, resolve } from 'node:path'; +import { pathToFileURL } from 'node:url'; + +import { NATIVE_ASSET_MANIFEST_VERSION, buildManifestKey } from './manifest.mjs'; +import { resolveTargetDeps, SUPPORTED_TARGETS } from './native-deps.mjs'; + +export { NATIVE_ASSET_MANIFEST_VERSION }; + +// Re-export for any external consumer that still needs it. Internally we +// use resolveTargetDeps() exclusively — no more if/else against package names. +export const NATIVE_TARGETS = Object.freeze( + Object.fromEntries( + SUPPORTED_TARGETS.map((t) => { + const deps = resolveTargetDeps(t); + const clipboardTarget = deps.find((d) => d.id === 'clipboard-target')?.resolvedName; + const koffiNativeFile = deps.find((d) => d.id === 'koffi')?.nativeFileRelatives?.[0]; + const koffiTriplet = koffiNativeFile?.match(/koffi\/([^/]+)\/koffi\.node$/)?.[1] ?? null; + return [t, { clipboardPackage: clipboardTarget, koffiTriplet }]; + }), + ), +); + +const jsExtensions = ['.js', '.cjs', '.mjs', '.json', '.node']; +const runtimeEntryNames = ['index.js', 'index.cjs', 'index.mjs']; + +function fail(message) { + throw new Error(message); +} + +function toPosixPath(path) { + return path.split('\\').join('/'); +} + +function sha256(bytes) { + return createHash('sha256').update(bytes).digest('hex'); +} + +async function readJson(path) { + return JSON.parse(await readFile(path, 'utf-8')); +} + +async function listFiles(root) { + const files = []; + + async function walk(dir) { + const entries = await readdir(dir, { withFileTypes: true }); + for (const entry of entries) { + const path = join(dir, entry.name); + if (entry.isDirectory()) { + await walk(path); + continue; + } + if (entry.isFile()) { + files.push(path); + } + } + } + + await walk(root); + return files; +} + +function resolvePackageRootGeneric(requireFromApp, packageName, parentPackageName, appRoot, target) { + try { + return dirname(requireFromApp.resolve(`${packageName}/package.json`)); + } catch (rootError) { + if (parentPackageName !== null) { + try { + const parentPackageJsonPath = realpathSync( + requireFromApp.resolve(`${parentPackageName}/package.json`), + ); + const requireFromParent = createRequire(pathToFileURL(parentPackageJsonPath)); + return dirname(requireFromParent.resolve(`${packageName}/package.json`)); + } catch {} + } + fail( + [ + `Native asset package is not installed for target ${target}: ${packageName}`, + parentPackageName ? `Searched via parent: ${parentPackageName}` : '', + `Resolve root: ${appRoot}`, + 'Run pnpm install --frozen-lockfile before building native assets.', + rootError instanceof Error ? rootError.message : String(rootError), + ] + .filter(Boolean) + .join('\n'), + ); + } +} + +function resolveFileCandidate(path) { + if (existsSync(path)) return path; + for (const extension of jsExtensions) { + const candidate = `${path}${extension}`; + if (existsSync(candidate)) return candidate; + } + for (const entryName of runtimeEntryNames) { + const candidate = join(path, entryName); + if (existsSync(candidate)) return candidate; + } + return null; +} + +function resolvePackageEntry(packageRoot, packageJson) { + const rawMain = + typeof packageJson.main === 'string' + ? packageJson.main + : typeof packageJson.module === 'string' + ? packageJson.module + : 'index.js'; + return resolveFileCandidate(resolve(packageRoot, rawMain)); +} + +function relativeRuntimeSpecifiers(text) { + const specifiers = new Set(); + for (const match of text.matchAll(/\brequire\(\s*["'](\.[^"']+)["']\s*\)/g)) { + specifiers.add(match[1]); + } + for (const match of text.matchAll(/(? a.localeCompare(b)); + if (includeNativeFiles && !sorted.some((file) => file.endsWith('.node'))) { + fail(`Native package ${packageName} does not contain a .node file at ${packageRoot}`); + } + return sorted; +} + +async function packageManifestEntries({ packageName, packageRoot, files, target }) { + const root = `node_modules/${packageName}`; + const entries = []; + const assets = {}; + + for (const file of files) { + const sourceBytes = await readFile(file); + const packageRelativePath = toPosixPath(relative(packageRoot, file)); + const relativePath = `${root}/${packageRelativePath}`; + const assetKey = `native/${target}/${relativePath}`; + entries.push({ + assetKey, + relativePath, + sha256: sha256(sourceBytes), + }); + assets[assetKey] = file; + } + + return { + packageManifest: { + name: packageName, + root, + files: entries, + }, + assets, + }; +} + +export const nativeAssetManifestKey = buildManifestKey; + +export function nativeAssetSummary(manifest) { + return manifest.packages.map((pkg) => `${pkg.name}: ${pkg.files.length} files`); +} + +export async function collectNativeAssets({ appRoot, target }) { + const requireFromApp = createRequire(pathToFileURL(resolve(appRoot, 'package.json'))); + const targetDeps = resolveTargetDeps(target); // throws on unsupported target + + const manifestPackages = []; + const assets = {}; + + for (const dep of targetDeps) { + const packageRoot = resolvePackageRootGeneric( + requireFromApp, + dep.resolvedName, + dep.parentName, + appRoot, + target, + ); + const files = await collectPackageFiles({ + packageName: dep.resolvedName, + packageRoot, + includeNativeFiles: dep.collect === 'native-files', + nativeFileRelatives: dep.nativeFileRelatives, + }); + const result = await packageManifestEntries({ + packageName: dep.resolvedName, + packageRoot, + files, + target, + }); + manifestPackages.push(result.packageManifest); + Object.assign(assets, result.assets); + } + + const manifest = { + version: NATIVE_ASSET_MANIFEST_VERSION, + target, + packages: manifestPackages, + }; + + return { + manifest, + manifestJson: `${JSON.stringify(manifest, null, 2)}\n`, + assets, + }; +} diff --git a/apps/kimi-code/scripts/native/build.mjs b/apps/kimi-code/scripts/native/build.mjs new file mode 100644 index 000000000..ec51d2368 --- /dev/null +++ b/apps/kimi-code/scripts/native/build.mjs @@ -0,0 +1,47 @@ +import { parseArgs } from 'node:util'; + +import { runBundleStep } from './01-bundle.mjs'; +import { runInjectStep } from './03-inject.mjs'; +import { runSeaBlobStep } from './02-sea-blob.mjs'; +import { runSignStep } from './04-sign.mjs'; +import { runVerifyStep } from './05-verify.mjs'; + +const { values } = parseArgs({ + options: { + profile: { type: 'string', default: 'local' }, + }, +}); + +const profile = values.profile; +if (!['local', 'release'].includes(profile)) { + console.error(`Unknown profile: ${profile}. Expected 'local' or 'release'.`); + process.exit(1); +} + +function ensureNodeVersion() { + const [major, minor] = process.versions.node.split('.').map(Number); + if (major < 24 || (major === 24 && minor < 15)) { + console.error( + `Kimi Code native SEA build requires Node.js >=24.15.0, current ${process.versions.node}.`, + ); + process.exit(1); + } +} + +ensureNodeVersion(); +console.log(`==> Native build (profile=${profile})`); + +await runBundleStep(); +await runSeaBlobStep(); +await runInjectStep(); + +const identity = + profile === 'release' ? (process.env.APPLE_SIGNING_IDENTITY ?? '-') : '-'; +const keychainPath = profile === 'release' ? (process.env.APPLE_KEYCHAIN_PATH ?? null) : null; +await runSignStep({ identity, keychainPath }); + +// Verify always runs (codesign -dv); spctl gatekeeper gate only after notarization +// (CI macos-notarize composite action) — orchestrator just self-checks signing here. +await runVerifyStep({ requireGatekeeper: false }); + +console.log('==> Native build complete'); diff --git a/apps/kimi-code/scripts/native/check-bundle.mjs b/apps/kimi-code/scripts/native/check-bundle.mjs new file mode 100644 index 000000000..a0479f209 --- /dev/null +++ b/apps/kimi-code/scripts/native/check-bundle.mjs @@ -0,0 +1,90 @@ +import { builtinModules } from 'node:module'; +import { readFileSync } from 'node:fs'; + +import { nativeJsBundlePath } from './paths.mjs'; + +const bundlePath = nativeJsBundlePath(); +const text = readFileSync(bundlePath, 'utf-8'); + +const builtins = new Set([ + ...builtinModules, + ...builtinModules.map((name) => `node:${name}`), +]); + +const optionalRuntimeRequires = new Set([ + 'ajv-formats/dist/formats', + 'ajv/dist/runtime/validation_error', + 'bufferutil', + 'canvas', + 'chokidar', + 'cpu-features', + 'utf-8-validate', +]); +const optionalRelativeRuntimeRequires = new Set(['./crypto/build/Release/sshcrypto.node']); +const handledNativeRuntimeRequires = new Set(['koffi']); + +function isAllowedSpecifier(specifier) { + if (builtins.has(specifier) || specifier.startsWith('node:')) return true; + if (optionalRuntimeRequires.has(specifier)) return true; + if (handledNativeRuntimeRequires.has(specifier)) return true; + return false; +} + +const errors = []; + +function executableLines() { + return text + .split('\n') + .map((line) => line.trim()) + .filter((line) => { + if (line.length === 0) return false; + if (line.startsWith('*') || line.startsWith('//') || line.startsWith('/*')) return false; + return true; + }); +} + +for (const line of executableLines()) { + for (const match of line.matchAll(/\brequire\(\s*["']([^"']+)["']\s*\)/g)) { + const specifier = match[1]; + if (specifier.startsWith('.') || specifier.startsWith('/')) { + if (optionalRelativeRuntimeRequires.has(specifier)) continue; + errors.push(`relative require remains: ${specifier}`); + continue; + } + if (!isAllowedSpecifier(specifier)) { + errors.push(`external require remains: ${specifier}`); + } + } + + for (const match of line.matchAll(/(? 0) { + console.error(`Native JS bundle check failed for ${bundlePath}:`); + for (const error of errors) { + console.error(`- ${error}`); + } + process.exit(1); +} diff --git a/apps/kimi-code/scripts/native/entitlements.plist b/apps/kimi-code/scripts/native/entitlements.plist new file mode 100644 index 000000000..d7e0e9d6b --- /dev/null +++ b/apps/kimi-code/scripts/native/entitlements.plist @@ -0,0 +1,24 @@ + + + + + + com.apple.security.cs.allow-jit + + + + com.apple.security.cs.allow-unsigned-executable-memory + + + + com.apple.security.cs.disable-library-validation + + + + com.apple.security.cs.allow-dyld-environment-variables + + + diff --git a/apps/kimi-code/scripts/native/exec.mjs b/apps/kimi-code/scripts/native/exec.mjs new file mode 100644 index 000000000..03ebe5b0c --- /dev/null +++ b/apps/kimi-code/scripts/native/exec.mjs @@ -0,0 +1,61 @@ +import { execFile } from 'node:child_process'; +import { basename } from 'node:path'; +import { promisify } from 'node:util'; + +import { appRoot } from './paths.mjs'; + +const execFileAsync = promisify(execFile); + +export function commandForExecFile(command, args, platform = process.platform, env = process.env) { + if (platform !== 'win32' || !/\.(?:bat|cmd)$/i.test(command)) { + return { command, args }; + } + const shellCommand = [command, ...args] + .map((arg) => `"${String(arg).replaceAll('"', '""')}"`) + .join(' '); + return { + command: env.ComSpec ?? 'cmd.exe', + args: ['/d', '/s', '/c', `"${shellCommand}"`], + options: { windowsVerbatimArguments: true }, + }; +} + +export function fail(message) { + console.error(message); + process.exit(1); +} + +export async function run(command, args, options = {}) { + const exec = commandForExecFile(command, args); + try { + const { stdout, stderr } = await execFileAsync(exec.command, exec.args, { + cwd: appRoot, + maxBuffer: 1024 * 1024 * 16, + ...exec.options, + ...options, + }); + if (stdout.trim()) console.log(stdout.trim()); + if (stderr.trim()) console.error(stderr.trim()); + } catch (error) { + const details = [error.stdout?.trim(), error.stderr?.trim(), error.message] + .filter(Boolean) + .join('\n'); + fail(`Command failed: ${basename(command)} ${args.join(' ')}\n${details}`); + } +} + +export async function tryRun(command, args, options = {}) { + const exec = commandForExecFile(command, args); + try { + await execFileAsync(exec.command, exec.args, { + cwd: appRoot, + maxBuffer: 1024 * 1024 * 16, + ...exec.options, + }); + } catch (error) { + const details = [error.stdout?.trim(), error.stderr?.trim(), error.message] + .filter(Boolean) + .join('\n'); + console.warn(`Warning: ${basename(command)} ${args.join(' ')} failed.\n${details}`); + } +} diff --git a/apps/kimi-code/scripts/native/manifest.mjs b/apps/kimi-code/scripts/native/manifest.mjs new file mode 100644 index 000000000..910738943 --- /dev/null +++ b/apps/kimi-code/scripts/native/manifest.mjs @@ -0,0 +1,13 @@ +export const NATIVE_ASSET_MANIFEST_VERSION = 1; + +export function buildManifestKey(target) { + return `native/${target}/manifest.json`; +} + +export function isManifestVersionSupported(version) { + return version === NATIVE_ASSET_MANIFEST_VERSION; +} + +export function buildAssetKey(target, packageRoot, relativePath) { + return `native/${target}/${packageRoot}/${relativePath}`; +} diff --git a/apps/kimi-code/scripts/native/native-deps.mjs b/apps/kimi-code/scripts/native/native-deps.mjs new file mode 100644 index 000000000..f195a3cb1 --- /dev/null +++ b/apps/kimi-code/scripts/native/native-deps.mjs @@ -0,0 +1,103 @@ +/** + * Native dependency registry. + * + * Each entry describes one native-bearing npm package: how to resolve its + * install root, what to collect (JS only / native binary only / both), and + * which other registered dep it nests under (for `pnpm`-style nested resolves). + * + * Adding a new native package = appending one object here. No edits to + * NATIVE_TARGETS table or resolvePackageRoot if/else chain. + */ + +export const SUPPORTED_TARGETS = Object.freeze([ + 'darwin-arm64', + 'darwin-x64', + 'linux-arm64', + 'linux-x64', + 'win32-arm64', + 'win32-x64', +]); + +const clipboardSubpackageByTarget = Object.freeze({ + 'darwin-arm64': '@mariozechner/clipboard-darwin-arm64', + 'darwin-x64': '@mariozechner/clipboard-darwin-x64', + 'linux-arm64': '@mariozechner/clipboard-linux-arm64-gnu', + 'linux-x64': '@mariozechner/clipboard-linux-x64-gnu', + 'win32-arm64': '@mariozechner/clipboard-win32-arm64-msvc', + 'win32-x64': '@mariozechner/clipboard-win32-x64-msvc', +}); + +const koffiTripletByTarget = Object.freeze({ + 'darwin-arm64': 'darwin_arm64', + 'darwin-x64': 'darwin_x64', + 'linux-arm64': 'linux_arm64', + 'linux-x64': 'linux_x64', + 'win32-arm64': 'win32_arm64', + 'win32-x64': 'win32_x64', +}); + +export function isSupportedTarget(target) { + return SUPPORTED_TARGETS.includes(target); +} + +/** + * @typedef {Object} NativeDepDescriptor + * @property {string} id — stable internal id used for parent refs + * @property {(target: string) => string} name + * — npm package name (may depend on target) + * @property {'js-only'|'native-files'|'js-and-native-file'|'virtual'} collect + * @property {string|null} parent + * — id of another registered dep this nests under (for pnpm), + * or null for top-level (resolvable from app root) + * @property {(target: string) => string[]} [nativeFileRelatives] + * — explicit list of .node files relative to package root + * (used by 'js-and-native-file'; native-files mode auto-scans *.node) + */ + +/** @type {readonly NativeDepDescriptor[]} */ +export const nativeDeps = Object.freeze([ + { + id: 'clipboard-host', + name: () => '@mariozechner/clipboard', + collect: 'js-only', + parent: null, + }, + { + id: 'clipboard-target', + name: (target) => clipboardSubpackageByTarget[target], + collect: 'native-files', + parent: 'clipboard-host', + }, + { + id: 'pi-tui', + name: () => '@earendil-works/pi-tui', + // pi-tui is bundled into main.cjs at build time — we don't collect it as + // a native dep, only register it so koffi can declare it as parent. + collect: 'virtual', + parent: null, + }, + { + id: 'koffi', + name: () => 'koffi', + collect: 'js-and-native-file', + parent: 'pi-tui', + nativeFileRelatives: (target) => [`build/koffi/${koffiTripletByTarget[target]}/koffi.node`], + }, +]); + +/** + * Resolve which deps need collecting for a given build target, with concrete names. + */ +export function resolveTargetDeps(target) { + if (!isSupportedTarget(target)) { + throw new Error(`Unsupported native asset target: ${target}`); + } + return nativeDeps + .filter((d) => d.collect !== 'virtual') + .map((d) => ({ + ...d, + resolvedName: d.name(target), + nativeFileRelatives: d.nativeFileRelatives?.(target) ?? [], + parentName: d.parent ? nativeDeps.find((p) => p.id === d.parent)?.name(target) ?? null : null, + })); +} diff --git a/apps/kimi-code/scripts/native/package.mjs b/apps/kimi-code/scripts/native/package.mjs new file mode 100644 index 000000000..e146af0ab --- /dev/null +++ b/apps/kimi-code/scripts/native/package.mjs @@ -0,0 +1,53 @@ +import { createHash } from 'node:crypto'; +import { createReadStream, createWriteStream } from 'node:fs'; +import { mkdir, stat, writeFile } from 'node:fs/promises'; +import { basename, resolve } from 'node:path'; +import { pipeline } from 'node:stream/promises'; + +import { ZipFile } from 'yazl'; + +import { executableName, nativeArtifactsDir, nativeBinPath, targetTriple } from './paths.mjs'; + +const target = targetTriple(); +const execName = executableName(); +const sourceBinary = nativeBinPath(target); +const artifactsDir = nativeArtifactsDir(); + +// Flat-name archive for GH Release (GitHub Release assets do not support subdirectories). +const artifactName = `kimi-code-${target}.zip`; +const artifactPath = resolve(artifactsDir, artifactName); +const checksumPath = `${artifactPath}.sha256`; + +function fail(message) { + console.error(message); + process.exit(1); +} + +async function sha256(path) { + return await new Promise((resolveHash, reject) => { + const hash = createHash('sha256'); + const stream = createReadStream(path); + stream.on('error', reject); + stream.on('data', (chunk) => hash.update(chunk)); + stream.on('end', () => resolveHash(hash.digest('hex'))); + }); +} + +try { + await stat(sourceBinary); +} catch { + fail(`Native executable not found at ${sourceBinary}. Run build:native:sea first.`); +} + +await mkdir(artifactsDir, { recursive: true }); + +const zip = new ZipFile(); +zip.addFile(sourceBinary, execName, { mode: 0o100755 }); +zip.end(); +await pipeline(zip.outputStream, createWriteStream(artifactPath)); + +const digest = await sha256(artifactPath); +await writeFile(checksumPath, `${digest} ${basename(artifactPath)}\n`); + +console.log(`Wrote native artifact: ${artifactPath}`); +console.log(`Wrote artifact checksum: ${checksumPath}`); diff --git a/apps/kimi-code/scripts/native/paths.mjs b/apps/kimi-code/scripts/native/paths.mjs new file mode 100644 index 000000000..ca0462072 --- /dev/null +++ b/apps/kimi-code/scripts/native/paths.mjs @@ -0,0 +1,57 @@ +import { resolve } from 'node:path'; + +export const appRoot = resolve(import.meta.dirname, '..', '..'); + +export function targetTriple({ platform = process.platform, arch = process.arch, env = process.env } = {}) { + return env.KIMI_CODE_BUILD_TARGET ?? `${platform}-${arch}`; +} + +export function executableName(platform = process.platform) { + return platform === 'win32' ? 'kimi.exe' : 'kimi'; +} + +export function nativeDistRoot() { + return resolve(appRoot, 'dist-native'); +} + +export function nativeIntermediatesDir() { + return resolve(nativeDistRoot(), 'intermediates'); +} + +export function nativeBinDir(target = targetTriple()) { + return resolve(nativeDistRoot(), 'bin', target); +} + +export function nativeBinPath(target = targetTriple(), platform = process.platform) { + return resolve(nativeBinDir(target), executableName(platform)); +} + +export function nativeJsBundlePath() { + return resolve(nativeIntermediatesDir(), 'main.cjs'); +} + +export function nativeBlobPath() { + return resolve(nativeIntermediatesDir(), 'kimi.blob'); +} + +export function nativeSeaConfigPath() { + return resolve(nativeIntermediatesDir(), 'sea-config.json'); +} + +export function nativeManifestDir(target = targetTriple()) { + return resolve(nativeIntermediatesDir(), 'native-assets', target); +} + +export function nativeArtifactsDir() { + return resolve(nativeDistRoot(), 'artifacts'); +} + +export function nativeSmokeHome() { + return resolve(nativeDistRoot(), 'smoke-home'); +} + +export function nativeManifestKey(target = targetTriple()) { + return `native/${target}/manifest.json`; +} + +export const SEA_SENTINEL_FUSE = 'NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2'; diff --git a/apps/kimi-code/scripts/native/produce-manifest.mjs b/apps/kimi-code/scripts/native/produce-manifest.mjs new file mode 100644 index 000000000..7718f64b1 --- /dev/null +++ b/apps/kimi-code/scripts/native/produce-manifest.mjs @@ -0,0 +1,55 @@ +/** + * Aggregate per-platform zip archive `.sha256` files into a single + * `manifest.json` written into the same input directory. + * + * Usage: + * node produce-manifest.mjs + * + * Input dir must contain files matching: kimi-code-.zip.sha256 + * (produced by package.mjs across the 6 native-build matrix runners). + * + * Output: + * /manifest.json ← consumed by install.sh / install.ps1 + * + */ + +import { readFile, readdir, writeFile } from 'node:fs/promises'; +import { basename, resolve } from 'node:path'; + +const [, , inputDir, tag] = process.argv; +if (!inputDir || !tag) { + console.error('Usage: produce-manifest.mjs '); + process.exit(1); +} + +// Tag 格式 `@moonshot-ai/kimi-code@x.y.z` 或 `vx.y.z` 或 `x.y.z`,都归一化到 x.y.z +const version = tag.replace(/^@moonshot-ai\/kimi-code@/, '').replace(/^v/, ''); + +const entries = await readdir(inputDir); +const sumFiles = entries.filter((f) => /^kimi-code-[a-z0-9-]+\.zip\.sha256$/.test(f)); + +if (sumFiles.length === 0) { + console.error(`No kimi-code-.zip.sha256 files found in ${inputDir}`); + process.exit(1); +} + +const platforms = {}; +for (const sumFile of sumFiles.sort()) { + const text = await readFile(resolve(inputDir, sumFile), 'utf-8'); + const [checksum] = text.trim().split(/\s+/, 1); + if (!checksum || !/^[a-f0-9]{64}$/.test(checksum)) { + console.error(`Invalid checksum in ${sumFile}: ${checksum}`); + process.exit(1); + } + const filename = basename(sumFile, '.sha256'); + // kimi-code-darwin-arm64.zip → darwin-arm64 + const target = filename.replace(/^kimi-code-/, '').replace(/\.zip$/, ''); + platforms[target] = { filename, checksum }; +} + +const manifest = { version, tag, platforms }; +const manifestPath = resolve(inputDir, 'manifest.json'); + +await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`); + +console.log(`Wrote ${manifestPath} (${Object.keys(platforms).length} platforms)`); diff --git a/apps/kimi-code/scripts/native/resolve-release.mjs b/apps/kimi-code/scripts/native/resolve-release.mjs new file mode 100644 index 000000000..10b4f9724 --- /dev/null +++ b/apps/kimi-code/scripts/native/resolve-release.mjs @@ -0,0 +1,55 @@ +import { appendFile, readFile } from 'node:fs/promises'; +import { resolve } from 'node:path'; + +import { appRoot } from './paths.mjs'; + +const packageName = '@moonshot-ai/kimi-code'; +const packageJson = JSON.parse( + await readFile(resolve(appRoot, 'package.json'), 'utf-8'), +); + +function parsePublishedPackages() { + const raw = process.env['CHANGESETS_PUBLISHED_PACKAGES']; + if (raw === undefined || raw.trim().length === 0) { + return []; + } + + try { + const parsed = JSON.parse(raw); + return Array.isArray(parsed) ? parsed : []; + } catch { + return []; + } +} + +function outputLine(name, value) { + return `${name}=${value}\n`; +} + +const publishedPackage = parsePublishedPackages().find( + (entry) => + typeof entry === 'object' && + entry !== null && + entry.name === packageName && + typeof entry.version === 'string', +); + +const version = publishedPackage?.version ?? packageJson.version; +const shouldPublish = publishedPackage !== undefined; +const tag = `${packageName}@${version}`; +const githubOutput = process.env['GITHUB_OUTPUT']; + +if (githubOutput !== undefined) { + await appendFile( + githubOutput, + [ + outputLine('should_publish', String(shouldPublish)), + outputLine('version', version), + outputLine('tag', tag), + ].join(''), + ); +} + +console.log(`should_publish=${String(shouldPublish)}`); +console.log(`version=${version}`); +console.log(`tag=${tag}`); diff --git a/apps/kimi-code/scripts/native/smoke.mjs b/apps/kimi-code/scripts/native/smoke.mjs new file mode 100644 index 000000000..ed3a8624f --- /dev/null +++ b/apps/kimi-code/scripts/native/smoke.mjs @@ -0,0 +1,82 @@ +import { execFile } from 'node:child_process'; +import { readFile, stat } from 'node:fs/promises'; +import { resolve } from 'node:path'; +import { promisify } from 'node:util'; + +import { appRoot, nativeBinPath, nativeSmokeHome, targetTriple } from './paths.mjs'; + +const execFileAsync = promisify(execFile); +const target = targetTriple(); +const executablePath = nativeBinPath(target); +const smokeHome = nativeSmokeHome(); +const packageJson = JSON.parse(await readFile(resolve(appRoot, 'package.json'), 'utf-8')); +const expectedVersion = packageJson.version; + +function fail(message) { + console.error(message); + process.exit(1); +} + +async function ensureExecutableExists() { + try { + await stat(executablePath); + } catch { + fail(`Native executable not found at ${executablePath}. Run build:native:sea first.`); + } +} + +async function runKimi(args) { + try { + const { stdout, stderr } = await execFileAsync(executablePath, args, { + cwd: appRoot, + maxBuffer: 1024 * 1024 * 16, + }); + return `${stdout}${stderr}`; + } catch (error) { + const detail = [error.stdout?.trim(), error.stderr?.trim(), error.message] + .filter(Boolean) + .join('\n'); + fail(`Native smoke failed: ${executablePath} ${args.join(' ')}\n${detail}`); + } +} + +async function runKimiWithEnv(args, env) { + try { + const { stdout, stderr } = await execFileAsync(executablePath, args, { + cwd: appRoot, + env: { ...process.env, ...env }, + maxBuffer: 1024 * 1024 * 16, + }); + return `${stdout}${stderr}`; + } catch (error) { + const detail = [error.stdout?.trim(), error.stderr?.trim(), error.message] + .filter(Boolean) + .join('\n'); + fail(`Native smoke failed: ${executablePath} ${args.join(' ')}\n${detail}`); + } +} + +function assertIncludes(output, expected, command) { + if (!output.includes(expected)) { + fail(`Native smoke output for "${command}" did not include "${expected}".\n${output}`); + } +} + +await ensureExecutableExists(); + +const versionOutput = await runKimi(['--version']); +assertIncludes(versionOutput, expectedVersion, '--version'); + +const helpOutput = await runKimi(['--help']); +assertIncludes(helpOutput, 'Usage: kimi', '--help'); + +const exportHelpOutput = await runKimi(['export', '--help']); +assertIncludes(exportHelpOutput, 'Usage: kimi export', 'export --help'); + +const nativeAssetOutput = await runKimiWithEnv(['--version'], { + KIMI_CODE_HOME: smokeHome, + KIMI_CODE_NATIVE_ASSET_SMOKE: '1', +}); +assertIncludes(nativeAssetOutput, `Native asset smoke passed: ${target}`, 'native asset smoke'); + +console.log(`Native smoke passed: ${executablePath}`); diff --git a/apps/kimi-code/scripts/postinstall.mjs b/apps/kimi-code/scripts/postinstall.mjs new file mode 100644 index 000000000..4662c43cf --- /dev/null +++ b/apps/kimi-code/scripts/postinstall.mjs @@ -0,0 +1,269 @@ +#!/usr/bin/env node +/** + * Postinstall hook for @moonshot-ai/kimi-code. + * + * Goal: when this package is installed globally, ensure typing `kimi` + * invokes the new TypeScript CLI. The npm `package.json` bin field + * installs a fresh `kimi` shim into the global bin dir; this script + * removes any pre-existing `kimi` shim left behind by the previous + * Python CLI (installed via `uv tool install`, `pipx install`, + * `pip install`, etc.) that would otherwise shadow ours via PATH + * ordering. The renamed shim is kept as `kimi-legacy` so users can + * still invoke the old CLI if they want to fall back. + * + * ## Hard rules + * + * - Only runs for global installs across npm, yarn (classic), and + * pnpm. Non-global installs (npx, local project deps, workspace + * bootstraps, `pnpm dlx`) are silent no-ops. + * - Never fails the install. Any error here is caught and reported, + * but the script always exits 0. + * - Does not touch a `kimi` we don't recognize as the previous + * Python CLI (matched by realpath-resolved shim head containing + * `kimi_cli`). + * - Cross-platform: POSIX and Windows. Windows-specific bits live + * in the helpers (PATHEXT-aware PATH walking, whole-file marker + * sniff for uv's Rust launcher .exe, extension-preserving + * rename target like `kimi.exe` → `kimi-legacy.exe`). + * + * ## Code layout + * + * This file is the orchestrator; the actual logic lives in + * sibling modules to keep each file under a manageable size: + * + * - `./postinstall/reach.mjs` — package-manager detection, + * global-install gate, own-package-root resolution, user-shell + * PATH lookup, reachability check. + * - `./postinstall/migrate.mjs` — legacy detection, + * `kimi`-vs-`kimi-legacy` classification, the rename / unlink + * primitives. + * - `./postinstall/ui.mjs` — `notify()` (with `/dev/tty` fallback), + * ANSI styling, the fixed-width box, and the five outcome + * renderers. + * + * ## Workflow + * + * What runs when a user types `npm install -g @moonshot-ai/kimi-code` + * (or the yarn / pnpm equivalent): + * + * 1. The manager extracts the package and runs lifecycle scripts. + * The `bin.kimi` mapping in `package.json` tells the manager to + * install a `kimi` shim under its global bin directory. + * 2. The manager invokes this script via the `scripts.postinstall` + * entry — orchestrated by `main` below. + * 3. Install-context gate: only proceed when this is a global + * install (`isGlobalInstall` checks `npm_config_global` / + * `pnpm_config_global` / `npm_config_location`). + * 4. Probe PATH once via `postinstallPaths()`: detection uses the + * union of shell PATH + process PATH; reachability uses the + * shell PATH alone (with a fallback to process PATH if the + * shell can't be probed). Sharing one probe keeps detection + * and reachability symmetric and avoids running `$SHELL -l` + * twice. + * 5. Detect EVERY previous Python `kimi-cli` shim on the detection + * PATH (`detectLegacyShims`). Returns `[]` for fresh-install / + * no-op. Multiple results happen when the user has installed + * `kimi-cli` through more than one Python tool (uv + pipx, or + * sudo-pip + pip-user). PATH order is preserved. + * 6. Pre-flight classify each shim (`classifyShim`) — pure + * filesystem inspection, no writes. Each shim ends up + * `renameable`, `consolidate`, `delete-only`, or `blocked`. + * 7. Decide abort vs proceed against the WHOLE set: + * `findFirstResolvableKimi` walks PATH treating the actionable + * shims as gone and reports what wins: + * - `own` → proceed to execute. + * - `blocked-legacy` → a legacy we can't remove still wins. + * Surface `logMigrationBlocked` with sudo / admin + * instructions; touch nothing. + * - `foreign` → some `kimi` we don't recognize (a user's own + * file) wins. Surface `logForeignKimiInTheWay` asking the + * user to delete or rename their own file; touch nothing. + * - `none` → no `kimi` on PATH at all (our shim's bin dir + * isn't in the shell's PATH). Surface + * `logNewCliNotOnPath`; touch nothing. + * 8. Execute. The FIRST classification in PATH order that we can + * touch becomes `kimi-legacy` (preserves what `kimi` referred + * to before this install). Each subsequent shim is `unlink`ed — + * keeping it as a dormant duplicate adds no value. If the + * first shim's `kimi-legacy` target is already user-managed, + * we delete `kimi` anyway (still achieves takeover) and tell + * the user we couldn't preserve a fallback. Extension is + * preserved on Windows (`kimi.exe` → `kimi-legacy.exe`). + * 9. One end-of-orchestration notice (`logMigrationDone`) + * summarizes every action — renames, consolidates, + * delete-only, deletes, and harmless blocked leftovers. The + * takeover-success line only fires on this path because Step 7 + * already certified it. + * 10. The manager completes the install with its usual summary. + * This script always exits 0; any uncaught error is swallowed + * by the top-level `catch` so the install never fails because + * of the migration. + */ + +import { + detectPackageManager, + findFirstResolvableKimi, + isGlobalInstall, + ownPackageRoot, + postinstallPaths, +} from './postinstall/reach.mjs'; +import { + classifyShim, + deleteShim, + detectLegacyShims, + renameInPlace, +} from './postinstall/migrate.mjs'; +import { + logForeignKimiInTheWay, + logMigrationBlocked, + logMigrationDone, + logNewCliNotOnPath, + notify, +} from './postinstall/ui.mjs'; + +async function main() { + // Step 1: skip non-global installs (npx, local project deps, + // workspace bootstraps). Windows is supported natively; the + // platform-specific bits (PATHEXT-aware PATH walk, whole-file + // marker sniff for uv's launcher .exe, extension-preserving + // rename) live in the helpers. + if (!isGlobalInstall()) return; + + // Step 2: locate our own installed package root once and share it + // with both detection (skip files inside our package) and + // reachability (only count our shim as "found"). + const ownRoot = await ownPackageRoot(import.meta.dirname); + const pm = detectPackageManager(); + + // Step 3: probe the user's shell PATH once so detection and + // reachability share a single consistent view. Detection uses the + // union of shell PATH + process PATH (so we catch a legacy shim + // visible to either); reachability uses the shell PATH alone (so + // we don't claim "kimi works now" when the shim only sits in the + // installer's env). + const paths = await postinstallPaths(); + + // Step 4: detect EVERY previous Python `kimi-cli` shim on the + // detection PATH. A user with both `uv tool install` and `pipx + // install` would have two; we must address all of them or the + // survivor still shadows the new CLI. + const detections = await detectLegacyShims(ownRoot, paths.detection); + if (detections.length === 0) return; + + // Step 5: pre-flight classify every shim WITHOUT touching the + // filesystem yet. The orchestrator decides abort-or-proceed against + // the whole set rather than discovering mid-loop that we got partway + // and have to backtrack. + const classifications = await Promise.all( + detections.map(async (detection) => { + const c = await classifyShim(detection.shimPath); + return { ...c, detection }; + }), + ); + + // Step 6: figure out what wins PATH resolution once every shim we + // CAN touch is treated as gone. Three possible blockers: + // - a legacy shim we couldn't classify as actionable (sudo/admin + // needed) + // - an unrelated `kimi` we don't recognize (a user's own wrapper + // script — they own the decision) + // - nothing resolves (our shim isn't on PATH at all) + // For each we render a different notice and touch NOTHING. The + // common-case fourth result is "our shim wins" — we proceed. + const actionable = classifications.filter((c) => c.kind !== 'blocked'); + const blocked = classifications.filter((c) => c.kind === 'blocked'); + const actionableShimPaths = actionable.map((c) => c.shimPath); + const allDetectedShimPaths = classifications.map((c) => c.shimPath); + + const blocker = await findFirstResolvableKimi( + ownRoot, + paths.reachability, + actionableShimPaths, + allDetectedShimPaths, + ); + if (blocker.kind !== 'own') { + if (blocker.kind === 'blocked-legacy') { + logMigrationBlocked(blocked, actionable, pm); + } else if (blocker.kind === 'foreign') { + logForeignKimiInTheWay(blocker.path, pm); + } else { + // 'none' — our shim isn't on PATH at all. + logNewCliNotOnPath(detections[0], pm); + } + return; + } + + // Step 7: execute. The FIRST classification in PATH order that + // we can touch becomes `kimi-legacy` (preserves what the user's + // `kimi` used to refer to). Every subsequent shim is just + // deleted — keeping it as a dormant duplicate adds no value. + const renames = []; + const consolidates = []; + const skippedForeignTarget = []; + const deletes = []; + const errors = []; + let preservedFirst = false; + + for (const c of classifications) { + if (c.kind === 'blocked') continue; // already established harmless + + if (!preservedFirst) { + preservedFirst = true; + if (c.kind === 'renameable') { + const r = await renameInPlace(c.shimPath, c.target); + if (r.success) { + renames.push(c); + } else { + errors.push({ ...c, ...r }); + } + continue; + } + if (c.kind === 'consolidate') { + const r = await deleteShim(c.shimPath); + if (r.success) { + consolidates.push(c); + } else { + errors.push({ ...c, ...r }); + } + continue; + } + if (c.kind === 'delete-only') { + const r = await deleteShim(c.shimPath); + if (r.success) { + skippedForeignTarget.push(c); + } else { + errors.push({ ...c, ...r }); + } + continue; + } + } else { + // Not the first actionable shim. Just delete it. + const r = await deleteShim(c.shimPath); + if (r.success) { + deletes.push(c); + } else { + errors.push({ ...c, ...r }); + } + } + } + + // Step 8: one notice summarizing everything that happened. The + // takeover-success language is only emitted when we know it's true + // (we already passed the reachability gate above). + logMigrationDone( + { + renames, + consolidates, + skippedForeignTarget, + deletes, + blockedHarmless: blocked, + errors, + }, + pm, + ); +} + +main().catch((err) => { + const message = err instanceof Error ? err.message : String(err); + notify(`[kimi-code] postinstall warning: ${message}`); +}); diff --git a/apps/kimi-code/scripts/postinstall/migrate.mjs b/apps/kimi-code/scripts/postinstall/migrate.mjs new file mode 100644 index 000000000..b1ae695a3 --- /dev/null +++ b/apps/kimi-code/scripts/postinstall/migrate.mjs @@ -0,0 +1,351 @@ +/** + * Detection of the previous Python `kimi-cli` shim and the actual + * filesystem operations that perform (or refuse) the rename. + * + * Detection: + * - {@link detectLegacyShims}: walks the caller-supplied + * `pathString` and returns every legacy `kimi` shim along the + * way (in PATH order). A shim qualifies when it realpath-resolves + * outside our own installed package root and its head 4 KiB + * contains the `kimi_cli` module marker — the setuptools + * entry-point format produced by `uv tool install`, + * `pipx install`, `pip install`, etc. Returning all hits (not + * just the first) matters because a user with both uv- and + * pipx-installed `kimi-cli` has two legacy shims in different + * dirs, and renaming only the earlier one leaves the later one + * shadowing our new CLI. Callers should pass the `detection` + * field from `postinstallPaths()` so detection sees the union of + * the shell PATH and the installer's PATH. + * - {@link isLegacyShim}: same criterion in standalone form, used to + * decide whether an existing `kimi-legacy` is itself a legacy CLI + * (safe to consolidate over) or a user-managed file (preserve). + * + * Classify + execute (two-phase): + * - {@link classifyShim}: pre-flight inspection that says what we + * COULD do to a given shim. Returns one of `renameable`, + * `consolidate`, `delete-only`, `blocked`. No filesystem writes. + * - {@link renameInPlace} / {@link deleteShim}: the primitive + * operations that actually mutate the filesystem. Run after the + * orchestrator has looked at the full set of classifications and + * decided to proceed. + * + * Splitting classification from execution lets the orchestrator make + * the abort-or-proceed decision once, against the whole detected set, + * rather than discovering mid-loop that something failed and ending up + * with a misleading "kimi now launches the new CLI" notice in front of + * a "permission denied" notice. Uses `fs.lstat` (not `fs.access`) to + * detect dangling symlinks at the target so we don't clobber them. + */ + +import { constants as fsConstants, promises as fs } from 'node:fs'; +import { delimiter, dirname, extname, join, sep } from 'node:path'; + +const LEGACY_BIN = 'kimi'; +const LEGACY_RENAME = 'kimi-legacy'; +const PYTHON_MARKER = 'kimi_cli'; +const IS_WINDOWS = process.platform === 'win32'; + +// Read window for the marker sniff. +// POSIX: setuptools entry-point scripts are a few hundred bytes — +// 4 KiB is generous. +// Windows: `uv tool install` produces a Rust-built launcher .exe +// (~45 KiB) and the `kimi_cli` module name sits embedded +// near the END of the file (verified offset ≈ 44103 on +// uv 0.11). Cap reads at 256 KiB so a hostile or +// unexpectedly large file can't make us hold a lot of +// memory. +const SHIM_SNIFF_BYTES_POSIX = 4096; +const SHIM_SNIFF_BYTES_WINDOWS_MAX = 256 * 1024; + +function pathEntries(pathString) { + if (!pathString) return []; + const seen = new Set(); + const out = []; + for (const entry of pathString.split(delimiter)) { + if (!entry || seen.has(entry)) continue; + seen.add(entry); + out.push(entry); + } + return out; +} + +/** + * Expand `kimi` into the set of filenames that resolve as executables + * on this platform. POSIX → just `['kimi']`. Windows → adds every + * `PATHEXT` extension (so we find `kimi.exe`, `kimi.cmd`, etc). + */ +function executableCandidates(basename) { + if (!IS_WINDOWS) return [basename]; + const pathext = (process.env['PATHEXT'] ?? '.EXE;.CMD;.BAT;.COM') + .toLowerCase() + .split(';') + .map((e) => e.trim()) + .filter(Boolean); + return [basename, ...pathext.map((ext) => basename + ext)]; +} + +async function isExecutableFile(filePath) { + try { + const info = await fs.stat(filePath); + if (!info.isFile()) return false; + // Windows: stat().mode doesn't reflect ACLs in any useful way. + // Callers already restrict to PATHEXT candidates, so existence + // suffices. + if (IS_WINDOWS) return true; + return (info.mode & 0o111) !== 0; + } catch { + return false; + } +} + +async function readShimHead(filePath) { + let handle; + try { + handle = await fs.open(filePath, 'r'); + const stat = await handle.stat(); + const limit = IS_WINDOWS ? SHIM_SNIFF_BYTES_WINDOWS_MAX : SHIM_SNIFF_BYTES_POSIX; + const target = Math.min(stat.size, limit); + const buffer = Buffer.alloc(target); + const { bytesRead } = await handle.read(buffer, 0, target, 0); + // `latin1` is a 1-to-1 byte→char mapping; we're searching for an + // ASCII substring, so we don't want UTF-8 decoding to mangle the + // bytes around the marker. + return buffer.subarray(0, bytesRead).toString('latin1'); + } catch { + return null; + } finally { + if (handle) await handle.close().catch(() => {}); + } +} + +/** + * Walk `pathString` and return every legacy `kimi` shim along the + * way, in PATH order. The orchestrator renames each in turn — a + * single rename is insufficient when the user has multiple legacy + * installs (e.g. one from `uv tool install` and another from `pipx + * install`) in different directories, because the survivor still + * shadows the new CLI. + * + * Each entry has the same shape as the previous single-return + * value: `{ shimPath, realPath }`. The empty array means + * "fresh-install / no-op". + */ +export async function detectLegacyShims(ownRoot, pathString) { + const ownRootPrefix = ownRoot ? ownRoot + sep : null; + const candidates = executableCandidates(LEGACY_BIN); + const results = []; + const seenShims = new Set(); + + for (const dir of pathEntries(pathString)) { + for (const name of candidates) { + const shimPath = join(dir, name); + if (seenShims.has(shimPath)) continue; + if (!(await isExecutableFile(shimPath))) continue; + + let realPath; + try { + realPath = await fs.realpath(shimPath); + } catch { + continue; + } + + // Defence-in-depth: never touch a `kimi` that resolves into our + // own installed package. The `kimi_cli` marker check below + // already excludes the manager's generated wrapper today, but + // this layer keeps us safe if anything in our bundle ever + // happens to contain the marker substring. + if ( + ownRootPrefix !== null && + (realPath === ownRoot || realPath.startsWith(ownRootPrefix)) + ) { + continue; + } + + const head = await readShimHead(realPath); + if (!head || !head.includes(PYTHON_MARKER)) continue; + + seenShims.add(shimPath); + results.push({ shimPath, realPath }); + } + } + return results; +} + +/** + * Does the file at `p` look like the legacy Python `kimi-cli`? + * + * Same criterion as {@link detectLegacyShim} uses to recognize the + * original shim: realpath-resolvable and the first 4 KiB of the + * resolved file contains the `kimi_cli` module name. Used to decide + * whether an existing `kimi-legacy` is itself a legacy shim (safe to + * drop the duplicate `kimi`) or a user-managed file we must not + * clobber. + */ +export async function isLegacyShim(p) { + let real; + try { + real = await fs.realpath(p); + } catch { + return false; + } + const head = await readShimHead(real); + return Boolean(head && head.includes(PYTHON_MARKER)); +} + +async function pathExists(p) { + try { + // lstat (not access/stat) so a dangling symlink at `p` still + // reports as existing — `fs.access` follows symlinks and would + // return ENOENT for a broken link, after which `fs.rename` would + // silently replace it. + await fs.lstat(p); + return true; + } catch { + return false; + } +} + +/** + * Compute where `shimPath` should be renamed to. Preserves the file + * extension so a Windows `C:\…\kimi.exe` ends up at `kimi-legacy.exe` + * rather than an extension-less `kimi-legacy` that `kimi.exe -- legacy` + * shells won't run. + */ +function renameTargetFor(shimPath) { + const ext = extname(shimPath); // "" on POSIX, ".exe" on Windows + return join(dirname(shimPath), LEGACY_RENAME + ext); +} + +/** + * Is the directory containing `shimPath` a system-managed location + * the current user can't write to? + * + * POSIX: dir owned by uid 0 (root) — captures + * `sudo pip install kimi-cli` → `/usr/local/bin/`. + * + * Windows: dir under one of the well-known system roots + * (`C:\Program Files`, `C:\ProgramData`, `C:\Windows`). uv tool + * installs land in user space (`%USERPROFILE%\.local\bin`) so this + * path almost never fires on Windows in practice, but the heuristic + * correctly classifies the rare admin-prefix install case. + * + * The dedicated permission-denied notice (`logPermissionDenied`) + * uses this to switch from a bare "rename it manually" message to a + * sudo-aware / admin-aware explanation. + */ +async function isSystemOwnedDir(shimPath) { + if (IS_WINDOWS) { + const dir = dirname(shimPath).toLowerCase(); + const systemRoots = [ + 'c:\\program files', + 'c:\\program files (x86)', + 'c:\\programdata', + 'c:\\windows', + ]; + return systemRoots.some( + (root) => dir === root || dir.startsWith(root + '\\'), + ); + } + try { + const info = await fs.stat(dirname(shimPath)); + return info.uid === 0; + } catch { + return false; + } +} + +async function canWriteDir(dir) { + try { + // For `fs.rename` and `fs.unlink` the parent dir needs to be both + // writable and executable (POSIX `wx`). `fs.access` follows the + // same semantics. On Windows ACL details are coarse but + // `fs.access(W_OK)` is a reasonable best-effort. + await fs.access(dir, fsConstants.W_OK | fsConstants.X_OK); + return true; + } catch { + return false; + } +} + +/** + * Pre-flight inspection of a single legacy shim. Returns what action + * we could take WITHOUT executing it. The orchestrator uses these + * classifications to decide the whole-set strategy (abort vs proceed, + * which shim becomes kimi-legacy) before any filesystem writes + * happen. + * + * Result shapes (all carry `shimPath` and `target` so the renderer + * has the paths): + * - `renameable` : can `fs.rename(shim → target)` cleanly; + * target slot is free. + * - `consolidate` : target already exists and is itself a legacy + * shim; we'd `fs.unlink(shim)` and leave the + * existing `kimi-legacy` as the canonical + * fallback (functionally equivalent — same + * upstream package). + * - `delete-only` : target exists but is a user-managed file we + * won't clobber. We can still `fs.unlink(shim)` + * to stop the shadowing; the + * "preserve original kimi" invariant fails for + * THIS dir (we tell the user). + * - `blocked` : we can't write to the parent dir. Carries + * `isSystemPath` so the renderer can suggest + * sudo (POSIX) or admin PowerShell (Windows). + */ +export async function classifyShim(shimPath) { + const target = renameTargetFor(shimPath); + const dir = dirname(shimPath); + + if (!(await canWriteDir(dir))) { + return { + kind: 'blocked', + shimPath, + target, + isSystemPath: await isSystemOwnedDir(shimPath), + }; + } + + if (await pathExists(target)) { + if (await isLegacyShim(target)) { + return { kind: 'consolidate', shimPath, target }; + } + return { kind: 'delete-only', shimPath, target }; + } + + return { kind: 'renameable', shimPath, target }; +} + +/** + * Execute an `fs.rename`. Pre-flight classification establishes + * whether this is expected to succeed; this primitive just runs the + * call and wraps the error. + */ +export async function renameInPlace(shimPath, target) { + try { + await fs.rename(shimPath, target); + return { success: true }; + } catch (err) { + const code = err && typeof err === 'object' ? err.code : undefined; + const message = err instanceof Error ? err.message : String(err); + return { success: false, code, message }; + } +} + +/** + * Execute an `fs.unlink`. Used when: + * - we're consolidating onto an existing legacy `kimi-legacy`, or + * - we couldn't rename (foreign target) but can still remove the + * shadow, or + * - this is a non-first shim and we just want to clear it out so + * PATH order resolves to our new CLI. + */ +export async function deleteShim(shimPath) { + try { + await fs.unlink(shimPath); + return { success: true }; + } catch (err) { + const code = err && typeof err === 'object' ? err.code : undefined; + const message = err instanceof Error ? err.message : String(err); + return { success: false, code, message }; + } +} diff --git a/apps/kimi-code/scripts/postinstall/reach.mjs b/apps/kimi-code/scripts/postinstall/reach.mjs new file mode 100644 index 000000000..e6a4c9385 --- /dev/null +++ b/apps/kimi-code/scripts/postinstall/reach.mjs @@ -0,0 +1,457 @@ +/** + * Where the user actually types things, and what `kimi` resolves to + * from there. + * + * Covers: + * + * - Package-manager detection ({@link detectPackageManager}) and + * manager-specific command hints ({@link pmGlobalBinCommand}, + * {@link pmGlobalInstallCommand}) used in user-facing notices. + * - Global-install gating ({@link isGlobalInstall}) — what counts as + * a global install across npm / yarn classic / pnpm. + * - Own-package-root location ({@link ownPackageRoot}) — walks up + * from `import.meta.dirname` looking for `package.json`. + * - User-shell PATH ({@link userShellPath}) — spawns `$SHELL -l` so + * we can check reachability from the shell the user will type + * `kimi` into, not just the installer's environment. + * - The combined PATH dispatcher ({@link postinstallPaths}) — + * called once by the orchestrator so detection and reachability + * stay symmetric and the shell probe doesn't run twice. + * - The reachability check ({@link findFirstResolvableKimi}) — + * walks PATH treating the to-be-renamed legacy shims as gone + * and reports what wins resolution. Distinguishes our own shim + * from a blocked legacy that's still in the way vs. an + * unknown/foreign `kimi` (e.g. a user-written wrapper). + * + * All functions are pure w.r.t. the filesystem (no mutations). + */ + +import { spawn } from 'node:child_process'; +import { promises as fs } from 'node:fs'; +import { delimiter, dirname, join, sep } from 'node:path'; + +const LEGACY_BIN = 'kimi'; +const IS_WINDOWS = process.platform === 'win32'; + +/** + * Expand a basename like `kimi` into the set of filenames the OS + * would actually match on PATH. + * + * On POSIX: just `['kimi']`. + * + * On Windows: `['kimi', 'kimi.exe', 'kimi.cmd', …]` — every + * extension in `PATHEXT`. Without this, our PATH walk would miss + * the typical `kimi.exe` shim produced by `uv tool install` on + * Windows. + */ +export function executableCandidates(basename) { + if (!IS_WINDOWS) return [basename]; + const pathext = (process.env['PATHEXT'] ?? '.EXE;.CMD;.BAT;.COM') + .toLowerCase() + .split(';') + .map((e) => e.trim()) + .filter(Boolean); + return [basename, ...pathext.map((ext) => basename + ext)]; +} + +/** + * Identify which package manager ran us. `npm_config_user_agent` is + * set by npm, yarn (classic + berry), and pnpm, and starts with the + * manager's name plus version. Defaults to `'npm'` for unknown / + * missing values. + */ +export function detectPackageManager() { + const ua = process.env['npm_config_user_agent'] ?? ''; + if (ua.startsWith('pnpm/')) return 'pnpm'; + if (ua.startsWith('yarn/')) return 'yarn'; + return 'npm'; +} + +/** + * Manager-specific shell command that prints (or expands to) the + * global bin directory. Used in the PATH-fix hint so the suggestion + * is valid regardless of which manager the user ran. + */ +export function pmGlobalBinCommand(pm) { + switch (pm) { + case 'pnpm': + return 'pnpm bin -g'; + case 'yarn': + return 'yarn global bin'; + case 'npm': + default: + return 'npm prefix -g'; + } +} + +/** Manager-specific reinstall command, used in the success-notice hint. */ +export function pmGlobalInstallCommand(pm, pkg) { + switch (pm) { + case 'pnpm': + return `pnpm add -g ${pkg}`; + case 'yarn': + return `yarn global add ${pkg}`; + case 'npm': + default: + return `npm install -g ${pkg}`; + } +} + +/** + * Did the user run a global install via some Node package manager? + * + * We accept four signals, any of which means "this install is + * landing in the manager's global bin directory": + * + * - `npm_config_global === 'true'` — set by `npm install -g`, and + * by pnpm on its global paths for back-compat. + * - `pnpm_config_global === 'true'` — set by pnpm in addition to + * the npm-compat flag above. + * - `npm_config_location === 'global'` — set by npm 7+ when the + * user passes `--location=global` (or runs `npm config set + * location global` persistently). npm intentionally does NOT + * also set `npm_config_global` in this case, so without this + * branch the migration silently no-ops for `npm install + * --location=global @moonshot-ai/kimi-code` — verified on npm + * 11.13.0. + * - {@link isYarnClassicGlobalAdd} — yarn classic does NOT set + * `npm_config_global` for `yarn global add`. The only reliable + * signal is parsing `npm_config_argv` and seeing the `global` + * subcommand. Verified on yarn 1.22.22. + * + * Local installs, `npx`, `pnpm dlx`, and workspace bootstraps leave + * all four signals false. + * + * Yarn berry (v2+) intentionally has no global-install concept, so + * it doesn't matter here. Postinstall on yarn berry runs in local + * context. + */ +export function isGlobalInstall() { + return ( + process.env['npm_config_global'] === 'true' || + process.env['pnpm_config_global'] === 'true' || + process.env['npm_config_location'] === 'global' || + isYarnClassicGlobalAdd() + ); +} + +/** + * `yarn global add` (yarn classic, v1.x) runs lifecycle scripts but + * leaves both `npm_config_global` and `npm_config_location` unset. + * The only reliable in-band signal is `npm_config_argv`, which yarn + * populates with the original command line as JSON: + * { original: ["global", "add", "", "--prefix=..."] } + * Parse it and require both: + * - `npm_config_user_agent` starts with `yarn/1.` (yarn classic; + * yarn berry has no global concept anyway). + * - Some token in argv is literally `"global"` AND the very next + * token is a known yarn-global subcommand (`add`, `remove`, + * etc). This handles the simple case (`yarn global add foo` → + * argv `["global","add",...]`) and the value-taking-flag case + * (`yarn --cwd /tmp global add foo` → argv `["--cwd","/tmp", + * "global","add",...]`) without having to maintain yarn's full + * flag table. It rejects `yarn add global` (the next token is + * undefined) and `yarn add @scope/global` (the literal string + * `"global"` doesn't appear). The remaining false positives + * (e.g., `yarn add global add` — installing two packages, one + * literally named `global`) are caught downstream by the + * reachability gate: a local install's bin dir isn't on PATH, + * so `isOwnCliResolvableFirst` will refuse to migrate. + */ +function isYarnClassicGlobalAdd() { + const ua = process.env['npm_config_user_agent'] ?? ''; + if (!ua.startsWith('yarn/1.')) return false; + const raw = process.env['npm_config_argv']; + if (!raw) return false; + let argv; + try { + argv = JSON.parse(raw); + } catch { + return false; + } + if (!Array.isArray(argv?.original)) return false; + const globalIdx = argv.original.indexOf('global'); + if (globalIdx === -1) return false; + const next = argv.original[globalIdx + 1]; + return typeof next === 'string' && YARN_GLOBAL_SUBCOMMANDS.has(next); +} + +// Yarn 1.x global subcommands. The install-class ones (`add`, +// `upgrade`, `upgrade-interactive`) are the ones that actually run +// our postinstall, but the read-only ones are included so the +// detection is consistent across all `yarn global ...` invocations. +const YARN_GLOBAL_SUBCOMMANDS = new Set([ + 'add', + 'remove', + 'upgrade', + 'upgrade-interactive', + 'list', + 'bin', + 'dir', +]); + +/** + * Locate the realpath of our own installed package root. + * + * Callers pass the starting directory (typically `import.meta.dirname` + * of the entry script, which lives at `/scripts/`). We + * walk up looking for the nearest `package.json`, then `realpath` the + * directory so symlinked install layouts (e.g. `/bin/kimi` + * symlinked into `lib/node_modules/.../dist/main.mjs`) compare equal + * in the caller's prefix check. + * + * Returns null if no `package.json` is found within a few levels — + * callers should treat that as "can't locate ourselves" and bail. + */ +export async function ownPackageRoot(startDir) { + let dir = startDir; + for (let i = 0; i < 6; i++) { + try { + await fs.access(join(dir, 'package.json')); + try { + return await fs.realpath(dir); + } catch { + return dir; + } + } catch { + // No `package.json` here; walk up. + } + const parent = dirname(dir); + if (parent === dir) break; + dir = parent; + } + return null; +} + +async function isExecutableFile(filePath) { + try { + const info = await fs.stat(filePath); + if (!info.isFile()) return false; + // Windows: ACLs aren't visible through stat().mode meaningfully — + // existence + a recognized extension is what PATHEXT-style lookup + // checks. Callers only pass us candidates that already match an + // extension in `executableCandidates()`, so "is a file" suffices. + if (IS_WINDOWS) return true; + return (info.mode & 0o111) !== 0; + } catch { + return false; + } +} + +// Package-manager shim generators embed the package's resolved path +// into the wrapper file. So any shim whose first KB contains our +// package name is one we own. Used as a fallback when realpath alone +// can't catch the shim — that happens for: +// - Windows cmd-shims (literal `.cmd` / `.ps1` files, not symlinks) +// - pnpm POSIX shims (literal `/bin/sh` scripts, not symlinks; pnpm +// does not symlink into the package root the way npm/yarn classic +// do on POSIX) +const PACKAGE_NAME_MARKERS = ['@moonshot-ai/kimi-code', '@moonshot-ai\\kimi-code']; + +async function shimReferencesOwnPackage(shimPath) { + try { + const handle = await fs.open(shimPath, 'r'); + try { + const buf = Buffer.alloc(4096); + const { bytesRead } = await handle.read(buf, 0, 4096, 0); + const text = buf.subarray(0, bytesRead).toString('latin1'); + return PACKAGE_NAME_MARKERS.some((m) => text.includes(m)); + } finally { + await handle.close().catch(() => {}); + } + } catch { + return false; + } +} + +async function classifyShim(shim, ownRoot, ownPrefix) { + let real; + try { + real = await fs.realpath(shim); + } catch { + return 'unreadable'; + } + if (real === ownRoot || real.startsWith(ownPrefix)) return 'own'; + if (await shimReferencesOwnPackage(shim)) return 'own'; + return 'other'; +} + +/** + * Walk `pathString` and report what the user's shell would resolve + * `kimi` to, AFTER the to-be-removed shims in `actionableShimPaths` + * are pretended gone. Returns the first match by PATH order: + * + * - { kind: 'own' } — our shim wins. + * - { kind: 'blocked-legacy', shim } — a legacy `kimi` we + * detected but couldn't touch (a "blocked" shim) wins. + * - { kind: 'foreign', path } — something else wins: a + * `kimi` we didn't recognize as a legacy CLI and didn't generate + * ourselves (e.g. a user-managed wrapper script in `~/bin`). + * - { kind: 'none' } — no `kimi` resolves at all. + * + * Used by the orchestrator to give an accurate "why the takeover + * can't proceed" notice: a blocked-legacy blocker needs different + * remediation (sudo / admin delete) than a foreign blocker (the + * user's own file, which only they can decide what to do with). + * + * `allDetectedShimPaths` is every legacy shim our detector found + * (both blocked and actionable). It lets us distinguish a blocked + * legacy that survived our hypothetical removal pass from a totally + * unknown `kimi`. + */ +export async function findFirstResolvableKimi( + ownRoot, + pathString, + actionableShimPaths, + allDetectedShimPaths, +) { + if (!ownRoot || !pathString) return { kind: 'none' }; + const ownPrefix = ownRoot + sep; + const candidates = executableCandidates(LEGACY_BIN); + const skipSet = new Set(actionableShimPaths ?? []); + const knownLegacySet = new Set(allDetectedShimPaths ?? []); + const seenDirs = new Set(); + for (const dir of pathString.split(delimiter)) { + if (!dir || seenDirs.has(dir)) continue; + seenDirs.add(dir); + for (const name of candidates) { + const shim = join(dir, name); + if (skipSet.has(shim)) continue; + if (!(await isExecutableFile(shim))) continue; + const kind = await classifyShim(shim, ownRoot, ownPrefix); + if (kind === 'unreadable') continue; + if (kind === 'own') return { kind: 'own' }; + if (knownLegacySet.has(shim)) { + return { kind: 'blocked-legacy', shim }; + } + return { kind: 'foreign', path: shim }; + } + } + return { kind: 'none' }; +} + +/** + * Read the user's default shell's view of `PATH`. + * + * Why: `process.env.PATH` reflects the environment of whichever + * shell the user happened to invoke the package manager from, which + * may or may not match the daily-driver shell they'll later type + * `kimi` into. A `~/.zshrc`-only PATH entry, a `~/.bash_profile` + * that doesn't source `~/.bashrc`, or a sudo'd install that scrubs + * HOME all break the installer's-PATH heuristic. + * + * We spawn `$SHELL -l -c 'printf %s "$PATH"'` so the shell reads + * its login-profile files (`.profile`, `.bash_profile`, `.zprofile`). + * `-i` would also pull in interactive rc files (`.bashrc`, `.zshrc`) + * but it requires a tty for some shells and is much more likely to + * have side effects (prompts, agent startup) — we accept the + * narrower view and let `'unknown'` fall through to a gentler + * check. + * + * Outcomes: + * { kind: 'ok', path: string } — shell printed its PATH. + * { kind: 'unknown', reason: string } — `$SHELL` missing, spawn + * failed, timed out, or + * shell printed no `PATH`. + */ +export async function userShellPath() { + // Windows has no `$SHELL`/login-shell concept worth probing — cmd.exe + // and PowerShell don't have profile files in the rc-chain sense, and + // their PATH already comes from the user's persistent registry env + // (which is what `process.env.PATH` reflects). Skip the spawn and + // let the caller fall back to `process.env.PATH`. + if (IS_WINDOWS) return { kind: 'unknown', reason: 'windows skip' }; + + const shell = process.env['SHELL']; + if (!shell) return { kind: 'unknown', reason: 'no SHELL env var' }; + + return new Promise((resolve) => { + let settled = false; + const settle = (value) => { + if (settled) return; + settled = true; + resolve(value); + }; + + let stdout = ''; + // Wrap PATH in delimiters we can parse out, defensively, in case + // the shell prints anything else (motd, prompt redraw, …). + const probe = + 'printf "<<>>%s<<>>\\n" "$PATH"'; + const child = spawn(shell, ['-l', '-c', probe], { + stdio: ['ignore', 'pipe', 'ignore'], + }); + child.stdout.on('data', (chunk) => { + stdout += chunk.toString('utf-8'); + }); + + // Bound the wait. Login rc files are usually quick, but a hostile + // `.profile` could hang indefinitely. + const timer = setTimeout(() => { + child.kill('SIGKILL'); + settle({ kind: 'unknown', reason: 'shell spawn timed out' }); + }, 5000); + + child.on('close', (code) => { + clearTimeout(timer); + const match = stdout.match( + /<<>>([\s\S]*?)<<>>/, + ); + if (match && match[1].length > 0) { + settle({ kind: 'ok', path: match[1] }); + return; + } + settle({ kind: 'unknown', reason: `no PATH printed (exit ${code})` }); + }); + child.on('error', (err) => { + clearTimeout(timer); + settle({ kind: 'unknown', reason: `spawn error: ${err.message}` }); + }); + }); +} + +/** + * Compute the two PATH strings the postinstall consults. + * + * - `detection`: a `delimiter`-joined union of the user's shell + * PATH and `process.env.PATH`, deduplicated. Used by + * {@link detectLegacyShim} to walk for legacy shims. We union + * because either source may contain a legacy `kimi` we want to + * rename — including one that lives in a directory the + * installer's environment can't see but the user's shell can + * (e.g. a sanitized lifecycle env from a packaged manager), and + * vice versa. + * + * - `reachability`: the user's shell PATH if we can probe it, else + * `process.env.PATH`. Used to verify our own shim is on the PATH + * the user will actually type `kimi` into. We do NOT union here: + * a shim that's only in the installer's PATH wouldn't help the + * user after the install, so unioning would mis-classify the + * install as reachable. + * + * Returns a single object so the (potentially slow) shell-probe runs + * just once per postinstall. + */ +export async function postinstallPaths() { + const shellResult = await userShellPath(); + const processPath = process.env['PATH'] ?? ''; + const shellPathStr = shellResult.kind === 'ok' ? shellResult.path : null; + const reachability = shellPathStr ?? processPath; + const detection = unionPaths(shellPathStr, processPath); + return { detection, reachability }; +} + +function unionPaths(...paths) { + const seen = new Set(); + const out = []; + for (const p of paths) { + if (!p) continue; + for (const entry of p.split(delimiter)) { + if (!entry || seen.has(entry)) continue; + seen.add(entry); + out.push(entry); + } + } + return out.join(delimiter); +} + diff --git a/apps/kimi-code/scripts/postinstall/ui.mjs b/apps/kimi-code/scripts/postinstall/ui.mjs new file mode 100644 index 000000000..84be992f5 --- /dev/null +++ b/apps/kimi-code/scripts/postinstall/ui.mjs @@ -0,0 +1,458 @@ +/** + * User-facing output for the postinstall: where lines go, ANSI styling, + * the fixed-width box layout, and the four outcome renderers: + * + * - `logMigrationDone` — the takeover succeeded (one or more legacy + * shims were processed). Lists every action taken: renames, + * consolidates, delete-only, plain deletes, and harmless blocked + * leftovers. Footer branches three ways: preserved-somewhere + * (standard "type kimi-legacy"), only skippedForeignTarget (we + * couldn't save the old CLI because a user file took the name), + * and only blockedHarmless (just notes the leftovers, no + * phantom-file talk). + * - `logMigrationBlocked` — a legacy `kimi` we can't remove sits + * on PATH ahead of our shim. Nothing was touched; user is told + * which paths need their manual attention with sudo / admin. + * - `logForeignKimiInTheWay` — a `kimi` we don't recognize (not + * ours, not a legacy CLI) sits ahead of our shim on PATH. User + * needs to delete or rename their own file. Different remediation + * from `logMigrationBlocked`. + * - `logNewCliNotOnPath` — we found a legacy but our own shim + * isn't on the user's shell PATH at all. Same "touch nothing" + * behavior, different prose. + * + * This module is intentionally self-contained: no PATH walking, no fs + * mutations, no shell spawning — just rendering. The orchestrator + * (`postinstall.mjs`) makes the abort-or-proceed decision once and + * calls exactly one renderer at the end. + */ + +import { writeFileSync } from 'node:fs'; + +import { pmGlobalInstallCommand, pmGlobalBinCommand } from './reach.mjs'; + +// Fixed-width box rendering. 80 cols is the de facto terminal default. +// We can't reliably read TTY width from a piped postinstall context, so +// we pin the width and truncate long content with an ellipsis if needed. +const BOX_WIDTH = 80; +const BOX_INNER = BOX_WIDTH - 2; // chars between the two vertical borders +const BOX_PAD_LEFT = 2; // leading spaces inside the box for breathing room + +// ANSI styling. Disabled when NO_COLOR is set (https://no-color.org/). +// We can't reliably tell whether `/dev/tty`'s far end supports color, +// but modern terminals all do; users who want plain output can set +// NO_COLOR. +const USE_COLOR = !process.env['NO_COLOR']; +const ANSI_ESCAPE = /\x1b\[[0-9;]*[a-zA-Z]/g; +const C_RESET = USE_COLOR ? '\x1b[0m' : ''; +const C_DIM = USE_COLOR ? '\x1b[2m' : ''; +const C_BOLD_GREEN = USE_COLOR ? '\x1b[1;32m' : ''; +const C_BOLD_YELLOW = USE_COLOR ? '\x1b[1;33m' : ''; +const C_CYAN = USE_COLOR ? '\x1b[36m' : ''; + +function color(c, text) { + return USE_COLOR ? c + text + C_RESET : text; +} + +function visibleLength(s) { + return s.replace(ANSI_ESCAPE, '').length; +} + +function stripAnsi(s) { + return s.replace(ANSI_ESCAPE, ''); +} + +// Platform-specific path to the controlling terminal device. Writing +// here bypasses the package manager's lifecycle stdout capture (npm 7+ +// hides script stdout/stderr by default). On POSIX it's `/dev/tty`; +// on Windows it's the special filename `CON`, which Node resolves to +// the console device. (The fully-qualified `\\.\CON` form looks +// equivalent but Node appends a trailing backslash that breaks the +// open call — confirmed empirically on Windows 11 / Node 22.) +const TERMINAL_DEVICE = process.platform === 'win32' ? 'CON' : '/dev/tty'; + +/** + * Print a user-facing line. npm 7+ captures lifecycle stdout/stderr by + * default, so messages here would be invisible to a user running + * `npm install -g`. Writing directly to the platform's terminal + * device bypasses the manager's capture when one is available + * (interactive terminals). In CI / non-TTY contexts the device isn't + * writable; fall back to stdout so the message is still preserved in + * npm's lifecycle log under `~/.npm/_logs/`, with ANSI stripped so + * the log file stays readable. + */ +export function notify(line) { + const text = line + '\n'; + try { + writeFileSync(TERMINAL_DEVICE, text); + return; + } catch { + // Terminal device not writable (CI, sandboxed environments). + } + process.stdout.write(stripAnsi(text)); +} + +// Single-quote `path` for safe interpolation in a POSIX `sh` command. +// Wraps in single quotes and escapes any embedded `'` as `'\''`. +function quotePosixPath(path) { + return "'" + path.replace(/'/g, "'\\''") + "'"; +} + +// Single-quote `path` for safe interpolation in a PowerShell command. +// PowerShell single-quoted strings disable expansion; embedded `'` is +// escaped by doubling. +function quotePowerShellPath(path) { + return "'" + path.replace(/'/g, "''") + "'"; +} + +function boxBorder(left, right, fill = '─') { + return color(C_DIM, left + fill.repeat(BOX_INNER) + right); +} + +function boxLine(content = '') { + const visible = visibleLength(content); + const padding = + visible < BOX_INNER ? ' '.repeat(BOX_INNER - visible) : ''; + return color(C_DIM, '│') + content + padding + color(C_DIM, '│'); +} + +function pad(content) { + return ' '.repeat(BOX_PAD_LEFT) + content; +} + +function renderBox(lines) { + const out = [boxBorder('╭', '╮'), boxLine('')]; + for (const line of lines) out.push(boxLine(line)); + out.push(boxLine(''), boxBorder('╰', '╯')); + return out; +} + +function emit(lines) { + notify(''); + for (const line of lines) notify(line); + notify(''); +} + +function pathInBox(path) { + // 7-space lead = box pad (2) + prose indent (3) + nesting under + // label (2). We intentionally do NOT truncate overflowing content: + // for command lines (`sudo rm `, `mv `), left-truncation + // would swallow the command verb and leave the user with + // un-copy-pasteable instructions. Long lines just overflow the box + // border, which is visually less pretty but keeps the content + // intact. + const lead = ' '.repeat(BOX_PAD_LEFT + 5); + return lead + color(C_CYAN, path); +} + +function successHeading(text) { + return pad(color(C_BOLD_GREEN, '✓ ' + text)); +} + +function warningHeading(text) { + return pad(color(C_BOLD_YELLOW, '! ' + text)); +} + +/** + * The takeover completed. Renders one box that lists every action + * taken, so the user sees a single coherent picture even when several + * shims were involved. + * + * Sections (each only shown when non-empty): + * - "Renamed" — the first PATH-order shim, preserved as + * `kimi-legacy`. The "`kimi` now launches the new CLI" claim is + * safe to make here because the orchestrator already verified + * reachability after this set of removals. + * - "Consolidated" — first shim's `kimi-legacy` already pointed at + * a legacy file (re-migration case); we deleted the duplicate + * source and kept the existing target. Same end state, different + * mechanism. + * - "Couldn't preserve as kimi-legacy" — first shim's `kimi-legacy` + * slot was a user-managed file; we deleted the source `kimi` to + * remove the shadow but left their file alone, so no fallback + * exists in that dir. + * - "Also removed" — non-first PATH-order shims that would have + * shadowed our new shim. Just `unlink`ed. + * - "Note: legacy left behind" — blocked shims that couldn't be + * removed but PATH order means they don't shadow us; the user + * can clean them up at leisure. + * - "Errors" — anything that failed during execution despite + * pre-flight saying it should work (race conditions, transient + * fs errors). Listed last so the user can see what to retry. + */ +export function logMigrationDone(outcomes, pm) { + const reinstallCmd = pmGlobalInstallCommand(pm, '@moonshot-ai/kimi-code'); + const { + renames, + consolidates, + skippedForeignTarget, + deletes, + blockedHarmless, + errors, + } = outcomes; + + const lines = [successHeading('kimi now runs the new version'), '']; + + if (renames.length > 0) { + lines.push(pad(' Renamed your old kimi so you can still run it as')); + lines.push(pad(' kimi-legacy:')); + for (const c of renames) { + lines.push(pathInBox(c.shimPath + ' -> ' + c.target)); + } + lines.push(''); + } + + if (consolidates.length > 0) { + lines.push(pad(' Removed an extra copy of your old kimi (kimi-legacy')); + lines.push(pad(' was already set up here from before):')); + for (const c of consolidates) { + lines.push(pathInBox(c.shimPath)); + lines.push(pathInBox(' (kimi-legacy is at ' + c.target + ')')); + } + lines.push(''); + } + + if (skippedForeignTarget.length > 0) { + lines.push(pad(' Removed your old kimi (a file you created was already')); + lines.push(pad(' using the name kimi-legacy, so we left it alone):')); + for (const c of skippedForeignTarget) { + lines.push(pathInBox(c.shimPath)); + lines.push(pathInBox(' (your file at ' + c.target + ' is untouched)')); + } + lines.push(''); + } + + if (deletes.length > 0) { + lines.push(pad(' Also removed (these would have run instead of the')); + lines.push(pad(' new kimi if we left them):')); + for (const c of deletes) { + lines.push(pathInBox(c.shimPath)); + } + lines.push(''); + } + + if (blockedHarmless.length > 0) { + lines.push(pad(' Note: we can\'t change these files, but it\'s OK —')); + lines.push(pad(' they won\'t run instead of the new kimi:')); + for (const c of blockedHarmless) { + lines.push(pathInBox(c.shimPath)); + } + lines.push(''); + } + + if (errors.length > 0) { + lines.push(pad(' Some changes didn\'t go through:')); + for (const e of errors) { + lines.push( + pathInBox(e.shimPath + ' (' + (e.message ?? e.code ?? 'error') + ')'), + ); + } + lines.push(''); + } + + // Footer has three branches based on what actually happened: + // + // 1. Preserved somewhere (rename or consolidate): the old CLI is + // available as `kimi-legacy`. Standard takeover footer. + // 2. Only skippedForeignTarget (no rename, no consolidate, no + // blockedHarmless): user has a file they made called + // `kimi-legacy`, so we couldn't save the old CLI under that + // name. Explain the situation honestly. + // 3. Only blockedHarmless (no rename, no consolidate, no + // skippedForeignTarget): we have nothing to celebrate or + // apologize for — the "Note: we can't change these files but + // it's OK" section above already covers it. Plain footer. + // + // If both skippedForeignTarget AND blockedHarmless are present + // (but no preservation), branch 2 wins — the foreign-target story + // is the more useful one for the user to know about. + const preservedSomewhere = renames.length > 0 || consolidates.length > 0; + if (preservedSomewhere) { + lines.push( + pad(' Now typing `kimi` runs the new version. To run the old'), + pad(' version, type `kimi-legacy` instead. Your settings from'), + pad(' the old version will be moved over the first time you'), + pad(' run `kimi`.'), + '', + pad(' Note: if you reinstall the old kimi later (e.g. with'), + pad(' `uv tool`, `pip`, or `pipx`), it will put `kimi` back.'), + pad(' Run this command again to switch to the new one:'), + pathInBox(reinstallCmd), + '', + pad(' If typing `kimi` still runs the old version, open a new'), + pad(' terminal window — your current one may have remembered'), + pad(' the old path.'), + ); + } else if (skippedForeignTarget.length > 0) { + lines.push( + pad(' Now typing `kimi` runs the new version. Your settings'), + pad(' from the old version will be moved over the first time'), + pad(' you run `kimi`.'), + '', + pad(' We couldn\'t save the old kimi as `kimi-legacy` because'), + pad(' that name was already taken by a file you\'d created.'), + pad(' If you need the old kimi back, install it again with'), + pad(' `uv tool install kimi-cli` (or pipx / pip).'), + '', + pad(' If typing `kimi` still runs the old version, open a new'), + pad(' terminal window — your current one may have remembered'), + pad(' the old path.'), + ); + } else { + lines.push( + pad(' Now typing `kimi` runs the new version. Your settings'), + pad(' from the old version will be moved over the first time'), + pad(' you run `kimi`.'), + '', + pad(' If typing `kimi` still runs the old version, open a new'), + pad(' terminal window — your current one may have remembered'), + pad(' the old path.'), + ); + } + + emit(renderBox(lines)); +} + +/** + * At least one blocked legacy shim still sits on PATH ahead of where + * our new shim would land. We refused to touch anything (pre-flight + * abort), so neither the user's existing setup nor the new install + * gets a half-migrated state. List each blocking path with the + * platform-appropriate manual fix. + */ +export function logMigrationBlocked(blocked, actionable, pm) { + const isWindows = process.platform === 'win32'; + const reinstallCmd = pmGlobalInstallCommand(pm, '@moonshot-ai/kimi-code'); + + const lines = [ + warningHeading('Can\'t switch to the new kimi yet'), + '', + pad(' There\'s an old kimi on your computer that we can\'t change.'), + pad(' As long as it\'s there, typing `kimi` will still run the old'), + pad(' version. Files we can\'t change:'), + ]; + + for (const c of blocked) { + lines.push(pathInBox(c.shimPath)); + } + + lines.push('', pad(' Please delete them yourself, then install again:')); + + for (const c of blocked) { + if (isWindows && c.isSystemPath) { + // Admin PowerShell needed. + lines.push(pathInBox('# in an elevated PowerShell:')); + lines.push(pathInBox('Remove-Item ' + quotePowerShellPath(c.shimPath))); + } else if (c.isSystemPath) { + lines.push(pathInBox('sudo rm ' + quotePosixPath(c.shimPath))); + } else if (isWindows) { + lines.push(pathInBox('Remove-Item ' + quotePowerShellPath(c.shimPath))); + } else { + lines.push(pathInBox('rm ' + quotePosixPath(c.shimPath))); + } + } + + if (actionable.length > 0) { + lines.push( + '', + pad(' We also found these old kimi files. We could remove them'), + pad(' ourselves, once the ones above are gone:'), + ); + for (const c of actionable) { + lines.push(pathInBox(c.shimPath)); + } + } + + lines.push( + '', + pad(' After deleting them, install again to finish:'), + pathInBox(reinstallCmd), + '', + pad(' Nothing on your computer was changed.'), + ); + + emit(renderBox(lines)); +} + +/** + * The reachability check found a `kimi` ahead of our shim on PATH + * that's neither ours nor a legacy Python CLI — almost certainly a + * wrapper the user wrote themselves (or installed from somewhere we + * don't recognize). Deleting blocked legacy shims wouldn't help + * here: the foreign file would still win resolution. So the + * remediation is "delete or rename your own file", which only the + * user can decide. + */ +export function logForeignKimiInTheWay(foreignPath, pm) { + const reinstallCmd = pmGlobalInstallCommand(pm, '@moonshot-ai/kimi-code'); + emit( + renderBox([ + warningHeading('Can\'t switch to the new kimi yet'), + '', + pad(' There\'s another file called `kimi` on your computer that\'s'), + pad(' not the new CLI and not the old one — it looks like'), + pad(' something you set up yourself. As long as it\'s there,'), + pad(' typing `kimi` will run it instead of the new version.'), + '', + pad(' We found it at:'), + pathInBox(foreignPath), + '', + pad(' To use the new kimi, delete or rename that file, then'), + pad(' install again:'), + pathInBox(reinstallCmd), + '', + pad(' Nothing on your computer was changed.'), + ]), + ); +} + +/** + * The legacy `kimi` was found, but the directory where the package + * manager placed the new `kimi` shim is not on the user's PATH. + * Renaming the legacy shim now would leave the user with NO reachable + * `kimi` command — the new one would still not be discoverable by + * their shell. Show them the PATH fix and leave the legacy CLI alone. + * + * The PATH-fix hint uses the manager-specific subshell command (one + * of `npm prefix -g`, `yarn global bin`, `pnpm bin -g`) so it works + * regardless of which manager the user ran, and renders in the + * syntax of the user's likely shell: + * - POSIX : `export PATH=...`. + * - Windows: `$env:Path = ...` (PowerShell). + * On Windows, npm places global shims directly under `` (no + * `bin` subdir), and pnpm/yarn already report the bin dir, so we + * skip the `/bin` suffix the POSIX branch needs for npm. + */ +export function logNewCliNotOnPath(detection, pm) { + const isWindows = process.platform === 'win32'; + const binCmd = pmGlobalBinCommand(pm); + const reinstallCmd = pmGlobalInstallCommand(pm, '@moonshot-ai/kimi-code'); + + const newPathHint = isWindows + ? `$env:Path = "$(${binCmd});$env:Path"` + : pm === 'npm' + ? `export PATH="$(${binCmd})/bin:$PATH"` + : `export PATH="$(${binCmd}):$PATH"`; + const rcLabel = isWindows ? 'PowerShell profile' : 'shell rc'; + + emit( + renderBox([ + warningHeading('New kimi is installed, but your terminal can\'t find it'), + '', + pad(' The old kimi is still here:'), + pathInBox(detection.shimPath), + '', + pad(' The new kimi was installed by ' + pm + ', but it landed in a'), + pad(' folder your terminal doesn\'t search. (Your terminal looks'), + pad(' for commands in folders listed in your PATH.) If we removed'), + pad(' the old kimi now, typing `kimi` wouldn\'t find anything.'), + '', + pad(' Add the new kimi\'s folder to your PATH (and save the change'), + pad(' in your ' + rcLabel + ' so it sticks), then install again:'), + pathInBox(newPathHint), + pathInBox(reinstallCmd), + '', + pad(' The old kimi is still where it was.'), + ]), + ); +} diff --git a/apps/kimi-code/scripts/smoke.mjs b/apps/kimi-code/scripts/smoke.mjs new file mode 100644 index 000000000..9c64dd80d --- /dev/null +++ b/apps/kimi-code/scripts/smoke.mjs @@ -0,0 +1,58 @@ +import { execFile } from 'node:child_process'; +import { readFile, stat } from 'node:fs/promises'; +import { fileURLToPath } from 'node:url'; +import { dirname, resolve } from 'node:path'; +import { promisify } from 'node:util'; + +const execFileAsync = promisify(execFile); +const appRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +const bundlePath = resolve(appRoot, 'dist', 'main.mjs'); +const packageJson = JSON.parse(await readFile(resolve(appRoot, 'package.json'), 'utf-8')); +const expectedVersion = packageJson.version; + +function fail(message) { + console.error(message); + process.exit(1); +} + +async function ensureBundleExists() { + try { + await stat(bundlePath); + } catch { + fail(`Bundle not found at ${bundlePath}. Run \`pnpm build\` first.`); + } +} + +async function runBundle(args) { + try { + const { stdout, stderr } = await execFileAsync(process.execPath, [bundlePath, ...args], { + cwd: appRoot, + maxBuffer: 1024 * 1024 * 16, + }); + return `${stdout}${stderr}`; + } catch (error) { + const detail = [error.stdout?.trim(), error.stderr?.trim(), error.message] + .filter(Boolean) + .join('\n'); + fail(`Bundle smoke failed: node ${bundlePath} ${args.join(' ')}\n${detail}`); + } +} + +function assertIncludes(output, expected, command) { + if (!output.includes(expected)) { + fail(`Bundle smoke output for "${command}" did not include "${expected}".\n${output}`); + } +} + +await ensureBundleExists(); + +const versionOutput = await runBundle(['--version']); +assertIncludes(versionOutput, expectedVersion, '--version'); + +const helpOutput = await runBundle(['--help']); +assertIncludes(helpOutput, 'Usage: kimi', '--help'); + +const exportHelpOutput = await runBundle(['export', '--help']); +assertIncludes(exportHelpOutput, 'Usage: kimi export', 'export --help'); + +console.log(`Bundle smoke passed: ${bundlePath}`); diff --git a/apps/kimi-code/src/cli/build-info.ts b/apps/kimi-code/src/cli/build-info.ts new file mode 100644 index 000000000..56c89a5b8 --- /dev/null +++ b/apps/kimi-code/src/cli/build-info.ts @@ -0,0 +1,34 @@ +declare const __KIMI_CODE_VERSION__: string | undefined; +declare const __KIMI_CODE_CHANNEL__: string | undefined; +declare const __KIMI_CODE_COMMIT__: string | undefined; +declare const __KIMI_CODE_BUILD_TARGET__: string | undefined; + +export interface KimiBuildInfo { + readonly version?: string; + readonly channel?: string; + readonly commit?: string; + readonly buildTarget?: string; +} + +function optionalBuildString(value: unknown): string | undefined { + return typeof value === 'string' && value.length > 0 ? value : undefined; +} + +export const KIMI_BUILD_INFO: KimiBuildInfo = { + version: + typeof __KIMI_CODE_VERSION__ === 'string' + ? optionalBuildString(__KIMI_CODE_VERSION__) + : undefined, + channel: + typeof __KIMI_CODE_CHANNEL__ === 'string' + ? optionalBuildString(__KIMI_CODE_CHANNEL__) + : undefined, + commit: + typeof __KIMI_CODE_COMMIT__ === 'string' + ? optionalBuildString(__KIMI_CODE_COMMIT__) + : undefined, + buildTarget: + typeof __KIMI_CODE_BUILD_TARGET__ === 'string' + ? optionalBuildString(__KIMI_CODE_BUILD_TARGET__) + : undefined, +}; diff --git a/apps/kimi-code/src/cli/commands.ts b/apps/kimi-code/src/cli/commands.ts new file mode 100644 index 000000000..b0a65f257 --- /dev/null +++ b/apps/kimi-code/src/cli/commands.ts @@ -0,0 +1,98 @@ +import { Command, Option } from 'commander'; + +import { CLI_COMMAND_NAME } from '#/constant/app'; + +import { registerMigrateCommand } from '#/migration/index'; + +import type { CLIOptions } from './options'; +import { registerExportCommand } from './sub/export'; + +export type MainCommandHandler = (opts: CLIOptions) => void; +export type MigrateCommandHandler = () => void; + +export function createProgram( + version: string, + onMain: MainCommandHandler, + onMigrate: MigrateCommandHandler, +): Command { + const program = new Command(CLI_COMMAND_NAME) + .description('The Starting Point for Next-Gen Agents') + .version(version, '-V, --version') + .allowUnknownOption(false) + .configureHelp({ helpWidth: 100 }) + .helpOption('-h, --help', 'Show help.') + .addHelpText( + 'after', + '\nDocumentation: https://moonshotai.github.io/kimi-code/\n' + ); + + program + .addOption( + new Option( + '-S, --session [id]', + 'Resume a session. With ID: resume that session. Without ID: interactively pick.', + ).argParser((val: string | boolean) => (val === true ? '' : (val as string))), + ) + .addOption( + new Option('-r, --resume [id]') + .hideHelp() + .argParser((val: string | boolean) => (val === true ? '' : (val as string))), + ) + .option('-C, --continue', 'Continue the previous session for the working directory.', false) + .option('-y, --yolo', 'Automatically approve all actions.', false) + .addOption( + new Option( + '-m, --model ', + 'LLM model alias to use for this invocation. Defaults to default_model in config.toml.', + ), + ) + .addOption( + new Option( + '-p, --prompt ', + 'Run one prompt non-interactively and print the response.', + ), + ) + .addOption( + new Option( + '--output-format ', + 'Output format for prompt mode. Defaults to text.', + ).choices(['text', 'stream-json']), + ) + .addOption( + new Option( + '--skills-dir ', + 'Load skills from this directory instead of auto-discovered user and project directories. Can be repeated.', + ) + .argParser((value: string, previous: string[] | undefined) => [...(previous ?? []), value]) + .default([]), + ) + .addOption(new Option('--yes').hideHelp().default(false)) + .addOption(new Option('--auto-approve').hideHelp().default(false)) + .option('--plan', 'Start in plan mode.', false); + + registerExportCommand(program); + registerMigrateCommand(program, onMigrate); + + program.action(() => { + const raw = program.opts>(); + + const rawSession = raw['session'] ?? raw['resume']; + const sessionValue = rawSession === true ? '' : (rawSession as string | undefined); + const yoloValue = raw['yolo'] === true || raw['yes'] === true || raw['autoApprove'] === true; + + const opts: CLIOptions = { + session: sessionValue, + continue: raw['continue'] as boolean, + yolo: yoloValue, + plan: raw['plan'] as boolean, + model: raw['model'] as string | undefined, + outputFormat: raw['outputFormat'] as CLIOptions['outputFormat'], + prompt: raw['prompt'] as string | undefined, + skillsDirs: raw['skillsDir'] as string[], + }; + + onMain(opts); + }); + + return program; +} diff --git a/apps/kimi-code/src/cli/options.ts b/apps/kimi-code/src/cli/options.ts new file mode 100644 index 000000000..67bc86d4f --- /dev/null +++ b/apps/kimi-code/src/cli/options.ts @@ -0,0 +1,58 @@ +export type UIMode = 'shell' | 'print'; +export type PromptOutputFormat = 'text' | 'stream-json'; + +export interface CLIOptions { + session: string | undefined; + continue: boolean; + yolo: boolean; + plan: boolean; + model: string | undefined; + outputFormat: PromptOutputFormat | undefined; + prompt: string | undefined; + skillsDirs: string[]; +} + +export interface ValidatedOptions { + options: CLIOptions; + uiMode: UIMode; +} + +export class OptionConflictError extends Error { + constructor(message: string) { + super(message); + this.name = 'OptionConflictError'; + } +} + +export function validateOptions(opts: CLIOptions): ValidatedOptions { + const prompt = opts.prompt; + const promptMode = prompt !== undefined; + if (promptMode && prompt.trim().length === 0) { + throw new OptionConflictError('Prompt cannot be empty.'); + } + if (opts.model !== undefined && opts.model.trim().length === 0) { + throw new OptionConflictError('Model cannot be empty.'); + } + if (!promptMode && opts.outputFormat !== undefined) { + throw new OptionConflictError('Output format is only supported in prompt mode.'); + } + if (promptMode && opts.yolo) { + throw new OptionConflictError('Cannot combine --prompt with --yolo.'); + } + if (promptMode && opts.plan) { + throw new OptionConflictError('Cannot combine --prompt with --plan.'); + } + if (promptMode && opts.session === '') { + throw new OptionConflictError('Cannot use --session without an id in prompt mode.'); + } + if (opts.continue && opts.session !== undefined) { + throw new OptionConflictError('Cannot combine --continue, --session.'); + } + if (!promptMode && (opts.continue || opts.session !== undefined) && opts.yolo) { + throw new OptionConflictError('Cannot combine --yolo with --continue or --session.'); + } + if (!promptMode && (opts.continue || opts.session !== undefined) && opts.plan) { + throw new OptionConflictError('Cannot combine --plan with --continue or --session.'); + } + return { options: opts, uiMode: promptMode ? 'print' : 'shell' }; +} diff --git a/apps/kimi-code/src/cli/run-prompt.ts b/apps/kimi-code/src/cli/run-prompt.ts new file mode 100644 index 000000000..806d60a6d --- /dev/null +++ b/apps/kimi-code/src/cli/run-prompt.ts @@ -0,0 +1,673 @@ +import { createKimiDeviceId, KIMI_CODE_PROVIDER_NAME } from '@moonshot-ai/kimi-code-oauth'; +import { + initializeTelemetry, + setCrashPhase, + setTelemetryContext, + shutdownTelemetry, + track, + withTelemetryContext, +} from '@moonshot-ai/kimi-telemetry'; +import { + KimiHarness, + log, + type Event, + type HookResultEvent, + type Session, + type SessionStatus, + type TelemetryClient, +} from '@moonshot-ai/kimi-code-sdk'; + +import { CLI_SHUTDOWN_TIMEOUT_MS, CLI_USER_AGENT_PRODUCT } from '#/constant/app'; + +import type { CLIOptions, PromptOutputFormat } from './options'; +import { createKimiCodeHostIdentity } from './version'; + +interface PromptOutput { + readonly columns?: number | undefined; + write(chunk: string): boolean; +} + +interface PromptRunIO { + readonly stdout?: PromptOutput; + readonly stderr?: PromptOutput; + readonly process?: PromptProcess; +} + +interface PromptProcess { + once(signal: NodeJS.Signals, listener: () => Promise): unknown; + off(signal: NodeJS.Signals, listener: () => Promise): unknown; + exit(code?: number): never | void; +} + +const PROMPT_UI_MODE = 'print'; +const PROMPT_MAIN_AGENT_ID = 'main'; +const PROMPT_BLOCK_BULLET = '• '; +const PROMPT_BLOCK_INDENT = ' '; + +export async function runPrompt( + opts: CLIOptions, + version: string, + io: PromptRunIO = {}, +): Promise { + const startedAt = Date.now(); + const stdout = io.stdout ?? process.stdout; + const stderr = io.stderr ?? process.stderr; + const promptProcess = io.process ?? process; + const workDir = process.cwd(); + const telemetryClient: TelemetryClient = { + track, + withContext: withTelemetryContext, + setContext: setTelemetryContext, + }; + const harness = new KimiHarness({ + identity: createKimiCodeHostIdentity(version), + uiMode: PROMPT_UI_MODE, + skillDirs: opts.skillsDirs, + telemetry: telemetryClient, + onOAuthRefresh: (outcome) => { + if (outcome.success) { + track('oauth_refresh', { success: true }); + return; + } + track('oauth_refresh', { success: false, reason: outcome.reason }); + }, + }); + log.info('kimi-code starting', { + version, + uiMode: PROMPT_UI_MODE, + nodeVersion: process.version, + platform: `${process.platform}/${process.arch}`, + workDir, + }); + let restorePromptSessionPermission = async (): Promise => {}; + let removeTerminationCleanup: (() => void) | undefined; + let cleanupPromise: Promise | undefined; + const cleanupPromptRun = async (): Promise => { + cleanupPromise ??= (async () => { + removeTerminationCleanup?.(); + setCrashPhase('shutdown'); + try { + await restorePromptSessionPermission(); + } finally { + await shutdownTelemetry({ timeoutMs: CLI_SHUTDOWN_TIMEOUT_MS }); + await harness.close(); + } + })(); + await cleanupPromise; + }; + removeTerminationCleanup = installPromptTerminationCleanup(promptProcess, cleanupPromptRun); + + try { + await harness.ensureConfigFile(); + const config = await harness.getConfig(); + const { session, resumed, restorePermission, telemetryModel } = await resolvePromptSession( + harness, + opts, + workDir, + config.defaultModel, + stderr, + (restorePermission) => { + restorePromptSessionPermission = restorePermission; + }, + ); + restorePromptSessionPermission = restorePermission; + + const deviceId = createKimiDeviceId(harness.homeDir); + initializeTelemetry({ + homeDir: harness.homeDir, + deviceId, + enabled: config.telemetry !== false, + appName: CLI_USER_AGENT_PRODUCT, + version, + uiMode: PROMPT_UI_MODE, + model: telemetryModel, + getAccessToken: async () => + (await harness.auth.getCachedAccessToken(KIMI_CODE_PROVIDER_NAME)) ?? null, + }); + setCrashPhase('runtime'); + + withTelemetryContext({ sessionId: session.id }).track('started', { + resumed, + yolo: false, + plan: false, + afk: true, + }); + + await runPromptTurn(session, opts.prompt!, opts.outputFormat ?? 'text', stdout, stderr); + stderr.write(`To resume this session: kimi -r ${session.id}\n`); + + withTelemetryContext({ sessionId: session.id }).track('exit', { + duration_s: (Date.now() - startedAt) / 1000, + }); + } finally { + await cleanupPromptRun(); + } +} + +interface ResolvedPromptSession { + readonly session: Session; + readonly resumed: boolean; + readonly restorePermission: () => Promise; + readonly telemetryModel?: string; +} + +async function resolvePromptSession( + harness: KimiHarness, + opts: CLIOptions, + workDir: string, + defaultModel: string | undefined, + stderr: PromptOutput, + setRestorePermission: (restorePermission: () => Promise) => void, +): Promise { + if (opts.session !== undefined) { + const session = await harness.resumeSession({ id: opts.session }); + const status = await session.getStatus(); + const restorePermission = await forcePromptPermission( + session, + status.permission, + setRestorePermission, + ); + if (opts.model !== undefined) { + await session.setModel(opts.model); + } + installHeadlessHandlers(session); + return { + session, + resumed: true, + restorePermission, + telemetryModel: configuredModel(opts.model, status.model, defaultModel), + }; + } + + if (opts.continue) { + const sessions = await harness.listSessions({ workDir }); + const previous = sessions[0]; + if (previous !== undefined) { + const session = await harness.resumeSession({ id: previous.id }); + const status = await session.getStatus(); + const restorePermission = await forcePromptPermission( + session, + status.permission, + setRestorePermission, + ); + if (opts.model !== undefined) { + await session.setModel(opts.model); + } + installHeadlessHandlers(session); + return { + session, + resumed: true, + restorePermission, + telemetryModel: configuredModel(opts.model, status.model, defaultModel), + }; + } + stderr.write(`No sessions to continue under "${workDir}"; starting a fresh session.\n`); + } + + const model = requireConfiguredModel(opts.model, defaultModel); + const session = await harness.createSession({ workDir, model, permission: 'auto' }); + installHeadlessHandlers(session); + return { session, resumed: false, restorePermission: async () => {}, telemetryModel: model }; +} + +async function forcePromptPermission( + session: Session, + previousPermission: SessionStatus['permission'], + setRestorePermission: (restorePermission: () => Promise) => void, +): Promise<() => Promise> { + let overridePermission: Promise | undefined; + const restorePermission = async () => { + await overridePermission?.catch(() => {}); + if (previousPermission !== 'auto') { + await session.setPermission(previousPermission); + } + }; + setRestorePermission(restorePermission); + if (previousPermission !== 'auto') { + overridePermission = session.setPermission('auto'); + await overridePermission; + } + return restorePermission; +} + +function requireConfiguredModel(...models: readonly (string | undefined)[]): string { + const model = configuredModel(...models); + if (model === undefined) { + throw new Error('No model configured. Set default_model in config.toml.'); + } + return model; +} + +function configuredModel(...models: readonly (string | undefined)[]): string | undefined { + return models.find((model) => model !== undefined && model.trim().length > 0); +} + +function installHeadlessHandlers(session: Session): void { + session.setApprovalHandler(() => ({ decision: 'approved' })); + session.setQuestionHandler(() => null); +} + +function installPromptTerminationCleanup( + promptProcess: PromptProcess, + cleanup: () => Promise, +): () => void { + let terminating = false; + const exitAfterCleanup = async (signal: NodeJS.Signals): Promise => { + if (terminating) return; + terminating = true; + try { + await cleanup(); + } finally { + promptProcess.exit(signalExitCode(signal)); + } + }; + const onSigint = () => exitAfterCleanup('SIGINT'); + const onSigterm = () => exitAfterCleanup('SIGTERM'); + promptProcess.once('SIGINT', onSigint); + promptProcess.once('SIGTERM', onSigterm); + return () => { + promptProcess.off('SIGINT', onSigint); + promptProcess.off('SIGTERM', onSigterm); + }; +} + +function signalExitCode(signal: NodeJS.Signals): number { + return signal === 'SIGINT' ? 130 : 143; +} + +function runPromptTurn( + session: Session, + prompt: string, + outputFormat: PromptOutputFormat, + stdout: PromptOutput, + stderr: PromptOutput, +): Promise { + let activeTurnId: number | undefined; + let activeAgentId: string | undefined; + const outputWriter = + outputFormat === 'stream-json' + ? new PromptJsonWriter(stdout) + : new PromptTranscriptWriter(stdout, stderr); + let settled = false; + let unsubscribe: (() => void) | undefined; + + return new Promise((resolve, reject) => { + const finish = (error?: Error): void => { + if (settled) return; + settled = true; + unsubscribe?.(); + outputWriter.finish(); + if (error !== undefined) { + reject(error); + return; + } + resolve(); + }; + + unsubscribe = session.onEvent((event) => { + if (event.type === 'error') { + if (event.agentId !== PROMPT_MAIN_AGENT_ID) { + return; + } + finish(new Error(`${event.code}: ${event.message}`)); + return; + } + if (event.type === 'turn.started' && activeTurnId === undefined) { + if (event.agentId !== PROMPT_MAIN_AGENT_ID) { + return; + } + activeTurnId = event.turnId; + activeAgentId = event.agentId; + return; + } + if ( + activeTurnId === undefined || + activeAgentId === undefined || + !hasTurnId(event) || + event.turnId !== activeTurnId || + event.agentId !== activeAgentId + ) { + return; + } + switch (event.type) { + case 'turn.step.started': + case 'turn.step.interrupted': + outputWriter.flushAssistant(); + return; + case 'turn.step.retrying': + outputWriter.discardAssistant(); + return; + case 'assistant.delta': + outputWriter.writeAssistantDelta(event.delta); + return; + case 'hook.result': + outputWriter.writeHookResult(event); + return; + case 'thinking.delta': + outputWriter.writeThinkingDelta(event.delta); + return; + case 'tool.call.started': + outputWriter.writeToolCall(event.toolCallId, event.name, event.args); + return; + case 'tool.call.delta': + outputWriter.writeToolCallDelta(event.toolCallId, event.name, event.argumentsPart); + return; + case 'tool.result': + outputWriter.writeToolResult(event.toolCallId, event.output); + return; + case 'tool.progress': + if (event.update.text !== undefined && event.update.text.length > 0) { + stderr.write( + event.update.text.endsWith('\n') ? event.update.text : `${event.update.text}\n`, + ); + } + return; + case 'turn.ended': + if (event.reason === 'completed') { + finish(); + return; + } + finish(new Error(formatTurnEndedFailure(event))); + return; + case 'agent.status.updated': + case 'background.task.started': + case 'background.task.terminated': + case 'background.task.updated': + case 'compaction.blocked': + case 'compaction.cancelled': + case 'compaction.completed': + case 'compaction.started': + case 'mcp.server.status': + case 'session.meta.updated': + case 'skill.activated': + case 'subagent.completed': + case 'subagent.failed': + case 'subagent.spawned': + case 'tool.list.updated': + case 'turn.started': + case 'turn.step.completed': + return; + } + }); + + session.prompt(prompt).catch((error: unknown) => { + finish(error instanceof Error ? error : new Error(String(error))); + }); + }); +} + +interface PromptTurnWriter { + writeAssistantDelta(delta: string): void; + writeHookResult(event: HookResultEvent): void; + writeThinkingDelta(delta: string): void; + writeToolCall(toolCallId: string, name: string, args: unknown): void; + writeToolCallDelta( + toolCallId: string, + name: string | undefined, + argumentsPart: string | undefined, + ): void; + writeToolResult(toolCallId: string, output: unknown): void; + flushAssistant(): void; + discardAssistant(): void; + finish(): void; +} + +class PromptTranscriptWriter implements PromptTurnWriter { + private readonly assistantWriter: PromptBlockWriter; + private readonly thinkingWriter: PromptBlockWriter; + + constructor(stdout: PromptOutput, stderr: PromptOutput) { + this.assistantWriter = new PromptBlockWriter(stdout); + this.thinkingWriter = new PromptBlockWriter(stderr); + } + + writeAssistantDelta(delta: string): void { + this.thinkingWriter.finish(); + this.assistantWriter.write(delta); + } + + writeHookResult(event: HookResultEvent): void { + this.thinkingWriter.finish(); + this.assistantWriter.finish(); + this.assistantWriter.write(formatHookResultPlain(event)); + this.assistantWriter.finish(); + } + + writeThinkingDelta(delta: string): void { + this.thinkingWriter.write(delta); + } + + writeToolCall(): void {} + + writeToolCallDelta(): void {} + + writeToolResult(): void {} + + flushAssistant(): void {} + + discardAssistant(): void {} + + finish(): void { + this.thinkingWriter.finish(); + this.assistantWriter.finish(); + } +} + +interface PromptJsonToolCall { + type: 'function'; + id: string; + function: { + name: string; + arguments: string; + }; +} + +interface PromptJsonAssistantMessage { + role: 'assistant'; + content?: string; + tool_calls?: PromptJsonToolCall[]; +} + +interface PromptJsonToolMessage { + role: 'tool'; + tool_call_id: string; + content: string; +} + +class PromptJsonWriter implements PromptTurnWriter { + private assistantText = ''; + private readonly toolCalls: PromptJsonToolCall[] = []; + + constructor(private readonly stdout: PromptOutput) {} + + writeAssistantDelta(delta: string): void { + this.assistantText += delta; + } + + writeHookResult(event: HookResultEvent): void { + this.flushAssistant(); + this.writeJsonLine({ + role: 'assistant', + content: formatHookResultPlain(event), + }); + } + + writeThinkingDelta(): void {} + + writeToolCall(toolCallId: string, name: string, args: unknown): void { + const existing = this.toolCalls.find((toolCall) => toolCall.id === toolCallId); + if (existing !== undefined) { + existing.function.name = name; + existing.function.arguments = stringifyJsonValue(args); + return; + } + this.toolCalls.push({ + type: 'function', + id: toolCallId, + function: { + name, + arguments: stringifyJsonValue(args), + }, + }); + } + + writeToolCallDelta( + toolCallId: string, + name: string | undefined, + argumentsPart: string | undefined, + ): void { + const toolCall = this.findOrCreateToolCall(toolCallId, name ?? ''); + if (name !== undefined) { + toolCall.function.name = name; + } + if (argumentsPart !== undefined) { + toolCall.function.arguments += argumentsPart; + } + } + + writeToolResult(toolCallId: string, output: unknown): void { + this.flushAssistant(); + this.writeJsonLine({ + role: 'tool', + tool_call_id: toolCallId, + content: stringifyToolOutput(output), + }); + } + + flushAssistant(): void { + if (this.assistantText.length === 0 && this.toolCalls.length === 0) return; + const message: PromptJsonAssistantMessage = { + role: 'assistant', + content: this.assistantText.length > 0 ? this.assistantText : undefined, + tool_calls: this.toolCalls.length > 0 ? [...this.toolCalls] : undefined, + }; + this.writeJsonLine(message); + this.discardAssistant(); + } + + discardAssistant(): void { + this.assistantText = ''; + this.toolCalls.length = 0; + } + + finish(): void { + this.flushAssistant(); + } + + private findOrCreateToolCall(toolCallId: string, name: string): PromptJsonToolCall { + const existing = this.toolCalls.find((toolCall) => toolCall.id === toolCallId); + if (existing !== undefined) return existing; + const toolCall: PromptJsonToolCall = { + type: 'function', + id: toolCallId, + function: { + name, + arguments: '', + }, + }; + this.toolCalls.push(toolCall); + return toolCall; + } + + private writeJsonLine(message: PromptJsonAssistantMessage | PromptJsonToolMessage): void { + this.stdout.write(`${JSON.stringify(message)}\n`); + } +} + +class PromptBlockWriter { + private started = false; + private atLineStart = false; + private lineWidth = 0; + private readonly wrapWidth: number | undefined; + + constructor(private readonly output: PromptOutput) { + this.wrapWidth = + typeof output.columns === 'number' && output.columns > PROMPT_BLOCK_INDENT.length + 1 + ? output.columns + : undefined; + } + + write(chunk: string): void { + if (chunk.length === 0) return; + let rendered = this.start(); + for (const char of chunk) { + if (this.atLineStart && char !== '\n') { + rendered += PROMPT_BLOCK_INDENT; + this.atLineStart = false; + this.lineWidth = PROMPT_BLOCK_INDENT.length; + } + const charWidth = visibleCharWidth(char); + if ( + this.wrapWidth !== undefined && + !this.atLineStart && + char !== '\n' && + this.lineWidth + charWidth > this.wrapWidth + ) { + rendered += `\n${PROMPT_BLOCK_INDENT}`; + this.lineWidth = PROMPT_BLOCK_INDENT.length; + } + rendered += char; + if (char === '\n') { + this.atLineStart = true; + this.lineWidth = 0; + } else { + this.lineWidth += charWidth; + } + } + this.output.write(rendered); + } + + finish(): void { + if (!this.started) return; + this.output.write(this.atLineStart ? '\n' : '\n\n'); + this.started = false; + this.atLineStart = false; + this.lineWidth = 0; + } + + private start(): string { + if (this.started) return ''; + this.started = true; + this.atLineStart = false; + this.lineWidth = PROMPT_BLOCK_BULLET.length; + return PROMPT_BLOCK_BULLET; + } +} + +function visibleCharWidth(char: string): number { + return char === '\t' ? 4 : 1; +} + +function formatHookResultPlain(event: HookResultEvent): string { + return `${formatHookResultTitle(event)}\n\n${formatHookResultBody(event)}`; +} + +function formatHookResultTitle(event: HookResultEvent): string { + return `${event.hookEvent} hook${event.blocked === true ? ' blocked' : ''}`; +} + +function formatHookResultBody(event: HookResultEvent): string { + const content = event.content.trim(); + return content.length === 0 ? '(empty)' : content; +} + +function stringifyJsonValue(value: unknown): string { + if (typeof value === 'string') return value; + const json = JSON.stringify(value); + return json ?? ''; +} + +function stringifyToolOutput(output: unknown): string { + if (typeof output === 'string') return output; + const json = JSON.stringify(output); + return json ?? String(output); +} + +function hasTurnId(event: Event): event is Event & { readonly turnId: number } { + return 'turnId' in event; +} + +function formatTurnEndedFailure(event: Extract): string { + if (event.error !== undefined) return `${event.error.code}: ${event.error.message}`; + return `Prompt turn ended with reason: ${event.reason}`; +} diff --git a/apps/kimi-code/src/cli/run-shell.ts b/apps/kimi-code/src/cli/run-shell.ts new file mode 100644 index 000000000..6e3e3204b --- /dev/null +++ b/apps/kimi-code/src/cli/run-shell.ts @@ -0,0 +1,180 @@ +import { execSync } from 'node:child_process'; +import { homedir } from 'node:os'; +import { join } from 'node:path'; + +import { createKimiDeviceId, KIMI_CODE_PROVIDER_NAME } from '@moonshot-ai/kimi-code-oauth'; +import { + initializeTelemetry, + setCrashPhase, + setTelemetryContext, + shutdownTelemetry, + track, + withTelemetryContext, +} from '@moonshot-ai/kimi-telemetry'; +import { KimiHarness, log, type TelemetryClient } from '@moonshot-ai/kimi-code-sdk'; + +import { CLI_SHUTDOWN_TIMEOUT_MS, CLI_UI_MODE, CLI_USER_AGENT_PRODUCT } from '#/constant/app'; +import { detectPendingMigration } from '#/migration/index'; +import type { TuiConfig } from '#/tui/config'; +import { loadTuiConfig, TuiConfigParseError } from '#/tui/config'; +import { CHROME_GUTTER } from '#/tui/constant/rendering'; +import { KimiTUI } from '#/tui/index'; +import { detectTerminalTheme } from '#/tui/theme/detect'; + +import type { CLIOptions } from './options'; +import { createKimiCodeHostIdentity } from './version'; + +export async function runShell( + opts: CLIOptions, + version: string, + runOptions: { readonly migrateOnly?: boolean } = {}, +): Promise { + const startedAt = Date.now(); + const configStartedAt = startedAt; + let tuiConfig: TuiConfig; + let configWarning: string | undefined; + try { + tuiConfig = await loadTuiConfig(); + } catch (error) { + if (!(error instanceof TuiConfigParseError)) throw error; + tuiConfig = error.fallback; + configWarning = error.message; + } + + // Resolve `theme = "auto"` against the live terminal once, before pi-tui + // grabs stdin. Explicit `dark` / `light` skip detection. + const resolvedTheme = tuiConfig.theme === 'auto' ? await detectTerminalTheme() : tuiConfig.theme; + + const workDir = process.cwd(); + const telemetryClient: TelemetryClient = { + track, + withContext: withTelemetryContext, + setContext: setTelemetryContext, + }; + const harness = new KimiHarness({ + identity: createKimiCodeHostIdentity(version), + telemetry: telemetryClient, + onOAuthRefresh: (outcome) => { + if (outcome.success) { + track('oauth_refresh', { success: true }); + return; + } + track('oauth_refresh', { + success: false, + reason: outcome.reason, + }); + }, + }); + log.info('kimi-code starting', { + version, + uiMode: CLI_UI_MODE, + nodeVersion: process.version, + platform: `${process.platform}/${process.arch}`, + workDir, + }); + await harness.ensureConfigFile(); + const migrationPlan = await detectPendingMigration({ + sourceHome: join(homedir(), '.kimi'), + targetHome: harness.homeDir, + ignoreMarker: runOptions.migrateOnly, + }); + if (runOptions.migrateOnly === true && migrationPlan === null) { + process.stdout.write(' Nothing to migrate from ~/.kimi/.\n'); + await harness.close(); + return; + } + const config = await harness.getConfig(); + const configMs = Date.now() - configStartedAt; + const tui = new KimiTUI(harness, { + cliOptions: opts, + tuiConfig, + version, + workDir, + startupNotice: configWarning, + resolvedTheme, + migrationPlan, + migrateOnly: runOptions.migrateOnly, + }); + + let firstLaunch = false; + const deviceId = createKimiDeviceId(harness.homeDir, { + onFirstLaunch: () => { + firstLaunch = true; + }, + }); + initializeTelemetry({ + homeDir: harness.homeDir, + deviceId, + enabled: config.telemetry !== false, + appName: CLI_USER_AGENT_PRODUCT, + version, + uiMode: CLI_UI_MODE, + model: config.defaultModel, + getAccessToken: async () => + (await harness.auth.getCachedAccessToken(KIMI_CODE_PROVIDER_NAME)) ?? null, + }); + setCrashPhase('runtime'); + + if (firstLaunch) { + harness.track('first_launch'); + } + const resumed = opts.continue || opts.session !== undefined; + const trackLifecycleForSession = ( + sessionId: string, + event: string, + properties?: Parameters[1], + ) => { + if (sessionId.length === 0) { + harness.track(event, properties); + return; + } + withTelemetryContext({ sessionId }).track(event, properties); + }; + const trackLifecycle = (event: string, properties?: Parameters[1]) => { + trackLifecycleForSession(tui.getCurrentSessionId(), event, properties); + }; + + tui.onExit = async (exitCode = 0) => { + const sessionId = tui.getCurrentSessionId(); + const hasContent = tui.hasSessionContent(); + setCrashPhase('shutdown'); + trackLifecycle('exit', { duration_s: (Date.now() - startedAt) / 1000 }); + await shutdownTelemetry({ timeoutMs: CLI_SHUTDOWN_TIMEOUT_MS }); + const gutter = ' '.repeat(CHROME_GUTTER); + process.stdout.write(`${gutter}Bye!\n`); + if (sessionId !== '' && hasContent) { + process.stderr.write(`\n${gutter}To resume this session: kimi -r ${sessionId}\n`); + } + process.exit(exitCode); + }; + try { + execSync('stty -ixon', { stdio: 'ignore' }); + } catch { + /* ignore */ + } + try { + const initStartedAt = Date.now(); + await tui.start(); + const initMs = Date.now() - initStartedAt; + trackLifecycle('started', { + resumed, + yolo: opts.yolo, + plan: opts.plan, + afk: false, + }); + const startupSessionId = tui.getCurrentSessionId(); + const mcpMs = await tui.getStartupMcpMs(); + trackLifecycleForSession(startupSessionId, 'startup_perf', { + duration_ms: Date.now() - startedAt, + config_ms: configMs, + init_ms: initMs, + mcp_ms: mcpMs, + }); + } catch (error) { + setCrashPhase('shutdown'); + trackLifecycle('exit', { duration_s: (Date.now() - startedAt) / 1000 }); + await shutdownTelemetry({ timeoutMs: CLI_SHUTDOWN_TIMEOUT_MS }); + await harness.close(); + throw error; + } +} diff --git a/apps/kimi-code/src/cli/startup-error.ts b/apps/kimi-code/src/cli/startup-error.ts new file mode 100644 index 000000000..b40b714f2 --- /dev/null +++ b/apps/kimi-code/src/cli/startup-error.ts @@ -0,0 +1,35 @@ +import { KIMI_ERROR_INFO, isKimiError } from '@moonshot-ai/kimi-code-sdk'; +import { chalkStderr } from 'chalk'; + +import { STARTUP_ERROR_COLOR } from '#/constant/startup-error'; + +export interface StartupErrorFormatOptions { + readonly errorStyle?: (text: string) => string; + readonly operation?: string; +} + +function formatUnknownErrorMessage(error: unknown): string { + return error instanceof Error ? error.message : String(error); +} + +export function formatStartupError( + error: unknown, + options: StartupErrorFormatOptions = {}, +): string { + const errorStyle = options.errorStyle ?? chalkStderr.hex(STARTUP_ERROR_COLOR); + + if (!isKimiError(error)) { + const operation = options.operation ?? 'start shell'; + return `${errorStyle(`error: failed to ${operation}: ${formatUnknownErrorMessage(error)}`)}\n`; + } + + const info = KIMI_ERROR_INFO[error.code]; + const lines = [ + errorStyle(`error: ${info.title}`), + '', + errorStyle('message:'), + errorStyle(error.message), + ]; + + return `${lines.join('\n')}\n`; +} diff --git a/apps/kimi-code/src/cli/sub/export.ts b/apps/kimi-code/src/cli/sub/export.ts new file mode 100644 index 000000000..93608cc3d --- /dev/null +++ b/apps/kimi-code/src/cli/sub/export.ts @@ -0,0 +1,224 @@ +/** + * `kimi export` sub-command. + * + * CLI glue only: session lookup, previous-session confirmation, and output. + * The actual ZIP/manifest export is owned by the SDK. + */ + +import { createInterface } from 'node:readline/promises'; + +import { createKimiDeviceId, KIMI_CODE_PROVIDER_NAME } from '@moonshot-ai/kimi-code-oauth'; +import { + initializeTelemetry, + setTelemetryContext, + shutdownTelemetry, + track, + withTelemetryContext, +} from '@moonshot-ai/kimi-telemetry'; +import { + KimiHarness, + type ExportSessionInput, + type ExportSessionResult, + type SessionSummary, + type TelemetryClient, +} from '@moonshot-ai/kimi-code-sdk'; +import type { Command } from 'commander'; + +import { CLI_SHUTDOWN_TIMEOUT_MS, CLI_UI_MODE, CLI_USER_AGENT_PRODUCT } from '#/constant/app'; +import { createKimiCodeHostIdentity } from '#/cli/version'; + +interface WritableLike { + write(chunk: string): boolean; +} + +export interface PreviousSessionSummary { + readonly workDir: string; + readonly sessionId: string; + readonly sessionDir: string; + readonly title?: string | undefined; +} + +export interface ExportDeps { + readonly listSessions: (workDir: string) => Promise; + readonly exportSession: (input: ExportSessionInput) => Promise; + readonly confirmPreviousSession: (summary: PreviousSessionSummary) => Promise; + readonly version: string; + readonly cwd: () => string; + readonly stdout: WritableLike; + readonly stderr: WritableLike; + readonly exit: (code: number) => never; +} + +export interface ExportOptions { + readonly yes: boolean; + readonly includeGlobalLog: boolean; +} + +export async function handleExport( + deps: ExportDeps, + sessionId: string | undefined, + output: string | undefined, + opts: ExportOptions, +): Promise { + const requestedId = normalizeOptionalSessionId(sessionId); + const previousSummary = requestedId === undefined ? await findPreviousSession(deps) : undefined; + + let resolvedId: string; + if (requestedId !== undefined) { + resolvedId = requestedId; + } else { + if (previousSummary === undefined) { + deps.stderr.write('No previous session found to export.\n'); + deps.exit(1); + } + resolvedId = previousSummary.id; + if (!opts.yes) { + const confirmed = await deps.confirmPreviousSession(toPreviousSessionSummary(previousSummary)); + if (!confirmed) { + deps.stdout.write('Export cancelled.\n'); + return; + } + } + } + + try { + const result = await deps.exportSession({ + id: resolvedId, + version: deps.version, + ...(output === undefined ? {} : { outputPath: output }), + ...(opts.includeGlobalLog ? { includeGlobalLog: true } : {}), + }); + deps.stdout.write(`${result.zipPath}\n`); + } catch (error) { + deps.stderr.write(`${errorMessage(error)}\n`); + deps.exit(1); + } +} + +export function registerExportCommand(parent: Command, deps?: Partial): void { + parent + .command('export') + .description('Export a session as a ZIP archive.') + .option('-o, --output ', 'Output ZIP path.') + .option('-y, --yes', 'Skip previous-session confirmation.') + .option( + '--no-include-global-log', + 'Skip bundling the active global diagnostic log (~/.kimi-code/logs/kimi-code.log, not rotated .1 files). By default the global log is included.', + ) + .argument('[sessionId]', 'Session id to export. Defaults to the most recent session.') + .action( + async ( + sessionId: string | undefined, + options: { output?: string; yes?: boolean; includeGlobalLog?: boolean }, + ) => { + await handleExport(createDefaultExportDeps(deps), sessionId, options.output, { + yes: options.yes === true, + includeGlobalLog: options.includeGlobalLog !== false, + }); + }, + ); +} + +function createDefaultExportDeps(overrides: Partial = {}): ExportDeps { + let harness: KimiHarness | undefined; + let telemetryInitialized = false; + let telemetryShutdown = false; + const identity = createKimiCodeHostIdentity(); + const telemetryClient: TelemetryClient = { + track, + withContext: withTelemetryContext, + setContext: setTelemetryContext, + }; + const getHarness = (): KimiHarness => { + harness ??= new KimiHarness({ + identity, + telemetry: telemetryClient, + }); + return harness; + }; + const initializeDefaultTelemetry = async (): Promise => { + if (telemetryInitialized) return; + const currentHarness = getHarness(); + await currentHarness.ensureConfigFile(); + const config = await currentHarness.getConfig(); + const deviceId = createKimiDeviceId(currentHarness.homeDir); + initializeTelemetry({ + homeDir: currentHarness.homeDir, + deviceId, + enabled: config.telemetry !== false, + appName: CLI_USER_AGENT_PRODUCT, + version: identity.version, + uiMode: CLI_UI_MODE, + model: config.defaultModel, + getAccessToken: async () => + (await currentHarness.auth.getCachedAccessToken(KIMI_CODE_PROVIDER_NAME)) ?? null, + }); + telemetryInitialized = true; + }; + const shutdownDefaultTelemetry = async (): Promise => { + if (!telemetryInitialized || telemetryShutdown) return; + telemetryShutdown = true; + await shutdownTelemetry({ timeoutMs: CLI_SHUTDOWN_TIMEOUT_MS }); + }; + return { + listSessions: + overrides.listSessions ?? + ((workDir: string) => + getHarness().listSessions({ + workDir, + })), + exportSession: + overrides.exportSession ?? + (async (input: ExportSessionInput) => { + await initializeDefaultTelemetry(); + try { + return await getHarness().exportSession(input); + } finally { + await shutdownDefaultTelemetry(); + } + }), + version: overrides.version ?? identity.version, + confirmPreviousSession: overrides.confirmPreviousSession ?? confirmPreviousSession, + cwd: overrides.cwd ?? (() => process.cwd()), + stdout: overrides.stdout ?? process.stdout, + stderr: overrides.stderr ?? process.stderr, + exit: overrides.exit ?? ((code: number) => process.exit(code)), + }; +} + +async function findPreviousSession(deps: Pick): Promise< + SessionSummary | undefined +> { + const sessions = await deps.listSessions(deps.cwd()); + return sessions[0]; +} + +function toPreviousSessionSummary(summary: SessionSummary): PreviousSessionSummary { + return { + workDir: summary.workDir, + sessionId: summary.id, + sessionDir: summary.sessionDir, + ...(summary.title === undefined ? {} : { title: summary.title }), + }; +} + +function normalizeOptionalSessionId(sessionId: string | undefined): string | undefined { + const trimmed = sessionId?.trim(); + return trimmed === undefined || trimmed === '' ? undefined : trimmed; +} + +async function confirmPreviousSession(summary: PreviousSessionSummary): Promise { + const rl = createInterface({ input: process.stdin, output: process.stderr }); + try { + const title = summary.title === undefined ? summary.sessionId : `${summary.title} (${summary.sessionId})`; + const answer = await rl.question(`Export previous session "${title}"? [Y/n] `); + const trimmed = answer.trim().toLowerCase(); + return trimmed === '' || trimmed === 'y' || trimmed === 'yes'; + } finally { + rl.close(); + } +} + +function errorMessage(error: unknown): string { + return error instanceof Error ? error.message : String(error); +} diff --git a/apps/kimi-code/src/cli/update/cache.ts b/apps/kimi-code/src/cli/update/cache.ts new file mode 100644 index 000000000..83132bf36 --- /dev/null +++ b/apps/kimi-code/src/cli/update/cache.ts @@ -0,0 +1,31 @@ +import { z } from 'zod'; + +import { getUpdateStateFile } from '#/utils/paths'; +import { readJsonFile, writeJsonFile } from '#/utils/persistence'; + +import { emptyUpdateCache, type UpdateCache } from './types'; + +const UpdateCacheSchema: z.ZodType = z + .object({ + source: z.literal('cdn'), + checkedAt: z.string().min(1).nullable(), + latest: z.string().min(1).nullable(), + }) + .strict(); + +export async function readUpdateCache( + filePath: string = getUpdateStateFile(), +): Promise { + try { + return await readJsonFile(filePath, UpdateCacheSchema, emptyUpdateCache()); + } catch { + return emptyUpdateCache(); + } +} + +export async function writeUpdateCache( + value: UpdateCache, + filePath: string = getUpdateStateFile(), +): Promise { + await writeJsonFile(filePath, UpdateCacheSchema, value); +} diff --git a/apps/kimi-code/src/cli/update/cdn.ts b/apps/kimi-code/src/cli/update/cdn.ts new file mode 100644 index 000000000..5664f021f --- /dev/null +++ b/apps/kimi-code/src/cli/update/cdn.ts @@ -0,0 +1,27 @@ +import { valid } from 'semver'; + +import { KIMI_CODE_CDN_LATEST_URL } from '#/constant/app'; + +/** + * Fetch the latest published Kimi Code version from the CDN. + * + * **Throws** on any failure (network error, non-2xx, empty body, non-semver + * text). Callers must catch — `refreshUpdateCache` deliberately lets the + * error propagate so the existing cache stays intact instead of being + * overwritten with a null `latest` on a transient blip. + * + * `fetchImpl` is injectable for tests; defaults to the global `fetch`. + */ +export async function fetchLatestVersionFromCdn( + fetchImpl: typeof fetch = fetch, +): Promise { + const response = await fetchImpl(KIMI_CODE_CDN_LATEST_URL); + if (!response.ok) { + throw new Error(`CDN /latest returned HTTP ${response.status}`); + } + const raw = (await response.text()).trim(); + if (valid(raw) === null) { + throw new Error(`CDN /latest returned invalid semver: ${JSON.stringify(raw)}`); + } + return raw; +} diff --git a/apps/kimi-code/src/cli/update/preflight.ts b/apps/kimi-code/src/cli/update/preflight.ts new file mode 100644 index 000000000..385f1807e --- /dev/null +++ b/apps/kimi-code/src/cli/update/preflight.ts @@ -0,0 +1,246 @@ +import { spawn } from 'node:child_process'; + +import type { TelemetryProperties } from '@moonshot-ai/kimi-telemetry'; + +import { + NATIVE_INSTALL_COMMAND_UNIX, + NATIVE_INSTALL_COMMAND_WIN, +} from '#/constant/app'; + +import { readUpdateCache } from './cache'; +import { promptForInstallConfirmation, type InstallPromptOptions } from './prompt'; +import { refreshUpdateCache } from './refresh'; +import { selectUpdateTarget } from './select'; +import { detectInstallSource } from './source'; +import { + NPM_PACKAGE_NAME, + type InstallSource, + type UpdateDecision, + type UpdatePreflightResult, + type UpdateTarget, +} from './types'; + +export type { UpdatePreflightResult } from './types'; + +export interface RunUpdatePreflightOptions { + readonly stdout?: { write(chunk: string): boolean }; + readonly stderr?: { write(chunk: string): boolean }; + readonly isTTY?: boolean; + readonly track?: (event: string, properties?: TelemetryProperties) => void; +} + +function withCmdSuffix(base: string, platform: NodeJS.Platform): string { + return platform === 'win32' ? `${base}.cmd` : base; +} + +function bunCommand(platform: NodeJS.Platform): string { + return platform === 'win32' ? 'bun.exe' : 'bun'; +} + +function installCommandFor( + source: InstallSource, + version: string, + platform: NodeJS.Platform, +): string { + switch (source) { + case 'npm-global': + return `npm install -g ${NPM_PACKAGE_NAME}@${version}`; + case 'pnpm-global': + return `pnpm add -g ${NPM_PACKAGE_NAME}@${version}`; + case 'yarn-global': + return `yarn global add ${NPM_PACKAGE_NAME}@${version}`; + case 'bun-global': + return `bun add -g ${NPM_PACKAGE_NAME}@${version}`; + case 'native': + return platform === 'win32' ? NATIVE_INSTALL_COMMAND_WIN : NATIVE_INSTALL_COMMAND_UNIX; + case 'unsupported': + return `npm install -g ${NPM_PACKAGE_NAME}@${version}`; + } +} + +function canAutoInstall(source: InstallSource, platform: NodeJS.Platform): boolean { + switch (source) { + case 'npm-global': + case 'pnpm-global': + case 'yarn-global': + case 'bun-global': + return true; + case 'native': + return platform !== 'win32'; + case 'unsupported': + return false; + } +} + +interface SpawnCommand { + readonly cmd: string; + readonly args: readonly string[]; +} + +function spawnForSource( + source: InstallSource, + version: string, + platform: NodeJS.Platform, +): SpawnCommand { + switch (source) { + case 'npm-global': + return { cmd: withCmdSuffix('npm', platform), args: ['install', '-g', `${NPM_PACKAGE_NAME}@${version}`] }; + case 'pnpm-global': + return { cmd: withCmdSuffix('pnpm', platform), args: ['add', '-g', `${NPM_PACKAGE_NAME}@${version}`] }; + case 'yarn-global': + return { cmd: withCmdSuffix('yarn', platform), args: ['global', 'add', `${NPM_PACKAGE_NAME}@${version}`] }; + case 'bun-global': + return { cmd: bunCommand(platform), args: ['add', '-g', `${NPM_PACKAGE_NAME}@${version}`] }; + case 'native': + return { cmd: 'bash', args: ['-c', NATIVE_INSTALL_COMMAND_UNIX] }; + case 'unsupported': + throw new Error('unsupported install source cannot be auto-installed'); + } +} + +function formatErrorMessage(error: unknown): string { + return error instanceof Error ? error.message : String(error); +} + +function renderManualUpdateMessage( + currentVersion: string, + target: UpdateTarget, + source: InstallSource, + installCommand: string, +): string { + const sourceDesc = + source === 'native' + ? 'native (windows). Auto-update is not supported on this platform.' + : 'unsupported package manager or layout.'; + return ( + `A newer version of ${NPM_PACKAGE_NAME} is available ` + + `(${currentVersion} -> ${target.version}).\n` + + `Detected install source: ${sourceDesc}\n` + + `To update manually, run: ${installCommand}\n` + ); +} + +function renderInstallSuccessMessage(target: UpdateTarget): string { + return `Updated ${NPM_PACKAGE_NAME} to ${target.version}. Restart the CLI to use the new version.\n`; +} + +function refreshInBackground(): void { + void refreshUpdateCache().catch(() => {}); +} + +function trackUpdatePrompted( + track: RunUpdatePreflightOptions['track'], + currentVersion: string, + target: UpdateTarget, + source: InstallSource, + decision: UpdateDecision, +): void { + try { + track?.('update_prompted', { + current: currentVersion, + latest: target.version, + current_version: currentVersion, + target_version: target.version, + source, + decision, + }); + } catch { + // Telemetry must never affect update prompting. + } +} + +async function promptInstall( + currentVersion: string, + target: UpdateTarget, + source: InstallSource, + installCommand: string, +): Promise { + const options: InstallPromptOptions = { + currentVersion, + target, + installSource: source, + installCommand, + }; + return promptForInstallConfirmation(options); +} + +async function installUpdate( + source: InstallSource, + version: string, + platform: NodeJS.Platform, +): Promise { + const { cmd, args } = spawnForSource(source, version, platform); + await new Promise((resolve, reject) => { + const child = spawn(cmd, [...args], { stdio: 'inherit' }); + child.once('error', reject); + child.once('exit', (code, signal) => { + if (code === 0) { + resolve(); + return; + } + const detail = signal !== null ? `signal ${signal}` : `code ${String(code)}`; + reject(new Error(`${cmd} exited with ${detail}`)); + }); + }); +} + +export function decideUpdateAction( + target: UpdateTarget | null, + isInteractive: boolean, + source: InstallSource, + platform: NodeJS.Platform, +): UpdateDecision { + if (target === null || !isInteractive) return 'none'; + return canAutoInstall(source, platform) ? 'prompt-install' : 'manual-command'; +} + +export async function runUpdatePreflight( + currentVersion: string, + options: RunUpdatePreflightOptions = {}, +): Promise { + const stdout = options.stdout ?? process.stdout; + const stderr = options.stderr ?? process.stderr; + const platform = process.platform; + + try { + const cache = await readUpdateCache().catch(() => null); + const latest = cache?.latest ?? null; + const target = selectUpdateTarget(currentVersion, latest); + refreshInBackground(); + + const isInteractive = + options.isTTY ?? (process.stdin.isTTY && process.stdout.isTTY); + const source: InstallSource = + target === null || !isInteractive + ? 'unsupported' + : await detectInstallSource().catch(() => 'unsupported' as const); + + const decision = decideUpdateAction(target, isInteractive, source, platform); + if (decision === 'none' || target === null) return 'continue'; + + const installCommand = installCommandFor(source, target.version, platform); + trackUpdatePrompted(options.track, currentVersion, target, source, decision); + + if (decision === 'manual-command') { + stdout.write(renderManualUpdateMessage(currentVersion, target, source, installCommand)); + return 'continue'; + } + + const confirmed = await promptInstall(currentVersion, target, source, installCommand); + if (!confirmed) return 'continue'; + + try { + await installUpdate(source, target.version, platform); + stdout.write(renderInstallSuccessMessage(target)); + return 'exit'; + } catch (error) { + stderr.write( + `warning: failed to install ${NPM_PACKAGE_NAME}@${target.version}: ` + + `${formatErrorMessage(error)}\n`, + ); + return 'continue'; + } + } catch { + return 'continue'; + } +} diff --git a/apps/kimi-code/src/cli/update/prompt.ts b/apps/kimi-code/src/cli/update/prompt.ts new file mode 100644 index 000000000..4bcd0e3e9 --- /dev/null +++ b/apps/kimi-code/src/cli/update/prompt.ts @@ -0,0 +1,184 @@ +import { clearLine, cursorTo, emitKeypressEvents, moveCursor } from 'node:readline'; + +import chalk from 'chalk'; + +import { PRODUCT_NAME } from '#/constant/app'; +import { HIDE_CURSOR, SHOW_CURSOR } from '#/constant/terminal'; +import { + UPDATE_PROMPT_MUTED, + UPDATE_PROMPT_PRIMARY, + UPDATE_PROMPT_SUCCESS, + UPDATE_PROMPT_TEXT_DIM, + UPDATE_PROMPT_WARNING, +} from '#/constant/update'; + +import { type InstallSource, type UpdateTarget } from './types'; + +export type InstallPromptChoiceValue = 'install' | 'skip'; + +export interface InstallPromptChoice { + readonly value: InstallPromptChoiceValue; + readonly label: string; +} + +export interface InstallPromptOptions { + readonly currentVersion: string; + readonly target: UpdateTarget; + readonly installCommand: string; + readonly installSource: InstallSource; + readonly input?: NodeJS.ReadStream; + readonly output?: NodeJS.WriteStream; +} + +const INSTALL_HINT = 'Install update now'; +const SKIP_HINT = 'Continue with current version'; + +export function createInstallPromptChoices(target: UpdateTarget): readonly InstallPromptChoice[] { + return [ + { value: 'install', label: `${INSTALL_HINT} (${target.version})` }, + { value: 'skip', label: SKIP_HINT }, + ]; +} + +export function getDefaultInstallPromptSelection(choices: readonly InstallPromptChoice[]): number { + const installIndex = choices.findIndex((choice) => choice.value === 'install'); + return Math.max(installIndex, 0); +} + +export function moveInstallPromptSelection( + currentIndex: number, + direction: 'up' | 'down', + choiceCount: number, +): number { + if (direction === 'up') { + return Math.max(0, currentIndex - 1); + } + return Math.min(choiceCount - 1, currentIndex + 1); +} + +function renderInstallPrompt( + options: InstallPromptOptions, + choices: readonly InstallPromptChoice[], + selectedIndex: number, +): readonly string[] { + const label = chalk.hex(UPDATE_PROMPT_TEXT_DIM).bold; + const currentVersion = chalk.hex(UPDATE_PROMPT_WARNING).bold(options.currentVersion); + const targetVersion = chalk.hex(UPDATE_PROMPT_SUCCESS).bold(options.target.version); + const sourceLabel = chalk.hex(UPDATE_PROMPT_PRIMARY).bold(options.installSource); + const command = chalk.hex(UPDATE_PROMPT_PRIMARY)(options.installCommand); + const lines = [ + chalk.hex(UPDATE_PROMPT_PRIMARY).bold('Kimi Code Update Available'), + chalk.hex(UPDATE_PROMPT_MUTED)(`${PRODUCT_NAME} has a newer release ready.`), + '', + `${label('Current')} ${currentVersion}`, + `${label('Target ')} ${targetVersion}`, + `${label('Source ')} ${sourceLabel}`, + `${label('Command')} ${command}`, + '', + chalk.hex(UPDATE_PROMPT_MUTED)('↑↓ choose · Enter confirm · Esc continue'), + '', + ]; + + for (let i = 0; i < choices.length; i++) { + const choice = choices[i]; + if (choice === undefined) continue; + const isSelected = i === selectedIndex; + const pointer = isSelected ? '❯' : ' '; + const content = ` ${pointer} ${choice.label}`; + if (isSelected) { + lines.push(chalk.hex(UPDATE_PROMPT_PRIMARY).bold(content)); + continue; + } + lines.push(chalk.hex(UPDATE_PROMPT_TEXT_DIM)(content)); + } + + return lines; +} + +function writePromptFrame( + output: NodeJS.WriteStream, + lines: readonly string[], + previousLineCount: number, +): number { + if (previousLineCount > 0) { + moveCursor(output, 0, -(previousLineCount - 1)); + } + + for (let i = 0; i < lines.length; i++) { + clearLine(output, 0); + cursorTo(output, 0); + output.write(lines[i] ?? ''); + if (i < lines.length - 1) { + output.write('\n'); + } + } + + return lines.length; +} + +export async function promptForInstallConfirmation( + options: InstallPromptOptions, +): Promise { + const input = options.input ?? process.stdin; + const output = options.output ?? process.stdout; + const choices = createInstallPromptChoices(options.target); + let selectedIndex = getDefaultInstallPromptSelection(choices); + + return new Promise((resolve) => { + let lineCount = 0; + const hadRawMode = 'isRaw' in input ? input.isRaw : false; + const canSetRawMode = typeof input.setRawMode === 'function'; + + const cleanup = (): void => { + input.off('keypress', onKeypress); + if (canSetRawMode) { + input.setRawMode(hadRawMode); + } + output.write(SHOW_CURSOR); + output.write('\n'); + }; + + const finish = (choice: InstallPromptChoiceValue): void => { + cleanup(); + resolve(choice === 'install'); + }; + + const render = (): void => { + lineCount = writePromptFrame( + output, + renderInstallPrompt(options, choices, selectedIndex), + lineCount, + ); + }; + + const onKeypress = (_input: string, key: { name?: string; ctrl?: boolean }): void => { + if (key.name === 'up') { + selectedIndex = moveInstallPromptSelection(selectedIndex, 'up', choices.length); + render(); + return; + } + if (key.name === 'down') { + selectedIndex = moveInstallPromptSelection(selectedIndex, 'down', choices.length); + render(); + return; + } + if (key.name === 'return' || key.name === 'enter') { + const chosen = choices[selectedIndex]?.value ?? 'skip'; + finish(chosen); + return; + } + if (key.name === 'escape' || (key.ctrl === true && key.name === 'c')) { + finish('skip'); + } + }; + + emitKeypressEvents(input); + if (canSetRawMode) { + input.setRawMode(true); + } + input.resume(); + input.on('keypress', onKeypress); + output.write(HIDE_CURSOR); + render(); + }); +} diff --git a/apps/kimi-code/src/cli/update/refresh.ts b/apps/kimi-code/src/cli/update/refresh.ts new file mode 100644 index 000000000..20bcdf0de --- /dev/null +++ b/apps/kimi-code/src/cli/update/refresh.ts @@ -0,0 +1,32 @@ +import { writeUpdateCache } from './cache'; +import { fetchLatestVersionFromCdn } from './cdn'; +import { type UpdateCache } from './types'; + +export interface RefreshUpdateCacheDeps { + /** Resolves with the latest semver. **Throws** on any failure — callers + * (including the default background invocation in preflight) must catch. + * Errors intentionally skip `writeCache` so a transient CDN blip does not + * overwrite a previously known `latest` with `null`. */ + readonly fetchLatest: () => Promise; + readonly writeCache: (cache: UpdateCache) => Promise; + readonly now: () => Date; +} + +export async function refreshUpdateCache( + overrides: Partial = {}, +): Promise { + const resolved: RefreshUpdateCacheDeps = { + fetchLatest: overrides.fetchLatest ?? (() => fetchLatestVersionFromCdn()), + writeCache: overrides.writeCache ?? writeUpdateCache, + now: overrides.now ?? (() => new Date()), + }; + + const latest = await resolved.fetchLatest(); + const cache: UpdateCache = { + source: 'cdn', + checkedAt: resolved.now().toISOString(), + latest, + }; + await resolved.writeCache(cache); + return cache; +} diff --git a/apps/kimi-code/src/cli/update/select.ts b/apps/kimi-code/src/cli/update/select.ts new file mode 100644 index 000000000..bf241ed80 --- /dev/null +++ b/apps/kimi-code/src/cli/update/select.ts @@ -0,0 +1,13 @@ +import { gt, valid } from 'semver'; + +import { type UpdateTarget } from './types'; + +export function selectUpdateTarget( + currentVersion: string, + latest: string | null, +): UpdateTarget | null { + if (latest === null) return null; + if (valid(currentVersion) === null || valid(latest) === null) return null; + if (!gt(latest, currentVersion)) return null; + return { version: latest }; +} diff --git a/apps/kimi-code/src/cli/update/source.ts b/apps/kimi-code/src/cli/update/source.ts new file mode 100644 index 000000000..4b544d140 --- /dev/null +++ b/apps/kimi-code/src/cli/update/source.ts @@ -0,0 +1,155 @@ +import { execFile } from 'node:child_process'; +import { realpathSync } from 'node:fs'; +import { createRequire } from 'node:module'; +import { join, resolve } from 'node:path'; + +import { getHostPackageRoot } from '#/cli/version'; + +import { NPM_PACKAGE_NAME, type InstallSource } from './types'; + +const nodeRequire = createRequire(import.meta.url); + +interface NodeSeaModule { + isSea(): boolean; +} + +let cachedSea: NodeSeaModule | null | undefined; + +function loadSeaModule(): NodeSeaModule | null { + if (cachedSea !== undefined) return cachedSea; + try { + cachedSea = nodeRequire('node:sea') as NodeSeaModule; + } catch { + cachedSea = null; + } + return cachedSea; +} + +/** Runtime SEA detection — true when running as a packaged native binary. */ +export function detectNativeInstall(): boolean { + const sea = loadSeaModule(); + if (sea === null) return false; + try { + return sea.isSea(); + } catch { + return false; + } +} + +// Path heuristic markers (compared in lowercase; both forward and backward slashes accepted). +const PNPM_PATH_SEGMENT = 'pnpm/global/'; +const YARN_PATH_SEGMENTS = ['.config/yarn/global/', '/.yarn/global/']; +const BUN_PATH_SEGMENT = '.bun/install/global/'; + +function normalizeForHeuristic(filePath: string): string { + return filePath.replaceAll('\\', '/').toLowerCase(); +} + +/** + * Heuristic classification by package root path segments. Returns the + * matching `InstallSource` or `null` if no heuristic matches (caller should + * fall through to npm-prefix comparison). + */ +export function classifyByPathHeuristic(packageRoot: string): InstallSource | null { + const normalized = normalizeForHeuristic(packageRoot); + if (normalized.includes(PNPM_PATH_SEGMENT)) return 'pnpm-global'; + for (const seg of YARN_PATH_SEGMENTS) { + if (normalized.includes(seg)) return 'yarn-global'; + } + if (normalized.includes(BUN_PATH_SEGMENT)) return 'bun-global'; + return null; +} + +export interface DetectInstallSourceDeps { + readonly getPackageRoot: () => string; + readonly getGlobalPrefix: () => Promise; + readonly detectNative: () => boolean; + readonly platform: NodeJS.Platform; +} + +function npmCommand(platform: NodeJS.Platform): string { + return platform === 'win32' ? 'npm.cmd' : 'npm'; +} + +function execFileText(command: string, args: readonly string[]): Promise { + return new Promise((resolveOutput, reject) => { + execFile(command, [...args], { encoding: 'utf-8' }, (error, stdout) => { + if (error) { + reject(error); + return; + } + resolveOutput(stdout); + }); + }); +} + +function normalizePathForComparison(filePath: string, platform: NodeJS.Platform): string | null { + const trimmed = filePath.trim(); + if (trimmed.length === 0) return null; + try { + return normalizeResolvedPath(realpathSync(trimmed), platform); + } catch { + return normalizeResolvedPath(resolve(trimmed), platform); + } +} + +function normalizeResolvedPath(filePath: string, platform: NodeJS.Platform): string { + const resolvedPath = resolve(filePath); + return platform === 'win32' ? resolvedPath.toLowerCase() : resolvedPath; +} + +function candidateGlobalPackageDirs( + globalPrefix: string, + platform: NodeJS.Platform, +): readonly string[] { + if (platform === 'win32') { + return [join(globalPrefix, 'node_modules', NPM_PACKAGE_NAME)]; + } + return [ + join(globalPrefix, 'lib', 'node_modules', NPM_PACKAGE_NAME), + join(globalPrefix, 'node_modules', NPM_PACKAGE_NAME), + ]; +} + +export function classifyInstallSource( + packageRoot: string, + globalPrefix: string, + platform: NodeJS.Platform = process.platform, +): InstallSource { + const normalizedPackageRoot = normalizePathForComparison(packageRoot, platform); + if (normalizedPackageRoot === null) return 'unsupported'; + + for (const candidate of candidateGlobalPackageDirs(globalPrefix, platform)) { + if (normalizePathForComparison(candidate, platform) === normalizedPackageRoot) { + return 'npm-global'; + } + } + return 'unsupported'; +} + +export async function detectInstallSource( + deps: Partial = {}, +): Promise { + const platform = deps.platform ?? process.platform; + const resolved: DetectInstallSourceDeps = { + getPackageRoot: deps.getPackageRoot ?? getHostPackageRoot, + getGlobalPrefix: + deps.getGlobalPrefix ?? + (() => execFileText(npmCommand(platform), ['prefix', '-g']).then((text) => text.trim())), + detectNative: deps.detectNative ?? detectNativeInstall, + platform, + }; + + if (resolved.detectNative()) return 'native'; + + const packageRoot = resolved.getPackageRoot(); + const heuristic = classifyByPathHeuristic(packageRoot); + if (heuristic !== null) return heuristic; + + try { + const globalPrefix = await resolved.getGlobalPrefix(); + return classifyInstallSource(packageRoot, globalPrefix, resolved.platform); + } catch { + return 'unsupported'; + } +} diff --git a/apps/kimi-code/src/cli/update/types.ts b/apps/kimi-code/src/cli/update/types.ts new file mode 100644 index 000000000..652fc97d5 --- /dev/null +++ b/apps/kimi-code/src/cli/update/types.ts @@ -0,0 +1,33 @@ +import { NPM_PACKAGE_NAME } from '#/constant/app'; + +export { NPM_PACKAGE_NAME }; + +/** Where the running CLI was installed from. Drives update command + spawn. */ +export type InstallSource = + | 'npm-global' + | 'pnpm-global' + | 'yarn-global' + | 'bun-global' + | 'native' + | 'unsupported'; + +export interface UpdateTarget { + readonly version: string; +} + +export interface UpdateCache { + readonly source: 'cdn'; + readonly checkedAt: string | null; + readonly latest: string | null; +} + +export type UpdateDecision = 'none' | 'prompt-install' | 'manual-command'; +export type UpdatePreflightResult = 'continue' | 'exit'; + +export function emptyUpdateCache(): UpdateCache { + return { + source: 'cdn', + checkedAt: null, + latest: null, + }; +} diff --git a/apps/kimi-code/src/cli/version.ts b/apps/kimi-code/src/cli/version.ts new file mode 100644 index 000000000..f0a9f695d --- /dev/null +++ b/apps/kimi-code/src/cli/version.ts @@ -0,0 +1,63 @@ +/** + * Kimi Code version helpers. + * + * `getVersion` reads the host CLI's `package.json#version`. + */ + +import { existsSync, readFileSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; + +import { createKimiDefaultHeaders, type KimiHostIdentity } from '@moonshot-ai/kimi-code-oauth'; + +import { CLI_USER_AGENT_PRODUCT } from '#/constant/app'; + +import { getDataDir } from '../utils/paths'; +import { KIMI_BUILD_INFO } from './build-info'; + +const MODULE_DIR = import.meta.dirname; + +export function getHostPackageJsonPath(): string { + // Walk upwards from this file's directory until a `package.json` shows up, + // so both dev (`tsx src/main.ts` — this file in `src/cli/`, pkg 2 levels + // up) and prod (`node dist/main.mjs` — this code bundled into `dist/`, + // pkg 1 level up) resolve correctly. + let dir = MODULE_DIR; + for (let i = 0; i < 6; i++) { + const candidate = resolve(dir, 'package.json'); + if (existsSync(candidate)) { + return candidate; + } + const parent = dirname(dir); + if (parent === dir) break; + dir = parent; + } + throw new Error(`Could not locate package.json near ${MODULE_DIR}`); +} + +export function getHostPackageRoot(): string { + return dirname(getHostPackageJsonPath()); +} + +export function getVersion(): string { + if (KIMI_BUILD_INFO.version !== undefined) { + return KIMI_BUILD_INFO.version; + } + const pkg = JSON.parse(readFileSync(getHostPackageJsonPath(), 'utf-8')) as { + version: string; + }; + return pkg.version; +} + +export function createKimiCodeHostIdentity(version = getVersion()): KimiHostIdentity { + return { + userAgentProduct: CLI_USER_AGENT_PRODUCT, + version, + }; +} + +export function buildKimiDefaultHeaders(version: string): Record { + return createKimiDefaultHeaders({ + homeDir: getDataDir(), + ...createKimiCodeHostIdentity(version), + }); +} diff --git a/apps/kimi-code/src/constant/app.ts b/apps/kimi-code/src/constant/app.ts new file mode 100644 index 000000000..d6d37c3a2 --- /dev/null +++ b/apps/kimi-code/src/constant/app.ts @@ -0,0 +1,49 @@ +import { ErrorCodes } from '@moonshot-ai/kimi-code-sdk'; + +export const PRODUCT_NAME = 'Kimi Code'; +export const CLI_COMMAND_NAME = 'kimi'; + +// Used in telemetry app names and HTTP User-Agent headers. +export const CLI_USER_AGENT_PRODUCT = 'kimi-code-cli'; +export const CLI_UI_MODE = 'shell'; + +// Give telemetry a short flush window without making CLI exit feel stuck. +export const CLI_SHUTDOWN_TIMEOUT_MS = 3000; + +// Published npm package name; this can differ from the executable command. +export const NPM_PACKAGE_NAME = '@moonshot-ai/kimi-code'; + +// App-owned data paths. SDK/core runtime config is intentionally not routed here. +export const KIMI_CODE_HOME_ENV = 'KIMI_CODE_HOME'; +export const KIMI_CODE_DATA_DIR_NAME = '.kimi-code'; +export const KIMI_CODE_LOG_DIR_NAME = 'logs'; +export const KIMI_CODE_UPDATE_DIR_NAME = 'updates'; +export const KIMI_CODE_UPDATE_STATE_FILE_NAME = 'latest.json'; +export const KIMI_CODE_INPUT_HISTORY_DIR_NAME = 'user-history'; + +// Managed Kimi auth provider key shared with OAuth/SDK config. +export const DEFAULT_OAUTH_PROVIDER_NAME = 'managed:kimi-code'; + +// SDK/core error code that tells the TUI to show a login-required startup +// notice. Derived from sdk's ErrorCodes so a future rename in core +// auto-propagates instead of silently breaking the startup recovery path. +export const OAUTH_LOGIN_REQUIRED_CODE = ErrorCodes.AUTH_LOGIN_REQUIRED; + +export const FEEDBACK_ISSUE_URL = 'https://github.com/MoonshotAI/kimi-code/issues'; + +// Sent in the feedback `version` field so the backend can distinguish this +// TypeScript client from clients that send a bare version. +export const FEEDBACK_VERSION_PREFIX = 'kimi-code-'; + +// Telemetry event name; keep stable for dashboard queries. +export const FEEDBACK_TELEMETRY_EVENT = 'feedback_submitted'; + +// CDN source of truth: all version checks and native install scripts pull from here. +export const KIMI_CODE_CDN_BASE = 'https://code.kimi.com/kimi-code'; +export const KIMI_CODE_CDN_LATEST_URL = `${KIMI_CODE_CDN_BASE}/latest`; +export const KIMI_CODE_INSTALL_SH_URL = `${KIMI_CODE_CDN_BASE}/install.sh`; +export const KIMI_CODE_INSTALL_PS1_URL = `${KIMI_CODE_CDN_BASE}/install.ps1`; + +// Native install commands, split by platform. Use these for prompt copy and spawn calls only; do not assemble the strings elsewhere. +export const NATIVE_INSTALL_COMMAND_UNIX = `curl -fsSL ${KIMI_CODE_INSTALL_SH_URL} | bash`; +export const NATIVE_INSTALL_COMMAND_WIN = `irm ${KIMI_CODE_INSTALL_PS1_URL} | iex`; diff --git a/apps/kimi-code/src/constant/index.ts b/apps/kimi-code/src/constant/index.ts new file mode 100644 index 000000000..afa11b2a1 --- /dev/null +++ b/apps/kimi-code/src/constant/index.ts @@ -0,0 +1,4 @@ +export * from './app'; +export * from './startup-error'; +export * from './terminal'; +export * from './update'; diff --git a/apps/kimi-code/src/constant/startup-error.ts b/apps/kimi-code/src/constant/startup-error.ts new file mode 100644 index 000000000..a59cb9093 --- /dev/null +++ b/apps/kimi-code/src/constant/startup-error.ts @@ -0,0 +1,2 @@ +// Pre-TUI startup errors render directly to stderr, before theme detection. +export const STARTUP_ERROR_COLOR = '#E85454'; diff --git a/apps/kimi-code/src/constant/terminal.ts b/apps/kimi-code/src/constant/terminal.ts new file mode 100644 index 000000000..22a4dfdfd --- /dev/null +++ b/apps/kimi-code/src/constant/terminal.ts @@ -0,0 +1,8 @@ +// C0/control-sequence bytes used to build terminal protocol messages. +export const ESC = '\u001B'; +export const BEL = '\u0007'; +export const ST = '\\'; + +// ANSI cursor visibility toggles used by CLI prompts. +export const HIDE_CURSOR = `${ESC}[?25l`; +export const SHOW_CURSOR = `${ESC}[?25h`; diff --git a/apps/kimi-code/src/constant/update.ts b/apps/kimi-code/src/constant/update.ts new file mode 100644 index 000000000..9881219cf --- /dev/null +++ b/apps/kimi-code/src/constant/update.ts @@ -0,0 +1,7 @@ +// Local palette for the pre-TUI update prompt. TUI colors still come +// from `src/tui/theme`; do not use these in interactive TUI components. +export const UPDATE_PROMPT_PRIMARY = '#1783ff'; +export const UPDATE_PROMPT_SUCCESS = '#16A34A'; +export const UPDATE_PROMPT_WARNING = '#CA8A04'; +export const UPDATE_PROMPT_MUTED = '#999999'; +export const UPDATE_PROMPT_TEXT_DIM = '#6B7280'; diff --git a/apps/kimi-code/src/main.ts b/apps/kimi-code/src/main.ts new file mode 100644 index 000000000..d2804a939 --- /dev/null +++ b/apps/kimi-code/src/main.ts @@ -0,0 +1,128 @@ +/** + * Kimi Code entry point. + * + * Parses CLI arguments via Commander.js, validates options, runs the + * outer update preflight, then delegates to the requested UI runner. + */ + +import { + flushDiagnosticLogs, + log, + resolveGlobalLogPath, + resolveKimiHome, +} from '@moonshot-ai/kimi-code-sdk'; +import { installCrashHandlers, track } from '@moonshot-ai/kimi-telemetry'; + +import { createProgram } from './cli/commands'; +import type { CLIOptions } from './cli/options'; +import { OptionConflictError, validateOptions } from './cli/options'; +import { runPrompt } from './cli/run-prompt'; +import { runShell } from './cli/run-shell'; +import { formatStartupError } from './cli/startup-error'; +import { runUpdatePreflight } from './cli/update/preflight'; +import { getVersion } from './cli/version'; +import { cleanupStaleNativeCacheForCurrent } from './native/native-assets'; +import { installNativeModuleHook } from './native/module-hook'; +import { runNativeAssetSmokeIfRequested } from './native/smoke'; +import { initProcessName } from './utils/process/proctitle'; + +export async function handleMainCommand(opts: CLIOptions, version: string): Promise { + let validated: ReturnType; + try { + validated = validateOptions(opts); + } catch (error) { + if (error instanceof OptionConflictError) { + process.stderr.write(`error: ${error.message}\n`); + process.exit(1); + } + throw error; + } + + const preflightResult = await runUpdatePreflight( + version, + validated.uiMode === 'print' ? { track, isTTY: false } : { track }, + ); + if (preflightResult === 'exit') { + process.exit(0); + } + + if (validated.uiMode === 'print') { + await runPrompt(validated.options, version); + return; + } + + await runShell(validated.options, version); +} + +/** `kimi migrate`: launch the migration screen only, then exit. */ +async function handleMigrateCommand(version: string): Promise { + await runShell(MIGRATE_CLI_OPTIONS, version, { migrateOnly: true }); +} + +/** A neutral CLIOptions value — `kimi migrate` never opens a chat session. */ +const MIGRATE_CLI_OPTIONS: CLIOptions = { + session: undefined, + continue: false, + yolo: false, + plan: false, + model: undefined, + outputFormat: undefined, + prompt: undefined, + skillsDirs: [], +}; + +export function main(): void { + initProcessName(); + installCrashHandlers(); + installNativeModuleHook(); + if (runNativeAssetSmokeIfRequested()) return; + + // Start the background cleanup of stale native cache. Fire-and-forget; must not block startup or throw. + queueMicrotask(() => { + try { + cleanupStaleNativeCacheForCurrent(); + } catch { + // ignore: cache GC must never affect process startup + } + }); + + const version = getVersion(); + + const program = createProgram( + version, + (opts) => { + void handleMainCommand(opts, version).catch(async (error: unknown) => { + const operation = opts.prompt !== undefined ? 'run prompt' : 'start shell'; + await logStartupFailure(operation, error); + process.stderr.write( + formatStartupError(error, { + operation, + }), + ); + process.stderr.write(`See log: ${resolveGlobalLogPath(resolveKimiHome())}\n`); + process.exit(1); + }); + }, + () => { + void handleMigrateCommand(version).catch(async (error: unknown) => { + await logStartupFailure('run migration', error); + process.stderr.write(formatStartupError(error, { operation: 'run migration' })); + process.stderr.write(`See log: ${resolveGlobalLogPath(resolveKimiHome())}\n`); + process.exit(1); + }); + }, + ); + + program.parse(process.argv); +} + +main(); + +async function logStartupFailure(operation: string, error: unknown): Promise { + log.error('startup failed', { operation, error }); + try { + await flushDiagnosticLogs(); + } catch { + // Best-effort diagnostic flush only. + } +} diff --git a/apps/kimi-code/src/migration/badge.ts b/apps/kimi-code/src/migration/badge.ts new file mode 100644 index 000000000..47b3fabb6 --- /dev/null +++ b/apps/kimi-code/src/migration/badge.ts @@ -0,0 +1,27 @@ +/** + * Pure helpers for composing session labels in the session picker. + * + * Detection rule for the `[imported]` badge: `metadata.imported_from_kimi_cli` + * is strictly the boolean `true`. This mirrors the value written by + * `migration-legacy` into the session's `state.json` `custom` block. + */ + +const IMPORTED_BADGE = '[imported]'; +const IMPORTED_FLAG_KEY = 'imported_from_kimi_cli'; + +export interface SessionLabelInput { + readonly title: string; + readonly metadata?: Readonly> | undefined; +} + +export function isImportedSession( + metadata: Readonly> | undefined, +): boolean { + if (metadata === undefined) return false; + return metadata[IMPORTED_FLAG_KEY] === true; +} + +export function formatSessionLabel(input: SessionLabelInput): string { + const prefix = isImportedSession(input.metadata) ? `${IMPORTED_BADGE} ` : ''; + return `${prefix}${input.title}`; +} diff --git a/apps/kimi-code/src/migration/command.ts b/apps/kimi-code/src/migration/command.ts new file mode 100644 index 000000000..13d02f885 --- /dev/null +++ b/apps/kimi-code/src/migration/command.ts @@ -0,0 +1,19 @@ +/** + * `kimi migrate` sub-command. + * + * A bare, flagless subcommand: it launches the native pi-tui migration screen + * (the same one shown on first launch), then exits. The screen collects the + * migration scope interactively, so there are no CLI options. The actual + * launch is delegated to a host-provided handler. + */ + +import type { Command } from 'commander'; + +export function registerMigrateCommand(parent: Command, onMigrate: () => void): void { + parent + .command('migrate') + .description('Migrate data from a legacy kimi-cli installation into kimi-code.') + .action(() => { + onMigrate(); + }); +} diff --git a/apps/kimi-code/src/migration/detect-pending.ts b/apps/kimi-code/src/migration/detect-pending.ts new file mode 100644 index 000000000..60e2219dd --- /dev/null +++ b/apps/kimi-code/src/migration/detect-pending.ts @@ -0,0 +1,69 @@ +/** + * Pre-TUI detection: decide whether a first-launch migration screen should be + * shown. Cheap, synchronous-ish, no TTY required. Returns the MigrationPlan to + * drive the screen, or null when there is nothing to offer. + */ +import { existsSync, readFileSync } from 'node:fs'; +import { join, resolve } from 'node:path'; + +import { detectMigration, type MigrationPlan } from '@moonshot-ai/migration-legacy'; + +export interface DetectPendingInput { + readonly sourceHome: string; + readonly targetHome: string; + /** + * When true, skip the marker-based suppression (`.migrated-to-kimi-code` / + * `.skip-migration-from-kimi-cli`). The explicit `kimi migrate` command sets + * this so a deliberate invocation always runs regardless of prior runs. + */ + readonly ignoreMarker?: boolean; +} + +export async function detectPendingMigration( + input: DetectPendingInput, +): Promise { + const { sourceHome, targetHome } = input; + if (!existsSync(sourceHome)) return null; + if (input.ignoreMarker !== true) { + if (migrationAlreadyTargeted(join(sourceHome, '.migrated-to-kimi-code'), targetHome)) { + return null; + } + if (existsSync(join(targetHome, '.skip-migration-from-kimi-cli'))) return null; + } + + let plan: MigrationPlan; + try { + plan = await detectMigration({ sourcePath: sourceHome }); + } catch { + // Detection failure must never block startup; skip the screen. + return null; + } + + const nothingToMigrate = + plan.totalSessions === 0 && + !plan.hasConfig && + !plan.hasMcp && + !plan.hasUserHistory && + plan.oauthCredentials.length === 0; + if (nothingToMigrate) return null; + + return plan; +} + +/** + * True when the legacy `.migrated-to-kimi-code` marker records a migration + * into *this* target home. A marker written for a different `KIMI_CODE_HOME` + * must not suppress the prompt — that target has never received migrated data. + * An unreadable/old marker without `target_path` is treated as "matches" + * (conservative: do not re-prompt when the marker exists but is ambiguous). + */ +function migrationAlreadyTargeted(markerPath: string, targetHome: string): boolean { + if (!existsSync(markerPath)) return false; + try { + const parsed = JSON.parse(readFileSync(markerPath, 'utf-8')) as { target_path?: unknown }; + if (typeof parsed.target_path !== 'string') return true; + return resolve(parsed.target_path) === resolve(targetHome); + } catch { + return true; + } +} diff --git a/apps/kimi-code/src/migration/index.ts b/apps/kimi-code/src/migration/index.ts new file mode 100644 index 000000000..fa4a385d2 --- /dev/null +++ b/apps/kimi-code/src/migration/index.ts @@ -0,0 +1,12 @@ +/** + * kimi-cli → kimi-code migration: host integration surface. + * + * Removable glue: the `kimi migrate` sub-command, the first-launch detection, + * the native pi-tui migration screen, and the session-picker `[imported]` + * badge helper. Migration logic itself lives in + * `@moonshot-ai/migration-legacy`. + */ +export { registerMigrateCommand } from './command'; +export { formatSessionLabel, isImportedSession, type SessionLabelInput } from './badge'; +export { detectPendingMigration } from './detect-pending'; +export { MigrationScreenComponent, type MigrationScreenResult } from './migration-screen'; diff --git a/apps/kimi-code/src/migration/migration-screen.ts b/apps/kimi-code/src/migration/migration-screen.ts new file mode 100644 index 000000000..3f9814924 --- /dev/null +++ b/apps/kimi-code/src/migration/migration-screen.ts @@ -0,0 +1,532 @@ +/** + * MigrationScreenComponent — native pi-tui first-launch migration experience. + * + * A single mounted Container & Focusable that runs a 3-phase state machine: + * ask (2-step choice wizard) -> progress -> result + * + * Pure decision mapping (choices -> MigrationScope) is delegated to the + * package's `resolveMigrationScope`. Rendering follows the `ChoicePicker` + * conventions in `apps/kimi-code/src/tui/components/dialogs/choice-picker.ts`. + * + * This file implements the ask, progress, and result phases. `beginMigration` + * drives the real runMigration flow (injectable for tests). + */ +import { Container, matchesKey, Key, truncateToWidth, type Focusable } from '@earendil-works/pi-tui'; +import chalk from 'chalk'; + +import type { ColorPalette } from '#/tui/theme/colors'; +import { + resolveMigrationScope, + runMigration as realRunMigration, + type AnyChoice, + type MigrationPlan, + type MigrationPromptResult, + type MigrationReport, + type MigrationScope, + type Prompt1Choice, + type Prompt2Choice, + type RunMigrationInput, +} from '@moonshot-ai/migration-legacy'; + +type Phase = 'ask1' | 'ask2' | 'progress' | 'result'; + +const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'] as const; + +/** Spinner frame cadence — one full braille cycle every ~800ms. */ +const SPINNER_INTERVAL_MS = 80; + +const STEP_LABELS: ReadonlyArray = [ + ['config', 'Config'], + ['mcp', 'MCP'], + ['user-history', 'REPL history'], + ['sessions', 'Sessions'], +]; + +export interface MigrationScreenOptions { + readonly plan: MigrationPlan; + readonly sourceHome: string; + readonly targetHome: string; + readonly colors: ColorPalette; + /** Called once the screen is finished; the host then restores the editor. */ + readonly onComplete: (result: MigrationScreenResult) => void; + /** Triggers a re-render; the host wires this to `ui.requestRender()`. */ + readonly requestRender?: () => void; + /** Injectable for tests; defaults to the package's runMigration. */ + readonly runMigration?: (input: RunMigrationInput) => Promise; + /** + * When true, the screen starts at the scope question and skips the + * now/later/never gate — used by the explicit `kimi migrate` command, where + * invoking the command is itself the decision to migrate. + */ + readonly skipDecisionStep?: boolean; +} + +/** What the screen reports back to the host when finished. */ +export interface MigrationScreenResult { + readonly decision: 'now' | 'later' | 'never'; + /** Resolved migration scope; present only when decision === 'now'. */ + readonly scope?: MigrationScope; + // present only when decision === 'now' and migration ran + readonly migrated?: boolean; +} + +interface StepDef { + readonly title: string; + readonly options: ReadonlyArray<{ readonly label: string; readonly value: AnyChoice }>; +} + +export class MigrationScreenComponent extends Container implements Focusable { + focused = false; + private readonly opts: MigrationScreenOptions; + private phase: Phase = 'ask1'; + private selectedIndex = 0; + private readonly choices: AnyChoice[] = []; + private progressDone = 0; + private progressTotal = 0; + private readonly stepStatus = new Map([ + ['config', 'pending'], + ['mcp', 'pending'], + ['user-history', 'pending'], + ['sessions', 'pending'], + ]); + private spinnerFrame = 0; + private spinnerTimer: ReturnType | undefined; + private report: MigrationReport | undefined; + private migrationFailed = false; + + constructor(opts: MigrationScreenOptions) { + super(); + this.opts = opts; + if (opts.skipDecisionStep === true) { + // Explicit `kimi migrate`: the now/later/never gate is meaningless, so + // start at the scope question with the decision already fixed to 'now'. + this.phase = 'ask2'; + this.choices.push('now'); + } + } + + /** Host calls this once runMigration resolves. */ + showResult(report: MigrationReport): void { + this.report = report; + this.phase = 'result'; + this.stopSpinner(); + } + + /** Host calls this if runMigration threw. */ + showFailure(): void { + this.migrationFailed = true; + this.phase = 'result'; + this.stopSpinner(); + } + + /** Host calls this when migration starts. */ + enterProgress(): void { + this.phase = 'progress'; + } + + /** Host wires this to runMigration's onProgress (step-level messages). */ + reportStep(msg: string): void { + // msg is like 'config done', 'mcp done', 'sessions done' + const key = msg.replace(/ done$/, ''); + if (this.stepStatus.has(key)) this.stepStatus.set(key, 'done'); + } + + /** Host wires this to runMigration's onSessionProgress. */ + reportSessionProgress(done: number, total: number): void { + this.progressDone = done; + this.progressTotal = total; + } + + // The braille spinner advances on its own timer so the progress screen stays + // visibly alive even while a single step (e.g. session translation) runs for + // a while without emitting progress events. Runs only for the progress + // phase: started on entering it, stopped the moment it ends. + private startSpinner(): void { + this.stopSpinner(); + this.spinnerTimer = setInterval(() => { + this.spinnerFrame = (this.spinnerFrame + 1) % SPINNER_FRAMES.length; + this.opts.requestRender?.(); + }, SPINNER_INTERVAL_MS); + // A decorative timer must never keep the process alive on its own. + this.spinnerTimer.unref(); + } + + private stopSpinner(): void { + if (this.spinnerTimer !== undefined) { + clearInterval(this.spinnerTimer); + this.spinnerTimer = undefined; + } + } + + // test hooks (thin aliases so tests don't depend on host wiring) + _testEnterProgress(): void { + this.enterProgress(); + } + _testUpdateStep(msg: string): void { + this.reportStep(msg); + } + _testUpdateSessionProgress(done: number, total: number): void { + this.reportSessionProgress(done, total); + } + _testShowResult(report: MigrationReport): void { + this.showResult(report); + } + + handleInput(data: string): void { + if (this.phase === 'ask1' || this.phase === 'ask2') { + this.handleAskInput(data); + return; + } + if (this.phase === 'result') { + if (matchesKey(data, Key.enter)) { + this.opts.onComplete({ decision: 'now', migrated: !this.migrationFailed }); + } + return; + } + // progress phase: ignore input + } + + private currentStep(): StepDef { + return stepFor(this.phase, this.opts.plan); + } + + private handleAskInput(data: string): void { + const step = this.currentStep(); + if (matchesKey(data, Key.up)) { + this.selectedIndex = Math.max(0, this.selectedIndex - 1); + return; + } + if (matchesKey(data, Key.down)) { + this.selectedIndex = Math.min(step.options.length - 1, this.selectedIndex + 1); + return; + } + if (matchesKey(data, Key.escape)) { + // Esc anywhere in ask == "later" + this.opts.onComplete({ decision: 'later' }); + return; + } + if (matchesKey(data, Key.enter)) { + const chosen = step.options[this.selectedIndex]; + if (chosen === undefined) return; + this.advance(chosen.value); + return; + } + } + + /** Apply a chosen value and move the state machine forward. */ + private advance(value: AnyChoice): void { + this.choices.push(value); + this.selectedIndex = 0; + + const result: MigrationPromptResult = resolveMigrationScope(this.choices); + if (this.phase === 'ask1') { + if (value === 'now') { + this.phase = 'ask2'; + return; + } + // 'later' | 'never' + this.opts.onComplete({ decision: value as 'later' | 'never' }); + return; + } + // ask2 — either choice resolves the full scope; run migration immediately. + this.beginMigration(result); + } + + /** Enter the progress phase and run the migration to completion. */ + private beginMigration(result: MigrationPromptResult): void { + if (result.decision !== 'now' || result.scope === undefined) { + this.opts.onComplete({ decision: 'later' }); + return; + } + this.enterProgress(); + this.startSpinner(); + this.opts.requestRender?.(); + const run = this.opts.runMigration ?? realRunMigration; + void run({ + plan: this.opts.plan, + scope: result.scope, + source: this.opts.sourceHome, + target: this.opts.targetHome, + onProgress: (msg) => { + this.reportStep(msg); + this.opts.requestRender?.(); + }, + onSessionProgress: (done, total) => { + this.reportSessionProgress(done, total); + this.opts.requestRender?.(); + }, + }).then( + (report) => { + this.showResult(report); + this.opts.requestRender?.(); + }, + () => { + this.showFailure(); + this.opts.requestRender?.(); + }, + ); + } + + override render(width: number): string[] { + if (this.phase === 'ask1' || this.phase === 'ask2') { + return this.renderAsk(width); + } + if (this.phase === 'progress') return this.renderProgress(width); + return this.renderResult(width); + } + + private renderResult(width: number): string[] { + const { colors } = this.opts; + const lines: string[] = [chalk.hex(colors.primary)('─'.repeat(width))]; + if (this.migrationFailed) { + lines.push(chalk.hex(colors.error).bold(' Migration failed')); + lines.push(''); + lines.push(chalk.hex(colors.text)(' You can retry later by running "kimi migrate".')); + lines.push(''); + lines.push(chalk.hex(colors.textMuted)(' ⏎ continue to kimi-code')); + lines.push(chalk.hex(colors.primary)('─'.repeat(width))); + return lines.map((l) => truncateToWidth(l, width)); + } + const r = this.report; + lines.push(chalk.hex(colors.primary).bold(' Migration complete')); + lines.push(''); + if (r !== undefined) { + const sum = r.summary; + if (sum.sessions.sessionsMigrated > 0) { + lines.push( + chalk.hex(colors.success)(` ✓ ${sum.sessions.sessionsMigrated} sessions migrated`), + ); + } + // Only claim a data class was migrated when the summary says it was — + // a skipped/failed step (e.g. malformed config.toml) must not show ✓. + const migratedKinds: string[] = []; + if (sum.config.migrated) migratedKinds.push('config'); + if (sum.config.migratedHooks > 0) migratedKinds.push('hooks'); + if (sum.mcp.mergedServers.length > 0) migratedKinds.push('MCP'); + if (sum.userHistory.copied > 0) migratedKinds.push('REPL history'); + if (migratedKinds.length > 0) { + lines.push(chalk.hex(colors.success)(` ✓ ${migratedKinds.join(' · ')}`)); + } + if (sum.sessions.sessionsMigrated === 0 && migratedKinds.length === 0) { + lines.push(chalk.hex(colors.textMuted)(' Nothing needed migrating.')); + } + if (r.notices.detectedPlugins.length > 0) { + lines.push( + chalk.hex(colors.warning)( + ` ⚠ ${r.notices.detectedPlugins.length} kimi-cli plugins — not yet supported for migration`, + ), + ); + } + if (r.notices.oauthLoginsRequiringRelogin.length > 0) { + lines.push( + chalk.hex(colors.warning)( + ' ⚠ kimi-cli login not migrated — run /login in kimi-code to sign in', + ), + ); + } + if (sum.config.droppedHooks > 0) { + lines.push( + chalk.hex(colors.warning)( + ` ⚠ ${sum.config.droppedHooks} hooks dropped (incompatible)`, + ), + ); + } + // Conflicts and partial failures: the report records them, so surface + // them here too — otherwise "✓ config / MCP" hides that the data only + // landed in a *.migrated-from-kimi-cli.* sibling or that sessions failed. + if (sum.config.configConflicts.length > 0) { + lines.push( + chalk.hex(colors.warning)( + ` ⚠ ${sum.config.configConflicts.length} config conflicts kept yours: ${sum.config.configConflicts.join(' · ')}`, + ), + ); + } + if (sum.config.wroteSiblingDueToConflict) { + // Sibling mode: the live config.toml could not be parsed, so the + // migrated content went to `config.migrated-from-kimi-cli.toml` and + // the user must merge it by hand. Show the enumeration of contents + // on a SEPARATE line below — a single-line message with the contents + // appended would overflow 80 columns and be truncated, silently + // hiding the very info we want users to see. + lines.push( + chalk.hex(colors.warning)( + ' ⚠ config.toml could not be parsed — review config.migrated-from-kimi-cli.toml', + ), + ); + const sc = sum.config.siblingContents; + const items: string[] = []; + if (sc.providers.length > 0) { + items.push(`${sc.providers.length} provider${sc.providers.length === 1 ? '' : 's'}`); + } + if (sc.models.length > 0) { + items.push(`${sc.models.length} model${sc.models.length === 1 ? '' : 's'}`); + } + if (sc.hooks > 0) { + items.push(`${sc.hooks} hook${sc.hooks === 1 ? '' : 's'}`); + } + if (items.length > 0) { + lines.push(chalk.hex(colors.warning)(` contains: ${items.join(', ')}`)); + } + } + if (sum.config.wroteTuiSibling) { + lines.push( + chalk.hex(colors.warning)( + ' ⚠ tui.toml conflicted — review tui.migrated-from-kimi-cli.toml', + ), + ); + } + if (sum.mcp.wroteSiblingDueToConflict) { + lines.push( + chalk.hex(colors.warning)( + ' ⚠ mcp.json unreadable — review mcp.migrated-from-kimi-cli.json', + ), + ); + } + if (r.notices.mcpOauthServersRequiringReauth.length > 0) { + lines.push( + chalk.hex(colors.warning)( + ` ⚠ ${r.notices.mcpOauthServersRequiringReauth.length} MCP servers need re-authentication`, + ), + ); + } + if (sum.sessions.sessionsFailed.length > 0) { + lines.push( + chalk.hex(colors.warning)( + ` ⚠ ${sum.sessions.sessionsFailed.length} sessions failed to migrate`, + ), + ); + } + if (sum.sessions.sessionsConflicts.length > 0) { + lines.push( + chalk.hex(colors.warning)( + ` ⚠ ${sum.sessions.sessionsConflicts.length} sessions skipped (target already occupied)`, + ), + ); + } + // Empty / user-cleared sessions carry no conversation — neutral info, + // not a failure, so it is shown muted rather than as a ⚠ warning. + if (sum.sessions.sessionsSkippedEmpty > 0) { + lines.push( + chalk.hex(colors.textMuted)( + ` ${sum.sessions.sessionsSkippedEmpty} empty sessions skipped`, + ), + ); + } + lines.push(''); + lines.push( + chalk.hex(colors.textMuted)(' Old data kept at ~/.kimi/ — kimi-cli still works.'), + ); + } + lines.push(''); + lines.push(chalk.hex(colors.textMuted)(' ⏎ continue to kimi-code')); + lines.push(chalk.hex(colors.primary)('─'.repeat(width))); + return lines.map((l) => truncateToWidth(l, width)); + } + + private renderProgress(width: number): string[] { + const { colors } = this.opts; + const spinner = SPINNER_FRAMES[this.spinnerFrame] ?? SPINNER_FRAMES[0]; + const lines: string[] = [ + chalk.hex(colors.primary)('─'.repeat(width)), + chalk.hex(colors.primary).bold(' Migrating from kimi-cli'), + '', + ]; + if (this.progressTotal > 0) { + lines.push( + chalk.hex(colors.accent)(` ${spinner} `) + + chalk.hex(colors.text)( + `Translating sessions… ${this.progressDone} / ${this.progressTotal}`, + ), + ); + lines.push(''); + } + for (const [key, label] of STEP_LABELS) { + const status = this.stepStatus.get(key) ?? 'pending'; + const mark = + status === 'done' + ? chalk.hex(colors.success)('✓') + : chalk.hex(colors.textDim)('◐'); + lines.push(` ${mark} ${chalk.hex(colors.text)(label)}`); + } + lines.push(''); + lines.push(chalk.hex(colors.primary)('─'.repeat(width))); + return lines.map((l) => truncateToWidth(l, width)); + } + + private renderAsk(width: number): string[] { + const { colors } = this.opts; + const step = this.currentStep(); + const lines: string[] = [ + chalk.hex(colors.primary)('─'.repeat(width)), + chalk.hex(colors.primary).bold(' Migrate from kimi-cli'), + '', + ]; + if (this.phase === 'ask1') { + lines.push(chalk.hex(colors.text)(' Found an existing kimi-cli installation:')); + lines.push(chalk.hex(colors.textMuted)(` ${summarizePlan(this.opts.plan)}`)); + lines.push(''); + } + lines.push(chalk.hex(colors.text)(` ${step.title}`)); + lines.push(''); + for (let i = 0; i < step.options.length; i++) { + const opt = step.options[i]!; + const isSel = i === this.selectedIndex; + const pointer = isSel ? '❯' : ' '; + const labelStyle = isSel ? chalk.hex(colors.primary).bold : chalk.hex(colors.text); + lines.push( + chalk.hex(isSel ? colors.primary : colors.textDim)(` ${pointer} `) + + labelStyle(opt.label), + ); + } + lines.push(''); + lines.push( + chalk.hex(colors.textMuted)( + ` ↑/↓ move · ⏎ select · esc ${this.opts.skipDecisionStep === true ? 'cancel' : 'later'}`, + ), + ); + lines.push(chalk.hex(colors.primary)('─'.repeat(width))); + return lines.map((l) => truncateToWidth(l, width)); + } +} + +function summarizePlan(plan: MigrationPlan): string { + const parts: string[] = []; + if (plan.totalSessions > 0) parts.push(`${plan.totalSessions} sessions`); + if (plan.hasConfig) parts.push('config.toml'); + if (plan.hasMcp) parts.push('mcp.json'); + if (plan.hasUserHistory) parts.push('REPL history'); + // OAuth credentials are detected but not migrated (refresh tokens rotate; + // re-login in kimi-code is the right answer). Surface the detection up + // front so an install whose only data is `credentials/*.json` does not + // render this line blank, and so the pre-migration screen stays consistent + // with the result screen's "kimi-cli login not migrated — run /login" line. + if (plan.oauthCredentials.length > 0) parts.push('kimi-cli login (needs /login)'); + return parts.join(' · '); +} + +function stepFor(phase: Phase, plan: MigrationPlan): StepDef { + if (phase === 'ask1') { + return { + title: 'Migrate this data to kimi-code?', + options: [ + { label: 'Migrate now', value: 'now' satisfies Prompt1Choice }, + { label: 'Ask me later', value: 'later' satisfies Prompt1Choice }, + { label: 'Never ask again', value: 'never' satisfies Prompt1Choice }, + ], + }; + } + // ask2 — the second option carries the actual session count so users can see + // the cost they are signing up for. Falls back to the singular "sessions" + // word only (no count) when no sessions were detected. + const sessionsLabel = + plan.totalSessions > 0 + ? `Config + ${plan.totalSessions} sessions` + : 'Config + all sessions'; + return { + title: 'Migrate chat sessions too? (they are bulky and slower)', + options: [ + { label: 'Config only', value: 'config-only' satisfies Prompt2Choice }, + { label: sessionsLabel, value: 'all-sessions' satisfies Prompt2Choice }, + ], + }; +} diff --git a/apps/kimi-code/src/native/module-hook.ts b/apps/kimi-code/src/native/module-hook.ts new file mode 100644 index 000000000..bbef5d4cb --- /dev/null +++ b/apps/kimi-code/src/native/module-hook.ts @@ -0,0 +1,40 @@ +import { createRequire } from 'node:module'; + +import { loadNativePackage } from './native-require'; + +type ModuleLoad = (request: string, parent: unknown, isMain: boolean) => unknown; + +interface ModuleWithLoad { + _load?: ModuleLoad; +} + +const nodeRequire = createRequire(import.meta.url); +let installed = false; +let loadingNativePackage = false; + +export function installNativeModuleHook(): void { + if (installed) return; + installed = true; + + const moduleBuiltin = nodeRequire('node:module') as ModuleWithLoad; + const originalLoad = moduleBuiltin._load; + if (originalLoad === undefined) return; + + moduleBuiltin._load = function loadWithNativeAssets( + this: unknown, + request: string, + parent: unknown, + isMain: boolean, + ): unknown { + if (request === 'koffi' && !loadingNativePackage) { + loadingNativePackage = true; + try { + const pkg = loadNativePackage('koffi'); + if (pkg !== null) return pkg; + } finally { + loadingNativePackage = false; + } + } + return originalLoad.call(this, request, parent, isMain); + }; +} diff --git a/apps/kimi-code/src/native/native-assets.ts b/apps/kimi-code/src/native/native-assets.ts new file mode 100644 index 000000000..576c8079e --- /dev/null +++ b/apps/kimi-code/src/native/native-assets.ts @@ -0,0 +1,371 @@ +import { createHash } from 'node:crypto'; +import { + existsSync, + mkdirSync, + readFileSync, + readdirSync, + renameSync, + rmSync, + statSync, + writeFileSync, +} from 'node:fs'; +import { createRequire } from 'node:module'; +import { homedir } from 'node:os'; +import { dirname, join, win32 as pathWin32 } from 'node:path'; + +import { KIMI_BUILD_INFO } from '#/cli/build-info'; +import { NATIVE_ASSET_MANIFEST_VERSION as MANIFEST_VERSION, buildManifestKey } from '../../scripts/native/manifest.mjs'; + +export const NATIVE_ASSET_MANIFEST_VERSION = MANIFEST_VERSION; + +export interface NativeAssetFile { + readonly assetKey: string; + readonly relativePath: string; + readonly sha256: string; + readonly mode?: number; +} + +export interface NativeAssetPackage { + readonly name: string; + readonly root: string; + readonly files: readonly NativeAssetFile[]; +} + +export interface NativeAssetManifest { + readonly version: typeof NATIVE_ASSET_MANIFEST_VERSION; + readonly target: string; + readonly packages: readonly NativeAssetPackage[]; +} + +export interface NativeAssetSource { + getAssetKeys(): readonly string[]; + getRawAsset(assetKey: string): ArrayBuffer | ArrayBufferView | Buffer | string; +} + +export interface NativeAssetOptions { + readonly source?: NativeAssetSource | null; + readonly manifest?: NativeAssetManifest | null; + readonly cacheBase?: string; + readonly env?: NodeJS.ProcessEnv; + readonly platform?: NodeJS.Platform; + readonly homeDir?: string; + readonly version?: string; +} + +type RawNativeAssetManifest = Omit & { + readonly version: number; +}; + +interface NodeSeaModule { + isSea(): boolean; + getAssetKeys(): string[]; + getRawAsset(assetKey: string): ArrayBuffer; +} + +const nodeRequire = createRequire(import.meta.url); +let seaModule: NodeSeaModule | null | undefined; + +function loadSeaModule(): NodeSeaModule | null { + if (seaModule !== undefined) return seaModule; + try { + seaModule = nodeRequire('node:sea') as NodeSeaModule; + } catch { + seaModule = null; + } + return seaModule; +} + +function currentTarget(): string { + return KIMI_BUILD_INFO.buildTarget ?? `${process.platform}-${process.arch}`; +} + +export function nativeAssetManifestKey(target: string = currentTarget()): string { + return buildManifestKey(target); +} + +function toBuffer(value: ArrayBuffer | ArrayBufferView | Buffer | string): Buffer { + if (Buffer.isBuffer(value)) return value; + if (typeof value === 'string') return Buffer.from(value); + if (ArrayBuffer.isView(value)) { + return Buffer.from(value.buffer, value.byteOffset, value.byteLength); + } + return Buffer.from(value); +} + +function sha256(bytes: Buffer | Uint8Array | string): string { + return createHash('sha256').update(bytes).digest('hex'); +} + +function optionalEnvValue(env: NodeJS.ProcessEnv, key: string): string | null { + const value = env[key]; + return typeof value === 'string' && value.length > 0 ? value : null; +} + +function sanitizeSegment(value: string): string { + const sanitized = value.replaceAll(/[^a-zA-Z0-9._-]/g, '_'); + return sanitized.length > 0 ? sanitized : 'unknown'; +} + +export function getSeaAssetSource(): NativeAssetSource | null { + const sea = loadSeaModule(); + if (sea === null || !sea.isSea()) return null; + return { + getAssetKeys: () => sea.getAssetKeys(), + getRawAsset: (assetKey) => sea.getRawAsset(assetKey), + }; +} + +export function getEmbeddedNativeAssetManifest( + source = getSeaAssetSource(), + target = currentTarget(), +): NativeAssetManifest | null { + if (source === null) return null; + const key = nativeAssetManifestKey(target); + if (!source.getAssetKeys().includes(key)) return null; + const raw = source.getRawAsset(key); + const manifest = JSON.parse(toBuffer(raw).toString('utf-8')) as RawNativeAssetManifest; + if (manifest.version !== NATIVE_ASSET_MANIFEST_VERSION) { + throw new Error(`Unsupported native asset manifest version: ${manifest.version}`); + } + if (manifest.target !== target) { + throw new Error(`Native asset manifest target mismatch: ${manifest.target} !== ${target}`); + } + return manifest as NativeAssetManifest; +} + +export function getNativeCacheBase(options: NativeAssetOptions = {}): string { + if (options.cacheBase !== undefined) return options.cacheBase; + + const env = options.env ?? process.env; + const platform = options.platform ?? process.platform; + const home = options.homeDir ?? homedir(); + + const cacheDirEnv = optionalEnvValue(env, 'KIMI_CODE_CACHE_DIR'); + if (cacheDirEnv !== null) return cacheDirEnv; + + if (platform === 'darwin') return join(home, 'Library', 'Caches', 'kimi-code'); + if (platform === 'win32') { + const localAppData = optionalEnvValue(env, 'LOCALAPPDATA'); + return localAppData !== null + ? pathWin32.join(localAppData, 'kimi-code') + : pathWin32.join(home, 'AppData', 'Local', 'kimi-code', 'Cache'); + } + + return join(optionalEnvValue(env, 'XDG_CACHE_HOME') ?? join(home, '.cache'), 'kimi-code'); +} + +export function getNativeAssetCacheRoot( + manifest: NativeAssetManifest, + options: NativeAssetOptions = {}, +): string { + const version = sanitizeSegment(options.version ?? KIMI_BUILD_INFO.version ?? 'dev'); + const manifestHash = sha256(JSON.stringify(manifest)); + return join( + getNativeCacheBase(options), + 'native', + version, + sanitizeSegment(manifest.target), + manifestHash, + ); +} + +function readFileSha256(path: string): string | null { + try { + return sha256(readFileSync(path)); + } catch { + return null; + } +} + +function ensureFile(path: string, bytes: Buffer, expectedSha256: string, mode?: number): void { + if (readFileSha256(path) === expectedSha256) return; + + mkdirSync(dirname(path), { recursive: true }); + const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`; + writeFileSync(tempPath, bytes, { mode: mode ?? 0o644 }); + + try { + renameSync(tempPath, path); + return; + } catch { + if (readFileSha256(path) === expectedSha256) { + rmSync(tempPath, { force: true }); + return; + } + } + + try { + rmSync(path, { force: true }); + renameSync(tempPath, path); + } catch (error) { + rmSync(tempPath, { force: true }); + if (readFileSha256(path) === expectedSha256) return; + throw error; + } +} + +function ensureEntryFile(cacheRoot: string): void { + const entryPath = join(cacheRoot, 'node_modules', '.kimi-native-entry.cjs'); + ensureFile( + entryPath, + Buffer.from('module.exports = require;\n'), + sha256('module.exports = require;\n'), + 0o644, + ); +} + +export function ensureNativeAssetTree(options: NativeAssetOptions = {}): string | null { + const source = options.source ?? getSeaAssetSource(); + if (source === null) return null; + + const manifest = + options.manifest ?? getEmbeddedNativeAssetManifest(source, currentTarget()); + if (manifest === null) return null; + + const cacheRoot = getNativeAssetCacheRoot(manifest, options); + for (const pkg of manifest.packages) { + for (const file of pkg.files) { + const bytes = toBuffer(source.getRawAsset(file.assetKey)); + const actualSha256 = sha256(bytes); + if (actualSha256 !== file.sha256) { + throw new Error( + `Native asset checksum mismatch for ${file.assetKey}: ${actualSha256} !== ${file.sha256}`, + ); + } + ensureFile(join(cacheRoot, file.relativePath), bytes, file.sha256, file.mode); + } + } + ensureEntryFile(cacheRoot); + return cacheRoot; +} + +export function getNativePackageRoot( + packageName: string, + options: NativeAssetOptions = {}, +): string | null { + const source = options.source ?? getSeaAssetSource(); + if (source === null) return null; + + const manifest = + options.manifest ?? getEmbeddedNativeAssetManifest(source, currentTarget()); + if (manifest === null) return null; + + const pkg = manifest.packages.find((entry) => entry.name === packageName); + if (pkg === undefined) return null; + + const cacheRoot = ensureNativeAssetTree({ ...options, source, manifest }); + return cacheRoot === null ? null : join(cacheRoot, pkg.root); +} + +export function hasNativePackage(packageName: string, manifest: NativeAssetManifest): boolean { + return manifest.packages.some((pkg) => pkg.name === packageName); +} + +export function nativeAssetCacheExists( + packageName: string, + options: NativeAssetOptions = {}, +): boolean { + const root = getNativePackageRoot(packageName, options); + return root !== null && existsSync(root); +} + +export interface CleanupOptions { + readonly cacheBase: string; + readonly version: string; + readonly target: string; + readonly currentRoot: string; +} + +export interface CleanupResult { + readonly kept: string[]; + readonly removed: string[]; + readonly errors: Array<{ path: string; error: unknown }>; +} + +/** + * Remove stale native asset cache directories for the current (version, target). + * + * Keeps: + * - the currentRoot (passed in by caller) + * - the most recently modified sibling (defensive: in case currentRoot calc changed) + * + * Deletes all other sibling directories. Other versions and + * other targets are never touched. Errors per-entry are collected and returned + * (never throw — this is fire-and-forget background work). + */ +export function cleanupStaleNativeCache(options: CleanupOptions): CleanupResult { + const { cacheBase, version, target, currentRoot } = options; + const targetDir = join(cacheBase, 'native', version, target); + const result: CleanupResult = { kept: [], removed: [], errors: [] }; + + let entries: string[]; + try { + entries = readdirSync(targetDir); + } catch { + return result; + } + + const siblings: Array<{ path: string; mtimeMs: number }> = []; + for (const name of entries) { + const path = join(targetDir, name); + try { + const st = statSync(path); + if (!st.isDirectory()) continue; + siblings.push({ path, mtimeMs: st.mtimeMs }); + } catch (error) { + (result.errors as Array<{ path: string; error: unknown }>).push({ path, error }); + } + } + + if (siblings.length === 0) return result; + + // sort newest first + siblings.sort((a, b) => b.mtimeMs - a.mtimeMs); + // Defensive: keep the most recently modified sibling that is not currentRoot + // so a previously-written cache survives in case currentRoot calc changed. + const mostRecentOther = siblings.find((entry) => entry.path !== currentRoot)?.path; + const keepSet = new Set( + mostRecentOther === undefined ? [currentRoot] : [currentRoot, mostRecentOther], + ); + + for (const { path } of siblings) { + if (keepSet.has(path)) { + result.kept.push(path); + continue; + } + try { + rmSync(path, { recursive: true, force: true }); + result.removed.push(path); + } catch (error) { + (result.errors as Array<{ path: string; error: unknown }>).push({ path, error }); + } + } + + return result; +} + +/** + * Convenience: discover currentRoot from embedded manifest + run cleanup. + * Safe to call without args from main.ts startup. Returns null if not in SEA mode. + */ +export function cleanupStaleNativeCacheForCurrent( + options: NativeAssetOptions = {}, +): CleanupResult | null { + const source = options.source ?? getSeaAssetSource(); + if (source === null) return null; + + const manifest = + options.manifest ?? getEmbeddedNativeAssetManifest(source, currentTarget()); + if (manifest === null) return null; + + const cacheBase = getNativeCacheBase(options); + const version = KIMI_BUILD_INFO.version ?? 'dev'; + const currentRoot = getNativeAssetCacheRoot(manifest, options); + + return cleanupStaleNativeCache({ + cacheBase, + version, + target: manifest.target, + currentRoot, + }); +} diff --git a/apps/kimi-code/src/native/native-require.ts b/apps/kimi-code/src/native/native-require.ts new file mode 100644 index 000000000..6840d610b --- /dev/null +++ b/apps/kimi-code/src/native/native-require.ts @@ -0,0 +1,30 @@ +import { createRequire } from 'node:module'; +import { join } from 'node:path'; + +import { + ensureNativeAssetTree, + getNativePackageRoot, + type NativeAssetOptions, +} from './native-assets'; + +export function createNativePackageRequire( + packageName: string, + options: NativeAssetOptions = {}, +): ReturnType | null { + const packageRoot = getNativePackageRoot(packageName, options); + if (packageRoot === null) return null; + + const cacheRoot = ensureNativeAssetTree(options); + if (cacheRoot === null) return null; + + return createRequire(join(cacheRoot, 'node_modules', '.kimi-native-entry.cjs')); +} + +export function loadNativePackage( + packageName: string, + options: NativeAssetOptions = {}, +): T | null { + const nativeRequire = createNativePackageRequire(packageName, options); + if (nativeRequire === null) return null; + return nativeRequire(packageName) as T; +} diff --git a/apps/kimi-code/src/native/smoke.ts b/apps/kimi-code/src/native/smoke.ts new file mode 100644 index 000000000..56d39253f --- /dev/null +++ b/apps/kimi-code/src/native/smoke.ts @@ -0,0 +1,26 @@ +import { getEmbeddedNativeAssetManifest, getNativePackageRoot } from './native-assets'; + +const smokePackages = ['@mariozechner/clipboard', 'koffi']; + +export function runNativeAssetSmokeIfRequested(): boolean { + if (process.env['KIMI_CODE_NATIVE_ASSET_SMOKE'] !== '1') return false; + + try { + const manifest = getEmbeddedNativeAssetManifest(); + if (manifest === null) { + throw new Error('Native asset manifest is not available.'); + } + for (const packageName of smokePackages) { + const packageRoot = getNativePackageRoot(packageName, { manifest }); + if (packageRoot === null) { + throw new Error(`Native package is not available: ${packageName}`); + } + } + process.stdout.write(`Native asset smoke passed: ${manifest.target}\n`); + process.exit(0); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + process.stderr.write(`Native asset smoke failed: ${message}\n`); + process.exit(1); + } +} diff --git a/apps/kimi-code/src/tui/actions/replay-ops.ts b/apps/kimi-code/src/tui/actions/replay-ops.ts new file mode 100644 index 000000000..8b11a2c7a --- /dev/null +++ b/apps/kimi-code/src/tui/actions/replay-ops.ts @@ -0,0 +1,699 @@ +/** + * Session replay hydration. + * + * Core owns durable history as raw session records. The TUI projects those + * records into the same transcript entries/components used by live events, + * without mutating core session state or responding to replayed data. + */ + +import type { + AgentReplayRecord, + BackgroundTaskInfo, + ContentPart, + ContextMessage, + PromptOrigin, + PermissionMode, + ResumedAgentState, + Session, + ToolCall, +} from '@moonshot-ai/kimi-code-sdk'; + +import { AgentGroupComponent } from '#/tui/components/messages/agent-group'; +import type { TodoItem } from '#/tui/components/chrome/todo-panel'; +import { ToolCallComponent } from '#/tui/components/messages/tool-call'; +import type { TUIState } from '#/tui/kimi-tui'; +import type { + AppState, + BackgroundAgentMetadata, + BackgroundAgentStatusData, + ToolCallBlockData, + TranscriptEntry, +} from '#/tui/types'; +import { formatErrorMessage, isTodoItemShape } from '#/tui/utils/event-payload'; +import { formatBackgroundAgentTranscript } from '#/tui/utils/background-agent-status'; +import { mediaUrlPartToText } from '#/tui/utils/media-url'; +import { nextTranscriptId } from '#/tui/utils/transcript-id'; + +export interface ReplayHydrationHooks { + readonly setAppState: (patch: Partial) => void; + readonly appendEntry: (entry: TranscriptEntry) => void; + readonly setTodoList: (todos: readonly TodoItem[]) => void; + readonly emitError: (message: string) => void; +} + +interface ReplayProjection { + readonly entries: readonly TranscriptEntry[]; + /** + * Background subagents still not completed or failed when replay ends, + * keyed by agent_id. `hydrateTranscriptFromReplay` seeds + * `state.backgroundAgents` from this so the footer badge starts accurate. + */ + readonly backgroundAgents: ReadonlySet; + /** + * Background agent metadata that remains needed after replay so live + * terminal events can keep rendering transcript copy after resume. + */ + readonly backgroundAgentMetadata: ReadonlyMap; +} + +interface OpenAssistant { + thinking: string[]; + text: string[]; +} + +interface ProjectionState { + entries: TranscriptEntry[]; + toolCalls: Map; + assistant: OpenAssistant; + skillActivationIds: Set; + permissionMode?: PermissionMode; + backgroundAgents: Set; + backgroundAgentMetadata: Map; + backgroundTasks: ReadonlyMap; +} + +type BackgroundTaskOrigin = Extract; + +const REPLAY_TURN_LIMIT = 10; + +export async function hydrateTranscriptFromReplay( + state: TUIState, + hooks: ReplayHydrationHooks, + session: Session, +): Promise { + hooks.setAppState({ isReplaying: true }); + try { + const main = session.getResumeState()?.agents['main']; + if (main === undefined) { + hooks.emitError('Session history is unavailable for this session.'); + return false; + } + + const projection = projectReplayRecords(main.replay, main.background); + hydrateProjectedEntries(state, projection.entries, hooks.appendEntry); + hydrateTodoPanelFromResume(main, hooks); + state.backgroundAgents = new Set(projection.backgroundAgents); + state.backgroundAgentMetadata = new Map(projection.backgroundAgentMetadata); + + // Seed the BPM-derived store from the resume snapshot. This is the + // authoritative source for footer count + transcript dedupe; the + // subagent-derived `backgroundAgents` set above is kept for legacy + // metadata lookups (agent name / description) until removed. + state.backgroundTasks = new Map( + main.background.map((info) => [info.taskId, info]), + ); + state.backgroundTaskTranscriptedTerminal.clear(); + // Resumed terminal tasks should not re-emit transcript cards. + for (const info of main.background) { + if ( + info.status === 'completed' || + info.status === 'failed' || + info.status === 'killed' || + info.status === 'lost' + ) { + state.backgroundTaskTranscriptedTerminal.add(info.taskId); + } + } + const counts = countActiveBackgroundTasks(state.backgroundTasks); + state.footer.setBackgroundCounts(counts); + hooks.setAppState(appStateFromResumeAgent(main)); + return true; + } catch (error) { + const message = formatErrorMessage(error); + hooks.emitError(`Failed to replay session history: ${message}`); + return false; + } finally { + hooks.setAppState({ isReplaying: false }); + } +} + +function hydrateTodoPanelFromResume( + agent: ResumedAgentState, + hooks: ReplayHydrationHooks, +): void { + const rawTodos = agent.toolStore?.['todo']; + if (!Array.isArray(rawTodos)) { + hooks.setTodoList([]); + return; + } + const todos = rawTodos + .filter((todo): todo is TodoItem => isTodoItemShape(todo)) + .map((todo) => ({ title: todo.title, status: todo.status })); + hooks.setTodoList(todos); +} + +function countActiveBackgroundTasks(tasks: ReadonlyMap): { + bashTasks: number; + agentTasks: number; +} { + let bashTasks = 0; + let agentTasks = 0; + for (const info of tasks.values()) { + if ( + info.status === 'completed' || + info.status === 'failed' || + info.status === 'killed' || + info.status === 'lost' + ) { + continue; + } + if (info.taskId.startsWith('agent-')) { + agentTasks += 1; + } else { + bashTasks += 1; + } + } + return { bashTasks, agentTasks }; +} + +function appStateFromResumeAgent(agent: ResumedAgentState): Partial { + const maxContextTokens = agent.config.modelCapabilities?.max_context_tokens ?? 0; + const contextTokens = agent.context.tokenCount; + const contextUsage = maxContextTokens > 0 ? contextTokens / maxContextTokens : 0; + return { + // `?? ''` so a resumed session with no resolvable model yields a string, + // not `undefined` — the editor's `appState.model.trim()` would otherwise + // throw. An empty model surfaces the normal "LLM not set" state. + model: agent.config.modelAlias ?? agent.config.provider?.model ?? '', + contextTokens, + maxContextTokens, + contextUsage, + planMode: agent.plan !== null, + yolo: agent.permission.mode === 'yolo', + permissionMode: agent.permission.mode, + }; +} + +export function projectReplayRecords( + records: readonly AgentReplayRecord[], + backgroundTasks: readonly BackgroundTaskInfo[] = [], +): ReplayProjection { + const state: ProjectionState = { + entries: [], + toolCalls: new Map(), + assistant: { thinking: [], text: [] }, + skillActivationIds: new Set(), + backgroundAgents: new Set(), + backgroundAgentMetadata: new Map(), + backgroundTasks: new Map(backgroundTasks.map((info) => [info.taskId, info])), + }; + + for (const record of limitReplayRecordsByTurn(records, REPLAY_TURN_LIMIT)) { + projectReplayRecord(state, record); + } + flushAssistant(state); + + return { + entries: state.entries, + backgroundAgents: state.backgroundAgents, + backgroundAgentMetadata: state.backgroundAgentMetadata, + }; +} + +function limitReplayRecordsByTurn( + records: readonly AgentReplayRecord[], + maxTurns: number, +): readonly AgentReplayRecord[] { + if (maxTurns <= 0) return []; + + const turnStarts = records.flatMap((record, index) => + isReplayUserTurnRecord(record) ? [index] : [], + ); + if (turnStarts.length <= maxTurns) return records; + + return records.slice(turnStarts[turnStarts.length - maxTurns]); +} + +function isReplayUserTurnRecord(record: AgentReplayRecord): boolean { + if (record.type !== 'message') return false; + const { message } = record; + if (message.role !== 'user') return false; + switch (message.origin?.kind) { + case undefined: + case 'user': + return true; + case 'skill_activation': + return message.origin.trigger === 'user-slash'; + case 'background_task': + case 'compaction_summary': + case 'hook_result': + case 'injection': + case 'system_trigger': + return false; + } +} + +function projectReplayRecord(state: ProjectionState, record: AgentReplayRecord): void { + switch (record.type) { + case 'message': + projectContextMessage(state, record.message); + return; + case 'plan_updated': + flushAssistant(state); + state.entries.push(entry('status', `Plan mode: ${record.enabled ? 'ON' : 'OFF'}`, 'notice')); + return; + case 'permission_updated': + flushAssistant(state); + projectPermissionUpdate(state, record.mode); + return; + case 'approval_result': { + flushAssistant(state); + const { record: approvalRecord } = record; + const { result } = approvalRecord; + const parts: string[] = []; + switch (result.decision) { + case 'approved': + parts.push(result.scope === 'session' ? 'Approved for session' : 'Approved'); + break; + case 'rejected': + parts.push('Rejected'); + break; + case 'cancelled': + parts.push('Cancelled'); + break; + } + parts.push(`: ${approvalRecord.action}`); + if (result.feedback !== undefined && result.feedback.length > 0) { + parts.push(` — "${result.feedback}"`); + } + state.entries.push(entry('status', parts.join(''), 'notice')); + return; + } + case 'config_updated': + return; + } +} + +function projectPermissionUpdate(state: ProjectionState, mode: PermissionMode): void { + if (mode === 'yolo') { + state.entries.push( + entry('status', 'YOLO mode: ON', 'notice', { + detail: 'All actions will be approved automatically. Use with caution.', + }), + ); + state.permissionMode = mode; + return; + } + if (state.permissionMode === 'yolo' && mode === 'manual') { + state.entries.push(entry('status', 'YOLO mode: OFF', 'notice')); + state.permissionMode = mode; + return; + } + state.entries.push(entry('status', `Permission mode: ${mode}`, 'notice')); + state.permissionMode = mode; +} + +interface SkillActivationProjection { + readonly activationId: string; + readonly skillName: string; + readonly skillArgs?: string; +} + +function projectSkillActivation( + state: ProjectionState, + skill: SkillActivationProjection | undefined, +): void { + if (skill === undefined) return; + if (state.skillActivationIds.has(skill.activationId)) return; + state.skillActivationIds.add(skill.activationId); + state.entries.push( + entry('skill_activation', `Activated skill: ${skill.skillName}`, 'plain', { + skillActivationId: skill.activationId, + skillName: skill.skillName, + skillArgs: skill.skillArgs, + }), + ); +} + +function projectContextMessage(state: ProjectionState, message: ContextMessage): void { + switch (message.role) { + case 'user': { + const origin = backgroundOrigin(message); + if (origin !== undefined) { + flushAssistant(state); + projectBackgroundTaskNotification(state, origin); + return; + } + if (message.origin?.kind === 'hook_result') { + projectHookResultMessage(state, message); + return; + } + if (message.origin?.kind === 'injection') { + return; + } + flushAssistant(state); + const skill = skillActivationFromOrigin(message.origin); + if (skill !== undefined) { + projectSkillActivation(state, skill); + return; + } + state.entries.push(entry('user', contentPartsToText(message.content), 'plain')); + return; + } + case 'assistant': + if (message.origin?.kind === 'hook_result') { + projectHookResultMessage(state, message); + projectMessageToolCalls(state, message.toolCalls); + return; + } + collectMessageContent(state.assistant, message.content); + flushAssistant(state); + projectMessageToolCalls(state, message.toolCalls); + return; + case 'tool': + flushAssistant(state); + projectMessageToolResult(state, message); + return; + case 'system': + return; + default: + return; + } +} + +function projectMessageToolCalls(state: ProjectionState, toolCalls: readonly ToolCall[]): void { + for (const rawToolCall of toolCalls) { + const toolCall = toolCallFromMessage(rawToolCall); + if (toolCall === undefined) continue; + state.toolCalls.set(toolCall.id, toolCall); + state.entries.push( + entry('tool_call', '', 'plain', { + toolCallData: toolCall, + }), + ); + } +} + +function projectMessageToolResult(state: ProjectionState, message: ContextMessage): void { + const toolCallId = message.toolCallId; + if (toolCallId === undefined) return; + const call = state.toolCalls.get(toolCallId); + if (call === undefined) return; + call.result = { + tool_call_id: toolCallId, + output: toolResultOutput(message.content), + is_error: message.isError, + }; +} + +function projectBackgroundTaskNotification( + state: ProjectionState, + origin: BackgroundTaskOrigin, +): void { + const task = state.backgroundTasks.get(origin.taskId); + const meta: BackgroundAgentMetadata = { + agentId: origin.taskId, + parentToolCallId: origin.taskId, + description: task?.description, + }; + let status = formatBackgroundAgentTranscript( + origin.status === 'completed' ? 'completed' : 'failed', + meta, + ); + if (origin.status === 'lost') { + status = { + ...status, + headline: status.headline.replace(' failed in background', ' lost in background'), + }; + } else if (origin.status === 'killed') { + status = { + ...status, + headline: status.headline.replace(' failed in background', ' stopped'), + }; + } + state.entries.push( + entry('status', status.headline, 'plain', { + detail: status.detail, + backgroundAgentStatus: status, + }), + ); + state.backgroundAgents.delete(meta.agentId); + state.backgroundAgentMetadata.delete(meta.agentId); +} + +function toolResultOutput(content: readonly ContentPart[]): string { + if (content.some((part) => part.type !== 'text')) { + return JSON.stringify(content); + } + return contentPartsToText(content); +} + +function flushAssistant(state: ProjectionState): void { + const thinking = state.assistant.thinking.join(''); + const text = state.assistant.text.join(''); + state.assistant = { thinking: [], text: [] }; + if (thinking.length > 0) { + state.entries.push(entry('thinking', thinking, 'plain')); + } + if (text.length > 0) { + state.entries.push(entry('assistant', text, 'markdown')); + } +} + +function projectHookResultMessage(state: ProjectionState, message: ContextMessage): void { + if (message.origin?.kind !== 'hook_result') return; + flushAssistant(state); + state.entries.push( + entry( + 'assistant', + formatHookResultMessageForTranscript( + contentPartsToText(message.content), + message.origin.event, + message.origin.blocked === true, + ), + 'markdown', + ), + ); +} + +const HOOK_RESULT_RE = + /\n?([\s\S]*?)\n?<\/hook_result>/g; + +function formatHookResultMessageForTranscript( + text: string, + fallbackEvent: string, + blocked: boolean, +): string { + const results: Array<{ event: string; body: string }> = []; + let lastIndex = 0; + + for (const match of text.matchAll(HOOK_RESULT_RE)) { + if (text.slice(lastIndex, match.index).trim().length > 0) { + return formatHookResultBlock(fallbackEvent, text, blocked); + } + const event = match[1]; + const body = match[2]; + if (event === undefined || body === undefined) { + return formatHookResultBlock(fallbackEvent, text, blocked); + } + results.push({ event, body }); + lastIndex = match.index + match[0].length; + } + + if (results.length === 0 || text.slice(lastIndex).trim().length > 0) { + return formatHookResultBlock(fallbackEvent, text, blocked); + } + + return results.map(({ event, body }) => formatHookResultBlock(event, body, blocked)).join('\n\n'); +} + +function formatHookResultBlock(event: string, body: string, blocked: boolean): string { + return `*${event} hook${blocked ? ' blocked' : ''}*\n\n${body.trim() || '(empty)'}`; +} + +function collectMessageContent(target: OpenAssistant, content: readonly ContentPart[]): void { + for (const part of content) { + switch (part.type) { + case 'think': + target.thinking.push(part.think); + break; + case 'text': + target.text.push(part.text); + break; + case 'audio_url': + case 'image_url': + case 'video_url': + break; + } + } +} + +function toolCallFromMessage(rawToolCall: ToolCall): ToolCallBlockData | undefined { + const id = rawToolCall.id; + const name = rawToolCall.function.name; + if (id.length === 0 || name.length === 0) return undefined; + return { + id, + name, + args: parseToolArguments(rawToolCall.function.arguments), + }; +} + +function parseToolArguments(value: string | null): Record { + if (value === null || value.length === 0) return {}; + try { + const parsed = JSON.parse(value); + return isObject(parsed) ? parsed : {}; + } catch { + return {}; + } +} + +function entry( + kind: TranscriptEntry['kind'], + content: string, + renderMode: TranscriptEntry['renderMode'], + extras?: { + turnId?: string; + toolCallData?: ToolCallBlockData; + detail?: string; + color?: string; + backgroundAgentStatus?: BackgroundAgentStatusData; + skillActivationId?: string; + skillName?: string; + skillArgs?: string; + }, +): TranscriptEntry { + return { + id: nextTranscriptId(), + kind, + renderMode, + content, + turnId: extras?.turnId, + detail: extras?.detail, + color: extras?.color, + toolCallData: extras?.toolCallData, + backgroundAgentStatus: extras?.backgroundAgentStatus, + skillActivationId: extras?.skillActivationId, + skillName: extras?.skillName, + skillArgs: extras?.skillArgs, + }; +} + +function contentPartsToText(content: readonly ContentPart[]): string { + return content.map(userPartToText).join(''); +} + +function backgroundOrigin(message: ContextMessage): BackgroundTaskOrigin | undefined { + return message.origin?.kind === 'background_task' ? message.origin : undefined; +} + +function userPartToText(part: ContentPart): string { + switch (part.type) { + case 'text': + return part.text; + case 'think': + return part.think; + case 'image_url': + return mediaUrlPartToText('image', part.imageUrl.url); + case 'video_url': + return mediaUrlPartToText('video', part.videoUrl.url); + case 'audio_url': + return mediaUrlPartToText('audio', part.audioUrl.url); + } +} + +function skillActivationFromOrigin( + origin: PromptOrigin | undefined, +): SkillActivationProjection | undefined { + if (origin?.kind !== 'skill_activation') return undefined; + return { + activationId: origin.activationId, + skillName: origin.skillName, + skillArgs: origin.skillArgs, + }; +} + +function isObject(value: unknown): value is Record { + return typeof value === 'object' && value !== null; +} + +/** + * Inject projected flat entries into live state. Adjacent Agent tool_call + * entries sharing `(turnId, step)` are grouped into an AgentGroupComponent so + * replay matches live behavior. Other entries use the original append path. + * + * Unlike `tryAttachAgentToolCall`, this does not write + * `state.pendingAgentGroup`; after replay, live events must take over from a + * clean pending group state. + */ +export function hydrateProjectedEntries( + state: TUIState, + entries: readonly TranscriptEntry[], + appendEntry: (entry: TranscriptEntry) => void, +): void { + let i = 0; + while (i < entries.length) { + const cur = entries[i]; + if (cur === undefined) { + i += 1; + continue; + } + if (cur.kind === 'skill_activation' && cur.skillActivationId !== undefined) { + if (state.renderedSkillActivationIds.has(cur.skillActivationId)) { + i += 1; + continue; + } + state.renderedSkillActivationIds.add(cur.skillActivationId); + } + const tc = cur.toolCallData; + if ( + cur.kind === 'tool_call' && + tc !== undefined && + tc.name === 'Agent' && + tc.step !== undefined + ) { + // Collect all adjacent Agent calls with the same step and turn id. + const batch: TranscriptEntry[] = [cur]; + let j = i + 1; + while (j < entries.length) { + const next = entries[j]; + if (next === undefined) break; + const nextTc = next.toolCallData; + if ( + next.kind === 'tool_call' && + nextTc !== undefined && + nextTc.name === 'Agent' && + nextTc.step === tc.step && + nextTc.turnId === tc.turnId + ) { + batch.push(next); + j++; + continue; + } + break; + } + if (batch.length >= 2) { + attachAgentBatchAsGroup(state, batch); + i = j; + continue; + } + // A single Agent stays on the standalone card path. + } + appendEntry(cur); + i++; + } +} + +function attachAgentBatchAsGroup(state: TUIState, batch: readonly TranscriptEntry[]): void { + const group = new AgentGroupComponent(state.theme.colors, state.ui); + state.transcriptContainer.addChild(group); + for (const item of batch) { + const tc = item.toolCallData; + if (tc === undefined) continue; + state.transcriptEntries.push(item); + const component = new ToolCallComponent( + tc, + tc.result, + state.theme.colors, + state.ui, + state.theme.markdownTheme, + state.appState.workDir, + ); + if (state.toolOutputExpanded) component.setExpanded(true); + if (state.planExpanded) component.setPlanExpanded(true); + state.pendingToolComponents.set(tc.id, component); + group.attach(tc.id, component); + } + state.ui.requestRender(); +} diff --git a/apps/kimi-code/src/tui/commands/index.ts b/apps/kimi-code/src/tui/commands/index.ts new file mode 100644 index 000000000..fab43f75c --- /dev/null +++ b/apps/kimi-code/src/tui/commands/index.ts @@ -0,0 +1,5 @@ +export * from './parse'; +export * from './registry'; +export * from './resolve'; +export * from './skills'; +export * from './types'; diff --git a/apps/kimi-code/src/tui/commands/parse.ts b/apps/kimi-code/src/tui/commands/parse.ts new file mode 100644 index 000000000..ee19d3585 --- /dev/null +++ b/apps/kimi-code/src/tui/commands/parse.ts @@ -0,0 +1,12 @@ +import type { ParsedSlashInput } from './types'; + +export function parseSlashInput(input: string): ParsedSlashInput | null { + if (!input.startsWith('/')) return null; + const trimmed = input.slice(1).trim(); + if (trimmed.length === 0) return null; + const spaceIdx = trimmed.indexOf(' '); + const name = spaceIdx === -1 ? trimmed : trimmed.slice(0, spaceIdx); + const args = spaceIdx === -1 ? '' : trimmed.slice(spaceIdx + 1).trim(); + if (name.includes('/')) return null; + return { name, args }; +} diff --git a/apps/kimi-code/src/tui/commands/registry.ts b/apps/kimi-code/src/tui/commands/registry.ts new file mode 100644 index 000000000..5195bf5e7 --- /dev/null +++ b/apps/kimi-code/src/tui/commands/registry.ts @@ -0,0 +1,181 @@ +import type { KimiSlashCommand, SlashCommandAvailability } from './types'; + +export const BUILTIN_SLASH_COMMANDS = [ + { + name: 'yolo', + aliases: ['yes'], + description: 'Toggle auto-approve mode', + priority: 100, + availability: 'always', + }, + { + name: 'permission', + aliases: [], + description: 'Select permission mode', + priority: 100, + availability: 'always', + }, + { + name: 'settings', + aliases: ['config'], + description: 'Open TUI settings', + priority: 100, + availability: 'always', + }, + { + name: 'plan', + aliases: [], + description: 'Toggle plan mode', + priority: 100, + availability: (args) => (args.trim().toLowerCase() === 'clear' ? 'idle-only' : 'always'), + }, + { + name: 'model', + aliases: [], + description: 'Switch LLM model', + priority: 100, + availability: 'always', + }, + { + name: 'help', + aliases: ['h', '?'], + description: 'Show available commands and shortcuts', + priority: 80, + availability: 'always', + }, + { + name: 'new', + aliases: ['clear'], + description: 'Start a fresh session in the current workspace', + priority: 80, + }, + { + name: 'sessions', + aliases: ['resume'], + description: 'Browse and resume sessions', + priority: 80, + availability: 'always', + }, + { + name: 'tasks', + aliases: ['task'], + description: 'Browse background tasks', + priority: 80, + availability: 'always', + }, + { + name: 'mcp', + aliases: [], + description: 'Show MCP server status', + priority: 60, + availability: 'always', + }, + { + name: 'compact', + aliases: [], + description: 'Compact the conversation context', + priority: 80, + }, + { + name: 'init', + aliases: [], + description: 'Analyze the codebase and generate AGENTS.md', + }, + { + name: 'fork', + aliases: [], + description: 'Fork the current session', + priority: 80, + }, + { + name: 'title', + aliases: ['rename'], + description: 'Set or show session title', + priority: 60, + availability: 'always', + }, + { + name: 'usage', + aliases: [], + description: 'Show session tokens + context window + plan quotas', + priority: 60, + availability: 'always', + }, + { + name: 'status', + aliases: [], + description: 'Show current session and runtime status', + priority: 60, + availability: 'always', + }, + { + name: 'feedback', + aliases: [], + description: 'Send feedback to make Kimi Code better', + priority: 60, + availability: 'always', + }, + { + name: 'editor', + aliases: [], + description: 'Set the external editor for Ctrl-G', + priority: 60, + availability: 'always', + }, + { + name: 'theme', + aliases: [], + description: 'Set the terminal UI theme', + priority: 60, + availability: 'always', + }, + { + name: 'logout', + aliases: [], + description: 'Clear credentials for the current platform', + priority: 40, + }, + { + name: 'login', + aliases: [], + description: 'Select a platform and authenticate', + priority: 40, + }, + { + name: 'exit', + aliases: ['quit', 'q'], + description: 'Exit the application', + priority: 20, + }, + { + name: 'version', + aliases: [], + description: 'Show version information', + priority: 20, + availability: 'always', + }, +] as const satisfies readonly KimiSlashCommand[]; + +export type BuiltinSlashCommand = (typeof BUILTIN_SLASH_COMMANDS)[number]; +export type BuiltinSlashCommandName = BuiltinSlashCommand['name']; + +export function findBuiltInSlashCommand(commandName: string): BuiltinSlashCommand | undefined { + const commands = BUILTIN_SLASH_COMMANDS as readonly KimiSlashCommand[]; + return commands.find( + (command) => command.name === commandName || command.aliases.includes(commandName), + ) as BuiltinSlashCommand | undefined; +} + +export function resolveSlashCommandAvailability( + command: KimiSlashCommand, + args: string, +): SlashCommandAvailability { + const availability = command.availability ?? 'idle-only'; + return typeof availability === 'function' ? availability(args) : availability; +} + +export function sortSlashCommands(commands: readonly KimiSlashCommand[]): KimiSlashCommand[] { + return [...commands].toSorted( + (a, b) => (b.priority ?? 0) - (a.priority ?? 0) || a.name.localeCompare(b.name), + ); +} diff --git a/apps/kimi-code/src/tui/commands/resolve.ts b/apps/kimi-code/src/tui/commands/resolve.ts new file mode 100644 index 000000000..2d0807bf4 --- /dev/null +++ b/apps/kimi-code/src/tui/commands/resolve.ts @@ -0,0 +1,115 @@ +import { + findBuiltInSlashCommand, + resolveSlashCommandAvailability, + type BuiltinSlashCommand, + type BuiltinSlashCommandName, +} from './registry'; +import { parseSlashInput } from './parse'; +import type { SlashCommandBusyReason, SlashCommandInvalidReason } from './types'; + +export type SlashCommandIntent = + | { readonly kind: 'not-command' } + | { + readonly kind: 'builtin'; + readonly command: BuiltinSlashCommand; + readonly name: BuiltinSlashCommandName; + readonly args: string; + } + | { + readonly kind: 'skill'; + readonly commandName: string; + readonly skillName: string; + readonly args: string; + } + | { readonly kind: 'message'; readonly input: string } + | { + readonly kind: 'blocked'; + readonly commandName: string; + readonly reason: SlashCommandBusyReason; + } + | { + readonly kind: 'invalid'; + readonly commandName: string; + readonly reason: SlashCommandInvalidReason; + }; + +export interface ResolveSlashCommandInput { + readonly input: string; + readonly skillCommandMap: ReadonlyMap; + readonly isStreaming: boolean; + readonly isCompacting: boolean; +} + +export function resolveSlashCommandInput(options: ResolveSlashCommandInput): SlashCommandIntent { + const parsed = parseSlashInput(options.input); + if (parsed === null) return { kind: 'not-command' }; + + const command = findBuiltInSlashCommand(parsed.name); + if (command !== undefined) { + const busyReason = slashCommandBusyReason(options); + if ( + busyReason !== undefined && + resolveSlashCommandAvailability(command, parsed.args) === 'idle-only' + ) { + return { + kind: 'blocked', + commandName: parsed.name, + reason: busyReason, + }; + } + return { + kind: 'builtin', + command, + name: command.name, + args: parsed.args, + }; + } + + const skillName = resolveSkillCommand(options.skillCommandMap, parsed.name); + if (skillName !== undefined) { + const busyReason = slashCommandBusyReason(options); + if (busyReason !== undefined) { + return { + kind: 'blocked', + commandName: parsed.name, + reason: busyReason, + }; + } + return { + kind: 'skill', + commandName: parsed.name, + skillName, + args: parsed.args.trim(), + }; + } + + return { + kind: 'message', + input: options.input, + }; +} + +export function resolveSkillCommand( + skillCommandMap: ReadonlyMap, + commandName: string, +): string | undefined { + return skillCommandMap.get(commandName) ?? skillCommandMap.get(`skill:${commandName}`); +} + +export function slashCommandBusyReason( + options: Pick, +): SlashCommandBusyReason | undefined { + if (options.isStreaming) return 'streaming'; + if (options.isCompacting) return 'compacting'; + return undefined; +} + +export function slashBusyMessage( + commandName: string, + reason: SlashCommandBusyReason, +): string { + if (reason === 'streaming') { + return `Cannot /${commandName} while streaming — press Esc or Ctrl-C first.`; + } + return `Cannot /${commandName} while compacting — wait for compaction to finish first.`; +} diff --git a/apps/kimi-code/src/tui/commands/skills.ts b/apps/kimi-code/src/tui/commands/skills.ts new file mode 100644 index 000000000..08dfafc7e --- /dev/null +++ b/apps/kimi-code/src/tui/commands/skills.ts @@ -0,0 +1,33 @@ +import type { Session, SkillSummary } from '@moonshot-ai/kimi-code-sdk'; + +import type { KimiSlashCommand } from './types'; + +export type SkillListSession = Pick; + +export interface SkillSlashCommands { + readonly commands: readonly KimiSlashCommand[]; + readonly commandMap: ReadonlyMap; +} + +export function isUserActivatableSkill(skill: SkillSummary): boolean { + return ( + skill.type === undefined || + skill.type === 'prompt' || + skill.type === 'inline' || + skill.type === 'flow' + ); +} + +export function buildSkillSlashCommands(skills: readonly SkillSummary[]): SkillSlashCommands { + const commandMap = new Map(); + const commands = skills.filter(isUserActivatableSkill).map((skill) => { + const commandName = `skill:${skill.name}`; + commandMap.set(commandName, skill.name); + return { + name: commandName, + aliases: [], + description: skill.description ?? '', + }; + }); + return { commands, commandMap }; +} diff --git a/apps/kimi-code/src/tui/commands/types.ts b/apps/kimi-code/src/tui/commands/types.ts new file mode 100644 index 000000000..cb784f84d --- /dev/null +++ b/apps/kimi-code/src/tui/commands/types.ts @@ -0,0 +1,20 @@ +import type { SlashCommand } from '@earendil-works/pi-tui'; + +export type SlashCommandAvailability = 'always' | 'idle-only'; + +export interface KimiSlashCommand extends SlashCommand { + readonly name: Name; + readonly aliases: readonly string[]; + readonly description: string; + readonly priority?: number; + readonly availability?: SlashCommandAvailability | ((args: string) => SlashCommandAvailability); +} + +export interface ParsedSlashInput { + readonly name: string; + readonly args: string; +} + +export type SlashCommandBusyReason = 'streaming' | 'compacting'; + +export type SlashCommandInvalidReason = 'unknown'; diff --git a/apps/kimi-code/src/tui/components/chrome/device-code-box.ts b/apps/kimi-code/src/tui/components/chrome/device-code-box.ts new file mode 100644 index 000000000..de2704228 --- /dev/null +++ b/apps/kimi-code/src/tui/components/chrome/device-code-box.ts @@ -0,0 +1,76 @@ +/** + * OAuth device-code panel rendered inside the transcript. + * + * Borrows the rounded-border layout from `WelcomeComponent` so the login + * prompt matches the rest of the chrome. All colors flow through the + * active palette so theme switches take effect on the next render. + */ + +import type { Component } from '@earendil-works/pi-tui'; +import { truncateToWidth, visibleWidth } from '@earendil-works/pi-tui'; +import chalk from 'chalk'; + +import type { ColorPalette } from '#/tui/theme/colors'; + +export interface DeviceCodeBoxParams { + readonly title: string; + readonly url: string; + readonly code: string; + readonly hint?: string; + readonly colors: ColorPalette; +} + +export class DeviceCodeBoxComponent implements Component { + private readonly params: DeviceCodeBoxParams; + + constructor(params: DeviceCodeBoxParams) { + this.params = params; + } + + invalidate(): void {} + + render(width: number): string[] { + const { title, url, code, hint, colors } = this.params; + const border = (s: string): string => chalk.hex(colors.primary)(s); + const safeWidth = Math.max(28, width); + const innerWidth = Math.max(10, safeWidth - 4); + const pad = ' '; + + const titleLine = truncateToWidth(chalk.bold.hex(colors.textStrong)(title), innerWidth, '…'); + const promptLine = truncateToWidth( + chalk.hex(colors.textDim)('Visit the URL below in your browser to authorize:'), + innerWidth, + '…', + ); + const urlLine = truncateToWidth(chalk.hex(colors.primary)(url), innerWidth, '…'); + + const codeLabel = chalk.bold.hex(colors.textDim)('Verification code: '); + const codeValue = chalk.bold.hex(colors.accent)(code); + const codeLine = truncateToWidth(`${codeLabel}${codeValue}`, innerWidth, '…'); + + const contentLines: string[] = [titleLine, '', promptLine, urlLine, '', codeLine]; + if (hint !== undefined && hint.length > 0) { + contentLines.push(''); + contentLines.push(truncateToWidth(chalk.hex(colors.textDim)(hint), innerWidth, '…')); + } + + const lines: string[] = [ + '', + border('╭' + '─'.repeat(safeWidth - 2) + '╮'), + border('│') + ' '.repeat(safeWidth - 2) + border('│'), + ]; + + for (const content of contentLines) { + const truncated = content; + const vis = visibleWidth(truncated); + const rightPad = Math.max(0, innerWidth - vis); + lines.push(border('│') + pad + truncated + ' '.repeat(rightPad) + border('│')); + } + + lines.push(border('│') + ' '.repeat(safeWidth - 2) + border('│')); + lines.push(border('╰' + '─'.repeat(safeWidth - 2) + '╯')); + lines.push(''); + + return lines; + } +} diff --git a/apps/kimi-code/src/tui/components/chrome/footer.ts b/apps/kimi-code/src/tui/components/chrome/footer.ts new file mode 100644 index 000000000..9079da44e --- /dev/null +++ b/apps/kimi-code/src/tui/components/chrome/footer.ts @@ -0,0 +1,266 @@ +/** + * Footer/status bar — multi-line status display at the bottom of the TUI. + * + * Layout: + * Line 1: [yolo] [plan] + * Line 2: context: XX.X% (tokens/max) + */ + +import type { Component } from '@earendil-works/pi-tui'; +import { truncateToWidth, visibleWidth } from '@earendil-works/pi-tui'; +import chalk from 'chalk'; + +import type { ColorPalette } from '#/tui/theme/colors'; +import type { AppState } from '#/tui/types'; +import { + createGitStatusCache, + formatGitBadgeBase, + formatPullRequestBadge, + type GitStatus, + type GitStatusCache, +} from '#/utils/git/git-status'; +import { safeUsageRatio } from '#/utils/usage/usage-format'; + +const MAX_CWD_SEGMENTS = 3; + +// Toolbar tips — rotates every 30s, shows 2 tips joined by " | " when +// space allows, falls back to 1. +const TIP_ROTATE_INTERVAL_MS = 30_000; +const TIP_SEPARATOR = ' | '; +const TOOLBAR_TIPS: readonly string[] = [ + 'shift+tab: plan mode', + '/yolo: toggle yolo', + 'ctrl+c: cancel', + '/help: show commands', + '/model: switch model', + '@: mention files', +]; + +function currentTipIndex(): number { + return Math.floor(Date.now() / TIP_ROTATE_INTERVAL_MS); +} + +function twoRotatingTips(index: number): string { + const n = TOOLBAR_TIPS.length; + if (n === 0) return ''; + if (n === 1) return TOOLBAR_TIPS[0]!; + const offset = ((index % n) + n) % n; + return TOOLBAR_TIPS[offset]! + TIP_SEPARATOR + TOOLBAR_TIPS[(offset + 1) % n]!; +} + +function oneRotatingTip(index: number): string { + const n = TOOLBAR_TIPS.length; + if (n === 0) return ''; + const offset = ((index % n) + n) % n; + return TOOLBAR_TIPS[offset]!; +} + +function shortenModel(model: string): string { + if (!model) return model; + const slash = model.lastIndexOf('/'); + return slash >= 0 ? model.slice(slash + 1) : model; +} + +function modelDisplayName(state: AppState): string { + const model = state.availableModels[state.model]; + return model?.displayName ?? model?.model ?? state.model; +} + +function shortenCwd(path: string): string { + if (!path) return path; + const home = process.env['HOME'] ?? ''; + let work = path; + if (home && path === home) { + return '~'; + } + if (home && path.startsWith(home + '/')) { + work = '~' + path.slice(home.length); + } + + const segments = work.split('/').filter((s) => s.length > 0); + if (segments.length <= MAX_CWD_SEGMENTS) return work; + const tail = segments.slice(-MAX_CWD_SEGMENTS).join('/'); + return `…/${tail}`; +} + +function formatTokenCount(n: number): string { + if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`; + if (n >= 1_000) return `${(n / 1_000).toFixed(1)}k`; + return String(n); +} + +function safeUsage(usage: number): number { + return safeUsageRatio(usage); +} + +function formatContextStatus(usage: number, tokens?: number, maxTokens?: number): string { + const pct = `${(safeUsage(usage) * 100).toFixed(1)}%`; + if (maxTokens && maxTokens > 0 && tokens !== undefined) { + return `context: ${pct} (${formatTokenCount(tokens)}/${formatTokenCount(maxTokens)})`; + } + return `context: ${pct}`; +} + +export function formatFooterGitBadge(status: GitStatus, colors: ColorPalette): string { + const base = chalk.hex(colors.status)(formatGitBadgeBase(status)); + if (status.pullRequest === null) return base; + + const pullRequest = chalk.hex(colors.primary)( + formatPullRequestBadge(status.pullRequest, { linkPullRequest: true }), + ); + return `${base} ${pullRequest}`; +} + +export class FooterComponent implements Component { + private state: AppState; + private colors: ColorPalette; + private readonly onGitStatusChange: () => void; + private gitCache: GitStatusCache; + private gitCacheWorkDir: string; + private transientHint: string | null = null; + /** + * Non-terminal background-task counts split by kind so the footer can + * render two distinct badges. `bashTasks` covers `bash-*` BPM tasks + * spawned via `Shell run_in_background=true`; `agentTasks` covers + * `agent-*` BPM tasks (background subagents). Either zero hides its + * respective badge. + */ + private backgroundBashTaskCount = 0; + private backgroundAgentCount = 0; + + constructor(state: AppState, colors: ColorPalette, onGitStatusChange: () => void = () => {}) { + this.state = state; + this.colors = colors; + this.onGitStatusChange = onGitStatusChange; + this.gitCacheWorkDir = state.workDir; + this.gitCache = createGitStatusCache(state.workDir, { onChange: this.onGitStatusChange }); + } + + setState(state: AppState): void { + if (state.workDir !== this.gitCacheWorkDir) { + this.gitCacheWorkDir = state.workDir; + this.gitCache = createGitStatusCache(state.workDir, { onChange: this.onGitStatusChange }); + } + this.state = state; + } + + setColors(colors: ColorPalette): void { + this.colors = colors; + } + + /** + * Short-lived hint that replaces the rotating toolbar tips on line 1. + * Used by the exit-confirmation double-tap flow to show "Press Ctrl+C + * again to exit" without requiring a toast/overlay subsystem. + * Pass `null` to clear. + */ + setTransientHint(hint: string | null): void { + this.transientHint = hint; + } + + /** + * Sync both background-task badges with live counts. Each non-zero + * count produces its own bracketed badge on line 1; zeros hide them + * independently. + */ + setBackgroundCounts(counts: { bashTasks: number; agentTasks: number }): void { + this.backgroundBashTaskCount = Math.max(0, counts.bashTasks); + this.backgroundAgentCount = Math.max(0, counts.agentTasks); + } + + invalidate(): void {} + + render(width: number): string[] { + const colors = this.colors; + const state = this.state; + + // ── Line 1: mode badges + model + [N task(s) running] + [N agent(s) running] + cwd + git + hints ── + const left: string[] = []; + if (state.permissionMode === 'auto') left.push(chalk.hex(colors.warning).bold('auto')); + if (state.permissionMode === 'yolo') left.push(chalk.hex(colors.warning).bold('yolo')); + if (state.planMode) left.push(chalk.hex(colors.primary).bold('plan')); + + const model = shortenModel(modelDisplayName(state)); + if (model) { + const thinkingLabel = state.thinking ? ' thinking' : ''; + left.push(chalk.hex(colors.text)(`${model}${thinkingLabel}`)); + } + + // Background-task badges sit immediately before cwd. `bash-*` tasks + // (shell processes) and `agent-*` tasks (background subagents) get + // separate badges so the user can distinguish them at a glance. + if (this.backgroundBashTaskCount > 0) { + const noun = this.backgroundBashTaskCount === 1 ? 'task' : 'tasks'; + left.push( + chalk.hex(colors.primary)(`[${String(this.backgroundBashTaskCount)} ${noun} running]`), + ); + } + if (this.backgroundAgentCount > 0) { + const noun = this.backgroundAgentCount === 1 ? 'agent' : 'agents'; + left.push( + chalk.hex(colors.primary)(`[${String(this.backgroundAgentCount)} ${noun} running]`), + ); + } + + const cwd = shortenCwd(state.workDir); + if (cwd) left.push(chalk.hex(colors.status)(cwd)); + + const git = this.gitCache.getStatus(); + if (git !== null) { + left.push(formatFooterGitBadge(git, colors)); + } + + const leftLine = left.join(' '); + const leftWidth = visibleWidth(leftLine); + + // Rotating hint tips, fill remaining space on line 1. + const tipIndex = currentTipIndex(); + const tipTwo = twoRotatingTips(tipIndex); + const tipOne = oneRotatingTip(tipIndex); + const gap = 2; + const remaining = Math.max(0, width - leftWidth - gap); + let tipText = ''; + if (tipTwo && visibleWidth(tipTwo) <= remaining) { + tipText = tipTwo; + } else if (tipOne && visibleWidth(tipOne) <= remaining) { + tipText = tipOne; + } + + let line1: string; + if (tipText) { + const pad = width - leftWidth - visibleWidth(tipText); + line1 = leftLine + ' '.repeat(Math.max(0, pad)) + chalk.hex(colors.textMuted)(tipText); + } else if (leftWidth <= width) { + line1 = leftLine; + } else { + line1 = truncateToWidth(leftLine, width, '…'); + } + + // ── Line 2: transient hint (bottom-left) + context (right) ── + const contextText = formatContextStatus( + state.contextUsage, + state.contextTokens, + state.maxContextTokens, + ); + const contextWidth = visibleWidth(contextText); + let line2: string; + if (this.transientHint) { + const maxHintWidth = Math.max(0, width - contextWidth - 1); + const shownHint = + visibleWidth(this.transientHint) <= maxHintWidth + ? this.transientHint + : truncateToWidth(this.transientHint, maxHintWidth, '…'); + const hintWidth = visibleWidth(shownHint); + const pad = Math.max(0, width - hintWidth - contextWidth); + line2 = + chalk.hex(colors.warning).bold(shownHint) + + ' '.repeat(pad) + + chalk.hex(colors.text)(contextText); + } else { + const leftPad = Math.max(0, width - contextWidth); + line2 = ' '.repeat(leftPad) + chalk.hex(colors.text)(contextText); + } + + return [truncateToWidth(line1, width), truncateToWidth(line2, width)]; + } +} diff --git a/apps/kimi-code/src/tui/components/chrome/gutter-container.ts b/apps/kimi-code/src/tui/components/chrome/gutter-container.ts new file mode 100644 index 000000000..39ed97c49 --- /dev/null +++ b/apps/kimi-code/src/tui/components/chrome/gutter-container.ts @@ -0,0 +1,33 @@ +/** + * Container that reserves left/right gutter columns around its children, + * so the chrome (statusline, transcript, panels) lines up with the input + * box's inner content area instead of butting up against the terminal edge. + * + * Children are rendered at `width - left - right` and each emitted line is + * prefixed with `left` plain spaces. Right padding is logical only — we + * never emit trailing spaces, since terminals already paint background to + * the edge and adding them would just churn the diff renderer. + */ + +import { Container } from '@earendil-works/pi-tui'; + +export class GutterContainer extends Container { + constructor( + private readonly leftPad: number, + private readonly rightPad: number, + ) { + super(); + } + + override render(width: number): string[] { + const inner = Math.max(1, width - this.leftPad - this.rightPad); + const lead = ' '.repeat(this.leftPad); + const out: string[] = []; + for (const child of this.children) { + for (const line of child.render(inner)) { + out.push(lead + line); + } + } + return out; + } +} diff --git a/apps/kimi-code/src/tui/components/chrome/moon-loader.ts b/apps/kimi-code/src/tui/components/chrome/moon-loader.ts new file mode 100644 index 000000000..0e88e6af4 --- /dev/null +++ b/apps/kimi-code/src/tui/components/chrome/moon-loader.ts @@ -0,0 +1,68 @@ +import { Text } from '@earendil-works/pi-tui'; +import type { TUI } from '@earendil-works/pi-tui'; + +import { + BRAILLE_SPINNER_FRAMES, + BRAILLE_SPINNER_INTERVAL_MS, + MOON_SPINNER_FRAMES, + MOON_SPINNER_INTERVAL_MS, +} from '#/tui/constant/rendering'; + +export type SpinnerStyle = 'moon' | 'braille'; + +export class MoonLoader extends Text { + private currentFrame = 0; + private intervalId: ReturnType | null = null; + private ui: TUI; + private frames: string[]; + private interval: number; + private colorFn?: (s: string) => string; + private label: string; + + constructor( + ui: TUI, + style: SpinnerStyle = 'moon', + colorFn?: (s: string) => string, + label: string = '', + ) { + super('', 1, 0); + this.ui = ui; + this.frames = style === 'moon' ? [...MOON_SPINNER_FRAMES] : [...BRAILLE_SPINNER_FRAMES]; + this.interval = style === 'moon' ? MOON_SPINNER_INTERVAL_MS : BRAILLE_SPINNER_INTERVAL_MS; + this.colorFn = colorFn; + this.label = label; + this.start(); + } + + start(): void { + this.updateDisplay(); + this.intervalId = setInterval(() => { + this.currentFrame = (this.currentFrame + 1) % this.frames.length; + this.updateDisplay(); + }, this.interval); + } + + stop(): void { + if (this.intervalId) { + clearInterval(this.intervalId); + this.intervalId = null; + } + } + + setLabel(label: string): void { + this.label = label; + this.updateDisplay(); + } + + setColorFn(colorFn: (s: string) => string): void { + this.colorFn = colorFn; + this.updateDisplay(); + } + + private updateDisplay(): void { + const frame = this.frames[this.currentFrame]!; + const coloredFrame = this.colorFn ? this.colorFn(frame) : frame; + this.setText(this.label ? `${coloredFrame} ${this.label}` : coloredFrame); + this.ui.requestRender(); + } +} diff --git a/apps/kimi-code/src/tui/components/chrome/todo-panel.ts b/apps/kimi-code/src/tui/components/chrome/todo-panel.ts new file mode 100644 index 000000000..ede986e60 --- /dev/null +++ b/apps/kimi-code/src/tui/components/chrome/todo-panel.ts @@ -0,0 +1,96 @@ +/** + * TodoPanel — live-updating TODO list shown before the input area. + * + * Mounted as a dedicated `Container` slot between the activity pane + * (spinners / thinking stream) and the queue / editor block. The host + * calls {@link setTodos} whenever the LLM invokes the `TodoList` + * tool; state survives across turns so the list stays visible until + * explicitly cleared (`todos: []`), a new session starts, or `/clear` + * is issued. + */ + +import type { Component } from '@earendil-works/pi-tui'; +import { truncateToWidth } from '@earendil-works/pi-tui'; +import chalk from 'chalk'; + +import type { ColorPalette } from '#/tui/theme/colors'; + +export type TodoStatus = 'pending' | 'in_progress' | 'done'; + +export interface TodoItem { + readonly title: string; + readonly status: TodoStatus; +} + +export class TodoPanelComponent implements Component { + private todos: readonly TodoItem[] = []; + private colors: ColorPalette; + + constructor(colors: ColorPalette) { + this.colors = colors; + } + + setTodos(todos: readonly TodoItem[]): void { + this.todos = todos.map((t) => ({ title: t.title, status: t.status })); + } + + getTodos(): readonly TodoItem[] { + return this.todos; + } + + clear(): void { + this.todos = []; + } + + isEmpty(): boolean { + return this.todos.length === 0; + } + + setColors(colors: ColorPalette): void { + this.colors = colors; + } + + invalidate(): void {} + + render(width: number): string[] { + if (this.todos.length === 0) return []; + const c = this.colors; + const lines: string[] = [ + chalk.hex(c.border)('─'.repeat(width)), + chalk.hex(c.primary).bold(' Todo'), + ]; + for (const todo of this.todos) { + lines.push(renderRow(todo, c)); + } + + return lines.map((line) => truncateToWidth(line, width)); + } +} + +function renderRow(todo: TodoItem, colors: ColorPalette): string { + const marker = statusMarker(todo.status, colors); + const titleStyled = styleTitle(todo.title, todo.status, colors); + return ` ${marker} ${titleStyled}`; +} + +function statusMarker(status: TodoStatus, colors: ColorPalette): string { + switch (status) { + case 'in_progress': + return chalk.hex(colors.primary).bold('●'); + case 'done': + return chalk.hex(colors.success)('✓'); + case 'pending': + return chalk.hex(colors.textDim)('○'); + } +} + +function styleTitle(title: string, status: TodoStatus, colors: ColorPalette): string { + switch (status) { + case 'in_progress': + return chalk.hex(colors.text).bold(title); + case 'done': + return chalk.hex(colors.textDim).strikethrough(title); + case 'pending': + return chalk.hex(colors.text)(title); + } +} diff --git a/apps/kimi-code/src/tui/components/chrome/welcome.ts b/apps/kimi-code/src/tui/components/chrome/welcome.ts new file mode 100644 index 000000000..e75d46df9 --- /dev/null +++ b/apps/kimi-code/src/tui/components/chrome/welcome.ts @@ -0,0 +1,86 @@ +/** + * Welcome panel shown at the top of the TUI. + * Renders a round-bordered box with the logo, session, model, and version. + */ + +import type { Component } from '@earendil-works/pi-tui'; +import { truncateToWidth, visibleWidth } from '@earendil-works/pi-tui'; +import chalk from 'chalk'; + +import type { ColorPalette } from '#/tui/theme/colors'; +import type { AppState } from '#/tui/types'; + +export class WelcomeComponent implements Component { + private state: AppState; + private colors: ColorPalette; + + constructor(state: AppState, colors: ColorPalette) { + this.state = state; + this.colors = colors; + } + + invalidate(): void {} + + render(width: number): string[] { + const primary = (s: string): string => chalk.hex(this.colors.primary)(s); + const innerWidth = Math.max(10, width - 4); + const pad = ' '; + + // Logo + side-by-side text. + const logo = ['▐█▛█▛█▌', '▐█████▌']; + const logoWidth = Math.max(...logo.map((row) => visibleWidth(row))); + const gap = ' '; + const textWidth = Math.max(4, innerWidth - logoWidth - gap.length); + + const rightRow0 = truncateToWidth( + chalk.bold.hex(this.colors.primary)('Welcome to Kimi Code!'), + textWidth, + '…', + ); + const isLoggedOut = !this.state.model; + const dim = chalk.hex(this.colors.textDim); + const labelStyle = chalk.bold.hex(this.colors.textDim); + const rightRow1 = truncateToWidth( + dim(isLoggedOut ? 'Run /login to sign in.' : 'Send /help for help information.'), + textWidth, + '…', + ); + + const headerLines = [ + primary(logo[0]!.padEnd(logoWidth)) + gap + rightRow0, + primary(logo[1]!.padEnd(logoWidth)) + gap + rightRow1, + ]; + + const modelValue = isLoggedOut + ? chalk.hex(this.colors.warning)('not set, send /login to login') + : this.state.model; + + const infoLines = [ + labelStyle('Directory: ') + this.state.workDir, + labelStyle('Session: ') + this.state.sessionId, + labelStyle('Model: ') + modelValue, + labelStyle('Version: ') + this.state.version, + ]; + + const contentLines: string[] = [...headerLines, '', ...infoLines]; + + const lines: string[] = [ + '', + primary('╭' + '─'.repeat(width - 2) + '╮'), + primary('│') + ' '.repeat(width - 2) + primary('│'), + ]; + + for (const content of contentLines) { + const truncated = truncateToWidth(content, innerWidth, '…'); + const vis = visibleWidth(truncated); + const rightPad = Math.max(0, innerWidth - vis); + lines.push(primary('│') + pad + truncated + ' '.repeat(rightPad) + primary('│')); + } + + lines.push(primary('│') + ' '.repeat(width - 2) + primary('│')); + lines.push(primary('╰' + '─'.repeat(width - 2) + '╯')); + lines.push(''); + + return lines; + } +} diff --git a/apps/kimi-code/src/tui/components/dialogs/api-key-input-dialog.ts b/apps/kimi-code/src/tui/components/dialogs/api-key-input-dialog.ts new file mode 100644 index 000000000..abf1b3e99 --- /dev/null +++ b/apps/kimi-code/src/tui/components/dialogs/api-key-input-dialog.ts @@ -0,0 +1,148 @@ +import { + Container, + Input, + Key, + matchesKey, + truncateToWidth, + visibleWidth, + type Focusable, +} from '@earendil-works/pi-tui'; +import chalk from 'chalk'; + +import type { ColorPalette } from '#/tui/theme/colors'; + +export type ApiKeyInputResult = + | { readonly kind: 'ok'; readonly value: string } + | { readonly kind: 'cancel' }; + +const FOOTER = 'Enter to submit · Esc to cancel'; + +function maskInputLine(raw: string): string { + const prefix = '> '; + if (!raw.startsWith(prefix)) return raw; + + // Strip trailing padding spaces so they stay as spaces. + let end = raw.length; + while (end > prefix.length && raw[end - 1] === ' ') { + end--; + } + const padding = raw.slice(end); + const content = raw.slice(prefix.length, end); + + // Protect ANSI escape sequences (reverse-video cursor, IME marker, etc.) + // while masking every other visible character. + const parts = content.split(/(\u001B(?:\[[0-9;]*m|_pi:c\u0007))/); + const maskedContent = parts + .map((part, index) => { + if (index % 2 === 1) return part; // ANSI sequence + return part.replaceAll(/./g, '•'); + }) + .join(''); + + return prefix + maskedContent + padding; +} + +export class ApiKeyInputDialogComponent extends Container implements Focusable { + focused = false; + + private readonly input = new Input(); + private readonly onDone: (result: ApiKeyInputResult) => void; + private readonly colors: ColorPalette; + private readonly title: string; + private readonly subtitle: string; + private done = false; + private emptyHinted = false; + + constructor( + platformName: string, + onDone: (result: ApiKeyInputResult) => void, + colors: ColorPalette, + ) { + super(); + this.onDone = onDone; + this.colors = colors; + this.title = `Enter API key for ${platformName}`; + this.subtitle = 'Your key will be saved to ~/.kimi-code/config.toml'; + this.input.onSubmit = (value) => { + this.submit(value); + }; + } + + handleInput(data: string): void { + if (this.done) return; + if ( + matchesKey(data, Key.escape) || + matchesKey(data, Key.ctrl('c')) || + matchesKey(data, Key.ctrl('d')) + ) { + this.cancel(); + return; + } + if (this.emptyHinted) { + this.emptyHinted = false; + } + this.input.handleInput(data); + } + + override invalidate(): void { + super.invalidate(); + this.input.invalidate(); + } + + override render(width: number): string[] { + this.input.focused = this.focused && !this.done; + + const safeWidth = Math.max(28, width); + const innerWidth = Math.max(10, safeWidth - 4); + const pad = ' '; + + const border = (s: string): string => chalk.hex(this.colors.primary)(s); + const titleStyled = chalk.bold.hex(this.colors.textStrong)(this.title); + const subtitleText = this.emptyHinted ? 'API key cannot be empty.' : this.subtitle; + const subtitleStyled = chalk.hex(this.colors.textDim)(subtitleText); + const footerStyled = chalk.hex(this.colors.textDim)(FOOTER); + + const titleLine = truncateToWidth(titleStyled, innerWidth, '…'); + const subtitleLine = truncateToWidth(subtitleStyled, innerWidth, '…'); + const footerLine = truncateToWidth(footerStyled, innerWidth, '…'); + const rawInputLine = this.input.render(innerWidth)[0] ?? '> '; + const inputLine = maskInputLine(rawInputLine); + + const contentLines: string[] = [titleLine, '', subtitleLine, '', inputLine, '', footerLine]; + + const lines: string[] = [ + '', + border('╭' + '─'.repeat(safeWidth - 2) + '╮'), + border('│') + ' '.repeat(safeWidth - 2) + border('│'), + ]; + + for (const content of contentLines) { + const vis = visibleWidth(content); + const rightPad = Math.max(0, innerWidth - vis); + lines.push(border('│') + pad + content + ' '.repeat(rightPad) + border('│')); + } + + lines.push(border('│') + ' '.repeat(safeWidth - 2) + border('│')); + lines.push(border('╰' + '─'.repeat(safeWidth - 2) + '╯')); + lines.push(''); + + return lines; + } + + private submit(value: string): void { + if (this.done) return; + const trimmed = value.trim(); + if (trimmed.length === 0) { + this.emptyHinted = true; + return; + } + this.done = true; + this.onDone({ kind: 'ok', value: trimmed }); + } + + private cancel(): void { + if (this.done) return; + this.done = true; + this.onDone({ kind: 'cancel' }); + } +} diff --git a/apps/kimi-code/src/tui/components/dialogs/approval-panel.ts b/apps/kimi-code/src/tui/components/dialogs/approval-panel.ts new file mode 100644 index 000000000..ed93602f6 --- /dev/null +++ b/apps/kimi-code/src/tui/components/dialogs/approval-panel.ts @@ -0,0 +1,406 @@ +/** + * ApprovalPanel — pi-tui version of the approval request UI. + * + * Container-based component with keyboard navigation. + */ + +import { + Container, + Input, + matchesKey, + Key, + decodeKittyPrintable, + type Focusable, + truncateToWidth, + visibleWidth, +} from '@earendil-works/pi-tui'; +import chalk from 'chalk'; + +import { highlightLines, langFromPath } from '#/tui/components/media/code-highlight'; +import { renderDiffLinesClustered } from '#/tui/components/media/diff-preview'; +import type { ApprovalPanelChoice, DisplayBlock, PendingApproval } from '#/tui/reverse-rpc/types'; +import type { ColorPalette } from '#/tui/theme/colors'; + +export interface ApprovalPanelResponse { + readonly response: 'approved' | 'approved_for_session' | 'rejected' | 'cancelled'; + readonly feedback?: string | undefined; + readonly selected_label?: string | undefined; +} + +function truncateOneLine(text: string, max: number): string { + const firstLine = text.split('\n')[0] ?? ''; + return firstLine.length > max ? firstLine.slice(0, max - 1) + '…' : firstLine; +} + +const DIFF_SUMMARY_MAX_LINES = 10; +const CONTENT_SUMMARY_MAX_LINES = 10; + +interface BlockStyles { + strong: (s: string) => string; + dim: (s: string) => string; + accent: (s: string) => string; + gutter: (s: string) => string; + errorBold: (s: string) => string; +} + +function makeBlockStyles(colors: ColorPalette): BlockStyles { + return { + strong: (s) => chalk.hex(colors.textStrong)(s), + dim: (s) => chalk.hex(colors.textDim)(s), + accent: (s) => chalk.hex(colors.accent)(s), + gutter: (s) => chalk.hex(colors.diffGutter)(s), + errorBold: (s) => chalk.bold.hex(colors.error)(s), + }; +} + +function renderDisplayBlock( + block: DisplayBlock, + expanded: boolean, + s: BlockStyles, + colors: ColorPalette, +): string[] { + switch (block.type) { + case 'diff': + return renderDiffLinesClustered(block.old_text, block.new_text, block.path, colors, { + contextLines: 3, + expandKeyHint: 'ctrl+e', + ...(expanded ? {} : { maxLines: DIFF_SUMMARY_MAX_LINES }), + }); + case 'file_content': { + const lang = block.language ?? langFromPath(block.path); + const allLines = highlightLines(block.content, lang); + const cap = expanded ? allLines.length : CONTENT_SUMMARY_MAX_LINES; + const shown = allLines.slice(0, cap); + const lines = [s.strong(block.path)]; + for (const [i, line] of shown.entries()) { + lines.push(s.gutter(String(i + 1).padStart(4) + ' ') + line); + } + const remaining = allLines.length - shown.length; + if (remaining > 0) { + lines.push( + s.dim( + ` … ${String(remaining)} more line${remaining > 1 ? 's' : ''} hidden (ctrl+e to expand)`, + ), + ); + } + return lines; + } + case 'shell': { + const lines: string[] = []; + if (block.cwd !== undefined && block.cwd.length > 0) { + lines.push(s.dim(`cwd: ${block.cwd}`)); + } + if (block.danger !== undefined) { + lines.push(s.errorBold(`Dangerous: ${block.danger}`)); + } + const cmdLines = block.command.length > 0 ? block.command.split('\n') : ['']; + cmdLines.forEach((cmdLine, idx) => { + const prefix = idx === 0 ? s.accent('$') : s.dim('·'); + lines.push(`${prefix} ${s.strong(cmdLine)}`); + }); + if (block.description !== undefined && block.description.length > 0) { + lines.push(` ${s.dim(block.description)}`); + } + return lines; + } + case 'file_op': { + const op = s.accent(block.operation.padEnd(5)); + const lines = [`${op} ${s.strong(block.path)}`]; + if (block.detail !== undefined && block.detail.length > 0) { + lines.push(s.dim(block.detail)); + } + return lines; + } + case 'url_fetch': { + const method = s.accent((block.method ?? 'GET').toUpperCase().padEnd(5)); + return [`${method} ${s.strong(block.url)}`]; + } + case 'search': { + const lines = [`${s.accent('search')} ${s.strong(block.query)}`]; + if (block.scope !== undefined && block.scope.length > 0) { + lines.push(s.dim(`scope: ${block.scope}`)); + } + return lines; + } + case 'invocation': { + const lines = [`${s.accent(block.kind.padEnd(5))} ${s.strong(block.name)}`]; + if (block.description !== undefined && block.description.length > 0) { + lines.push(s.dim(truncateOneLine(block.description, 200))); + } + return lines; + } + case 'brief': + return block.text + ? block.text.split('\n').map((line) => (line.length > 0 ? s.strong(line) : '')) + : []; + case 'background_task': + return [ + s.strong(`${block.status} ${block.kind} task ${block.task_id}: ${block.description}`), + ]; + case 'todo': + return block.items.map((item) => s.strong(`- [${item.status}] ${item.title}`)); + default: + return []; + } +} + +function normalizeApprovalText(text: string): string { + return text.replaceAll('\r\n', '\n').trim(); +} + +function isDuplicateBriefBlock(block: DisplayBlock, description: string): boolean { + if (block.type !== 'brief' || block.text.trim().length === 0) return false; + const normalizedDescription = normalizeApprovalText(description); + if (normalizedDescription.length === 0) return false; + const normalizedBlockText = normalizeApprovalText(block.text); + if (normalizedBlockText === normalizedDescription) return true; + const blockLines = normalizedBlockText.split('\n'); + if (blockLines.length <= 1) return false; + return normalizeApprovalText(blockLines.slice(1).join('\n')) === normalizedDescription; +} + +function headerFor(toolName: string): string { + switch (toolName) { + case 'Bash': + return 'Run this command?'; + case 'Write': + return 'Write this file?'; + case 'Edit': + return 'Apply these edits?'; + case 'TaskStop': + return 'Stop this task?'; + case 'ExitPlanMode': + return 'Ready to build with this plan?'; + default: + return `Approve ${toolName}?`; + } +} + +export class ApprovalPanelComponent extends Container implements Focusable { + focused = false; + private selectedIndex = 0; + private feedbackMode = false; + private readonly feedbackInput = new Input(); + private expanded = false; + private onResponse: (response: ApprovalPanelResponse) => void; + private request: PendingApproval; + private readonly colors: ColorPalette; + private readonly onToggleToolOutput: (() => void) | undefined; + private readonly onTogglePlanExpand: (() => void) | undefined; + + constructor( + request: PendingApproval, + onResponse: (response: ApprovalPanelResponse) => void, + colors: ColorPalette, + onToggleToolOutput?: () => void, + onTogglePlanExpand?: () => void, + ) { + super(); + this.request = request; + this.onResponse = onResponse; + this.colors = colors; + this.onToggleToolOutput = onToggleToolOutput; + this.onTogglePlanExpand = onTogglePlanExpand; + this.feedbackInput.onSubmit = (value) => { + this.submit(this.selectedIndex, value); + }; + this.feedbackInput.onEscape = () => { + this.feedbackMode = false; + this.feedbackInput.setValue(''); + }; + } + + private submit(index: number, feedback: string = ''): void { + const option = this.choiceAt(index); + if (!option) return; + this.onResponse({ + response: option.response, + feedback: feedback || undefined, + selected_label: option.selected_label, + }); + } + + private selectAndSubmit(index: number): void { + const option = this.choiceAt(index); + if (!option) return; + if (option.requires_feedback === true) { + this.selectedIndex = index; + this.feedbackMode = true; + } else { + this.submit(index); + } + } + + handleInput(data: string): void { + if ( + matchesKey(data, Key.escape) || + matchesKey(data, Key.ctrl('c')) || + matchesKey(data, Key.ctrl('d')) + ) { + this.onResponse({ response: 'rejected' }); + return; + } + + if (matchesKey(data, Key.ctrl('e'))) { + this.expanded = !this.expanded; + this.onTogglePlanExpand?.(); + return; + } + + if (matchesKey(data, Key.ctrl('o'))) { + this.onToggleToolOutput?.(); + return; + } + + if (this.feedbackMode) { + if (matchesKey(data, Key.up)) { + this.feedbackMode = false; + this.selectedIndex = (this.selectedIndex - 1 + this.choiceCount()) % this.choiceCount(); + return; + } + if (matchesKey(data, Key.down)) { + this.feedbackMode = false; + this.selectedIndex = (this.selectedIndex + 1) % this.choiceCount(); + return; + } + this.feedbackInput.handleInput(data); + return; + } + + if (this.choiceCount() === 0) return; + if (matchesKey(data, Key.up)) { + this.selectedIndex = (this.selectedIndex - 1 + this.choiceCount()) % this.choiceCount(); + return; + } + if (matchesKey(data, Key.down)) { + this.selectedIndex = (this.selectedIndex + 1) % this.choiceCount(); + return; + } + if (matchesKey(data, Key.enter)) { + this.selectAndSubmit(this.selectedIndex); + return; + } + + const printable = decodeKittyPrintable(data) ?? data; + const numericIndex = Number(printable) - 1; + if (Number.isInteger(numericIndex) && numericIndex >= 0 && numericIndex < this.choiceCount()) { + this.selectAndSubmit(numericIndex); + } + } + + override render(width: number): string[] { + this.clear(); + this.ensureValidSelection(); + this.feedbackInput.focused = this.focused && this.feedbackMode; + const { data } = this.request; + const blockStyles = makeBlockStyles(this.colors); + const borderColor = chalk.hex(this.colors.borderFocus); + const borderColorBold = chalk.bold.hex(this.colors.borderFocus); + const selectColorBold = chalk.bold.hex(this.colors.accent); + const dim = chalk.hex(this.colors.textDim); + const strong = chalk.hex(this.colors.textStrong); + const horizontalBar = borderColor('─'.repeat(width)); + const indent = (s: string): string => ` ${s}`; + + const title = headerFor(data.tool_name); + const lines: string[] = [ + horizontalBar, + indent(`${borderColorBold('▶')} ${borderColorBold(title)}`), + ]; + + const dedupedBlocks = data.display.filter( + (block) => !isDuplicateBriefBlock(block, data.description), + ); + const visibleBlocks = dedupedBlocks.slice(0, 5); + const hasExpandable = visibleBlocks.some( + (block) => block.type === 'diff' || block.type === 'file_content', + ); + + if (visibleBlocks.length > 0) { + lines.push(''); + for (const block of visibleBlocks) { + const blockLines = renderDisplayBlock(block, this.expanded, blockStyles, this.colors); + for (const line of blockLines) { + lines.push(indent(line)); + } + } + } else if (data.description) { + lines.push(''); + for (const descLine of data.description.split('\n')) { + lines.push(indent(dim(descLine))); + } + } + + lines.push(''); + for (let idx = 0; idx < data.choices.length; idx++) { + const option = data.choices[idx]; + if (option === undefined) continue; + const isSelected = idx === this.selectedIndex; + const num = idx + 1; + + const labelWithNum = `${String(num)}. ${option.label}`; + if (this.feedbackMode && option.requires_feedback === true && isSelected) { + lines.push(indent(this.renderInlineFeedbackLine(width - 2, labelWithNum))); + } else if (isSelected) { + lines.push(indent(`${selectColorBold('▶')} ${selectColorBold(labelWithNum)}`)); + } else { + lines.push(indent(strong(` ${labelWithNum}`))); + } + } + + lines.push(''); + if (this.feedbackMode) { + lines.push(indent(dim('Type feedback · ↵ submit.'))); + } else { + const expandHint = hasExpandable ? ` · ctrl+e ${this.expanded ? 'collapse' : 'expand'}` : ''; + lines.push( + indent( + dim( + `↑/↓ select · ${buildNumericHint(data.choices.length)} choose · ↵ confirm${expandHint}`, + ), + ), + ); + } + lines.push(horizontalBar); + + return lines.map((line) => truncateToWidth(line, width)); + } + + private choiceAt(index: number): ApprovalPanelChoice | undefined { + return this.request.data.choices[index]; + } + + private choiceCount(): number { + return this.request.data.choices.length; + } + + private ensureValidSelection(): void { + const count = this.choiceCount(); + if (count === 0) { + this.selectedIndex = 0; + return; + } + if (this.selectedIndex < 0 || this.selectedIndex >= count) { + this.selectedIndex = Math.max(0, Math.min(this.selectedIndex, count - 1)); + } + } + + private renderInlineFeedbackLine(width: number, labelWithNum: string): string { + const selectColorBold = chalk.bold.hex(this.colors.accent); + const prefix = `${selectColorBold('▶')} ${selectColorBold(labelWithNum)} `; + const inputWidth = Math.max(4, width - visibleWidth(prefix) + 2); + const inputLine = this.feedbackInput.render(inputWidth)[0] ?? '> '; + const inlineInput = inputLine.startsWith('> ') ? inputLine.slice(2) : inputLine; + return prefix + inlineInput; + } + + override invalidate(): void { + super.invalidate(); + this.feedbackInput.invalidate(); + } +} + +function buildNumericHint(count: number): string { + if (count <= 0) return '↵'; + return Array.from({ length: Math.min(count, 9) }, (_, idx) => String(idx + 1)).join('/'); +} diff --git a/apps/kimi-code/src/tui/components/dialogs/choice-picker.ts b/apps/kimi-code/src/tui/components/dialogs/choice-picker.ts new file mode 100644 index 000000000..66bca1a65 --- /dev/null +++ b/apps/kimi-code/src/tui/components/dialogs/choice-picker.ts @@ -0,0 +1,133 @@ +/** + * ChoicePicker — modal single-select list for slash commands that ask + * the user to pick from a small set of preset values. + * + * Mirrors SessionPickerComponent's container-replacement pattern: host + * calls `showChoicePicker(...)` which clears the editor container, + * addChild(picker), setFocus(picker); the picker invokes `onSelect` or + * `onCancel`, and the host tears it down. + */ + +import { + Container, + matchesKey, + Key, + truncateToWidth, + visibleWidth, + type Focusable, +} from '@earendil-works/pi-tui'; +import chalk from 'chalk'; + +import type { ColorPalette } from '#/tui/theme/colors'; + +export interface ChoiceOption { + /** Value passed to onSelect (e.g. the actual editor command string). */ + readonly value: string; + /** Display text shown in the list. */ + readonly label: string; + /** Optional explanatory text shown below the label. */ + readonly description?: string | undefined; +} + +export interface ChoicePickerOptions { + readonly title: string; + readonly hint?: string; + readonly options: readonly ChoiceOption[]; + readonly currentValue?: string; + readonly colors: ColorPalette; + readonly onSelect: (value: string) => void; + readonly onCancel: () => void; +} + +const CURRENT_MARK = '← current'; + +function wrapDescription(text: string, width: number): string[] { + const maxWidth = Math.max(1, width); + const words = text + .trim() + .split(/\s+/) + .filter((word) => word.length > 0); + const lines: string[] = []; + let current = ''; + + for (const word of words) { + const candidate = current.length === 0 ? word : `${current} ${word}`; + if (visibleWidth(candidate) <= maxWidth) { + current = candidate; + continue; + } + if (current.length > 0) lines.push(current); + current = visibleWidth(word) <= maxWidth ? word : truncateToWidth(word, maxWidth, '…'); + } + + if (current.length > 0) lines.push(current); + return lines; +} + +export class ChoicePickerComponent extends Container implements Focusable { + focused = false; + private readonly opts: ChoicePickerOptions; + private selectedIndex: number; + + constructor(opts: ChoicePickerOptions) { + super(); + this.opts = opts; + const currentIdx = opts.options.findIndex((o) => o.value === opts.currentValue); + this.selectedIndex = Math.max(currentIdx, 0); + } + + handleInput(data: string): void { + if (matchesKey(data, Key.escape)) { + this.opts.onCancel(); + return; + } + if (matchesKey(data, Key.up)) { + this.selectedIndex = Math.max(0, this.selectedIndex - 1); + return; + } + if (matchesKey(data, Key.down)) { + this.selectedIndex = Math.min(this.opts.options.length - 1, this.selectedIndex + 1); + return; + } + if (matchesKey(data, Key.enter)) { + const chosen = this.opts.options[this.selectedIndex]; + if (chosen !== undefined) this.opts.onSelect(chosen.value); + return; + } + } + + override render(width: number): string[] { + const { colors } = this.opts; + const hint = this.opts.hint ?? '↑↓ navigate · Enter select · Esc cancel'; + const lines: string[] = [ + chalk.hex(colors.primary)('─'.repeat(width)), + chalk.hex(colors.primary).bold(` ${this.opts.title}`), + chalk.hex(colors.textMuted)(` ${hint}`), + '', + ]; + + for (let i = 0; i < this.opts.options.length; i++) { + const opt = this.opts.options[i]!; + const isSelected = i === this.selectedIndex; + const isCurrent = opt.value === this.opts.currentValue; + const pointer = isSelected ? '❯' : ' '; + const labelStyle = isSelected ? chalk.hex(colors.primary).bold : chalk.hex(colors.text); + let line = chalk.hex(isSelected ? colors.primary : colors.textDim)(` ${pointer} `); + line += labelStyle(opt.label); + if (isCurrent) { + line += ' ' + chalk.hex(colors.success)(CURRENT_MARK); + } + lines.push(line); + if (opt.description !== undefined && opt.description.length > 0) { + const descriptionWidth = Math.max(1, width - 4); + for (const descLine of wrapDescription(opt.description, descriptionWidth)) { + lines.push(chalk.hex(colors.textMuted)(` ${descLine}`)); + } + } + } + + lines.push(''); + lines.push(chalk.hex(colors.primary)('─'.repeat(width))); + return lines.map((line) => truncateToWidth(line, width)); + } +} diff --git a/apps/kimi-code/src/tui/components/dialogs/compaction.ts b/apps/kimi-code/src/tui/components/dialogs/compaction.ts new file mode 100644 index 000000000..6a55ede98 --- /dev/null +++ b/apps/kimi-code/src/tui/components/dialogs/compaction.ts @@ -0,0 +1,109 @@ +/** + * Renders a compaction block in the transcript. + * + * Lifecycle: + * - constructed on `compaction.started` → blinking white bullet + + * "Compacting context..." and optional custom instruction + * - `markDone()` on `compaction.completed` → solid green bullet + + * "Compaction complete (X → Y tokens)" + * - `markCanceled()` on `compaction.cancelled` → solid warning bullet + + * "Compaction cancelled" + * + * Bullet animation mirrors `ToolCallComponent` (500ms blink) so the user + * reads the same "work in progress" signal across the UI. + */ + +import { Container, Text, Spacer } from '@earendil-works/pi-tui'; +import type { TUI } from '@earendil-works/pi-tui'; +import chalk from 'chalk'; + +import { STATUS_BULLET } from '#/tui/constant/symbols'; +import type { ColorPalette } from '#/tui/theme/colors'; + +const BLINK_INTERVAL = 500; + +export class CompactionComponent extends Container { + private readonly colors: ColorPalette; + private readonly ui: TUI | undefined; + private readonly headerText: Text; + private blinkOn = true; + private blinkTimer: ReturnType | null = null; + private done = false; + private canceled = false; + private tokensBefore: number | undefined; + private tokensAfter: number | undefined; + + constructor(colors: ColorPalette, ui?: TUI, instruction?: string | undefined) { + super(); + this.colors = colors; + this.ui = ui; + + // Top margin so the block isn't glued to the previous transcript + // entry (status line, tool result, etc.). + this.addChild(new Spacer(1)); + this.headerText = new Text(this.buildHeader(), 0, 0); + this.addChild(this.headerText); + if (instruction !== undefined) { + this.addChild(new Text(chalk.dim(` ${instruction}`), 0, 0)); + } + + this.startBlink(); + } + + markDone(tokensBefore?: number, tokensAfter?: number): void { + if (this.done || this.canceled) return; + this.done = true; + this.tokensBefore = tokensBefore; + this.tokensAfter = tokensAfter; + this.stopBlink(); + this.headerText.setText(this.buildHeader()); + this.ui?.requestRender(); + } + + markCanceled(): void { + if (this.done || this.canceled) return; + this.canceled = true; + this.stopBlink(); + this.headerText.setText(this.buildHeader()); + this.ui?.requestRender(); + } + + dispose(): void { + this.stopBlink(); + } + + private buildHeader(): string { + if (this.done) { + const bullet = chalk.hex(this.colors.success)(STATUS_BULLET); + const label = chalk.hex(this.colors.success).bold('Compaction complete'); + const detail = + this.tokensBefore !== undefined && this.tokensAfter !== undefined + ? chalk.dim(` (${String(this.tokensBefore)} → ${String(this.tokensAfter)} tokens)`) + : ''; + return `${bullet}${label}${detail}`; + } + if (this.canceled) { + const bullet = chalk.hex(this.colors.warning)(STATUS_BULLET); + const label = chalk.hex(this.colors.warning).bold('Compaction cancelled'); + return `${bullet}${label}`; + } + const bullet = this.blinkOn ? chalk.hex(this.colors.roleAssistant)(STATUS_BULLET) : ' '; + const label = chalk.hex(this.colors.primary).bold('Compacting context...'); + return `${bullet}${label}`; + } + + private startBlink(): void { + this.blinkTimer = setInterval(() => { + this.blinkOn = !this.blinkOn; + this.headerText.setText(this.buildHeader()); + this.ui?.requestRender(); + }, BLINK_INTERVAL); + } + + private stopBlink(): void { + if (this.blinkTimer !== null) { + clearInterval(this.blinkTimer); + this.blinkTimer = null; + } + } +} diff --git a/apps/kimi-code/src/tui/components/dialogs/editor-selector.ts b/apps/kimi-code/src/tui/components/dialogs/editor-selector.ts new file mode 100644 index 000000000..24467dd05 --- /dev/null +++ b/apps/kimi-code/src/tui/components/dialogs/editor-selector.ts @@ -0,0 +1,31 @@ +import { ChoicePickerComponent, type ChoiceOption } from './choice-picker'; + +import type { ColorPalette } from '#/tui/theme/colors'; + +const EDITOR_OPTIONS: readonly ChoiceOption[] = [ + { value: 'code --wait', label: 'VS Code (code --wait)' }, + { value: 'vim', label: 'Vim' }, + { value: 'nvim', label: 'Neovim' }, + { value: 'nano', label: 'Nano' }, + { value: '', label: 'Auto-detect ($VISUAL / $EDITOR)' }, +]; + +export interface EditorSelectorOptions { + readonly currentValue: string; + readonly colors: ColorPalette; + readonly onSelect: (value: string) => void; + readonly onCancel: () => void; +} + +export class EditorSelectorComponent extends ChoicePickerComponent { + constructor(opts: EditorSelectorOptions) { + super({ + title: 'Select external editor', + options: [...EDITOR_OPTIONS], + currentValue: opts.currentValue, + colors: opts.colors, + onSelect: opts.onSelect, + onCancel: opts.onCancel, + }); + } +} diff --git a/apps/kimi-code/src/tui/components/dialogs/feedback-input-dialog.ts b/apps/kimi-code/src/tui/components/dialogs/feedback-input-dialog.ts new file mode 100644 index 000000000..1fb963625 --- /dev/null +++ b/apps/kimi-code/src/tui/components/dialogs/feedback-input-dialog.ts @@ -0,0 +1,126 @@ +/** + * FeedbackInputDialog — blue rounded box that collects a single line of + * user feedback before submitting it to the managed Kimi Code platform. + * + * Geometry mirrors `DeviceCodeBox` so the chrome stays consistent with + * the OAuth login flow. The box embeds a `pi-tui` Input for the actual + * text entry; cursor visibility tracks the dialog's `focused` flag. + */ + +import { + Container, + Input, + Key, + matchesKey, + truncateToWidth, + visibleWidth, + type Focusable, +} from '@earendil-works/pi-tui'; +import chalk from 'chalk'; + +import type { ColorPalette } from '#/tui/theme/colors'; + +export type FeedbackInputDialogResult = + | { readonly kind: 'ok'; readonly value: string } + | { readonly kind: 'cancel' }; + +const TITLE = 'Send feedback to Kimi Code'; +const SUBTITLE_DEFAULT = "Tell us what's working or what's not."; +const SUBTITLE_EMPTY = 'Feedback cannot be empty.'; +const FOOTER = 'Enter to submit · Esc to cancel'; + +export class FeedbackInputDialogComponent extends Container implements Focusable { + focused = false; + + private readonly input = new Input(); + private readonly onDone: (result: FeedbackInputDialogResult) => void; + private readonly colors: ColorPalette; + private done = false; + private emptyHinted = false; + + constructor(onDone: (result: FeedbackInputDialogResult) => void, colors: ColorPalette) { + super(); + this.onDone = onDone; + this.colors = colors; + this.input.onSubmit = (value) => { + this.submit(value); + }; + } + + handleInput(data: string): void { + if (this.done) return; + if ( + matchesKey(data, Key.escape) || + matchesKey(data, Key.ctrl('c')) || + matchesKey(data, Key.ctrl('d')) + ) { + this.cancel(); + return; + } + if (this.emptyHinted) { + this.emptyHinted = false; + } + this.input.handleInput(data); + } + + override invalidate(): void { + super.invalidate(); + this.input.invalidate(); + } + + override render(width: number): string[] { + this.input.focused = this.focused && !this.done; + + const safeWidth = Math.max(28, width); + const innerWidth = Math.max(10, safeWidth - 4); + const pad = ' '; + + const border = (s: string): string => chalk.hex(this.colors.primary)(s); + const titleStyled = chalk.bold.hex(this.colors.textStrong)(TITLE); + const subtitleText = this.emptyHinted ? SUBTITLE_EMPTY : SUBTITLE_DEFAULT; + const subtitleStyled = chalk.hex(this.colors.textDim)(subtitleText); + const footerStyled = chalk.hex(this.colors.textDim)(FOOTER); + + const titleLine = truncateToWidth(titleStyled, innerWidth, '…'); + const subtitleLine = truncateToWidth(subtitleStyled, innerWidth, '…'); + const footerLine = truncateToWidth(footerStyled, innerWidth, '…'); + const inputLine = this.input.render(innerWidth)[0] ?? '> '; + + const contentLines: string[] = [titleLine, '', subtitleLine, '', inputLine, '', footerLine]; + + const lines: string[] = [ + '', + border('╭' + '─'.repeat(safeWidth - 2) + '╮'), + border('│') + ' '.repeat(safeWidth - 2) + border('│'), + ]; + + for (const content of contentLines) { + const vis = visibleWidth(content); + const rightPad = Math.max(0, innerWidth - vis); + lines.push(border('│') + pad + content + ' '.repeat(rightPad) + border('│')); + } + + lines.push(border('│') + ' '.repeat(safeWidth - 2) + border('│')); + lines.push(border('╰' + '─'.repeat(safeWidth - 2) + '╯')); + lines.push(''); + + return lines; + } + + private submit(value: string): void { + if (this.done) return; + const trimmed = value.trim(); + if (trimmed.length === 0) { + this.emptyHinted = true; + return; + } + this.done = true; + this.onDone({ kind: 'ok', value: trimmed }); + } + + private cancel(): void { + if (this.done) return; + this.done = true; + this.onDone({ kind: 'cancel' }); + } +} diff --git a/apps/kimi-code/src/tui/components/dialogs/help-panel.ts b/apps/kimi-code/src/tui/components/dialogs/help-panel.ts new file mode 100644 index 000000000..36713a204 --- /dev/null +++ b/apps/kimi-code/src/tui/components/dialogs/help-panel.ts @@ -0,0 +1,148 @@ +/** + * HelpPanel — modal `/help` display. Lists keyboard shortcuts, slash + * commands (with aliases + descriptions) in colour-coded sections. + * + * Mirrors the container-replacement pattern used by SessionPicker / + * ApprovalPanel: host mounts the panel into `editorContainer`, picks + * it as the focused component, and tears it down on the `onClose` + * callback (fired on Esc / Enter / q). + */ + +import { + Container, + matchesKey, + Key, + decodeKittyPrintable, + type Focusable, + truncateToWidth, +} from '@earendil-works/pi-tui'; +import chalk from 'chalk'; + +import type { ColorPalette } from '#/tui/theme/colors'; + +export interface KeyboardShortcut { + readonly keys: string; + readonly description: string; +} + +export interface HelpPanelCommand { + readonly name: string; + readonly aliases: readonly string[]; + readonly description: string; +} + +/** Static list — keep in sync with the global editor bindings. */ +export const DEFAULT_KEYBOARD_SHORTCUTS: readonly KeyboardShortcut[] = [ + { keys: 'Shift-Tab', description: 'Toggle plan mode' }, + // { keys: 'Ctrl-G', description: 'Edit in external editor ($VISUAL / $EDITOR)' }, + { keys: 'Ctrl-O', description: 'Toggle tool output expansion' }, + { keys: 'Ctrl-S', description: 'Steer — inject a follow-up during streaming' }, + { keys: 'Shift-Enter', description: 'Insert newline' }, + { keys: 'Ctrl-C', description: 'Interrupt stream / clear input' }, + { keys: 'Ctrl-D', description: 'Exit (on empty input)' }, + { keys: 'Esc', description: 'Close dialogs / interrupt streaming' }, + { keys: '↑ / ↓', description: 'Browse input history' }, + { keys: 'Enter', description: 'Submit' }, +]; + +export interface HelpPanelOptions { + readonly commands: readonly HelpPanelCommand[]; + readonly shortcuts?: readonly KeyboardShortcut[]; + readonly colors: ColorPalette; + readonly onClose: () => void; + /** Terminal height — used to decide whether to show the hint tail. */ + readonly maxVisible?: number; +} + +export class HelpPanelComponent extends Container implements Focusable { + focused = false; + private readonly opts: HelpPanelOptions; + private scrollTop = 0; + + constructor(opts: HelpPanelOptions) { + super(); + this.opts = opts; + } + + handleInput(data: string): void { + const printable = decodeKittyPrintable(data) ?? data; + if ( + matchesKey(data, Key.escape) || + matchesKey(data, Key.enter) || + printable === 'q' || + printable === 'Q' + ) { + this.opts.onClose(); + return; + } + if (matchesKey(data, Key.up)) { + this.scrollTop = Math.max(0, this.scrollTop - 1); + return; + } + if (matchesKey(data, Key.down)) { + this.scrollTop += 1; // render clamps + return; + } + if (matchesKey(data, Key.pageUp)) { + this.scrollTop = Math.max(0, this.scrollTop - 10); + return; + } + if (matchesKey(data, Key.pageDown)) { + this.scrollTop += 10; + } + } + + override render(width: number): string[] { + const colors = this.opts.colors; + const accent = chalk.hex(colors.primary); + const dim = chalk.hex(colors.textDim); + const muted = chalk.hex(colors.textMuted); + const kbdColor = chalk.hex(colors.warning); + const slashColor = chalk.hex(colors.primary); + + const shortcuts = this.opts.shortcuts ?? DEFAULT_KEYBOARD_SHORTCUTS; + const kbdWidth = Math.max(8, ...shortcuts.map((s) => s.keys.length)); + const sortedCmds = [...this.opts.commands].toSorted((a, b) => a.name.localeCompare(b.name)); + const cmdLabels = sortedCmds.map((c) => { + const aliases = c.aliases.length > 0 ? ` (${c.aliases.map((a) => '/' + a).join(', ')})` : ''; + return `/${c.name}${aliases}`; + }); + const cmdWidth = Math.max(12, ...cmdLabels.map((l) => l.length)); + const lines: string[] = [ + accent('─'.repeat(width)), + accent.bold(' help ') + muted('· Esc / Enter / q to close · ↑↓ scroll'), + '', + // Greeting + ` ${dim('Sure, Kimi is ready to help! Just send a message to get started.')}`, + '', + // Section: keyboard shortcuts + ` ${chalk.bold('Keyboard shortcuts')}`, + ...shortcuts.map((s) => ` ${kbdColor(s.keys.padEnd(kbdWidth))} ${dim(s.description)}`), + '', + // Section: slash commands + ` ${chalk.bold('Slash commands')}`, + ...sortedCmds.map((cmd, i) => { + const label = cmdLabels[i] ?? `/${cmd.name}`; + return ` ${slashColor(label.padEnd(cmdWidth))} ${dim(cmd.description)}`; + }), + '', + accent('─'.repeat(width)), + ]; + + // Apply scroll windowing — keep the borders visible. + const content = lines.slice(1, lines.length - 1); + const maxVisible = Math.max(5, this.opts.maxVisible ?? 24); + if (content.length > maxVisible) { + this.scrollTop = Math.max(0, Math.min(this.scrollTop, content.length - maxVisible)); + const slice = content.slice(this.scrollTop, this.scrollTop + maxVisible); + const scrollInfo = muted( + ` showing ${String(this.scrollTop + 1)}-${String(this.scrollTop + slice.length)} of ${String(content.length)}`, + ); + return [lines[0] ?? '', ...slice, scrollInfo, lines.at(-1) ?? ''].map((line) => + truncateToWidth(line, width), + ); + } + this.scrollTop = 0; + return lines.map((line) => truncateToWidth(line, width)); + } +} diff --git a/apps/kimi-code/src/tui/components/dialogs/model-selector.ts b/apps/kimi-code/src/tui/components/dialogs/model-selector.ts new file mode 100644 index 000000000..600d0ca5a --- /dev/null +++ b/apps/kimi-code/src/tui/components/dialogs/model-selector.ts @@ -0,0 +1,184 @@ +import type { ModelAlias } from '@moonshot-ai/kimi-code-sdk'; +import { + Container, + Key, + matchesKey, + truncateToWidth, + type Focusable, +} from '@earendil-works/pi-tui'; +import chalk from 'chalk'; + +import { DEFAULT_OAUTH_PROVIDER_NAME, PRODUCT_NAME } from '#/constant/app'; +import type { ColorPalette } from '#/tui/theme/colors'; + +import type { ChoiceOption } from './choice-picker'; + +type ThinkingAvailability = 'toggle' | 'always-on' | 'unsupported'; + +interface ModelChoice { + readonly alias: string; + readonly model: ModelAlias; + readonly label: string; +} + +export interface ModelSelection { + readonly alias: string; + readonly thinking: boolean; +} + +export function modelDisplayName(alias: string, model: ModelAlias | undefined): string { + return model?.displayName ?? model?.model ?? alias; +} + +export function providerDisplayName(provider: string): string { + if (provider === DEFAULT_OAUTH_PROVIDER_NAME) return PRODUCT_NAME; + if (provider.startsWith('managed:')) return provider.slice('managed:'.length); + return provider; +} + +export function createModelChoiceOptions( + models: Record, +): readonly ChoiceOption[] { + return Object.entries(models).map(([alias, cfg]) => ({ + value: alias, + label: `${modelDisplayName(alias, cfg)} (${providerDisplayName(cfg.provider)})`, + })); +} + +export interface ModelSelectorOptions { + readonly models: Record; + readonly currentValue: string; + readonly selectedValue?: string; + readonly currentThinking: boolean; + readonly colors: ColorPalette; + readonly onSelect: (selection: ModelSelection) => void; + readonly onCancel: () => void; +} + +function createModelChoices(models: Record): readonly ModelChoice[] { + return Object.entries(models).map(([alias, cfg]) => ({ + alias, + model: cfg, + label: `${modelDisplayName(alias, cfg)} (${providerDisplayName(cfg.provider)})`, + })); +} + +function thinkingAvailability(model: ModelAlias): ThinkingAvailability { + const caps = model.capabilities ?? []; + if (caps.includes('always_thinking')) return 'always-on'; + if (caps.includes('thinking')) return 'toggle'; + return 'unsupported'; +} + +function effectiveThinking(model: ModelAlias, thinkingDraft: boolean): boolean { + const availability = thinkingAvailability(model); + if (availability === 'always-on') return true; + if (availability === 'unsupported') return false; + return thinkingDraft; +} + +export class ModelSelectorComponent extends Container implements Focusable { + focused = false; + private readonly opts: ModelSelectorOptions; + private readonly choices: readonly ModelChoice[]; + private selectedIndex: number; + private thinkingDraft: boolean; + + constructor(opts: ModelSelectorOptions) { + super(); + this.opts = opts; + this.choices = createModelChoices(opts.models); + const selectedValue = opts.selectedValue ?? opts.currentValue; + const selectedIdx = this.choices.findIndex((choice) => choice.alias === selectedValue); + this.selectedIndex = Math.max(selectedIdx, 0); + this.thinkingDraft = opts.currentThinking; + } + + handleInput(data: string): void { + if (matchesKey(data, Key.escape)) { + this.opts.onCancel(); + return; + } + if (matchesKey(data, Key.up)) { + this.selectedIndex = Math.max(0, this.selectedIndex - 1); + return; + } + if (matchesKey(data, Key.down)) { + this.selectedIndex = Math.max(0, Math.min(this.choices.length - 1, this.selectedIndex + 1)); + return; + } + const selected = this.selectedChoice(); + if (selected !== undefined && thinkingAvailability(selected.model) === 'toggle') { + if (matchesKey(data, Key.left)) { + this.thinkingDraft = true; + return; + } + if (matchesKey(data, Key.right)) { + this.thinkingDraft = false; + return; + } + } + if (matchesKey(data, Key.enter)) { + if (selected === undefined) return; + this.opts.onSelect({ + alias: selected.alias, + thinking: effectiveThinking(selected.model, this.thinkingDraft), + }); + } + } + + override render(width: number): string[] { + const { colors } = this.opts; + const lines: string[] = [ + chalk.hex(colors.primary)('─'.repeat(width)), + chalk.hex(colors.primary).bold(' Select a model'), + chalk.hex(colors.textMuted)(' ↑↓ model · ←→ thinking · Enter apply · Esc cancel'), + '', + ]; + + for (let i = 0; i < this.choices.length; i++) { + const choice = this.choices[i]!; + const isSelected = i === this.selectedIndex; + const isCurrent = choice.alias === this.opts.currentValue; + const pointer = isSelected ? '❯' : ' '; + const labelStyle = isSelected ? chalk.hex(colors.primary).bold : chalk.hex(colors.text); + let line = chalk.hex(isSelected ? colors.primary : colors.textDim)(` ${pointer} `); + line += labelStyle(choice.label); + if (isCurrent) { + line += ' ' + chalk.hex(colors.success)('← current'); + } + lines.push(line); + } + + lines.push(''); + lines.push(chalk.hex(colors.textMuted)(' Thinking')); + const selected = this.selectedChoice(); + if (selected !== undefined) { + lines.push(this.renderThinkingControl(selected.model)); + } + lines.push(''); + lines.push(chalk.hex(colors.primary)('─'.repeat(width))); + return lines.map((line) => truncateToWidth(line, width)); + } + + private selectedChoice(): ModelChoice | undefined { + return this.choices[this.selectedIndex]; + } + + private renderThinkingControl(model: ModelAlias): string { + const { colors } = this.opts; + const segment = (label: string, active: boolean): string => + active + ? chalk.hex(colors.primary).bold(`[ ${label} ]`) + : chalk.hex(colors.text)(` ${label} `); + + const availability = thinkingAvailability(model); + if (availability === 'always-on') { + return ` ${segment('Always on', true)}`; + } + if (availability === 'unsupported') { + return ` ${segment('Off', true)} ${chalk.hex(colors.textMuted)('unsupported')}`; + } + return ` ${segment('On', this.thinkingDraft)} ${segment('Off', !this.thinkingDraft)}`; + } +} diff --git a/apps/kimi-code/src/tui/components/dialogs/permission-selector.ts b/apps/kimi-code/src/tui/components/dialogs/permission-selector.ts new file mode 100644 index 000000000..6445206d3 --- /dev/null +++ b/apps/kimi-code/src/tui/components/dialogs/permission-selector.ts @@ -0,0 +1,52 @@ +import type { PermissionMode } from '@moonshot-ai/kimi-code-sdk'; + +import { ChoicePickerComponent, type ChoiceOption } from './choice-picker'; + +import type { ColorPalette } from '#/tui/theme/colors'; + +const PERMISSION_OPTIONS: readonly ChoiceOption[] = [ + { + value: 'manual', + label: 'Manual', + description: + 'Ask before commands, edits, and other risky actions. Read/search tools run directly; session approval rules are respected.', + }, + { + value: 'auto', + label: 'Auto', + description: + 'Run fully non-interactively. Tool actions are approved automatically, and agent questions are skipped so it can decide on its own.', + }, + { + value: 'yolo', + label: 'YOLO', + description: + 'Automatically approve tool actions and plan transitions. The agent can still ask you explicit questions when your input is needed.', + }, +]; + +function isPermissionModeChoice(value: string): value is PermissionMode { + return value === 'manual' || value === 'auto' || value === 'yolo'; +} + +export interface PermissionSelectorOptions { + readonly currentValue: PermissionMode; + readonly colors: ColorPalette; + readonly onSelect: (mode: PermissionMode) => void; + readonly onCancel: () => void; +} + +export class PermissionSelectorComponent extends ChoicePickerComponent { + constructor(opts: PermissionSelectorOptions) { + super({ + title: 'Select permission mode', + options: [...PERMISSION_OPTIONS], + currentValue: opts.currentValue, + colors: opts.colors, + onSelect: (value) => { + if (isPermissionModeChoice(value)) opts.onSelect(value); + }, + onCancel: opts.onCancel, + }); + } +} diff --git a/apps/kimi-code/src/tui/components/dialogs/platform-selector.ts b/apps/kimi-code/src/tui/components/dialogs/platform-selector.ts new file mode 100644 index 000000000..d01b26e50 --- /dev/null +++ b/apps/kimi-code/src/tui/components/dialogs/platform-selector.ts @@ -0,0 +1,27 @@ +import { ChoicePickerComponent, type ChoiceOption } from './choice-picker'; + +import type { ColorPalette } from '#/tui/theme/colors'; + +const PLATFORM_OPTIONS: readonly ChoiceOption[] = [ + { value: 'kimi-code', label: 'Kimi Code' }, + { value: 'moonshot-cn', label: 'Moonshot AI Open Platform (moonshot.cn)' }, + { value: 'moonshot-ai', label: 'Moonshot AI Open Platform (moonshot.ai)' }, +]; + +export interface PlatformSelectorOptions { + readonly colors: ColorPalette; + readonly onSelect: (platformId: string) => void; + readonly onCancel: () => void; +} + +export class PlatformSelectorComponent extends ChoicePickerComponent { + constructor(opts: PlatformSelectorOptions) { + super({ + title: 'Select a platform', + options: [...PLATFORM_OPTIONS], + colors: opts.colors, + onSelect: opts.onSelect, + onCancel: opts.onCancel, + }); + } +} diff --git a/apps/kimi-code/src/tui/components/dialogs/question-dialog.ts b/apps/kimi-code/src/tui/components/dialogs/question-dialog.ts new file mode 100644 index 000000000..534e2c2f2 --- /dev/null +++ b/apps/kimi-code/src/tui/components/dialogs/question-dialog.ts @@ -0,0 +1,755 @@ +/** + * QuestionDialog — pi-tui version of the structured question prompt. + * + * Each question collects an answer locally, and a final Submit tab + * reviews everything before the answers are emitted upstream. + */ + +import { + Container, + Input, + matchesKey, + Key, + decodeKittyPrintable, + type Focusable, + truncateToWidth, + visibleWidth, +} from '@earendil-works/pi-tui'; +import chalk from 'chalk'; + +import type { + PendingQuestion, + QuestionPanelResponse, + QuestionSubmissionMethod, +} from '#/tui/reverse-rpc/types'; +import type { ColorPalette } from '#/tui/theme/colors'; + +const NUMBER_KEYS = ['1', '2', '3', '4', '5', '6', '7', '8', '9']; +const MAX_BODY_LINES = 12; +const DEFAULT_OTHER_LABEL = 'Other'; +const NOT_ANSWERED_LABEL = 'Not answered'; +const REVIEW_TITLE = 'Review your answer before submit'; +const SUBMIT_PROMPT = 'Ready to submit your answers?'; +const UNANSWERED_WARNING = 'Some questions are still unanswered.'; +const SUBMIT_ACTIONS = ['Submit', 'Cancel'] as const; + +interface DisplayOption { + readonly label: string; + readonly description?: string | undefined; + readonly kind: 'preset' | 'other'; +} + +export class QuestionDialogComponent extends Container implements Focusable { + focused = false; + + private readonly request: PendingQuestion; + private readonly colors: ColorPalette; + private readonly onAnswer: (response: QuestionPanelResponse) => void; + private readonly maxVisibleOptions: number; + private readonly otherInput = new Input(); + + private currentTab = 0; + private submitActionIdx = 0; + private editingOther = false; + private reviewMessage: string | undefined; + private lastAnswerMethod: QuestionSubmissionMethod | undefined; + + /** Per-question cursor position. */ + private readonly cursors: number[]; + /** Per-question single-select choice. */ + private readonly singleSelections: (number | undefined)[]; + /** Per-question multi-select choices. */ + private readonly multiSelections: Set[]; + /** Per-question free-text drafts for the synthetic Other option. */ + private readonly otherDrafts: string[]; + /** Per-question committed Other values. */ + private readonly committedOtherValues: (string | undefined)[]; + /** Per-question derived answers used by tabs + review. */ + private readonly answers: (string | undefined)[]; + + private readonly onToggleToolOutput: (() => void) | undefined; + private readonly onTogglePlanExpand: (() => void) | undefined; + + constructor( + request: PendingQuestion, + onAnswer: (response: QuestionPanelResponse) => void, + colors: ColorPalette, + maxVisibleOptions = 6, + onToggleToolOutput?: () => void, + onTogglePlanExpand?: () => void, + ) { + super(); + this.request = request; + this.onAnswer = onAnswer; + this.colors = colors; + this.maxVisibleOptions = maxVisibleOptions; + this.onToggleToolOutput = onToggleToolOutput; + this.onTogglePlanExpand = onTogglePlanExpand; + this.otherInput.onSubmit = (value) => { + this.commitOtherInput(value, 'enter'); + }; + + const total = request.data.questions.length; + this.cursors = Array.from({ length: total }, (): number => 0); + this.singleSelections = Array.from({ length: total }, (): number | undefined => undefined); + this.multiSelections = Array.from({ length: total }, () => new Set()); + this.otherDrafts = Array.from({ length: total }, (): string => ''); + this.committedOtherValues = Array.from({ length: total }, (): string | undefined => undefined); + this.answers = Array.from({ length: total }, (): string | undefined => undefined); + } + + // ── Input ───────────────────────────────────────────────────────── + + handleInput(data: string): void { + if (matchesKey(data, Key.escape)) { + this.onAnswer({ answers: [] }); + return; + } + + if (matchesKey(data, Key.ctrl('c')) || matchesKey(data, Key.ctrl('d'))) { + this.onAnswer({ answers: [] }); + return; + } + + if (matchesKey(data, Key.ctrl('o'))) { + this.onToggleToolOutput?.(); + return; + } + + if (matchesKey(data, Key.ctrl('e'))) { + this.onTogglePlanExpand?.(); + return; + } + + if (this.isEditingOther()) { + this.handleOtherInput(data); + return; + } + + if (this.isSubmitTab()) { + this.handleSubmitInput(data); + return; + } + + const questionIdx = this.currentQuestionIndex(); + if (questionIdx === undefined) return; + const question = this.request.data.questions[questionIdx]; + if (question === undefined) return; + + const optionCount = this.displayOptions(questionIdx).length; + if (optionCount === 0) return; + + if (matchesKey(data, Key.up)) { + this.moveQuestionCursor(-1); + return; + } + if (matchesKey(data, Key.down)) { + this.moveQuestionCursor(1); + return; + } + + if (matchesKey(data, Key.left)) { + this.gotoTab(this.currentTab - 1); + return; + } + if (matchesKey(data, Key.right) || matchesKey(data, Key.tab)) { + this.gotoTab(this.currentTab + 1); + return; + } + + if (matchesKey(data, Key.enter)) { + this.activateQuestionOption(this.currentCursor(), 'enter'); + return; + } + + const printable = decodeKittyPrintable(data) ?? data; + const numIdx = NUMBER_KEYS.indexOf(printable); + if (numIdx >= 0 && numIdx < optionCount) { + this.cursors[questionIdx] = numIdx; + this.activateQuestionOption(numIdx, 'number_key'); + return; + } + + if ((printable === ' ' || matchesKey(data, Key.space)) && question.multi_select) { + this.activateQuestionOption(this.currentCursor(), 'space'); + } + } + + private handleOtherInput(data: string): void { + const questionIdx = this.currentQuestionIndex(); + if (questionIdx === undefined) return; + + if (matchesKey(data, Key.tab)) { + this.syncOtherDraft(questionIdx); + this.editingOther = false; + this.gotoTab(this.currentTab + 1); + return; + } + if (matchesKey(data, Key.up)) { + this.syncOtherDraft(questionIdx); + this.editingOther = false; + this.moveQuestionCursor(-1); + return; + } + if (matchesKey(data, Key.down)) { + this.syncOtherDraft(questionIdx); + this.editingOther = false; + this.moveQuestionCursor(1); + return; + } + + this.otherInput.handleInput(data); + this.syncOtherDraft(questionIdx); + this.reviewMessage = undefined; + } + + private handleSubmitInput(data: string): void { + if (matchesKey(data, Key.up)) { + this.submitActionIdx = + (this.submitActionIdx - 1 + SUBMIT_ACTIONS.length) % SUBMIT_ACTIONS.length; + this.reviewMessage = undefined; + return; + } + if (matchesKey(data, Key.down)) { + this.submitActionIdx = (this.submitActionIdx + 1) % SUBMIT_ACTIONS.length; + this.reviewMessage = undefined; + return; + } + + if (matchesKey(data, Key.left)) { + this.gotoTab(this.currentTab - 1); + return; + } + if (matchesKey(data, Key.right) || matchesKey(data, Key.tab)) { + this.gotoTab(this.currentTab + 1); + return; + } + + if (matchesKey(data, Key.enter)) { + this.executeSubmitAction(this.submitActionIdx, 'enter'); + return; + } + + const printable = decodeKittyPrintable(data) ?? data; + if (printable === '1') { + this.submitActionIdx = 0; + this.executeSubmitAction(0, 'number_key'); + return; + } + if (printable === '2') { + this.submitActionIdx = 1; + this.executeSubmitAction(1, 'number_key'); + } + } + + // ── State mutation ──────────────────────────────────────────────── + + private gotoTab(target: number): void { + const total = this.totalTabs(); + if (total <= 0) return; + + const wrapped = ((target % total) + total) % total; + if (wrapped === this.currentTab) return; + + this.currentTab = wrapped; + this.editingOther = false; + this.reviewMessage = undefined; + if (this.isSubmitTab()) this.submitActionIdx = 0; + } + + private moveQuestionCursor(delta: number): void { + const questionIdx = this.currentQuestionIndex(); + if (questionIdx === undefined) return; + + const total = this.displayOptions(questionIdx).length; + if (total <= 0) return; + + this.cursors[questionIdx] = (this.currentCursor() + delta + total) % total; + this.reviewMessage = undefined; + } + + private activateQuestionOption(optionIdx: number, method: QuestionSubmissionMethod): void { + const questionIdx = this.currentQuestionIndex(); + if (questionIdx === undefined) return; + + const question = this.request.data.questions[questionIdx]; + if (question === undefined) return; + + this.cursors[questionIdx] = optionIdx; + this.editingOther = false; + this.reviewMessage = undefined; + + if (this.isOtherOption(questionIdx, optionIdx)) { + this.enterOtherInput(questionIdx); + return; + } + + if (question.multi_select) { + const set = this.multiSelections[questionIdx]; + if (set === undefined) return; + if (set.has(optionIdx)) set.delete(optionIdx); + else set.add(optionIdx); + this.lastAnswerMethod = method; + this.updateAnswer(questionIdx); + return; + } + + this.singleSelections[questionIdx] = optionIdx; + this.committedOtherValues[questionIdx] = undefined; + this.lastAnswerMethod = method; + this.updateAnswer(questionIdx); + this.advanceAfterSingleSelect(questionIdx); + } + + private enterOtherInput(questionIdx: number): void { + this.cursors[questionIdx] = this.otherOptionIndex(questionIdx); + this.editingOther = true; + this.otherInput.setValue(this.otherDraftValue(questionIdx)); + this.reviewMessage = undefined; + } + + private commitOtherInput(rawValue: string | undefined, method: QuestionSubmissionMethod): void { + const questionIdx = this.currentQuestionIndex(); + if (questionIdx === undefined) return; + + const question = this.request.data.questions[questionIdx]; + if (question === undefined) return; + + const value = (rawValue ?? this.otherInput.getValue()).trim(); + if (value.length === 0) return; + + this.otherInput.setValue(value); + this.otherDrafts[questionIdx] = value; + this.committedOtherValues[questionIdx] = value; + + if (question.multi_select) { + this.multiSelections[questionIdx]?.add(this.otherOptionIndex(questionIdx)); + } else { + this.singleSelections[questionIdx] = this.otherOptionIndex(questionIdx); + } + + this.lastAnswerMethod = method; + this.updateAnswer(questionIdx); + this.editingOther = false; + this.reviewMessage = undefined; + + if (!question.multi_select) this.advanceAfterSingleSelect(questionIdx); + } + + private advanceAfterSingleSelect(questionIdx: number): void { + const next = this.findNextUnansweredAfter(questionIdx); + this.currentTab = next ?? this.submitTabIndex(); + this.reviewMessage = undefined; + if (this.isSubmitTab()) this.submitActionIdx = 0; + } + + private findNextUnansweredAfter(fromIdx: number): number | null { + const total = this.request.data.questions.length; + for (let idx = fromIdx + 1; idx < total; idx++) { + if (!this.isAnswered(idx)) return idx; + } + return null; + } + + private updateAnswer(questionIdx: number): void { + const question = this.request.data.questions[questionIdx]; + if (question === undefined) return; + + if (question.multi_select) { + const labels: string[] = []; + const set = this.multiSelections[questionIdx] ?? new Set(); + const otherIdx = this.otherOptionIndex(questionIdx); + for (let i = 0; i < question.options.length; i++) { + if (!set.has(i)) continue; + const label = question.options[i]?.label; + if (label !== undefined && label.length > 0) labels.push(label); + } + const otherText = this.committedOtherValues[questionIdx]; + if (set.has(otherIdx) && otherText !== undefined && otherText.length > 0) { + labels.push(otherText); + } + this.answers[questionIdx] = labels.length > 0 ? labels.join(', ') : undefined; + return; + } + + const selection = this.singleSelections[questionIdx]; + if (selection === undefined) { + this.answers[questionIdx] = undefined; + return; + } + + if (this.isOtherOption(questionIdx, selection)) { + const otherText = this.committedOtherValues[questionIdx]; + this.answers[questionIdx] = + otherText !== undefined && otherText.length > 0 ? otherText : undefined; + return; + } + + const label = question.options[selection]?.label; + this.answers[questionIdx] = label !== undefined && label.length > 0 ? label : undefined; + } + + private executeSubmitAction(actionIdx: number, method: QuestionSubmissionMethod): void { + if (actionIdx === 1) { + this.onAnswer({ answers: [] }); + return; + } + + this.reviewMessage = undefined; + this.emitAnswers(method); + } + + private emitAnswers(method: QuestionSubmissionMethod): void { + const out: string[] = []; + for (let i = 0; i < this.answers.length; i++) { + const answer = this.answers[i]; + if (answer !== undefined && answer.length > 0) out[i] = answer; + } + this.onAnswer({ answers: out, method: this.lastAnswerMethod ?? method }); + } + + // ── Render ──────────────────────────────────────────────────────── + + override render(width: number): string[] { + this.otherInput.focused = this.focused && this.isEditingOther(); + return this.isSubmitTab() ? this.renderSubmitTab(width) : this.renderQuestionTab(width); + } + + private renderQuestionTab(width: number): string[] { + const questionIdx = this.currentQuestionIndex(); + if (questionIdx === undefined) return this.renderSubmitTab(width); + + const question = this.request.data.questions[questionIdx]; + if (question === undefined) return []; + + const colors = this.colors; + const accent = chalk.hex(colors.primary); + const dim = chalk.hex(colors.textDim); + const success = chalk.hex(colors.success); + + const renderWidth = Math.max(1, width); + const lines: string[] = [accent('─'.repeat(renderWidth)), accent.bold(' question'), '']; + this.pushTabs(lines); + lines.push(''); + + lines.push(accent(` ? ${question.question}`)); + if (this.isEditingOther()) { + lines.push(dim(' Type your answer, then press Enter to save.')); + } + + if (question.body !== undefined && question.body.trim().length > 0) { + lines.push(''); + const bodyLines = question.body.trim().split('\n'); + const visibleBodyLines = bodyLines.slice(0, MAX_BODY_LINES); + for (const bodyLine of visibleBodyLines) { + lines.push(dim(` ${bodyLine}`)); + } + if (bodyLines.length > visibleBodyLines.length) { + lines.push(dim(` ... ${String(bodyLines.length - visibleBodyLines.length)} more lines`)); + } + } + + lines.push(''); + + const options = this.displayOptions(questionIdx); + const cursor = this.currentCursor(); + const visibleStart = this.computeVisibleStart(cursor, options.length); + const visibleEnd = Math.min(options.length, visibleStart + this.maxVisibleOptions); + const multiSet = this.multiSelections[questionIdx] ?? new Set(); + const singleSelection = this.singleSelections[questionIdx]; + + for (let i = visibleStart; i < visibleEnd; i++) { + const option = options[i]; + if (option === undefined) continue; + const num = i + 1; + const isCursor = i === cursor; + const isOther = option.kind === 'other'; + const isSelected = question.multi_select ? multiSet.has(i) : singleSelection === i; + + if (this.isEditingOther() && isCursor && isOther) { + lines.push(this.renderEditingOtherLine(renderWidth, questionIdx, option, num, isSelected)); + continue; + } + + const label = this.renderOptionLabel(questionIdx, option, isCursor); + + let line: string; + if (question.multi_select) { + const checked = isSelected ? '✓' : ' '; + const body = `[${checked}] ${label}`; + if (isSelected && isCursor) line = success.bold(` ${body}`); + else if (isSelected) line = success(` ${body}`); + else if (isCursor) line = accent(` ${body}`); + else line = dim(` ${body}`); + } else if (isSelected && this.isAnswered(questionIdx)) { + line = isCursor + ? success.bold(` → [${String(num)}] ${label}`) + : success(` [${String(num)}] ${label}`); + } else if (isCursor) { + line = accent(` → [${String(num)}] ${label}`); + } else { + line = dim(` [${String(num)}] ${label}`); + } + lines.push(line); + + if ( + option.description !== undefined && + option.description.length > 0 && + !(this.isEditingOther() && isCursor && isOther) + ) { + lines.push(dim(` ${option.description}`)); + } + } + + if (visibleEnd < options.length || visibleStart > 0) { + lines.push( + dim( + ` showing ${String(visibleStart + 1)}-${String(visibleEnd)} of ${String(options.length)}`, + ), + ); + } + + lines.push(''); + lines.push(this.buildQuestionHint(dim, questionIdx)); + lines.push(accent('─'.repeat(renderWidth))); + + return lines.map((line) => truncateToWidth(line, width)); + } + + private renderSubmitTab(width: number): string[] { + const colors = this.colors; + const accent = chalk.hex(colors.primary); + const dim = chalk.hex(colors.textDim); + const text = chalk.hex(colors.text); + const warning = chalk.hex(colors.warning); + + const renderWidth = Math.max(1, width); + const lines: string[] = [accent('─'.repeat(renderWidth)), accent.bold(' question'), '']; + this.pushTabs(lines); + lines.push(''); + lines.push(text.bold(` ${REVIEW_TITLE}`)); + const reviewWarning = + this.reviewMessage ?? (this.hasUnansweredQuestions() ? UNANSWERED_WARNING : undefined); + if (reviewWarning !== undefined) { + lines.push(warning(` ${reviewWarning}`)); + } + lines.push(''); + + for (let i = 0; i < this.request.data.questions.length; i++) { + const question = this.request.data.questions[i]; + if (question === undefined) continue; + const answer = this.answers[i]; + lines.push(` ${dim('Q')} ${question.question}`); + if (answer !== undefined && answer.length > 0) { + lines.push(` ${accent('→')} ${text(answer)}`); + } else { + lines.push(` ${dim('→')} ${dim(NOT_ANSWERED_LABEL)}`); + } + } + + lines.push(''); + lines.push(text(` ${SUBMIT_PROMPT}`)); + lines.push(''); + + for (let i = 0; i < SUBMIT_ACTIONS.length; i++) { + const label = SUBMIT_ACTIONS[i]; + if (label === undefined) continue; + const num = i + 1; + if (i === this.submitActionIdx) { + lines.push(accent(` → [${String(num)}] ${label}`)); + } else { + lines.push(dim(` [${String(num)}] ${label}`)); + } + } + + lines.push(''); + lines.push(this.buildSubmitHint(dim)); + lines.push(accent('─'.repeat(renderWidth))); + + return lines.map((line) => truncateToWidth(line, width)); + } + + private pushTabs(lines: string[]): void { + const dim = chalk.hex(this.colors.textDim); + const active = chalk.bgHex(this.colors.primary).hex(this.colors.text).bold; + + const tabs: string[] = []; + for (let i = 0; i < this.request.data.questions.length; i++) { + const question = this.request.data.questions[i]; + if (question === undefined) continue; + const label = + question.header !== undefined && question.header.length > 0 + ? question.header + : `Q${String(i + 1)}`; + if (i === this.currentTab) tabs.push(active(` ${label} `)); + else if (this.isAnswered(i)) tabs.push(chalk.hex(this.colors.success)(`(✓) ${label}`)); + else tabs.push(dim(`(○) ${label}`)); + } + + const submitLabel = 'Submit'; + if (this.isSubmitTab()) tabs.push(active(` ${submitLabel} `)); + else tabs.push(dim(` ${submitLabel} `)); + + lines.push(` ${tabs.join(' ')}`); + } + + private buildQuestionHint(dim: (s: string) => string, questionIdx: number): string { + if (this.isEditingOther()) { + const parts: string[] = [ + 'type answer', + '↵ save', + ...(this.totalTabs() > 1 ? ['tab switch'] : []), + 'esc dismiss', + ]; + return dim(` ${parts.join(' ')}`); + } + + const optionCount = Math.min(this.displayOptions(questionIdx).length, NUMBER_KEYS.length); + const numberHint = optionCount <= 1 ? '1' : `1-${String(optionCount)}`; + const question = this.request.data.questions[questionIdx]; + if (question === undefined) return dim(' esc dismiss'); + + const parts: string[] = [ + '▲/▼ select', + `${numberHint} / ↵ ${question.multi_select ? 'toggle' : 'choose'}`, + ]; + if (this.totalTabs() > 1) parts.push('←/→/tab switch'); + parts.push('esc dismiss'); + return dim(` ${parts.join(' ')}`); + } + + private buildSubmitHint(dim: (s: string) => string): string { + const parts: string[] = ['▲/▼ select', '1/2 choose', '↵ confirm']; + if (this.totalTabs() > 1) parts.push('←/→/tab switch'); + parts.push('esc dismiss'); + return dim(` ${parts.join(' ')}`); + } + + private computeVisibleStart(cursor: number, total: number): number { + if (total <= this.maxVisibleOptions) return 0; + const half = Math.floor(this.maxVisibleOptions / 2); + const max = Math.max(0, total - this.maxVisibleOptions); + return Math.max(0, Math.min(cursor - half, max)); + } + + // ── Helpers ─────────────────────────────────────────────────────── + + private totalTabs(): number { + return this.request.data.questions.length + 1; + } + + private submitTabIndex(): number { + return this.request.data.questions.length; + } + + private isSubmitTab(): boolean { + return this.currentTab === this.submitTabIndex(); + } + + private isEditingOther(): boolean { + return this.editingOther && !this.isSubmitTab(); + } + + private currentQuestionIndex(): number | undefined { + return this.isSubmitTab() ? undefined : this.currentTab; + } + + private currentCursor(): number { + const questionIdx = this.currentQuestionIndex(); + if (questionIdx === undefined) return 0; + return this.cursors[questionIdx] ?? 0; + } + + private displayOptions(questionIdx: number): DisplayOption[] { + const question = this.request.data.questions[questionIdx]; + if (question === undefined) return []; + + return [ + ...question.options.map((option) => ({ + label: option.label, + description: option.description, + kind: 'preset' as const, + })), + { + label: question.other_label?.length ? question.other_label : DEFAULT_OTHER_LABEL, + description: question.other_description?.length ? question.other_description : undefined, + kind: 'other' as const, + }, + ]; + } + + private otherOptionIndex(questionIdx: number): number { + return this.request.data.questions[questionIdx]?.options.length ?? 0; + } + + private isOtherOption(questionIdx: number, optionIdx: number): boolean { + return optionIdx === this.otherOptionIndex(questionIdx); + } + + private renderOptionLabel(questionIdx: number, option: DisplayOption, isCursor: boolean): string { + if (option.kind !== 'other') return option.label; + + const value = this.otherDraftValue(questionIdx); + if (this.isEditingOther() && isCursor) { + return `${option.label}: ${value ?? ''}█`; + } + if (value !== undefined && value.length > 0) return `${option.label}: ${value}`; + return option.label; + } + + private renderEditingOtherLine( + width: number, + questionIdx: number, + option: DisplayOption, + num: number, + isSelected: boolean, + ): string { + const question = this.request.data.questions[questionIdx]; + if (question === undefined) return option.label; + + let prefix: string; + if (question.multi_select) { + const checked = isSelected ? '✓' : ' '; + const body = ` [${checked}] ${option.label}: `; + prefix = isSelected + ? chalk.hex(this.colors.success).bold(body) + : chalk.hex(this.colors.primary)(body); + } else { + const body = ` → [${String(num)}] ${option.label}: `; + prefix = + isSelected && this.isAnswered(questionIdx) + ? chalk.hex(this.colors.success).bold(body) + : chalk.hex(this.colors.primary)(body); + } + + const inputWidth = Math.max(4, width - visibleWidth(prefix) + 2); + const inputLine = this.otherInput.render(inputWidth)[0] ?? '> '; + const inlineInput = inputLine.startsWith('> ') ? inputLine.slice(2) : inputLine; + return prefix + inlineInput; + } + + private otherDraftValue(questionIdx: number): string { + return (this.otherDrafts[questionIdx] ?? this.committedOtherValues[questionIdx]) ?? ''; + } + + private syncOtherDraft(questionIdx: number): void { + this.otherDrafts[questionIdx] = this.otherInput.getValue(); + } + + private isAnswered(questionIdx: number): boolean { + const answer = this.answers[questionIdx]; + return answer !== undefined && answer.length > 0; + } + + private hasUnansweredQuestions(): boolean { + for (let i = 0; i < this.request.data.questions.length; i++) { + if (!this.isAnswered(i)) return true; + } + return false; + } + + override invalidate(): void { + super.invalidate(); + this.otherInput.invalidate(); + } +} diff --git a/apps/kimi-code/src/tui/components/dialogs/session-picker.ts b/apps/kimi-code/src/tui/components/dialogs/session-picker.ts new file mode 100644 index 000000000..da1a139e6 --- /dev/null +++ b/apps/kimi-code/src/tui/components/dialogs/session-picker.ts @@ -0,0 +1,268 @@ +/** + * SessionPicker — pi-tui version of the session selection dialog. + */ + +import { + Container, + matchesKey, + Key, + truncateToWidth, + visibleWidth, + type Focusable, +} from '@earendil-works/pi-tui'; +import chalk from 'chalk'; + +import { formatSessionLabel } from '#/migration/index'; +import type { ColorPalette } from '#/tui/theme/colors'; + +export interface SessionRow { + readonly id: string; + readonly title: string | null; + readonly last_prompt?: string | null; + readonly work_dir: string; + readonly updated_at: number; + readonly metadata?: Readonly> | undefined; +} + +const ELLIPSIS = '…'; +const CURRENT_BADGE = '(current)'; + +function formatRelativeTime(ts: number): string { + // SessionSummary timestamps come from filesystem stat `*timeMs`, + // so they use the same millisecond unit as `Date.now()`. + if (!Number.isFinite(ts) || ts <= 0) return ''; + const diffSec = Math.floor(Math.max(0, Date.now() - ts) / 1000); + if (diffSec < 60) return 'just now'; + const minutes = Math.floor(diffSec / 60); + if (minutes < 60) return `${String(minutes)}m ago`; + const hours = Math.floor(minutes / 60); + if (hours < 24) return `${String(hours)}h ago`; + const days = Math.floor(hours / 24); + return `${String(days)}d ago`; +} + +function homeAlias(path: string): string { + const home = process.env['HOME'] ?? ''; + if (home && path.startsWith(home)) return '~' + path.slice(home.length); + return path; +} + +// Truncates from the LEFT (keeps the tail), prefixing an ellipsis when clipped. +// Paths typically carry the relevant info near the end, so we drop the prefix. +function truncatePathLeft(path: string, maxWidth: number): string { + if (maxWidth <= 0) return ''; + if (visibleWidth(path) <= maxWidth) return path; + if (maxWidth === 1) return ELLIPSIS; + // Walk graphemes from the end accumulating width, keep the longest tail + // whose width + ellipsis fits. + const segmenter = new Intl.Segmenter(undefined, { granularity: 'grapheme' }); + const segments = [...segmenter.segment(path)].map((s) => s.segment); + let used = 0; + const budget = maxWidth - 1; // reserve 1 column for ellipsis + let i = segments.length - 1; + while (i >= 0) { + const seg = segments[i]; + if (seg === undefined) break; + const w = visibleWidth(seg); + if (used + w > budget) break; + used += w; + i--; + } + return ELLIPSIS + segments.slice(i + 1).join(''); +} + +function singleLine(text: string): string { + return text.replaceAll(/\s+/g, ' ').trim(); +} + +export class SessionPickerComponent extends Container implements Focusable { + private sessions: SessionRow[]; + private currentSessionId: string; + private colors: ColorPalette; + private onSelect: (sessionId: string) => void; + private onCancel: () => void; + private maxVisibleSessions: number; + private loading: boolean; + + focused = false; + private selectedIndex = 0; + + constructor(opts: { + sessions: SessionRow[]; + loading: boolean; + currentSessionId: string; + colors: ColorPalette; + onSelect: (sessionId: string) => void; + onCancel: () => void; + maxVisibleSessions?: number; + }) { + super(); + this.sessions = opts.sessions; + this.loading = opts.loading; + this.currentSessionId = opts.currentSessionId; + this.colors = opts.colors; + this.onSelect = opts.onSelect; + this.onCancel = opts.onCancel; + this.maxVisibleSessions = opts.maxVisibleSessions ?? 4; + } + + handleInput(data: string): void { + if (matchesKey(data, Key.escape)) { + this.onCancel(); + return; + } + if (matchesKey(data, Key.enter) && this.sessions.length > 0) { + const session = this.sessions[this.selectedIndex]; + if (session) this.onSelect(session.id); + return; + } + if (matchesKey(data, Key.up)) { + this.selectedIndex = Math.max(0, this.selectedIndex - 1); + return; + } + if (matchesKey(data, Key.down)) { + this.selectedIndex = Math.min(this.sessions.length - 1, this.selectedIndex + 1); + } + } + + override render(width: number): string[] { + const colors = this.colors; + const lines: string[] = [chalk.hex(colors.primary)('─'.repeat(width))]; + + if (this.loading) { + lines.push(chalk.hex(colors.primary).bold(truncateToWidth('Sessions', width, ELLIPSIS))); + lines.push( + chalk.hex(colors.textMuted)(truncateToWidth('Loading sessions...', width, ELLIPSIS)), + ); + lines.push(chalk.hex(colors.primary)('─'.repeat(width))); + return lines; + } + + if (this.sessions.length === 0) { + lines.push(chalk.hex(colors.primary).bold(truncateToWidth('Sessions', width, ELLIPSIS))); + lines.push( + chalk.hex(colors.textMuted)( + truncateToWidth('No sessions found. Press Escape to close.', width, ELLIPSIS), + ), + ); + lines.push(chalk.hex(colors.primary)('─'.repeat(width))); + return lines; + } + + const headerLabel = 'Sessions '; + const headerHint = '(↑↓ navigate, Enter select, Esc cancel)'; + const labelWidth = visibleWidth(headerLabel); + const hintBudget = Math.max(0, width - labelWidth); + const shownHint = truncateToWidth(headerHint, hintBudget, ELLIPSIS); + lines.push( + chalk.hex(colors.primary).bold(headerLabel) + chalk.hex(colors.textMuted)(shownHint), + ); + lines.push(''); + + const visibleStart = Math.max( + 0, + Math.min( + this.selectedIndex - Math.floor(this.maxVisibleSessions / 2), + Math.max(0, this.sessions.length - this.maxVisibleSessions), + ), + ); + const visibleSessions = this.sessions.slice( + visibleStart, + visibleStart + this.maxVisibleSessions, + ); + + for (const [vi, session] of visibleSessions.entries()) { + const index = visibleStart + vi; + const isSelected = index === this.selectedIndex; + const isCurrent = session.id === this.currentSessionId; + const card = this.renderSessionCard(width, session, isSelected, isCurrent); + lines.push(...card); + if (vi < visibleSessions.length - 1) lines.push(''); + } + + if (this.sessions.length > visibleSessions.length) { + lines.push(''); + const footer = `Showing ${String(visibleStart + 1)}-${String(visibleStart + visibleSessions.length)} of ${String(this.sessions.length)} sessions`; + lines.push(chalk.hex(colors.textMuted)(truncateToWidth(footer, width, ELLIPSIS))); + } + + lines.push(chalk.hex(colors.primary)('─'.repeat(width))); + return lines; + } + + private renderSessionCard( + width: number, + session: SessionRow, + isSelected: boolean, + isCurrent: boolean, + ): string[] { + const colors = this.colors; + const pointer = isSelected ? '❯' : ' '; + const indent = ' '; + const indentWidth = visibleWidth(indent); + const titleColor = isSelected ? colors.primary : colors.text; + const titleStyle = isSelected ? chalk.hex(titleColor).bold : chalk.hex(titleColor); + + const time = formatRelativeTime(session.updated_at); + const badge = isCurrent ? CURRENT_BADGE : ''; + const rawTitle = (session.title ?? session.id).trim() || session.id; + const titleSource = formatSessionLabel({ title: rawTitle, metadata: session.metadata }); + + // Inline trailing parts after the title: " <time> (current)". + const trailingParts = [time, badge].filter((p) => p.length > 0); + const trailingText = trailingParts.length > 0 ? ' ' + trailingParts.join(' ') : ''; + const trailingWidth = visibleWidth(trailingText); + const headerPrefixWidth = visibleWidth(pointer) + 1; // pointer + space + const titleBudget = Math.max(8, width - headerPrefixWidth - trailingWidth); + const shownTitle = truncateToWidth(singleLine(titleSource), titleBudget, ELLIPSIS); + + let header = chalk.hex(isSelected ? colors.primary : colors.textDim)(pointer + ' '); + header += titleStyle(shownTitle); + if (time.length > 0) header += ' ' + chalk.hex(colors.textDim)(time); + if (badge.length > 0) header += ' ' + chalk.hex(colors.success)(badge); + const card: string[] = [header]; + + // Session id is rendered in full (no truncation). The directory wraps to + // its own line if it would push past the terminal edge. + const fullId = session.id; + const idWidth = visibleWidth(fullId); + const metaGap = ' '; + const metaGapWidth = visibleWidth(metaGap); + const idLineWidth = indentWidth + idWidth; + const aliasedDir = homeAlias(session.work_dir); + const dirWidth = visibleWidth(aliasedDir); + + if (idLineWidth + metaGapWidth + dirWidth <= width) { + card.push( + indent + + chalk.hex(colors.textMuted)(fullId) + + chalk.hex(colors.textDim)(metaGap) + + chalk.hex(colors.textMuted)(aliasedDir), + ); + } else { + // Not enough room for both on one line — keep the id intact and put the + // directory on the next line (left-truncated only if it still doesn't fit). + card.push( + indent + + chalk.hex(colors.textMuted)( + truncateToWidth(fullId, Math.max(idWidth, width - indentWidth), ELLIPSIS), + ), + ); + const dirBudget = Math.max(8, width - indentWidth); + const dir = truncatePathLeft(aliasedDir, dirBudget); + card.push(indent + chalk.hex(colors.textMuted)(dir)); + } + + const rawPrompt = session.last_prompt?.trim(); + if (rawPrompt && rawPrompt.length > 0) { + const promptMarker = '› '; + const promptMarkerWidth = visibleWidth(promptMarker); + const promptBudget = Math.max(8, width - indentWidth - promptMarkerWidth); + const promptText = truncateToWidth(singleLine(rawPrompt), promptBudget, ELLIPSIS); + const promptLine = indent + chalk.hex(colors.textDim)(promptMarker + promptText); + card.push(promptLine); + } + + return card; + } +} diff --git a/apps/kimi-code/src/tui/components/dialogs/settings-selector.ts b/apps/kimi-code/src/tui/components/dialogs/settings-selector.ts new file mode 100644 index 000000000..fb224345b --- /dev/null +++ b/apps/kimi-code/src/tui/components/dialogs/settings-selector.ts @@ -0,0 +1,63 @@ +import { ChoicePickerComponent, type ChoiceOption } from './choice-picker'; + +import type { ColorPalette } from '#/tui/theme/colors'; + +export type SettingsSelection = 'model' | 'theme' | 'editor' | 'permission' | 'usage'; + +const SETTINGS_OPTIONS: readonly ChoiceOption[] = [ + { + value: 'model', + label: 'Model', + description: 'Switch the active model and thinking mode.', + }, + { + value: 'permission', + label: 'Permission', + description: 'Choose how tool actions are approved.', + }, + { + value: 'theme', + label: 'Theme', + description: 'Change the terminal UI theme.', + }, + { + value: 'editor', + label: 'Editor', + description: 'Set the external editor command.', + }, + { + value: 'usage', + label: 'Usage', + description: 'Show session tokens, context window, and plan quotas.', + }, +]; + +function isSettingsSelection(value: string): value is SettingsSelection { + return ( + value === 'model' || + value === 'theme' || + value === 'editor' || + value === 'permission' || + value === 'usage' + ); +} + +export interface SettingsSelectorOptions { + readonly colors: ColorPalette; + readonly onSelect: (value: SettingsSelection) => void; + readonly onCancel: () => void; +} + +export class SettingsSelectorComponent extends ChoicePickerComponent { + constructor(opts: SettingsSelectorOptions) { + super({ + title: 'Settings', + options: [...SETTINGS_OPTIONS], + colors: opts.colors, + onSelect: (value) => { + if (isSettingsSelection(value)) opts.onSelect(value); + }, + onCancel: opts.onCancel, + }); + } +} diff --git a/apps/kimi-code/src/tui/components/dialogs/task-output-viewer.ts b/apps/kimi-code/src/tui/components/dialogs/task-output-viewer.ts new file mode 100644 index 000000000..0f1fa04b7 --- /dev/null +++ b/apps/kimi-code/src/tui/components/dialogs/task-output-viewer.ts @@ -0,0 +1,261 @@ +/** + * TaskOutputViewer — full-screen pi-tui rendered output viewer for + * a single background task. Replaces the previous "shell out to less" + * approach so the experience stays inside the TUI: same colors, same + * fonts, same redraw cycle, no alt-screen flip-flop. + * + * Mounted by `kimi-tui.ts` via nested container swap on top of the + * TasksBrowserApp. Snapshot view (no live tail) — content is fetched + * once when the viewer opens. + */ + +import { + Container, + Key, + matchesKey, + type Terminal, + truncateToWidth, + visibleWidth, + type Focusable, +} from '@earendil-works/pi-tui'; +import type { BackgroundTaskInfo, BackgroundTaskStatus } from '@moonshot-ai/kimi-code-sdk'; +import chalk from 'chalk'; + +import type { ColorPalette } from '@/tui/theme/colors'; +import { printableChar } from '@/tui/utils/printable-key'; + +const ELLIPSIS = '…'; + +export interface TaskOutputViewerProps { + readonly taskId: string; + readonly info: BackgroundTaskInfo | undefined; + readonly output: string; + readonly colors: ColorPalette; + readonly onClose: () => void; +} + +const STATUS_LABEL: Record<BackgroundTaskStatus, string> = { + running: 'running', + awaiting_approval: 'awaiting', + completed: 'completed', + failed: 'failed', + killed: 'killed', + lost: 'lost', +}; + +function statusColor(colors: ColorPalette, status: BackgroundTaskStatus): string { + switch (status) { + case 'running': + return colors.success; + case 'awaiting_approval': + return colors.warning; + case 'completed': + return colors.textMuted; + case 'failed': + case 'killed': + case 'lost': + return colors.error; + } +} + +function padToWidth(line: string, width: number): string { + const w = visibleWidth(line); + if (w === width) return line; + if (w > width) return truncateToWidth(line, width, ELLIPSIS); + return line + ' '.repeat(width - w); +} + +function fitExactly(line: string, width: number): string { + let s = line; + if (visibleWidth(s) > width) s = truncateToWidth(s, width, ELLIPSIS); + return padToWidth(s, width); +} + +export class TaskOutputViewer extends Container implements Focusable { + focused = false; + + private props: TaskOutputViewerProps; + private readonly terminal: Terminal; + /** Output split on '\n'. Replaced on `setProps` when `output` changes. */ + private lines: string[]; + /** Index of the topmost visible line. */ + private scrollTop = 0; + + constructor(props: TaskOutputViewerProps, terminal: Terminal) { + super(); + this.props = props; + this.terminal = terminal; + this.lines = this.splitOutput(props.output); + } + + /** + * Update viewer props. When `output` grows (the watched task wrote + * new content), follow the tail like `less +F` if the user is parked + * at the bottom; otherwise keep the user's current scroll position + * so they can read history without being yanked around. + */ + setProps(next: TaskOutputViewerProps): void { + const previousOutput = this.props.output; + const wasAtBottom = this.scrollTop >= this.maxScroll(); + this.props = next; + if (next.output !== previousOutput) { + this.lines = this.splitOutput(next.output); + if (wasAtBottom) this.scrollTop = this.maxScroll(); + else this.scrollTop = Math.min(this.scrollTop, this.maxScroll()); + } + this.invalidate(); + } + + private splitOutput(output: string): string[] { + return (output.length > 0 ? output : '[no output captured]').split('\n'); + } + + // ── input ────────────────────────────────────────────────────────── + + handleInput(data: string): void { + const visible = this.viewableRows(); + const k = printableChar(data); + + if (matchesKey(data, Key.escape) || k === 'q' || k === 'Q') { + this.props.onClose(); + return; + } + if (matchesKey(data, Key.up) || k === 'k') { + this.scrollBy(-1); + return; + } + if (matchesKey(data, Key.down) || k === 'j') { + this.scrollBy(1); + return; + } + if (matchesKey(data, Key.pageUp) || k === ' ' || data === '\u0002' /* C-b */) { + this.scrollBy(-Math.max(1, visible - 1)); + return; + } + if (matchesKey(data, Key.pageDown) || data === '\u0006' /* C-f */) { + this.scrollBy(Math.max(1, visible - 1)); + return; + } + if (matchesKey(data, Key.home) || k === 'g') { + this.scrollTo(0); + return; + } + if (matchesKey(data, Key.end) || k === 'G') { + this.scrollTo(this.maxScroll()); + return; + } + } + + private scrollBy(delta: number): void { + this.scrollTo(this.scrollTop + delta); + } + + private scrollTo(target: number): void { + this.scrollTop = Math.max(0, Math.min(target, this.maxScroll())); + this.invalidate(); + } + + private maxScroll(): number { + return Math.max(0, this.lines.length - this.viewableRows()); + } + + /** + * Number of content rows visible inside the body frame: total terminal + * rows minus header(1) + footer(1) + top border(1) + bottom border(1). + */ + private viewableRows(): number { + return Math.max(1, this.terminal.rows - 4); + } + + // ── render ───────────────────────────────────────────────────────── + + override render(width: number): string[] { + const rows = Math.max(3, this.terminal.rows); + const bodyHeight = rows - 2; + + const header = this.renderHeader(width); + const body = this.renderBody(width, bodyHeight); + const footer = this.renderFooter(width, bodyHeight); + + const out: string[] = [header]; + for (const line of body) out.push(line); + out.push(footer); + return out; + } + + private renderHeader(width: number): string { + const colors = this.props.colors; + const title = chalk.hex(colors.primary).bold(' Task output '); + const id = chalk.hex(colors.text).bold(this.props.taskId); + const info = this.props.info; + const segments: string[] = []; + if (info !== undefined) { + segments.push(chalk.hex(statusColor(colors, info.status))(STATUS_LABEL[info.status])); + if (info.exitCode !== null && info.exitCode !== undefined) { + segments.push(chalk.hex(colors.textMuted)(`exit ${String(info.exitCode)}`)); + } + if (info.description && info.description.length > 0) { + segments.push(chalk.hex(colors.textMuted)(info.description)); + } + } + const composed = title + id + (segments.length > 0 ? ' ' + segments.join(' ') : ''); + return fitExactly(composed, width); + } + + private renderBody(width: number, bodyHeight: number): string[] { + const colors = this.props.colors; + const stroke = colors.primary; + + // Reserve 1 col for left/right border each, 1 col for left padding. + const innerWidth = Math.max(1, width - 4); + + // Re-clamp scroll in case the terminal got resized smaller. + const max = this.maxScroll(); + if (this.scrollTop > max) this.scrollTop = max; + if (this.scrollTop < 0) this.scrollTop = 0; + + const viewRows = bodyHeight - 2; // inside top + bottom border + const top = chalk.hex(stroke)('┌' + '─'.repeat(Math.max(0, width - 2)) + '┐'); + const bottom = chalk.hex(stroke)('└' + '─'.repeat(Math.max(0, width - 2)) + '┘'); + + const out: string[] = [top]; + for (let i = 0; i < viewRows; i++) { + const lineIndex = this.scrollTop + i; + const raw = this.lines[lineIndex] ?? ''; + const inner = fitExactly(chalk.hex(colors.text)(raw), innerWidth); + out.push(chalk.hex(stroke)('│ ') + inner + chalk.hex(stroke)(' │')); + } + out.push(bottom); + return out; + } + + private renderFooter(width: number, bodyHeight: number): string { + const colors = this.props.colors; + const key = (text: string): string => chalk.hex(colors.primary).bold(text); + const dim = (text: string): string => chalk.hex(colors.textMuted)(text); + + const total = this.lines.length; + const viewRows = Math.max(1, bodyHeight - 2); + const maxScroll = Math.max(0, total - viewRows); + const percent = + maxScroll === 0 ? 100 : Math.round((this.scrollTop / maxScroll) * 100); + const lineFrom = this.scrollTop + 1; + const lineTo = Math.min(total, this.scrollTop + viewRows); + + const position = chalk.hex(colors.textMuted)( + ` ${String(lineFrom)}-${String(lineTo)} / ${String(total)} (${String(percent)}%) `, + ); + const keys = + `${key('↑↓')} ${dim('line')} ` + + `${key('PgUp/PgDn')} ${dim('page')} ` + + `${key('g/G')} ${dim('top/bot')} ` + + `${key('Q/Esc')} ${dim('back')}`; + const left = ` ${keys}`; + const leftW = visibleWidth(left); + const rightW = visibleWidth(position); + if (leftW + 2 + rightW <= width) { + return left + ' '.repeat(width - leftW - rightW) + position; + } + return fitExactly(left, width); + } +} diff --git a/apps/kimi-code/src/tui/components/dialogs/tasks-browser.ts b/apps/kimi-code/src/tui/components/dialogs/tasks-browser.ts new file mode 100644 index 000000000..8634e045d --- /dev/null +++ b/apps/kimi-code/src/tui/components/dialogs/tasks-browser.ts @@ -0,0 +1,603 @@ +/** + * TasksBrowserApp — full-screen alt-screen takeover for browsing + * background tasks. Three-pane layout (left task list, right top + * detail, right bottom preview output) framed by a header row and + * footer key hint. + * + * Mounted by `kimi-tui.ts` via container swap rather than `showOverlay` + * — the main TUI's children are saved, cleared, and this component is + * added as the sole child so it covers the entire screen. The + * controller restores the children when the user exits. + * + * Data (tasks list, tail output) flows in via `setProps`; user actions + * fire the `on*` callbacks back to the controller. + */ + +import { + Container, + Key, + matchesKey, + type Terminal, + truncateToWidth, + visibleWidth, + type Focusable, +} from '@earendil-works/pi-tui'; +import type { BackgroundTaskInfo, BackgroundTaskStatus } from '@moonshot-ai/kimi-code-sdk'; +import chalk from 'chalk'; + +import type { ColorPalette } from '@/tui/theme/colors'; +import { printableChar } from '@/tui/utils/printable-key'; + +const ELLIPSIS = '…'; + +export type TasksFilter = 'all' | 'active'; + +export interface TasksBrowserProps { + readonly tasks: readonly BackgroundTaskInfo[]; + readonly filter: TasksFilter; + readonly selectedTaskId: string | undefined; + readonly tailOutput: string | undefined; + readonly tailLoading: boolean; + readonly flashMessage: string | undefined; + readonly colors: ColorPalette; + readonly onSelect: (taskId: string) => void; + readonly onToggleFilter: () => void; + readonly onRefresh: () => void; + readonly onCancel: () => void; + /** Fired when the user confirms a stop request via the inline `y` prompt. */ + readonly onStopConfirmed: (taskId: string) => void; + /** Fired when the user presses Enter or O on a selected task. */ + readonly onOpenOutput: (taskId: string) => void; + /** Fired when stop is requested on a task that cannot be stopped. */ + readonly onStopIgnored?: (taskId: string, reason: 'terminal') => void; +} + +const STATUS_LABEL: Record<BackgroundTaskStatus, string> = { + running: 'running', + awaiting_approval: 'awaiting', + completed: 'completed', + failed: 'failed', + killed: 'killed', + lost: 'lost', +}; + +/** Auto-cancel the inline stop confirmation after this many ms. */ +const STOP_CONFIRM_TIMEOUT_MS = 5_000; + +/** Minimum dimensions before we just print a "too small" message. */ +const MIN_WIDTH = 48; +const MIN_HEIGHT = 10; + +/** Hard caps so a tiny / huge terminal still gets a sensible left-column width. */ +const LIST_COL_MIN = 28; +const LIST_COL_MAX = 44; +const LIST_COL_RATIO = 0.32; + +function statusColor(colors: ColorPalette, status: BackgroundTaskStatus): string { + switch (status) { + case 'running': + return colors.success; + case 'awaiting_approval': + return colors.warning; + case 'completed': + return colors.textMuted; + case 'failed': + case 'killed': + case 'lost': + return colors.error; + } +} + +function isTerminal(status: BackgroundTaskStatus): boolean { + return ( + status === 'completed' || status === 'failed' || status === 'killed' || status === 'lost' + ); +} + +function formatRelativeTime(ts: number | null | undefined): string { + if (ts === null || ts === undefined || !Number.isFinite(ts) || ts <= 0) return ''; + const diffSec = Math.floor(Math.max(0, Date.now() - ts) / 1000); + if (diffSec < 60) return 'just now'; + const minutes = Math.floor(diffSec / 60); + if (minutes < 60) return `${String(minutes)}m ago`; + const hours = Math.floor(minutes / 60); + if (hours < 24) return `${String(hours)}h ago`; + const days = Math.floor(hours / 24); + return `${String(days)}d ago`; +} + +function singleLine(text: string): string { + return text.replaceAll(/\s+/g, ' ').trim(); +} + +function padToWidth(line: string, width: number): string { + const w = visibleWidth(line); + if (w === width) return line; + if (w > width) return truncateToWidth(line, width, ELLIPSIS); + return line + ' '.repeat(width - w); +} + +/** Fit `line` into exactly `width` columns, even after CJK-edge truncation. */ +function fitExactly(line: string, width: number): string { + let s = line; + if (visibleWidth(s) > width) s = truncateToWidth(s, width, ELLIPSIS); + return padToWidth(s, width); +} + +function visibleTasks( + tasks: readonly BackgroundTaskInfo[], + filter: TasksFilter, +): BackgroundTaskInfo[] { + if (filter === 'all') return [...tasks]; + return tasks.filter((t) => !isTerminal(t.status)); +} + +function compareTasks(a: BackgroundTaskInfo, b: BackgroundTaskInfo): number { + const aTerminal = isTerminal(a.status); + const bTerminal = isTerminal(b.status); + if (aTerminal !== bTerminal) return aTerminal ? 1 : -1; + if (!aTerminal) return a.startedAt - b.startedAt; + return (b.endedAt ?? b.startedAt) - (a.endedAt ?? a.startedAt); +} + +interface StatusCounts { + running: number; + awaiting: number; + completed: number; + terminalFailed: number; +} + +function countByStatus(tasks: readonly BackgroundTaskInfo[]): StatusCounts { + const counts: StatusCounts = { running: 0, awaiting: 0, completed: 0, terminalFailed: 0 }; + for (const t of tasks) { + switch (t.status) { + case 'running': + counts.running += 1; + break; + case 'awaiting_approval': + counts.awaiting += 1; + break; + case 'completed': + counts.completed += 1; + break; + case 'failed': + case 'killed': + case 'lost': + counts.terminalFailed += 1; + break; + } + } + return counts; +} + +export class TasksBrowserApp extends Container implements Focusable { + focused = false; + + private props: TasksBrowserProps; + private readonly terminal: Terminal; + private sortedVisible: BackgroundTaskInfo[]; + private selectedIndex = 0; + private listScroll = 0; + private pendingStopTaskId: string | undefined = undefined; + private pendingStopTimer: NodeJS.Timeout | undefined = undefined; + + constructor(props: TasksBrowserProps, terminal: Terminal) { + super(); + this.props = props; + this.terminal = terminal; + this.sortedVisible = visibleTasks(props.tasks, props.filter).toSorted(compareTasks); + this.syncSelectionFromProps(); + } + + setProps(next: TasksBrowserProps): void { + this.props = next; + this.sortedVisible = visibleTasks(next.tasks, next.filter).toSorted(compareTasks); + this.syncSelectionFromProps(); + if (this.pendingStopTaskId !== undefined) { + const task = next.tasks.find((t) => t.taskId === this.pendingStopTaskId); + if (task === undefined || isTerminal(task.status)) this.clearPendingStop(); + } + this.invalidate(); + } + + private syncSelectionFromProps(): void { + if (this.sortedVisible.length === 0) { + this.selectedIndex = 0; + this.listScroll = 0; + return; + } + if (this.props.selectedTaskId !== undefined) { + const idx = this.sortedVisible.findIndex((t) => t.taskId === this.props.selectedTaskId); + if (idx !== -1) { + this.selectedIndex = idx; + return; + } + } + if (this.selectedIndex >= this.sortedVisible.length) { + this.selectedIndex = this.sortedVisible.length - 1; + } + } + + private clearPendingStop(): void { + this.pendingStopTaskId = undefined; + if (this.pendingStopTimer !== undefined) { + clearTimeout(this.pendingStopTimer); + this.pendingStopTimer = undefined; + } + } + + private emitSelect(): void { + const task = this.sortedVisible[this.selectedIndex]; + if (task) this.props.onSelect(task.taskId); + } + + handleInput(data: string): void { + const k = printableChar(data); + + if (this.pendingStopTaskId !== undefined) { + if (k === 'y' || k === 'Y') { + const taskId = this.pendingStopTaskId; + this.clearPendingStop(); + this.props.onStopConfirmed(taskId); + this.invalidate(); + return; + } + this.clearPendingStop(); + this.invalidate(); + return; + } + + if (matchesKey(data, Key.escape) || k === 'q' || k === 'Q') { + this.props.onCancel(); + return; + } + if (matchesKey(data, Key.up) || k === 'k') { + if (this.sortedVisible.length === 0) return; + this.selectedIndex = Math.max(0, this.selectedIndex - 1); + this.emitSelect(); + this.invalidate(); + return; + } + if (matchesKey(data, Key.down) || k === 'j') { + if (this.sortedVisible.length === 0) return; + this.selectedIndex = Math.min(this.sortedVisible.length - 1, this.selectedIndex + 1); + this.emitSelect(); + this.invalidate(); + return; + } + if (matchesKey(data, Key.tab) || k === '\t') { + this.props.onToggleFilter(); + return; + } + if (k === 'r' || k === 'R') { + this.props.onRefresh(); + return; + } + if (k === 's' || k === 'S') { + const task = this.sortedVisible[this.selectedIndex]; + if (task === undefined) return; + if (isTerminal(task.status)) { + this.props.onStopIgnored?.(task.taskId, 'terminal'); + return; + } + this.pendingStopTaskId = task.taskId; + this.pendingStopTimer = setTimeout(() => { + this.clearPendingStop(); + this.invalidate(); + }, STOP_CONFIRM_TIMEOUT_MS); + this.invalidate(); + return; + } + if (k === 'o' || k === 'O' || matchesKey(data, Key.enter)) { + const task = this.sortedVisible[this.selectedIndex]; + if (task) this.props.onOpenOutput(task.taskId); + return; + } + } + + /** + * Render the entire screen as `terminal.rows` lines of `width` cols. + * Layout: header(1) + body(rows-2) + footer(1). + */ + override render(width: number): string[] { + const rows = Math.max(1, this.terminal.rows); + if (width < MIN_WIDTH || rows < MIN_HEIGHT) { + return this.renderTooSmall(width, rows); + } + + const header = this.renderHeader(width); + const footer = this.renderFooter(width); + const bodyHeight = rows - 2; + + const listWidth = Math.max( + LIST_COL_MIN, + Math.min(LIST_COL_MAX, Math.floor(width * LIST_COL_RATIO)), + ); + const rightWidth = width - listWidth; + + const listFrame = this.renderListFrame(listWidth, bodyHeight); + const rightFrames = this.renderRightStack(rightWidth, bodyHeight); + + const lines: string[] = [header]; + for (let i = 0; i < bodyHeight; i++) { + lines.push((listFrame[i] ?? ' '.repeat(listWidth)) + (rightFrames[i] ?? ' '.repeat(rightWidth))); + } + lines.push(footer); + return lines; + } + + // ── header / footer ────────────────────────────────────────────────── + + private renderHeader(width: number): string { + const colors = this.props.colors; + const title = chalk.hex(colors.primary).bold(' TASK BROWSER '); + const filterText = chalk.hex(colors.textMuted)( + ` filter=${this.props.filter === 'all' ? 'ALL' : 'ACTIVE'} `, + ); + const counts = countByStatus(this.props.tasks); + const countSegments: string[] = []; + if (counts.running > 0) + countSegments.push(chalk.hex(colors.success)(` ${String(counts.running)} running `)); + if (counts.awaiting > 0) + countSegments.push(chalk.hex(colors.warning)(` ${String(counts.awaiting)} awaiting `)); + if (counts.completed > 0) + countSegments.push(chalk.hex(colors.textDim)(` ${String(counts.completed)} completed `)); + if (counts.terminalFailed > 0) + countSegments.push( + chalk.hex(colors.error)(` ${String(counts.terminalFailed)} interrupted `), + ); + const totals = chalk.hex(colors.textMuted)(` ${String(this.props.tasks.length)} total `); + + const composed = title + filterText + countSegments.join('') + totals; + return fitExactly(composed, width); + } + + private renderFooter(width: number): string { + const colors = this.props.colors; + const key = (text: string): string => chalk.hex(colors.primary).bold(text); + const dim = (text: string): string => chalk.hex(colors.textMuted)(text); + + if (this.pendingStopTaskId !== undefined) { + const warn = (text: string): string => chalk.hex(colors.warning).bold(text); + const line = + ` ${warn('Stop')} ${chalk.hex(colors.text)(this.pendingStopTaskId)}? ` + + `${key('Y')} ${dim('confirm')} ${key('N')} ${dim('cancel')} `; + return fitExactly(line, width); + } + + const parts = [ + ` ${key('↑↓')} ${dim('select')}`, + `${key('Enter/O')} ${dim('output')}`, + `${key('S')} ${dim('stop')}`, + `${key('R')} ${dim('refresh')}`, + `${key('Tab')} ${dim('filter')}`, + `${key('Q/Esc')} ${dim('exit')} `, + ]; + const left = parts.join(' '); + const flash = this.props.flashMessage; + if (flash !== undefined && flash.length > 0) { + const flashStyled = chalk.hex(colors.warning)(` ${flash} `); + const total = visibleWidth(left) + visibleWidth(flashStyled); + if (total <= width) { + return left + ' '.repeat(width - total) + flashStyled; + } + } + return fitExactly(left, width); + } + + // ── frame primitive ────────────────────────────────────────────────── + + /** + * Render a framed box: `┌─ Title ─┐` top, `│ <content> │` sides, `└─┘` + * bottom. Result is exactly `width × height` cells. `content` is a + * pre-rendered array of inner-width-sized lines; extra rows are padded. + */ + private renderFrame( + title: string, + content: readonly string[], + width: number, + height: number, + ): string[] { + if (height < 2 || width < 4) { + const out: string[] = []; + for (let i = 0; i < height; i++) out.push(' '.repeat(width)); + return out; + } + const stroke = this.props.colors.primary; + const innerWidth = width - 2; + const innerHeight = height - 2; + + const titleStyled = chalk.hex(this.props.colors.textStrong).bold(title); + const titleWidth = visibleWidth(titleStyled); + const titleSegment = `─ ${titleStyled} `; + const titleSegmentWidth = visibleWidth(titleSegment); + const remainingDashes = Math.max(0, innerWidth - titleSegmentWidth); + const topMid = + titleWidth > 0 && titleSegmentWidth <= innerWidth + ? chalk.hex(stroke)('─ ') + + titleStyled + + ' ' + + chalk.hex(stroke)('─'.repeat(remainingDashes)) + : chalk.hex(stroke)('─'.repeat(innerWidth)); + const top = chalk.hex(stroke)('┌') + topMid + chalk.hex(stroke)('┐'); + const bottom = chalk.hex(stroke)('└' + '─'.repeat(innerWidth) + '┘'); + + const lines: string[] = [top]; + for (let i = 0; i < innerHeight; i++) { + const inner = content[i] ?? ''; + lines.push(chalk.hex(stroke)('│') + fitExactly(inner, innerWidth) + chalk.hex(stroke)('│')); + } + lines.push(bottom); + return lines; + } + + // ── left: task list frame ──────────────────────────────────────────── + + private renderListFrame(width: number, height: number): string[] { + const title = `Tasks [${this.props.filter}]`; + const innerHeight = Math.max(0, height - 2); + + if (this.sortedVisible.length === 0) { + const empty = + this.props.filter === 'active' + ? 'No active tasks. Tab = show all.' + : 'No background tasks in this session.'; + const lines: string[] = [chalk.hex(this.props.colors.textMuted)(empty)]; + while (lines.length < innerHeight) lines.push(''); + return this.renderFrame(title, lines, width, height); + } + + this.adjustScroll(innerHeight); + const start = this.listScroll; + const window = this.sortedVisible.slice(start, start + innerHeight); + + const innerWidth = width - 2; + const lines: string[] = []; + for (const [vi, task] of window.entries()) { + const index = start + vi; + lines.push(this.renderListRow(task, index === this.selectedIndex, innerWidth)); + } + while (lines.length < innerHeight) lines.push(''); + + return this.renderFrame(title, lines, width, height); + } + + private renderListRow(task: BackgroundTaskInfo, selected: boolean, innerWidth: number): string { + const colors = this.props.colors; + const pointer = selected ? '> ' : ' '; + const pointerStyled = chalk.hex(selected ? colors.primary : colors.textDim)(pointer); + + const idColor = selected ? colors.primary : task.taskId.startsWith('agent-') + ? colors.success + : colors.accent; + const idText = selected + ? chalk.hex(idColor).bold(task.taskId) + : chalk.hex(idColor)(task.taskId); + const idPad = ' '.repeat(Math.max(0, 17 - task.taskId.length)); + + const status = STATUS_LABEL[task.status]; + const statusBadge = chalk.hex(statusColor(colors, task.status))(status); + + const prefix = `${pointerStyled}${idText}${idPad} ${statusBadge}`; + const prefixWidth = visibleWidth(prefix); + const descBudget = Math.max(0, innerWidth - prefixWidth - 1); + if (descBudget < 4) return fitExactly(prefix, innerWidth); + + const description = + singleLine(task.description) || singleLine(task.command) || '(no description)'; + const desc = truncateToWidth(description, descBudget, ELLIPSIS); + return fitExactly(`${prefix} ${chalk.hex(colors.text)(desc)}`, innerWidth); + } + + private adjustScroll(visibleRows: number): void { + if (visibleRows <= 0) { + this.listScroll = 0; + return; + } + if (this.selectedIndex < this.listScroll) { + this.listScroll = this.selectedIndex; + } else if (this.selectedIndex >= this.listScroll + visibleRows) { + this.listScroll = this.selectedIndex - visibleRows + 1; + } + const maxScroll = Math.max(0, this.sortedVisible.length - visibleRows); + if (this.listScroll < 0) this.listScroll = 0; + if (this.listScroll > maxScroll) this.listScroll = maxScroll; + } + + // ── right: detail + preview stack ──────────────────────────────────── + + private renderRightStack(width: number, height: number): string[] { + // Detail gets ~8 rows (or 40% of body, whichever is larger). Preview + // takes the rest. Both rendered as separate frames stacked vertically. + const detailHeight = Math.max(8, Math.min(Math.floor(height * 0.4), height - 5)); + const previewHeight = height - detailHeight; + return [ + ...this.renderDetailFrame(width, detailHeight), + ...this.renderPreviewFrame(width, previewHeight), + ]; + } + + private renderDetailFrame(width: number, height: number): string[] { + const colors = this.props.colors; + const innerHeight = Math.max(0, height - 2); + const task = this.sortedVisible[this.selectedIndex]; + if (task === undefined) { + const empty = chalk.hex(colors.textMuted)('Select a task from the list.'); + const lines: string[] = [empty]; + while (lines.length < innerHeight) lines.push(''); + return this.renderFrame('Detail', lines, width, height); + } + + const label = (text: string): string => chalk.hex(colors.textMuted)(text.padEnd(14)); + const value = (text: string): string => chalk.hex(colors.text)(text); + + const lines: string[] = [ + `${label('Task ID:')}${value(task.taskId)}`, + `${label('Status:')}${chalk.hex(statusColor(colors, task.status))(STATUS_LABEL[task.status])}`, + `${label('Description:')}${value(singleLine(task.description) || '—')}`, + ]; + if (task.command && task.command !== task.description) { + lines.push(`${label('Command:')}${value(singleLine(task.command))}`); + } + const timing = + task.status === 'running' || task.status === 'awaiting_approval' + ? `running ${formatRelativeTime(task.startedAt)}` + : task.endedAt !== null && task.endedAt !== undefined + ? `finished ${formatRelativeTime(task.endedAt)}` + : ''; + if (timing.length > 0) lines.push(`${label('Time:')}${chalk.hex(colors.textMuted)(timing)}`); + if (task.pid > 0) lines.push(`${label('Pid:')}${chalk.hex(colors.textMuted)(String(task.pid))}`); + if (task.exitCode !== null && task.exitCode !== undefined) { + lines.push(`${label('Exit code:')}${chalk.hex(colors.textMuted)(String(task.exitCode))}`); + } + if (task.stopReason !== undefined && task.stopReason.length > 0) { + lines.push(`${label('Stop reason:')}${chalk.hex(colors.textMuted)(task.stopReason)}`); + } + if (task.timedOut === true) { + lines.push(`${label('Timed out:')}${chalk.hex(colors.warning)('yes')}`); + } + if (task.approvalReason !== undefined && task.approvalReason.length > 0) { + lines.push( + `${label('Awaiting:')}${chalk.hex(colors.warning)(singleLine(task.approvalReason))}`, + ); + } + + while (lines.length < innerHeight) lines.push(''); + return this.renderFrame('Detail', lines, width, height); + } + + private renderPreviewFrame(width: number, height: number): string[] { + const colors = this.props.colors; + const innerHeight = Math.max(0, height - 2); + const task = this.sortedVisible[this.selectedIndex]; + if (task === undefined) { + const lines: string[] = [chalk.hex(colors.textMuted)('No task selected.')]; + while (lines.length < innerHeight) lines.push(''); + return this.renderFrame('Preview Output', lines, width, height); + } + + let body: string; + if (this.props.tailLoading) body = '[loading…]'; + else if (this.props.tailOutput === undefined || this.props.tailOutput.length === 0) + body = '[no output captured]'; + else body = this.props.tailOutput; + + const rawLines = body.split('\n'); + const tailLines = rawLines.slice(-innerHeight); + const styled = tailLines.map((line) => chalk.hex(colors.textDim)(line)); + while (styled.length < innerHeight) styled.push(''); + return this.renderFrame('Preview Output', styled, width, height); + } + + // ── too-small fallback ────────────────────────────────────────────── + + private renderTooSmall(width: number, rows: number): string[] { + const lines: string[] = []; + const msg = chalk.hex(this.props.colors.error)( + `Terminal too small (need ≥ ${String(MIN_WIDTH)} × ${String(MIN_HEIGHT)})`, + ); + lines.push(fitExactly(msg, width)); + for (let i = 1; i < rows; i++) lines.push(' '.repeat(width)); + return lines; + } +} diff --git a/apps/kimi-code/src/tui/components/dialogs/theme-selector.ts b/apps/kimi-code/src/tui/components/dialogs/theme-selector.ts new file mode 100644 index 000000000..8d6381c61 --- /dev/null +++ b/apps/kimi-code/src/tui/components/dialogs/theme-selector.ts @@ -0,0 +1,36 @@ +import { ChoicePickerComponent, type ChoiceOption } from './choice-picker'; + +import type { ColorPalette } from '#/tui/theme/colors'; +import type { Theme } from '#/tui/theme/index'; + +const THEME_OPTIONS: readonly ChoiceOption[] = [ + { value: 'auto', label: 'Auto (match terminal)' }, + { value: 'dark', label: 'Dark' }, + { value: 'light', label: 'Light' }, +]; + +function isThemeChoice(value: string): value is Theme { + return value === 'auto' || value === 'dark' || value === 'light'; +} + +export interface ThemeSelectorOptions { + readonly currentValue: Theme; + readonly colors: ColorPalette; + readonly onSelect: (theme: Theme) => void; + readonly onCancel: () => void; +} + +export class ThemeSelectorComponent extends ChoicePickerComponent { + constructor(opts: ThemeSelectorOptions) { + super({ + title: 'Select theme', + options: [...THEME_OPTIONS], + currentValue: opts.currentValue, + colors: opts.colors, + onSelect: (value) => { + if (isThemeChoice(value)) opts.onSelect(value); + }, + onCancel: opts.onCancel, + }); + } +} diff --git a/apps/kimi-code/src/tui/components/editor/custom-editor.ts b/apps/kimi-code/src/tui/components/editor/custom-editor.ts new file mode 100644 index 000000000..8450d37e4 --- /dev/null +++ b/apps/kimi-code/src/tui/components/editor/custom-editor.ts @@ -0,0 +1,362 @@ +/** + * Custom editor extending pi-tui Editor with app-level keybindings. + */ + +import { Editor, isKeyRelease, matchesKey, Key, type TUI } from '@earendil-works/pi-tui'; +import chalk from 'chalk'; + +import type { ColorPalette } from '#/tui/theme/colors'; +import { createEditorTheme } from '#/tui/theme/pi-tui-theme'; + +// oxlint-disable-next-line no-control-regex -- ESC (\x1b) is required to match ANSI SGR escape sequences +const ANSI_SGR = /\u001B\[[0-9;]*m/g; + +// Kitty keyboard protocol CSI-u sequence: ESC [ keycode ; modifier[:eventType] u. +// We intentionally match only the simple two-field form — enough to rewrite +// `ctrl+<LETTER>` with caps_lock into `ctrl+<letter>` without caps_lock. +// oxlint-disable-next-line no-control-regex -- ESC (\x1b) is required to match CSI +const KITTY_CSI_U = /^\u001B\[(\d+);(\d+)((?::\d+)*)u$/; +// Kitty modifier bit layout: shift=1, alt=2, ctrl=4, super=8, hyper=16, +// meta=32, caps_lock=64, num_lock=128. Reported value is `mask + 1`. +const CAPS_LOCK_BIT = 64; +const CTRL_BIT = 4; +const SHIFT_BIT = 1; + +interface AutocompleteInternals { + cancelAutocomplete(): void; + readonly autocompleteAbort?: AbortController; + readonly autocompleteDebounceTimer?: ReturnType<typeof setTimeout>; +} + +/** + * Workaround for a pi-tui bug that surfaces when Kitty keyboard protocol + * is active AND caps_lock is on. In that state terminals emit, e.g., + * `ESC[68;69u` for ctrl+d (codepoint=68=`D`, modifier=ctrl|caps_lock). + * pi-tui's `matchesKittySequence` masks `caps_lock` out of the *modifier* + * but leaves the *codepoint* capitalised, so `matchesKey(data, "ctrl+d")` + * (which expects codepoint=100=`d`) fails and every ctrl-shortcut is + * silently dropped. + * + * We rewrite the sequence back to its unlocked form before dispatching, + * but only when ctrl is held and shift is not — i.e. exactly the + * `ctrl+<letter>` case. Plain uppercase (caps_lock only, no ctrl) and + * explicit ctrl+shift+<letter> are left alone. + */ +export function normalizeCapsLockedCtrl(data: string): string { + const m = data.match(KITTY_CSI_U); + if (m === null) return data; + const codepoint = Number(m[1]); + const modifierPlus1 = Number(m[2]); + const tail = m[3] ?? ''; + if (!Number.isFinite(codepoint) || !Number.isFinite(modifierPlus1)) return data; + const modifier = modifierPlus1 - 1; + if ((modifier & CAPS_LOCK_BIT) === 0) return data; + if ((modifier & CTRL_BIT) === 0) return data; + if ((modifier & SHIFT_BIT) !== 0) return data; + if (codepoint < 65 || codepoint > 90) return data; + const loweredCodepoint = codepoint + 32; + const strippedModifier = (modifier & ~CAPS_LOCK_BIT) + 1; + return `\u001B[${String(loweredCodepoint)};${String(strippedModifier)}${tail}u`; +} + +/** Convert a visible-char index (ANSI-stripped) back to an index into the raw ANSI-bearing string. */ +function mapVisibleIdxToRaw(line: string, visibleIdx: number): number { + let visibleCount = 0; + let i = 0; + const re = new RegExp(ANSI_SGR.source, 'y'); + while (i < line.length && visibleCount < visibleIdx) { + re.lastIndex = i; + const m = re.exec(line); + if (m !== null && m.index === i) { + i += m[0].length; + } else { + visibleCount++; + i++; + } + } + return i; +} + +function stripSgr(s: string): string { + return s.replace(ANSI_SGR, ''); +} + +function isNewlineShortcut(data: string): boolean { + return data === '\n' || data === '\u001B\r' || data === '\u001B[13;2~'; +} + +export class CustomEditor extends Editor { + public onEscape?: () => void; + public onCtrlD?: () => void; + public onCtrlC?: () => void; + public onToggleToolExpand?: () => void; + // Returns true when a plan card actually handled the toggle. When it + // returns false (no plan in the transcript) the keystroke falls through + // to pi-tui's default ctrl+e binding (move cursor to end of line). + public onTogglePlanExpand?: () => boolean; + public onOpenExternalEditor?: () => void; + public onCtrlS?: () => void; + public onUndo?: () => void; + public onInsertNewline?: () => void; + public onTextPaste?: () => void; + /** + * Called when ↑ is pressed in an empty editor. Return `true` to consume + * the key (e.g. recalled a queued message); return `false` to fall + * through so pi-tui's built-in history navigation runs. + */ + public onUpArrowEmpty?: () => boolean; + public onShiftTab?: () => void; + /** + * Called when the user triggers "paste image" (Ctrl-V on Unix, + * Alt-V on Windows — Ctrl-V is terminal-reserved there). Return + * `true` to consume the key (image was read and handled); return + * `false` to let the key fall through to the normal paste path. + * The callback may be async; pi-tui awaits it before dispatching + * the next keystroke. + */ + public onPasteImage?: () => Promise<boolean>; + + /** + * `colors` is the live `ColorPalette` reference — the host mutates it + * in place on theme switch (`Object.assign(state.theme.colors, ...)`), so + * reading `this.colors.<token>` at render time always sees the + * current theme without any setter plumbing. The `EditorTheme` that + * pi-tui's `Editor` requires is derived from the same palette, and + * `paddingX: 2` reserves the two leading columns where `render()` + * paints the terminal-style `> ` prompt — both are implementation + * details, not caller knobs. + */ + constructor( + tui: TUI, + private readonly colors: ColorPalette, + ) { + // paddingX: 4 reserves column 0 for the left vertical border (│), + // column 1 as a single space between border and prompt, column 2 for + // the `>` prompt token, and column 3 as the space between prompt and + // content. The right side mirrors with 3 padding columns and the right + // border at the last column. + super(tui, createEditorTheme(colors), { paddingX: 4 }); + } + + private hasAutocompleteActivity(): boolean { + const autocomplete = this as unknown as AutocompleteInternals; + return ( + this.isShowingAutocomplete() || + autocomplete.autocompleteAbort !== undefined || + autocomplete.autocompleteDebounceTimer !== undefined + ); + } + + private cancelAutocompleteActivity(): void { + // pi-tui exposes `isShowingAutocomplete()` but keeps cancellation private. + // Kimi needs Esc to win over app-level cancel while the slash menu request is active. + (this as unknown as AutocompleteInternals).cancelAutocomplete(); + } + + override render(width: number): string[] { + const lines = super.render(width); + if (lines.length < 3) return lines; + const firstContentIdx = 1; + const text = this.getText().trimStart(); + if (text.startsWith('/')) { + // Paint only the FIRST editor content line; multi-line slash commands + // are not a thing in practice. + const original = lines[firstContentIdx]; + if (original !== undefined) { + const highlighted = highlightFirstSlashToken(original, this.colors.primary); + if (highlighted !== undefined) { + lines[firstContentIdx] = highlighted; + } + } + } + const firstContent = lines[firstContentIdx]; + if (firstContent !== undefined) { + const withPrompt = injectPromptSymbol(firstContent); + if (withPrompt !== undefined) { + lines[firstContentIdx] = withPrompt; + } + } + // `this.borderColor` is pi-tui's per-render paint function. The host may + // overwrite it (e.g. plan-mode / slash-context highlight via + // `editor.borderColor = chalk.hex(primary)`), so we route corners and + // side bars through the same hook to stay in sync. + return wrapWithSideBorders(lines, (s) => this.borderColor(s)); + } + + override handleInput(data: string): void { + const normalized = normalizeCapsLockedCtrl(data); + if (isKeyRelease(normalized)) { + return; + } + // Paste image binding — platform-aware: + // Windows terminals reserve Ctrl-V for their own paste handling + // (e.g. Windows Terminal's Ctrl+V shortcut), so we listen for + // Alt-V there. Everywhere else Ctrl-V pastes. When the host + // reports no image available, we fall through to pi-tui's + // normal paste path so text from the clipboard still works. + const pasteKey = process.platform === 'win32' ? 'alt+v' : Key.ctrl('v'); + if (matchesKey(normalized, pasteKey) && this.onPasteImage !== undefined) { + const handler = this.onPasteImage; + void handler().then((handled) => { + if (!handled) { + this.onTextPaste?.(); + // No image on the clipboard — forward the original keystroke + // through the base handler so a textual clipboard still works. + super.handleInput.call(this, normalized); + } + }); + return; + } + + if (matchesKey(normalized, Key.ctrl('d'))) { + if (this.getText().length === 0) { + this.onCtrlD?.(); + return; + } + } + + if (matchesKey(normalized, Key.ctrl('c'))) { + this.onCtrlC?.(); + return; + } + + if (matchesKey(normalized, Key.ctrl('g'))) { + this.onOpenExternalEditor?.(); + return; + } + + if (matchesKey(normalized, Key.ctrl('o'))) { + this.onToggleToolExpand?.(); + return; + } + + if (matchesKey(normalized, Key.ctrl('e'))) { + if (this.onTogglePlanExpand?.() === true) return; + // No plan to toggle — fall through to pi-tui's end-of-line. + } + + if (matchesKey(normalized, Key.ctrl('s'))) { + this.onCtrlS?.(); + return; + } + + if (matchesKey(normalized, 'shift+tab')) { + this.onShiftTab?.(); + return; + } + + if (matchesKey(normalized, Key.ctrl('-'))) { + this.onUndo?.(); + } + + if (isNewlineShortcut(normalized)) { + this.onInsertNewline?.(); + } + + if (matchesKey(normalized, Key.up)) { + if (this.getText().length === 0 && this.onUpArrowEmpty) { + if (this.onUpArrowEmpty()) return; + // fall through to super so Editor's built-in history navigation runs + } + } + + if (matchesKey(normalized, Key.escape)) { + if (this.hasAutocompleteActivity()) { + this.cancelAutocompleteActivity(); + return; + } + this.onEscape?.(); + return; + } + + super.handleInput(normalized); + } +} + +/** + * Return a copy of `line` with the first `/token` coloured using `hex`. + * `line` may already contain SGR escapes (cursor inverse, etc.); we + * locate `/` via visible-index math so ANSI pass-through survives. + * Returns `undefined` if no token is found. + */ +export function highlightFirstSlashToken(line: string, hex: string): string | undefined { + const visible = stripSgr(line); + const slashIdx = visible.indexOf('/'); + if (slashIdx < 0) return undefined; + // Guard: only paint when `/` is the first non-whitespace character + // on the line (avoids colouring a mid-sentence slash). + for (let i = 0; i < slashIdx; i++) { + if (visible[i] !== ' ' && visible[i] !== '\t') return undefined; + } + // Token ends at the next whitespace (or the visible end). + let endVisible = slashIdx + 1; + while (endVisible < visible.length) { + const ch = visible[endVisible]; + if (ch === ' ' || ch === '\t') break; + endVisible++; + } + const visibleToken = visible.slice(slashIdx, endVisible); + if (visibleToken.slice(1).includes('/')) return undefined; + const rawStart = mapVisibleIdxToRaw(line, slashIdx); + const rawEnd = mapVisibleIdxToRaw(line, endVisible); + const before = line.slice(0, rawStart); + const token = line.slice(rawStart, rawEnd); + const after = line.slice(rawEnd); + return before + chalk.hex(hex).bold(token) + after; +} + +/** + * Overlay a terminal-style `> ` prompt symbol on the first content line. + * Column 0 is reserved for the left vertical border (overlaid later by + * wrapWithSideBorders); column 1 is a single-space gap, so the `>` token + * lives at column 2 with column 3 separating it from content. + * Relies on the editor being configured with `paddingX >= 4` so the line + * starts with at least four literal spaces. Emits no SGR so the terminal's + * default foreground colour renders the symbol. Returns `undefined` if the + * line is too short or doesn't begin with the expected padding. + */ +export function injectPromptSymbol(line: string): string | undefined { + if (line.length < 4) return undefined; + for (let i = 0; i < 4; i++) { + if (line[i] !== ' ') return undefined; + } + return ' > ' + line.slice(4); +} + +/** + * Post-process pi-tui's editor output to draw a full box around it. + * + * pi-tui only renders horizontal top/bottom borders; we wrap them with + * `╭╮╰╯` corners and add vertical `│` bars on each row's outer columns. + * Horizontal-border rows (those whose first visible char is `─`, including + * scroll indicators like `── ↑ N more ──`) are stripped of their existing + * SGR and repainted as a single box-drawn span. Content rows keep their + * inner SGR intact; only column 0 and the last column are overlaid, and + * only if they're literal spaces — that protects the cursor-overflow + * case where the rightmost column is an SGR-tagged inverse cursor. + */ +export function wrapWithSideBorders( + lines: string[], + paint: (s: string) => string, +): string[] { + let seenTop = false; + return lines.map((line) => { + const plain = stripSgr(line); + if (plain.length > 0 && plain[0] === '─') { + const leftCorner = seenTop ? '╰' : '╭'; + const rightCorner = seenTop ? '╯' : '╮'; + seenTop = true; + if (plain.length === 1) return paint(leftCorner); + const middle = plain.slice(1, -1); + return paint(leftCorner + middle + rightCorner); + } + if (line.length === 0) return line; + const firstCh = line[0]; + const lastCh = line.at(-1); + const head = firstCh === ' ' ? paint('│') : (firstCh ?? ''); + const tail = + line.length > 1 && lastCh === ' ' ? paint('│') : (lastCh ?? ''); + if (line.length === 1) return head; + return head + line.slice(1, -1) + tail; + }); +} diff --git a/apps/kimi-code/src/tui/components/editor/file-mention-provider.ts b/apps/kimi-code/src/tui/components/editor/file-mention-provider.ts new file mode 100644 index 000000000..d2a7d39c1 --- /dev/null +++ b/apps/kimi-code/src/tui/components/editor/file-mention-provider.ts @@ -0,0 +1,272 @@ +/** + * `@file` autocomplete provider for the input box. + * + * pi-tui's `CombinedAutocompleteProvider` handles the mechanical parts + * (extract `@…` prefix, insert completion with the right quoting). This + * wrapper adds kimi-specific ranking + filtering so the default "empty + * `@`" list surfaces files the user actually wants, not alphabetical + * noise from `.agents/skills/*` et al. + * + * Sort order — empty query: + * 1. recently edited (from `git log --name-only`) + * 2. recent fs mtime + * 3. basename alphabetical + * (first 15, not 50 — pi-tui's menu height is ~6-10 lines anyway) + * + * Sort order — non-empty query (strict to fuzzy): + * cat 0: basename starts-with query + * cat 1: basename contains query + * cat 2: fuzzyMatch succeeds on full path + * tie-break within each cat: recency rank → mtime → basename length + * (first 50) + * + * Filter — dot directories are hidden by default. User can opt in by starting the query + * with `.` (e.g. `@.github/`), since those paths rarely need + * completion. + * + * When `fd` is available the inner pi-tui provider owns the `@` branch + * verbatim — its fd invocation respects `.gitignore` and is strictly + * better than anything we can cheaply reproduce in TS. We only kick in + * when `fd` is missing AND we're in a git repo. + */ + +import { basename } from 'node:path'; + +import { + CombinedAutocompleteProvider, + fuzzyFilter, + fuzzyMatch, + type AutocompleteItem, + type AutocompleteProvider, + type AutocompleteSuggestions, + type SlashCommand, +} from '@earendil-works/pi-tui'; + +import type { GitLsFilesCache, GitSnapshot } from '#/utils/git/git-ls-files'; + +const MAX_SUGGESTIONS_WHEN_QUERY = 50; +const MAX_SUGGESTIONS_WHEN_EMPTY = 15; + +// Mirrors pi-tui's PATH_DELIMITERS. Keeping a local copy so @-detection +// stays aligned even if pi-tui extends its set. +const PATH_DELIMITERS = new Set([' ', '\t', '"', "'", '=']); + +export class FileMentionProvider implements AutocompleteProvider { + private readonly inner: CombinedAutocompleteProvider; + + constructor( + slashCommands: SlashCommand[], + workDir: string, + private readonly fdPath: string | null, + private readonly gitCache: GitLsFilesCache, + ) { + this.inner = new CombinedAutocompleteProvider(slashCommands, workDir, fdPath); + } + + async getSuggestions( + lines: string[], + cursorLine: number, + cursorCol: number, + options: { signal: AbortSignal; force?: boolean }, + ): Promise<AutocompleteSuggestions | null> { + const textBeforeCursor = (lines[cursorLine] ?? '').slice(0, cursorCol); + const atPrefix = extractAtPrefix(textBeforeCursor); + + // Non-`@` branch (slash commands, `/path`, quoted paths) — pi-tui + // already owns the edge cases. No intercept. + if (atPrefix === null) { + return this.inner.getSuggestions(lines, cursorLine, cursorCol, options); + } + + // `fd` available → inner's fuzzy search is strictly better than our + // git fallback (fd respects .gitignore AND covers unstaged paths + // without a second spawn). Accept its output as-is. + if (this.fdPath !== null) { + return this.inner.getSuggestions(lines, cursorLine, cursorCol, options); + } + + const snapshot = this.gitCache.getSnapshot(); + if (snapshot === null || snapshot.files.length === 0) { + return this.inner.getSuggestions(lines, cursorLine, cursorCol, options); + } + + const query = atPrefix.slice(1); // strip leading '@' + const includeDotDirs = query.startsWith('.'); + const candidates = includeDotDirs + ? snapshot.files + : snapshot.files.filter((p) => !containsDotSegment(p)); + + const items = + query.length === 0 + ? rankForEmptyQuery(candidates, snapshot) + : rankForQuery(candidates, query, snapshot); + + if (items.length === 0) { + // Git cache had nothing useful — fall through to readdir (user + // may be typing a path that exists but isn't tracked, e.g. a + // freshly created file not yet in the 2s cache). + return this.inner.getSuggestions(lines, cursorLine, cursorCol, options); + } + return { items, prefix: atPrefix }; + } + + applyCompletion( + lines: string[], + cursorLine: number, + cursorCol: number, + item: AutocompleteItem, + prefix: string, + ): { lines: string[]; cursorLine: number; cursorCol: number } { + // Reuse pi-tui's insertion logic — it handles `@` prefix, quoted + // paths, directory trailing slash. Our item shape matches what + // pi-tui produces. + return this.inner.applyCompletion(lines, cursorLine, cursorCol, item, prefix); + } +} + +/** + * Return the `@…` token ending at the cursor, or `null` if we're not in + * an `@` mention. Mirrors pi-tui's `extractAtPrefix` — the token + * boundary is the last PATH_DELIMITER before the cursor, and the token + * must start with `@`. + */ +function extractAtPrefix(text: string): string | null { + let tokenStart = 0; + for (let i = text.length - 1; i >= 0; i -= 1) { + if (PATH_DELIMITERS.has(text[i] ?? '')) { + tokenStart = i + 1; + break; + } + } + if (text[tokenStart] !== '@') return null; + return text.slice(tokenStart); +} + +/** True when any path segment starts with a dot (e.g. `.github/x.yml`). */ +function containsDotSegment(path: string): boolean { + for (const segment of path.split('/')) { + if (segment.startsWith('.')) return true; + } + return false; +} + +/** + * Empty-query ranking: stratified by signal strength. + * + * Layer 1: files touched in the last RECENT_COMMIT_DEPTH commits, + * ordered by how recently. Strongest signal — if the user + * just worked on it, they probably want to mention it. + * Layer 2: files with the newest fs mtime (covers uncommitted edits + * and files edited but not yet added to git). + * Layer 3: everything else, alphabetical by basename so + * README/package.json-style top-level files bubble up + * relative to deeply-nested alphabetical paths. + * + * Cap at MAX_SUGGESTIONS_WHEN_EMPTY. Layers fill in order; dedup by + * path so a recently-edited file isn't also listed in layer 2. + */ +function rankForEmptyQuery(files: readonly string[], snapshot: GitSnapshot): AutocompleteItem[] { + const picked = new Set<string>(); + const result: string[] = []; + const cap = MAX_SUGGESTIONS_WHEN_EMPTY; + const inFiles = new Set(files); + + // Layer 1 — git log recency. + const byRecency = [...snapshot.recencyOrder.entries()] + .filter(([path]) => inFiles.has(path)) + .toSorted((a, b) => a[1] - b[1]); + for (const [path] of byRecency) { + if (result.length >= cap) break; + if (picked.has(path)) continue; + picked.add(path); + result.push(path); + } + + // Layer 2 — fs mtime. + if (result.length < cap) { + const byMtime = files + .filter((p) => !picked.has(p) && snapshot.mtimeByPath.has(p)) + .toSorted((a, b) => (snapshot.mtimeByPath.get(b) ?? 0) - (snapshot.mtimeByPath.get(a) ?? 0)); + for (const path of byMtime) { + if (result.length >= cap) break; + picked.add(path); + result.push(path); + } + } + + // Layer 3 — alphabetical by basename. + if (result.length < cap) { + const rest = files + .filter((p) => !picked.has(p)) + .toSorted((a, b) => basename(a).localeCompare(basename(b)) || a.localeCompare(b)); + for (const path of rest) { + if (result.length >= cap) break; + result.push(path); + } + } + + return result.map(toItem); +} + +/** + * Non-empty-query ranking: three strictness tiers, with recency / + * mtime as tie-breakers inside each tier so "the readme you just + * edited" beats "a readme deep in a vendor dir". + */ +function rankForQuery( + files: readonly string[], + query: string, + snapshot: GitSnapshot, +): AutocompleteItem[] { + const lowerQuery = query.toLowerCase(); + const scored: Array<{ path: string; cat: number; fuzzyScore: number }> = []; + for (const path of files) { + const base = basename(path).toLowerCase(); + if (base.startsWith(lowerQuery)) { + scored.push({ path, cat: 0, fuzzyScore: 0 }); + continue; + } + if (base.includes(lowerQuery)) { + scored.push({ path, cat: 1, fuzzyScore: 0 }); + continue; + } + const fuzzy = fuzzyMatch(query, path); + if (fuzzy.matches) { + scored.push({ path, cat: 2, fuzzyScore: fuzzy.score }); + } + } + + if (scored.length === 0) { + // pi-tui's fuzzyFilter is slightly different (token-splitting); + // try it as a last-resort safety net. + return fuzzyFilter([...files], query, (p) => p) + .slice(0, MAX_SUGGESTIONS_WHEN_QUERY) + .map(toItem); + } + + scored.sort((a, b) => { + if (a.cat !== b.cat) return a.cat - b.cat; + if (a.cat === 2 && a.fuzzyScore !== b.fuzzyScore) return a.fuzzyScore - b.fuzzyScore; + const ra = snapshot.recencyOrder.get(a.path); + const rb = snapshot.recencyOrder.get(b.path); + if (ra !== undefined && rb !== undefined && ra !== rb) return ra - rb; + if (ra !== undefined && rb === undefined) return -1; + if (ra === undefined && rb !== undefined) return 1; + const ma = snapshot.mtimeByPath.get(a.path) ?? 0; + const mb = snapshot.mtimeByPath.get(b.path) ?? 0; + if (ma !== mb) return mb - ma; + const baseLenDiff = basename(a.path).length - basename(b.path).length; + if (baseLenDiff !== 0) return baseLenDiff; + return a.path.localeCompare(b.path); + }); + + return scored.slice(0, MAX_SUGGESTIONS_WHEN_QUERY).map((entry) => toItem(entry.path)); +} + +function toItem(path: string): AutocompleteItem { + return { + value: `@${path}`, + label: basename(path), + description: path, + }; +} diff --git a/apps/kimi-code/src/tui/components/index.ts b/apps/kimi-code/src/tui/components/index.ts new file mode 100644 index 000000000..bcff5b2ac --- /dev/null +++ b/apps/kimi-code/src/tui/components/index.ts @@ -0,0 +1,35 @@ +export * from './chrome/device-code-box'; +export * from './chrome/footer'; +export * from './chrome/moon-loader'; +export * from './chrome/todo-panel'; +export * from './chrome/welcome'; +export * from './dialogs/approval-panel'; +export * from './dialogs/choice-picker'; +export * from './dialogs/compaction'; +export * from './dialogs/editor-selector'; +export * from './dialogs/help-panel'; +export * from './dialogs/model-selector'; +export * from './dialogs/permission-selector'; +export * from './dialogs/question-dialog'; +export * from './dialogs/session-picker'; +export * from './dialogs/settings-selector'; +export * from './dialogs/theme-selector'; +export * from './editor/custom-editor'; +export * from './editor/file-mention-provider'; +export * from './media/code-highlight'; +export * from './media/diff-preview'; +export * from './media/image-thumbnail'; +export * from './messages/agent-group'; +export * from './messages/assistant-message'; +export * from './messages/background-agent-status'; +export * from './messages/plan-box'; +export * from './messages/read-group'; +export * from './messages/shell-execution'; +export * from './messages/skill-activation'; +export * from './messages/status-message'; +export * from './messages/thinking'; +export * from './messages/tool-call'; +export * from './messages/usage-panel'; +export * from './messages/user-message'; +export * from './panes/activity-pane'; +export * from './panes/queue-pane'; diff --git a/apps/kimi-code/src/tui/components/media/code-highlight.ts b/apps/kimi-code/src/tui/components/media/code-highlight.ts new file mode 100644 index 000000000..bfef3d91b --- /dev/null +++ b/apps/kimi-code/src/tui/components/media/code-highlight.ts @@ -0,0 +1,52 @@ +/** + * Shared syntax-highlighting helpers for code previews + * (tool-call Write/Edit, approval-panel Write content, etc.). + */ + +import { extname } from 'node:path'; + +import { highlight, supportsLanguage } from 'cli-highlight'; + +const EXT_LANG_MAP: Record<string, string> = { + ts: 'typescript', + tsx: 'typescript', + js: 'javascript', + jsx: 'javascript', + py: 'python', + rb: 'ruby', + rs: 'rust', + go: 'go', + java: 'java', + sh: 'bash', + bash: 'bash', + zsh: 'bash', + json: 'json', + yaml: 'yaml', + yml: 'yaml', + toml: 'toml', + md: 'markdown', + css: 'css', + html: 'html', + sql: 'sql', + c: 'c', + cpp: 'cpp', + h: 'c', + hpp: 'cpp', +}; + +export function langFromPath(filePath: string): string | undefined { + const ext = extname(filePath).slice(1).toLowerCase(); + if (ext.length === 0) return undefined; + const lang = EXT_LANG_MAP[ext] ?? ext; + return supportsLanguage(lang) ? lang : undefined; +} + +export function highlightLines(code: string, lang: string | undefined): string[] { + const normalizedLang = lang?.trim().toLowerCase(); + if (!normalizedLang || !supportsLanguage(normalizedLang)) return code.split('\n'); + try { + return highlight(code, { language: normalizedLang, ignoreIllegals: true }).split('\n'); + } catch { + return code.split('\n'); + } +} diff --git a/apps/kimi-code/src/tui/components/media/diff-preview.ts b/apps/kimi-code/src/tui/components/media/diff-preview.ts new file mode 100644 index 000000000..5a978723d --- /dev/null +++ b/apps/kimi-code/src/tui/components/media/diff-preview.ts @@ -0,0 +1,310 @@ +/** + * Diff preview rendering as plain ANSI strings. + * + * Reuses the diff algorithm from approval/DiffPreview.tsx, but outputs + * formatted text lines instead of React elements. + */ + +import chalk from 'chalk'; + +import type { ColorPalette } from '#/tui/theme/colors'; + +export type DiffLineKind = 'context' | 'add' | 'delete'; + +interface DiffStyles { + add: (s: string) => string; + del: (s: string) => string; + addBold: (s: string) => string; + delBold: (s: string) => string; + gutter: (s: string) => string; + meta: (s: string) => string; +} + +function makeDiffStyles(colors: ColorPalette): DiffStyles { + return { + add: (s) => chalk.hex(colors.diffAdded)(s), + del: (s) => chalk.hex(colors.diffRemoved)(s), + addBold: (s) => chalk.bold.hex(colors.diffAddedStrong)(s), + delBold: (s) => chalk.bold.hex(colors.diffRemovedStrong)(s), + gutter: (s) => chalk.hex(colors.diffGutter)(s), + meta: (s) => chalk.hex(colors.diffMeta)(s), + }; +} + +export interface DiffLine { + kind: DiffLineKind; + lineNum: number; + code: string; +} + +export function computeDiffLines( + oldText: string, + newText: string, + oldStart: number = 1, + newStart: number = 1, + isIncomplete: boolean = false, +): DiffLine[] { + const oldLines = oldText ? oldText.split('\n') : []; + const newLines = newText ? newText.split('\n') : []; + const m = oldLines.length; + const n = newLines.length; + + const dp: number[][] = Array.from({ length: m + 1 }, () => + Array.from({ length: n + 1 }, () => 0), + ); + for (let i = 1; i <= m; i++) { + for (let j = 1; j <= n; j++) { + if (oldLines[i - 1] === newLines[j - 1]) { + dp[i]![j] = dp[i - 1]![j - 1]! + 1; + } else { + dp[i]![j] = Math.max(dp[i - 1]![j]!, dp[i]![j - 1]!); + } + } + } + + const reversed: DiffLine[] = []; + let i = m; + let j = n; + while (i > 0 || j > 0) { + if (i > 0 && j > 0 && oldLines[i - 1] === newLines[j - 1]) { + reversed.push({ kind: 'context', lineNum: newStart + j - 1, code: newLines[j - 1]! }); + i--; + j--; + } else if (j > 0 && (i === 0 || dp[i]![j - 1]! >= dp[i - 1]![j]!)) { + reversed.push({ kind: 'add', lineNum: newStart + j - 1, code: newLines[j - 1]! }); + j--; + } else { + reversed.push({ kind: 'delete', lineNum: oldStart + i - 1, code: oldLines[i - 1]! }); + i--; + } + } + + const result: DiffLine[] = []; + for (let k = reversed.length - 1; k >= 0; k--) { + result.push(reversed[k]!); + } + + // While the text is still streaming, suppress trailing delete lines. + // They are likely artefacts of newText not having arrived yet rather + // than genuine deletions. + if (isIncomplete && result.length > 0) { + let lastNonDelete = result.length - 1; + while (lastNonDelete >= 0 && result[lastNonDelete]!.kind === 'delete') { + lastNonDelete--; + } + if (lastNonDelete >= 0) { + result.length = lastNonDelete + 1; + } else { + // Every line would be shown as deleted; suppress them all so the + // UI doesn't flash a wall of red before newText starts arriving. + result.length = 0; + } + } + + return result; +} + +export function renderDiffLines( + oldText: string, + newText: string, + path: string, + colors: ColorPalette, + isIncomplete: boolean = false, + oldStart?: number, + newStart?: number, + maxLines?: number, +): string[] { + const s = makeDiffStyles(colors); + const diffLines = computeDiffLines(oldText, newText, oldStart ?? 1, newStart ?? 1, isIncomplete); + const changedLines = diffLines.filter((l) => l.kind !== 'context'); + const added = changedLines.filter((l) => l.kind === 'add').length; + const removed = changedLines.filter((l) => l.kind === 'delete').length; + + const output: string[] = []; + + let header = ''; + if (added > 0) header += s.addBold(`+${String(added)} `); + if (removed > 0) header += s.delBold(`-${String(removed)} `); + header += path; + output.push(header); + + const shown = + maxLines !== undefined && maxLines >= 0 && changedLines.length > maxLines + ? changedLines.slice(0, maxLines) + : changedLines; + + for (const line of shown) { + const marker = line.kind === 'add' ? '+' : '-'; + const color = line.kind === 'add' ? s.add : s.del; + output.push(s.gutter(String(line.lineNum).padStart(4) + ' ') + color(marker + ' ' + line.code)); + } + + const hidden = changedLines.length - shown.length; + if (hidden > 0) { + output.push( + s.meta( + ` … ${String(hidden)} more change${hidden > 1 ? 's' : ''} hidden (ctrl+o to expand)`, + ), + ); + } + + return output; +} + +export interface ClusteredDiffOptions { + readonly contextLines?: number; + readonly maxLines?: number; + readonly isIncomplete?: boolean; + readonly expandKeyHint?: string; +} + +interface Cluster { + readonly start: number; + readonly end: number; +} + +function buildClusters( + diffLines: DiffLine[], + contextLines: number, +): { clusters: Cluster[]; changedCount: number; addedCount: number; removedCount: number } { + const changeIndices: number[] = []; + let added = 0; + let removed = 0; + for (const [i, line] of diffLines.entries()) { + if (line.kind === 'add') { + added++; + changeIndices.push(i); + } else if (line.kind === 'delete') { + removed++; + changeIndices.push(i); + } + } + + const clusters: Cluster[] = []; + if (changeIndices.length === 0) { + return { clusters, changedCount: 0, addedCount: added, removedCount: removed }; + } + + const mergeGap = 2 * contextLines; + let groupStart = changeIndices[0]!; + let groupEnd = changeIndices[0]!; + for (let i = 1; i < changeIndices.length; i++) { + const idx = changeIndices[i]!; + if (idx - groupEnd <= mergeGap) { + groupEnd = idx; + } else { + clusters.push({ + start: Math.max(0, groupStart - contextLines), + end: Math.min(diffLines.length - 1, groupEnd + contextLines), + }); + groupStart = idx; + groupEnd = idx; + } + } + clusters.push({ + start: Math.max(0, groupStart - contextLines), + end: Math.min(diffLines.length - 1, groupEnd + contextLines), + }); + + return { + clusters, + changedCount: changeIndices.length, + addedCount: added, + removedCount: removed, + }; +} + +function formatDiffRow(line: DiffLine, s: DiffStyles): string { + const gutter = s.gutter(String(line.lineNum).padStart(4) + ' '); + if (line.kind === 'add') return gutter + s.add('+ ' + line.code); + if (line.kind === 'delete') return gutter + s.del('- ' + line.code); + return gutter + ' ' + line.code; +} + +/** + * Render a diff with surrounding context, eliding unchanged middle + * regions between change clusters with a `… N unchanged lines …` + * separator. When `maxLines` is set, the body is capped at a cluster + * boundary and a `ctrl+o to expand` footer is appended. + * + * Used by Edit's call preview where we want to show *what changed* + * with enough context to read the change, but not the whole file. + */ +export function renderDiffLinesClustered( + oldText: string, + newText: string, + path: string, + colors: ColorPalette, + opts: ClusteredDiffOptions = {}, +): string[] { + const s = makeDiffStyles(colors); + const contextLines = opts.contextLines ?? 3; + const maxLines = opts.maxLines; + const diffLines = computeDiffLines(oldText, newText, 1, 1, opts.isIncomplete ?? false); + const { clusters, changedCount, addedCount, removedCount } = buildClusters( + diffLines, + contextLines, + ); + + const output: string[] = []; + let header = ''; + if (addedCount > 0) header += s.addBold(`+${String(addedCount)} `); + if (removedCount > 0) header += s.delBold(`-${String(removedCount)} `); + header += path; + output.push(header); + + if (clusters.length === 0) return output; + + const cap = maxLines !== undefined && maxLines >= 0 ? maxLines : Number.POSITIVE_INFINITY; + let body = 0; + let prevEnd = -1; + let truncated = false; + let shownChanges = 0; + + outer: for (const cluster of clusters) { + if (body >= cap) { + truncated = true; + break; + } + if (prevEnd >= 0) { + const gap = cluster.start - prevEnd - 1; + if (gap > 0) { + if (body + 1 > cap) { + truncated = true; + break; + } + output.push(s.meta(` … ${String(gap)} unchanged line${gap > 1 ? 's' : ''} …`)); + body++; + } + } + // Emit cluster rows one at a time; allow mid-cluster truncation so + // a single huge cluster (e.g. the whole file replaced inline) still + // shows the leading lines instead of degenerating to "N changes + // hidden" with no body at all. + for (let i = cluster.start; i <= cluster.end; i++) { + if (body >= cap) { + truncated = true; + break outer; + } + const line = diffLines[i]!; + output.push(formatDiffRow(line, s)); + body++; + if (line.kind !== 'context') shownChanges++; + prevEnd = i; + } + } + + if (truncated) { + const hidden = changedCount - shownChanges; + if (hidden > 0) { + const hint = opts.expandKeyHint ?? 'ctrl+o'; + output.push( + s.meta( + ` … ${String(hidden)} more change${hidden > 1 ? 's' : ''} hidden (${hint} to expand)`, + ), + ); + } + } + + return output; +} diff --git a/apps/kimi-code/src/tui/components/media/image-thumbnail.ts b/apps/kimi-code/src/tui/components/media/image-thumbnail.ts new file mode 100644 index 000000000..86253582f --- /dev/null +++ b/apps/kimi-code/src/tui/components/media/image-thumbnail.ts @@ -0,0 +1,55 @@ +/** + * Transcript-side rendering of a pasted image. + * + * On terminals that speak the Kitty graphics protocol or iTerm2 inline + * image protocol (detected by pi-tui's `getCapabilities()`), we show + * the actual image. Everywhere else we fall back to a one-line text + * marker matching the placeholder the user sees in the input box — + * this keeps the transcript readable on Terminal.app / Linux default + * terminals / `script` recordings without extra chrome. + * + * Height is capped at ~12 rows so a single screenshot can't monopolize + * the viewport; pi-tui handles proportional scaling internally. + */ + +import { Container, Image, Text, type ImageTheme, getCapabilities } from '@earendil-works/pi-tui'; +import chalk from 'chalk'; + +import type { ColorPalette } from '#/tui/theme/colors'; +import type { ImageAttachment } from '#/tui/utils/image-attachment-store'; + +const MAX_IMAGE_ROWS = 12; +const MAX_IMAGE_WIDTH = 40; + +export class ImageThumbnail extends Container { + constructor(attachment: ImageAttachment, colors: ColorPalette) { + super(); + + const caps = getCapabilities(); + const supportsInline = caps.images === 'kitty' || caps.images === 'iterm2'; + + if (!supportsInline) { + // Non-graphic terminal — show the placeholder text in dim cyan so + // it's clearly an attachment reference but doesn't shout. + this.addChild(new Text(chalk.hex(colors.accent)(attachment.placeholder), 0, 0)); + return; + } + + const theme: ImageTheme = { + fallbackColor: (s: string) => chalk.hex(colors.textDim)(s), + }; + const base64 = Buffer.from(attachment.bytes).toString('base64'); + const image = new Image( + base64, + attachment.mime, + theme, + { + maxHeightCells: MAX_IMAGE_ROWS, + maxWidthCells: MAX_IMAGE_WIDTH, + filename: attachment.placeholder, + }, + { widthPx: attachment.width, heightPx: attachment.height }, + ); + this.addChild(image); + } +} diff --git a/apps/kimi-code/src/tui/components/messages/agent-group.ts b/apps/kimi-code/src/tui/components/messages/agent-group.ts new file mode 100644 index 000000000..593ae9746 --- /dev/null +++ b/apps/kimi-code/src/tui/components/messages/agent-group.ts @@ -0,0 +1,240 @@ +/** + * AgentGroupComponent renders 2+ Agent tool calls from the same step as one group. + * + * Design: + * - State container: each child Agent keeps its real state in its + * `ToolCallComponent` (subagent meta, phase, sub-tool calls, tokens, text). + * AgentGroup only stores references and does not copy state. Event handlers + * still route through `state.pendingToolComponents.get(parent_tool_call_id)`. + * - Subscription: `attach` registers a snapshot listener on each child so the + * group can refresh when child state changes. + * - Throttling: normal changes are coalesced into one render every 200ms. + * Phase transitions (spawning -> running -> done/failed) flush immediately. + * - Mounting: `KimiTUI` attaches the group to the transcript at the + * right time; the group handles `invalidate` plus `ui.requestRender`. + * - Ungrouping is not implemented. Once formed, a group stays grouped. + */ + +import type { TUI } from '@earendil-works/pi-tui'; +import { Container, Spacer, Text } from '@earendil-works/pi-tui'; +import chalk from 'chalk'; + +import { STATUS_BULLET } from '#/tui/constant/symbols'; +import type { ColorPalette } from '#/tui/theme/colors'; + +import type { ToolCallComponent, ToolCallSubagentSnapshot } from './tool-call'; + +const THROTTLE_MS = 200; + +interface AgentEntry { + readonly toolCallId: string; + readonly tc: ToolCallComponent; +} + +export class AgentGroupComponent extends Container { + private readonly entries: AgentEntry[] = []; + private readonly headerText: Text; + private readonly bodyContainer: Container; + private throttleTimer: ReturnType<typeof setTimeout> | null = null; + private lastFlushPhases = new Map<string, ToolCallSubagentSnapshot['phase']>(); + + constructor( + private readonly colors: ColorPalette, + private readonly ui: TUI | undefined, + ) { + super(); + this.addChild(new Spacer(1)); + this.headerText = new Text('', 0, 0); + this.addChild(this.headerText); + this.bodyContainer = new Container(); + this.addChild(this.bodyContainer); + } + + size(): number { + return this.entries.length; + } + + /** + * Borrows a standalone `ToolCallComponent` into the group as a hidden state + * container. Snapshot changes trigger throttled refreshes. Re-attaching the + * same toolCallId is a no-op. + */ + attach(toolCallId: string, tc: ToolCallComponent): void { + if (this.entries.some((e) => e.toolCallId === toolCallId)) return; + this.entries.push({ toolCallId, tc }); + tc.setSnapshotListener(() => { + this.scheduleRender(); + }); + this.flushRender(); + } + + /** + * Schedules a repaint. Real phase transitions force an immediate refresh; + * other changes such as latestActivity, tokens, or toolCount are throttled. + */ + private scheduleRender(): void { + if (this.detectPhaseTransition()) { + this.flushRender(); + return; + } + if (this.throttleTimer !== null) return; + this.throttleTimer = setTimeout(() => { + this.throttleTimer = null; + this.flushRender(); + }, THROTTLE_MS); + } + + /** + * Compares each child's current phase with the phase captured at the last + * flush. Any change is treated as a phase transition. + */ + private detectPhaseTransition(): boolean { + let changed = false; + for (const e of this.entries) { + const phase = e.tc.getSubagentSnapshot().phase; + if (this.lastFlushPhases.get(e.toolCallId) !== phase) { + changed = true; + break; + } + } + return changed; + } + + private flushRender(): void { + if (this.throttleTimer !== null) { + clearTimeout(this.throttleTimer); + this.throttleTimer = null; + } + + const snapshots = this.entries.map((e) => e.tc.getSubagentSnapshot()); + this.headerText.setText(this.buildHeader(snapshots)); + this.bodyContainer.clear(); + snapshots.forEach((snap, idx) => { + const isLast = idx === snapshots.length - 1; + this.appendLines(snap, isLast); + }); + + this.lastFlushPhases.clear(); + this.entries.forEach((entry, i) => { + const snap = snapshots[i]; + if (snap !== undefined) this.lastFlushPhases.set(entry.toolCallId, snap.phase); + }); + + this.invalidate(); + this.ui?.requestRender(); + } + + private buildHeader(snapshots: readonly ToolCallSubagentSnapshot[]): string { + const colors = this.colors; + const total = snapshots.length; + const done = snapshots.filter((s) => s.phase === 'done').length; + const failed = snapshots.filter((s) => s.phase === 'failed').length; + const finished = done + failed; + const allDone = finished === total; + const bullet = allDone + ? chalk.hex(colors.success)(STATUS_BULLET) + : chalk.hex(colors.roleAssistant)(STATUS_BULLET); + + if (allDone) { + const types = new Set(snapshots.map((s) => s.agentName).filter((n) => n !== undefined)); + const headerLabel = + types.size === 1 + ? `${String(total)} ${[...types][0]} agents finished` + : `${String(total)} agents finished`; + const totalTools = snapshots.reduce((acc, s) => acc + s.toolCount, 0); + const totalTokens = snapshots.reduce((acc, s) => acc + s.tokens, 0); + const tail = formatHeaderTail(totalTools, totalTokens); + return `${bullet}${chalk.hex(colors.primary).bold(headerLabel)}${tail}`; + } + + let headerText = `Running ${String(total)} agents`; + // Mixed status gets a breakdown so the current state is clear. + if (finished > 0) { + const running = total - finished; + const parts: string[] = []; + if (done > 0) parts.push(`${String(done)} done`); + if (failed > 0) parts.push(`${String(failed)} failed`); + if (running > 0) parts.push(`${String(running)} running`); + headerText = `Running ${String(total)} agents (${parts.join(', ')})`; + } + return `${bullet}${chalk.hex(colors.primary).bold(headerText)}`; + } + + private appendLines(snap: ToolCallSubagentSnapshot, isLast: boolean): void { + const colors = this.colors; + const dim = chalk.dim; + + // First-level branch line. + const branch1 = isLast ? '└─' : '├─'; + const agentType = snap.agentName ?? 'agent'; + const desc = snap.toolCallDescription || '(no description)'; + const tail = formatLineTail(snap, colors); + const namePart = chalk.hex(colors.primary)(agentType); + const descPart = dim(`· ${desc}`); + const stats = formatStats(snap); + const line1 = ` ${branch1} ${namePart} ${descPart}${stats}${tail}`; + this.bodyContainer.addChild(new Text(line1, 0, 0)); + + // Second-level line: latest activity, or Error for failures. + const branch2 = isLast ? ' ' : '│ '; + if (snap.phase === 'failed') { + // Show one error line; error messages can be long. + const errLine = (snap.errorText ?? 'Failed').split('\n').at(0) ?? 'Failed'; + const errStr = chalk.hex(colors.error)(`Error: ${errLine}`); + this.bodyContainer.addChild(new Text(` ${branch2} ${errStr}`, 0, 0)); + return; + } + if (snap.phase === 'done' || snap.phase === 'backgrounded') { + // Terminal states omit the second line. + return; + } + // Running or not-yet-started agents show latest activity, with a fallback. + const activity = snap.latestActivity ?? 'Initializing…'; + this.bodyContainer.addChild(new Text(` ${branch2} ${dim(activity)}`, 0, 0)); + } + + /** Releases throttle timers so destroyed components cannot refresh later. */ + dispose(): void { + if (this.throttleTimer !== null) { + clearTimeout(this.throttleTimer); + this.throttleTimer = null; + } + for (const e of this.entries) { + e.tc.setSnapshotListener(undefined); + } + } +} + +function formatStats(snap: ToolCallSubagentSnapshot): string { + const dim = chalk.dim; + const tools = ` · ${String(snap.toolCount)} tool${snap.toolCount === 1 ? '' : 's'}`; + const tokens = snap.tokens > 0 ? ` · ${formatTokens(snap.tokens)}` : ''; + return dim(`${tools}${tokens}`); +} + +function formatLineTail(snap: ToolCallSubagentSnapshot, colors: ColorPalette): string { + if (snap.phase === 'done') { + return chalk.dim(' · ') + chalk.hex(colors.success)('✓ Completed'); + } + if (snap.phase === 'failed') { + return chalk.dim(' · ') + chalk.hex(colors.error)('✗ Failed'); + } + if (snap.phase === 'backgrounded') { + return chalk.dim(' · ◐ backgrounded'); + } + return ''; +} + +function formatHeaderTail(toolCount: number, tokens: number): string { + const dim = chalk.dim; + const parts: string[] = []; + if (toolCount > 0) parts.push(`${String(toolCount)} tool${toolCount === 1 ? '' : 's'}`); + if (tokens > 0) parts.push(formatTokens(tokens)); + return parts.length > 0 ? dim(` · ${parts.join(' · ')}`) : ''; +} + +function formatTokens(n: number): string { + if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M tok`; + if (n >= 1_000) return `${(n / 1_000).toFixed(1)}k tok`; + return `${String(n)} tok`; +} diff --git a/apps/kimi-code/src/tui/components/messages/assistant-message.ts b/apps/kimi-code/src/tui/components/messages/assistant-message.ts new file mode 100644 index 000000000..1be89b2ca --- /dev/null +++ b/apps/kimi-code/src/tui/components/messages/assistant-message.ts @@ -0,0 +1,63 @@ +/** + * Renders an assistant message using pi-tui Markdown. + * + * Displays a white bullet prefix with markdown content indented + * to align after the bullet. + */ + +import type { Component, MarkdownTheme } from '@earendil-works/pi-tui'; +import { Container, Markdown, visibleWidth } from '@earendil-works/pi-tui'; +import chalk from 'chalk'; + +import { MESSAGE_INDENT } from '#/tui/constant/rendering'; +import { STATUS_BULLET } from '#/tui/constant/symbols'; +import type { ColorPalette } from '#/tui/theme/colors'; + +export class AssistantMessageComponent implements Component { + private contentContainer: Container; + private markdownTheme: MarkdownTheme; + private bulletColor: string; + private lastText = ''; + private showBullet: boolean; + + constructor(markdownTheme: MarkdownTheme, colors: ColorPalette, showBullet: boolean = true) { + this.markdownTheme = markdownTheme; + this.bulletColor = colors.roleAssistant; + this.showBullet = showBullet; + this.contentContainer = new Container(); + } + + setShowBullet(show: boolean): void { + this.showBullet = show; + } + + updateContent(text: string): void { + const displayText = text; + if (displayText === this.lastText) return; + this.lastText = displayText; + this.contentContainer.clear(); + if (displayText.trim().length > 0) { + this.contentContainer.addChild(new Markdown(displayText.trim(), 0, 0, this.markdownTheme)); + } + } + + invalidate(): void { + this.contentContainer.invalidate?.(); + } + + render(width: number): string[] { + if (this.lastText.trim().length === 0) return []; + + const prefix = this.showBullet ? STATUS_BULLET : MESSAGE_INDENT; + const contentWidth = Math.max(1, width - visibleWidth(prefix)); + const contentLines = this.contentContainer.render(contentWidth); + + const lines: string[] = ['']; + for (let i = 0; i < contentLines.length; i++) { + const p = + i === 0 && this.showBullet ? chalk.hex(this.bulletColor)(STATUS_BULLET) : MESSAGE_INDENT; + lines.push(p + contentLines[i]); + } + return lines; + } +} diff --git a/apps/kimi-code/src/tui/components/messages/background-agent-status.ts b/apps/kimi-code/src/tui/components/messages/background-agent-status.ts new file mode 100644 index 000000000..c1086f805 --- /dev/null +++ b/apps/kimi-code/src/tui/components/messages/background-agent-status.ts @@ -0,0 +1,42 @@ +import type { Component } from '@earendil-works/pi-tui'; +import { Text } from '@earendil-works/pi-tui'; +import chalk from 'chalk'; + +import { MESSAGE_INDENT } from '#/tui/constant/rendering'; +import { FAILURE_MARK, STATUS_BULLET } from '#/tui/constant/symbols'; +import type { ColorPalette } from '#/tui/theme/colors'; +import type { BackgroundAgentStatusData } from '#/tui/types'; + +export class BackgroundAgentStatusComponent implements Component { + constructor( + private readonly data: BackgroundAgentStatusData, + private readonly colors: ColorPalette, + ) {} + + invalidate(): void {} + + render(width: number): string[] { + const tone = + this.data.phase === 'started' + ? this.colors.primary + : this.data.phase === 'completed' + ? this.colors.success + : this.colors.error; + + const bullet = + this.data.phase === 'failed' ? chalk.hex(tone)(FAILURE_MARK) : chalk.hex(tone)(STATUS_BULLET); + const text = + chalk.hex(tone)(this.data.headline) + + (this.data.detail !== undefined && this.data.detail.length > 0 + ? chalk.hex(this.colors.textDim)(` (${this.data.detail})`) + : ''); + + const textComponent = new Text(text, 0, 0); + const contentWidth = Math.max(1, width - MESSAGE_INDENT.length); + const contentLines = textComponent.render(contentWidth); + return [ + '', + ...contentLines.map((line, index) => (index === 0 ? bullet : MESSAGE_INDENT) + line), + ]; + } +} diff --git a/apps/kimi-code/src/tui/components/messages/mcp-status-panel.ts b/apps/kimi-code/src/tui/components/messages/mcp-status-panel.ts new file mode 100644 index 000000000..9b7905eb0 --- /dev/null +++ b/apps/kimi-code/src/tui/components/messages/mcp-status-panel.ts @@ -0,0 +1,143 @@ +import type { McpServerInfo } from '@moonshot-ai/kimi-code-sdk'; +import chalk from 'chalk'; + +import type { ColorPalette } from '#/tui/theme/colors'; + +export interface McpStatusReportOptions { + readonly colors: ColorPalette; + readonly servers: readonly McpServerInfo[]; +} + +const STATUS_PRIORITY: Record<McpServerInfo['status'], number> = { + failed: 0, + 'needs-auth': 1, + pending: 2, + connected: 3, + disabled: 4, +}; + +const STATUS_LABEL: Record<McpServerInfo['status'], string> = { + connected: 'connected', + pending: 'pending', + 'needs-auth': 'needs auth', + failed: 'failed', + disabled: 'disabled', +}; + +const SUMMARY_ORDER: readonly McpServerInfo['status'][] = [ + 'connected', + 'pending', + 'needs-auth', + 'failed', + 'disabled', +]; + +function statusPainter( + status: McpServerInfo['status'], + colors: ColorPalette, +): (text: string) => string { + switch (status) { + case 'connected': + return chalk.hex(colors.success); + case 'failed': + return chalk.hex(colors.error); + case 'needs-auth': + case 'pending': + return chalk.hex(colors.warning); + case 'disabled': + return chalk.hex(colors.textDim); + } +} + +function formatToolCount(server: McpServerInfo): string { + if (server.status === 'disabled') return '—'; + return `${server.toolCount} tool${server.toolCount === 1 ? '' : 's'}`; +} + +function formatToolsAvailable(count: number): string { + return `${count} tool${count === 1 ? '' : 's'} available`; +} + +function sortedServers(servers: readonly McpServerInfo[]): McpServerInfo[] { + return servers.toSorted( + (a, b) => + STATUS_PRIORITY[a.status] - STATUS_PRIORITY[b.status] || a.name.localeCompare(b.name), + ); +} + +function buildSummary(servers: readonly McpServerInfo[]): string { + const counts: Partial<Record<McpServerInfo['status'], number>> = {}; + let toolsAvailable = 0; + for (const server of servers) { + counts[server.status] = (counts[server.status] ?? 0) + 1; + if (server.status === 'connected') toolsAvailable += server.toolCount; + } + const parts: string[] = []; + for (const status of SUMMARY_ORDER) { + const n = counts[status]; + if (n === undefined || n === 0) continue; + parts.push(`${n} ${STATUS_LABEL[status]}`); + } + parts.push(formatToolsAvailable(toolsAvailable)); + return parts.join(' · '); +} + +export function buildMcpStatusReportLines(options: McpStatusReportOptions): string[] { + const servers = sortedServers(options.servers); + const colors = options.colors; + const accent = chalk.hex(colors.primary).bold; + const muted = chalk.hex(colors.textDim); + const value = chalk.hex(colors.text); + const error = chalk.hex(colors.error); + + const lines: string[] = [accent('Servers')]; + + if (servers.length === 0) { + lines.push(muted(' No MCP servers configured. Run /mcp-config to add one.')); + return lines; + } + + const nameWidth = Math.max('Name'.length, ...servers.map((server) => server.name.length)); + const statusWidth = Math.max( + 'Status'.length, + ...servers.map((server) => STATUS_LABEL[server.status].length), + ); + const transportWidth = Math.max( + 'Transport'.length, + ...servers.map((server) => server.transport.length), + ); + + lines.push( + ` ${muted('Name'.padEnd(nameWidth))} ${muted('Status'.padEnd(statusWidth))} ${muted( + 'Transport'.padEnd(transportWidth), + )} ${muted('Tools')}`, + ); + + for (const server of servers) { + const status = statusPainter( + server.status, + colors, + )(STATUS_LABEL[server.status].padEnd(statusWidth)); + lines.push( + ` ${value(server.name.padEnd(nameWidth))} ${status} ${muted( + server.transport.padEnd(transportWidth), + )} ${value(formatToolCount(server))}`, + ); + + if ( + server.status === 'failed' && + server.error !== undefined && + server.error.trim().length > 0 + ) { + lines.push(` ${muted('error:')} ${error(server.error.trim())}`); + } + if (server.status === 'needs-auth') { + lines.push(` ${muted('action:')} ${value(`run /mcp-config login ${server.name}`)}`); + } + } + + lines.push(''); + lines.push(` ${value(buildSummary(servers))}`); + + return lines; +} diff --git a/apps/kimi-code/src/tui/components/messages/plan-box.ts b/apps/kimi-code/src/tui/components/messages/plan-box.ts new file mode 100644 index 000000000..8f16e0580 --- /dev/null +++ b/apps/kimi-code/src/tui/components/messages/plan-box.ts @@ -0,0 +1,124 @@ +/** + * PlanBoxComponent — renders an ExitPlanMode plan inside a full box + * border, width-aware. The plan text is parsed as Markdown so headings, + * lists, bold, inline code etc. render the same way assistant messages do. + */ + +import path from 'node:path'; +import { pathToFileURL } from 'node:url'; + +import type { Component, MarkdownTheme } from '@earendil-works/pi-tui'; +import { Markdown, visibleWidth } from '@earendil-works/pi-tui'; +import chalk from 'chalk'; + +const LEFT_MARGIN = 2; // two-space indent matching other tool call children +const SIDE_PADDING = 1; // space between the │ and the content on each side +const TITLE_PREFIX = ' plan: '; +const TITLE_SUFFIX = ' '; + +export interface PlanBoxOptions { + maxContentLines?: number; + expanded?: boolean; +} + +export class PlanBoxComponent implements Component { + private readonly markdown: Markdown; + private readonly maxContentLines: number | undefined; + private readonly expanded: boolean; + private cachedWidth: number | undefined; + private cachedLines: string[] | undefined; + + constructor( + plan: string, + markdownTheme: MarkdownTheme, + private readonly borderHex: string, + private readonly planPath?: string, + opts?: PlanBoxOptions, + ) { + // Build the Markdown instance once — pi-tui's Markdown caches its own + // parse + wrap output keyed on (text, width), so reusing the same + // instance means repeated render() calls from the parent Container + // hit the cache instead of re-parsing on every frame. + this.markdown = new Markdown(plan.trim(), 0, 0, markdownTheme); + this.maxContentLines = opts?.maxContentLines; + this.expanded = opts?.expanded ?? false; + } + + invalidate(): void { + this.cachedWidth = undefined; + this.cachedLines = undefined; + this.markdown.invalidate?.(); + } + + render(width: number): string[] { + if (this.cachedLines !== undefined && this.cachedWidth === width) { + return this.cachedLines; + } + + // Box layout: " ┌──...──┐" + // " │ <content> │" + // " └──...──┘" + // width = LEFT_MARGIN + 1 + horzLen + 1 ⇒ horzLen = width - 4 + // content width = horzLen - 2 * SIDE_PADDING = width - 6 + const horzLen = Math.max(2, width - LEFT_MARGIN - 2); + const contentWidth = Math.max(1, horzLen - 2 * SIDE_PADDING); + + const paint = (s: string): string => chalk.hex(this.borderHex)(s); + const indent = ' '.repeat(LEFT_MARGIN); + + const title = this.buildTitle(horzLen); + const trailingDashLen = Math.max(0, horzLen - visibleWidth(title)); + const top = + indent + paint('┌') + paint(title) + paint('─'.repeat(trailingDashLen)) + paint('┐'); + const bottom = indent + paint('└' + '─'.repeat(horzLen) + '┘'); + + const rawLines = this.markdown.render(contentWidth); + const { shown, hiddenCount } = this.capContentLines(rawLines); + + const lines: string[] = [top]; + for (const raw of shown) { + const pad = Math.max(0, contentWidth - visibleWidth(raw)); + lines.push(indent + paint('│') + ' ' + raw + ' '.repeat(pad) + ' ' + paint('│')); + } + if (hiddenCount > 0) { + const footer = chalk.dim( + `... (${String(hiddenCount)} more line${hiddenCount === 1 ? '' : 's'}, ctrl+e to expand)`, + ); + const pad = Math.max(0, contentWidth - visibleWidth(footer)); + lines.push(indent + paint('│') + ' ' + footer + ' '.repeat(pad) + ' ' + paint('│')); + } + lines.push(bottom); + + this.cachedWidth = width; + this.cachedLines = lines; + return lines; + } + + private capContentLines(rawLines: string[]): { shown: string[]; hiddenCount: number } { + const cap = this.maxContentLines; + if (this.expanded || cap === undefined || rawLines.length <= cap) { + return { shown: rawLines, hiddenCount: 0 }; + } + const shownCount = Math.max(0, cap - 1); + return { shown: rawLines.slice(0, shownCount), hiddenCount: rawLines.length - shownCount }; + } + + private buildTitle(horzLen: number): string { + const fallback = ' plan '; + const planPath = this.planPath; + if (planPath === undefined || planPath.length === 0) return fallback; + const basename = path.basename(planPath); + if (basename.length === 0) return fallback; + const wrapperLen = TITLE_PREFIX.length + TITLE_SUFFIX.length; + const budget = horzLen - 1; + if (wrapperLen + basename.length > budget) return fallback; + const linked = path.isAbsolute(planPath) + ? toTerminalHyperlink(basename, pathToFileURL(planPath).href) + : basename; + return TITLE_PREFIX + linked + TITLE_SUFFIX; + } +} + +function toTerminalHyperlink(text: string, url: string): string { + return `\u001B]8;;${url}\u0007${text}\u001B]8;;\u0007`; +} diff --git a/apps/kimi-code/src/tui/components/messages/read-group.ts b/apps/kimi-code/src/tui/components/messages/read-group.ts new file mode 100644 index 000000000..1d48f3c37 --- /dev/null +++ b/apps/kimi-code/src/tui/components/messages/read-group.ts @@ -0,0 +1,188 @@ +/** + * ReadGroupComponent renders 2+ Read tool calls from the same step as one group. + * + * It follows the same structure as `AgentGroupComponent`, with a smaller + * surface: + * - one summary header and a tree body listing each file path and status; + * - permanently grouped, while the body remains visible; + * - 200ms throttling, matching AgentGroup; + * - state stays in each `ToolCallComponent`; the group only reads snapshots. + * + * Header forms: + * pending > 0: Reading {N} files + * all done: Read {N} files · {L} lines + * some failed: append · {F} failed + * all failed: Read {N} files · failed + * + * Body lines follow AgentGroup's branch style: + * src/main.ts · 51 lines + * src/cli.ts · reading + * src/missing.ts · failed + */ + +import type { TUI } from '@earendil-works/pi-tui'; +import { Container, Spacer, Text } from '@earendil-works/pi-tui'; +import chalk from 'chalk'; + +import { STATUS_BULLET } from '#/tui/constant/symbols'; +import type { ColorPalette } from '#/tui/theme/colors'; + +import type { ToolCallComponent, ToolCallReadSnapshot } from './tool-call'; + +const THROTTLE_MS = 200; + +interface ReadEntry { + readonly toolCallId: string; + readonly tc: ToolCallComponent; +} + +export class ReadGroupComponent extends Container { + private readonly entries: ReadEntry[] = []; + private readonly headerText: Text; + private readonly bodyContainer: Container; + private throttleTimer: ReturnType<typeof setTimeout> | null = null; + private lastFlushPhases = new Map<string, ToolCallReadSnapshot['phase']>(); + + constructor( + private readonly colors: ColorPalette, + private readonly ui: TUI | undefined, + ) { + super(); + this.addChild(new Spacer(1)); + this.headerText = new Text('', 0, 0); + this.addChild(this.headerText); + this.bodyContainer = new Container(); + this.addChild(this.bodyContainer); + } + + size(): number { + return this.entries.length; + } + + /** + * Borrows a standalone `ToolCallComponent` into the group as a hidden state + * container. Snapshot changes trigger throttled refreshes. Re-attaching the + * same toolCallId is a no-op. + */ + attach(toolCallId: string, tc: ToolCallComponent): void { + if (this.entries.some((e) => e.toolCallId === toolCallId)) return; + this.entries.push({ toolCallId, tc }); + tc.setSnapshotListener(() => { + this.scheduleRender(); + }); + this.flushRender(); + } + + /** + * The pending -> done/failed transition is the important visible change, so + * it refreshes immediately. Other changes are throttled. + */ + private scheduleRender(): void { + if (this.detectPhaseTransition()) { + this.flushRender(); + return; + } + if (this.throttleTimer !== null) return; + this.throttleTimer = setTimeout(() => { + this.throttleTimer = null; + this.flushRender(); + }, THROTTLE_MS); + } + + private detectPhaseTransition(): boolean { + for (const e of this.entries) { + const phase = e.tc.getReadSnapshot().phase; + if (this.lastFlushPhases.get(e.toolCallId) !== phase) return true; + } + return false; + } + + private flushRender(): void { + if (this.throttleTimer !== null) { + clearTimeout(this.throttleTimer); + this.throttleTimer = null; + } + + const snapshots = this.entries.map((e) => e.tc.getReadSnapshot()); + let pending = 0; + let failed = 0; + let totalLines = 0; + for (const snap of snapshots) { + if (snap.phase === 'pending') pending += 1; + else if (snap.phase === 'failed') failed += 1; + else totalLines += snap.lines; + } + this.headerText.setText(this.buildHeader(snapshots.length, pending, failed, totalLines)); + + this.bodyContainer.clear(); + const visibleSnapshots = snapshots.filter( + (snap) => snap.filePath !== undefined && snap.filePath.length > 0, + ); + visibleSnapshots.forEach((snap, idx) => { + const isLast = idx === visibleSnapshots.length - 1; + this.bodyContainer.addChild(new Text(this.buildBodyLine(snap, isLast), 0, 0)); + }); + + this.lastFlushPhases.clear(); + this.entries.forEach((entry, i) => { + const snap = snapshots[i]; + if (snap !== undefined) this.lastFlushPhases.set(entry.toolCallId, snap.phase); + }); + + this.invalidate(); + this.ui?.requestRender(); + } + + private buildHeader(total: number, pending: number, failed: number, totalLines: number): string { + const colors = this.colors; + const dim = chalk.dim; + + if (pending > 0) { + const bullet = chalk.hex(colors.roleAssistant)(STATUS_BULLET); + const label = chalk.hex(colors.primary).bold(`Reading ${String(total)} files…`); + return `${bullet}${label}`; + } + + // All reads have finished, either successfully or with failures. + if (failed === total) { + const bullet = chalk.hex(colors.error)('✗ '); + const label = chalk.hex(colors.error).bold(`Read ${String(total)} files`); + return `${bullet}${label}${chalk.hex(colors.error)(' · failed')}`; + } + + const bullet = chalk.hex(colors.success)(STATUS_BULLET); + const label = chalk.hex(colors.primary).bold(`Read ${String(total)} files`); + const linesPart = dim(` · ${String(totalLines)} ${totalLines === 1 ? 'line' : 'lines'}`); + const failPart = failed > 0 ? chalk.hex(colors.error)(` · ${String(failed)} failed`) : ''; + return `${bullet}${label}${linesPart}${failPart}`; + } + + private buildBodyLine(snap: ToolCallReadSnapshot, isLast: boolean): string { + const colors = this.colors; + const dim = chalk.dim; + const branch = isLast ? '└─' : '├─'; + const path = snap.filePath ?? ''; + const pathPart = chalk.hex(colors.text)(path); + + let tail: string; + if (snap.phase === 'pending') { + tail = dim(' · reading…'); + } else if (snap.phase === 'failed') { + tail = chalk.hex(colors.error)(' · failed'); + } else { + tail = dim(` · ${String(snap.lines)} ${snap.lines === 1 ? 'line' : 'lines'}`); + } + return ` ${branch} ${pathPart}${tail}`; + } + + /** Releases throttle timers so destroyed components cannot refresh later. */ + dispose(): void { + if (this.throttleTimer !== null) { + clearTimeout(this.throttleTimer); + this.throttleTimer = null; + } + for (const e of this.entries) { + e.tc.setSnapshotListener(undefined); + } + } +} diff --git a/apps/kimi-code/src/tui/components/messages/shell-execution.ts b/apps/kimi-code/src/tui/components/messages/shell-execution.ts new file mode 100644 index 000000000..45279fd94 --- /dev/null +++ b/apps/kimi-code/src/tui/components/messages/shell-execution.ts @@ -0,0 +1,88 @@ +import type { Component } from '@earendil-works/pi-tui'; +import { Container, Text } from '@earendil-works/pi-tui'; +import chalk from 'chalk'; + +import { COMMAND_PREVIEW_LINES } from '#/tui/constant/rendering'; +import type { ColorPalette } from '#/tui/theme/colors'; +import type { ToolCallBlockData, ToolResultBlockData } from '#/tui/types'; + +import type { ResultRenderer } from './tool-renderers/types'; +import { PREVIEW_LINES } from './tool-renderers/types'; + +export interface ShellExecutionOptions { + readonly command?: string; + readonly result?: ToolResultBlockData; + readonly colors: ColorPalette; + readonly expanded?: boolean; + readonly showCommand?: boolean; + readonly commandPreviewLines?: number; + readonly resultPreviewLines?: number; +} + +export class ShellExecutionComponent extends Container { + constructor(options: ShellExecutionOptions) { + super(); + + if (options.showCommand === true) { + this.addCommandPreview( + options.command ?? '', + options.commandPreviewLines ?? COMMAND_PREVIEW_LINES, + ); + } + + if (options.result !== undefined) { + this.addResultPreview( + options.result, + options.colors, + options.expanded ?? false, + options.resultPreviewLines ?? PREVIEW_LINES, + ); + } + } + + private addCommandPreview(command: string, previewLines: number): void { + if (command.length === 0) return; + const lines = command.split('\n').slice(0, previewLines); + for (const [i, line] of lines.entries()) { + const prefix = i === 0 ? '$ ' : ' '; + this.addChild(new Text(chalk.dim(prefix + line), 2, 0)); + } + } + + private addResultPreview( + result: ToolResultBlockData, + colors: ColorPalette, + expanded: boolean, + previewLines: number, + ): void { + if (!result.output) return; + const tint = result.is_error ? chalk.hex(colors.error) : chalk.dim; + if (expanded) { + this.addChild(new Text(tint(result.output), 2, 0)); + return; + } + + const lines = result.output.split('\n'); + const shown = lines.slice(0, previewLines); + const remaining = lines.length - shown.length; + this.addChild(new Text(tint(shown.join('\n')), 2, 0)); + if (remaining > 0) { + this.addChild( + new Text(chalk.dim(`... (${String(remaining)} more lines, ctrl+o to expand)`), 2, 0), + ); + } + } +} + +export const shellExecutionResultRenderer: ResultRenderer = ( + toolCall: ToolCallBlockData, + result: ToolResultBlockData, + ctx, +): Component[] => [ + new ShellExecutionComponent({ + command: typeof toolCall.args['command'] === 'string' ? toolCall.args['command'] : '', + result, + colors: ctx.colors, + expanded: ctx.expanded, + }), +]; diff --git a/apps/kimi-code/src/tui/components/messages/skill-activation.ts b/apps/kimi-code/src/tui/components/messages/skill-activation.ts new file mode 100644 index 000000000..7f9e05c9f --- /dev/null +++ b/apps/kimi-code/src/tui/components/messages/skill-activation.ts @@ -0,0 +1,36 @@ +/** + * Skill activation card. + * + * When the user runs `/skill:foo bar`, the TUI renders a compact card instead + * of expanding the SKILL.md body into the user bubble: + * + * ▶ Activated skill: foo + * bar + * + * The args line is optional. Core expands the skill body into the LLM context; + * the TUI only consumes the `skill.activated` event and user_message origin + * metadata. + */ + +import { Container, Text, Spacer } from '@earendil-works/pi-tui'; +import chalk from 'chalk'; + +import type { ColorPalette } from '#/tui/theme/colors'; + +const ARGS_PREVIEW_MAX = 200; + +export class SkillActivationComponent extends Container { + constructor(name: string, args: string | undefined, colors: ColorPalette) { + super(); + this.addChild(new Spacer(1)); + const head = + chalk.hex(colors.primary).bold('▶ Activated skill: ') + chalk.hex(colors.roleUser).bold(name); + this.addChild(new Text(head, 0, 0)); + const trimmed = args?.trim() ?? ''; + if (trimmed.length > 0) { + const preview = + trimmed.length > ARGS_PREVIEW_MAX ? trimmed.slice(0, ARGS_PREVIEW_MAX) + '…' : trimmed; + this.addChild(new Text(' ' + chalk.hex(colors.textDim)(preview), 0, 0)); + } + } +} diff --git a/apps/kimi-code/src/tui/components/messages/status-message.ts b/apps/kimi-code/src/tui/components/messages/status-message.ts new file mode 100644 index 000000000..be2292358 --- /dev/null +++ b/apps/kimi-code/src/tui/components/messages/status-message.ts @@ -0,0 +1,23 @@ +import { Container, Spacer, Text } from '@earendil-works/pi-tui'; +import chalk from 'chalk'; + +import type { ColorPalette } from '../../theme/colors'; + +export class StatusMessageComponent extends Container { + constructor(content: string, colors: ColorPalette, color?: string) { + super(); + const text = color === undefined ? chalk.hex(colors.textDim)(content) : chalk.hex(color)(content); + this.addChild(new Text(` ${text}`, 0, 0)); + } +} + +export class NoticeMessageComponent extends Container { + constructor(title: string, detail: string | undefined, colors: ColorPalette) { + super(); + this.addChild(new Spacer(1)); + this.addChild(new Text(` ${chalk.hex(colors.textStrong)(title)}`, 0, 0)); + if (detail !== undefined && detail.length > 0) { + this.addChild(new Text(` ${chalk.hex(colors.textDim)(detail)}`, 0, 0)); + } + } +} diff --git a/apps/kimi-code/src/tui/components/messages/status-panel.ts b/apps/kimi-code/src/tui/components/messages/status-panel.ts new file mode 100644 index 000000000..c60e2f546 --- /dev/null +++ b/apps/kimi-code/src/tui/components/messages/status-panel.ts @@ -0,0 +1,148 @@ +/** + * Status report line builder for `/status`. + * + * It mirrors `/usage` visual language but keeps runtime status formatting + * separate from the TUI orchestration layer. + */ + +import type { ModelAlias, PermissionMode, SessionStatus } from '@moonshot-ai/kimi-code-sdk'; +import chalk from 'chalk'; + +import { PRODUCT_NAME } from '#/constant/app'; +import type { ColorPalette } from '#/tui/theme/colors'; +import { + formatTokenCount, + ratioSeverity, + renderProgressBar, + safeUsageRatio, +} from '#/utils/usage/usage-format'; + +import { buildManagedUsageReportLines, type ManagedUsageReport } from './usage-panel'; + +interface FieldRow { + readonly label: string; + readonly value: string; + readonly severity?: 'error'; +} + +export interface StatusReportOptions { + readonly colors: ColorPalette; + readonly version: string; + readonly model: string; + readonly workDir: string; + readonly sessionId: string; + readonly sessionTitle: string | null; + readonly thinking: boolean; + readonly permissionMode: PermissionMode; + readonly planMode: boolean; + readonly contextUsage: number; + readonly contextTokens: number; + readonly maxContextTokens: number; + readonly availableModels: Record<string, ModelAlias>; + readonly status?: SessionStatus; + readonly statusError?: string; + readonly managedUsage?: ManagedUsageReport; + readonly managedUsageError?: string; +} + +type Colorize = (text: string) => string; + +function displayModelName(alias: string, models: Record<string, ModelAlias>): string { + const model = models[alias]; + return model?.displayName ?? model?.model ?? alias; +} + +function formatModelStatus(options: StatusReportOptions): string { + const model = options.status?.model ?? options.model; + if (model.trim().length === 0) return 'not set'; + + const thinking = (options.status?.thinkingLevel ?? (options.thinking ? 'on' : 'off')) === 'off' + ? 'off' + : 'on'; + return `${displayModelName(model, options.availableModels)} (thinking ${thinking})`; +} + +function addFieldRows( + lines: string[], + rows: readonly FieldRow[], + muted: Colorize, + value: Colorize, + errorStyle: Colorize, +): void { + const labelWidth = Math.max(10, ...rows.map((row) => row.label.length)); + for (const row of rows) { + const colorize = row.severity === 'error' ? errorStyle : value; + lines.push(` ${muted(row.label.padEnd(labelWidth, ' '))} ${colorize(row.value)}`); + } +} + +function contextValues(options: StatusReportOptions): { + ratio: number; + tokens: number; + maxTokens: number; +} { + return { + ratio: options.status?.contextUsage ?? options.contextUsage, + tokens: options.status?.contextTokens ?? options.contextTokens, + maxTokens: options.status?.maxContextTokens ?? options.maxContextTokens, + }; +} + +export function buildStatusReportLines(options: StatusReportOptions): string[] { + const colors = options.colors; + const accent = chalk.hex(colors.primary).bold; + const value = chalk.hex(colors.text); + const muted = chalk.hex(colors.textDim); + const errorStyle = chalk.hex(colors.error); + const severityHex = (sev: 'ok' | 'warn' | 'danger'): string => + sev === 'danger' ? colors.error : sev === 'warn' ? colors.warning : colors.success; + + const permission = options.status?.permission ?? options.permissionMode; + const planMode = options.status?.planMode ?? options.planMode; + const sessionId = options.sessionId.trim().length > 0 ? options.sessionId : 'none'; + const rows: FieldRow[] = [ + { label: 'Model', value: formatModelStatus(options) }, + { label: 'Directory', value: options.workDir }, + { label: 'Permissions', value: permission }, + { label: 'Plan mode', value: planMode ? 'on' : 'off' }, + { label: 'Session', value: sessionId }, + ]; + const title = options.sessionTitle?.trim(); + if (title !== undefined && title.length > 0) rows.push({ label: 'Title', value: title }); + if (options.statusError !== undefined) { + rows.push({ label: 'Warning', value: options.statusError, severity: 'error' }); + } + + const lines: string[] = [ + `${accent(`>_ ${PRODUCT_NAME}`)} ${muted(`(v${options.version})`)}`, + '', + ]; + addFieldRows(lines, rows, muted, value, errorStyle); + + const { ratio, tokens, maxTokens } = contextValues(options); + lines.push(''); + lines.push(accent('Context window')); + if (maxTokens > 0) { + const safeRatio = safeUsageRatio(ratio); + const bar = renderProgressBar(safeRatio, 20); + const barColoured = chalk.hex(severityHex(ratioSeverity(safeRatio)))(bar); + lines.push( + ` ${barColoured} ${value(`${(safeRatio * 100).toFixed(1)}%`.padStart(6, ' '))} ` + + muted(`(${formatTokenCount(tokens)} / ${formatTokenCount(maxTokens)})`), + ); + } else { + lines.push(` ${muted('No context window data available.')}`); + } + + const managedSection = buildManagedUsageReportLines({ + colors, + managedUsage: options.managedUsage, + managedUsageError: options.managedUsageError, + }); + if (managedSection.length > 0) { + lines.push(''); + lines.push(...managedSection); + } + + return lines; +} diff --git a/apps/kimi-code/src/tui/components/messages/thinking.ts b/apps/kimi-code/src/tui/components/messages/thinking.ts new file mode 100644 index 000000000..632cef307 --- /dev/null +++ b/apps/kimi-code/src/tui/components/messages/thinking.ts @@ -0,0 +1,122 @@ +/** + * Renders thinking content in the transcript. + * Supports live in-place updates while thinking streams, then finalizes + * without replacing the component. + * Supports expand/collapse via Ctrl+O (shared with tool output). + */ + +import type { Component, TUI } from '@earendil-works/pi-tui'; +import { Text } from '@earendil-works/pi-tui'; +import chalk from 'chalk'; + +import { + BRAILLE_SPINNER_FRAMES, + BRAILLE_SPINNER_INTERVAL_MS, + MESSAGE_INDENT, + RESULT_PREVIEW_LINES, +} from '#/tui/constant/rendering'; +import { STATUS_BULLET } from '#/tui/constant/symbols'; +import type { ColorPalette } from '#/tui/theme/colors'; + +export type ThinkingRenderMode = 'live' | 'finalized'; + +export class ThinkingComponent implements Component { + private text: string; + private color: string; + private showMarker: boolean; + private mode: ThinkingRenderMode; + private expanded = false; + private readonly ui: TUI | undefined; + private spinnerFrame = 0; + private spinnerInterval: ReturnType<typeof setInterval> | undefined; + + constructor( + text: string, + colors: ColorPalette, + showMarker: boolean = true, + mode: ThinkingRenderMode = 'finalized', + ui?: TUI, + ) { + this.text = text; + this.color = colors.roleThinking; + this.showMarker = showMarker; + this.mode = mode; + this.ui = ui; + if (mode === 'live') { + this.startSpinner(); + } + } + + invalidate(): void {} + + setText(text: string): void { + this.text = text; + } + + finalize(): void { + this.mode = 'finalized'; + this.stopSpinner(); + } + + dispose(): void { + this.stopSpinner(); + } + + setExpanded(expanded: boolean): void { + if (this.expanded === expanded) return; + this.expanded = expanded; + } + + render(width: number): string[] { + const contentWidth = Math.max(1, width - MESSAGE_INDENT.length); + const textComponent = new Text(chalk.hex(this.color).italic(this.text), 0, 0); + const contentLines = this.text.length > 0 ? textComponent.render(contentWidth) : ['']; + + if (this.mode === 'live') { + const visibleLines = + contentLines.length > RESULT_PREVIEW_LINES + ? contentLines.slice(contentLines.length - RESULT_PREVIEW_LINES) + : contentLines; + const spinner = chalk.hex(this.color)( + `${BRAILLE_SPINNER_FRAMES[this.spinnerFrame] ?? BRAILLE_SPINNER_FRAMES[0]} `, + ); + return [ + '', + spinner + chalk.hex(this.color)('thinking...'), + ...visibleLines.map((line) => MESSAGE_INDENT + line), + ]; + } + + const rendered: string[] = ['']; + for (let i = 0; i < contentLines.length; i++) { + const p = i === 0 && this.showMarker ? chalk.hex(this.color)(STATUS_BULLET) : MESSAGE_INDENT; + rendered.push(p + contentLines[i]); + } + + if (this.expanded || contentLines.length <= RESULT_PREVIEW_LINES) { + return rendered; + } + + // Leading blank + first PREVIEW_LINES content lines + hint line. + const truncated = rendered.slice(0, 1 + RESULT_PREVIEW_LINES); + const remaining = contentLines.length - RESULT_PREVIEW_LINES; + truncated.push( + MESSAGE_INDENT + chalk.dim(`... (${String(remaining)} more lines, ctrl+o to expand)`), + ); + return truncated; + } + + private startSpinner(): void { + if (this.ui === undefined || this.spinnerInterval !== undefined) return; + this.spinnerInterval = setInterval(() => { + this.spinnerFrame = (this.spinnerFrame + 1) % BRAILLE_SPINNER_FRAMES.length; + this.ui?.requestRender(); + }, BRAILLE_SPINNER_INTERVAL_MS); + } + + private stopSpinner(): void { + if (this.spinnerInterval === undefined) return; + clearInterval(this.spinnerInterval); + this.spinnerInterval = undefined; + } +} diff --git a/apps/kimi-code/src/tui/components/messages/tool-call.ts b/apps/kimi-code/src/tui/components/messages/tool-call.ts new file mode 100644 index 000000000..dc831e8be --- /dev/null +++ b/apps/kimi-code/src/tui/components/messages/tool-call.ts @@ -0,0 +1,1675 @@ +/** + * Renders a tool call entry in the transcript. + * Supports expand/collapse via Ctrl+O. + */ + +import { isAbsolute, relative, sep } from 'node:path'; + +import { Container, Text, Spacer, visibleWidth } from '@earendil-works/pi-tui'; +import type { Component, MarkdownTheme, TUI } from '@earendil-works/pi-tui'; +import chalk from 'chalk'; + +import { highlightLines, langFromPath } from '#/tui/components/media/code-highlight'; +import { renderDiffLinesClustered } from '#/tui/components/media/diff-preview'; +import { COMMAND_PREVIEW_LINES } from '#/tui/constant/rendering'; +import { STREAMING_ARGS_FIELD_RE } from '#/tui/constant/streaming'; +import { STATUS_BULLET } from '#/tui/constant/symbols'; +import type { ColorPalette } from '#/tui/theme/colors'; +import type { ToolCallBlockData, ToolResultBlockData } from '#/tui/types'; +import { decodeMcpToolName } from '#/tui/utils/mcp-tool-name'; + +import { PlanBoxComponent } from './plan-box'; +import { ShellExecutionComponent } from './shell-execution'; +import { countNonEmptyLines, pickChip } from './tool-renderers/chip'; +import { pickResultRenderer } from './tool-renderers/registry'; + +const MAX_ARG_LENGTH = 60; +const MAX_SUB_TOOL_CALLS_SHOWN = 4; +const MAX_SINGLE_SUBAGENT_TOOL_ROWS = 4; +const APPROVED_PLAN_MARKER = '## Approved Plan:'; +const STREAMING_PROGRESS_INTERVAL_MS = 1000; +const SUBAGENT_ELAPSED_INTERVAL_MS = 1000; +const PROGRESS_URL_RE = /https?:\/\/\S+/g; + +type SubagentTextKind = 'thinking' | 'text'; + +interface SubagentTokenUsage { + readonly input?: number | undefined; + readonly inputOther?: number | undefined; + readonly inputCacheRead?: number | undefined; + readonly inputCacheCreation?: number | undefined; + readonly output: number; +} + +interface FinishedSubCall { + readonly name: string; + readonly args: Record<string, unknown>; + readonly output: string; + readonly isError: boolean; +} + +interface OngoingSubCall { + readonly name: string; + readonly args: Record<string, unknown>; + readonly streamingArguments?: string | undefined; +} + +interface SubToolActivity { + readonly id: string; + name: string; + args: Record<string, unknown>; + phase: 'ongoing' | 'done' | 'failed'; + readonly orderSeq: number; +} + +/** + * Immutable subagent state snapshot. `AgentGroupComponent` reads one-time + * views via `ToolCallComponent.getSubagentSnapshot()` and renders its own + * branch lines; `onSnapshotChange` notifies it when state changes. + * + * `latestActivity` priority, used only while running: + * 1. latest ongoing sub-tool (`Using {name} ({keyArg})`) + * 2. latest finished sub-tool (`Used {name} ({keyArg})`) + * 3. last non-empty line from accumulated subagent text + */ +export interface ToolCallSubagentSnapshot { + readonly toolCallId: string; + readonly toolName: string; + readonly toolCallDescription: string; + readonly agentName: string | undefined; + readonly phase: 'spawning' | 'running' | 'done' | 'failed' | 'backgrounded' | undefined; + readonly toolCount: number; + readonly tokens: number; + readonly isError: boolean; + readonly errorText: string | undefined; + readonly latestActivity: string | undefined; +} + +/** + * Immutable Read tool state snapshot. `ReadGroupComponent` reads one-time + * views via `ToolCallComponent.getReadSnapshot()` and sums lines for the group + * header. `lines` is 0 while pending or failed, and the non-empty result line + * count when done, matching the single-card chip. + */ +export interface ToolCallReadSnapshot { + readonly toolCallId: string; + readonly filePath: string | undefined; + readonly phase: 'pending' | 'done' | 'failed'; + readonly lines: number; +} + +function str(v: unknown): string { + return typeof v === 'string' ? v : ''; +} + +function usageInputTotal(usage: SubagentTokenUsage): number { + return ( + usage.input ?? + (usage.inputOther ?? 0) + (usage.inputCacheRead ?? 0) + (usage.inputCacheCreation ?? 0) + ); +} + +function formatSubagentTokens(usage: SubagentTokenUsage | undefined): string | undefined { + if (usage === undefined) return undefined; + const total = usageInputTotal(usage) + usage.output; + if (total <= 0) return undefined; + const formatted = total >= 1000 ? `${(total / 1000).toFixed(1)}k` : String(total); + return `${formatted} tok`; +} + +function formatByteSize(bytes: number): string { + if (bytes < 1024) return `${String(bytes)} B`; + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; + return `${(bytes / 1024 / 1024).toFixed(1)} MB`; +} + +function formatElapsed(seconds: number): string { + if (seconds < 60) return `${String(seconds)}s`; + const minutes = Math.floor(seconds / 60); + const remainder = seconds % 60; + return `${String(minutes)}m ${String(remainder)}s`; +} + +function extractApprovedPlan(output: string): string { + const markerIndex = output.indexOf(APPROVED_PLAN_MARKER); + if (markerIndex < 0) return ''; + return output.slice(markerIndex + APPROVED_PLAN_MARKER.length).trim(); +} + +interface ExitPlanModeOutcome { + readonly kind: 'approved' | 'rejected'; + readonly chosen?: string; + readonly feedback?: string; + readonly path?: string; +} + +const REJECT_PREFIX = 'User rejected the plan.'; +const REJECT_FEEDBACK_PREFIX = 'User rejected the plan. Feedback:'; +const APPROVED_OPTION_RE = /^User approved option "([^"]+)"\./; +const PLAN_REJECT_PREFIX = 'Plan rejected by user.'; +const SELECTED_APPROACH_RE = /^Exited plan mode\. Selected approach: ([^\n]+)\n/; +const PLAN_SAVED_TO_RE = /\nPlan saved to: ([^\n]+)\n/; + +/** + * Parses the ExitPlanMode result content string to recover the approval outcome + * and optional plan path. Core-side templates live in + * `packages/agent-core/src/tools/builtin/planning/exit-plan-mode.ts`: + * - Approved output starts with 'Exited plan mode.' and selected options + * are reported as 'Selected approach: <label>'. Older outputs may start + * with 'User approved option "<label>".' Plan-file mode may include + * 'Plan saved to: <path>'. + * - Rejected output starts with 'Plan rejected by user.' or older + * 'User rejected the plan.'; feedback uses 'User rejected the plan. + * Feedback:\n\n<text>'. + * This is a string protocol rather than a structured payload. Prefer a + * structured event payload if core starts emitting one. + */ +function interpretExitPlanModeOutcome(output: string): ExitPlanModeOutcome { + if (output.startsWith(REJECT_PREFIX)) { + if (output.startsWith(REJECT_FEEDBACK_PREFIX)) { + const feedback = output.slice(REJECT_FEEDBACK_PREFIX.length).trimStart(); + return { kind: 'rejected', feedback }; + } + return { kind: 'rejected' }; + } + if (output.startsWith(PLAN_REJECT_PREFIX)) { + return { kind: 'rejected' }; + } + const pathMatch = PLAN_SAVED_TO_RE.exec(output); + const path = pathMatch?.[1]?.trim(); + const optionMatch = SELECTED_APPROACH_RE.exec(output) ?? APPROVED_OPTION_RE.exec(output); + if (optionMatch !== null) { + return path !== undefined && path.length > 0 + ? { kind: 'approved', chosen: optionMatch[1], path } + : { kind: 'approved', chosen: optionMatch[1] }; + } + return path !== undefined && path.length > 0 ? { kind: 'approved', path } : { kind: 'approved' }; +} + +function unescapeJsonString(s: string): string { + return s.replaceAll(/\\(["\\/bfnrt])/g, (_, ch: string) => { + switch (ch) { + case 'n': + return '\n'; + case 't': + return '\t'; + case 'r': + return '\r'; + case 'b': + return '\b'; + case 'f': + return '\f'; + case '"': + return '"'; + case '\\': + return '\\'; + case '/': + return '/'; + default: + return ch; + } + }); +} + +/** + * Pull the live value of a JSON string field out of partially-streamed + * arguments, even if the closing quote hasn't arrived yet. Handles the + * common JSON string escapes so `\n` in a streamed `content` becomes a + * real newline we can highlight. Returns `undefined` if the field hasn't + * started streaming yet. + */ +function extractPartialStringField(text: string, key: string): string | undefined { + const opener = new RegExp(`"${key}"\\s*:\\s*"`); + const match = opener.exec(text); + if (match === null) return undefined; + const start = match.index + match[0].length; + let out = ''; + let i = start; + while (i < text.length) { + const ch = text[i]; + if (ch === '\\') { + const next = text[i + 1]; + if (next === undefined) return out; + switch (next) { + case 'n': + out += '\n'; + break; + case 't': + out += '\t'; + break; + case 'r': + out += '\r'; + break; + case 'b': + out += '\b'; + break; + case 'f': + out += '\f'; + break; + case '"': + out += '"'; + break; + case '\\': + out += '\\'; + break; + case '/': + out += '/'; + break; + case 'u': { + if (i + 5 >= text.length) return out; + const hex = text.slice(i + 2, i + 6); + const code = Number.parseInt(hex, 16); + if (Number.isNaN(code)) return out; + out += String.fromCodePoint(code); + i += 6; + continue; + } + default: + out += next; + } + i += 2; + continue; + } + if (ch === '"') return out; + out += ch; + i++; + } + return out; +} + +function parseArgsPreview(value: string): Record<string, unknown> { + if (value.trim().length === 0) return {}; + if (value.trimEnd().endsWith('}')) { + try { + const parsed = JSON.parse(value) as unknown; + if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) { + return parsed as Record<string, unknown>; + } + } catch { + // fall through to partial scan + } + } + const result: Record<string, unknown> = {}; + for (const match of value.matchAll(STREAMING_ARGS_FIELD_RE)) { + const key = match[1]; + const rawValue = match[2]; + if (key === undefined || rawValue === undefined) continue; + if (!(key in result)) result[key] = unescapeJsonString(rawValue); + } + return result; +} + +const PATH_KEYS = new Set(['path', 'file_path']); + +function truncateArgValue(key: string, value: string): string { + if (value.length <= MAX_ARG_LENGTH) return value; + if (PATH_KEYS.has(key)) { + // Preserve the tail (filename) — drop the prefix so the user can + // still tell which file is being touched. + return '…' + value.slice(value.length - (MAX_ARG_LENGTH - 1)); + } + return value.slice(0, MAX_ARG_LENGTH - 3) + '...'; +} + +function makeWorkspaceRelativePath(filePath: string, workspaceDir: string | undefined): string { + if (workspaceDir === undefined || workspaceDir.length === 0 || !isAbsolute(filePath)) { + return filePath; + } + const relativePath = relative(workspaceDir, filePath); + if ( + relativePath.length === 0 || + relativePath === '..' || + relativePath.startsWith(`..${sep}`) || + isAbsolute(relativePath) + ) { + return filePath; + } + return relativePath; +} + +function formatKeyArgument( + toolName: string, + key: string, + value: string, + workspaceDir: string | undefined, +): string { + const displayValue = + toolName === 'Read' && PATH_KEYS.has(key) + ? makeWorkspaceRelativePath(value, workspaceDir) + : value; + return truncateArgValue(key, displayValue); +} + +function extractKeyArgument( + toolName: string, + args: Record<string, unknown>, + workspaceDir?: string, +): string | null { + const keyMap: Record<string, string[]> = { + Bash: ['command'], + Read: ['path', 'file_path'], + Write: ['path', 'file_path'], + Edit: ['path', 'file_path'], + Grep: ['pattern'], + Glob: ['pattern'], + FetchURL: ['url'], + WebSearch: ['query'], + // Prefer the short `description` so the header preview never spills a + // multi-line `prompt` into the TUI chrome. + Agent: ['description', 'prompt'], + }; + + const candidates = keyMap[toolName] ?? Object.keys(args); + for (const key of candidates) { + const val = args[key]; + if (typeof val === 'string' && val.length > 0) { + const firstLine = val.split('\n')[0] ?? val; + return formatKeyArgument(toolName, key, firstLine, workspaceDir); + } + } + return null; +} + +function formatSubagentLabel(agentName: string | undefined): string { + const raw = agentName?.trim(); + if (raw === undefined || raw.length === 0) return 'SubAgent'; + const label = raw + .split(/[-_\s]+/) + .filter((part) => part.length > 0) + .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) + .join(' '); + if (/\bagent$/i.test(label)) return label; + return `${label} Agent`; +} + +function tailNonEmptyLines(text: string, maxLines: number): string[] { + if (text.length === 0) return []; + return text + .split('\n') + .map((line) => line.trimEnd()) + .filter((line) => line.trim().length > 0) + .slice(-maxLines); +} + +class PrefixedWrappedLine implements Component { + constructor( + private readonly firstPrefix: string, + private readonly continuationPrefix: string, + private readonly text: string, + ) { } + + invalidate(): void { } + + render(width: number): string[] { + const prefixWidth = Math.max( + visibleWidth(this.firstPrefix), + visibleWidth(this.continuationPrefix), + ); + const contentWidth = Math.max(1, width - prefixWidth); + const lines = new Text(this.text, 0, 0).render(contentWidth); + return lines.map((line, index) => + index === 0 ? `${this.firstPrefix}${line}` : `${this.continuationPrefix}${line}`, + ); + } +} + +export class ToolCallComponent extends Container { + private expanded = false; + private planExpanded = false; + private toolCall: ToolCallBlockData; + private result: ToolResultBlockData | undefined; + private colors: ColorPalette; + private ui: TUI | undefined; + private markdownTheme: MarkdownTheme | undefined; + private planPath: string | undefined; + /** + * Fallback plan body used when the LLM uses plan-file mode and + * `args.plan` is empty. `KimiTUI` calls `setPlanInfo` with + * `session.getPlan()` content so the plan box can render while + * approval is pending, and so rejected or revised results still show + * the plan body even without a `## Approved Plan:` marker. + */ + private currentPlan: string | undefined; + private headerText: Text; + private callPreviewEndIndex = 0; + + // ── Subagent state ─────────────────────────────────────────────── + // + // Populated by `setSubagentMeta` / `appendSubToolCall` / `finishSubToolCall` + // when KimiTUI routes a `subagent.event` with this tool call + // id as its `parent_tool_call_id`. Rendered at the tail of + // buildContent so it shows up both during streaming and after the + // parent tool call resolves. + private subagentAgentId: string | undefined; + private subagentAgentName: string | undefined; + private readonly ongoingSubCalls = new Map<string, OngoingSubCall>(); + private readonly finishedSubCalls: FinishedSubCall[] = []; + private readonly subToolActivities = new Map<string, SubToolActivity>(); + private subToolOrderSeq = 0; + private hiddenSubCallCount = 0; + /** + * Recent normal-output lines from the child agent. Historical replay can also + * store mixed text here. + */ + private subagentText = ''; + private subagentThinkingText = ''; + // ── Subagent lifecycle state from subagent.spawned/completed/failed ── + private subagentPhase: 'spawning' | 'running' | 'done' | 'failed' | 'backgrounded' | undefined; + private subagentUsage: SubagentTokenUsage | undefined; + private subagentResultSummary: string | undefined; + private subagentError: string | undefined; + private streamingProgressTimer: ReturnType<typeof setInterval> | undefined; + private subagentElapsedTimer: ReturnType<typeof setInterval> | undefined; + private subagentStartedAtMs: number | undefined; + private subagentEndedAtMs: number | undefined; + + // ── Live progress lines ────────────────────────────────────────── + // + // Populated by `appendProgress` whenever the tool emits an + // `onUpdate({kind:'status', text})` while still running. Used by + // long-blocking tools (e.g. the MCP `authenticate` synthetic tool + // whose 15-minute browser wait would otherwise display only a + // spinner). Cleared when the result lands — the result is the + // authoritative final state. + private progressLines: string[] = []; + private static readonly MAX_PROGRESS_LINES = 24; + + /** + * Registered by a group container (`AgentGroupComponent` or + * `ReadGroupComponent`) when this component is borrowed as a hidden state + * container. Any state change (subagent meta, phase, sub-tool, result, etc.) + * triggers a throttled group re-render. `undefined` means no group is + * subscribed and standalone rendering is unaffected. A ToolCallComponent can + * only belong to one group at a time, so one listener slot is enough. + */ + private onSnapshotChange: (() => void) | undefined; + + constructor( + toolCall: ToolCallBlockData, + result: ToolResultBlockData | undefined, + colors: ColorPalette, + ui?: TUI, + markdownTheme?: MarkdownTheme, + private readonly workspaceDir?: string, + ) { + super(); + this.toolCall = toolCall; + this.result = result; + this.colors = colors; + this.ui = ui; + this.markdownTheme = markdownTheme; + this.applySubagentReplay(toolCall.subagent); + + this.addChild(new Spacer(1)); + this.headerText = new Text(this.buildHeader(), 0, 0); + this.addChild(this.headerText); + this.buildCallPreview(); + this.callPreviewEndIndex = this.children.length; + this.buildProgressBlock(); + this.buildContent(); + this.buildSubagentBlock(); + this.syncStreamingProgressTimer(); + this.syncSubagentElapsedTimer(); + } + + setExpanded(expanded: boolean): void { + if (this.expanded === expanded) return; + this.expanded = expanded; + // rebuildBody (not rebuildContent) so the args-driven call preview + // — which is what carries Write content / Edit diff — re-renders + // with the new line cap. rebuildContent only touches result-driven + // children and would leave the call preview stuck at its initial + // collapsed size. + this.rebuildBody(); + } + + // Toggle the plan box's expanded state independently from tool-output + // expansion. Returns true iff this card actually owns a plan preview + // (ExitPlanMode), so the caller can decide whether to consume the keystroke. + setPlanExpanded(expanded: boolean): boolean { + if (this.toolCall.name !== 'ExitPlanMode') return false; + if (this.planExpanded === expanded) return true; + this.planExpanded = expanded; + this.rebuildBody(); + return true; + } + + setResult(result: ToolResultBlockData): void { + this.result = result; + // Result supersedes any live progress chatter; the result body is the + // authoritative final state. Without this clear, a finished tool would + // show both the streamed status lines and the final output stacked. + this.progressLines = []; + this.finalizeSubagentElapsedIfNeeded(); + this.syncStreamingProgressTimer(); + this.syncSubagentElapsedTimer(); + this.headerText.setText(this.buildHeader()); + // rebuildBody (not rebuildContent) so the call preview re-renders + // with the collapsed cap applied — Write streaming previews and + // Edit's progress placeholder needs to snap to the final preview on + // result. + this.rebuildBody(); + // Final results affect group summaries, especially failed/done counts. + this.notifySnapshotChange(); + } + + updateToolCall(toolCall: ToolCallBlockData): void { + this.toolCall = toolCall; + this.syncStreamingProgressTimer(); + this.headerText.setText(this.buildHeader()); + this.rebuildBody(); + this.notifySnapshotChange(); + this.ui?.requestRender(); + } + + /** + * Append a live progress line emitted by the tool via + * `onUpdate({kind:'status', text})`. Splits on newlines so multi-line + * status payloads render row-by-row. Old lines are dropped once the + * buffer fills past {@link ToolCallComponent.MAX_PROGRESS_LINES} so a + * misbehaving tool can't grow the box unboundedly. + */ + appendProgress(text: string): void { + if (this.result !== undefined) return; + for (const line of text.split('\n')) { + this.progressLines.push(line); + } + while (this.progressLines.length > ToolCallComponent.MAX_PROGRESS_LINES) { + this.progressLines.shift(); + } + this.rebuildBody(); + this.notifySnapshotChange(); + this.ui?.requestRender(); + } + + dispose(): void { + this.stopStreamingProgressTimer(); + this.stopSubagentElapsedTimer(); + } + + /** + * Injects plan body/path asynchronously. Only ExitPlanMode cards use + * this: plan-file mode leaves `args.plan` empty, so `KimiTUI` fetches + * the plan via `session.getPlan()` and calls this method to render the + * plan box. + */ + setPlanInfo(info: { plan?: string; path?: string }): void { + if (this.toolCall.name !== 'ExitPlanMode') return; + let changed = false; + if (info.plan !== undefined && info.plan.length > 0 && this.currentPlan !== info.plan) { + this.currentPlan = info.plan; + changed = true; + } + if (info.path !== undefined && info.path.length > 0 && this.planPath !== info.path) { + this.planPath = info.path; + changed = true; + } + if (!changed) return; + this.rebuildBody(); + this.ui?.requestRender(); + } + + private applySubagentReplay(subagent: ToolCallBlockData['subagent']): void { + if (subagent === undefined) return; + this.subagentAgentId = subagent.id; + this.subagentAgentName = subagent.name; + this.subagentText = subagent.text ?? ''; + for (const call of subagent.toolCalls ?? []) { + if (call.result === undefined) { + this.ongoingSubCalls.set(call.id, { name: call.name, args: call.args }); + this.upsertSubToolActivity(call.id, call.name, call.args, 'ongoing'); + continue; + } + this.finishedSubCalls.push({ + name: call.name, + args: call.args, + output: call.result.output, + isError: call.result.is_error ?? false, + }); + this.upsertSubToolActivity( + call.id, + call.name, + call.args, + call.result.is_error === true ? 'failed' : 'done', + ); + } + while (this.finishedSubCalls.length > MAX_SUB_TOOL_CALLS_SHOWN) { + this.finishedSubCalls.shift(); + this.hiddenSubCallCount += 1; + } + } + + // ── Subagent API (called by KimiTUI event routing) ─────────────── + + setSubagentMeta(agentId: string, agentName?: string): void { + if (this.subagentAgentId === agentId && this.subagentAgentName === agentName) return; + this.subagentAgentId = agentId; + this.subagentAgentName = agentName; + this.headerText.setText(this.buildHeader()); + this.rebuildContent(); + this.notifySnapshotChange(); + this.ui?.requestRender(); + } + + /** + * Lets group containers (AgentGroup or ReadGroup) subscribe to this card's + * state changes. Registration immediately calls back so the group receives + * the current snapshot without separately calling getSubagentSnapshot or + * getReadSnapshot. Pass `undefined` to unsubscribe. + */ + setSnapshotListener(cb: (() => void) | undefined): void { + this.onSnapshotChange = cb; + if (cb !== undefined) cb(); + } + + getSubagentSnapshot(): ToolCallSubagentSnapshot { + const finished = this.finishedSubCalls.length + this.hiddenSubCallCount; + const tokens = + this.subagentUsage === undefined + ? 0 + : usageInputTotal(this.subagentUsage) + this.subagentUsage.output; + const latestActivity = computeLatestActivity( + this.ongoingSubCalls, + this.finishedSubCalls, + this.getCombinedSubagentText(), + this.workspaceDir, + ); + // Terminal-state priority: SDK `tool.result` is authoritative for Agent + // tool calls. Once it arrives, force done/failed over intermediate + // spawning/running states for two reasons: + // 1. Replay does not replay spawned/completed/failed events, so + // `subagentPhase` stays undefined and result must be used. + // 2. Live type-validation failures may skip `subagent.failed`, or + // `tool.result` may arrive first; otherwise the UI can stay stuck at + // 'spawning' and keep showing `Initializing...`. + // Intermediate states without a result still use `subagentPhase`. + // `backgrounded` has no result because background agents do not enter the + // transcript. + const derivedPhase: ToolCallSubagentSnapshot['phase'] = + this.result !== undefined ? (this.result.is_error ? 'failed' : 'done') : this.subagentPhase; + return { + toolCallId: this.toolCall.id, + toolName: this.toolCall.name, + toolCallDescription: str(this.toolCall.args['description']) || str(this.toolCall.description), + agentName: this.subagentAgentName, + phase: derivedPhase, + toolCount: finished, + tokens, + isError: derivedPhase === 'failed', + errorText: + this.subagentError ?? (derivedPhase === 'failed' ? this.result?.output : undefined), + latestActivity, + }; + } + + /** + * Used by `ReadGroupComponent` to sum line counts across same-step Read + * cards. `lines` matches the single-card chip + * (`pluralize(countNonEmptyLines(...), 'line')`) so group and card counts do + * not drift. + */ + getReadSnapshot(): ToolCallReadSnapshot { + const args = this.toolCall.args; + const filePathRaw = args['file_path'] ?? args['path']; + const filePath = + typeof filePathRaw === 'string' + ? makeWorkspaceRelativePath(filePathRaw, this.workspaceDir) + : undefined; + if (this.result === undefined) { + return { toolCallId: this.toolCall.id, filePath, phase: 'pending', lines: 0 }; + } + if (this.result.is_error === true) { + return { toolCallId: this.toolCall.id, filePath, phase: 'failed', lines: 0 }; + } + return { + toolCallId: this.toolCall.id, + filePath, + phase: 'done', + lines: countNonEmptyLines(this.result.output), + }; + } + + // Readonly view for group access to toolCall metadata (id, name, description). + get toolCallView(): Readonly<ToolCallBlockData> { + return this.toolCall; + } + + /** Notifies the listener when internal state changes, if a group is attached. */ + private notifySnapshotChange(): void { + this.onSnapshotChange?.(); + } + + private upsertSubToolActivity( + id: string, + name: string, + args: Record<string, unknown>, + phase: SubToolActivity['phase'], + ): void { + const existing = this.subToolActivities.get(id); + if (existing !== undefined) { + existing.name = name; + existing.args = args; + existing.phase = phase; + return; + } + this.subToolActivities.set(id, { + id, + name, + args, + phase, + orderSeq: ++this.subToolOrderSeq, + }); + } + + private getCombinedSubagentText(): string { + return [this.subagentThinkingText, this.subagentText].filter((s) => s.length > 0).join('\n'); + } + + private isStreamingEditPreview(): boolean { + return ( + this.toolCall.name === 'Edit' && + this.result === undefined && + this.toolCall.streamingArguments !== undefined + ); + } + + private syncStreamingProgressTimer(): void { + if (!this.isStreamingEditPreview()) { + this.stopStreamingProgressTimer(); + return; + } + if (this.ui === undefined || this.streamingProgressTimer !== undefined) return; + this.streamingProgressTimer = setInterval(() => { + if (!this.isStreamingEditPreview()) { + this.stopStreamingProgressTimer(); + return; + } + this.rebuildBody(); + this.ui?.requestRender(); + }, STREAMING_PROGRESS_INTERVAL_MS); + } + + private stopStreamingProgressTimer(): void { + if (this.streamingProgressTimer === undefined) return; + clearInterval(this.streamingProgressTimer); + this.streamingProgressTimer = undefined; + } + + private syncSubagentElapsedTimer(): void { + const phase = this.getDerivedSubagentPhase(); + const shouldTick = + this.isSingleSubagentView() && + this.subagentStartedAtMs !== undefined && + (phase === 'spawning' || phase === 'running'); + if (!shouldTick) { + this.stopSubagentElapsedTimer(); + return; + } + if (this.ui === undefined || this.subagentElapsedTimer !== undefined) return; + this.subagentElapsedTimer = setInterval(() => { + const latestPhase = this.getDerivedSubagentPhase(); + if (latestPhase !== 'spawning' && latestPhase !== 'running') { + this.stopSubagentElapsedTimer(); + return; + } + this.headerText.setText(this.buildHeader()); + this.invalidate(); + this.ui?.requestRender(); + }, SUBAGENT_ELAPSED_INTERVAL_MS); + } + + private stopSubagentElapsedTimer(): void { + if (this.subagentElapsedTimer === undefined) return; + clearInterval(this.subagentElapsedTimer); + this.subagentElapsedTimer = undefined; + } + + private finalizeSubagentElapsedIfNeeded(): void { + if ( + this.toolCall.name === 'Agent' && + this.subagentStartedAtMs !== undefined && + this.subagentEndedAtMs === undefined + ) { + this.subagentEndedAtMs = Date.now(); + } + } + + /** + * Handles SDK `subagent.spawned`. The child agent is registered, but internal + * activity events (`assistant.delta` or `tool.call.started`) may not have + * arrived yet, so the UI moves to the 'spawning' placeholder state unless the + * agent is running in the background. + */ + onSubagentSpawned(meta: { + agentId: string; + agentName?: string | undefined; + runInBackground: boolean; + }): void { + this.subagentAgentId = meta.agentId; + this.subagentAgentName = meta.agentName; + this.subagentPhase = meta.runInBackground ? 'backgrounded' : 'spawning'; + this.subagentStartedAtMs = Date.now(); + this.subagentEndedAtMs = undefined; + this.syncSubagentElapsedTimer(); + this.headerText.setText(this.buildHeader()); + this.rebuildContent(); + this.notifySnapshotChange(); + this.ui?.requestRender(); + } + + /** + * Handles SDK `subagent.completed`. Moves the phase to 'done' and records + * token usage plus the result summary for the header chip and tail summary. + */ + onSubagentCompleted(payload: { + usage?: SubagentTokenUsage | undefined; + resultSummary: string; + }): void { + this.subagentPhase = 'done'; + this.subagentEndedAtMs ??= Date.now(); + this.subagentUsage = payload.usage; + this.subagentResultSummary = + payload.resultSummary.length > 0 ? payload.resultSummary : undefined; + if (this.subagentText.trim().length === 0 && this.subagentResultSummary !== undefined) { + this.subagentText = this.subagentResultSummary; + } + this.syncSubagentElapsedTimer(); + this.headerText.setText(this.buildHeader()); + this.rebuildContent(); + this.notifySnapshotChange(); + this.ui?.requestRender(); + } + + /** Handles SDK `subagent.failed`. */ + onSubagentFailed(payload: { error: string }): void { + this.subagentPhase = 'failed'; + this.subagentEndedAtMs ??= Date.now(); + this.subagentError = payload.error; + this.syncSubagentElapsedTimer(); + this.headerText.setText(this.buildHeader()); + this.rebuildContent(); + this.notifySnapshotChange(); + this.ui?.requestRender(); + } + + appendSubagentText(text: string, kind: SubagentTextKind = 'text'): void { + if (kind === 'thinking') { + this.subagentThinkingText += text; + } else { + this.subagentText += text; + } + // Child-agent activity means it is running unless already terminal/backgrounded. + if (this.subagentPhase === undefined || this.subagentPhase === 'spawning') { + this.subagentPhase = 'running'; + } + this.headerText.setText(this.buildHeader()); + this.rebuildContent(); + this.notifySnapshotChange(); + this.ui?.requestRender(); + } + + appendSubToolCall(call: { id: string; name: string; args: Record<string, unknown> }): void { + const existing = this.ongoingSubCalls.get(call.id); + this.ongoingSubCalls.set(call.id, { + name: call.name, + args: call.args, + ...(existing?.streamingArguments !== undefined + ? { streamingArguments: existing.streamingArguments } + : {}), + }); + this.upsertSubToolActivity(call.id, call.name, call.args, 'ongoing'); + if (this.subagentPhase === undefined || this.subagentPhase === 'spawning') { + this.subagentPhase = 'running'; + } + this.headerText.setText(this.buildHeader()); + this.rebuildContent(); + this.notifySnapshotChange(); + this.ui?.requestRender(); + } + + appendSubToolCallDelta(delta: { + id: string; + name?: string | undefined; + argumentsPart: string | null; + }): void { + const existing = this.ongoingSubCalls.get(delta.id); + const nextArgsText = `${existing?.streamingArguments ?? ''}${delta.argumentsPart ?? ''}`; + const parsed = parseArgsPreview(nextArgsText); + this.ongoingSubCalls.set(delta.id, { + name: delta.name ?? existing?.name ?? 'Tool', + args: parsed, + streamingArguments: nextArgsText, + }); + this.upsertSubToolActivity(delta.id, delta.name ?? existing?.name ?? 'Tool', parsed, 'ongoing'); + this.headerText.setText(this.buildHeader()); + this.rebuildContent(); + this.notifySnapshotChange(); + this.ui?.requestRender(); + } + + finishSubToolCall(result: { + tool_call_id: string; + output: string; + is_error?: boolean | undefined; + }): void { + const ongoing = this.ongoingSubCalls.get(result.tool_call_id); + if (ongoing === undefined) return; + this.ongoingSubCalls.delete(result.tool_call_id); + this.finishedSubCalls.push({ + name: ongoing.name, + args: ongoing.args, + output: result.output, + isError: result.is_error ?? false, + }); + this.upsertSubToolActivity( + result.tool_call_id, + ongoing.name, + ongoing.args, + result.is_error === true ? 'failed' : 'done', + ); + while (this.finishedSubCalls.length > MAX_SUB_TOOL_CALLS_SHOWN) { + this.finishedSubCalls.shift(); + this.hiddenSubCallCount += 1; + } + this.headerText.setText(this.buildHeader()); + this.rebuildContent(); + this.notifySnapshotChange(); + this.ui?.requestRender(); + } + + private buildHeader(): string { + const { toolCall, result, colors } = this; + const isFinished = result !== undefined; + const isError = result?.is_error ?? false; + const isTruncated = toolCall.truncated === true && !isFinished; + + let bullet: string; + if (isFinished) { + bullet = isError ? chalk.hex(colors.error)('✗ ') : chalk.hex(colors.success)(STATUS_BULLET); + } else if (isTruncated) { + bullet = chalk.hex(colors.error)('✗ '); + } else { + // Solid bullet for in-flight tools — the previous marker ↔ blank + // toggle caused visible flicker on every re-render. + bullet = chalk.hex(colors.roleAssistant)(STATUS_BULLET); + } + + if (toolCall.name === 'ExitPlanMode') { + const label = chalk.hex(colors.primary).bold('Current plan'); + if (!isFinished || result === undefined || result.is_error === true) { + return label; + } + const outcome = interpretExitPlanModeOutcome(result.output); + if (outcome.kind === 'approved') { + const chipText = + outcome.chosen !== undefined && outcome.chosen.length > 0 + ? `Approved: ${outcome.chosen}` + : 'Approved'; + return `${label}${chalk.hex(colors.success)(` · ${chipText}`)}`; + } + return `${label}${chalk.hex(colors.error)(' · Rejected')}`; + } + + if (toolCall.name === 'AskUserQuestion') { + const label = isFinished + ? isError + ? 'Could not collect your input' + : 'Collected your answers' + : 'Waiting for your input'; + const tone = isError ? chalk.hex(colors.error) : chalk.hex(colors.primary); + return `${bullet}${tone.bold(label)}`; + } + + if (this.isSingleSubagentView()) { + return this.buildSingleSubagentHeader(); + } + + const verb = isFinished ? 'Used' : isTruncated ? 'Truncated' : 'Using'; + const keyArg = extractKeyArgument(toolCall.name, toolCall.args, this.workspaceDir); + const decoded = decodeMcpToolName(toolCall.name); + const verbStyled = isTruncated + ? chalk.hex(colors.error)(verb) + : verb; + const toolLabel = + decoded !== null + ? `${chalk.hex(colors.primary).bold(decoded.toolName)}${chalk.dim(` · MCP/${decoded.serverName}`)}` + : chalk.hex(colors.primary).bold(toolCall.name); + const argStr = keyArg ? chalk.dim(` (${keyArg})`) : ''; + let chipStr = ''; + if (isFinished && result) chipStr = this.buildHeaderChip(result); + return `${bullet}${verbStyled} ${toolLabel}${argStr}${chipStr}`; + } + + private buildHeaderChip(result: ToolResultBlockData): string { + const provider = pickChip(this.toolCall.name); + if (provider === undefined) return ''; + const text = provider(this.toolCall, result); + if (text.length === 0) return ''; + const tone = result.is_error ? chalk.hex(this.colors.error) : chalk.dim; + return tone(` · ${text}`); + } + + private rebuildContent(): void { + while (this.children.length > this.callPreviewEndIndex) { + this.children.pop(); + } + this.buildProgressBlock(); + this.buildContent(); + this.buildSubagentBlock(); + } + + private rebuildBody(): void { + while (this.children.length > 2) { + this.children.pop(); + } + this.buildCallPreview(); + this.callPreviewEndIndex = this.children.length; + this.buildProgressBlock(); + this.buildContent(); + this.buildSubagentBlock(); + } + + /** + * Render the accumulated `progressLines` between the call preview and + * the result body. URLs inside a line are wrapped in an OSC 8 hyperlink + * sequence so terminals that support it (iTerm2, Ghostty, kitty, modern + * Terminal.app, VS Code) make the URL Cmd-clickable and expose + * "Copy Link" via the context menu — even when pi-tui soft-wraps the + * URL across multiple rows (pi-tui's wrapTextWithAnsi re-opens the + * active OSC 8 link on each continuation line). Each embedded URL is + * styled individually so surrounding prose keeps its default dim tone. + */ + private buildProgressBlock(): void { + if (this.progressLines.length === 0) return; + if (this.result !== undefined) return; + for (const raw of this.progressLines) { + if (raw.length === 0) { + this.addChild(new Text('', 2, 0)); + continue; + } + PROGRESS_URL_RE.lastIndex = 0; + const styled = PROGRESS_URL_RE.test(raw) + ? raw.replace(PROGRESS_URL_RE, (url) => { + const visible = chalk.hex(this.colors.warning).underline(url); + return `\u001B]8;;${url}\u001B\\${visible}\u001B]8;;\u001B\\`; + }) + : chalk.dim(raw); + PROGRESS_URL_RE.lastIndex = 0; + this.addChild(new Text(styled, 2, 0)); + } + } + + private buildSubagentBlock(): void { + if ( + this.subagentAgentId === undefined && + this.ongoingSubCalls.size === 0 && + this.finishedSubCalls.length === 0 && + this.subagentText.length === 0 && + this.subagentPhase === undefined + ) { + return; + } + + if (this.isSingleSubagentView()) { + this.buildSingleSubagentBlock(); + return; + } + + const dim = chalk.dim; + const phaseChip = this.formatPhaseChip(); + const headerLabel = + this.subagentAgentName !== undefined + ? `subagent ${this.subagentAgentName} (${this.formatAgentId()})` + : `subagent (${this.formatAgentId()})`; + this.addChild(new Text(` ${dim(`↳ ${headerLabel}`)}${phaseChip}`, 0, 0)); + + if (this.hiddenSubCallCount > 0) { + const suffix = this.hiddenSubCallCount > 1 ? 's' : ''; + this.addChild( + new Text( + dim.italic(` ${String(this.hiddenSubCallCount)} more tool call${suffix} ...`), + 0, + 0, + ), + ); + } + + for (const sub of this.finishedSubCalls) { + const mark = sub.isError + ? chalk.hex(this.colors.error)('✗') + : chalk.hex(this.colors.success)('•'); + const keyArg = extractKeyArgument(sub.name, sub.args, this.workspaceDir); + const nameCol = chalk.hex(this.colors.primary)(sub.name); + const argCol = keyArg ? dim(` (${keyArg})`) : ''; + this.addChild(new Text(` ${mark} Used ${nameCol}${argCol}`, 0, 0)); + } + + for (const [id, call] of this.ongoingSubCalls) { + const keyArg = extractKeyArgument(call.name, call.args, this.workspaceDir); + const nameCol = chalk.hex(this.colors.primary)(call.name); + const argCol = keyArg ? dim(` (${keyArg})`) : ''; + void id; + this.addChild(new Text(` ${dim('…')} Using ${nameCol}${argCol}`, 0, 0)); + } + + if (this.subagentText.length > 0) { + const tailLines = this.subagentText.split('\n').slice(-3); + for (const line of tailLines) { + this.addChild(new Text(` ${dim(line)}`, 0, 0)); + } + } + + // Result summary from subagent.completed. + if (this.subagentPhase === 'done' && this.subagentResultSummary !== undefined) { + const summaryLines = this.subagentResultSummary.split('\n').slice(0, 2); + for (const line of summaryLines) { + this.addChild(new Text(` ${dim('└')} ${line}`, 0, 0)); + } + } + + // Full error text from subagent.failed; do not collapse it. + if (this.subagentPhase === 'failed' && this.subagentError !== undefined) { + const errLines = this.subagentError.split('\n'); + for (const line of errLines) { + this.addChild(new Text(` ${chalk.hex(this.colors.error)('└')} ${line}`, 0, 0)); + } + } + } + + /** + * Header phase/token chip. No chip is shown when phase is undefined. + * spawning -> starting + * running -> running + * done -> N tools, 8.4k tok + * failed -> failed + * backgrounded -> backgrounded + */ + private formatPhaseChip(): string { + if (this.subagentPhase === undefined) return ''; + const dim = chalk.dim; + const parts: string[] = []; + switch (this.subagentPhase) { + case 'spawning': + parts.push('↻ starting…'); + break; + case 'running': + parts.push('↻ running'); + break; + case 'done': { + parts.push(chalk.hex(this.colors.success)('✓ done')); + const toolCount = this.finishedSubCalls.length + this.hiddenSubCallCount; + if (toolCount > 0) parts.push(`${String(toolCount)} tool${toolCount > 1 ? 's' : ''}`); + const tokens = formatSubagentTokens(this.subagentUsage); + if (tokens !== undefined) parts.push(tokens); + break; + } + case 'failed': + parts.push(chalk.hex(this.colors.error)('✗ failed')); + break; + case 'backgrounded': + parts.push('◐ backgrounded'); + break; + } + return parts.length > 0 ? dim(` · ${parts.join(' · ')}`) : ''; + } + + private formatAgentId(): string { + const id = this.subagentAgentId ?? ''; + return id.length > 10 ? id.slice(0, 10) + '…' : id; + } + + private hasSubagentState(): boolean { + return ( + this.subagentAgentId !== undefined || + this.ongoingSubCalls.size > 0 || + this.finishedSubCalls.length > 0 || + this.subToolActivities.size > 0 || + this.subagentText.length > 0 || + this.subagentThinkingText.length > 0 || + this.subagentPhase !== undefined + ); + } + + private isSingleSubagentView(): boolean { + return this.toolCall.name === 'Agent' && this.hasSubagentState(); + } + + private getDerivedSubagentPhase(): + | 'spawning' + | 'running' + | 'done' + | 'failed' + | 'backgrounded' + | undefined { + if (this.result !== undefined) return this.result.is_error ? 'failed' : 'done'; + return this.subagentPhase; + } + + private buildSingleSubagentHeader(): string { + const phase = this.getDerivedSubagentPhase(); + const isFailed = phase === 'failed'; + const isDone = phase === 'done'; + const bullet = isFailed + ? chalk.hex(this.colors.error)('✗ ') + : isDone + ? chalk.hex(this.colors.success)(STATUS_BULLET) + : chalk.hex(this.colors.roleAssistant)(STATUS_BULLET); + const labelText = formatSubagentLabel(this.subagentAgentName); + const label = chalk.hex(this.colors.primary).bold(labelText); + const status = this.formatSingleSubagentStatus(phase); + const description = str(this.toolCall.args['description']); + const descriptionPlain = description.length > 0 ? ` (${description})` : ''; + const descriptionText = descriptionPlain.length > 0 ? chalk.dim(descriptionPlain) : ''; + const statsText = this.formatSingleSubagentStatsText(); + if (isDone) { + const success = chalk.hex(this.colors.success); + return `${bullet}${success.bold(labelText)} ${success(`Completed${descriptionPlain}${statsText}`)}`; + } + const stats = chalk.dim(statsText); + return `${bullet}${label} ${status}${descriptionText}${stats}`; + } + + private formatSingleSubagentStatus( + phase: 'spawning' | 'running' | 'done' | 'failed' | 'backgrounded' | undefined, + ): string { + switch (phase) { + case 'done': + return chalk.hex(this.colors.success)('Completed'); + case 'failed': + return chalk.hex(this.colors.error)('Failed'); + case 'running': + return chalk.hex(this.colors.primary)('Running'); + case 'backgrounded': + return 'Backgrounded'; + case 'spawning': + case undefined: + return chalk.hex(this.colors.primary)('Starting'); + } + } + + private formatSingleSubagentStatsText(): string { + const parts = [ + `${String(this.subToolActivities.size)} tool${this.subToolActivities.size === 1 ? '' : 's'}`, + ]; + const elapsed = this.getSubagentElapsedSeconds(); + if (elapsed !== undefined) parts.push(formatElapsed(elapsed)); + return ` · ${parts.join(' · ')}`; + } + + private getSubagentElapsedSeconds(): number | undefined { + if (this.subagentStartedAtMs === undefined) return undefined; + const end = this.subagentEndedAtMs ?? Date.now(); + return Math.max(0, Math.floor((end - this.subagentStartedAtMs) / 1000)); + } + + private buildSingleSubagentBlock(): void { + for (const activity of this.getRecentSubToolActivities()) { + const mark = + activity.phase === 'failed' + ? chalk.hex(this.colors.error)('✗') + : activity.phase === 'done' + ? chalk.hex(this.colors.success)('•') + : chalk.hex(this.colors.text)('•'); + const verb = activity.phase === 'ongoing' ? 'Using' : 'Used'; + this.addChild(new Text(` ${mark} ${this.formatSubToolActivity(verb, activity)}`, 0, 0)); + } + + if (this.getDerivedSubagentPhase() === 'failed' && this.subagentError !== undefined) { + const errorLine = tailNonEmptyLines(this.subagentError, 1).at(-1); + if (errorLine !== undefined) { + this.addChild( + new PrefixedWrappedLine( + ` ${chalk.hex(this.colors.error)('└')} `, + ' ', + chalk.hex(this.colors.error)(errorLine), + ), + ); + } + return; + } + + const outputLine = tailNonEmptyLines(this.subagentText, 1).at(-1); + const thinkingLine = tailNonEmptyLines(this.subagentThinkingText, 1).at(-1); + if (this.getDerivedSubagentPhase() !== 'done' && thinkingLine !== undefined) { + this.addChild( + new PrefixedWrappedLine(` ${chalk.dim('◌')} `, ' ', chalk.dim(thinkingLine)), + ); + } + if (outputLine !== undefined) { + this.addChild( + new PrefixedWrappedLine( + ` ${chalk.hex(this.colors.text)('└')} `, + ' ', + chalk.hex(this.colors.text)(outputLine), + ), + ); + } + } + + private getRecentSubToolActivities(): SubToolActivity[] { + return [...this.subToolActivities.values()] + .toSorted((a, b) => a.orderSeq - b.orderSeq) + .slice(-MAX_SINGLE_SUBAGENT_TOOL_ROWS); + } + + private formatSubToolActivity(verb: string, activity: SubToolActivity): string { + const keyArg = extractKeyArgument(activity.name, activity.args, this.workspaceDir); + const nameCol = chalk.hex(this.colors.primary)(activity.name); + const argCol = keyArg ? chalk.dim(` (${keyArg})`) : ''; + return `${verb} ${nameCol}${argCol}`; + } + + private buildCallPreview(): void { + const name = this.toolCall.name; + if (name === 'ExitPlanMode') { + this.buildPlanPreview(); + return; + } + if (this.result === undefined && this.toolCall.truncated === true) { + this.addChild( + new Text( + chalk.dim('Tool call arguments truncated by max_tokens — call never executed.'), + 2, + 0, + ), + ); + return; + } + if (this.result === undefined && this.toolCall.streamingArguments !== undefined) { + this.buildStreamingPreview(this.toolCall.streamingArguments); + return; + } + // Collapse only kicks in once the result lands (bullet turns + // green). Between args-finalize and result we may sit in approval + // for a while — keep the preview fully visible during that gap so + // the user reviews the full change in context. + const shouldCap = this.result !== undefined && !this.expanded; + if (name === 'Write') { + const content = str(this.toolCall.args['content']); + if (content.length === 0) return; + const filePath = str(this.toolCall.args['file_path'] ?? this.toolCall.args['path']); + const lang = langFromPath(filePath); + const allLines = highlightLines(content, lang); + const shown = shouldCap ? allLines.slice(0, COMMAND_PREVIEW_LINES) : allLines; + const remaining = allLines.length - shown.length; + for (const [i, line] of shown.entries()) { + const lineNum = chalk.dim(String(i + 1).padStart(4) + ' '); + this.addChild(new Text(lineNum + line, 2, 0)); + } + if (shouldCap && remaining > 0) { + this.addChild( + new Text( + chalk.dim( + `... (${String(remaining)} more lines, ${String(allLines.length)} total, ctrl+o to expand)`, + ), + 2, + 0, + ), + ); + } + } else if (name === 'Edit') { + const oldStr = str(this.toolCall.args['old_string']); + const newStr = str(this.toolCall.args['new_string']); + if (oldStr.length === 0 && newStr.length === 0) return; + const filePath = str(this.toolCall.args['file_path'] ?? this.toolCall.args['path']); + const lines = renderDiffLinesClustered(oldStr, newStr, filePath, this.colors, { + contextLines: 3, + ...(shouldCap ? { maxLines: COMMAND_PREVIEW_LINES } : {}), + }); + for (const line of lines) { + this.addChild(new Text(line, 2, 0)); + } + } + } + + /** + * Live-rendering during the `tool.call.delta` streaming window. + * + * For tools we recognise, we reach into the partial JSON (via + * `extractPartialStringField`) and render a stable high-signal + * preview: Write's `content` as highlighted code, Edit's argument + * receive progress, Bash's `$ command`, etc. While args are still + * streaming we render the body in full (no 10-line cap) — once the + * result lands, the preview snaps to the collapsed cap unless the user + * has expanded. + */ + private buildStreamingPreview(streamText: string): void { + const name = this.toolCall.name; + if (name === 'Write') { + const content = extractPartialStringField(streamText, 'content'); + if (content === undefined || content.length === 0) return; + const filePath = + extractPartialStringField(streamText, 'file_path') ?? + extractPartialStringField(streamText, 'path') ?? + ''; + const lang = langFromPath(filePath); + const allLines = highlightLines(content, lang); + for (const [i, line] of allLines.entries()) { + const lineNum = chalk.dim(String(i + 1).padStart(4) + ' '); + this.addChild(new Text(lineNum + line, 2, 0)); + } + return; + } + if (name === 'Edit') { + const filePath = + extractPartialStringField(streamText, 'file_path') ?? + extractPartialStringField(streamText, 'path') ?? + ''; + const bytes = Buffer.byteLength(streamText, 'utf8'); + const startedAtMs = this.toolCall.streamingStartedAtMs; + const elapsedSeconds = + startedAtMs === undefined ? 0 : Math.max(0, Math.floor((Date.now() - startedAtMs) / 1000)); + const target = filePath.length > 0 ? ` for ${filePath}` : ''; + const progress = `Preparing changes${target}... ${formatByteSize(bytes)} · ${formatElapsed( + elapsedSeconds, + )} elapsed`; + this.addChild(new Text(chalk.dim(progress), 2, 0)); + return; + } + if (name === 'Bash') { + const cmd = extractPartialStringField(streamText, 'command'); + if (cmd === undefined || cmd.length === 0) return; + this.addChild( + new ShellExecutionComponent({ + command: cmd, + colors: this.colors, + showCommand: true, + commandPreviewLines: COMMAND_PREVIEW_LINES, + }), + ); + } + // Unknown tools: nothing sensible to stream without a schema, so + // leave the body blank and let the header do the talking. + } + + private buildPlanPreview(): void { + // Priority: inline `args.plan`, approved plan parsed from result, then + // asynchronously injected currentPlan used while approval is in flight. + // Once a plan is found, PlanBoxComponent renders it. Without markdownTheme + // (unit tests), fall back to indented dim text so it remains visible. + const plan = this.resolvePlanForPreview(); + if (plan.length === 0) return; + const path = this.resolvePlanPath(); + if (this.markdownTheme !== undefined) { + this.addChild( + new PlanBoxComponent(plan, this.markdownTheme, this.colors.success, path, { + maxContentLines: this.computePlanBoxMaxContentLines(), + expanded: this.planExpanded, + }), + ); + } else { + this.addChild(new Text(chalk.dim(plan), 2, 0)); + } + } + + private computePlanBoxMaxContentLines(): number | undefined { + const rows = this.ui?.terminal.rows; + if (rows === undefined || !Number.isFinite(rows) || rows <= 0) return undefined; + return Math.max(8, Math.floor(rows * 0.6) - 4); + } + + private resolvePlanForPreview(): string { + const inlinePlan = str(this.toolCall.args['plan']); + if (inlinePlan.length > 0) return inlinePlan; + if (this.result !== undefined && !this.result.is_error) { + const approved = extractApprovedPlan(this.result.output); + if (approved.length > 0) return approved; + } + return this.currentPlan ?? ''; + } + + // Priority: approved result.output with 'Plan saved to: <path>', then the + // planPath asynchronously injected by setPlanInfo while approval is in flight. + private resolvePlanPath(): string | undefined { + if (this.result !== undefined && !this.result.is_error) { + const fromResult = interpretExitPlanModeOutcome(this.result.output).path; + if (fromResult !== undefined && fromResult.length > 0) return fromResult; + } + return this.planPath; + } + + private buildContent(): void { + const { result } = this; + if (result === undefined || !result.output) return; + + if (this.isSingleSubagentView()) { + return; + } + + // Outputs that start with a `<system…>` tag are harness-injected + // reminders piggy-backing on a tool result. They are noise for the + // user, so suppress the body while keeping the header chip intact. + if (result.output.trimStart().startsWith('<system')) { + return; + } + + if (this.toolCall.name === 'ExitPlanMode' && !result.is_error) { + // Approved plans are already rendered by buildCallPreview via + // resolvePlanForPreview. Rejected or revise feedback uses a warning label + // plus normal body text so it remains visible in the transcript. + const outcome = interpretExitPlanModeOutcome(result.output); + if (outcome.kind === 'rejected' && outcome.feedback !== undefined) { + const trimmed = outcome.feedback.trim(); + if (trimmed.length > 0) { + const labelTone = chalk.hex(this.colors.warning).bold; + this.addChild(new Text(labelTone('↪ Suggestion'), 2, 0)); + for (const line of trimmed.split('\n')) { + this.addChild(new Text(line, 4, 0)); + } + } + } + return; + } + + // TodoList: the authoritative list is shown in the dedicated + // TodoPanel before the input area, so repeating the text dump here is + // pure clutter. Keep the headline, drop the body. + if (this.toolCall.name === 'TodoList' && !result.is_error) { + return; + } + + if (this.toolCall.name === 'EnterPlanMode' && !result.is_error) { + return; + } + + if ( + this.toolCall.name === 'AskUserQuestion' && + !result.is_error && + this.renderAskUserQuestionResult(result.output) + ) { + return; + } + + const renderer = pickResultRenderer(this.toolCall.name); + const components = renderer(this.toolCall, result, { + expanded: this.expanded, + colors: this.colors, + }); + for (const component of components) { + this.addChild(component); + } + } + + /** + * Render AskUserQuestion's JSON payload as a friendly Q/A list. + * Returns true on success (caller skips the default JSON dump); + * false on parse failure (caller falls back to raw display). + */ + private renderAskUserQuestionResult(output: string): boolean { + let parsed: unknown; + try { + parsed = JSON.parse(output); + } catch { + return false; + } + if (typeof parsed !== 'object' || parsed === null) return false; + + const colors = this.colors; + const dim = chalk.dim; + const accent = chalk.hex(colors.primary); + + const answers = (parsed as { answers?: unknown }).answers; + const note = (parsed as { note?: unknown }).note; + + const hasAnswers = + typeof answers === 'object' && answers !== null && Object.keys(answers).length > 0; + + if (!hasAnswers) { + const noteText = + typeof note === 'string' && note.length > 0 ? note : 'User dismissed the question.'; + this.addChild(new Text(dim(` ${noteText}`), 0, 0)); + return true; + } + + for (const [question, answer] of Object.entries(answers as Record<string, unknown>)) { + const answerText = typeof answer === 'string' ? answer : JSON.stringify(answer); + this.addChild(new Text(` ${dim('Q')} ${question}`, 0, 0)); + this.addChild(new Text(` ${accent('→')} ${answerText}`, 0, 0)); + } + return true; + } +} + +/** + * Computes the second-level "latest activity" line for group rows: + * 1. latest ongoing sub-tool (`Using {name} ({keyArg})`) + * 2. latest finished sub-tool (`Used {name} ({keyArg})`) + * 3. last non-empty line from accumulated subagent text + */ +function computeLatestActivity( + ongoing: ReadonlyMap<string, OngoingSubCall>, + finished: readonly FinishedSubCall[], + text: string, + workspaceDir?: string, +): string | undefined { + if (ongoing.size > 0) { + const lastOngoing = [...ongoing.values()].at(-1); + if (lastOngoing !== undefined) { + return formatActivityLine('Using', lastOngoing.name, lastOngoing.args, workspaceDir); + } + } + if (finished.length > 0) { + const last = finished.at(-1); + if (last !== undefined) { + return formatActivityLine('Used', last.name, last.args, workspaceDir); + } + } + if (text.length > 0) { + const tail = text + .split('\n') + .toReversed() + .find((l) => l.trim().length > 0); + if (tail !== undefined) return tail.trim(); + } + return undefined; +} + +function formatActivityLine( + verb: string, + toolName: string, + args: Record<string, unknown>, + workspaceDir?: string, +): string { + const keyArg = extractKeyArgument(toolName, args, workspaceDir); + return keyArg ? `${verb} ${toolName} (${keyArg})` : `${verb} ${toolName}`; +} diff --git a/apps/kimi-code/src/tui/components/messages/tool-renderers/chip.ts b/apps/kimi-code/src/tui/components/messages/tool-renderers/chip.ts new file mode 100644 index 000000000..a0200a761 --- /dev/null +++ b/apps/kimi-code/src/tui/components/messages/tool-renderers/chip.ts @@ -0,0 +1,126 @@ +/** + * Header chip providers — produce a short "stat" suffix appended to the + * tool call header once a result has arrived. Chips own the *numeric* + * summary (line counts, exit codes, byte sizes), so summary renderers + * below don't repeat them. + * + * A chip returning `''` is suppressed; tools without an entry in the + * registry get no chip at all. + */ + +import { computeDiffLines } from '#/tui/components/media/diff-preview'; +import type { ToolCallBlockData, ToolResultBlockData } from '#/tui/types'; + +import { readMediaChip } from './media'; +import { strArg } from './types'; + +export type ChipProvider = (toolCall: ToolCallBlockData, result: ToolResultBlockData) => string; + +export function countNonEmptyLines(text: string): number { + if (text.length === 0) return 0; + let n = 0; + for (const line of text.split('\n')) if (line.length > 0) n++; + return n; +} + +function pluralize(n: number, singular: string, plural?: string): string { + return `${String(n)} ${n === 1 ? singular : (plural ?? `${singular}s`)}`; +} + +function formatBytes(bytes: number): string { + if (bytes < 1024) return `${String(bytes)} B`; + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; + return `${(bytes / 1024 / 1024).toFixed(1)} MB`; +} + +export interface EditStats { + readonly added: number; + readonly removed: number; +} + +export interface WriteStats { + readonly lines: number; +} + +export function computeEditStats(args: Record<string, unknown>): EditStats { + const oldStr = strArg(args, 'old_string'); + const newStr = strArg(args, 'new_string'); + if (oldStr.length === 0 && newStr.length === 0) return { added: 0, removed: 0 }; + const diff = computeDiffLines(oldStr, newStr); + let added = 0; + let removed = 0; + for (const line of diff) { + if (line.kind === 'add') added++; + else if (line.kind === 'delete') removed++; + } + return { added, removed }; +} + +export function computeWriteStats(args: Record<string, unknown>): WriteStats { + const content = strArg(args, 'content'); + const normalized = content.endsWith('\n') ? content.slice(0, -1) : content; + const lines = normalized.length > 0 ? normalized.split('\n').length : 0; + return { lines }; +} + +export function formatEditChip(stats: EditStats): string { + const parts: string[] = []; + if (stats.added > 0) parts.push(`+${String(stats.added)}`); + if (stats.removed > 0) parts.push(`-${String(stats.removed)}`); + return parts.join(' '); +} + +export function formatWriteChip(stats: WriteStats): string { + return pluralize(stats.lines, 'line'); +} + +const editChip: ChipProvider = (toolCall) => { + const stats = computeEditStats(toolCall.args); + if (stats.added === 0 && stats.removed === 0) return ''; + return formatEditChip(stats); +}; + +const writeChip: ChipProvider = (toolCall) => formatWriteChip(computeWriteStats(toolCall.args)); + +const readChip: ChipProvider = (_toolCall, result) => + pluralize(countNonEmptyLines(result.output), 'line'); + +const grepChip: ChipProvider = (_toolCall, result) => { + const matches = countNonEmptyLines(result.output); + if (matches === 0) return 'no matches'; + return pluralize(matches, 'match', 'matches'); +}; + +const globChip: ChipProvider = (_toolCall, result) => { + const files = countNonEmptyLines(result.output); + if (files === 0) return 'no files'; + return pluralize(files, 'file'); +}; + +const fetchChip: ChipProvider = (_toolCall, result) => + formatBytes(Buffer.byteLength(result.output, 'utf8')); + +const webSearchChip: ChipProvider = (_toolCall, result) => { + const lines = result.output.split('\n').filter((l) => l.trim().length > 0); + let count = 0; + for (const line of lines) { + if (/^\s*(\d+\.|[-*])\s+/.test(line)) count++; + } + if (count === 0) return lines.length === 0 ? 'no results' : 'web result'; + return pluralize(count, 'result'); +}; + +const REGISTRY: Record<string, ChipProvider> = { + Edit: editChip, + Write: writeChip, + Read: readChip, + ReadMediaFile: readMediaChip, + Grep: grepChip, + Glob: globChip, + FetchURL: fetchChip, + WebSearch: webSearchChip, +}; + +export function pickChip(toolName: string): ChipProvider | undefined { + return REGISTRY[toolName]; +} diff --git a/apps/kimi-code/src/tui/components/messages/tool-renderers/media.ts b/apps/kimi-code/src/tui/components/messages/tool-renderers/media.ts new file mode 100644 index 000000000..fd753cd27 --- /dev/null +++ b/apps/kimi-code/src/tui/components/messages/tool-renderers/media.ts @@ -0,0 +1,152 @@ +/** + * ReadMediaFile renderer. + * + * The ReadMediaFile tool `output` is the JSON-serialized array of + * content parts the tool returned — which includes the full base64 of + * the image/video. Dumping that string into the transcript blasts a + * multi-screen blob of base64. This renderer parses the envelope and + * surfaces just the human-readable bits (kind, path, mime, size) via + * a header chip + a tiny expanded body. It never emits the base64. + * + * On error, or when the output isn't the expected media envelope, we + * fall back to the truncated renderer so the user still sees the raw + * message. + */ + +import type { Component } from '@earendil-works/pi-tui'; +import { Text } from '@earendil-works/pi-tui'; +import chalk from 'chalk'; + +import type { ChipProvider } from './chip'; +import { renderTruncated } from './truncated'; +import type { ResultRenderer } from './types'; + +export interface ReadMediaSummary { + kind: 'image' | 'video'; + path?: string; + mimeType?: string; + bytes?: number; + url?: string; + originalSize?: string; +} + +const PATH_TAG_RE = /^<(image|video)\s+path="([^"]+)">$/; +const ORIGINAL_SIZE_RE = /original size\s+(\d+x\d+px)/; +const DATA_URL_RE = /^data:([^;]+);base64,(.*)$/s; + +function bytesFromBase64(b64: string): number { + const len = b64.length; + if (len === 0) return 0; + const padding = b64.endsWith('==') ? 2 : b64.endsWith('=') ? 1 : 0; + return Math.floor((len * 3) / 4) - padding; +} + +export function parseReadMediaOutput(output: string): ReadMediaSummary | null { + let parsed: unknown; + try { + parsed = JSON.parse(output); + } catch { + return null; + } + if (!Array.isArray(parsed)) return null; + + let kind: 'image' | 'video' | undefined; + let path: string | undefined; + let mimeType: string | undefined; + let bytes: number | undefined; + let url: string | undefined; + let originalSize: string | undefined; + let foundMedia = false; + + for (const raw of parsed) { + if (typeof raw !== 'object' || raw === null) continue; + const part = raw as Record<string, unknown>; + const type = part['type']; + + if (type === 'text' && typeof part['text'] === 'string') { + const text = part['text']; + const tag = PATH_TAG_RE.exec(text); + if (tag) { + kind = tag[1] as 'image' | 'video'; + path = tag[2]; + continue; + } + const size = ORIGINAL_SIZE_RE.exec(text); + if (size) originalSize = size[1]; + continue; + } + + if (type === 'image_url' || type === 'video_url') { + foundMedia = true; + kind = type === 'image_url' ? 'image' : 'video'; + const holder = part[type === 'image_url' ? 'imageUrl' : 'videoUrl']; + if (typeof holder === 'object' && holder !== null) { + const h = holder as Record<string, unknown>; + const u = h['url']; + if (typeof u === 'string') { + const data = DATA_URL_RE.exec(u); + if (data && data[1] !== undefined && data[2] !== undefined) { + mimeType = data[1]; + bytes = bytesFromBase64(data[2]); + } else { + url = u; + } + } + } + } + } + + if (!foundMedia || kind === undefined) return null; + + const summary: ReadMediaSummary = { kind }; + if (path !== undefined) summary.path = path; + if (mimeType !== undefined) summary.mimeType = mimeType; + if (bytes !== undefined) summary.bytes = bytes; + if (url !== undefined) summary.url = url; + if (originalSize !== undefined) summary.originalSize = originalSize; + return summary; +} + +function formatBytes(bytes: number): string { + if (bytes < 1024) return `${String(bytes)} B`; + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; + return `${(bytes / 1024 / 1024).toFixed(1)} MB`; +} + +function metaSegments(summary: ReadMediaSummary): string[] { + const segs: string[] = []; + if (summary.mimeType !== undefined) segs.push(summary.mimeType); + if (summary.bytes !== undefined) segs.push(formatBytes(summary.bytes)); + if (summary.originalSize !== undefined) segs.push(summary.originalSize); + return segs; +} + +export const readMediaChip: ChipProvider = (_toolCall, result) => { + if (result.is_error) return ''; + const summary = parseReadMediaOutput(result.output); + if (summary === null) return ''; + const meta = metaSegments(summary); + if (meta.length === 0) { + return summary.url !== undefined ? `${summary.kind} · uploaded` : summary.kind; + } + return `${summary.kind} (${meta.join(', ')})`; +}; + +export const readMediaSummary: ResultRenderer = (toolCall, result, ctx) => { + if (result.is_error) return renderTruncated(toolCall, result, ctx); + const summary = parseReadMediaOutput(result.output); + if (summary === null) return renderTruncated(toolCall, result, ctx); + if (!ctx.expanded) return []; + + const dim = chalk.dim; + const out: Component[] = []; + if (summary.path !== undefined) { + out.push(new Text(` ${dim(summary.path)}`, 0, 0)); + } + const meta = metaSegments(summary); + const tail: string[] = [summary.kind]; + if (meta.length > 0) tail.push(meta.join(', ')); + if (summary.url !== undefined) tail.push(summary.url); + out.push(new Text(` ${dim(tail.join(' · '))}`, 0, 0)); + return out; +}; diff --git a/apps/kimi-code/src/tui/components/messages/tool-renderers/registry.ts b/apps/kimi-code/src/tui/components/messages/tool-renderers/registry.ts new file mode 100644 index 000000000..2d59b805b --- /dev/null +++ b/apps/kimi-code/src/tui/components/messages/tool-renderers/registry.ts @@ -0,0 +1,55 @@ +/** + * Tool result renderer registry. + * + * Each tool name maps to a `ResultRenderer` that turns the tool's + * `ToolResultBlockData` into renderable Components. Tools without an + * explicit entry fall through to `renderTruncated` (the original + * 3-line + ctrl+o behavior). + * + * Keep this dispatch flat — tool names live next to the renderer they + * choose, so adding a new tool means appending one case. + */ + +import { readMediaSummary } from './media'; +import { shellExecutionResultRenderer } from '../shell-execution'; +import { + editSummary, + fetchSummary, + globSummary, + grepSummary, + readSummary, + thinkSummary, + webSearchSummary, + writeSummary, +} from './summary'; +import { renderTruncated } from './truncated'; +import type { ResultRenderer } from './types'; + +export function pickResultRenderer(toolName: string): ResultRenderer { + switch (toolName) { + case 'Read': + return readSummary; + case 'ReadMediaFile': + return readMediaSummary; + case 'Grep': + return grepSummary; + case 'Glob': + return globSummary; + case 'FetchURL': + return fetchSummary; + case 'WebSearch': + return webSearchSummary; + case 'Bash': + return shellExecutionResultRenderer; + case 'Think': + return thinkSummary; + case 'Edit': + return editSummary; + case 'Write': + return writeSummary; + default: + return renderTruncated; + } +} + +export type { ResultRenderer } from './types'; diff --git a/apps/kimi-code/src/tui/components/messages/tool-renderers/summary.ts b/apps/kimi-code/src/tui/components/messages/tool-renderers/summary.ts new file mode 100644 index 000000000..a3f929dcc --- /dev/null +++ b/apps/kimi-code/src/tui/components/messages/tool-renderers/summary.ts @@ -0,0 +1,91 @@ +/** + * Summary-style renderers — produce optional inline-glance content for + * tools whose raw output is high-volume but low-information (Grep, + * Glob). The numeric summary (line counts, exit codes, sizes) lives in + * the header chip (see chip.ts), so most tools intentionally render an + * empty body and only expose details when the global expand toggle is + * on. + * + * Errors always fall through to the truncated renderer so the user + * sees the actual error message, not a synthetic summary. + */ + +import type { Component } from '@earendil-works/pi-tui'; +import { Text } from '@earendil-works/pi-tui'; +import chalk from 'chalk'; + +import { renderTruncated } from './truncated'; +import type { ResultRenderer } from './types'; + +const GLANCE_SAMPLES = 3; + +type GlanceFn = ( + toolCall: Parameters<ResultRenderer>[0], + result: Parameters<ResultRenderer>[1], +) => string; + +function withGlance(glance: GlanceFn | null): ResultRenderer { + return (toolCall, result, ctx) => { + if (result.is_error) return renderTruncated(toolCall, result, ctx); + + const out: Component[] = []; + if (glance !== null) { + const line = glance(toolCall, result); + if (line.length > 0) { + out.push(new Text(` ${chalk.dim(line)}`, 0, 0)); + } + } + if (ctx.expanded && result.output.length > 0) { + out.push(new Text(chalk.dim(result.output), 4, 0)); + } + return out; + }; +} + +function nonEmptyLines(text: string): string[] { + if (text.length === 0) return []; + return text.split('\n').filter((line) => line.length > 0); +} + +// Strip a trailing `:line:col:text` so the glance shows the file path +// only, even when grep is in `content` mode (`src/foo.ts:42: foo()`). +function pathFromGrepLine(line: string): string { + const idx = line.indexOf(':'); + if (idx <= 0) return line; + const second = line.indexOf(':', idx + 1); + if (second <= 0) return line; + return line.slice(0, second); +} + +const grepGlance: GlanceFn = (_toolCall, result) => { + const lines = nonEmptyLines(result.output); + if (lines.length === 0) return ''; + const samples = lines.slice(0, GLANCE_SAMPLES).map(pathFromGrepLine); + const remaining = lines.length - samples.length; + const tail = remaining > 0 ? `, +${String(remaining)} more` : ''; + return `${samples.join(', ')}${tail}`; +}; + +const globGlance: GlanceFn = (_toolCall, result) => { + const lines = nonEmptyLines(result.output); + if (lines.length === 0) return ''; + const samples = lines.slice(0, GLANCE_SAMPLES); + const remaining = lines.length - samples.length; + const tail = remaining > 0 ? `, +${String(remaining)} more` : ''; + return `${samples.join(', ')}${tail}`; +}; + +// ── Exports ────────────────────────────────────────────────────────── + +// Tools whose chip already conveys everything — the body is empty in +// the collapsed state and only the raw output appears when expanded. +export const readSummary: ResultRenderer = withGlance(null); +export const fetchSummary: ResultRenderer = withGlance(null); +export const webSearchSummary: ResultRenderer = withGlance(null); +export const thinkSummary: ResultRenderer = withGlance(null); +export const editSummary: ResultRenderer = withGlance(null); +export const writeSummary: ResultRenderer = withGlance(null); + +// Tools that benefit from inline path samples below the chip. +export const grepSummary: ResultRenderer = withGlance(grepGlance); +export const globSummary: ResultRenderer = withGlance(globGlance); diff --git a/apps/kimi-code/src/tui/components/messages/tool-renderers/truncated.ts b/apps/kimi-code/src/tui/components/messages/tool-renderers/truncated.ts new file mode 100644 index 000000000..2bd9cf01b --- /dev/null +++ b/apps/kimi-code/src/tui/components/messages/tool-renderers/truncated.ts @@ -0,0 +1,22 @@ +import type { Component } from '@earendil-works/pi-tui'; +import { Text } from '@earendil-works/pi-tui'; +import chalk from 'chalk'; + +import type { ResultRenderer } from './types'; +import { PREVIEW_LINES } from './types'; + +export const renderTruncated: ResultRenderer = (_toolCall, result, ctx) => { + if (!result.output) return []; + const tint = result.is_error ? chalk.hex(ctx.colors.error) : chalk.dim; + const lines = result.output.split('\n'); + if (ctx.expanded) { + return [new Text(tint(result.output), 2, 0)]; + } + const shown = lines.slice(0, PREVIEW_LINES); + const remaining = lines.length - shown.length; + const out: Component[] = [new Text(tint(shown.join('\n')), 2, 0)]; + if (remaining > 0) { + out.push(new Text(chalk.dim(`... (${String(remaining)} more lines, ctrl+o to expand)`), 2, 0)); + } + return out; +}; diff --git a/apps/kimi-code/src/tui/components/messages/tool-renderers/types.ts b/apps/kimi-code/src/tui/components/messages/tool-renderers/types.ts new file mode 100644 index 000000000..462b53a44 --- /dev/null +++ b/apps/kimi-code/src/tui/components/messages/tool-renderers/types.ts @@ -0,0 +1,26 @@ +import type { Component } from '@earendil-works/pi-tui'; + +import { RESULT_PREVIEW_LINES } from '#/tui/constant/rendering'; +import type { ColorPalette } from '#/tui/theme/colors'; +import type { ToolCallBlockData, ToolResultBlockData } from '#/tui/types'; + +export interface RendererContext { + readonly expanded: boolean; + readonly colors: ColorPalette; +} + +export type ResultRenderer = ( + toolCall: ToolCallBlockData, + result: ToolResultBlockData, + ctx: RendererContext, +) => Component[]; + +export const PREVIEW_LINES = RESULT_PREVIEW_LINES; + +export function strArg(args: Record<string, unknown>, ...keys: string[]): string { + for (const key of keys) { + const v = args[key]; + if (typeof v === 'string' && v.length > 0) return v; + } + return ''; +} diff --git a/apps/kimi-code/src/tui/components/messages/usage-panel.ts b/apps/kimi-code/src/tui/components/messages/usage-panel.ts new file mode 100644 index 000000000..4db3d8c88 --- /dev/null +++ b/apps/kimi-code/src/tui/components/messages/usage-panel.ts @@ -0,0 +1,246 @@ +/** + * UsagePanelComponent — wraps pre-coloured `/usage` lines in a blue box + * border with a left indent, mirroring the PlanBoxComponent layout so + * the pattern stays consistent across command-triggered panels. + */ + +import type { Component } from '@earendil-works/pi-tui'; +import { truncateToWidth, visibleWidth } from '@earendil-works/pi-tui'; +import type { SessionUsage, TokenUsage } from '@moonshot-ai/kimi-code-sdk'; +import chalk from 'chalk'; + +import { + formatTokenCount, + ratioSeverity, + renderProgressBar, + safeUsageRatio, +} from '#/utils/usage/usage-format'; +import type { ColorPalette } from '#/tui/theme/colors'; + +const LEFT_MARGIN = 2; +const SIDE_PADDING = 1; +const MIN_INTERIOR_WIDTH = 20; + +type Colorize = (text: string) => string; + +export interface ManagedUsageRow { + readonly label: string; + readonly used: number; + readonly limit: number; + readonly resetHint?: string; +} + +export interface ManagedUsageReport { + readonly summary: ManagedUsageRow | null; + readonly limits: readonly ManagedUsageRow[]; +} + +export interface UsageReportOptions { + readonly colors: ColorPalette; + readonly sessionUsage?: SessionUsage; + readonly sessionUsageError?: string; + readonly contextUsage: number; + readonly contextTokens: number; + readonly maxContextTokens: number; + readonly managedUsage?: ManagedUsageReport; + readonly managedUsageError?: string; +} + +export interface ManagedUsageReportLineOptions { + readonly colors: ColorPalette; + readonly managedUsage?: ManagedUsageReport; + readonly managedUsageError?: string; +} + +function usageNumber(value: unknown): number { + return typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : 0; +} + +function usageInputTotal(usage: TokenUsage): number { + return ( + usageNumber(usage.inputOther) + + usageNumber(usage.inputCacheRead) + + usageNumber(usage.inputCacheCreation) + ); +} + +function buildSessionUsageSection( + usage: SessionUsage | undefined, + error: string | undefined, + value: Colorize, + muted: Colorize, + errorStyle: Colorize, +): string[] { + if (error !== undefined) return [errorStyle(` ${error}`)]; + const byModel = (usage as { readonly byModel?: Record<string, TokenUsage> } | undefined) + ?.byModel; + const entries = Object.entries(byModel ?? {}); + if (entries.length === 0) return [muted(' No token usage recorded yet.')]; + + const lines: string[] = []; + let totalInput = 0; + let totalOutput = 0; + for (const [model, row] of entries) { + const input = usageInputTotal(row); + const output = usageNumber(row.output); + totalInput += input; + totalOutput += output; + lines.push( + ` ${muted(model)} input ${value(formatTokenCount(input))} output ${value( + formatTokenCount(output), + )} total ${value(formatTokenCount(input + output))}`, + ); + } + if (entries.length > 1) { + lines.push( + ` ${muted('total')} input ${value(formatTokenCount(totalInput))} output ${value( + formatTokenCount(totalOutput), + )} total ${value(formatTokenCount(totalInput + totalOutput))}`, + ); + } + return lines; +} + +function buildManagedUsageSection( + usage: ManagedUsageReport | undefined, + error: string | undefined, + accent: Colorize, + value: Colorize, + muted: Colorize, + errorStyle: Colorize, + severityHex: (sev: 'ok' | 'warn' | 'danger') => string, +): string[] { + if (error !== undefined) return [accent('Plan usage'), errorStyle(` ${error}`)]; + if (usage === undefined) return []; + const { summary, limits } = usage; + if (summary === null && limits.length === 0) { + return [accent('Plan usage'), muted(' No usage data available.')]; + } + + const rows: ManagedUsageRow[] = []; + if (summary !== null) rows.push(summary); + rows.push(...limits); + const labelWidth = Math.max(10, ...rows.map((r) => r.label.length)); + const out: string[] = [accent('Plan usage')]; + for (const row of rows) { + const ratioUsed = row.limit > 0 ? row.used / row.limit : 0; + const leftRatio = 1 - Math.max(0, Math.min(ratioUsed, 1)); + const bar = renderProgressBar(Math.max(0, Math.min(ratioUsed, 1)), 20); + const pct = `${Math.round(leftRatio * 100)}% left`; + const barColoured = chalk.hex(severityHex(ratioSeverity(ratioUsed)))(bar); + const label = row.label.padEnd(labelWidth, ' '); + const resetStr = row.resetHint ? muted(` (${row.resetHint})`) : ''; + out.push(` ${muted(label)} ${barColoured} ${value(pct)}${resetStr}`); + } + return out; +} + +export function buildManagedUsageReportLines(options: ManagedUsageReportLineOptions): string[] { + const colors = options.colors; + const accent = chalk.hex(colors.primary).bold; + const value = chalk.hex(colors.text); + const muted = chalk.hex(colors.textDim); + const errorStyle = chalk.hex(colors.error); + const severityHex = (sev: 'ok' | 'warn' | 'danger'): string => + sev === 'danger' ? colors.error : sev === 'warn' ? colors.warning : colors.success; + + return buildManagedUsageSection( + options.managedUsage, + options.managedUsageError, + accent, + value, + muted, + errorStyle, + severityHex, + ); +} + +export function buildUsageReportLines(options: UsageReportOptions): string[] { + const colors = options.colors; + const accent = chalk.hex(colors.primary).bold; + const value = chalk.hex(colors.text); + const muted = chalk.hex(colors.textDim); + const errorStyle = chalk.hex(colors.error); + const severityHex = (sev: 'ok' | 'warn' | 'danger'): string => + sev === 'danger' ? colors.error : sev === 'warn' ? colors.warning : colors.success; + + const lines: string[] = [ + accent('Session usage'), + ...buildSessionUsageSection( + options.sessionUsage, + options.sessionUsageError, + value, + muted, + errorStyle, + ), + ]; + + if (options.maxContextTokens > 0) { + const ratio = safeUsageRatio(options.contextUsage); + const bar = renderProgressBar(ratio, 20); + const pct = `${(ratio * 100).toFixed(1)}%`; + const barColoured = chalk.hex(severityHex(ratioSeverity(ratio)))(bar); + lines.push(''); + lines.push(accent('Context window')); + lines.push( + ` ${barColoured} ${value(pct.padStart(6, ' '))} ` + + muted( + `(${formatTokenCount(options.contextTokens)} / ${formatTokenCount( + options.maxContextTokens, + )})`, + ), + ); + } + + const managedSection = buildManagedUsageReportLines({ + colors, + managedUsage: options.managedUsage, + managedUsageError: options.managedUsageError, + }); + if (managedSection.length > 0) { + lines.push(''); + lines.push(...managedSection); + } + + return lines; +} + +export class UsagePanelComponent implements Component { + constructor( + private readonly lines: readonly string[], + private readonly borderHex: string, + private readonly title: string = ' Usage ', + ) {} + + invalidate(): void {} + + render(width: number): string[] { + const paint = (s: string): string => chalk.hex(this.borderHex)(s); + const indent = ' '.repeat(LEFT_MARGIN); + + const availableInterior = Math.max( + MIN_INTERIOR_WIDTH, + width - LEFT_MARGIN - 2 - 2 * SIDE_PADDING, + ); + const longestLine = this.lines.reduce((max, line) => Math.max(max, visibleWidth(line)), 0); + const contentWidth = Math.max( + MIN_INTERIOR_WIDTH, + Math.min(availableInterior, longestLine, Math.max(longestLine, this.title.length)), + ); + const horzLen = contentWidth + 2 * SIDE_PADDING; + + const trailingDashLen = Math.max(0, horzLen - this.title.length); + const top = + indent + paint('╭') + paint(this.title) + paint('─'.repeat(trailingDashLen)) + paint('╮'); + const bottom = indent + paint('╰' + '─'.repeat(horzLen) + '╯'); + + const out: string[] = [top]; + for (const line of this.lines) { + const clipped = visibleWidth(line) > contentWidth ? truncateToWidth(line, contentWidth) : line; + const pad = Math.max(0, contentWidth - visibleWidth(clipped)); + out.push(indent + paint('│') + ' ' + clipped + ' '.repeat(pad) + ' ' + paint('│')); + } + out.push(bottom); + return out; + } +} diff --git a/apps/kimi-code/src/tui/components/messages/user-message.ts b/apps/kimi-code/src/tui/components/messages/user-message.ts new file mode 100644 index 000000000..8617598a0 --- /dev/null +++ b/apps/kimi-code/src/tui/components/messages/user-message.ts @@ -0,0 +1,63 @@ +/** + * Renders a user message in the transcript. + */ + +import type { Component } from '@earendil-works/pi-tui'; +import { Spacer, Text, visibleWidth } from '@earendil-works/pi-tui'; +import chalk from 'chalk'; + +import { ImageThumbnail } from '#/tui/components/media/image-thumbnail'; +import { USER_MESSAGE_BULLET } from '#/tui/constant/symbols'; +import type { ColorPalette } from '#/tui/theme/colors'; +import type { ImageAttachment } from '#/tui/utils/image-attachment-store'; + +export class UserMessageComponent implements Component { + private color: string; + private textComponent: Text; + private spacerComponent: Spacer; + private imageThumbnails: ImageThumbnail[]; + + constructor(text: string, colors: ColorPalette, images?: ImageAttachment[]) { + this.color = colors.roleUser; + this.textComponent = new Text(chalk.hex(colors.roleUser).bold(text), 0, 0); + this.spacerComponent = new Spacer(1); + this.imageThumbnails = images?.map((img) => new ImageThumbnail(img, colors)) ?? []; + } + + invalidate(): void { + this.textComponent.invalidate(); + for (const img of this.imageThumbnails) { + img.invalidate?.(); + } + } + + render(width: number): string[] { + const bullet = chalk.hex(this.color).bold(USER_MESSAGE_BULLET); + const bulletWidth = visibleWidth(bullet); + const contentWidth = Math.max(1, width - bulletWidth); + + const lines: string[] = []; + + // Spacer + for (const line of this.spacerComponent.render(width)) { + lines.push(line); + } + + // Text + const textLines = this.textComponent.render(contentWidth); + for (let i = 0; i < textLines.length; i++) { + const prefix = i === 0 ? bullet : ' '.repeat(bulletWidth); + lines.push(prefix + textLines[i]); + } + + // Images — indented to align with text after the bullet + for (const thumbnail of this.imageThumbnails) { + const imageLines = thumbnail.render(contentWidth); + for (const line of imageLines) { + lines.push(' '.repeat(bulletWidth) + line); + } + } + + return lines; + } +} diff --git a/apps/kimi-code/src/tui/components/panes/activity-pane.ts b/apps/kimi-code/src/tui/components/panes/activity-pane.ts new file mode 100644 index 000000000..2a1d8ea1d --- /dev/null +++ b/apps/kimi-code/src/tui/components/panes/activity-pane.ts @@ -0,0 +1,29 @@ +import { Container, Spacer } from '@earendil-works/pi-tui'; + +import type { MoonLoader } from '../chrome/moon-loader'; + +export type ActivityPaneMode = 'hidden' | 'waiting' | 'thinking' | 'composing' | 'tool'; + +export interface ActivityPaneOptions { + readonly mode: ActivityPaneMode; + readonly spinner?: MoonLoader; +} + +export class ActivityPaneComponent extends Container { + constructor(options: ActivityPaneOptions) { + super(); + + if (options.mode === 'waiting' || options.mode === 'tool') { + if (options.spinner !== undefined) { + this.addChild(new Spacer(1)); + this.addChild(options.spinner); + } + return; + } + + if (options.mode === 'composing' && options.spinner !== undefined) { + this.addChild(new Spacer(1)); + this.addChild(options.spinner); + } + } +} diff --git a/apps/kimi-code/src/tui/components/panes/queue-pane.ts b/apps/kimi-code/src/tui/components/panes/queue-pane.ts new file mode 100644 index 000000000..87b0d924f --- /dev/null +++ b/apps/kimi-code/src/tui/components/panes/queue-pane.ts @@ -0,0 +1,36 @@ +import { Container, Text } from '@earendil-works/pi-tui'; +import chalk from 'chalk'; + +import type { QueuedMessage } from '../../types'; +import type { ColorPalette } from '../../theme/colors'; + +export interface QueuePaneOptions { + readonly messages: readonly QueuedMessage[]; + readonly colors: ColorPalette; + readonly isCompacting: boolean; + readonly isStreaming: boolean; + readonly canSteerImmediately: boolean; +} + +export class QueuePaneComponent extends Container { + constructor(options: QueuePaneOptions) { + super(); + + const accent = chalk.hex(options.colors.accent); + const dim = chalk.hex(options.colors.textDim); + + for (const item of options.messages) { + this.addChild(new Text(accent(` ❯ ${item.text}`), 0, 0)); + } + + if (options.messages.length > 0) { + const hint = + options.isCompacting && !options.isStreaming + ? ' ↑ to edit · will send after compaction' + : !options.canSteerImmediately + ? ' ↑ to edit · will send after current task' + : ' ↑ to edit · ctrl-s to steer immediately'; + this.addChild(new Text(dim(hint), 0, 0)); + } + } +} diff --git a/apps/kimi-code/src/tui/config.ts b/apps/kimi-code/src/tui/config.ts new file mode 100644 index 000000000..e09774a40 --- /dev/null +++ b/apps/kimi-code/src/tui/config.ts @@ -0,0 +1,153 @@ +/** + * TUI-owned configuration. + * + * Agent/runtime settings live in core's `config.toml`; this file owns only + * terminal UI preferences for the kimi-code client. + */ + +import { existsSync } from 'node:fs'; +import { mkdir, readFile, writeFile } from 'node:fs/promises'; +import { dirname, join } from 'node:path'; + +import { parse as parseToml } from 'smol-toml'; +import { z } from 'zod'; + +import { getDataDir } from '#/utils/paths'; + +export const INVALID_TUI_CONFIG_MESSAGE = + 'Invalid TUI config in ~/.kimi-code/tui.toml; using defaults.'; + +export const TuiThemeSchema = z.enum(['dark', 'light', 'auto']); + +export const NotificationConditionSchema = z.enum(['unfocused', 'always']); + +export const NotificationsConfigSchema = z.object({ + enabled: z.boolean(), + condition: NotificationConditionSchema, +}); + +export const TuiConfigFileSchema = z.object({ + theme: TuiThemeSchema.optional(), + editor: z + .object({ + command: z.string().optional(), + }) + .optional(), + notifications: z + .object({ + enabled: z.boolean().optional(), + notification_condition: NotificationConditionSchema.optional(), + }) + .optional(), +}); + +export const TuiConfigSchema = z.object({ + theme: TuiThemeSchema, + editorCommand: z.string().nullable(), + notifications: NotificationsConfigSchema, +}); + +export type TuiConfigFileShape = z.infer<typeof TuiConfigFileSchema>; +export type TuiConfig = z.infer<typeof TuiConfigSchema>; +export type NotificationsConfig = z.infer<typeof NotificationsConfigSchema>; + +export const DEFAULT_NOTIFICATIONS_CONFIG: NotificationsConfig = { + enabled: true, + condition: 'unfocused', +}; + +export const DEFAULT_TUI_CONFIG: TuiConfig = TuiConfigSchema.parse({ + theme: 'auto', + editorCommand: null, + notifications: DEFAULT_NOTIFICATIONS_CONFIG, +}); + +/** + * Thrown by `loadTuiConfig` when the on-disk TOML cannot be parsed. + * Carries `fallback` so the caller can recover without re-running the + * I/O, and use `message` (== `INVALID_TUI_CONFIG_MESSAGE`) as a + * user-facing notice. + */ +export class TuiConfigParseError extends Error { + override readonly name = 'TuiConfigParseError'; + readonly fallback: TuiConfig; + constructor(fallback: TuiConfig) { + super(INVALID_TUI_CONFIG_MESSAGE); + this.fallback = fallback; + } +} + +export function getTuiConfigPath(): string { + return join(getDataDir(), 'tui.toml'); +} + +export async function loadTuiConfig(filePath: string = getTuiConfigPath()): Promise<TuiConfig> { + if (!existsSync(filePath)) { + await saveTuiConfig(DEFAULT_TUI_CONFIG, filePath); + return DEFAULT_TUI_CONFIG; + } + + try { + const text = await readFile(filePath, 'utf-8'); + return parseTuiConfig(text); + } catch { + throw new TuiConfigParseError(DEFAULT_TUI_CONFIG); + } +} + +export function parseTuiConfig(tomlText: string): TuiConfig { + if (tomlText.trim().length === 0) { + return DEFAULT_TUI_CONFIG; + } + const raw = parseToml(tomlText) as Record<string, unknown>; + const parsed = TuiConfigFileSchema.parse(raw); + return normalizeTuiConfig(parsed); +} + +export async function saveTuiConfig( + config: TuiConfig, + filePath: string = getTuiConfigPath(), +): Promise<void> { + await mkdir(dirname(filePath), { recursive: true }); + await writeFile(filePath, renderTuiConfig(config), 'utf-8'); +} + +export function normalizeTuiConfig(config: TuiConfigFileShape): TuiConfig { + const command = config.editor?.command?.trim(); + return TuiConfigSchema.parse({ + theme: config.theme ?? DEFAULT_TUI_CONFIG.theme, + editorCommand: command === undefined || command.length === 0 ? null : command, + notifications: { + enabled: config.notifications?.enabled ?? DEFAULT_NOTIFICATIONS_CONFIG.enabled, + condition: + config.notifications?.notification_condition ?? DEFAULT_NOTIFICATIONS_CONFIG.condition, + }, + }); +} + +export function renderTuiConfig(config: TuiConfig): string { + return `# ~/.kimi-code/tui.toml +# Terminal UI preferences for kimi-code. +# Agent/runtime settings stay in ~/.kimi-code/config.toml. + +theme = "${config.theme}" # "auto" | "dark" | "light" + +[editor] +command = "${escapeTomlBasicString(config.editorCommand ?? '')}" # Empty uses $VISUAL / $EDITOR + +[notifications] +enabled = ${String(config.notifications.enabled)} # true | false +notification_condition = "${config.notifications.condition}" # "unfocused" | "always" +`; +} + +function escapeTomlBasicString(value: string): string { + return value + .replaceAll('\\', '\\\\') + .replaceAll('"', '\\"') + .replaceAll('\b', '\\b') + .replaceAll('\t', '\\t') + .replaceAll('\n', '\\n') + .replaceAll('\f', '\\f') + .replaceAll('\r', '\\r'); +} diff --git a/apps/kimi-code/src/tui/constant/feedback.ts b/apps/kimi-code/src/tui/constant/feedback.ts new file mode 100644 index 000000000..37a50a43a --- /dev/null +++ b/apps/kimi-code/src/tui/constant/feedback.ts @@ -0,0 +1,42 @@ +/** + * Constants for the /feedback command — endpoints, telemetry keys, and + * the status messages shown around the feedback submission flow. + * + * Dialog-internal copy (the box title, subtitle, footer) lives next to + * the dialog component itself, since it is part of that component's + * visual contract. + */ + +import { FEEDBACK_VERSION_PREFIX } from '#/constant/app'; + +export { + FEEDBACK_ISSUE_URL, + FEEDBACK_TELEMETRY_EVENT, + FEEDBACK_VERSION_PREFIX, +} from '#/constant/app'; + +export const FEEDBACK_STATUS_SUBMITTING = 'Submitting feedback…'; +export const FEEDBACK_STATUS_SUCCESS = 'Feedback submitted, thank you!'; +export const FEEDBACK_STATUS_CANCELLED = 'Feedback cancelled.'; +export const FEEDBACK_STATUS_NETWORK_ERROR = 'Network error, failed to submit feedback.'; +export const FEEDBACK_STATUS_FALLBACK = 'Opening GitHub Issues as fallback…'; +export const FEEDBACK_STATUS_NOT_SIGNED_IN = + "You're not signed in. Opening GitHub Issues for feedback…"; + +export function feedbackHttpErrorMessage(status: number): string { + return `Failed to submit feedback (HTTP ${String(status)}).`; +} + +export function feedbackSessionLine(sessionId: string): string { + return `Session: ${sessionId}`; +} + +// Hint shown beneath session-level error messages in the TUI to point users +// at the `kimi export` workflow so they can share diagnostics with us. +export function errorReportHintLine(sessionId: string): string { + return `If this persists, run \`kimi export ${sessionId}\` and share the file with us for diagnosis. Please don't share it publicly.`; +} + +export function withFeedbackVersionPrefix(version: string): string { + return `${FEEDBACK_VERSION_PREFIX}${version}`; +} diff --git a/apps/kimi-code/src/tui/constant/kimi-tui.ts b/apps/kimi-code/src/tui/constant/kimi-tui.ts new file mode 100644 index 000000000..03ef814c0 --- /dev/null +++ b/apps/kimi-code/src/tui/constant/kimi-tui.ts @@ -0,0 +1,17 @@ +import { DEFAULT_OAUTH_PROVIDER_NAME } from '#/constant/app'; + +export { DEFAULT_OAUTH_PROVIDER_NAME, OAUTH_LOGIN_REQUIRED_CODE } from '#/constant/app'; + +export const LLM_NOT_SET_MESSAGE = 'LLM not set, send "/login" to login'; +export const NO_ACTIVE_SESSION_MESSAGE = 'No active session. Send /login to login.'; +export const CTRL_D_HINT = 'Press Ctrl+D again to exit'; +export const CTRL_C_HINT = 'Press Ctrl+C again to exit'; +export const MAIN_AGENT_ID = 'main'; +export const OAUTH_LOGIN_REQUIRED_STARTUP_NOTICE = 'OAuth login expired. Send /login to login.'; +export const EXIT_CONFIRM_WINDOW_MS = 1500; + +export function isManagedUsageProvider( + providerKey: string | undefined, +): providerKey is typeof DEFAULT_OAUTH_PROVIDER_NAME { + return providerKey === DEFAULT_OAUTH_PROVIDER_NAME; +} diff --git a/apps/kimi-code/src/tui/constant/rendering.ts b/apps/kimi-code/src/tui/constant/rendering.ts new file mode 100644 index 000000000..c96abf466 --- /dev/null +++ b/apps/kimi-code/src/tui/constant/rendering.ts @@ -0,0 +1,19 @@ +// Continuation indent for transcript rows that use a two-cell leading marker. +export const MESSAGE_INDENT = ' '; + +// Outer left/right padding applied to the transcript, panels, and the +// statusline so the chrome's left edge lines up with the input box's +// interior (the `>` prompt). The editor itself stays at column 0 — its +// vertical borders are the visual anchor everything else aligns against. +export const CHROME_GUTTER = 1; + +// Shared preview caps used by thinking, tool results, and shell snippets. +export const RESULT_PREVIEW_LINES = 3; +export const COMMAND_PREVIEW_LINES = 10; + +// Animation frames are shared by the login/update loaders and live thinking. +export const BRAILLE_SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']; +export const BRAILLE_SPINNER_INTERVAL_MS = 80; + +export const MOON_SPINNER_FRAMES = ['🌑', '🌒', '🌓', '🌔', '🌕', '🌖', '🌗', '🌘']; +export const MOON_SPINNER_INTERVAL_MS = 120; diff --git a/apps/kimi-code/src/tui/constant/streaming.ts b/apps/kimi-code/src/tui/constant/streaming.ts new file mode 100644 index 000000000..e52117249 --- /dev/null +++ b/apps/kimi-code/src/tui/constant/streaming.ts @@ -0,0 +1,4 @@ +// Extracts useful string fields from partially streamed JSON tool args. +// This is intentionally a preview parser, not a full JSON parser. +export const STREAMING_ARGS_FIELD_RE = + /"(path|file_path|command|pattern|query|url|description|title|name)"\s*:\s*"((?:\\.|[^"\\])*)"/g; diff --git a/apps/kimi-code/src/tui/constant/symbols.ts b/apps/kimi-code/src/tui/constant/symbols.ts new file mode 100644 index 000000000..670b94eb0 --- /dev/null +++ b/apps/kimi-code/src/tui/constant/symbols.ts @@ -0,0 +1,7 @@ +// Use U+25CF instead of U+23FA to avoid emoji/fallback rendering in terminals. +export const STATUS_BULLET = '● '; + +// Shared transcript markers. Keep widths stable because message wrapping +// assumes the marker occupies the leading cells. +export const USER_MESSAGE_BULLET = '✨ '; +export const FAILURE_MARK = '✗ '; diff --git a/apps/kimi-code/src/tui/constant/terminal.ts b/apps/kimi-code/src/tui/constant/terminal.ts new file mode 100644 index 000000000..9d68af25f --- /dev/null +++ b/apps/kimi-code/src/tui/constant/terminal.ts @@ -0,0 +1,40 @@ +import { BEL, ESC, ST } from "#/constant/terminal"; + +export { BEL, ESC, ST } from "#/constant/terminal"; + +// Terminal theme reporting uses private CSI sequences: enable reporting, +// query once, then parse dark/light reports from the input stream. +export const QUERY_TERMINAL_THEME = `${ESC}[?996n`; +export const TERMINAL_THEME_DARK = `${ESC}[?997;1n`; +export const TERMINAL_THEME_LIGHT = `${ESC}[?997;2n`; +export const ENABLE_TERMINAL_THEME_REPORTING = `${ESC}[?2031h`; +export const DISABLE_TERMINAL_THEME_REPORTING = `${ESC}[?2031l`; + +// Xterm-style focus reporting. Input listeners consume these bytes so +// they do not leak into the editor. +export const TERMINAL_FOCUS_IN = `${ESC}[I`; +export const TERMINAL_FOCUS_OUT = `${ESC}[O`; +export const ENABLE_TERMINAL_FOCUS_REPORTING = `${ESC}[?1004h`; +export const DISABLE_TERMINAL_FOCUS_REPORTING = `${ESC}[?1004l`; + +// Standard OSC 11 background-color query. The response regex intentionally +// allows a missing leading ESC because terminals can echo replies alongside +// other raw input, but it requires an OSC terminator so fragmented color +// channels are not parsed as complete replies. +export const OSC11_QUERY = `${ESC}]11;?${BEL}`; +const OSC11_RESPONSE_TERMINATOR_PATTERN = `(?:${BEL}|${ESC}\\\\)`; +export const OSC11_RESPONSE = new RegExp( + String.raw`${ESC}?\]11;rgb:([0-9a-f]{1,4})\/([0-9a-f]{1,4})\/([0-9a-f]{1,4})${OSC11_RESPONSE_TERMINATOR_PATTERN}`, + "i", +); +export const OSC11_RESPONSE_PREFIX = `${ESC}]11;rgb:`; +export const OSC11_RESPONSE_PREFIX_NO_ESC = "]11;rgb:"; + +// Keep notification/title payloads bounded so terminal tabs and desktop +// notifications stay readable. +export const MAX_TERMINAL_NOTIFICATION_MESSAGE_LENGTH = 240; +export const MAX_PROCESS_TITLE_LENGTH = 80; + +// OSC 11 probing must be short because unsupported terminals do not reply. +export const TERMINAL_THEME_DETECT_TIMEOUT_MS = 250; +export const TERMINAL_THEME_INPUT_BUFFER_MAX_LENGTH = 512; diff --git a/apps/kimi-code/src/tui/index.ts b/apps/kimi-code/src/tui/index.ts new file mode 100644 index 000000000..e4581aa39 --- /dev/null +++ b/apps/kimi-code/src/tui/index.ts @@ -0,0 +1,3 @@ +export { KimiTUI } from './kimi-tui'; +export type { KimiTUIStartupInput } from './kimi-tui'; +export type { KimiTUIOptions } from './kimi-tui'; diff --git a/apps/kimi-code/src/tui/kimi-tui.ts b/apps/kimi-code/src/tui/kimi-tui.ts new file mode 100644 index 000000000..9dc684f3c --- /dev/null +++ b/apps/kimi-code/src/tui/kimi-tui.ts @@ -0,0 +1,5221 @@ +/** + * KimiTUI owns the terminal UI shell for a Kimi Code session. + * + * It builds the pi-tui layout, tracks view state, wires editor shortcuts and + * slash commands, drives session startup/switching, renders SDK events into the + * transcript and live panes, and bridges approval, question, auth, and config + * flows back to the harness. + */ + +import { writeFileSync } from 'node:fs'; +import { release as osRelease, type as osType } from 'node:os'; +import { join } from 'node:path'; + +import { + Container, + deleteAllKittyImages, + type Component, + type Focusable, + getCapabilities, + ProcessTerminal, + type SlashCommand, + Spacer, + TUI, +} from '@earendil-works/pi-tui'; +import type { MigrationPlan } from '@moonshot-ai/migration-legacy'; +import { + applyOpenPlatformConfig, + capabilitiesForModel, + fetchOpenPlatformModels, + filterModelsByPrefix, + getOpenPlatformById, + isOpenPlatformId, + OpenPlatformApiError, + type DeviceAuthorization, + type ManagedKimiCodeModelInfo, + type ManagedKimiConfigShape, + type OpenPlatformDefinition, +} from '@moonshot-ai/kimi-code-oauth'; +import { log } from '@moonshot-ai/kimi-code-sdk'; +import type { + AgentStatusUpdatedEvent, + ApprovalRequest, + ApprovalResponse, + AssistantDeltaEvent, + BackgroundTaskInfo, + BackgroundTaskStartedEvent, + BackgroundTaskTerminatedEvent, + BackgroundTaskUpdatedEvent, + CompactionCancelledEvent, + CompactionCompletedEvent, + CompactionStartedEvent, + CreateSessionOptions, + ErrorEvent, + Event, + HookResultEvent, + KimiHarness, + ModelAlias, + McpServerInfo, + PermissionMode, + PromptPart, + Session, + SessionMetaUpdatedEvent, + SessionStatus, + SessionUsage, + SkillActivatedEvent, + SubagentCompletedEvent, + SubagentFailedEvent, + SubagentSpawnedEvent, + ThinkingDeltaEvent, + ToolCallDeltaEvent, + ToolCallStartedEvent, + ToolProgressEvent, + ToolResultEvent, + TurnEndedEvent, + TurnStartedEvent, + TurnStepCompletedEvent, + TurnStepInterruptedEvent, + TurnStepStartedEvent, +} from '@moonshot-ai/kimi-code-sdk'; +import chalk from 'chalk'; + +import type { CLIOptions } from '#/cli/options'; +import { MigrationScreenComponent, type MigrationScreenResult } from '#/migration/index'; +import { ClipboardMediaError, readClipboardMedia } from '#/utils/clipboard/clipboard-image'; +import type { GitLsFilesCache } from '#/utils/git/git-ls-files'; +import { createGitLsFilesCache } from '#/utils/git/git-ls-files'; +import { appendInputHistory, loadInputHistory } from '#/utils/history/input-history'; +import { parseImageMeta } from '#/utils/image/image-mime'; +import { getInputHistoryFile } from '#/utils/paths'; +import { editInExternalEditor, resolveEditorCommand } from '#/utils/process/external-editor'; +import { detectFdPath } from '#/utils/process/fd-detect'; + +import { hydrateTranscriptFromReplay, type ReplayHydrationHooks } from './actions/replay-ops'; +import { + BUILTIN_SLASH_COMMANDS, + buildSkillSlashCommands, + parseSlashInput, + resolveSlashCommandInput, + slashBusyMessage, + sortSlashCommands, + type BuiltinSlashCommandName, + type KimiSlashCommand, + type SkillListSession, +} from './commands'; +import { DeviceCodeBoxComponent } from './components/chrome/device-code-box'; +import { FooterComponent } from './components/chrome/footer'; +import { GutterContainer } from './components/chrome/gutter-container'; +import { CHROME_GUTTER } from './constant/rendering'; +import { MoonLoader, type SpinnerStyle } from './components/chrome/moon-loader'; +import { TodoPanelComponent, type TodoItem } from './components/chrome/todo-panel'; +import { WelcomeComponent } from './components/chrome/welcome'; +import { + ApprovalPanelComponent, + type ApprovalPanelResponse, +} from './components/dialogs/approval-panel'; +import { + ApiKeyInputDialogComponent, + type ApiKeyInputResult, +} from './components/dialogs/api-key-input-dialog'; +import { CompactionComponent } from './components/dialogs/compaction'; +import { EditorSelectorComponent } from './components/dialogs/editor-selector'; +import { + FeedbackInputDialogComponent, + type FeedbackInputDialogResult, +} from './components/dialogs/feedback-input-dialog'; +import { HelpPanelComponent } from './components/dialogs/help-panel'; +import { ModelSelectorComponent } from './components/dialogs/model-selector'; +import { PlatformSelectorComponent } from './components/dialogs/platform-selector'; +import { PermissionSelectorComponent } from './components/dialogs/permission-selector'; +import { QuestionDialogComponent } from './components/dialogs/question-dialog'; +import { SessionPickerComponent, type SessionRow } from './components/dialogs/session-picker'; +import { TaskOutputViewer } from './components/dialogs/task-output-viewer'; +import { TasksBrowserApp, type TasksFilter } from './components/dialogs/tasks-browser'; +import { + SettingsSelectorComponent, + type SettingsSelection, +} from './components/dialogs/settings-selector'; +import { ThemeSelectorComponent } from './components/dialogs/theme-selector'; +import { CustomEditor } from './components/editor/custom-editor'; +import { FileMentionProvider } from './components/editor/file-mention-provider'; +import { AgentGroupComponent } from './components/messages/agent-group'; +import { AssistantMessageComponent } from './components/messages/assistant-message'; +import { BackgroundAgentStatusComponent } from './components/messages/background-agent-status'; +import { buildMcpStatusReportLines } from './components/messages/mcp-status-panel'; +import { ReadGroupComponent } from './components/messages/read-group'; +import { SkillActivationComponent } from './components/messages/skill-activation'; +import { + NoticeMessageComponent, + StatusMessageComponent, +} from './components/messages/status-message'; +import { buildStatusReportLines } from './components/messages/status-panel'; +import { ThinkingComponent } from './components/messages/thinking'; +import { ToolCallComponent } from './components/messages/tool-call'; +import { + buildUsageReportLines, + UsagePanelComponent, + type ManagedUsageReport, +} from './components/messages/usage-panel'; +import { UserMessageComponent } from './components/messages/user-message'; +import { ActivityPaneComponent, type ActivityPaneMode } from './components/panes/activity-pane'; +import { QueuePaneComponent } from './components/panes/queue-pane'; +import { saveTuiConfig, type TuiConfig } from './config'; +import { + FEEDBACK_ISSUE_URL, + FEEDBACK_STATUS_CANCELLED, + FEEDBACK_STATUS_FALLBACK, + FEEDBACK_STATUS_NETWORK_ERROR, + FEEDBACK_STATUS_NOT_SIGNED_IN, + FEEDBACK_STATUS_SUBMITTING, + FEEDBACK_STATUS_SUCCESS, + FEEDBACK_TELEMETRY_EVENT, + errorReportHintLine, + feedbackHttpErrorMessage, + feedbackSessionLine, + withFeedbackVersionPrefix, +} from './constant/feedback'; +import { + CTRL_C_HINT, + CTRL_D_HINT, + DEFAULT_OAUTH_PROVIDER_NAME, + EXIT_CONFIRM_WINDOW_MS, + isManagedUsageProvider, + LLM_NOT_SET_MESSAGE, + MAIN_AGENT_ID, + NO_ACTIVE_SESSION_MESSAGE, + OAUTH_LOGIN_REQUIRED_CODE, + OAUTH_LOGIN_REQUIRED_STARTUP_NOTICE, +} from './constant/kimi-tui'; +import { adaptPanelResponse } from './reverse-rpc/approval/adapter'; +import { ApprovalController } from './reverse-rpc/approval/controller'; +import { createApprovalRequestHandler } from './reverse-rpc/approval/handler'; +import { registerReverseRPCHandlers } from './reverse-rpc/index'; +import { QuestionController } from './reverse-rpc/question/controller'; +import { createQuestionAskHandler } from './reverse-rpc/question/handler'; +import type { ApprovalPanelData, QuestionPanelData } from './reverse-rpc/types'; +import { createKimiTUIThemeBundle, type KimiTUIThemeBundle } from './theme/bundle'; +import type { ResolvedTheme } from './theme/colors'; +import { isTheme, type Theme } from './theme/index'; +import { + INITIAL_LIVE_PANE, + type AppState, + type BackgroundAgentMetadata, + type LivePaneState, + type QueuedMessage, + type ToolCallBlockData, + type ToolResultBlockData, + type TranscriptEntry, +} from './types'; +import { formatBackgroundAgentTranscript } from './utils/background-agent-status'; +import { formatBackgroundTaskTranscript } from './utils/background-task-status'; +import { hasDispose, isExpandable, isPlanExpandable } from './utils/component-capabilities'; +import { + argsRecord, + formatErrorMessage, + isTodoItemShape, + parseStreamingArgs, + serializeToolResultOutput, + stringValue, +} from './utils/event-payload'; +import { isAbortError } from './utils/errors'; +import { ImageAttachmentStore, type ImageAttachment } from './utils/image-attachment-store'; +import { extractMediaAttachments } from './utils/image-placeholder'; +import { McpOAuthAuthorizationUrlOpener } from './utils/mcp-oauth'; +import { + formatMcpStartupStatusSummary, + mcpServerStatusKey, + type McpServerStatusSnapshot, + selectMcpStartupStatusRows, +} from './utils/mcp-server-status'; +import { openUrl } from './utils/open-url'; +import { setProcessTitle } from './utils/proctitle'; +import { installTerminalFocusTracking } from './utils/terminal-focus'; +import { notifyTerminalOnce } from './utils/terminal-notification'; +import { createTerminalState, type TerminalState } from './utils/terminal-state'; +import { installTerminalThemeTracking } from './utils/terminal-theme'; +import { nextTranscriptId } from './utils/transcript-id'; + +export interface KimiTUIStartupInput { + readonly cliOptions: CLIOptions; + readonly tuiConfig: TuiConfig; + readonly version: string; + readonly workDir: string; + readonly startupNotice?: string; + readonly resolvedTheme?: ResolvedTheme; + readonly migrationPlan?: MigrationPlan | null; + /** When true, run only the migration screen, then exit (the `kimi migrate` command). */ + readonly migrateOnly?: boolean; +} + +export interface PendingExit { + readonly kind: 'ctrl-c' | 'ctrl-d'; + readonly timer: ReturnType<typeof setTimeout>; +} + +type EffectiveActivityPaneMode = ActivityPaneMode | 'idle' | 'session'; + +export interface TUIStartupOptions { + readonly sessionFlag?: string; + readonly continueLast: boolean; + readonly yolo: boolean; + readonly plan: boolean; + readonly model?: string; + readonly startupNotice?: string; +} + +export type TUIStartupState = 'pending' | 'ready' | 'picker'; + +export interface KimiTUIOptions { + initialAppState: AppState; + startup: TUIStartupOptions; + resolvedTheme?: ResolvedTheme; +} + +export interface TUIState { + ui: TUI; + terminal: ProcessTerminal; + transcriptContainer: Container; + activityContainer: Container; + todoPanelContainer: Container; + todoPanel: TodoPanelComponent; + queueContainer: Container; + editorContainer: Container; + footer: FooterComponent; + editor: CustomEditor; + theme: KimiTUIThemeBundle; + appState: AppState; + startupState: TUIStartupState; + startupNotice: string | undefined; + livePane: LivePaneState; + transcriptEntries: TranscriptEntry[]; + terminalState: TerminalState; + activitySpinner: MoonLoader | undefined; + activitySpinnerStyle: SpinnerStyle | undefined; + activeThinkingComponent: ThinkingComponent | undefined; + streamingComponent: AssistantMessageComponent | undefined; + streamingTranscriptEntry: TranscriptEntry | undefined; + activeCompactionBlock: CompactionComponent | undefined; + toolOutputExpanded: boolean; + planExpanded: boolean; + lastActivityMode: string | undefined; + lastHistoryContent: string | undefined; + pendingToolComponents: Map<string, ToolCallComponent>; + pendingAgentGroup: { + readonly turnId: string | undefined; + readonly step: number; + solo?: ToolCallComponent; + group?: AgentGroupComponent; + } | null; + pendingReadGroup: { + readonly turnId: string | undefined; + readonly step: number; + solo?: ToolCallComponent; + group?: ReadGroupComponent; + } | null; + backgroundAgents: Set<string>; + backgroundAgentMetadata: Map<string, BackgroundAgentMetadata>; + /** + * Authoritative live mirror of the BPM. Keyed by `taskId`. Includes + * both bash and agent tasks, and retains terminal entries until they + * are explicitly forgotten (kept so transcript replay and footer + * lookups stay consistent). + */ + backgroundTasks: Map<string, BackgroundTaskInfo>; + /** + * Task IDs whose terminal transcript card has already been pushed. + * Used to dedupe between the BPM `background.task.terminated` event + * and the older `subagent.completed/failed` flow, both of which + * arrive for `agent-*` tasks. + */ + backgroundTaskTranscriptedTerminal: Set<string>; + renderedSkillActivationIds: Set<string>; + renderedMcpServerStatusKeys: Map<string, string>; + mcpServerStatusSpinners: Map<string, MoonLoader>; + subagentParentToolCallIds: Map<string, string>; + subagentNames: Map<string, string>; + sessions: SessionRow[]; + loadingSessions: boolean; + showingSessionPicker: boolean; + showingHelpPanel: boolean; + /** + * Active `/tasks` full-screen takeover. When non-undefined, the main + * TUI's children have been replaced by `component`; `savedChildren` + * holds the original list so we can restore on exit. + */ + tasksBrowser: + | { + component: TasksBrowserApp; + savedChildren: readonly Component[]; + filter: TasksFilter; + selectedTaskId: string | undefined; + tailOutput: string | undefined; + tailLoading: boolean; + tailRequestId: number; + flashMessage: string | undefined; + flashTimer: NodeJS.Timeout | undefined; + pollTimer: NodeJS.Timeout | undefined; + /** + * Active nested output viewer (TaskOutputViewer). Undefined when + * the browser is showing its normal 3-pane layout. + */ + viewer: + | { + component: TaskOutputViewer; + savedChildren: readonly Component[]; + /** Task whose output the viewer is currently following. */ + taskId: string; + /** Latest output snapshot pushed into the viewer. */ + output: string; + /** Last in-flight refresh — used to ignore late responses. */ + refreshId: number; + /** 1s background poll so live tail still works if events drop. */ + pollTimer: NodeJS.Timeout; + } + | undefined; + } + | undefined; + externalEditorRunning: boolean; + currentTurnId: string | undefined; + currentStep: number; + assistantDraft: string; + assistantStreamActive: boolean; + thinkingDraft: string; + activeToolCalls: Map<string, ToolCallBlockData>; + streamingToolCallArguments: Map< + string, + { name?: string; argumentsText: string; startedAtMs: number } + >; + queuedMessages: QueuedMessage[]; +} + +// Builds the app-state snapshot used before a session is attached. +function createInitialAppState(input: KimiTUIStartupInput): AppState { + const startupPermission: PermissionMode = input.cliOptions.yolo ? 'yolo' : 'manual'; + return { + model: '', + workDir: input.workDir, + sessionId: '', + yolo: input.cliOptions.yolo, + permissionMode: startupPermission, + planMode: input.cliOptions.plan, + thinking: false, + contextUsage: 0, + contextTokens: 0, + maxContextTokens: 0, + isStreaming: false, + isCompacting: false, + isReplaying: false, + streamingPhase: 'idle', + streamingStartTime: 0, + theme: input.tuiConfig.theme, + version: input.version, + editorCommand: input.tuiConfig.editorCommand, + notifications: input.tuiConfig.notifications, + availableModels: {}, + availableProviders: {}, + sessionTitle: null, + }; +} + +// Creates all pi-tui components and mutable runtime state owned by KimiTUI. +export function createTUIState(options: KimiTUIOptions): TUIState { + const initialAppState = options.initialAppState; + const theme = createKimiTUIThemeBundle(initialAppState.theme, options.resolvedTheme); + + const terminal = new ProcessTerminal(); + const ui = new TUI(terminal); + + // Every chrome container runs with a 2-column outer gutter on each + // side. That gives the transcript, panels, the editor and the + // statusline a shared left edge — the input box's `│` lines up with + // panel borders like Welcome's `│`, and bullets / `>` share a column. + const transcriptContainer = new GutterContainer(CHROME_GUTTER, CHROME_GUTTER); + const activityContainer = new GutterContainer(CHROME_GUTTER, CHROME_GUTTER); + const todoPanelContainer = new GutterContainer(CHROME_GUTTER, CHROME_GUTTER); + const todoPanel = new TodoPanelComponent(theme.colors); + const queueContainer = new GutterContainer(CHROME_GUTTER, CHROME_GUTTER); + const editorContainer = new GutterContainer(CHROME_GUTTER, CHROME_GUTTER); + const editor = new CustomEditor(ui, theme.colors); + const footer = new FooterComponent({ ...initialAppState }, theme.colors, () => { + ui.requestRender(); + }); + + return { + ui, + terminal, + transcriptContainer, + activityContainer, + todoPanelContainer, + todoPanel, + queueContainer, + editorContainer, + footer, + editor, + theme, + appState: { ...initialAppState }, + startupState: 'pending', + startupNotice: options.startup.startupNotice, + livePane: { ...INITIAL_LIVE_PANE }, + transcriptEntries: [], + terminalState: createTerminalState(), + activitySpinner: undefined, + activitySpinnerStyle: undefined, + activeThinkingComponent: undefined, + streamingComponent: undefined, + streamingTranscriptEntry: undefined, + activeCompactionBlock: undefined, + toolOutputExpanded: false, + planExpanded: false, + lastActivityMode: undefined, + lastHistoryContent: undefined, + pendingToolComponents: new Map<string, ToolCallComponent>(), + pendingAgentGroup: null, + pendingReadGroup: null, + backgroundAgents: new Set<string>(), + backgroundAgentMetadata: new Map<string, BackgroundAgentMetadata>(), + backgroundTasks: new Map<string, BackgroundTaskInfo>(), + backgroundTaskTranscriptedTerminal: new Set<string>(), + renderedSkillActivationIds: new Set<string>(), + renderedMcpServerStatusKeys: new Map<string, string>(), + mcpServerStatusSpinners: new Map<string, MoonLoader>(), + subagentParentToolCallIds: new Map<string, string>(), + subagentNames: new Map<string, string>(), + sessions: [], + loadingSessions: false, + showingSessionPicker: false, + showingHelpPanel: false, + tasksBrowser: undefined, + externalEditorRunning: false, + currentTurnId: undefined, + currentStep: 0, + assistantDraft: '', + assistantStreamActive: false, + thinkingDraft: '', + activeToolCalls: new Map<string, ToolCallBlockData>(), + streamingToolCallArguments: new Map(), + queuedMessages: [], + }; +} + +// Merges startup notices while preserving their display order. +function combineStartupNotice( + existing: string | undefined, + next: string | undefined, +): string | undefined { + if (existing !== undefined && next !== undefined) { + return `${existing}\n${next}`; + } + return existing ?? next; +} + +function isOAuthLoginRequiredError(error: unknown): boolean { + if (typeof error !== 'object' || error === null) return false; + return (error as { readonly code?: unknown }).code === OAUTH_LOGIN_REQUIRED_CODE; +} + +interface SessionUsageResult { + readonly usage?: SessionUsage; + readonly error?: string; +} + +interface ManagedUsageResult { + readonly usage?: ManagedUsageReport; + readonly error?: string; +} + +interface RuntimeStatusResult { + readonly status?: SessionStatus; + readonly error?: string; +} + +interface SendMessageOptions { + readonly parts?: readonly PromptPart[]; + readonly imageAttachmentIds?: readonly number[]; + readonly hasMedia?: boolean; +} + +interface LoginProgressSpinnerHandle { + /** Stops the login progress row and replaces it with a final status. */ + stop(opts: { ok: boolean; label: string }): void; +} + +export class KimiTUI { + private readonly harness: KimiHarness; + private readonly options: KimiTUIOptions; + private session: Session | undefined; + private state: TUIState; + private readonly approvalController = new ApprovalController(); + private readonly questionController = new QuestionController(); + private readonly reverseRpcDisposers: Array<() => void> = []; + private skillCommands: readonly KimiSlashCommand[] = []; + private readonly skillCommandMap = new Map<string, string>(); + private readonly imageStore = new ImageAttachmentStore(); + private readonly fdPath: string | null = detectFdPath(); + private readonly gitLsFilesCache: GitLsFilesCache; + private sessionEventUnsubscribe: (() => void) | undefined; + private pendingExit: PendingExit | null = null; + private cancelInFlight: (() => void) | undefined; + // Queues editor messages instead of sending or steering them. Used by /init. + private deferUserMessages = false; + private aborted = false; + private terminalFocusTrackingDispose: (() => void) | undefined; + private terminalThemeTrackingDispose: (() => void) | undefined; + // First-launch migration plan detected pre-TUI; null when nothing to migrate. + private readonly migrationPlan: MigrationPlan | null; + // When true, the migration screen is the whole session: run it, then exit. + private readonly migrateOnly: boolean; + + public onExit?: (exitCode?: number) => Promise<void>; + + private track( + event: string, + properties?: Parameters<KimiHarness['track']>[1], + ): void { + this.harness.track(event, properties); + } + + // Initializes state, reverse-RPC handlers, editor callbacks, and layout. + constructor(harness: KimiHarness, startupInput: KimiTUIStartupInput) { + this.harness = harness; + const tuiOptions: KimiTUIOptions = { + initialAppState: createInitialAppState(startupInput), + startup: { + sessionFlag: startupInput.cliOptions.session, + continueLast: startupInput.cliOptions.continue, + yolo: startupInput.cliOptions.yolo, + plan: startupInput.cliOptions.plan, + model: startupInput.cliOptions.model, + startupNotice: startupInput.startupNotice, + }, + resolvedTheme: startupInput.resolvedTheme, + }; + this.options = tuiOptions; + this.migrationPlan = startupInput.migrationPlan ?? null; + this.migrateOnly = startupInput.migrateOnly ?? false; + this.state = createTUIState(tuiOptions); + this.gitLsFilesCache = createGitLsFilesCache(tuiOptions.initialAppState.workDir); + + // Register approval / question UI controllers before SDK handlers. + this.reverseRpcDisposers.push( + ...registerReverseRPCHandlers(this.approvalController, this.questionController, { + showApprovalPanel: (payload) => { + this.showApprovalPanel(payload); + }, + hideApprovalPanel: () => { + this.hideApprovalPanel(); + }, + showQuestionDialog: (payload) => { + this.showQuestionDialog(payload); + }, + hideQuestionDialog: () => { + this.hideQuestionDialog(); + }, + }), + ); + this.setupEditorHandlers(); + this.buildLayout(); + } + + // ========================================================================= + // Startup Helpers + // ========================================================================= + + // Returns built-in and dynamically loaded slash commands in display order. + private getSlashCommands(): readonly KimiSlashCommand[] { + return [...sortSlashCommands(BUILTIN_SLASH_COMMANDS), ...this.skillCommands]; + } + + // Rebuilds editor autocomplete from slash commands and file mentions. + private setupAutocomplete(): void { + const slashCommands: SlashCommand[] = this.getSlashCommands().map((cmd) => ({ + name: cmd.name, + description: cmd.description, + })); + const provider = new FileMentionProvider( + slashCommands, + this.state.appState.workDir, + this.fdPath, + this.gitLsFilesCache, + ); + this.state.editor.setAutocompleteProvider(provider); + } + + // Loads skill-backed slash commands from the active session. + private async refreshSkillCommands(session?: SkillListSession): Promise<void> { + if (session === undefined) { + this.skillCommands = []; + this.skillCommandMap.clear(); + this.setupAutocomplete(); + return; + } + + let skills; + try { + skills = await session.listSkills(); + } catch { + return; + } + const skillCommands = buildSkillSlashCommands(skills); + this.skillCommands = skillCommands.commands; + this.skillCommandMap.clear(); + for (const [commandName, skillName] of skillCommands.commandMap) { + this.skillCommandMap.set(commandName, skillName); + } + this.setupAutocomplete(); + } + + // Restores persisted input history for the current working directory. + private async loadPersistedInputHistory(): Promise<void> { + try { + const file = getInputHistoryFile(this.state.appState.workDir); + const entries = await loadInputHistory(file); + for (const entry of entries) { + this.state.editor.addToHistory(entry.content); + } + this.state.lastHistoryContent = entries.at(-1)?.content; + } catch { + /* history is best-effort */ + } + } + + // ========================================================================= + // Lifecycle + // ========================================================================= + + // Starts the TUI, performs startup routing, and begins session event handling. + async start(): Promise<void> { + // Migration path: the migration screen is a pi-tui component, so the event + // loop must run first. It then renders as the very first thing on screen, + // before the session is created and the Welcome banner is drawn. + if (this.migrationPlan !== null) { + this.startEventLoop(); + try { + const migrationResult = await this.runMigrationScreen(this.migrationPlan); + if (this.migrateOnly) { + // Explicit `kimi migrate`: the screen is the whole command — exit + // instead of continuing into the chat TUI. A migration that ran but + // failed exits non-zero so scripted callers can detect it. + const failed = + migrationResult.decision === 'now' && migrationResult.migrated === false; + // Restore the terminal before `onExit` calls `process.exit`: dispose + // the focus/theme tracking `startEventLoop()` installed, then stop + // the pi-tui loop. Skipping either leaves the terminal in raw mode + // or still emitting focus/OSC sequences after the command finishes. + this.disposeTerminalTracking(); + this.state.ui.stop(); + await this.onExit?.(failed ? 1 : 0); + return; + } + const shouldReplayHistory = await this.initMainTui(); + await this.finishStartup(shouldReplayHistory); + } catch (error) { + // The pi-tui loop is running and startEventLoop() installed focus/ + // theme tracking; a startup failure must tear all of it down before + // the exception propagates, otherwise the terminal is left in raw + // mode or still emitting focus/OSC sequences. + this.disposeTerminalTracking(); + this.state.ui.stop(); + throw error; + } + return; + } + + // No-migration path: ordering is identical to the original `start()`. + const shouldReplayHistory = await this.initMainTui(); + this.startEventLoop(); + try { + await this.finishStartup(shouldReplayHistory); + } catch (error) { + // The pi-tui loop is running and startEventLoop() installed focus/theme + // tracking; tear all of it down so a finishStartup failure does not + // leave the terminal in raw mode or emitting focus/OSC sequences. + this.disposeTerminalTracking(); + this.state.ui.stop(); + throw error; + } + } + + // Creates/resumes the session, renders the Welcome banner, configures + // autocomplete and input history, and mounts the editor. Returns whether + // transcript history should be replayed. + private async initMainTui(): Promise<boolean> { + const shouldReplayHistory = await this.init(); + + this.renderWelcome(); + this.setupAutocomplete(); + void this.loadPersistedInputHistory(); + this.state.editorContainer.clear(); + this.state.editorContainer.addChild(this.state.editor); + this.state.ui.setFocus(this.state.editor); + return shouldReplayHistory; + } + + // Starts the pi-tui event loop and installs terminal focus/theme tracking. + private startEventLoop(): void { + this.state.ui.start(); + this.terminalFocusTrackingDispose = installTerminalFocusTracking(this.state); + this.refreshTerminalThemeTracking(); + } + + // Runs post-init startup tasks: startup notice, picker bootstrap, transcript + // replay, and session event subscriptions. + private async finishStartup(shouldReplayHistory: boolean): Promise<void> { + if (this.state.startupNotice !== undefined) { + this.showStatus(this.state.startupNotice); + this.state.startupNotice = undefined; + } + if (this.state.startupState === 'picker') { + void this.bootstrapFromPicker(); + // resumeSession (fired on picker select) owns post-pick init; nothing + // else to do here until the user makes a choice. + return; + } + if (shouldReplayHistory) { + await hydrateTranscriptFromReplay( + this.state, + this.replayHydrationHooks(), + this.requireSession(), + ); + } + if (this.session !== undefined) { + this.startSessionEventSubscription(); + } + void this.fetchSessions(); + if (this.session !== undefined) { + this.refreshSessionTitle(); + } + void this.refreshSkillCommands(this.session); + } + + // Creates or resumes the startup session and reports whether history should replay. + private async init(): Promise<boolean> { + await this.refreshAvailableModels(); + + const { startup } = this.options; + const { workDir } = this.state.appState; + let session: Session | undefined; + let shouldReplayHistory = false; + const isResumeStartup = startup.sessionFlag !== undefined || startup.continueLast; + const createSessionOptions: CreateSessionOptions = { + workDir, + model: startup.model, + permission: startup.yolo ? 'yolo' : undefined, + planMode: startup.plan ? true : undefined, + }; + + try { + if (isResumeStartup) { + if (startup.sessionFlag === '') { + this.state.startupState = 'picker'; + return false; + } + + if (startup.sessionFlag !== undefined) { + const sessions = await this.harness.listSessions({ workDir }); + const target = sessions.find((candidate) => candidate.id === startup.sessionFlag); + if (target === undefined) { + throw new Error(`Session "${startup.sessionFlag}" not found.`); + } + session = await this.harness.resumeSession({ id: startup.sessionFlag }); + shouldReplayHistory = true; + } else { + const sessions = await this.harness.listSessions({ workDir }); + const target = sessions[0]; + if (target !== undefined) { + session = await this.harness.resumeSession({ id: target.id }); + shouldReplayHistory = true; + } else { + session = await this.harness.createSession(createSessionOptions); + this.state.startupNotice = combineStartupNotice( + this.state.startupNotice, + `No sessions to continue under "${workDir}"; starting a fresh session.`, + ); + } + } + } else { + session = await this.harness.createSession(createSessionOptions); + } + if (session !== undefined && startup.model !== undefined && isResumeStartup) { + await session.setModel(startup.model); + } + } catch (error) { + if (!isOAuthLoginRequiredError(error)) throw error; + this.enterLoginRequiredStartupState(); + return false; + } + + if (session === undefined) { + throw new Error('Startup session was not initialized.'); + } + await this.setSession(session); + await this.syncRuntimeState(session); + this.state.startupState = 'ready'; + return shouldReplayHistory; + } + + // Stops UI resources, active sessions, reverse-RPC handlers, and the harness. + async stop(): Promise<void> { + this.aborted = true; + if (this.pendingExit) { + clearTimeout(this.pendingExit.timer); + this.pendingExit = null; + } + for (const dispose of this.reverseRpcDisposers) { + dispose(); + } + this.reverseRpcDisposers.length = 0; + this.disposeTerminalTracking(); + await this.closeSession('shutting down'); + await this.harness.close(); + this.stopAllMcpServerStatusSpinners(); + this.state.ui.stop(); + if (this.onExit) { + await this.onExit(); + } + } + + // Tears down the terminal focus + theme tracking installed by + // `startEventLoop()`. Every exit path must run this, or the terminal is + // left with focus-reporting / theme-query modes on and emits stray + // focus/OSC sequences after the process exits. + private disposeTerminalTracking(): void { + this.stopTerminalThemeTracking(); + this.terminalFocusTrackingDispose?.(); + this.terminalFocusTrackingDispose = undefined; + } + + // Returns the currently selected session id shown by the UI. + getCurrentSessionId(): string { + return this.state.appState.sessionId; + } + + // Reports whether the transcript contains user-visible session content. + hasSessionContent(): boolean { + return this.state.transcriptEntries.length > 0; + } + + async getStartupMcpMs(): Promise<number> { + const session = this.session; + if (session === undefined) return 0; + try { + const metrics = await session.getMcpStartupMetrics(); + return metrics.durationMs; + } catch { + return 0; + } + } + + // ========================================================================= + // Auth / Model Bootstrap + // ========================================================================= + + // Refreshes model metadata from the harness config. + private async refreshAvailableModels(): Promise<void> { + const config = await this.harness.getConfig({ reload: true }); + this.setAppState({ + availableModels: config.models ?? {}, + availableProviders: config.providers ?? {}, + }); + } + + // Allows the shell to start even when the managed OAuth token needs login. + private enterLoginRequiredStartupState(): void { + this.resetSessionRuntime(); + this.setAppState({ + sessionId: '', + model: '', + thinking: false, + contextTokens: 0, + maxContextTokens: 0, + contextUsage: 0, + sessionTitle: null, + }); + this.state.startupNotice = combineStartupNotice( + this.state.startupNotice, + OAUTH_LOGIN_REQUIRED_STARTUP_NOTICE, + ); + this.state.startupState = 'ready'; + } + + // Ensures a usable session exists for the default model after login. + private async activateModelAfterLogin(model: string, thinking?: boolean): Promise<void> { + const level = thinking === undefined ? undefined : thinking ? 'on' : 'off'; + if (this.session !== undefined) { + await this.session.setModel(model); + if (level !== undefined) { + await this.session.setThinking(level); + } + return; + } + + const session = await this.harness.createSession({ + workDir: this.state.appState.workDir, + model, + thinking: level, + permission: this.options.startup.yolo ? 'yolo' : undefined, + planMode: this.state.appState.planMode ? true : undefined, + }); + await this.setSession(session); + this.setAppState({ + sessionId: session.id, + sessionTitle: session.summary?.title ?? null, + }); + await this.syncRuntimeState(session); + this.startSessionEventSubscription(); + void this.fetchSessions(); + this.refreshSessionTitle(); + void this.refreshSkillCommands(this.session); + } + + // Clears the active session and runtime UI after logout. + private async clearActiveSessionAfterLogout(): Promise<void> { + await this.closeSession('logged out'); + this.resetSessionRuntime(); + this.setAppState({ + sessionId: '', + model: '', + sessionTitle: null, + }); + await this.refreshSkillCommands(); + } + + // Reloads config after login and selects the configured default model. + private async refreshConfigAfterLogin(): Promise<void> { + const config = await this.harness.getConfig({ reload: true }); + const availableModels = config.models ?? {}; + const availableProviders = config.providers ?? {}; + const defaultModel = this.options.startup.model ?? config.defaultModel; + const selected = defaultModel !== undefined ? availableModels[defaultModel] : undefined; + + if (defaultModel === undefined || selected === undefined) { + this.setAppState({ availableModels, availableProviders }); + return; + } + + await this.activateModelAfterLogin(defaultModel, config.defaultThinking); + const appStatePatch: Partial<AppState> = { + availableModels, + availableProviders, + model: defaultModel, + maxContextTokens: selected.maxContextSize, + }; + if (config.defaultThinking !== undefined) { + appStatePatch.thinking = config.defaultThinking; + } + this.setAppState(appStatePatch); + } + + // Reloads config after logout and clears model-dependent state. + private async refreshConfigAfterLogout(): Promise<void> { + const config = await this.harness.getConfig({ reload: true }); + const availableModels = config.models ?? {}; + const availableProviders = config.providers ?? {}; + this.setAppState({ + availableModels, + availableProviders, + model: '', + thinking: false, + maxContextTokens: 0, + contextUsage: 0, + contextTokens: 0, + }); + } + + // ========================================================================= + // Layout / Editor Setup + // ========================================================================= + + // Mounts the root TUI containers in their rendering order. + private buildLayout(): void { + const { ui } = this.state; + ui.clear(); + ui.addChild(this.state.transcriptContainer); + ui.addChild(this.state.activityContainer); + ui.addChild(this.state.todoPanelContainer); + ui.addChild(this.state.queueContainer); + ui.addChild(this.state.editorContainer); + // FooterComponent isn't a Container; wrap it so it picks up the same + // outer gutter as the transcript/panels above. + const footerWrap = new GutterContainer(CHROME_GUTTER, CHROME_GUTTER); + footerWrap.addChild(this.state.footer); + ui.addChild(footerWrap); + } + + // Wires editor shortcuts, submission, paste, and navigation callbacks. + private setupEditorHandlers(): void { + const editor = this.state.editor; + + editor.onSubmit = (text: string) => { + this.handleUserInput(text); + }; + + editor.onChange = (text: string) => { + if (this.pendingExit) this.clearPendingExit(); + this.updateEditorBorderHighlight(text); + }; + + editor.onCtrlC = () => { + if (this.cancelInFlight !== undefined) { + const cancel = this.cancelInFlight; + this.cancelInFlight = undefined; + this.clearPendingExit(); + cancel(); + return; + } + + if (this.state.appState.isStreaming) { + this.clearPendingExit(); + this.cancelCurrentStream(); + return; + } + + if (this.state.appState.isCompacting) { + this.clearPendingExit(); + this.cancelCurrentCompaction(); + return; + } + + if (this.pendingExit?.kind === 'ctrl-c') { + this.clearPendingExit(); + void this.stop(); + return; + } + + if (editor.getText().length > 0) { + editor.setText(''); + } + this.armPendingExit('ctrl-c', CTRL_C_HINT); + }; + + editor.onCtrlD = () => { + if (this.pendingExit?.kind === 'ctrl-d') { + this.clearPendingExit(); + void this.stop(); + return; + } + this.armPendingExit('ctrl-d', CTRL_D_HINT); + }; + + editor.onEscape = () => { + if (this.pendingExit) this.clearPendingExit(); + if (this.state.showingSessionPicker) { + this.hideSessionPicker(); + return; + } + if (this.state.appState.isStreaming) { + this.cancelCurrentStream(); + return; + } + if (this.state.appState.isCompacting) { + this.cancelCurrentCompaction(); + } + }; + + editor.onShiftTab = () => { + const session = this.session; + if (session === undefined) { + this.showError(NO_ACTIVE_SESSION_MESSAGE); + return; + } + const next = !this.state.appState.planMode; + this.track('shortcut_plan_toggle', { enabled: next }); + this.track('shortcut_mode_switch', { to_mode: next ? 'plan' : 'agent' }); + void this.applyPlanMode(session, next); + }; + + editor.onOpenExternalEditor = () => { + this.track('shortcut_editor'); + void this.openExternalEditor(); + }; + + editor.onToggleToolExpand = () => { + this.track('shortcut_expand'); + this.toggleToolOutputExpansion(); + }; + + editor.onTogglePlanExpand = () => this.togglePlanExpansion(); + + editor.onCtrlS = () => { + if (!this.state.appState.isStreaming || this.state.appState.isCompacting) return; + const text = editor.getText().trim(); + const queuedTexts = this.state.queuedMessages.map((m) => m.text); + this.state.queuedMessages = []; + + const parts: string[] = []; + for (const q of queuedTexts) { + const trimmed = q.trim(); + if (trimmed.length > 0) parts.push(trimmed); + } + if (text.length > 0) parts.push(text); + + if (parts.length > 0) { + editor.setText(''); + const session = this.session; + if (this.state.appState.model.trim().length === 0 || session === undefined) { + this.showError(LLM_NOT_SET_MESSAGE); + } else { + this.steerMessage(session, parts); + } + } + this.updateQueueDisplay(); + this.state.ui.requestRender(); + }; + + editor.onUndo = () => { + this.track('undo'); + }; + + editor.onInsertNewline = () => { + this.track('shortcut_newline'); + }; + + editor.onTextPaste = () => { + this.track('shortcut_paste', { kind: 'text' }); + }; + + editor.onUpArrowEmpty = () => { + if (!this.state.appState.isStreaming && !this.state.appState.isCompacting) return false; + const recalled = this.recallLastQueued(); + if (recalled !== undefined) { + editor.setText(recalled); + this.updateQueueDisplay(); + this.state.ui.requestRender(); + return true; + } + return false; + }; + + editor.onPasteImage = async () => this.handleClipboardImagePaste(); + } + + // Cancels the pending double-key exit prompt. + private clearPendingExit(): void { + if (!this.pendingExit) return; + clearTimeout(this.pendingExit.timer); + this.state.footer.setTransientHint(null); + this.pendingExit = null; + } + + // Starts a timed confirmation window for Ctrl-C or Ctrl-D exit. + private armPendingExit(kind: 'ctrl-c' | 'ctrl-d', hint: string): void { + this.clearPendingExit(); + this.state.footer.setTransientHint(hint); + + const timer = setTimeout(() => { + if (this.pendingExit?.timer === timer) { + this.clearPendingExit(); + this.state.ui.requestRender(); + } + }, EXIT_CONFIRM_WINDOW_MS); + + this.pendingExit = { kind, timer }; + this.state.ui.requestRender(); + } + + // Reads image or video data from the clipboard and inserts an attachment placeholder. + private async handleClipboardImagePaste(): Promise<boolean> { + let media; + try { + media = await readClipboardMedia(); + } catch (error) { + if (error instanceof ClipboardMediaError) { + this.showError(error.message); + return true; + } + return false; + } + if (media === null) return false; + + if (media.kind === 'video') { + const attachment = this.imageStore.addVideo(media.mimeType, media.sourcePath, media.filename); + this.state.editor.insertTextAtCursor?.(`${attachment.placeholder} `); + this.state.ui.requestRender(); + this.track('shortcut_paste', { kind: 'video' }); + return true; + } + + const meta = parseImageMeta(media.bytes); + if (meta === null) return false; + const attachment = this.imageStore.addImage(media.bytes, meta.mime, meta.width, meta.height); + this.state.editor.insertTextAtCursor?.(`${attachment.placeholder} `); + this.state.ui.requestRender(); + this.track('shortcut_paste', { kind: 'image' }); + return true; + } + + // Opens the configured external editor and writes the edited text back. + private async openExternalEditor(): Promise<void> { + if (this.state.externalEditorRunning) return; + const cmd = resolveEditorCommand(this.state.appState.editorCommand); + if (cmd === undefined) { + this.showError('No editor configured. Set $VISUAL / $EDITOR, or run /editor <command>.'); + return; + } + this.state.externalEditorRunning = true; + const seed = this.state.editor.getExpandedText?.() ?? this.state.editor.getText(); + this.state.ui.stop(); + await new Promise<void>((resolve) => { + setImmediate(resolve); + }); + try { + const result = await editInExternalEditor(seed, cmd); + if (result !== undefined) { + this.state.editor.setText(result.replaceAll('\r\n', '\n').replace(/\n$/, '')); + } + } catch (error) { + const msg = formatErrorMessage(error); + this.showError(`External editor failed: ${msg}`); + } finally { + if (typeof process.stdin.pause === 'function') { + process.stdin.pause(); + } + this.state.ui.start(); + this.state.ui.setFocus(this.state.editor); + this.state.ui.requestRender(true); + this.state.externalEditorRunning = false; + } + } + + // ========================================================================= + // Input Dispatch + // ========================================================================= + + // Routes submitted editor text to slash command handling or normal prompting. + private handleUserInput(text: string): void { + if (text.trim().length === 0) return; + if (this.state.appState.isReplaying) { + this.showError('Cannot send input while session history is replaying.'); + return; + } + void this.persistInputHistory(text); + if (parseSlashInput(text) !== null) { + void this.executeSlashCommand(text); + return; + } + + this.sendNormalUserInput(text); + } + + // Parses and executes a slash command intent. + private async executeSlashCommand(input: string): Promise<void> { + const parsedCommand = parseSlashInput(input); + const intent = resolveSlashCommandInput({ + input, + skillCommandMap: this.skillCommandMap, + isStreaming: this.state.appState.isStreaming, + isCompacting: this.state.appState.isCompacting, + }); + + switch (intent.kind) { + case 'not-command': + return; + case 'blocked': + this.track('input_command_invalid', { reason: 'blocked', command: intent.commandName }); + this.showError(slashBusyMessage(intent.commandName, intent.reason)); + return; + case 'skill': { + const session = this.session; + if (this.state.appState.model.trim().length === 0 || session === undefined) { + this.showError(LLM_NOT_SET_MESSAGE); + return; + } + this.track('input_command', { + command: intent.commandName, + skill_name: intent.skillName, + }); + this.sendSkillActivation(session, intent.skillName, intent.args); + return; + } + case 'message': { + this.sendNormalUserInput(intent.input); + return; + } + case 'builtin': + this.track('input_command', { command: intent.name }); + if (intent.name === 'new' && parsedCommand?.name === 'clear') { + this.track('clear'); + } + try { + await this.handleBuiltInSlashCommand(intent.name, intent.args); + } catch (error) { + this.showError(formatErrorMessage(error)); + } + return; + } + } + + // Dispatches a built-in slash command to its concrete handler. + private async handleBuiltInSlashCommand( + name: BuiltinSlashCommandName, + args: string, + ): Promise<void> { + switch (name) { + case 'exit': + void this.stop(); + return; + case 'help': + this.showHelpPanel(); + return; + case 'version': + this.showStatus(`Kimi Code v${this.state.appState.version}`); + return; + case 'new': + await this.createNewSession(); + this.state.ui.requestRender(); + return; + case 'sessions': + void this.showSessionPicker(); + return; + case 'tasks': + void this.showTasksBrowser(); + return; + case 'mcp': + void this.showMcpServers(); + return; + case 'editor': + await this.handleEditorCommand(args, {}); + return; + case 'theme': + await this.handleThemeCommand(args); + return; + case 'model': + this.handleModelCommand(args); + return; + case 'permission': + this.showPermissionPicker(); + return; + case 'settings': + this.showSettingsSelector(); + return; + case 'usage': + void this.showUsage(); + return; + case 'status': + void this.showStatusReport(); + return; + case 'feedback': + await this.handleFeedbackCommand(); + return; + case 'title': + await this.handleTitleCommand(args); + return; + case 'yolo': + await this.handleYoloCommand(args); + return; + case 'plan': + await this.handlePlanCommand(args); + return; + case 'compact': + await this.handleCompactCommand(args); + return; + case 'init': + await this.handleInitCommand(); + return; + case 'fork': + await this.handleForkCommand(args); + return; + case 'login': + await this.handleLoginCommand(); + return; + case 'logout': + await this.handleLogoutCommand(); + return; + default: + this.showError(`Unknown slash command: /${String(name)}`); + return; + } + } + + // Sends regular user input after validating model and media support. + private sendNormalUserInput(text: string): void { + if (this.state.appState.model.trim().length === 0) { + this.showError(LLM_NOT_SET_MESSAGE); + return; + } + const extraction = extractMediaAttachments(text, this.imageStore); + if (!this.validateMediaCapabilities(extraction)) return; + const session = this.session; + if (session === undefined) { + this.showError(LLM_NOT_SET_MESSAGE); + return; + } + if (extraction.hasMedia) { + this.sendMessage(session, text, { + hasMedia: true, + parts: extraction.parts, + imageAttachmentIds: extraction.imageAttachmentIds, + }); + } else { + this.sendMessage(session, text); + } + this.updateQueueDisplay(); + this.state.ui.requestRender(); + } + + // Checks whether the current model can accept attached media. + private validateMediaCapabilities( + extraction: ReturnType<typeof extractMediaAttachments>, + ): boolean { + if (!extraction.hasMedia) return true; + if ( + extraction.imageAttachmentIds.length > 0 && + !this.supportsCurrentModelCapability('image_in') + ) { + this.showError('Current model does not support image input.'); + return false; + } + if ( + extraction.videoAttachmentIds.length > 0 && + !this.supportsCurrentModelCapability('video_in') + ) { + this.showError('Current model does not support video input.'); + return false; + } + return true; + } + + // Tests the active model's advertised capability list. + private supportsCurrentModelCapability(capability: string): boolean { + const capabilities = + this.state.appState.availableModels[this.state.appState.model]?.capabilities; + if (capabilities === undefined) return true; + return capabilities.includes(capability); + } + + // Persists a submitted input line and mirrors it into editor history. + private async persistInputHistory(text: string): Promise<void> { + const trimmed = text.trim(); + if (trimmed.length === 0) return; + if (trimmed === this.state.lastHistoryContent) return; + this.state.editor.addToHistory(trimmed); + try { + const file = getInputHistoryFile(this.state.appState.workDir); + const written = await appendInputHistory(file, trimmed, this.state.lastHistoryContent); + if (written) this.state.lastHistoryContent = trimmed; + } catch { + this.state.lastHistoryContent = trimmed; + } + } + + // Pops the most recent queued message back into the editor. + private recallLastQueued(): string | undefined { + if (this.state.queuedMessages.length === 0) return undefined; + const last = this.state.queuedMessages.at(-1)!; + this.state.queuedMessages = this.state.queuedMessages.slice(0, -1); + return last.text; + } + + // ========================================================================= + // Session Requests / Queues + // ========================================================================= + + // Adds a message to the queue for delivery after current work finishes. + private enqueueMessage(text: string, options?: SendMessageOptions): void { + this.state.queuedMessages.push({ + text, + agentId: this.harness.interactiveAgentId, + parts: options?.parts, + imageAttachmentIds: + options?.imageAttachmentIds !== undefined && options.imageAttachmentIds.length > 0 + ? options.imageAttachmentIds + : undefined, + }); + this.track('input_queue'); + } + + // Resets request-scoped state before submitting work to the active session. + private beginSessionRequest(): void { + this.state.currentTurnId = undefined; + this.resetLiveTextRuntime(); + this.resetLiveToolUiState(); + this.resetToolCallState(); + + this.patchLivePane({ + mode: 'waiting', + pendingApproval: null, + pendingQuestion: null, + }); + this.setAppState({ + isStreaming: true, + streamingPhase: 'waiting', + streamingStartTime: Date.now(), + }); + } + + // Ends a failed session request and renders the failure to the transcript. + private failSessionRequest(message: string): void { + this.setAppState({ isStreaming: false, streamingPhase: 'idle' }); + this.resetLivePane(); + this.showError(message); + } + + // Sends a queued message after restoring the agent target captured at enqueue time. + private sendQueuedMessage(session: Session, item: QueuedMessage): void { + this.harness.interactiveAgentId = item.agentId ?? MAIN_AGENT_ID; + this.sendMessageInternal(session, item.text, { + parts: item.parts, + imageAttachmentIds: item.imageAttachmentIds, + }); + } + + // Appends the user message and sends the prompt to the session immediately. + private sendMessageInternal(session: Session, input: string, options?: SendMessageOptions): void { + const imageAttachmentIds = + options?.imageAttachmentIds !== undefined && options.imageAttachmentIds.length > 0 + ? options.imageAttachmentIds + : undefined; + this.appendTranscriptEntry({ + id: nextTranscriptId(), + kind: 'user', + turnId: undefined, + renderMode: 'plain', + content: input, + imageAttachmentIds, + }); + + this.beginSessionRequest(); + + const sdkInput = options?.parts ?? input; + void session.prompt(sdkInput).catch((error: unknown) => { + const message = formatErrorMessage(error); + this.failSessionRequest(`Failed to send: ${message}`); + }); + } + + // Starts a skill activation turn on the session. + private sendSkillActivation(session: Session, skillName: string, skillArgs: string): void { + this.beginSessionRequest(); + void session.activateSkill(skillName, skillArgs).catch((error: unknown) => { + const message = formatErrorMessage(error); + this.failSessionRequest(`Skill "${skillName}" failed: ${message}`); + }); + } + + // Sends a message now or queues it when the session is busy. + private sendMessage(session: Session, input: string, options?: SendMessageOptions): void { + if ( + this.deferUserMessages || + this.state.appState.isStreaming || + this.state.appState.isCompacting + ) { + this.enqueueMessage(input, options); + return; + } + this.sendMessageInternal(session, input, options); + } + + // Sends steering input into an active stream or falls back to normal prompts. + private steerMessage(session: Session, input: string[]): void { + if (this.deferUserMessages || this.state.appState.isCompacting) { + for (const part of input) { + this.enqueueMessage(part); + } + return; + } + if (!this.state.appState.isStreaming) { + for (const part of input) { + this.sendMessageInternal(session, part); + } + return; + } + + for (const part of input) { + this.appendTranscriptEntry({ + id: nextTranscriptId(), + kind: 'user', + turnId: this.state.currentTurnId, + renderMode: 'plain', + content: part, + }); + } + + void session.steer(input.join('\n\n')).catch((error: unknown) => { + const message = formatErrorMessage(error); + this.showError(`Failed to steer: ${message}`); + }); + } + + // Requests cancellation of the active session stream. + private cancelCurrentStream(): void { + const session = this.session; + if (session === undefined) return; + void session.cancel(); + } + + private cancelCurrentCompaction(): void { + const session = this.session; + if (session === undefined) return; + void session.cancelCompaction().catch((error: unknown) => { + const message = formatErrorMessage(error); + this.showError(`Failed to cancel compaction: ${message}`); + }); + } + + // Finalizes live thinking output and moves the live pane to the next mode. + private flushThinkingToTranscript(nextMode: LivePaneState['mode'] = 'idle'): void { + if (this.state.thinkingDraft.length === 0) { + this.patchLivePane({ mode: nextMode }); + return; + } + this.state.thinkingDraft = ''; + this.onThinkingEnd(); + this.patchLivePane({ mode: nextMode }); + } + + // Finalizes live assistant text and clears streaming component state. + private finalizeAssistantStream(): void { + if (this.state.assistantStreamActive) { + this.onStreamingTextEnd(); + this.state.assistantStreamActive = false; + } + this.state.assistantDraft = ''; + this.updateActivityPane(); + this.state.ui.requestRender(); + } + + // Discards live thinking and assistant text state without finalizing transcript output. + private resetLiveTextRuntime(): void { + this.state.assistantDraft = ''; + this.state.assistantStreamActive = false; + this.state.streamingComponent = undefined; + this.state.streamingTranscriptEntry = undefined; + this.state.thinkingDraft = ''; + this.disposeActiveThinkingComponent(); + } + + // Clears live tool UI state while preserving active tool-call tracking. + private resetLiveToolUiState(): void { + this.state.streamingToolCallArguments.clear(); + this.disposeAndClearPendingToolComponents(); + this.state.pendingAgentGroup = null; + this.state.pendingReadGroup = null; + } + + // Clears SDK tool-call tracking. + private resetToolCallState(): void { + this.state.activeToolCalls.clear(); + } + + // Finalizes any live thinking and assistant text for a phase transition. + private finalizeLiveTextBuffers(nextMode: LivePaneState['mode'] = 'idle'): void { + this.flushThinkingToTranscript(nextMode); + this.finalizeAssistantStream(); + } + + // Completes a turn, dispatches queued work, and sends completion notification. + private finalizeTurn(sendQueued: (item: QueuedMessage) => void): void { + if (!this.state.appState.isStreaming) return; + this.deferUserMessages = false; + const completedTurnKey = + this.state.currentTurnId ?? `local:${String(this.state.appState.streamingStartTime)}`; + this.finalizeLiveTextBuffers('idle'); + this.resetToolCallState(); + this.state.currentTurnId = undefined; + + if (this.state.queuedMessages.length > 0) { + const [next, ...rest] = this.state.queuedMessages; + this.state.queuedMessages = rest; + this.setAppState({ isStreaming: false, streamingPhase: 'idle' }); + this.resetLivePane(); + if (next !== undefined) { + setTimeout(() => { + sendQueued(next); + }, 0); + } + return; + } + + this.setAppState({ isStreaming: false, streamingPhase: 'idle' }); + this.resetLivePane(); + notifyTerminalOnce(this.state, `turn-complete:${completedTurnKey}`, { + title: 'Kimi Code task complete', + body: this.state.appState.sessionTitle ?? undefined, + }); + } + + // ========================================================================= + // State Helpers + // ========================================================================= + + // Applies app-state changes and refreshes dependent UI surfaces. + private setAppState(patch: Partial<AppState>): void { + const busyChanged = 'isStreaming' in patch || 'isCompacting' in patch; + Object.assign(this.state.appState, patch); + if ('planMode' in patch) this.updateEditorBorderHighlight(); + this.state.footer.setState(this.state.appState); + this.updateActivityPane(); + if (busyChanged) this.updateQueueDisplay(); + this.state.ui.requestRender(); + } + + // Applies live-pane changes and refreshes activity presentation. + private patchLivePane(patch: Partial<LivePaneState>): void { + Object.assign(this.state.livePane, patch); + this.updateActivityPane(); + this.state.ui.requestRender(); + } + + // Restores the live pane to its initial idle state. + private resetLivePane(): void { + this.state.livePane = { ...INITIAL_LIVE_PANE }; + this.updateActivityPane(); + this.state.ui.requestRender(); + } + + // ========================================================================= + // Session Runtime + // ========================================================================= + + // Returns the active session or raises the standard no-session error. + private requireSession(): Session { + if (this.session === undefined) { + throw new Error(NO_ACTIVE_SESSION_MESSAGE); + } + return this.session; + } + + // Creates a session using the current model, known session runtime, permission, and plan state. + private async createSessionFromCurrentState(): Promise<Session> { + const model = this.state.appState.model.trim(); + if (model.length === 0) { + throw new Error(LLM_NOT_SET_MESSAGE); + } + return this.harness.createSession({ + workDir: this.state.appState.workDir, + model, + thinking: + this.session === undefined ? undefined : this.state.appState.thinking ? 'on' : 'off', + permission: this.state.appState.permissionMode, + planMode: this.state.appState.planMode ? true : undefined, + }); + } + + // Replaces the active session and installs approval/question handlers. + private async setSession(session: Session): Promise<void> { + const previous = this.unloadCurrentSession('switching session'); + await previous?.close(); + this.session = session; + this.harness.setTelemetryContext({ sessionId: session.id }); + this.registerSessionHandlers(session); + } + + // Pulls runtime session status into the app state. + private async syncRuntimeState(session: Session = this.requireSession()): Promise<void> { + const status = await session.getStatus(); + this.setAppState({ + sessionId: session.id, + model: status.model ?? '', + thinking: status.thinkingLevel !== 'off', + permissionMode: status.permission, + yolo: status.permission === 'yolo', + planMode: status.planMode, + contextTokens: status.contextTokens, + maxContextTokens: status.maxContextTokens, + contextUsage: status.contextUsage, + sessionTitle: session.summary?.title ?? null, + }); + } + + // Applies current permission and plan settings to the active session. + private async activateRuntime(): Promise<void> { + const session = this.requireSession(); + await session.setPermission(this.state.appState.permissionMode); + if (this.state.appState.planMode) { + await session.setPlanMode(true); + } + await this.syncRuntimeState(session); + } + + // Detaches and closes the current session. + private async closeSession(reason: string): Promise<void> { + const previous = this.unloadCurrentSession(reason); + await previous?.close(); + } + + // Detaches session subscriptions and cancels pending interactive requests. + private unloadCurrentSession(reason: string): Session | undefined { + const previous = this.session; + this.sessionEventUnsubscribe?.(); + this.sessionEventUnsubscribe = undefined; + this.clearReverseRpcPanels(); + previous?.setApprovalHandler(undefined); + previous?.setQuestionHandler(undefined); + this.approvalController.cancelAll(reason); + this.questionController.cancelAll(reason); + this.session = undefined; + this.harness.setTelemetryContext({ sessionId: null }); + return previous; + } + + private clearReverseRpcPanels(): void { + for (const dispose of this.reverseRpcDisposers) { + dispose(); + } + } + + // Connects session approval and question requests to local controllers. + private registerSessionHandlers(session: Session): void { + session.setApprovalHandler( + createApprovalRequestHandler(this.approvalController, (request, response) => { + this.appendApprovalTranscriptEntry(request, response); + }), + ); + session.setQuestionHandler(createQuestionAskHandler(this.questionController)); + } + + // Loads session picker rows for the current working directory. + private async fetchSessions(): Promise<void> { + this.state.loadingSessions = true; + try { + const sessions = await this.harness.listSessions({ workDir: this.state.appState.workDir }); + this.state.sessions = sessions.map((session) => ({ + id: session.id, + title: session.title ?? null, + last_prompt: session.lastPrompt ?? null, + work_dir: session.workDir, + updated_at: session.updatedAt ?? session.createdAt ?? 0, + metadata: session.metadata, + })); + } catch { + /* silently ignore */ + } finally { + this.state.loadingSessions = false; + } + } + + // Syncs the process title with the current session title and id. + private refreshSessionTitle(): void { + setProcessTitle(this.state.appState.sessionTitle, this.state.appState.sessionId); + } + + // Resets turn, tool, queue, and background-agent state for a session switch. + private resetSessionRuntime(): void { + this.aborted = false; + this.state.queuedMessages = []; + this.harness.interactiveAgentId = MAIN_AGENT_ID; + this.resetToolCallState(); + this.resetLiveToolUiState(); + this.state.backgroundAgents.clear(); + this.state.backgroundAgentMetadata.clear(); + this.state.backgroundTasks.clear(); + this.state.backgroundTaskTranscriptedTerminal.clear(); + this.closeTasksBrowser(); + this.state.subagentParentToolCallIds.clear(); + this.state.subagentNames.clear(); + this.state.renderedSkillActivationIds.clear(); + this.state.renderedMcpServerStatusKeys.clear(); + this.stopAllMcpServerStatusSpinners(); + this.state.footer.setBackgroundCounts({ bashTasks: 0, agentTasks: 0 }); + this.setTodoList([]); + this.state.currentTurnId = undefined; + this.state.currentStep = 0; + this.resetLiveTextRuntime(); + this.updateQueueDisplay(); + } + + // Switches to an existing session and replays its transcript. + private async resumeSession(targetSessionId: string): Promise<boolean> { + if (targetSessionId === this.state.appState.sessionId) { + this.showStatus('Already on this session.'); + return true; + } + if (this.state.appState.isStreaming) { + this.showError('Cannot switch sessions while streaming — press Esc or Ctrl-C first.'); + return false; + } + if (this.state.appState.isReplaying) { + this.showError('Cannot switch sessions while history is replaying.'); + return false; + } + + let session: Session; + try { + session = await this.harness.resumeSession({ id: targetSessionId }); + } catch (error) { + const msg = formatErrorMessage(error); + this.showError(`Failed to resume session ${targetSessionId}: ${msg}`); + return false; + } + + await this.switchToSession(session, `Resumed session (${session.id}).`); + return true; + } + + // Switches to a provided session and replays its transcript. + private async switchToSession(session: Session, statusMessage: string): Promise<void> { + this.resetSessionRuntime(); + await this.setSession(session); + await this.syncRuntimeState(session); + this.refreshSessionTitle(); + try { + await this.refreshSkillCommands(this.session); + } catch { + /* keep the switched session usable even if dynamic skills fail */ + } + this.clearTranscriptAndRedraw(); + try { + await hydrateTranscriptFromReplay(this.state, this.replayHydrationHooks(), session); + } catch (error) { + const msg = formatErrorMessage(error); + this.showError(`Failed to replay session history: ${msg}`); + } finally { + this.startSessionEventSubscription(); + } + this.showStatus(statusMessage); + } + + // Creates a fresh session from current UI settings and resets the transcript. + private async createNewSession(): Promise<void> { + if (this.state.appState.isReplaying) { + this.showError('Cannot start a new session while history is replaying.'); + return; + } + + let session: Session; + try { + session = await this.createSessionFromCurrentState(); + } catch (error) { + const msg = formatErrorMessage(error); + this.showError(`Failed to start a new session: ${msg}`); + return; + } + + this.resetSessionRuntime(); + await this.setSession(session); + this.setAppState({ sessionId: session.id }); + try { + await this.activateRuntime(); + await this.syncRuntimeState(session); + } catch (error) { + const msg = formatErrorMessage(error); + this.showError(`Post-create setup failed: ${msg}`); + return; + } + try { + await this.refreshSkillCommands(this.session); + } catch { + /* keep the new session usable even if dynamic skills fail */ + } + this.startSessionEventSubscription(); + this.clearTranscriptAndRedraw(); + this.showStatus(`Started a new session (${session.id}).`); + } + + // ========================================================================= + // Session Events + // ========================================================================= + + private startSessionEventSubscription(): void { + const session = this.requireSession(); + const sendQueued = (item: QueuedMessage): void => { + this.sendQueuedMessage(session, item); + }; + this.sessionEventUnsubscribe?.(); + const mcpOAuthOpener = new McpOAuthAuthorizationUrlOpener(openUrl); + const { sessionId } = this.state.appState; + this.sessionEventUnsubscribe = session.onEvent((event) => { + if (this.aborted) return; + if (event.sessionId !== sessionId) return; + if (event.type === 'tool.progress') { + mcpOAuthOpener.handleToolProgress(event); + } + this.handleEvent(event, sendQueued); + }); + void this.syncMcpServerStatusSnapshot(session); + } + + private async syncMcpServerStatusSnapshot(session: Session): Promise<void> { + let servers: readonly McpServerStatusSnapshot[]; + try { + servers = await session.listMcpServers(); + } catch (error) { + if (this.session !== session || this.aborted) return; + const message = error instanceof Error ? error.message : String(error); + this.showError(`Failed to sync MCP server status: ${message}`); + return; + } + if (this.session !== session || this.state.appState.sessionId !== session.id) return; + + const visible = selectMcpStartupStatusRows(servers); + const visibleNames = new Set(visible.map((server) => server.name)); + for (const server of visible) { + if (this.state.renderedMcpServerStatusKeys.has(server.name)) continue; + this.renderMcpServerStatus(server); + } + + const hidden: McpServerStatusSnapshot[] = []; + for (const server of servers) { + if (visibleNames.has(server.name)) continue; + if (this.state.renderedMcpServerStatusKeys.has(server.name)) continue; + this.state.renderedMcpServerStatusKeys.set(server.name, mcpServerStatusKey(server)); + hidden.push(server); + } + if (hidden.length > 0) { + this.showStatus( + formatMcpStartupStatusSummary(hidden, visible.length), + this.state.theme.colors.textMuted, + ); + } + } + + // Routes an SDK event to the matching TUI state transition. + private handleEvent(event: Event, sendQueued: (item: QueuedMessage) => void): void { + if (this.routeSubagentEvent(event)) { + return; + } + + if ('turnId' in event && event.turnId !== undefined) { + this.state.currentTurnId = String(event.turnId); + } + + switch (event.type) { + case 'turn.started': + this.handleTurnBegin(event); + break; + case 'turn.ended': + this.handleTurnEnd(event, sendQueued); + break; + case 'turn.step.started': + this.handleStepBegin(event); + break; + case 'turn.step.interrupted': + this.handleStepInterrupted(event); + break; + case 'turn.step.completed': + this.handleStepCompleted(event); + break; + case 'turn.step.retrying': + break; + case 'tool.progress': + this.handleToolProgress(event); + break; + case 'assistant.delta': + this.handleAssistantDelta(event); + break; + case 'hook.result': + this.handleHookResult(event); + break; + case 'thinking.delta': + this.handleThinkingDelta(event); + break; + case 'tool.call.started': + this.handleToolCall(event); + break; + case 'tool.call.delta': + this.handleToolCallDelta(event); + break; + case 'tool.result': + this.handleToolResult(event); + break; + case 'agent.status.updated': + this.handleStatusUpdate(event); + break; + case 'session.meta.updated': + this.handleSessionMetaChanged(event); + break; + case 'skill.activated': + this.handleSkillActivated(event); + break; + case 'error': + this.handleSessionError(event); + break; + case 'compaction.started': + this.handleCompactionBegin(event); + break; + case 'compaction.completed': + this.handleCompactionEnd(event, sendQueued); + break; + case 'compaction.blocked': + break; + case 'compaction.cancelled': + this.handleCompactionCancel(event, sendQueued); + break; + case 'subagent.spawned': + this.handleSubagentSpawned(event); + break; + case 'subagent.completed': + this.handleSubagentCompleted(event); + break; + case 'subagent.failed': + this.handleSubagentFailed(event); + break; + case 'background.task.started': + case 'background.task.updated': + case 'background.task.terminated': + this.handleBackgroundTaskEvent(event); + break; + case 'mcp.server.status': + this.renderMcpServerStatus(event.server); + break; + case 'tool.list.updated': + break; + default: + break; + } + } + + // Routes child-agent events into their parent tool-call component. + private routeSubagentEvent(event: Event): boolean { + const subagentId = event.agentId; + if (subagentId === MAIN_AGENT_ID) return false; + + const parentToolCallId = this.state.subagentParentToolCallIds.get(subagentId); + if (parentToolCallId === undefined || parentToolCallId.length === 0) return true; + const sourceName = this.state.subagentNames.get(subagentId); + const toolCall = this.state.pendingToolComponents.get(parentToolCallId); + if (toolCall === undefined) return true; + toolCall.setSubagentMeta(subagentId, sourceName); + + switch (event.type) { + case 'hook.result': + toolCall.appendSubagentText(formatHookResultPlain(event), 'text'); + return true; + case 'assistant.delta': + toolCall.appendSubagentText(event.delta, 'text'); + return true; + case 'thinking.delta': + toolCall.appendSubagentText(event.delta, 'thinking'); + return true; + case 'tool.call.started': + toolCall.appendSubToolCall({ + id: `${subagentId}:${event.toolCallId}`, + name: event.name, + args: argsRecord(event.args), + }); + return true; + case 'tool.call.delta': + toolCall.appendSubToolCallDelta({ + id: `${subagentId}:${event.toolCallId}`, + name: event.name, + argumentsPart: event.argumentsPart ?? null, + }); + return true; + case 'tool.result': + toolCall.finishSubToolCall({ + tool_call_id: `${subagentId}:${event.toolCallId}`, + output: serializeToolResultOutput(event.output), + is_error: event.isError, + }); + return true; + case 'agent.status.updated': + case 'background.task.started': + case 'background.task.updated': + case 'background.task.terminated': + case 'compaction.blocked': + case 'compaction.cancelled': + case 'compaction.completed': + case 'compaction.started': + case 'error': + case 'session.meta.updated': + case 'skill.activated': + case 'subagent.completed': + case 'subagent.failed': + case 'subagent.spawned': + case 'tool.progress': + case 'tool.list.updated': + case 'mcp.server.status': + case 'turn.ended': + case 'turn.started': + case 'turn.step.completed': + case 'turn.step.interrupted': + case 'turn.step.retrying': + case 'turn.step.started': + return true; + default: + return true; + } + } + + // Initializes turn-scoped buffers when the SDK starts a turn. + private handleTurnBegin(_event: TurnStartedEvent): void { + void _event; + this.resetLiveToolUiState(); + this.state.currentStep = 0; + this.patchLivePane({ + mode: 'waiting', + pendingApproval: null, + pendingQuestion: null, + }); + this.setAppState({ + isStreaming: true, + streamingPhase: 'waiting', + streamingStartTime: Date.now(), + }); + } + + // Finalizes turn-scoped state when the SDK completes a turn. + private handleTurnEnd(_event: TurnEndedEvent, sendQueued: (item: QueuedMessage) => void): void { + void _event; + const todos = this.state.todoPanel.getTodos(); + if (todos.length > 0 && todos.every((t) => t.status === 'done')) { + this.setTodoList([]); + } + this.resetLiveToolUiState(); + this.finalizeTurn(sendQueued); + } + + // Resets live render state for a new turn step. + private handleStepBegin(event: TurnStepStartedEvent): void { + this.state.currentStep = event.step; + this.resetLiveToolUiState(); + this.finalizeLiveTextBuffers('waiting'); + this.patchLivePane({ + mode: 'waiting', + pendingApproval: null, + pendingQuestion: null, + }); + this.setAppState({ + streamingPhase: 'waiting', + streamingStartTime: Date.now(), + }); + } + + // Surfaces step-level outcomes the user needs to act on. The common + // case (finishReason === 'tool_use' or 'end_turn') is silent — those + // already render via tool.call.started/tool.result and assistant.delta. + // The interesting case is max_tokens: the model started a tool_use but + // ran out of budget before finalizing it, so the partial tool call is + // still pinned in 'Preparing' state with no signal that anything went + // wrong. Flip those into a visible 'Truncated' state and append a + // notice pointing at the config knob. + private handleStepCompleted(event: TurnStepCompletedEvent): void { + if (event.finishReason !== 'max_tokens') return; + + // Scope the truncation marking to tool calls that belong to the + // step that just completed. Without this guard, stale entries from + // earlier retry attempts (or unrelated still-tracked calls) would + // get relabeled and counted, producing misleading "tool call was + // truncated" notices for the wrong step. + const eventTurnId = String(event.turnId); + let truncatedCount = 0; + for (const toolCall of this.state.activeToolCalls.values()) { + if (toolCall.result !== undefined) continue; + if (toolCall.streamingArguments === undefined) continue; + if (toolCall.turnId !== eventTurnId) continue; + if (toolCall.step !== event.step) continue; + toolCall.truncated = true; + const component = this.state.pendingToolComponents.get(toolCall.id); + if (component !== undefined) { + component.updateToolCall(toolCall); + } + truncatedCount += 1; + } + this.state.streamingToolCallArguments.clear(); + + const title = + truncatedCount > 0 + ? 'Model hit max_tokens — tool call was truncated before it could run.' + : 'Model hit max_tokens — no tool call was emitted.'; + // The `max_output_size` knob is only wired through to provider + // requests for the Anthropic provider (see toKosongProviderConfig). + // For OpenAI / Kimi / Google sessions the advice would be a + // dead end, so skip the second line on those providers. + const detail = this.isAnthropicSessionActive() + ? 'If this limit is wrong for your model, set `max_output_size` on the model alias in your kimi-code config.' + : undefined; + this.showNotice(title, detail); + } + + private isAnthropicSessionActive(): boolean { + const providerKey = this.state.appState.availableModels[this.state.appState.model]?.provider; + if (providerKey === undefined) return false; + return this.state.appState.availableProviders[providerKey]?.type === 'anthropic'; + } + + // Renders user-facing status for an interrupted turn step. + private handleStepInterrupted(event: TurnStepInterruptedEvent): void { + this.resetLiveToolUiState(); + this.finalizeLiveTextBuffers('idle'); + const reason = event.reason; + if (reason === 'error') return; + if (reason === 'aborted' || reason === undefined || reason === '') { + this.showStatus('Interrupted by user', this.state.theme.colors.error); + return; + } + this.showError( + reason === 'max_steps' + ? 'reached per-turn step limit (max_steps)' + : `step interrupted (${reason})`, + ); + } + + // Appends a thinking delta to the live thinking block. + private handleThinkingDelta(event: ThinkingDeltaEvent): void { + this.state.thinkingDraft += event.delta; + this.onThinkingUpdate(this.state.thinkingDraft); + this.patchLivePane({ mode: 'idle' }); + this.setAppState({ streamingPhase: 'thinking' }); + } + + // Appends an assistant text delta to the live assistant block. + private handleAssistantDelta(event: AssistantDeltaEvent): void { + if (this.state.thinkingDraft.length > 0) { + this.flushThinkingToTranscript('idle'); + } + + if (!this.state.assistantStreamActive) { + this.state.assistantStreamActive = true; + this.onStreamingTextStart(); + } + + this.state.assistantDraft += event.delta; + this.onStreamingTextUpdate(this.state.assistantDraft); + + this.patchLivePane({ + mode: 'idle', + pendingApproval: null, + pendingQuestion: null, + }); + this.setAppState({ + streamingPhase: 'composing', + streamingStartTime: Date.now(), + }); + } + + private handleHookResult(event: HookResultEvent): void { + if (this.state.thinkingDraft.length > 0) { + this.flushThinkingToTranscript('idle'); + } + this.finalizeAssistantStream(); + this.appendTranscriptEntry({ + id: nextTranscriptId(), + kind: 'assistant', + turnId: String(event.turnId), + renderMode: 'markdown', + content: formatHookResultMarkdown(event), + }); + this.patchLivePane({ + mode: 'idle', + pendingApproval: null, + pendingQuestion: null, + }); + } + + // Starts or updates a rendered tool call from a tool-call start event. + private handleToolCall(event: ToolCallStartedEvent): void { + const toolCall: ToolCallBlockData = { + id: event.toolCallId, + name: event.name, + args: argsRecord(event.args), + description: event.description, + display: event.display, + step: this.state.currentStep, + turnId: this.state.currentTurnId, + }; + const existing = this.state.activeToolCalls.get(event.toolCallId); + this.state.activeToolCalls.set(event.toolCallId, toolCall); + this.state.streamingToolCallArguments.delete(event.toolCallId); + const existingComponent = this.state.pendingToolComponents.get(event.toolCallId); + if (existingComponent !== undefined) { + existingComponent.updateToolCall(toolCall); + } else if (existing === undefined) { + this.finalizeLiveTextBuffers('tool'); + if (event.name !== 'Agent') { + this.onToolCallStart(toolCall); + } + } + this.patchLivePane({ + mode: 'tool', + pendingApproval: null, + pendingQuestion: null, + }); + } + + // Accumulates streaming tool-call arguments and updates the rendered call. + private handleToolCallDelta(event: ToolCallDeltaEvent): void { + if (event.toolCallId.length === 0) return; + const id = event.toolCallId; + const existing = this.state.streamingToolCallArguments.get(id); + const argumentsText = `${existing?.argumentsText ?? ''}${event.argumentsPart ?? ''}`; + const name = event.name ?? existing?.name ?? this.state.activeToolCalls.get(id)?.name ?? 'Tool'; + const startedAtMs = existing?.startedAtMs ?? Date.now(); + this.state.streamingToolCallArguments.set(id, { name, argumentsText, startedAtMs }); + + const toolCall: ToolCallBlockData = { + id, + name, + args: parseStreamingArgs(argumentsText), + streamingArguments: argumentsText, + streamingStartedAtMs: startedAtMs, + step: this.state.currentStep, + turnId: this.state.currentTurnId, + }; + this.state.activeToolCalls.set(id, toolCall); + + if (this.state.thinkingDraft.length > 0 || this.state.assistantStreamActive) { + this.finalizeLiveTextBuffers('tool'); + } + + const existingComponent = this.state.pendingToolComponents.get(id); + if (existingComponent !== undefined) { + existingComponent.updateToolCall(toolCall); + } else if (name !== 'Agent') { + this.onToolCallStart(toolCall); + } + + this.patchLivePane({ + mode: 'tool', + pendingApproval: null, + pendingQuestion: null, + }); + this.setAppState({ + streamingPhase: 'composing', + streamingStartTime: Date.now(), + }); + } + + // Streams a `{kind:'status'}` progress text into the live tool box so + // long-blocking tools (e.g. the MCP synthetic `authenticate` tool whose + // 15-minute browser wait would otherwise show only a spinner) can surface + // their authorization URL. Non-status update kinds stay out of the terminal + // transcript because only status text needs persistent display. + private handleToolProgress(event: ToolProgressEvent): void { + if (event.update.kind !== 'status') return; + const text = event.update.text; + if (text === undefined || text.length === 0) return; + const tc = this.state.pendingToolComponents.get(event.toolCallId); + if (tc === undefined) return; + tc.appendProgress(text); + } + + // Completes a tool call and applies any tool-specific UI side effects. + private handleToolResult(event: ToolResultEvent): void { + const matchedCall = this.state.activeToolCalls.get(event.toolCallId); + const resultData: ToolResultBlockData = { + tool_call_id: event.toolCallId, + output: serializeToolResultOutput(event.output), + is_error: event.isError, + synthetic: event.synthetic, + }; + if (matchedCall !== undefined) { + this.onToolCallEnd(event.toolCallId, resultData); + if (matchedCall.name === 'TodoList' && !event.isError) { + const rawTodos = (matchedCall.args as { todos?: unknown }).todos; + if (Array.isArray(rawTodos)) { + const sanitized = rawTodos + .filter((todo): todo is { title: string; status: 'pending' | 'in_progress' | 'done' } => + isTodoItemShape(todo), + ) + .map((t) => ({ title: t.title, status: t.status })); + this.setTodoList(sanitized); + } + } + } + this.state.activeToolCalls.delete(event.toolCallId); + this.state.streamingToolCallArguments.delete(event.toolCallId); + this.patchLivePane({ mode: 'waiting' }); + } + + // Applies agent status updates to app state. + private handleStatusUpdate(event: AgentStatusUpdatedEvent): void { + const patch: Partial<AppState> = {}; + if (event.contextUsage !== undefined) patch.contextUsage = event.contextUsage; + if (event.contextTokens !== undefined) patch.contextTokens = event.contextTokens; + if (event.maxContextTokens !== undefined) patch.maxContextTokens = event.maxContextTokens; + if (event.planMode !== undefined) patch.planMode = event.planMode; + if (event.permission !== undefined) { + patch.permissionMode = event.permission; + patch.yolo = event.permission === 'yolo'; + } + if (event.model !== undefined) patch.model = event.model; + if (Object.keys(patch).length > 0) this.setAppState(patch); + } + + // Applies session metadata changes to the UI and process title. + private handleSessionMetaChanged(event: SessionMetaUpdatedEvent): void { + const title = event.title ?? stringValue(event.patch?.['title']); + if (title !== undefined) { + this.setAppState({ sessionTitle: title }); + setProcessTitle(title, this.state.appState.sessionId); + } + } + + // Finalizes live buffers and renders a session error. + private handleSessionError(event: ErrorEvent): void { + this.resetLiveToolUiState(); + this.finalizeLiveTextBuffers('idle'); + if (event.code === OAUTH_LOGIN_REQUIRED_CODE) { + this.showError(OAUTH_LOGIN_REQUIRED_STARTUP_NOTICE); + return; + } + this.showError(`[${event.code}] ${event.message}`); + const sessionId = this.state.appState.sessionId; + if (sessionId.length > 0) { + this.showStatus(errorReportHintLine(sessionId)); + } + } + + private renderMcpServerStatus(server: McpServerStatusSnapshot): void { + const key = mcpServerStatusKey(server); + if (this.state.renderedMcpServerStatusKeys.get(server.name) === key) return; + this.state.renderedMcpServerStatusKeys.set(server.name, key); + + const colors = this.state.theme.colors; + switch (server.status) { + case 'connected': { + const toolStr = `${server.toolCount} tool${server.toolCount === 1 ? '' : 's'}`; + const message = `MCP server "${server.name}" connected · ${toolStr} (${server.transport})`; + this.finalizeMcpServerStatusRow(server.name, message, colors.success); + return; + } + case 'failed': { + const message = `MCP server "${server.name}" failed${server.error !== undefined ? `: ${server.error}` : ''}`; + this.finalizeMcpServerStatusRow(server.name, message, colors.error); + return; + } + case 'needs-auth': { + const message = `MCP server "${server.name}" needs OAuth — run /mcp-config login ${server.name}`; + this.finalizeMcpServerStatusRow(server.name, message, colors.warning); + return; + } + case 'disabled': + this.finalizeMcpServerStatusRow( + server.name, + `MCP server "${server.name}" disabled`, + colors.textMuted, + ); + return; + case 'pending': + this.showMcpServerStatusSpinner(server.name); + return; + } + } + + private showMcpServerStatusSpinner(name: string): void { + const label = `MCP server "${name}" connecting…`; + const existing = this.state.mcpServerStatusSpinners.get(name); + if (existing !== undefined) { + existing.setLabel(label); + return; + } + const tint = (s: string): string => chalk.hex(this.state.theme.colors.textMuted)(s); + const spinner = new MoonLoader(this.state.ui, 'braille', tint, label); + this.state.transcriptContainer.addChild(spinner); + this.state.mcpServerStatusSpinners.set(name, spinner); + this.state.ui.requestRender(); + } + + private finalizeMcpServerStatusRow(name: string, message: string, color: string): void { + const spinner = this.state.mcpServerStatusSpinners.get(name); + if (spinner === undefined) { + this.showStatus(message, color); + return; + } + spinner.stop(); + const status = new StatusMessageComponent(message, this.state.theme.colors, color); + const children = this.state.transcriptContainer.children; + const idx = children.indexOf(spinner); + if (idx >= 0) { + children[idx] = status; + this.state.transcriptContainer.invalidate(); + } else { + this.state.transcriptContainer.addChild(status); + } + this.state.mcpServerStatusSpinners.delete(name); + this.state.ui.requestRender(); + } + + private stopAllMcpServerStatusSpinners(): void { + for (const spinner of this.state.mcpServerStatusSpinners.values()) { + spinner.stop(); + } + this.state.mcpServerStatusSpinners.clear(); + } + + // Adds a skill activation entry to the transcript once. + private handleSkillActivated(event: SkillActivatedEvent): void { + if (this.state.renderedSkillActivationIds.has(event.activationId)) return; + this.state.renderedSkillActivationIds.add(event.activationId); + this.appendTranscriptEntry({ + id: nextTranscriptId(), + kind: 'skill_activation', + turnId: undefined, + renderMode: 'plain', + content: `Activated skill: ${event.skillName}`, + skillActivationId: event.activationId, + skillName: event.skillName, + skillArgs: event.skillArgs, + }); + } + + // Starts the compaction UI block and marks the app as compacting. + private handleCompactionBegin(event: CompactionStartedEvent): void { + this.finalizeLiveTextBuffers('waiting'); + this.setAppState({ + isCompacting: true, + streamingPhase: 'waiting', + streamingStartTime: Date.now(), + }); + this.beginCompaction(event.instruction); + } + + // Finishes compaction and resumes queued work when possible. + private handleCompactionEnd( + event: CompactionCompletedEvent, + sendQueued: (item: QueuedMessage) => void, + ): void { + this.endCompaction(event.result.tokensBefore, event.result.tokensAfter); + this.finishCompaction(sendQueued); + } + + private handleCompactionCancel( + _event: CompactionCancelledEvent, + sendQueued: (item: QueuedMessage) => void, + ): void { + this.cancelCompactionBlock(); + this.finishCompaction(sendQueued); + } + + private finishCompaction(sendQueued: (item: QueuedMessage) => void): void { + if (!this.state.appState.isStreaming) { + this.setAppState({ + isCompacting: false, + streamingPhase: 'idle', + }); + this.resetLivePane(); + if (this.state.queuedMessages.length > 0) { + const [next, ...rest] = this.state.queuedMessages; + this.state.queuedMessages = rest; + if (next !== undefined) { + setTimeout(() => { + sendQueued(next); + }, 0); + } + } + } else { + this.setAppState({ isCompacting: false }); + } + } + + // Registers a spawned subagent and renders foreground or background status. + private handleSubagentSpawned(event: SubagentSpawnedEvent): void { + this.state.subagentParentToolCallIds.set(event.subagentId, event.parentToolCallId); + this.state.subagentNames.set(event.subagentId, event.subagentName); + + if (event.runInBackground) { + const meta = this.buildBackgroundAgentMetadata(event); + this.state.backgroundAgentMetadata.set(event.subagentId, meta); + this.state.backgroundAgents.add(event.subagentId); + this.appendBackgroundAgentEntry('started', meta); + this.syncBackgroundAgentBadge(); + return; + } + + let tc = this.state.pendingToolComponents.get(event.parentToolCallId); + if (tc === undefined) { + const toolCall = this.state.activeToolCalls.get(event.parentToolCallId); + if (toolCall !== undefined) { + this.onToolCallStart(toolCall); + tc = this.state.pendingToolComponents.get(event.parentToolCallId); + } + } + tc ??= this.createStandaloneSubagentToolCall(event); + if (tc === undefined) return; + tc.onSubagentSpawned({ + agentId: event.subagentId, + agentName: event.subagentName, + runInBackground: event.runInBackground, + }); + } + + // Completes a subagent in its parent tool call or background transcript entry. + private handleSubagentCompleted(event: SubagentCompletedEvent): void { + const backgroundMeta = this.state.backgroundAgentMetadata.get(event.subagentId); + if (this.state.backgroundAgents.delete(event.subagentId)) { + this.syncBackgroundAgentBadge(); + } + if (backgroundMeta !== undefined) { + this.state.backgroundAgentMetadata.delete(event.subagentId); + // Dedupe: if the BPM `background.task.terminated` for the + // matching agent task already pushed a terminal card, skip. + // Otherwise mark the subagent id so a later BPM event skips. + const taskId = this.findAgentTaskId(event.subagentId); + if (taskId !== undefined && this.state.backgroundTaskTranscriptedTerminal.has(taskId)) { + return; + } + if (taskId !== undefined) { + this.state.backgroundTaskTranscriptedTerminal.add(taskId); + } + const extras = + event.resultSummary === undefined ? undefined : { resultSummary: event.resultSummary }; + this.appendBackgroundAgentEntry('completed', backgroundMeta, extras); + return; + } + const tc = this.state.pendingToolComponents.get(event.parentToolCallId); + if (tc === undefined) return; + tc.onSubagentCompleted({ + usage: event.usage, + resultSummary: event.resultSummary, + }); + if (!this.state.activeToolCalls.has(event.parentToolCallId)) { + this.state.pendingToolComponents.delete(event.parentToolCallId); + } + } + + // Marks a subagent failure in its parent tool call or background transcript entry. + private handleSubagentFailed(event: SubagentFailedEvent): void { + const backgroundMeta = this.state.backgroundAgentMetadata.get(event.subagentId); + if (this.state.backgroundAgents.delete(event.subagentId)) { + this.syncBackgroundAgentBadge(); + } + if (backgroundMeta !== undefined) { + this.state.backgroundAgentMetadata.delete(event.subagentId); + const taskId = this.findAgentTaskId(event.subagentId); + if (taskId !== undefined && this.state.backgroundTaskTranscriptedTerminal.has(taskId)) { + return; + } + if (taskId !== undefined) { + this.state.backgroundTaskTranscriptedTerminal.add(taskId); + } + this.appendBackgroundAgentEntry('failed', backgroundMeta, { error: event.error }); + return; + } + const tc = this.state.pendingToolComponents.get(event.parentToolCallId); + if (tc === undefined) return; + tc.onSubagentFailed({ error: event.error }); + if (!this.state.activeToolCalls.has(event.parentToolCallId)) { + this.state.pendingToolComponents.delete(event.parentToolCallId); + } + } + + // Mounts subagents launched by session-level commands that do not originate + // from a model-issued Agent tool call. + private createStandaloneSubagentToolCall(event: SubagentSpawnedEvent): ToolCallComponent | undefined { + const description = event.description ?? `Run ${event.subagentName} agent`; + const toolCall: ToolCallBlockData = { + id: event.parentToolCallId, + name: 'Agent', + args: { + description, + subagent_type: event.subagentName, + }, + description, + step: this.state.currentStep, + turnId: this.state.currentTurnId, + }; + this.onToolCallStart(toolCall); + return this.state.pendingToolComponents.get(event.parentToolCallId); + } + + /** + * Locate the BPM `agent-*` task id whose `description` matches the + * spawned subagent's recorded description. Used only for dedupe + * between the BPM and subagent flows — best-effort: if there is no + * unique match (e.g. multiple agent tasks with the same description) + * the caller treats the dedupe as a miss, which is safe. + */ + private findAgentTaskId(subagentId: string): string | undefined { + const meta = this.state.backgroundAgentMetadata.get(subagentId); + const description = meta?.description ?? meta?.agentName; + if (description === undefined) return undefined; + let match: string | undefined; + for (const info of this.state.backgroundTasks.values()) { + if (!info.taskId.startsWith('agent-')) continue; + if (info.description !== description) continue; + if (match !== undefined) return undefined; // ambiguous + match = info.taskId; + } + return match; + } + + // Builds transcript metadata for a background subagent. + private buildBackgroundAgentMetadata(event: SubagentSpawnedEvent): BackgroundAgentMetadata { + const parent = this.state.activeToolCalls.get(event.parentToolCallId); + const description = parent?.args['description'] ?? event.description; + return { + agentId: event.subagentId, + parentToolCallId: event.parentToolCallId, + agentName: event.subagentName, + description: typeof description === 'string' ? description : undefined, + }; + } + + // Appends a background-agent status row to the transcript. + private appendBackgroundAgentEntry( + phase: 'started' | 'completed' | 'failed', + meta: BackgroundAgentMetadata, + extras: { resultSummary?: string; error?: string } | undefined = undefined, + ): void { + const status = formatBackgroundAgentTranscript(phase, meta, extras); + const entry: TranscriptEntry = { + id: nextTranscriptId(), + kind: 'status', + turnId: this.state.currentTurnId, + renderMode: 'plain', + content: status.headline, + detail: status.detail, + backgroundAgentStatus: status, + }; + this.appendTranscriptEntry(entry); + } + + // Updates the footer badge for active background agents. + private syncBackgroundAgentBadge(): void { + this.syncBackgroundTaskBadge(); + } + + // ========================================================================= + // Background task lifecycle (BPM-derived, covers both bash + agent tasks) + // ========================================================================= + + private handleBackgroundTaskEvent( + event: BackgroundTaskStartedEvent | BackgroundTaskUpdatedEvent | BackgroundTaskTerminatedEvent, + ): void { + const { info } = event; + const previous = this.state.backgroundTasks.get(info.taskId); + this.state.backgroundTasks.set(info.taskId, info); + + // If the user is currently viewing this task's output, nudge a + // refresh immediately so they see new content without waiting for + // the 1s poll. Same dedupe-by-output-equality applies inside. + const viewer = this.state.tasksBrowser?.viewer; + if (viewer !== undefined && viewer.taskId === info.taskId) { + void this.refreshTaskOutputViewer({ silent: true }); + } + + const isTerminal = + info.status === 'completed' || + info.status === 'failed' || + info.status === 'killed' || + info.status === 'lost'; + + if (event.type === 'background.task.started') { + // For agent-* tasks, the legacy subagent.spawned flow already + // pushed a 'started' transcript card; skip to avoid duplicates. + if (info.taskId.startsWith('agent-')) { + this.syncBackgroundTaskBadge(); + this.repaintTasksBrowser(); + return; + } + this.appendBackgroundTaskEntry(info); + this.syncBackgroundTaskBadge(); + this.repaintTasksBrowser(); + return; + } + + if (event.type === 'background.task.terminated' && isTerminal) { + if (!this.state.backgroundTaskTranscriptedTerminal.has(info.taskId)) { + // For agent-* tasks, the older subagent.completed/failed flow + // may also produce a terminal card; whoever wins records the + // dedupe marker first. See handleSubagentCompleted/Failed. + if (info.taskId.startsWith('bash-')) { + this.appendBackgroundTaskEntry(info); + } + this.state.backgroundTaskTranscriptedTerminal.add(info.taskId); + } + this.syncBackgroundTaskBadge(); + this.repaintTasksBrowser(); + return; + } + + // updated: status flipped between running and awaiting_approval. + // No transcript card — just sync the badge if the active count + // changed (awaiting_approval still counts as active). + if (previous?.status !== info.status) { + this.syncBackgroundTaskBadge(); + } + this.repaintTasksBrowser(); + } + + private appendBackgroundTaskEntry(info: BackgroundTaskInfo): void { + const status = formatBackgroundTaskTranscript(info); + const entry: TranscriptEntry = { + id: nextTranscriptId(), + kind: 'status', + turnId: this.state.currentTurnId, + renderMode: 'plain', + content: status.headline, + detail: status.detail, + backgroundAgentStatus: status, + }; + this.appendTranscriptEntry(entry); + } + + // Footer counts are BPM-derived: every task that is not terminal, + // split by id prefix so bash and agent badges render independently. + // awaiting_approval still counts as active; lost/killed/completed/ + // failed do not. + private syncBackgroundTaskBadge(): void { + let bashTasks = 0; + let agentTasks = 0; + for (const info of this.state.backgroundTasks.values()) { + if ( + info.status === 'completed' || + info.status === 'failed' || + info.status === 'killed' || + info.status === 'lost' + ) { + continue; + } + if (info.taskId.startsWith('agent-')) { + agentTasks += 1; + } else { + bashTasks += 1; + } + } + this.state.footer.setBackgroundCounts({ bashTasks, agentTasks }); + this.state.ui.requestRender(); + } + + // ========================================================================= + // Live Render Hooks + // ========================================================================= + + // Creates the live assistant transcript component. + private onStreamingTextStart(): void { + this.state.pendingAgentGroup = null; + this.state.pendingReadGroup = null; + const entry = { + id: nextTranscriptId(), + kind: 'assistant' as const, + turnId: this.state.currentTurnId, + renderMode: 'markdown' as const, + content: '', + }; + this.state.streamingComponent = new AssistantMessageComponent( + this.state.theme.markdownTheme, + this.state.theme.colors, + ); + this.state.streamingTranscriptEntry = entry; + this.state.transcriptEntries.push(entry); + this.state.transcriptContainer.addChild(this.state.streamingComponent); + this.state.ui.requestRender(); + } + + // Updates the live assistant transcript component. + private onStreamingTextUpdate(fullText: string): void { + if (this.state.streamingTranscriptEntry !== undefined) { + this.state.streamingTranscriptEntry.content = fullText; + } + if (this.state.streamingComponent) { + this.state.streamingComponent.updateContent(fullText); + this.state.ui.requestRender(); + } + } + + // Clears live assistant component references after streaming ends. + private onStreamingTextEnd(): void { + this.state.streamingComponent = undefined; + this.state.streamingTranscriptEntry = undefined; + } + + // Creates or updates the live thinking transcript component. + private onThinkingUpdate(fullText: string): void { + if (this.state.activeThinkingComponent === undefined) { + this.state.pendingAgentGroup = null; + this.state.pendingReadGroup = null; + this.state.activeThinkingComponent = new ThinkingComponent( + fullText, + this.state.theme.colors, + true, + 'live', + this.state.ui, + ); + if (this.state.toolOutputExpanded) this.state.activeThinkingComponent.setExpanded(true); + this.state.transcriptContainer.addChild(this.state.activeThinkingComponent); + } else { + this.state.activeThinkingComponent.setText(fullText); + } + this.state.ui.requestRender(); + } + + // Finalizes the live thinking transcript component. + private onThinkingEnd(): void { + if (this.state.activeThinkingComponent === undefined) return; + this.state.activeThinkingComponent.finalize(); + this.state.activeThinkingComponent = undefined; + this.state.ui.requestRender(); + } + + // Creates and mounts a live tool-call component. + private onToolCallStart(toolCall: ToolCallBlockData): void { + if (toolCall.name === 'AskUserQuestion') return; + + const tc = new ToolCallComponent( + toolCall, + undefined, + this.state.theme.colors, + this.state.ui, + this.state.theme.markdownTheme, + this.state.appState.workDir, + ); + if (this.state.toolOutputExpanded) tc.setExpanded(true); + if (this.state.planExpanded) tc.setPlanExpanded(true); + this.state.pendingToolComponents.set(toolCall.id, tc); + + if (toolCall.name !== 'Agent') this.state.pendingAgentGroup = null; + if (toolCall.name !== 'Read') this.state.pendingReadGroup = null; + + let handled = this.tryAttachAgentToolCall(toolCall, tc); + if (!handled) handled = this.tryAttachReadToolCall(toolCall, tc); + if (!handled) { + this.state.transcriptContainer.addChild(tc); + this.state.ui.requestRender(); + } + + if (toolCall.name === 'ExitPlanMode' && typeof toolCall.args['plan'] !== 'string') { + const session = this.requireSession(); + void (async () => { + try { + const plan = await session.getPlan(); + tc.setPlanInfo(plan === null ? {} : { plan: plan.content, path: plan.path }); + } catch { + tc.setPlanInfo({}); + } + })(); + } + } + + // Applies a tool result to a live or completed tool-call component. + private onToolCallEnd(toolCallId: string, result: ToolResultBlockData): void { + const matchedCall = this.state.activeToolCalls.get(toolCallId); + const tc = this.state.pendingToolComponents.get(toolCallId); + if (tc) { + tc.setResult(result); + this.state.pendingToolComponents.delete(toolCallId); + this.state.ui.requestRender(); + return; + } + + if (matchedCall?.name === 'AskUserQuestion') { + const completed = new ToolCallComponent( + matchedCall, + result, + this.state.theme.colors, + this.state.ui, + this.state.theme.markdownTheme, + this.state.appState.workDir, + ); + if (this.state.toolOutputExpanded) completed.setExpanded(true); + if (this.state.planExpanded) completed.setPlanExpanded(true); + this.state.transcriptContainer.addChild(completed); + this.state.ui.requestRender(); + } + } + + // Replaces the visible todo list panel. + private setTodoList(todos: readonly TodoItem[]): void { + this.state.todoPanel.setTodos(todos); + this.state.todoPanelContainer.clear(); + if (!this.state.todoPanel.isEmpty()) { + this.state.todoPanelContainer.addChild(this.state.todoPanel); + } + this.state.ui.requestRender(); + } + + // Renders a compaction block in the transcript. + private beginCompaction(instruction?: string): void { + if (this.state.activeCompactionBlock !== undefined) { + this.state.activeCompactionBlock.markDone(); + this.state.activeCompactionBlock = undefined; + } + const block = new CompactionComponent(this.state.theme.colors, this.state.ui, instruction); + this.state.activeCompactionBlock = block; + this.state.transcriptContainer.addChild(block); + this.state.ui.requestRender(); + } + + // Marks the active compaction block complete. + private endCompaction(tokensBefore?: number, tokensAfter?: number): void { + const block = this.state.activeCompactionBlock; + if (block === undefined) return; + block.markDone(tokensBefore, tokensAfter); + this.state.activeCompactionBlock = undefined; + this.state.ui.requestRender(); + } + + private cancelCompactionBlock(): void { + const block = this.state.activeCompactionBlock; + if (block === undefined) return; + block.markCanceled(); + this.state.activeCompactionBlock = undefined; + this.state.ui.requestRender(); + } + + // Groups Agent tool calls that belong to the same turn step. + private tryAttachAgentToolCall(toolCall: ToolCallBlockData, tc: ToolCallComponent): boolean { + if (toolCall.name !== 'Agent') { + this.state.pendingAgentGroup = null; + return false; + } + + const step = toolCall.step ?? this.state.currentStep; + const turnId = toolCall.turnId ?? this.state.currentTurnId; + const pending = this.state.pendingAgentGroup; + + if (pending !== null && (pending.step !== step || pending.turnId !== turnId)) { + this.state.pendingAgentGroup = null; + } + + const cur = this.state.pendingAgentGroup; + if (cur === null) { + this.state.pendingAgentGroup = { step, turnId, solo: tc }; + this.state.transcriptContainer.addChild(tc); + this.state.ui.requestRender(); + return true; + } + + if (cur.group !== undefined) { + cur.group.attach(toolCall.id, tc); + return true; + } + + const solo = cur.solo; + if (solo === undefined) { + this.state.pendingAgentGroup = { step, turnId, solo: tc }; + this.state.transcriptContainer.addChild(tc); + this.state.ui.requestRender(); + return true; + } + const group = this.upgradeSoloAgentToGroup(solo); + group.attach(toolCall.id, tc); + this.state.pendingAgentGroup = { step, turnId, group }; + this.state.ui.requestRender(); + return true; + } + + // Replaces a single Agent tool call with an Agent group component. + private upgradeSoloAgentToGroup(solo: ToolCallComponent): AgentGroupComponent { + const group = new AgentGroupComponent(this.state.theme.colors, this.state.ui); + const children = this.state.transcriptContainer.children; + const idx = children.indexOf(solo); + if (idx >= 0) { + children[idx] = group; + this.state.transcriptContainer.invalidate(); + } else { + this.state.transcriptContainer.addChild(group); + } + group.attach(solo.toolCallView.id, solo); + return group; + } + + // Groups Read tool calls that belong to the same turn step. + private tryAttachReadToolCall(toolCall: ToolCallBlockData, tc: ToolCallComponent): boolean { + if (toolCall.name !== 'Read') { + this.state.pendingReadGroup = null; + return false; + } + + const step = toolCall.step ?? this.state.currentStep; + const turnId = toolCall.turnId ?? this.state.currentTurnId; + const pending = this.state.pendingReadGroup; + + if (pending !== null && (pending.step !== step || pending.turnId !== turnId)) { + this.state.pendingReadGroup = null; + } + + const cur = this.state.pendingReadGroup; + if (cur === null) { + this.state.pendingReadGroup = { step, turnId, solo: tc }; + this.state.transcriptContainer.addChild(tc); + this.state.ui.requestRender(); + return true; + } + + if (cur.group !== undefined) { + cur.group.attach(toolCall.id, tc); + return true; + } + + const solo = cur.solo; + if (solo === undefined) { + this.state.pendingReadGroup = { step, turnId, solo: tc }; + this.state.transcriptContainer.addChild(tc); + this.state.ui.requestRender(); + return true; + } + const group = this.upgradeSoloReadToGroup(solo); + group.attach(toolCall.id, tc); + this.state.pendingReadGroup = { step, turnId, group }; + this.state.ui.requestRender(); + return true; + } + + // Replaces a single Read tool call with a Read group component. + private upgradeSoloReadToGroup(solo: ToolCallComponent): ReadGroupComponent { + const group = new ReadGroupComponent(this.state.theme.colors, this.state.ui); + const children = this.state.transcriptContainer.children; + const idx = children.indexOf(solo); + if (idx >= 0) { + children[idx] = group; + this.state.transcriptContainer.invalidate(); + } else { + this.state.transcriptContainer.addChild(group); + } + group.attach(solo.toolCallView.id, solo); + return group; + } + + // ========================================================================= + // Transcript Rendering + // ========================================================================= + + // Creates the pi-tui component that renders a transcript entry. + private createTranscriptComponent(entry: TranscriptEntry): Component | null { + if (entry.compactionData !== undefined) { + const data = entry.compactionData; + const block = new CompactionComponent( + this.state.theme.colors, + this.state.ui, + data.instruction, + ); + block.markDone(data.tokensBefore, data.tokensAfter); + return block; + } + + switch (entry.kind) { + case 'user': { + const images = entry.imageAttachmentIds + ?.map((id) => this.imageStore.get(id)) + .filter((a): a is ImageAttachment => a?.kind === 'image'); + return new UserMessageComponent(entry.content, this.state.theme.colors, images); + } + case 'skill_activation': + return new SkillActivationComponent( + entry.skillName ?? entry.content, + entry.skillArgs, + this.state.theme.colors, + ); + case 'assistant': { + const component = new AssistantMessageComponent( + this.state.theme.markdownTheme, + this.state.theme.colors, + ); + component.updateContent(entry.content); + return component; + } + case 'thinking': { + const thinking = new ThinkingComponent(entry.content, this.state.theme.colors, true); + if (this.state.toolOutputExpanded) thinking.setExpanded(true); + return thinking; + } + case 'tool_call': + if (entry.toolCallData) { + const tc = new ToolCallComponent( + entry.toolCallData, + entry.toolCallData.result, + this.state.theme.colors, + this.state.ui, + this.state.theme.markdownTheme, + this.state.appState.workDir, + ); + if (this.state.toolOutputExpanded) tc.setExpanded(true); + if (this.state.planExpanded) tc.setPlanExpanded(true); + return tc; + } + if (entry.backgroundAgentStatus !== undefined) { + return new BackgroundAgentStatusComponent( + entry.backgroundAgentStatus, + this.state.theme.colors, + ); + } + return entry.renderMode === 'notice' + ? new NoticeMessageComponent(entry.content, entry.detail, this.state.theme.colors) + : new StatusMessageComponent(entry.content, this.state.theme.colors, entry.color); + case 'status': + if (entry.backgroundAgentStatus !== undefined) { + return new BackgroundAgentStatusComponent( + entry.backgroundAgentStatus, + this.state.theme.colors, + ); + } + return entry.renderMode === 'notice' + ? new NoticeMessageComponent(entry.content, entry.detail, this.state.theme.colors) + : new StatusMessageComponent(entry.content, this.state.theme.colors, entry.color); + case 'welcome': + return null; + default: + return null; + } + } + + // Stores a transcript entry and mounts its component if renderable. + private appendTranscriptEntry(entry: TranscriptEntry): void { + this.state.transcriptEntries.push(entry); + const component = this.createTranscriptComponent(entry); + if (component) { + this.state.transcriptContainer.addChild(component); + this.state.ui.requestRender(); + } + } + + // Appends an approval-result entry to the transcript. + private appendApprovalTranscriptEntry(request: ApprovalRequest, response: ApprovalResponse): void { + const parts: string[] = []; + switch (response.decision) { + case 'approved': + parts.push(response.scope === 'session' ? 'Approved for session' : 'Approved'); + break; + case 'rejected': + parts.push('Rejected'); + break; + case 'cancelled': + parts.push('Cancelled'); + break; + } + parts.push(`: ${request.action}`); + if (response.feedback !== undefined && response.feedback.length > 0) { + parts.push(` — "${response.feedback}"`); + } + this.appendTranscriptEntry({ + id: nextTranscriptId(), + kind: 'status', + renderMode: 'notice', + content: parts.join(''), + }); + } + + // Adds the welcome component to the transcript. + private renderWelcome(): void { + const welcome = new WelcomeComponent(this.state.appState, this.state.theme.colors); + this.state.transcriptContainer.addChild(welcome); + } + + // Disposes the active compaction component if one is mounted. + private disposeActiveCompactionBlock(): void { + if (this.state.activeCompactionBlock !== undefined) { + this.state.activeCompactionBlock.dispose(); + this.state.activeCompactionBlock = undefined; + } + } + + // Disposes the active thinking component if one is mounted. + private disposeActiveThinkingComponent(): void { + if (this.state.activeThinkingComponent !== undefined) { + this.state.activeThinkingComponent.dispose(); + this.state.activeThinkingComponent = undefined; + } + } + + // Disposes and forgets all pending live tool-call components. + private disposeAndClearPendingToolComponents(): void { + for (const component of this.state.pendingToolComponents.values()) { + if (hasDispose(component)) component.dispose(); + } + this.state.pendingToolComponents.clear(); + } + + private clearTerminalInlineImages(): void { + if (getCapabilities().images !== 'kitty') return; + this.state.terminal.write(deleteAllKittyImages()); + } + + // Clears transcript-related state and redraws the welcome view. + private clearTranscriptAndRedraw(): void { + this.state.transcriptEntries = []; + this.disposeActiveCompactionBlock(); + this.resetLiveTextRuntime(); + this.resetLiveToolUiState(); + this.stopAllMcpServerStatusSpinners(); + this.state.transcriptContainer.clear(); + this.clearTerminalInlineImages(); + this.state.todoPanel.clear(); + this.state.todoPanelContainer.clear(); + this.imageStore.clear(); + this.renderWelcome(); + } + + // Appends a status message to the transcript. + private showStatus(message: string, color?: string): void { + this.state.transcriptContainer.addChild( + new StatusMessageComponent(message, this.state.theme.colors, color), + ); + this.state.ui.requestRender(); + } + + // Appends a notice message to the transcript. + private showNotice(title: string, detail?: string): void { + this.state.transcriptContainer.addChild( + new NoticeMessageComponent(title, detail, this.state.theme.colors), + ); + this.state.ui.requestRender(); + } + + // Appends an error status message to the transcript. + private showError(message: string): void { + this.showStatus(`Error: ${message}`, this.state.theme.colors.error); + } + + // Adds an animated login progress row to the transcript. + private showLoginProgressSpinner(label: string): LoginProgressSpinnerHandle { + const tint = (s: string): string => chalk.hex(this.state.theme.colors.primary)(s); + const spinner = new MoonLoader(this.state.ui, 'braille', tint, label); + this.state.transcriptContainer.addChild(new Spacer(1)); + this.state.transcriptContainer.addChild(spinner); + this.state.ui.requestRender(); + return { + stop: ({ ok, label: finalLabel }) => { + spinner.stop(); + const tone = ok ? this.state.theme.colors.success : this.state.theme.colors.error; + const symbol = ok ? '✓' : '✗'; + spinner.setText(chalk.hex(tone)(`${symbol} ${finalLabel}`)); + this.state.ui.requestRender(); + }, + }; + } + + // Opens the device-code URL and renders the login authorization prompt. + private showLoginAuthorizationPrompt(auth: DeviceAuthorization): LoginProgressSpinnerHandle { + openUrl(auth.verificationUriComplete); + this.state.transcriptContainer.addChild( + new DeviceCodeBoxComponent({ + title: 'Sign in to Kimi Code', + url: auth.verificationUriComplete, + code: auth.userCode, + hint: 'Press Ctrl-C to cancel', + colors: this.state.theme.colors, + }), + ); + this.state.ui.requestRender(); + return this.showLoginProgressSpinner('Waiting for authorization…'); + } + + // Provides UI callbacks used while hydrating transcript history. + private replayHydrationHooks(): ReplayHydrationHooks { + return { + setAppState: (patch) => { + this.setAppState(patch); + }, + appendEntry: (entry) => { + this.appendTranscriptEntry(entry); + }, + setTodoList: (todos) => { + this.setTodoList(todos); + }, + emitError: (message) => { + this.showError(message); + }, + }; + } + + // ========================================================================= + // Panes / Presentation State + // ========================================================================= + + // Rebuilds the activity pane for the current live and streaming state. + private updateActivityPane(): void { + const effectiveMode = this.resolveActivityPaneMode(); + this.syncTerminalProgress(this.shouldShowTerminalProgress(effectiveMode)); + + if ( + effectiveMode === this.state.lastActivityMode && + (effectiveMode === 'waiting' || effectiveMode === 'thinking' || effectiveMode === 'tool') + ) { + return; + } + + this.state.lastActivityMode = effectiveMode; + this.state.activityContainer.clear(); + + switch (effectiveMode) { + case 'hidden': + this.stopActivitySpinner(); + this.state.ui.requestRender(); + return; + case 'waiting': { + const spinner = this.ensureActivitySpinner('moon'); + this.state.activityContainer.addChild( + new ActivityPaneComponent({ + mode: 'waiting', + spinner, + }), + ); + break; + } + case 'thinking': { + this.stopActivitySpinner(); + break; + } + case 'composing': { + const spinner = this.ensureActivitySpinner('braille', 'working...', (s) => + chalk.hex(this.state.theme.colors.primary)(s), + ); + this.state.activityContainer.addChild( + new ActivityPaneComponent({ + mode: 'composing', + spinner, + }), + ); + break; + } + case 'tool': { + const spinner = this.ensureActivitySpinner('moon'); + this.state.activityContainer.addChild( + new ActivityPaneComponent({ + mode: 'tool', + spinner, + }), + ); + break; + } + case 'idle': + case 'session': { + this.stopActivitySpinner(); + break; + } + } + this.state.ui.requestRender(); + } + + // Computes the effective activity-pane mode from modal and streaming state. + private resolveActivityPaneMode(): EffectiveActivityPaneMode { + if (this.state.showingSessionPicker) return 'hidden'; + if (this.state.livePane.pendingApproval !== null) return 'hidden'; + if (this.state.appState.isCompacting) return 'hidden'; + if (this.state.livePane.pendingQuestion !== null) return 'hidden'; + + const streamingPhase = this.state.appState.streamingPhase; + if (this.state.livePane.mode === 'idle') { + if (streamingPhase === 'thinking' || streamingPhase === 'composing') { + return streamingPhase; + } + } + + return this.state.livePane.mode; + } + + // Re-renders the queued-message pane. + private updateQueueDisplay(): void { + this.state.queueContainer.clear(); + const queued = this.state.queuedMessages; + if (queued.length === 0) return; + + this.state.queueContainer.addChild( + new QueuePaneComponent({ + messages: queued, + colors: this.state.theme.colors, + isCompacting: this.state.appState.isCompacting, + isStreaming: this.state.appState.isStreaming, + canSteerImmediately: !this.deferUserMessages, + }), + ); + } + + // Toggles expansion for all expandable tool-output components. + private toggleToolOutputExpansion(): void { + this.state.toolOutputExpanded = !this.state.toolOutputExpanded; + for (const child of this.state.transcriptContainer.children) { + if (isExpandable(child)) { + child.setExpanded(this.state.toolOutputExpanded); + } + } + this.state.ui.requestRender(); + } + + // Toggles expansion for plan-preview cards (ExitPlanMode). Returns true + // iff at least one plan card was actually toggled so the caller can decide + // whether to consume the keystroke vs. let pi-tui's default end-of-line run. + private togglePlanExpansion(): boolean { + const next = !this.state.planExpanded; + let toggled = false; + for (const child of this.state.transcriptContainer.children) { + if (isPlanExpandable(child) && child.setPlanExpanded(next)) { + toggled = true; + } + } + if (!toggled) return false; + this.state.planExpanded = next; + this.state.ui.requestRender(); + return true; + } + + // Updates the editor border color for slash command and plan-mode context. + private updateEditorBorderHighlight(text?: string): void { + const trimmed = (text ?? this.state.editor.getText()).trimStart(); + const colorToken = + this.state.appState.planMode || trimmed.startsWith('/') + ? this.state.theme.colors.primary + : this.state.theme.colors.border; + this.state.editor.borderColor = (s: string) => chalk.hex(colorToken)(s); + this.state.ui.requestRender(); + } + + // Applies a theme bundle to all stateful UI theme references. + private applyTheme(theme: Theme, resolved?: ResolvedTheme): void { + const nextTheme = createKimiTUIThemeBundle(theme, resolved); + Object.assign(this.state.theme.colors, nextTheme.colors); + this.state.theme.resolvedTheme = nextTheme.resolvedTheme; + this.state.theme.styles = nextTheme.styles; + this.state.theme.markdownTheme = nextTheme.markdownTheme; + this.setAppState({ theme }); + this.updateEditorBorderHighlight(); + this.state.ui.requestRender(true); + } + + // Starts or stops terminal theme notifications according to the user preference. + private refreshTerminalThemeTracking(): void { + this.stopTerminalThemeTracking(); + if (this.state.appState.theme !== 'auto') return; + + this.terminalThemeTrackingDispose = installTerminalThemeTracking(this.state, (resolved) => { + this.applyResolvedAutoTheme(resolved); + }); + } + + // Stops terminal theme notifications if they were enabled for auto mode. + private stopTerminalThemeTracking(): void { + this.terminalThemeTrackingDispose?.(); + this.terminalThemeTrackingDispose = undefined; + } + + // Applies a concrete terminal-reported theme while keeping the preference as auto. + private applyResolvedAutoTheme(resolved: ResolvedTheme): void { + if (this.state.appState.theme !== 'auto') return; + if (this.state.theme.resolvedTheme === resolved) return; + this.applyTheme('auto', resolved); + } + + // Determines whether the terminal should expose progress state. + private shouldShowTerminalProgress(effectiveMode: EffectiveActivityPaneMode): boolean { + if (this.state.appState.isCompacting) return true; + return ( + effectiveMode === 'waiting' || + effectiveMode === 'thinking' || + effectiveMode === 'composing' || + effectiveMode === 'tool' + ); + } + + // Syncs terminal progress only when the active flag changes. + private syncTerminalProgress(active: boolean): void { + if (this.state.terminalState.progressActive === active) return; + this.state.terminal.setProgress(active); + this.state.terminalState.progressActive = active; + } + + // Returns an activity spinner with the requested style and presentation. + private ensureActivitySpinner( + style: SpinnerStyle, + label = '', + colorFn?: (s: string) => string, + ): MoonLoader { + if (this.state.activitySpinnerStyle !== style) { + this.stopActivitySpinner(); + } + + if (this.state.activitySpinner === undefined) { + this.state.activitySpinner = new MoonLoader(this.state.ui, style, colorFn, label); + this.state.activitySpinnerStyle = style; + return this.state.activitySpinner; + } + + this.state.activitySpinner.setLabel(label); + if (colorFn !== undefined) { + this.state.activitySpinner.setColorFn(colorFn); + } + return this.state.activitySpinner; + } + + // Stops and clears the activity spinner. + private stopActivitySpinner(): void { + if (this.state.activitySpinner) { + this.state.activitySpinner.stop(); + this.state.activitySpinner = undefined; + } + this.state.activitySpinnerStyle = undefined; + } + + // ========================================================================= + // Dialogs / Selectors + // ========================================================================= + + // Replaces the editor with a focusable dialog or selector panel. + private mountEditorReplacement(panel: Component & Focusable): void { + this.state.editorContainer.clear(); + this.state.editorContainer.addChild(panel); + this.state.ui.setFocus(panel); + this.state.ui.requestRender(); + } + + // Restores the main editor after a dialog or selector closes. + private restoreEditor(): void { + this.state.editorContainer.clear(); + this.state.editorContainer.addChild(this.state.editor); + this.state.ui.setFocus(this.state.editor); + this.state.ui.requestRender(); + } + + // Runs the first-launch migration screen, if a plan was detected pre-TUI. + // Resolves with the screen's result when the user dismisses it; the editor + // is then restored. + private async runMigrationScreen(plan: MigrationPlan): Promise<MigrationScreenResult> { + const result = await new Promise<MigrationScreenResult>((resolve) => { + const screen = new MigrationScreenComponent({ + plan, + // Reuse the source path detection already resolved — the single source + // of truth — rather than re-deriving it here. + sourceHome: plan.sourceHome, + targetHome: this.harness.homeDir, + colors: this.state.theme.colors, + skipDecisionStep: this.migrateOnly, + requestRender: () => { + this.state.ui.requestRender(); + }, + onComplete: (r) => { + resolve(r); + }, + }); + this.mountEditorReplacement(screen); + }); + this.restoreEditor(); + if (result.decision === 'never') { + // Persist the skip marker `detectPendingMigration` checks, so "Never ask + // again" actually stops the prompt from reappearing every launch. + try { + writeFileSync( + join(this.harness.homeDir, '.skip-migration-from-kimi-cli'), + '', + 'utf-8', + ); + } catch { + // Non-blocking: a failed marker write must never crash startup. + } + } + return result; + } + + // Shows the help panel with the current slash command list. + private showHelpPanel(): void { + this.state.showingHelpPanel = true; + this.mountEditorReplacement( + new HelpPanelComponent({ + commands: this.getSlashCommands(), + colors: this.state.theme.colors, + onClose: () => { + this.hideHelpPanel(); + }, + }), + ); + } + + // Hides the help panel and returns focus to the editor. + private hideHelpPanel(): void { + this.state.showingHelpPanel = false; + this.restoreEditor(); + } + + // Loads sessions and shows the session picker. + private async showSessionPicker(): Promise<void> { + await this.fetchSessions(); + this.mountSessionPicker(() => { + this.hideSessionPicker(); + }); + } + + // Shows the startup session picker and exits when it is cancelled. + private async bootstrapFromPicker(): Promise<void> { + await this.fetchSessions(); + this.mountSessionPicker(() => { + this.hideSessionPicker(); + void this.stop(); + }); + } + + // Hides the session picker and restores the editor. + private hideSessionPicker(): void { + this.state.showingSessionPicker = false; + this.restoreEditor(); + } + + // Mounts a session picker with shared selection behavior. + private mountSessionPicker(onCancel: () => void): void { + this.state.showingSessionPicker = true; + this.mountEditorReplacement( + new SessionPickerComponent({ + sessions: this.state.sessions, + loading: this.state.loadingSessions, + currentSessionId: this.state.appState.sessionId, + colors: this.state.theme.colors, + onSelect: (sessionId: string) => { + void this.resumeSession(sessionId).then((switched) => { + if (switched) { + this.hideSessionPicker(); + } + }); + }, + onCancel, + }), + ); + } + + // ========================================================================= + // Background tasks browser (`/tasks`) + // ========================================================================= + + /** + * Open the `/tasks` overlay. Idempotent: a second `/tasks` while the + * panel is already open is a no-op (the focus stays on the existing + * overlay) — prevents accidental stacking. + */ + private async showTasksBrowser(): Promise<void> { + if (this.state.tasksBrowser !== undefined) return; + const session = this.session; + if (session === undefined) { + this.showError('No active session.'); + return; + } + + let tasks: readonly BackgroundTaskInfo[] = []; + try { + tasks = await session.listBackgroundTasks({ activeOnly: false }); + } catch (error) { + this.showError( + `Failed to load tasks: ${error instanceof Error ? error.message : String(error)}`, + ); + return; + } + // Race: panel might have been opened then immediately closed by + // another path while the await above was in flight. Bail out then. + if (this.state.tasksBrowser !== undefined) return; + + const filter: TasksFilter = 'all'; + const selectedTaskId = this.pickInitialSelection(tasks, filter); + const component = new TasksBrowserApp( + { + tasks, + filter, + selectedTaskId, + tailOutput: undefined, + tailLoading: false, + flashMessage: undefined, + colors: this.state.theme.colors, + ...this.buildTasksBrowserCallbacks(), + }, + this.state.terminal, + ); + + // Alt-screen takeover: save the main TUI's children, then replace + // them with this single full-screen component. `closeTasksBrowser` + // restores the original layout. Mirrors the Python `Application( + // full_screen=True, erase_when_done=True)` pattern. + const savedChildren = [...this.state.ui.children]; + this.state.ui.clear(); + this.state.ui.addChild(component); + this.state.ui.setFocus(component); + this.state.ui.requestRender(true); + + const pollTimer = setInterval(() => { + void this.refreshTasksBrowser({ silent: true }); + }, 1000); + + this.state.tasksBrowser = { + component, + savedChildren, + filter, + selectedTaskId, + tailOutput: undefined, + tailLoading: false, + tailRequestId: 0, + flashMessage: undefined, + flashTimer: undefined, + pollTimer, + viewer: undefined, + }; + + if (selectedTaskId !== undefined) { + this.loadTasksBrowserTail(selectedTaskId); + } + } + + private pickInitialSelection( + tasks: readonly BackgroundTaskInfo[], + filter: TasksFilter, + ): string | undefined { + const candidates = + filter === 'all' + ? tasks + : tasks.filter( + (t) => + t.status !== 'completed' && + t.status !== 'failed' && + t.status !== 'killed' && + t.status !== 'lost', + ); + if (candidates.length === 0) return undefined; + // Prefer the first non-terminal task; fall back to the first one. + return ( + candidates.find( + (t) => t.status === 'running' || t.status === 'awaiting_approval', + )?.taskId ?? candidates[0]!.taskId + ); + } + + private async refreshTasksBrowser(opts: { silent?: boolean } = {}): Promise<void> { + const browser = this.state.tasksBrowser; + if (browser === undefined) return; + const session = this.session; + if (session === undefined) return; + + let tasks: readonly BackgroundTaskInfo[]; + try { + tasks = await session.listBackgroundTasks({ activeOnly: false }); + } catch (error) { + if (!opts.silent) { + this.flashTasksBrowser( + `Refresh failed: ${error instanceof Error ? error.message : String(error)}`, + ); + } + return; + } + if (this.state.tasksBrowser !== browser) return; + this.pushTasksBrowserProps(tasks); + } + + private pushTasksBrowserProps(tasks: readonly BackgroundTaskInfo[]): void { + const browser = this.state.tasksBrowser; + if (browser === undefined) return; + browser.component.setProps({ + tasks, + filter: browser.filter, + selectedTaskId: browser.selectedTaskId, + tailOutput: browser.tailOutput, + tailLoading: browser.tailLoading, + flashMessage: browser.flashMessage, + colors: this.state.theme.colors, + ...this.buildTasksBrowserCallbacks(), + }); + this.state.ui.requestRender(); + } + + /** Callback bundle for `TasksBrowserComponent`. Single source of truth. */ + private buildTasksBrowserCallbacks(): { + onSelect: (taskId: string) => void; + onToggleFilter: () => void; + onRefresh: () => void; + onCancel: () => void; + onStopConfirmed: (taskId: string) => void; + onOpenOutput: (taskId: string) => void; + onStopIgnored: (taskId: string, reason: 'terminal') => void; + } { + return { + onSelect: (taskId) => { + this.handleTasksBrowserSelect(taskId); + }, + onToggleFilter: () => { + this.handleTasksBrowserToggleFilter(); + }, + onRefresh: () => { + this.handleTasksBrowserRefresh(); + }, + onCancel: () => { + this.closeTasksBrowser(); + }, + onStopConfirmed: (taskId) => { + void this.handleTasksBrowserStop(taskId); + }, + onOpenOutput: (taskId) => { + void this.handleTasksBrowserOpenOutput(taskId); + }, + onStopIgnored: (taskId, reason) => { + if (reason === 'terminal') { + this.flashTasksBrowser(`${taskId} is already terminal — nothing to stop.`); + } + }, + }; + } + + private handleTasksBrowserSelect(taskId: string): void { + const browser = this.state.tasksBrowser; + if (browser === undefined) return; + if (browser.selectedTaskId === taskId) return; + browser.selectedTaskId = taskId; + browser.tailOutput = undefined; + browser.tailLoading = true; + this.repaintTasksBrowser(); + this.loadTasksBrowserTail(taskId); + } + + private handleTasksBrowserToggleFilter(): void { + const browser = this.state.tasksBrowser; + if (browser === undefined) return; + browser.filter = browser.filter === 'all' ? 'active' : 'all'; + this.repaintTasksBrowser(); + } + + private handleTasksBrowserRefresh(): void { + this.flashTasksBrowser('Refreshing…', 600); + void this.refreshTasksBrowser(); + } + + /** + * Re-render the `/tasks` panel from the in-memory BPM store (no RPC + * fetch). Safe to call when the panel is closed (no-op). Use this + * after any local state change — selection, filter, flash message, + * or an incoming `background.task.*` event — so the UI stays in sync. + * Use `refreshTasksBrowser` instead when you also want fresh data + * from the agent (e.g. a manual `R refresh`). + */ + private repaintTasksBrowser(): void { + const browser = this.state.tasksBrowser; + if (browser === undefined) return; + const tasks = [...this.state.backgroundTasks.values()]; + this.pushTasksBrowserProps(tasks); + } + + private loadTasksBrowserTail(taskId: string): void { + const browser = this.state.tasksBrowser; + if (browser === undefined) return; + const session = this.session; + if (session === undefined) { + browser.tailLoading = false; + this.repaintTasksBrowser(); + return; + } + const requestId = ++browser.tailRequestId; + void session + .getBackgroundTaskOutput(taskId, { tail: 4000 }) + .then((output) => { + const current = this.state.tasksBrowser; + if (current === undefined) return; + if (current !== browser || current.tailRequestId !== requestId) return; + if (current.selectedTaskId !== taskId) return; + current.tailOutput = output; + current.tailLoading = false; + this.repaintTasksBrowser(); + }) + .catch(() => { + const current = this.state.tasksBrowser; + if (current === undefined) return; + if (current !== browser || current.tailRequestId !== requestId) return; + if (current.selectedTaskId !== taskId) return; + current.tailOutput = ''; + current.tailLoading = false; + this.repaintTasksBrowser(); + }); + } + + private flashTasksBrowser(message: string, durationMs = 2500): void { + const browser = this.state.tasksBrowser; + if (browser === undefined) return; + if (browser.flashTimer !== undefined) clearTimeout(browser.flashTimer); + browser.flashMessage = message; + browser.flashTimer = setTimeout(() => { + const current = this.state.tasksBrowser; + if (current !== browser) return; + current.flashMessage = undefined; + current.flashTimer = undefined; + this.repaintTasksBrowser(); + }, durationMs); + this.repaintTasksBrowser(); + } + + private async handleTasksBrowserStop(taskId: string): Promise<void> { + const browser = this.state.tasksBrowser; + if (browser === undefined) return; + const session = this.session; + if (session === undefined) { + this.flashTasksBrowser('No active session.'); + return; + } + this.flashTasksBrowser(`Stopping ${taskId}…`, 1500); + try { + await session.stopBackgroundTask(taskId, { reason: 'stopped from /tasks' }); + // Force a refresh so the row flips to `killed` immediately. The + // `background.task.terminated` event will repaint again shortly. + await this.refreshTasksBrowser({ silent: true }); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + this.flashTasksBrowser(`Stop failed: ${message}`); + } + } + + private async handleTasksBrowserOpenOutput(taskId: string): Promise<void> { + const browser = this.state.tasksBrowser; + if (browser === undefined) return; + if (browser.viewer !== undefined) return; // already viewing + const session = this.session; + if (session === undefined) { + this.flashTasksBrowser('No active session.'); + return; + } + + let output: string; + try { + output = await session.getBackgroundTaskOutput(taskId); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + this.flashTasksBrowser(`Cannot open output: ${message}`); + return; + } + // Race: panel might have been closed while the await was in flight. + const current = this.state.tasksBrowser; + if (current === undefined || current !== browser) return; + + const info = this.state.backgroundTasks.get(taskId); + const viewer = new TaskOutputViewer( + { + taskId, + info, + output, + colors: this.state.theme.colors, + onClose: () => { + this.closeTaskOutputViewer(); + }, + }, + this.state.terminal, + ); + + // Nested takeover: save the TasksBrowser layer (which itself is a + // single-child swap of the main TUI), then put the viewer in its + // place. `closeTaskOutputViewer` reverses this without touching the + // outer "main TUI ↔ TasksBrowser" swap state. + const savedBrowserChildren = [...this.state.ui.children]; + this.state.ui.clear(); + this.state.ui.addChild(viewer); + this.state.ui.setFocus(viewer); + this.state.ui.requestRender(true); + + // Live-tail: keep re-fetching the output every second so the viewer + // shows new content as the task writes it. The viewer itself decides + // whether to follow the tail or preserve scroll position. + const pollTimer = setInterval(() => { + void this.refreshTaskOutputViewer({ silent: true }); + }, 1000); + + browser.viewer = { + component: viewer, + savedChildren: savedBrowserChildren, + taskId, + output, + refreshId: 0, + pollTimer, + }; + } + + /** + * Re-fetch the current viewer task's output and push it into the + * component. Safe to call when the viewer is closed (no-op). Stale + * responses (issued before a more recent call) are discarded via the + * monotonically-increasing `refreshId`. + */ + private async refreshTaskOutputViewer(opts: { silent?: boolean } = {}): Promise<void> { + const browser = this.state.tasksBrowser; + const viewer = browser?.viewer; + if (browser === undefined || viewer === undefined) return; + const session = this.session; + if (session === undefined) return; + + const myRefreshId = ++viewer.refreshId; + let output: string; + try { + output = await session.getBackgroundTaskOutput(viewer.taskId); + } catch (error) { + if (!opts.silent) { + const message = error instanceof Error ? error.message : String(error); + this.flashTasksBrowser(`Output refresh failed: ${message}`); + } + return; + } + // If the viewer was closed or another refresh raced ahead, drop this result. + const current = this.state.tasksBrowser?.viewer; + if (current === undefined || current !== viewer || current.refreshId !== myRefreshId) { + return; + } + // Skip the setProps round-trip when nothing changed — keeps the + // differential renderer from re-emitting the same frame. + if (output === viewer.output) return; + viewer.output = output; + const info = this.state.backgroundTasks.get(viewer.taskId); + viewer.component.setProps({ + taskId: viewer.taskId, + info, + output, + colors: this.state.theme.colors, + onClose: () => { + this.closeTaskOutputViewer(); + }, + }); + this.state.ui.requestRender(); + } + + private closeTaskOutputViewer(): void { + const browser = this.state.tasksBrowser; + if (browser === undefined || browser.viewer === undefined) return; + const viewer = browser.viewer; + clearInterval(viewer.pollTimer); + browser.viewer = undefined; + this.state.ui.clear(); + for (const child of viewer.savedChildren) { + this.state.ui.addChild(child); + } + this.state.ui.setFocus(browser.component); + this.state.ui.requestRender(true); + } + + private closeTasksBrowser(): void { + const browser = this.state.tasksBrowser; + if (browser === undefined) return; + // If the output viewer is open, fold it back before tearing down + // the browser so the saved-children stack stays consistent. + if (browser.viewer !== undefined) this.closeTaskOutputViewer(); + if (browser.pollTimer !== undefined) clearInterval(browser.pollTimer); + if (browser.flashTimer !== undefined) clearTimeout(browser.flashTimer); + + // Restore the main TUI's children we saved when opening. After + // clearing, re-add in original order, then return focus to the + // editor so the user is back at the prompt. + this.state.ui.clear(); + for (const child of browser.savedChildren) { + this.state.ui.addChild(child); + } + this.state.tasksBrowser = undefined; + this.state.ui.setFocus(this.state.editor); + this.state.ui.requestRender(true); + } + + // Shows the editor command selector. + private showEditorPicker(): void { + const currentValue = this.state.appState.editorCommand ?? ''; + this.mountEditorReplacement( + new EditorSelectorComponent({ + currentValue, + colors: this.state.theme.colors, + onSelect: (value) => { + this.restoreEditor(); + void this.applyEditorChoice(value); + }, + onCancel: () => { + this.restoreEditor(); + }, + }), + ); + } + + // Persists and applies the selected external editor command. + private async applyEditorChoice(value: string): Promise<void> { + const previous = this.state.appState.editorCommand ?? ''; + if (value === previous && value.length > 0) { + this.showStatus(`Editor unchanged: ${value.length > 0 ? value : 'auto-detect'}`); + return; + } + + const editorCommand = value.length > 0 ? value : null; + try { + await saveTuiConfig({ + theme: this.state.appState.theme, + editorCommand, + notifications: this.state.appState.notifications, + }); + } catch (error) { + this.showStatus( + `Failed to save editor: ${formatErrorMessage(error)}`, + this.state.theme.colors.error, + ); + return; + } + + this.setAppState({ editorCommand }); + this.showStatus( + value.length > 0 + ? `Editor set to "${value}".` + : 'Editor set to auto-detect ($VISUAL / $EDITOR).', + ); + } + + // Shows the model selector when models are available. + private showModelPicker(selectedValue: string = this.state.appState.model): void { + const entries = Object.entries(this.state.appState.availableModels); + if (entries.length === 0) { + this.showError('No models configured.'); + return; + } + this.mountEditorReplacement( + new ModelSelectorComponent({ + models: this.state.appState.availableModels, + currentValue: this.state.appState.model, + selectedValue, + currentThinking: this.state.appState.thinking, + colors: this.state.theme.colors, + onSelect: ({ alias, thinking }) => { + this.restoreEditor(); + void this.performModelSwitch(alias, thinking); + }, + onCancel: () => { + this.restoreEditor(); + }, + }), + ); + } + + // Applies model and thinking changes to the active or newly created session. + private async performModelSwitch(alias: string, thinking: boolean): Promise<void> { + if (this.state.appState.isStreaming) { + this.showError('Cannot switch models while streaming — press Esc or Ctrl-C first.'); + return; + } + + if (alias === this.state.appState.model && thinking === this.state.appState.thinking) { + this.showStatus(`Already using ${alias} with thinking ${thinking ? 'on' : 'off'}.`); + return; + } + + const level = thinking ? 'on' : 'off'; + const prevModel = this.state.appState.model; + const prevThinking = this.state.appState.thinking; + + try { + const session = this.session; + if (session === undefined) { + await this.activateModelAfterLogin(alias, thinking); + } else { + if (alias !== prevModel) { + await session.setModel(alias); + } + if (thinking !== prevThinking) { + await session.setThinking(level); + } + } + this.setAppState({ model: alias, thinking }); + if (session === undefined) { + if (alias !== prevModel) { + this.track('model_switch', { model: alias }); + } + if (thinking !== prevThinking) { + this.track('thinking_toggle', { enabled: thinking }); + } + } + this.showStatus( + `Switched to ${alias} with thinking ${level}.`, + this.state.theme.colors.success, + ); + } catch (error) { + const msg = formatErrorMessage(error); + this.showError(`Failed to switch model: ${msg}`); + } + } + + // Shows the theme selector. + private showThemePicker(): void { + this.mountEditorReplacement( + new ThemeSelectorComponent({ + currentValue: this.state.appState.theme, + colors: this.state.theme.colors, + onSelect: (value) => { + this.restoreEditor(); + void this.applyThemeChoice(value); + }, + onCancel: () => { + this.restoreEditor(); + }, + }), + ); + } + + // Shows the permission mode selector. + private showPermissionPicker(): void { + this.mountEditorReplacement( + new PermissionSelectorComponent({ + currentValue: this.state.appState.permissionMode, + colors: this.state.theme.colors, + onSelect: (value) => { + this.restoreEditor(); + void this.applyPermissionChoice(value); + }, + onCancel: () => { + this.restoreEditor(); + }, + }), + ); + } + + // Shows the settings selector entry point. + private showSettingsSelector(): void { + this.mountEditorReplacement( + new SettingsSelectorComponent({ + colors: this.state.theme.colors, + onSelect: (value) => { + this.handleSettingsSelection(value); + }, + onCancel: () => { + this.restoreEditor(); + }, + }), + ); + } + + // Routes a settings selection to the matching selector or panel. + private handleSettingsSelection(value: SettingsSelection): void { + this.restoreEditor(); + switch (value) { + case 'model': + this.showModelPicker(); + return; + case 'permission': + this.showPermissionPicker(); + return; + case 'theme': + this.showThemePicker(); + return; + case 'editor': + this.showEditorPicker(); + return; + case 'usage': + void this.showUsage(); + return; + } + } + + // Applies a permission mode choice to the active session and app state. + private async applyPermissionChoice(mode: PermissionMode): Promise<void> { + if (mode === this.state.appState.permissionMode) { + this.showStatus(`Permission mode unchanged: ${mode}.`); + return; + } + + try { + await this.requireSession().setPermission(mode); + } catch (error) { + const msg = formatErrorMessage(error); + this.showError(`Failed to set permission mode: ${msg}`); + return; + } + + this.setAppState({ permissionMode: mode, yolo: mode === 'yolo' }); + this.showNotice(`Permission mode: ${mode}`); + } + + // Persists and applies a theme choice. + private async applyThemeChoice(theme: Theme): Promise<void> { + if (theme === this.state.appState.theme) { + if (theme === 'auto') this.refreshTerminalThemeTracking(); + this.showStatus(`Theme unchanged: "${theme}".`); + return; + } + + try { + await saveTuiConfig({ + theme, + editorCommand: this.state.appState.editorCommand, + notifications: this.state.appState.notifications, + }); + } catch (error) { + this.showStatus( + `Failed to save theme: ${formatErrorMessage(error)}`, + this.state.theme.colors.error, + ); + return; + } + + const resolved = theme === 'auto' ? this.state.theme.resolvedTheme : theme; + this.applyTheme(theme, resolved); + this.refreshTerminalThemeTracking(); + this.track('theme_switch', { theme }); + const detail = theme === 'auto' ? ` (tracking terminal; current: ${resolved})` : ''; + this.showStatus(`Theme set to "${theme}"${detail}.`); + } + + // Loads and renders current usage information. + private async showUsage(): Promise<void> { + const sessionUsage = await this.loadSessionUsageReport(); + const managedUsage = await this.loadManagedUsageReport(); + const lines = buildUsageReportLines({ + colors: this.state.theme.colors, + sessionUsage: sessionUsage.usage, + sessionUsageError: sessionUsage.error, + contextUsage: this.state.appState.contextUsage, + contextTokens: this.state.appState.contextTokens, + maxContextTokens: this.state.appState.maxContextTokens, + managedUsage: managedUsage?.usage, + managedUsageError: managedUsage?.error, + }); + const panel = new UsagePanelComponent(lines, this.state.theme.colors.primary); + this.state.transcriptContainer.addChild(panel); + this.state.ui.requestRender(); + } + + // Loads and renders current runtime status. + private async showStatusReport(): Promise<void> { + const [runtimeStatus, managedUsage] = await Promise.all([ + this.loadRuntimeStatusReport(), + this.loadManagedUsageReport(), + ]); + const appState = this.state.appState; + const lines = buildStatusReportLines({ + colors: this.state.theme.colors, + version: appState.version, + model: appState.model, + workDir: appState.workDir, + sessionId: appState.sessionId, + sessionTitle: appState.sessionTitle, + thinking: appState.thinking, + permissionMode: appState.permissionMode, + planMode: appState.planMode, + contextUsage: appState.contextUsage, + contextTokens: appState.contextTokens, + maxContextTokens: appState.maxContextTokens, + availableModels: appState.availableModels, + status: runtimeStatus.status, + statusError: runtimeStatus.error, + managedUsage: managedUsage?.usage, + managedUsageError: managedUsage?.error, + }); + const panel = new UsagePanelComponent(lines, this.state.theme.colors.primary, ' Status '); + this.state.transcriptContainer.addChild(panel); + this.state.ui.requestRender(); + } + + // Loads and renders current MCP server status. + private async showMcpServers(): Promise<void> { + let servers: readonly McpServerInfo[]; + try { + servers = await this.requireSession().listMcpServers(); + } catch (error) { + this.showError(`Failed to load MCP servers: ${formatErrorMessage(error)}`); + return; + } + + const lines = buildMcpStatusReportLines({ + colors: this.state.theme.colors, + servers, + }); + const title = servers.length > 0 ? ` MCP (${servers.length}) ` : ' MCP '; + const panel = new UsagePanelComponent(lines, this.state.theme.colors.primary, title); + this.state.transcriptContainer.addChild(panel); + this.state.ui.requestRender(); + } + + // Loads per-session usage and captures displayable errors. + private async loadSessionUsageReport(): Promise<SessionUsageResult> { + try { + return { usage: await this.requireSession().getUsage() }; + } catch (error) { + return { error: formatErrorMessage(error) }; + } + } + + // Loads per-session runtime status and captures displayable errors. + private async loadRuntimeStatusReport(): Promise<RuntimeStatusResult> { + try { + return { status: await this.requireSession().getStatus() }; + } catch (error) { + return { error: error instanceof Error ? error.message : String(error) }; + } + } + + // Loads managed-provider usage when the active model supports it. + private async loadManagedUsageReport(): Promise<ManagedUsageResult | undefined> { + const alias = this.state.appState.model; + const providerKey = this.state.appState.availableModels[alias]?.provider; + if (!isManagedUsageProvider(providerKey)) return undefined; + + let res; + try { + res = await this.harness.auth.getManagedUsage(providerKey); + } catch (error) { + return { error: formatErrorMessage(error) }; + } + if (res.kind === 'error') { + return { error: res.message }; + } + return { usage: { summary: res.summary, limits: res.limits } }; + } + + // Shows an approval panel and connects its response callback. + private showApprovalPanel(payload: ApprovalPanelData): void { + this.patchLivePane({ pendingApproval: { data: payload } }); + notifyTerminalOnce(this.state, `approval:${payload.id}`, { + title: 'Kimi Code approval required', + body: payload.tool_name, + }); + const panel = new ApprovalPanelComponent( + { data: payload }, + (response: ApprovalPanelResponse) => { + this.approvalController.respond(adaptPanelResponse(response)); + }, + this.state.theme.colors, + () => { + this.toggleToolOutputExpansion(); + }, + () => { + this.togglePlanExpansion(); + }, + ); + this.mountEditorReplacement(panel); + } + + // Hides the active approval panel. + private hideApprovalPanel(): void { + this.patchLivePane({ pendingApproval: null }); + this.restoreEditor(); + } + + // Shows a question dialog and connects its response callback. + private showQuestionDialog(payload: QuestionPanelData): void { + this.patchLivePane({ pendingQuestion: { data: payload } }); + notifyTerminalOnce(this.state, `question:${payload.id}`, { + title: 'Kimi Code needs your answer', + body: payload.questions[0]?.question, + }); + const dialog = new QuestionDialogComponent( + { data: payload }, + (response) => { + this.questionController.respond(response); + }, + this.state.theme.colors, + undefined, + () => { + this.toggleToolOutputExpansion(); + }, + () => { + this.togglePlanExpansion(); + }, + ); + this.mountEditorReplacement(dialog); + } + + // Hides the active question dialog. + private hideQuestionDialog(): void { + this.patchLivePane({ pendingQuestion: null }); + this.restoreEditor(); + } + + // ========================================================================= + // Slash Command Handlers + // ========================================================================= + + // Applies plan mode through the session and mirrors it into UI state. + private async applyPlanMode(session: Session, enabled: boolean): Promise<void> { + try { + await session.setPlanMode(enabled); + this.setAppState({ planMode: enabled }); + if (enabled) { + const plan = await session.getPlan().catch(() => null); + this.showNotice( + 'Plan mode: ON', + plan?.path !== undefined ? `Plan will be created here: ${plan.path}` : undefined, + ); + return; + } + this.showNotice('Plan mode: OFF'); + } catch (error) { + const msg = formatErrorMessage(error); + this.showError(`Failed to set plan mode: ${msg}`); + } + } + + // Handles the /editor command. + private async handleEditorCommand(args: string, _eCtx: {}): Promise<void> { + const command = args.trim(); + if (command.length === 0) { + this.showEditorPicker(); + return; + } + await this.applyEditorChoice(command); + } + + // Handles the /theme command. + private async handleThemeCommand(args: string): Promise<void> { + const theme = args.trim(); + if (theme.length === 0) { + this.showThemePicker(); + return; + } + if (!isTheme(theme)) { + this.showError(`Unknown theme: ${theme}`); + return; + } + await this.applyThemeChoice(theme); + } + + // Handles the /model command. + private handleModelCommand(args: string): void { + const alias = args.trim(); + if (alias.length === 0) { + this.showModelPicker(); + return; + } + if (this.state.appState.availableModels[alias] === undefined) { + this.showError(`Unknown model alias: ${alias}`); + return; + } + this.showModelPicker(alias); + } + + // Handles the /title command. + private async handleTitleCommand(args: string): Promise<void> { + const title = args.trim(); + if (title.length === 0) { + const current = this.state.appState.sessionTitle; + this.showStatus( + current !== null && current.length > 0 + ? `Session title: ${current}` + : `Session title: (not set) — id: ${this.state.appState.sessionId}`, + ); + return; + } + + const session = this.session; + if (session === undefined) { + this.showError(NO_ACTIVE_SESSION_MESSAGE); + return; + } + + const newTitle = title.slice(0, 200); + try { + await this.harness.renameSession({ id: session.id, title: newTitle }); + } catch (error) { + const msg = formatErrorMessage(error); + this.showError(`Failed to set title: ${msg}`); + return; + } + this.showStatus(`Session title set to: ${newTitle}`); + } + + // Handles the /fork command. + private async handleForkCommand(args: string): Promise<void> { + void args; + + const session = this.session; + if (session === undefined) { + this.showError(NO_ACTIVE_SESSION_MESSAGE); + return; + } + + const sourceTitle = this.forkSourceTitle(session); + let forked: Session; + try { + forked = await this.harness.forkSession({ + id: session.id, + title: `Fork: ${sourceTitle}`, + }); + } catch (error) { + const msg = formatErrorMessage(error); + this.showError(`Failed to fork session: ${msg}`); + return; + } + + try { + await this.switchToSession(forked, `Session forked (${forked.id}).`); + } catch (error) { + const msg = formatErrorMessage(error); + this.showError(`Failed to switch to forked session: ${msg}`); + } + } + + private forkSourceTitle(session: Session): string { + const currentTitle = this.state.appState.sessionTitle?.trim(); + if (currentTitle !== undefined && currentTitle.length > 0) return currentTitle; + + const summaryTitle = + typeof session.summary?.title === 'string' ? session.summary.title.trim() : ''; + return summaryTitle.length > 0 ? summaryTitle : session.id; + } + + // Handles the /yolo command. + private async handleYoloCommand(args: string): Promise<void> { + const session = this.session; + if (session === undefined) { + this.showError(NO_ACTIVE_SESSION_MESSAGE); + return; + } + + let enabled: boolean; + if (args === 'on') enabled = true; + else if (args === 'off') enabled = false; + else enabled = !this.state.appState.yolo; + + await session.setPermission(enabled ? 'yolo' : 'manual'); + this.setAppState({ yolo: enabled, permissionMode: enabled ? 'yolo' : 'manual' }); + if (enabled) { + this.showNotice( + 'YOLO mode: ON', + 'All actions will be approved automatically. Use with caution.', + ); + return; + } + this.showNotice('YOLO mode: OFF'); + } + + // Handles the /plan command. + private async handlePlanCommand(args: string): Promise<void> { + const session = this.session; + if (session === undefined) { + this.showError(NO_ACTIVE_SESSION_MESSAGE); + return; + } + + const subcmd = args.trim().toLowerCase(); + if (subcmd === 'clear') { + await session.clearPlan(); + this.showNotice('Plan cleared'); + return; + } + + let enabled: boolean; + if (subcmd.length === 0) enabled = !this.state.appState.planMode; + else if (subcmd === 'on') enabled = true; + else if (subcmd === 'off') enabled = false; + else { + this.showError(`Unknown plan subcommand: ${subcmd}`); + return; + } + + await this.applyPlanMode(session, enabled); + } + + // Handles the /compact command. + private async handleCompactCommand(args: string): Promise<void> { + const session = this.session; + if (session === undefined) { + this.showError(NO_ACTIVE_SESSION_MESSAGE); + return; + } + + const customInstruction = args.trim() || undefined; + await session.compact({ instruction: customInstruction }); + } + + // Handles the /init command. + private async handleInitCommand(): Promise<void> { + const session = this.session; + if (this.state.appState.model.trim().length === 0 || session === undefined) { + this.showError(LLM_NOT_SET_MESSAGE); + return; + } + + this.deferUserMessages = true; + this.beginSessionRequest(); + try { + await session.init(); + this.track('init_complete'); + this.finalizeTurn((item) => { + this.sendQueuedMessage(session, item); + }); + } catch (error) { + if (isAbortError(error)) { + this.setAppState({ isStreaming: false, streamingPhase: 'idle' }); + this.resetLivePane(); + return; + } + const msg = error instanceof Error ? error.message : String(error); + this.failSessionRequest(`Init failed: ${msg}`); + } finally { + this.deferUserMessages = false; + } + } + + // Handles the /login command. + private async handleLoginCommand(): Promise<void> { + const platformId = await this.promptPlatformSelection(); + if (platformId === undefined) return; + + if (platformId === 'kimi-code') { + await this.handleKimiCodeOAuthLogin(); + return; + } + + const platform = getOpenPlatformById(platformId); + if (platform === undefined) return; + await this.handleOpenPlatformLogin(platform); + } + + // Kimi Code OAuth login flow. + private async handleKimiCodeOAuthLogin(): Promise<void> { + const status = await this.harness.auth.status(DEFAULT_OAUTH_PROVIDER_NAME); + const alreadyLoggedIn = status.providers.some( + (provider) => provider.providerName === DEFAULT_OAUTH_PROVIDER_NAME && provider.hasToken, + ); + + let spinner: LoginProgressSpinnerHandle | undefined; + const controller = new AbortController(); + const cancelLogin = (): void => { + controller.abort(); + }; + this.cancelInFlight = cancelLogin; + try { + await this.harness.auth.login(DEFAULT_OAUTH_PROVIDER_NAME, { + signal: controller.signal, + onDeviceCode: (data) => { + spinner = this.showLoginAuthorizationPrompt(data); + }, + }); + spinner?.stop({ ok: true, label: 'Logged in.' }); + spinner = undefined; + try { + await this.refreshConfigAfterLogin(); + } catch (refreshError) { + const message = formatErrorMessage(refreshError); + this.showError(`Authentication successful, but failed to refresh config: ${message}`); + return; + } + this.track('login', { + provider: DEFAULT_OAUTH_PROVIDER_NAME, + already_logged_in: alreadyLoggedIn, + }); + if (alreadyLoggedIn) { + this.showStatus('Already logged in. Model configuration refreshed.'); + } + } catch (error) { + const cancelled = controller.signal.aborted; + spinner?.stop({ + ok: false, + label: cancelled ? 'Login cancelled.' : 'Login failed.', + }); + spinner = undefined; + if (cancelled) return; + log.warn('login failed', { + providerName: DEFAULT_OAUTH_PROVIDER_NAME, + alreadyLoggedIn, + sessionId: this.session?.id, + error, + }); + const message = formatErrorMessage(error); + this.showError(`Login failed: ${message}`); + } finally { + if (this.cancelInFlight === cancelLogin) { + this.cancelInFlight = undefined; + } + } + } + + // Open platform API key login flow. + private async handleOpenPlatformLogin( + platform: OpenPlatformDefinition, + ): Promise<void> { + const apiKey = await this.promptApiKey(platform.name); + if (apiKey === undefined) return; + + const controller = new AbortController(); + const cancelLogin = (): void => { + controller.abort(); + }; + this.cancelInFlight = cancelLogin; + + let models: ManagedKimiCodeModelInfo[]; + try { + models = await fetchOpenPlatformModels(platform, apiKey, fetch, controller.signal); + models = filterModelsByPrefix(models, platform); + } catch (error) { + if (controller.signal.aborted) return; + const msg = formatErrorMessage(error); + this.showError(`Failed to verify API key: ${msg}`); + if ( + error instanceof OpenPlatformApiError && + error.status === 401 + ) { + this.showStatus( + 'Hint: If your API key was obtained from Kimi Code, please select "Kimi Code" instead.', + ); + } + return; + } finally { + if (this.cancelInFlight === cancelLogin) { + this.cancelInFlight = undefined; + } + } + + if (models.length === 0) { + this.showError('No models available for this platform.'); + return; + } + + const selection = await this.promptModelSelectionForOpenPlatform(models, platform); + if (selection === undefined) return; + + // Remove stale provider config first so old model aliases are fully + // cleared (setConfig patch merge cannot delete nested keys). + const existingConfig = await this.harness.getConfig(); + if (existingConfig.providers[platform.id] !== undefined) { + await this.harness.removeProvider(platform.id); + } + + const config = await this.harness.getConfig(); + applyOpenPlatformConfig(config as ManagedKimiConfigShape, { + platform, + models, + selectedModel: selection.model, + thinking: selection.thinking, + apiKey, + }); + + await this.harness.setConfig({ + providers: config.providers, + models: config.models, + defaultModel: config.defaultModel, + defaultThinking: config.defaultThinking, + }); + + await this.refreshConfigAfterLogin(); + this.track('login', { provider: platform.id, method: 'api_key' }); + this.showStatus(`Setup complete: ${platform.name} · ${selection.model.id}`); + } + + // Handles the /feedback command — opens an inline input dialog and POSTs + // the result to the managed Kimi Code platform. Falls back to the GitHub + // Issues page when the user is not signed in or the request fails. + private async handleFeedbackCommand(): Promise<void> { + const fallback = (reason: string): void => { + this.showStatus(reason); + this.showStatus(FEEDBACK_ISSUE_URL); + openUrl(FEEDBACK_ISSUE_URL); + }; + + const providerKey = this.state.appState.availableModels[this.state.appState.model]?.provider; + if (!isManagedUsageProvider(providerKey)) { + fallback(FEEDBACK_STATUS_NOT_SIGNED_IN); + return; + } + + const content = await this.promptFeedbackInput(); + if (content === undefined) { + this.showStatus(FEEDBACK_STATUS_CANCELLED); + return; + } + + const spinner = this.showLoginProgressSpinner(FEEDBACK_STATUS_SUBMITTING); + const res = await this.harness.auth.submitFeedback({ + content, + sessionId: this.state.appState.sessionId, + version: withFeedbackVersionPrefix(this.state.appState.version), + os: `${osType()} ${osRelease()}`, + model: this.state.appState.model.length > 0 ? this.state.appState.model : null, + }); + + if (res.kind === 'ok') { + spinner.stop({ ok: true, label: FEEDBACK_STATUS_SUCCESS }); + this.showStatus(feedbackSessionLine(this.state.appState.sessionId)); + this.track(FEEDBACK_TELEMETRY_EVENT); + return; + } + + const failLabel = + res.status !== undefined + ? feedbackHttpErrorMessage(res.status) + : FEEDBACK_STATUS_NETWORK_ERROR; + spinner.stop({ ok: false, label: failLabel }); + fallback(FEEDBACK_STATUS_FALLBACK); + } + + // Mounts the feedback input dialog and resolves with the trimmed value + // when submitted, or undefined when the user cancels. + private promptFeedbackInput(): Promise<string | undefined> { + return new Promise((resolve) => { + const dialog = new FeedbackInputDialogComponent((result: FeedbackInputDialogResult) => { + this.restoreEditor(); + resolve(result.kind === 'ok' ? result.value : undefined); + }, this.state.theme.colors); + this.mountEditorReplacement(dialog); + }); + } + + // Handles the /logout command. + private async handleLogoutCommand(): Promise<void> { + const currentModel = this.state.appState.model.trim(); + const currentProvider = this.state.appState.availableModels[currentModel]?.provider; + + if (currentProvider === undefined || currentProvider === DEFAULT_OAUTH_PROVIDER_NAME) { + await this.harness.auth.logout(DEFAULT_OAUTH_PROVIDER_NAME); + await this.refreshConfigAfterLogout(); + await this.clearActiveSessionAfterLogout(); + this.track('logout', { provider: DEFAULT_OAUTH_PROVIDER_NAME }); + this.showStatus('Logged out.'); + return; + } + + if (isOpenPlatformId(currentProvider)) { + await this.harness.removeProvider(currentProvider); + await this.refreshConfigAfterLogout(); + await this.clearActiveSessionAfterLogout(); + this.track('logout', { provider: currentProvider }); + this.showStatus(`Logged out from ${currentProvider}.`); + return; + } + + this.showStatus('Nothing to logout.'); + } + + // --------------------------------------------------------------------------- + // Login / setup prompts + // --------------------------------------------------------------------------- + + private promptPlatformSelection(): Promise<string | undefined> { + return new Promise((resolve) => { + const selector = new PlatformSelectorComponent({ + colors: this.state.theme.colors, + onSelect: (platformId) => { + this.restoreEditor(); + resolve(platformId); + }, + onCancel: () => { + this.restoreEditor(); + resolve(undefined); + }, + }); + this.mountEditorReplacement(selector); + }); + } + + private promptApiKey(platformName: string): Promise<string | undefined> { + return new Promise((resolve) => { + const dialog = new ApiKeyInputDialogComponent( + platformName, + (result: ApiKeyInputResult) => { + this.restoreEditor(); + resolve(result.kind === 'ok' ? result.value : undefined); + }, + this.state.theme.colors, + ); + this.mountEditorReplacement(dialog); + }); + } + + private promptModelSelectionForOpenPlatform( + models: ManagedKimiCodeModelInfo[], + platform: OpenPlatformDefinition, + ): Promise<{ model: ManagedKimiCodeModelInfo; thinking: boolean } | undefined> { + return new Promise((resolve) => { + const modelDict: Record<string, ModelAlias> = {}; + for (const m of models) { + const alias = `${platform.id}/${m.id}`; + modelDict[alias] = { + provider: platform.id, + model: m.id, + maxContextSize: m.contextLength, + capabilities: capabilitiesForModel(m), + displayName: m.displayName, + }; + } + + const firstAlias = Object.keys(modelDict)[0] ?? ''; + const firstModel = modelDict[firstAlias]; + const initialThinking = (() => { + const caps = firstModel?.capabilities ?? []; + return caps.includes('always_thinking') || caps.includes('thinking'); + })(); + + const selector = new ModelSelectorComponent({ + models: modelDict, + currentValue: firstAlias, + currentThinking: initialThinking, + colors: this.state.theme.colors, + onSelect: ({ alias, thinking }) => { + this.restoreEditor(); + const model = models.find((m) => `${platform.id}/${m.id}` === alias); + resolve(model ? { model, thinking } : undefined); + }, + onCancel: () => { + this.restoreEditor(); + resolve(undefined); + }, + }); + this.mountEditorReplacement(selector); + }); + } +} + +function formatHookResultMarkdown(event: HookResultEvent): string { + return `*${formatHookResultTitle(event)}*\n\n${formatHookResultBody(event)}`; +} + +function formatHookResultPlain(event: HookResultEvent): string { + return `${formatHookResultTitle(event)}\n\n${formatHookResultBody(event)}`; +} + +function formatHookResultTitle(event: HookResultEvent): string { + return `${event.hookEvent} hook${event.blocked === true ? ' blocked' : ''}`; +} + +function formatHookResultBody(event: HookResultEvent): string { + const content = event.content.trim(); + return content.length === 0 ? '(empty)' : content; +} diff --git a/apps/kimi-code/src/tui/reverse-rpc/approval/adapter.ts b/apps/kimi-code/src/tui/reverse-rpc/approval/adapter.ts new file mode 100644 index 000000000..c53cce753 --- /dev/null +++ b/apps/kimi-code/src/tui/reverse-rpc/approval/adapter.ts @@ -0,0 +1,339 @@ +import type { ApprovalRequest, ApprovalResponse, ToolInputDisplay } from '@moonshot-ai/kimi-code-sdk'; + +import type { ApprovalPanelResponse } from '#/tui/components/dialogs/approval-panel'; +import type { ApprovalPanelChoice, ApprovalPanelData, DisplayBlock } from '#/tui/reverse-rpc/types'; + +const DEFAULT_APPROVAL_CHOICES: ApprovalPanelChoice[] = [ + { label: 'Approve once', response: 'approved' }, + { label: 'Approve for this session', response: 'approved_for_session' }, + { label: 'Reject', response: 'rejected' }, + { label: 'Reject with feedback', response: 'rejected', requires_feedback: true }, +]; + +const PLAN_REJECT_CHOICES: ApprovalPanelChoice[] = [ + { label: 'Reject', response: 'rejected', selected_label: 'Reject' }, + { label: 'Revise', response: 'rejected', selected_label: 'Revise', requires_feedback: true }, +]; + +export function adaptApprovalRequest(event: ApprovalRequest): ApprovalPanelData { + const resolved = resolveDisplay(event.toolName, event.display, event.action); + return { + id: event.toolCallId, + tool_call_id: event.toolCallId, + tool_name: event.toolName, + action: event.action, + description: resolved.description, + display: resolved.blocks, + choices: adaptChoices(event.toolName, event.display), + }; +} + +interface ResolvedDisplay { + blocks: DisplayBlock[]; + description: string; +} + +function resolveDisplay( + toolName: string, + display: ToolInputDisplay, + action: string, +): ResolvedDisplay { + if (display.kind === 'generic' && isRecord(display.detail)) { + const extracted = extractFromArgs(toolName, display.detail); + if (extracted !== null) return extracted; + } + return { + blocks: adaptDisplay(display), + description: describeApproval(display, action), + }; +} + +function isRecord(value: unknown): value is Record<string, unknown> { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + +function stringField(detail: Record<string, unknown>, key: string): string | undefined { + const value = detail[key]; + return typeof value === 'string' ? value : undefined; +} + +function extractFromArgs( + toolName: string, + detail: Record<string, unknown>, +): ResolvedDisplay | null { + const command = stringField(detail, 'command'); + if (command !== undefined) { + const cwd = stringField(detail, 'cwd'); + const toolDescription = stringField(detail, 'description'); + const danger = detectDanger(command); + const language = stringField(detail, 'language') ?? 'bash'; + return { + blocks: [ + { + type: 'shell', + language, + command, + cwd, + description: toolDescription, + danger, + }, + ], + description: toolDescription ?? '', + }; + } + + const oldString = stringField(detail, 'old_string'); + const newString = stringField(detail, 'new_string'); + if (oldString !== undefined && newString !== undefined) { + const path = stringField(detail, 'file_path') ?? stringField(detail, 'path') ?? ''; + // Diff block carries its own `+N -M path` header — no separate + // file_op title row needed. + return { + blocks: [{ type: 'diff', path, old_text: oldString, new_text: newString }], + description: '', + }; + } + + const filePath = stringField(detail, 'file_path') ?? stringField(detail, 'path'); + const content = stringField(detail, 'content'); + if (filePath !== undefined && content !== undefined) { + // Write is a brand-new file: render the content as a syntax- + // highlighted code block, not a diff full of `+` markers. + return { + blocks: [{ type: 'file_content', path: filePath, content }], + description: '', + }; + } + + const url = stringField(detail, 'url'); + if (url !== undefined) { + const method = stringField(detail, 'method'); + return { + blocks: [{ type: 'url_fetch', url, method }], + description: '', + }; + } + + const query = stringField(detail, 'query'); + if (query !== undefined) { + return { + blocks: [{ type: 'search', query }], + description: '', + }; + } + + const pattern = stringField(detail, 'pattern'); + if (pattern !== undefined) { + const scope = stringField(detail, 'path'); + return { + blocks: [{ type: 'search', query: pattern, scope }], + description: '', + }; + } + + if (filePath !== undefined) { + const operation = inferFileOp(toolName); + return { + blocks: [{ type: 'file_op', operation, path: filePath }], + description: '', + }; + } + + return null; +} + +function inferFileOp(toolName: string): 'read' | 'write' | 'edit' | 'glob' | 'grep' { + const lower = toolName.toLowerCase(); + if (lower.includes('glob')) return 'glob'; + if (lower.includes('grep')) return 'grep'; + if (lower.includes('edit')) return 'edit'; + if (lower.includes('write')) return 'write'; + return 'read'; +} + +export function adaptPanelResponse(response: ApprovalPanelResponse): ApprovalResponse { + if (response.response === 'approved_for_session') { + return { + decision: 'approved', + scope: 'session', + feedback: response.feedback, + selectedLabel: response.selected_label, + }; + } + return { + decision: + response.response === 'approved' + ? 'approved' + : response.response === 'rejected' + ? 'rejected' + : 'cancelled', + feedback: response.feedback, + selectedLabel: response.selected_label, + }; +} + +function describeApproval(display: ToolInputDisplay, action: string): string { + switch (display.kind) { + case 'plan_review': + return ''; + case 'generic': + if (typeof display.detail === 'string' && display.detail.length > 0) { + return display.detail; + } + return display.summary ?? action; + case 'command': + return display.description ?? display.command ?? action; + case 'diff': + return `edit ${display.path ?? ''}`.trim(); + case 'file_io': + return `${display.operation ?? 'file'} ${display.path ?? ''}`.trim(); + case 'task_stop': + return `stop task: ${display.task_description ?? display.task_id ?? ''}`.trim(); + case 'agent_call': + return `spawn ${display.agent_name ?? 'agent'}`; + case 'skill_call': + return `invoke skill ${display.skill_name ?? ''}`.trim(); + case 'url_fetch': + return `fetch ${display.url ?? ''}`.trim(); + case 'search': + return `search: ${display.query ?? ''}`.trim(); + case 'todo_list': + return `update todo list (${String(display.items?.length ?? 0)} items)`; + case 'background_task': + return `${display.status ?? 'background'} task ${display.task_id ?? ''}: ${ + display.description ?? '' + }`.trim(); + default: + return action; + } +} + +const DANGER_PATTERNS: Array<{ pattern: RegExp; label: string }> = [ + { pattern: /\brm\s+(-[a-zA-Z]*[rRfF][a-zA-Z]*|--recursive|--force)/i, label: 'recursive delete' }, + { pattern: /\bsudo\b/i, label: 'sudo' }, + { pattern: /\b(curl|wget)\b[^|]*\|\s*(sh|bash|zsh)\b/i, label: 'pipe to shell' }, + { pattern: /\bdd\b[^|]*\bof=/i, label: 'dd write' }, + { pattern: /\bmkfs\b/i, label: 'mkfs' }, + { pattern: />\s*\/dev\/(sd|nvme|disk|hd)/i, label: 'write to raw device' }, + { pattern: /\bchmod\s+-R?\s*777\b/i, label: 'chmod 777' }, + { pattern: /:\(\)\s*\{\s*:\|:&\s*\}/i, label: 'fork bomb' }, +]; + +function detectDanger(command: string): string | undefined { + for (const { pattern, label } of DANGER_PATTERNS) { + if (pattern.test(command)) return label; + } + return undefined; +} + +function adaptDisplay(display: ToolInputDisplay): DisplayBlock[] { + switch (display.kind) { + case 'command': { + const command = display.command ?? ''; + const danger = detectDanger(command); + return [ + { + type: 'shell', + language: display.language ?? 'bash', + command, + cwd: display.cwd, + description: display.description, + danger, + }, + ]; + } + case 'diff': + return [ + { + type: 'diff', + path: display.path ?? '', + old_text: display.before ?? '', + new_text: display.after ?? '', + }, + ]; + case 'file_io': + return [ + { + type: 'file_op', + operation: display.operation, + path: display.path ?? '', + detail: display.detail, + }, + ]; + case 'url_fetch': + return [ + { + type: 'url_fetch', + url: display.url ?? '', + method: display.method, + }, + ]; + case 'search': + return [ + { + type: 'search', + query: display.query ?? '', + scope: display.scope, + }, + ]; + case 'agent_call': + return [ + { + type: 'invocation', + kind: 'agent', + name: display.agent_name ?? '', + description: display.prompt, + }, + ]; + case 'skill_call': + return [ + { + type: 'invocation', + kind: 'skill', + name: display.skill_name ?? '', + description: display.args, + }, + ]; + case 'task_stop': + return [ + { + type: 'brief', + text: `Stop task ${display.task_id ?? ''}: ${display.task_description ?? ''}`, + }, + ]; + case 'plan_review': + return []; + case 'generic': + return []; + case 'todo_list': + return []; + case 'background_task': + return []; + default: + return []; + } +} + +function adaptChoices(toolName: string, display: ToolInputDisplay): ApprovalPanelChoice[] { + if (toolName === 'ExitPlanMode' || display.kind === 'plan_review') { + return adaptPlanReviewChoices(display); + } + + return DEFAULT_APPROVAL_CHOICES.map((choice) => cloneChoice(choice)); +} + +function adaptPlanReviewChoices(display: ToolInputDisplay): ApprovalPanelChoice[] { + const optionChoices = + display.kind === 'plan_review' && display.options !== undefined && display.options.length >= 2 + ? display.options.map((option) => ({ + label: option.label, + response: 'approved' as const, + selected_label: option.label, + })) + : [{ label: 'Approve', response: 'approved' as const, selected_label: 'Approve' }]; + return [...optionChoices, ...PLAN_REJECT_CHOICES].map((choice) => cloneChoice(choice)); +} + +function cloneChoice(choice: ApprovalPanelChoice): ApprovalPanelChoice { + return { ...choice }; +} diff --git a/apps/kimi-code/src/tui/reverse-rpc/approval/controller.ts b/apps/kimi-code/src/tui/reverse-rpc/approval/controller.ts new file mode 100644 index 000000000..5578a5189 --- /dev/null +++ b/apps/kimi-code/src/tui/reverse-rpc/approval/controller.ts @@ -0,0 +1,27 @@ +import type { ApprovalResponse } from '@moonshot-ai/kimi-code-sdk'; + +import { ReverseRpcController } from '#/tui/reverse-rpc/base-controller'; +import type { ApprovalPanelData } from '#/tui/reverse-rpc/types'; + +export class ApprovalController extends ReverseRpcController< + ApprovalPanelData, + ApprovalResponse +> { + protected createCancelResponse(reason: string): ApprovalResponse { + return { decision: 'cancelled', feedback: reason }; + } + + protected override autoResolveFor( + resolvedPayload: ApprovalPanelData, + response: ApprovalResponse, + queuedPayload: ApprovalPanelData, + ): ApprovalResponse | undefined { + if (response.decision !== 'approved') return undefined; + if (response.scope !== 'session') return undefined; + if (resolvedPayload.action !== queuedPayload.action) return undefined; + // Inherit the session-scoped approval. Drop `feedback` and + // `selectedLabel` — those described the user's interaction with the + // first request only and would be misleading on auto-resolved ones. + return { decision: 'approved', scope: 'session' }; + } +} diff --git a/apps/kimi-code/src/tui/reverse-rpc/approval/handler.ts b/apps/kimi-code/src/tui/reverse-rpc/approval/handler.ts new file mode 100644 index 000000000..8640dd5d9 --- /dev/null +++ b/apps/kimi-code/src/tui/reverse-rpc/approval/handler.ts @@ -0,0 +1,24 @@ +import type { ApprovalHandler, ApprovalRequest, ApprovalResponse } from '@moonshot-ai/kimi-code-sdk'; + +import { adaptApprovalRequest } from './adapter'; +import type { ApprovalController } from './controller'; + +export function createApprovalRequestHandler( + controller: ApprovalController, + onResponse?: (request: ApprovalRequest, response: ApprovalResponse) => void, +): ApprovalHandler { + return async (event): Promise<ApprovalResponse> => { + try { + const response = await controller.show(adaptApprovalRequest(event)); + onResponse?.(event, response); + return response; + } catch { + const response: ApprovalResponse = { + decision: 'cancelled', + feedback: 'approval handler failed', + }; + onResponse?.(event, response); + return response; + } + }; +} diff --git a/apps/kimi-code/src/tui/reverse-rpc/base-controller.ts b/apps/kimi-code/src/tui/reverse-rpc/base-controller.ts new file mode 100644 index 000000000..a32dcb047 --- /dev/null +++ b/apps/kimi-code/src/tui/reverse-rpc/base-controller.ts @@ -0,0 +1,111 @@ +/** + * Base class for promise-based reverse RPC dialog controllers. + * + * Approval and question flows wait for a UI action before returning a response. + * Subclasses only need to define the default cancellation response. + * + * When concurrent requests arrive (e.g. multiple parallel subagents each + * needing approval), only one panel is shown at a time; additional requests + * are queued in arrival order and advance after the current one resolves. + */ + +export interface ReverseRpcUIHooks<TPayload> { + showPanel(payload: TPayload): void; + hidePanel(): void; +} + +interface Pending<TPayload, TResponse> { + readonly payload: TPayload; + readonly resolve: (data: TResponse) => void; +} + +export abstract class ReverseRpcController<TPayload, TResponse> { + private uiHooks: ReverseRpcUIHooks<TPayload> | null = null; + private current: Pending<TPayload, TResponse> | null = null; + private queue: Array<Pending<TPayload, TResponse>> = []; + + setUIHooks(hooks: ReverseRpcUIHooks<TPayload>): void { + this.uiHooks = hooks; + } + + /** + * Called when a reverse RPC request arrives from core. The returned promise + * resolves after the user responds or `cancelAll` forces cancellation. + */ + show(payload: TPayload): Promise<TResponse> { + return new Promise<TResponse>((resolve) => { + const entry: Pending<TPayload, TResponse> = { payload, resolve }; + if (this.current === null) { + this.current = entry; + this.uiHooks?.showPanel(payload); + } else { + this.queue.push(entry); + } + }); + } + + /** Called by the UI after the user makes a panel choice. */ + respond(data: TResponse): void { + const pending = this.current; + this.current = null; + pending?.resolve(data); + if (pending !== null) { + this.drainAutoResolved(pending.payload, data); + } + this.advanceOrHide(); + } + + /** Cancels all pending requests during shutdown or session switches. */ + cancelAll(reason: string): void { + const all = [...(this.current === null ? [] : [this.current]), ...this.queue]; + this.current = null; + this.queue = []; + this.uiHooks?.hidePanel(); + for (const entry of all) { + entry.resolve(this.createCancelResponse(reason)); + } + } + + hasPending(): boolean { + return this.current !== null || this.queue.length > 0; + } + + private advanceOrHide(): void { + const next = this.queue.shift(); + if (next === undefined) { + this.uiHooks?.hidePanel(); + return; + } + this.current = next; + this.uiHooks?.showPanel(next.payload); + } + + private drainAutoResolved(resolvedPayload: TPayload, response: TResponse): void { + const remaining: Array<Pending<TPayload, TResponse>> = []; + for (const entry of this.queue) { + const auto = this.autoResolveFor(resolvedPayload, response, entry.payload); + if (auto === undefined) { + remaining.push(entry); + } else { + entry.resolve(auto); + } + } + this.queue = remaining; + } + + /** + * Subclasses override to short-circuit queued requests when an answer to the + * just-resolved one (e.g. an approve-for-session) implies the same answer + * for matching queued requests. Return `undefined` to leave the queued + * request waiting for its own panel turn. + */ + protected autoResolveFor( + _resolvedPayload: TPayload, + _response: TResponse, + _queuedPayload: TPayload, + ): TResponse | undefined { + return undefined; + } + + protected abstract createCancelResponse(reason: string): TResponse; +} diff --git a/apps/kimi-code/src/tui/reverse-rpc/index.ts b/apps/kimi-code/src/tui/reverse-rpc/index.ts new file mode 100644 index 000000000..3a00ae889 --- /dev/null +++ b/apps/kimi-code/src/tui/reverse-rpc/index.ts @@ -0,0 +1,44 @@ +import type { ApprovalController } from './approval/controller'; +import type { QuestionController } from './question/controller'; +import { ReverseRpcModalCoordinator } from './modal-coordinator'; +import type { ApprovalPanelData, QuestionPanelData } from './types'; + +export interface ReverseRPCUIHooks { + readonly showApprovalPanel: (payload: ApprovalPanelData) => void; + readonly hideApprovalPanel: () => void; + readonly showQuestionDialog: (payload: QuestionPanelData) => void; + readonly hideQuestionDialog: () => void; +} + +export function registerReverseRPCHandlers( + approvalController: ApprovalController, + questionController: QuestionController, + uiHooks: ReverseRPCUIHooks, +): Array<() => void> { + const modalCoordinator = new ReverseRpcModalCoordinator(uiHooks); + + // Setup UI hooks for controllers + approvalController.setUIHooks({ + showPanel: (payload) => { + modalCoordinator.showApproval(payload); + }, + hidePanel: () => { + modalCoordinator.hide('approval'); + }, + }); + + questionController.setUIHooks({ + showPanel: (payload) => { + modalCoordinator.showQuestion(payload); + }, + hidePanel: () => { + modalCoordinator.hide('question'); + }, + }); + + return [ + () => { + modalCoordinator.clear(); + }, + ]; +} diff --git a/apps/kimi-code/src/tui/reverse-rpc/modal-coordinator.ts b/apps/kimi-code/src/tui/reverse-rpc/modal-coordinator.ts new file mode 100644 index 000000000..623ec761a --- /dev/null +++ b/apps/kimi-code/src/tui/reverse-rpc/modal-coordinator.ts @@ -0,0 +1,97 @@ +import type { ApprovalPanelData, QuestionPanelData } from './types'; + +export type ReverseRpcModalOwner = 'approval' | 'question'; + +export interface ReverseRpcModalUIHooks { + readonly showApprovalPanel: (payload: ApprovalPanelData) => void; + readonly hideApprovalPanel: () => void; + readonly showQuestionDialog: (payload: QuestionPanelData) => void; + readonly hideQuestionDialog: () => void; +} + +interface ReverseRpcModalEntry { + readonly owner: ReverseRpcModalOwner; + readonly show: () => void; + readonly hide: () => void; +} + +export class ReverseRpcModalCoordinator { + private active: ReverseRpcModalEntry | null = null; + private readonly queued: ReverseRpcModalEntry[] = []; + + constructor(private readonly hooks: ReverseRpcModalUIHooks) {} + + showApproval(payload: ApprovalPanelData): void { + this.show({ + owner: 'approval', + show: () => { + this.hooks.showApprovalPanel(payload); + }, + hide: () => { + this.hooks.hideApprovalPanel(); + }, + }); + } + + showQuestion(payload: QuestionPanelData): void { + this.show({ + owner: 'question', + show: () => { + this.hooks.showQuestionDialog(payload); + }, + hide: () => { + this.hooks.hideQuestionDialog(); + }, + }); + } + + hide(owner: ReverseRpcModalOwner): void { + if (this.active?.owner === owner) { + const active = this.active; + this.active = null; + active.hide(); + this.showNext(); + return; + } + + const queuedIndex = this.queued.findIndex((entry) => entry.owner === owner); + if (queuedIndex >= 0) this.queued.splice(queuedIndex, 1); + } + + clear(): void { + const active = this.active; + this.active = null; + this.queued.length = 0; + active?.hide(); + } + + private show(entry: ReverseRpcModalEntry): void { + const active = this.active; + if (active === null) { + this.active = entry; + entry.show(); + return; + } + + if (active.owner === entry.owner) { + this.active = entry; + entry.show(); + return; + } + + const queuedIndex = this.queued.findIndex((queued) => queued.owner === entry.owner); + if (queuedIndex >= 0) { + this.queued[queuedIndex] = entry; + return; + } + + this.queued.push(entry); + } + + private showNext(): void { + const next = this.queued.shift(); + if (next === undefined) return; + this.active = next; + next.show(); + } +} diff --git a/apps/kimi-code/src/tui/reverse-rpc/question/controller.ts b/apps/kimi-code/src/tui/reverse-rpc/question/controller.ts new file mode 100644 index 000000000..a87c24daf --- /dev/null +++ b/apps/kimi-code/src/tui/reverse-rpc/question/controller.ts @@ -0,0 +1,11 @@ +import { ReverseRpcController } from '#/tui/reverse-rpc/base-controller'; +import type { QuestionPanelData, QuestionPanelResponse } from '#/tui/reverse-rpc/types'; + +export class QuestionController extends ReverseRpcController< + QuestionPanelData, + QuestionPanelResponse +> { + protected createCancelResponse(_reason: string): QuestionPanelResponse { + return { answers: [] }; + } +} diff --git a/apps/kimi-code/src/tui/reverse-rpc/question/handler.ts b/apps/kimi-code/src/tui/reverse-rpc/question/handler.ts new file mode 100644 index 000000000..3bf564b02 --- /dev/null +++ b/apps/kimi-code/src/tui/reverse-rpc/question/handler.ts @@ -0,0 +1,57 @@ +import type { QuestionHandler, QuestionRequest, QuestionResult } from '@moonshot-ai/kimi-code-sdk'; + +import type { + QuestionPanelData, + QuestionPanelResponse, +} from '#/tui/reverse-rpc/types'; + +import type { QuestionController } from './controller'; + +export function createQuestionAskHandler(controller: QuestionController): QuestionHandler { + return async (event): Promise<QuestionResult> => { + try { + const answers = await controller.show(adaptQuestionRequest(event)); + return adaptQuestionAnswers(event, answers); + } catch { + return null; + } + }; +} + +export function adaptQuestionRequest(event: QuestionRequest): QuestionPanelData { + const id = + event.toolCallId ?? + (event.turnId === undefined ? 'question' : `question-${String(event.turnId)}`); + return { + id, + tool_call_id: id, + questions: event.questions.map((question) => ({ + question: question.question, + header: question.header, + body: question.body, + multi_select: question.multiSelect ?? false, + other_label: question.otherLabel, + other_description: question.otherDescription, + options: question.options.map((option) => ({ + label: option.label, + description: option.description, + })), + })), + }; +} + +export function adaptQuestionAnswers( + event: QuestionRequest, + response: QuestionPanelResponse, +): QuestionResult { + const result: Record<string, string | true> = {}; + for (let i = 0; i < event.questions.length; i++) { + const question = event.questions[i]; + const answer = response.answers[i]; + if (question === undefined || typeof answer !== 'string' || answer.length === 0) continue; + result[question.question] = answer; + } + return Object.keys(result).length > 0 + ? { answers: result, method: response.method } + : null; +} diff --git a/apps/kimi-code/src/tui/reverse-rpc/types.ts b/apps/kimi-code/src/tui/reverse-rpc/types.ts new file mode 100644 index 000000000..c23c938f0 --- /dev/null +++ b/apps/kimi-code/src/tui/reverse-rpc/types.ts @@ -0,0 +1,151 @@ +/** + * Reverse RPC view-layer types. + * + * These types are the contract between the UI layer and reverse RPC + * controllers, not SDK event payloads. Approval and question adapters convert + * core payloads into these shapes for panel components. + */ + +import type { QuestionAnswerMethod } from '@moonshot-ai/kimi-code-sdk'; + +// ── Display blocks (approval panel) ────────────────────────────────── + +export interface BriefDisplayBlock { + type: 'brief'; + text: string; +} + +export interface DiffDisplayBlock { + type: 'diff'; + path: string; + old_text: string; + new_text: string; + old_start?: number | undefined; + new_start?: number | undefined; + is_summary?: boolean | undefined; +} + +export interface ShellDisplayBlock { + type: 'shell'; + language: string; + command: string; + cwd?: string | undefined; + description?: string | undefined; + danger?: string | undefined; +} + +export interface FileOpDisplayBlock { + type: 'file_op'; + operation: 'read' | 'write' | 'edit' | 'glob' | 'grep'; + path: string; + detail?: string | undefined; +} + +/** Full file content preview for Write — a code block, not a diff. */ +export interface FileContentDisplayBlock { + type: 'file_content'; + path: string; + content: string; + language?: string | undefined; +} + +export interface UrlFetchDisplayBlock { + type: 'url_fetch'; + url: string; + method?: string | undefined; +} + +export interface SearchDisplayBlock { + type: 'search'; + query: string; + scope?: string | undefined; +} + +export interface InvocationDisplayBlock { + type: 'invocation'; + kind: 'agent' | 'skill'; + name: string; + description?: string | undefined; +} + +export interface TodoDisplayItem { + title: string; + status: 'pending' | 'in_progress' | 'done'; +} + +export interface TodoDisplayBlock { + type: 'todo'; + items: TodoDisplayItem[]; +} + +export interface BackgroundTaskDisplayBlock { + type: 'background_task'; + task_id: string; + kind: string; + status: string; + description: string; +} + +export type DisplayBlock = + | BriefDisplayBlock + | DiffDisplayBlock + | ShellDisplayBlock + | FileOpDisplayBlock + | FileContentDisplayBlock + | UrlFetchDisplayBlock + | SearchDisplayBlock + | InvocationDisplayBlock + | TodoDisplayBlock + | BackgroundTaskDisplayBlock; + +export interface ApprovalPanelChoice { + label: string; + response: 'approved' | 'approved_for_session' | 'rejected' | 'cancelled'; + selected_label?: string | undefined; + requires_feedback?: boolean | undefined; +} + +// ── Approval / Question view payloads ──────────────────────────────── + +export interface ApprovalPanelData { + id: string; + tool_call_id: string; + tool_name: string; + action: string; + description: string; + display: DisplayBlock[]; + choices: ApprovalPanelChoice[]; +} + +export interface QuestionPanelItem { + question: string; + header?: string; + body?: string; + multi_select: boolean; + other_label?: string; + other_description?: string; + options: Array<{ label: string; description?: string }>; +} + +export interface QuestionPanelData { + id: string; + tool_call_id: string; + questions: QuestionPanelItem[]; +} + +export type QuestionSubmissionMethod = QuestionAnswerMethod; + +export interface QuestionPanelResponse { + readonly answers: string[]; + readonly method?: QuestionSubmissionMethod | undefined; +} + +// ── Pending state wrappers ─────────────────────────────────────────── + +export interface PendingApproval { + data: ApprovalPanelData; +} + +export interface PendingQuestion { + data: QuestionPanelData; +} diff --git a/apps/kimi-code/src/tui/theme/bundle.ts b/apps/kimi-code/src/tui/theme/bundle.ts new file mode 100644 index 000000000..8cfd0e921 --- /dev/null +++ b/apps/kimi-code/src/tui/theme/bundle.ts @@ -0,0 +1,27 @@ +import type { MarkdownTheme } from '@earendil-works/pi-tui'; + +import { getColorPalette, type ColorPalette, type ResolvedTheme } from './colors'; +import { createMarkdownTheme } from './pi-tui-theme'; +import { createThemeStyles, type ThemeStyles } from './styles'; +import { resolveThemeSync, type Theme } from './index'; + +export interface KimiTUIThemeBundle { + resolvedTheme: ResolvedTheme; + colors: ColorPalette; + styles: ThemeStyles; + markdownTheme: MarkdownTheme; +} + +export function createKimiTUIThemeBundle( + theme: Theme, + resolvedTheme?: ResolvedTheme, +): KimiTUIThemeBundle { + const actualTheme = resolvedTheme ?? resolveThemeSync(theme); + const colors = { ...getColorPalette(actualTheme) }; + return { + resolvedTheme: actualTheme, + colors, + styles: createThemeStyles(colors), + markdownTheme: createMarkdownTheme(colors), + }; +} diff --git a/apps/kimi-code/src/tui/theme/colors.ts b/apps/kimi-code/src/tui/theme/colors.ts new file mode 100644 index 000000000..4a9b37bad --- /dev/null +++ b/apps/kimi-code/src/tui/theme/colors.ts @@ -0,0 +1,148 @@ +/** + * Color palette definitions for dark and light themes. + * + * Two layers: + * - private `dark` / `light` raw palettes — unsemantic constants reused + * across multiple semantic tokens to avoid hex literal duplication. + * - exported `darkColors` / `lightColors` — the semantic `ColorPalette` + * consumed by every UI component via chalk.hex(...). + * + * Light palette values are tuned for ≥ 4.5:1 contrast against #FFFFFF + * for text tokens and ≥ 3:1 for chrome (border / large text), matching + * WCAG AA. + */ + +const dark = { + blue400: '#4FA8FF', + cyan400: '#5BC0BE', + gray50: '#F5F5F5', + gray100: '#E0E0E0', + gray500: '#888888', + gray600: '#6B6B6B', + gray700: '#5A5A5A', + green400: '#4EC87E', + green300: '#7AD99B', + red400: '#E85454', + red300: '#F08585', + amber400: '#E8A838', + orange300: '#FFCB6B', +} as const; + +const light = { + blue600: '#1565C0', + cyan700: '#00838F', + gray900: '#1A1A1A', + gray700: '#454545', + gray600: '#5F5F5F', + gray500: '#737373', + green700: '#0E7A38', + red700: '#B91C1C', + amber800: '#92660A', + orange700: '#9A4A00', +} as const; + +export interface ColorPalette { + // Brand + primary: string; + accent: string; + + // Text + text: string; + textStrong: string; + textDim: string; + textMuted: string; + + // Surface + border: string; + borderFocus: string; + + // State + success: string; + warning: string; + error: string; + + // Diff + diffAdded: string; + diffRemoved: string; + diffAddedStrong: string; + diffRemovedStrong: string; + diffGutter: string; + diffMeta: string; + + // Roles + roleUser: string; + roleAssistant: string; + roleThinking: string; + roleTool: string; + + // Status + status: string; +} + +export const darkColors: ColorPalette = { + primary: dark.blue400, + accent: dark.cyan400, + + text: dark.gray100, + textStrong: dark.gray50, + textDim: dark.gray500, + textMuted: dark.gray600, + + border: dark.gray700, + borderFocus: dark.amber400, + + success: dark.green400, + warning: dark.amber400, + error: dark.red400, + + diffAdded: dark.green400, + diffRemoved: dark.red400, + diffAddedStrong: dark.green300, + diffRemovedStrong: dark.red300, + diffGutter: dark.gray600, + diffMeta: dark.gray500, + + roleUser: dark.orange300, + roleAssistant: dark.gray100, + roleThinking: dark.gray500, + roleTool: dark.amber400, + + status: dark.gray500, +}; + +export const lightColors: ColorPalette = { + primary: light.blue600, + accent: light.cyan700, + + text: light.gray900, + textStrong: light.gray900, + textDim: light.gray700, + textMuted: light.gray600, + + border: light.gray500, + borderFocus: light.amber800, + + success: light.green700, + warning: light.amber800, + error: light.red700, + + diffAdded: light.green700, + diffRemoved: light.red700, + diffAddedStrong: light.green700, + diffRemovedStrong: light.red700, + diffGutter: light.gray500, + diffMeta: light.gray600, + + roleUser: light.orange700, + roleAssistant: light.gray900, + roleThinking: light.gray700, + roleTool: light.amber800, + + status: light.gray700, +}; + +export type ResolvedTheme = 'dark' | 'light'; + +export function getColorPalette(theme: ResolvedTheme): ColorPalette { + return theme === 'dark' ? darkColors : lightColors; +} diff --git a/apps/kimi-code/src/tui/theme/detect.ts b/apps/kimi-code/src/tui/theme/detect.ts new file mode 100644 index 000000000..0f4d40953 --- /dev/null +++ b/apps/kimi-code/src/tui/theme/detect.ts @@ -0,0 +1,121 @@ +/** + * Terminal background detection. + * + * Strategy, in priority order: + * 1. Reject — non-TTY, NO_COLOR, FORCE_COLOR=0, CI → safe `'dark'`. + * 2. OSC 11 — write `ESC ] 11 ; ? BEL`, parse `ESC ] 11 ; rgb:RR/GG/BB BEL`, + * compute relative luminance. Capped at `timeoutMs` so unsupported + * terminals don't hang. + * 3. COLORFGBG — VT100 / xterm fallback exposing `"fg;bg"`. + * 4. Default — `'dark'`. + * + * Must run before pi-tui enters raw mode; once the framework owns stdin + * the OSC reply gets eaten by the input loop. + */ + +import { OSC11_QUERY, TERMINAL_THEME_DETECT_TIMEOUT_MS } from "#/tui/constant/terminal"; + +import type { ResolvedTheme } from "./colors"; +import { parseOsc11BackgroundTheme } from "./terminal-background"; + +export interface DetectOptions { + readonly timeoutMs?: number; +} + +export async function detectTerminalTheme(opts: DetectOptions = {}): Promise<ResolvedTheme> { + if (!isInteractiveTerminal()) return "dark"; + if (isColorOptOut()) return "dark"; + + const fromOsc = await queryOsc11({ + timeoutMs: opts.timeoutMs ?? TERMINAL_THEME_DETECT_TIMEOUT_MS, + }); + if (fromOsc !== null) return fromOsc; + + const fromColorFgBg = parseColorFgBg(process.env["COLORFGBG"]); + if (fromColorFgBg !== null) return fromColorFgBg; + + return "dark"; +} + +function isInteractiveTerminal(): boolean { + return (process.stdin.isTTY ?? false) && (process.stdout.isTTY ?? false); +} + +function isColorOptOut(): boolean { + const env = process.env; + if (env["NO_COLOR"] !== undefined && env["NO_COLOR"] !== "") return true; + if (env["FORCE_COLOR"] === "0") return true; + if (env["CI"] !== undefined && env["CI"] !== "" && env["CI"] !== "0") return true; + return false; +} + +interface RawModeStdin { + isRaw?: boolean; + setRawMode(mode: boolean): NodeJS.ReadStream; + on(event: "data", listener: (data: Buffer) => void): NodeJS.ReadStream; + off(event: "data", listener: (data: Buffer) => void): NodeJS.ReadStream; +} + +async function queryOsc11(opts: { timeoutMs: number }): Promise<ResolvedTheme | null> { + const stdin = process.stdin as unknown as RawModeStdin; + if (typeof stdin.setRawMode !== "function") return null; + // If something else is already listening on stdin (e.g. another raw-mode + // consumer), don't fight for it — punt to COLORFGBG instead. + if (process.stdin.listenerCount("data") > 0) return null; + + const wasRaw = stdin.isRaw === true; + let buffer = ""; + let listener: ((data: Buffer) => void) | null = null; + let timer: NodeJS.Timeout | null = null; + + try { + if (!wasRaw) stdin.setRawMode(true); + + const result = await new Promise<ResolvedTheme | null>((resolve) => { + listener = (chunk: Buffer): void => { + buffer += chunk.toString("utf8"); + const theme = parseOsc11BackgroundTheme(buffer); + if (theme !== null) resolve(theme); + }; + stdin.on("data", listener); + timer = setTimeout(() => { + resolve(null); + }, opts.timeoutMs); + try { + process.stdout.write(OSC11_QUERY); + } catch { + resolve(null); + } + }); + + return result; + } catch { + return null; + } finally { + if (timer !== null) clearTimeout(timer); + if (listener !== null) stdin.off("data", listener); + if (!wasRaw) { + try { + stdin.setRawMode(false); + } catch { + /* ignore — raw mode restoration best-effort */ + } + } + } +} + +/** + * COLORFGBG is `"fg;bg"` (sometimes `"fg;default;bg"`). The last token is + * the background ANSI 16-color index; 0–6 and 8 are dark, the rest light. + */ +export function parseColorFgBg(value: string | undefined): ResolvedTheme | null { + if (value === undefined || value === "") return null; + const parts = value.split(";"); + const bgRaw = parts.at(-1); + if (bgRaw === undefined) return null; + const bg = parseInt(bgRaw, 10); + if (!Number.isInteger(bg)) return null; + // ANSI 0=black, 1=red, 2=green, 3=yellow, 4=blue, 5=magenta, 6=cyan, 8=bright black. + const darkBgs = new Set([0, 1, 2, 3, 4, 5, 6, 8]); + return darkBgs.has(bg) ? "dark" : "light"; +} diff --git a/apps/kimi-code/src/tui/theme/index.ts b/apps/kimi-code/src/tui/theme/index.ts new file mode 100644 index 000000000..a9a143633 --- /dev/null +++ b/apps/kimi-code/src/tui/theme/index.ts @@ -0,0 +1,46 @@ +/** + * Theme system public API. + */ + +import type { ResolvedTheme } from './colors'; +import { detectTerminalTheme } from './detect'; + +export { darkColors, lightColors, getColorPalette } from './colors'; +export type { ColorPalette, ResolvedTheme } from './colors'; +export { createThemeStyles } from './styles'; +export type { ThemeStyles } from './styles'; +export { createMarkdownTheme, createEditorTheme } from './pi-tui-theme'; +export { detectTerminalTheme } from './detect'; + +/** + * User-facing theme preference. `'auto'` defers to terminal background + * detection at startup; `'dark'` / `'light'` are explicit overrides that + * never trigger detection. The persisted value in `tui.toml` is always + * one of these three; the detected `ResolvedTheme` is computed at + * startup and held only in memory. + */ +export type Theme = 'dark' | 'light' | 'auto'; + +export function isTheme(value: string): value is Theme { + return value === 'dark' || value === 'light' || value === 'auto'; +} + +/** + * Resolve a user preference to a concrete palette key. `'auto'` triggers + * terminal background detection (OSC 11 with COLORFGBG / dark fallback); + * explicit choices pass through. + */ +export async function resolveTheme(theme: Theme): Promise<ResolvedTheme> { + if (theme === 'auto') return detectTerminalTheme(); + return theme; +} + +/** + * Synchronous fallback used by paths that cannot wait on terminal probes + * (initial state construction, in-TUI theme switches). `'auto'` collapses + * to `'dark'`; explicit choices pass through. + */ +export function resolveThemeSync(theme: Theme): ResolvedTheme { + if (theme === 'auto') return 'dark'; + return theme; +} diff --git a/apps/kimi-code/src/tui/theme/pi-tui-theme.ts b/apps/kimi-code/src/tui/theme/pi-tui-theme.ts new file mode 100644 index 000000000..dc3b1b9ad --- /dev/null +++ b/apps/kimi-code/src/tui/theme/pi-tui-theme.ts @@ -0,0 +1,71 @@ +/** + * Pi-tui theme adapters — MarkdownTheme and EditorTheme from our ColorPalette. + * + * All chalk calls route through `ColorPalette` tokens so themes flip + * cleanly. No raw `chalk.gray` / `chalk.dim` / `chalk.white` here. + */ + +import type { MarkdownTheme, EditorTheme } from '@earendil-works/pi-tui'; +import chalk from 'chalk'; +import { highlight, supportsLanguage } from 'cli-highlight'; + +import type { ColorPalette } from './colors'; + +// pi-tui's renderer emits literal "### " / "#### " / ... markers for h3-h6 +// headings (h1/h2 are rendered without the `#` prefix). The prefix arrives +// here already wrapped in bold SGR codes, so we strip it — after any leading +// ANSI sequences — before re-styling. Without this, h3+ renders as raw +// "### Title" and reads like unparsed markdown. +// eslint-disable-next-line no-control-regex -- intentionally matches the ESC byte that opens ANSI SGR sequences. +const HEADING_HASH_PREFIX = /^((?:\u001B\[[0-9;]*m)*)#{1,6}[ \t]+/; + +export function createMarkdownTheme(colors: ColorPalette): MarkdownTheme { + const stripHash = (text: string): string => text.replace(HEADING_HASH_PREFIX, '$1'); + const muted = chalk.hex(colors.textMuted); + const dim = chalk.hex(colors.textDim); + const border = chalk.hex(colors.border); + return { + heading: (text) => chalk.bold.hex(colors.text)(stripHash(text)), + link: (text) => chalk.hex(colors.primary)(text), + linkUrl: (text) => muted(text), + code: (text) => chalk.hex(colors.primary)(text), + codeBlock: (text) => text, + codeBlockBorder: (text) => muted(text), + quote: (text) => dim(text), + quoteBorder: (text) => dim(text), + hr: (text) => border(text), + // Match the assistant-message bullet so list markers read like a reply + // prefix. Ordered lists arrive as `"1. "` / `"2. "` and are left + // untouched by the leading-dash anchor. + listBullet: (text) => chalk.hex(colors.roleAssistant)(text.replace(/^-/, '•')), + bold: (text) => chalk.bold(text), + italic: (text) => chalk.italic(text), + strikethrough: (text) => chalk.strikethrough(text), + underline: (text) => chalk.underline(text), + highlightCode: (code: string, lang?: string) => { + const normalizedLang = lang?.trim().toLowerCase(); + const language = + normalizedLang !== undefined && supportsLanguage(normalizedLang) ? normalizedLang : 'text'; + try { + const highlighted = highlight(code, { language, ignoreIllegals: true }); + return highlighted.split('\n'); + } catch { + return code.split('\n'); + } + }, + }; +} + +export function createEditorTheme(colors: ColorPalette): EditorTheme { + const muted = chalk.hex(colors.textMuted); + return { + borderColor: (s) => chalk.hex(colors.border)(s), + selectList: { + selectedPrefix: (s) => chalk.hex(colors.primary)(s), + selectedText: (s) => chalk.hex(colors.primary)(s), + description: (s) => muted(s), + scrollInfo: (s) => muted(s), + noMatch: (s) => muted(s), + }, + }; +} diff --git a/apps/kimi-code/src/tui/theme/styles.ts b/apps/kimi-code/src/tui/theme/styles.ts new file mode 100644 index 000000000..625302e45 --- /dev/null +++ b/apps/kimi-code/src/tui/theme/styles.ts @@ -0,0 +1,66 @@ +/** + * Theme-aware style helpers built on chalk. Components hold a reference + * to a `ThemeStyles` instance via `state.theme.styles` and never reach into + * raw chalk color names — that keeps theme switches consistent and lets + * every visual token route through `ColorPalette`. + */ + +import chalk from 'chalk'; + +import type { ColorPalette } from './colors'; + +export interface ThemeStyles { + colors: ColorPalette; + + /** Brand primary (links, focus, slash highlight). */ + primary(text: string): string; + /** Secondary brand accent (command operators, approval labels). */ + accent(text: string): string; + /** Dimmed text — secondary but still readable. */ + dim(text: string): string; + /** Muted text — most faded; for unchanged-line counters, scroll info. */ + muted(text: string): string; + /** Body text — same color as default but explicit for theming. */ + text(text: string): string; + /** Strong / emphasized text — paths, URLs, command bodies. */ + strong(text: string): string; + + error(text: string): string; + warning(text: string): string; + success(text: string): string; + + /** Bold + dim, for label cells. */ + label(text: string): string; + /** Body color, for value cells. */ + value(text: string): string; + + diffAdd(text: string): string; + diffDel(text: string): string; + diffAddBold(text: string): string; + diffDelBold(text: string): string; + diffGutter(text: string): string; + diffMeta(text: string): string; +} + +export function createThemeStyles(colors: ColorPalette): ThemeStyles { + return { + colors, + primary: (s) => chalk.hex(colors.primary)(s), + accent: (s) => chalk.hex(colors.accent)(s), + dim: (s) => chalk.hex(colors.textDim)(s), + muted: (s) => chalk.hex(colors.textMuted)(s), + text: (s) => chalk.hex(colors.text)(s), + strong: (s) => chalk.hex(colors.textStrong)(s), + error: (s) => chalk.hex(colors.error)(s), + warning: (s) => chalk.hex(colors.warning)(s), + success: (s) => chalk.hex(colors.success)(s), + label: (s) => chalk.bold.hex(colors.textDim)(s), + value: (s) => chalk.hex(colors.text)(s), + diffAdd: (s) => chalk.hex(colors.diffAdded)(s), + diffDel: (s) => chalk.hex(colors.diffRemoved)(s), + diffAddBold: (s) => chalk.bold.hex(colors.diffAddedStrong)(s), + diffDelBold: (s) => chalk.bold.hex(colors.diffRemovedStrong)(s), + diffGutter: (s) => chalk.hex(colors.diffGutter)(s), + diffMeta: (s) => chalk.hex(colors.diffMeta)(s), + }; +} diff --git a/apps/kimi-code/src/tui/theme/terminal-background.ts b/apps/kimi-code/src/tui/theme/terminal-background.ts new file mode 100644 index 000000000..ba9803fdb --- /dev/null +++ b/apps/kimi-code/src/tui/theme/terminal-background.ts @@ -0,0 +1,28 @@ +import { OSC11_RESPONSE } from "#/tui/constant/terminal"; + +import type { ResolvedTheme } from "./colors"; + +export function parseOsc11BackgroundTheme(data: string): ResolvedTheme | null { + const match = OSC11_RESPONSE.exec(data); + if (match === null) return null; + const [, r, g, b] = match; + if (r === undefined || g === undefined || b === undefined) return null; + return themeFromHexChannels(r, g, b); +} + +export function themeFromHexChannels(rHex: string, gHex: string, bHex: string): ResolvedTheme { + const r = normalizeChannel(rHex); + const g = normalizeChannel(gHex); + const b = normalizeChannel(bHex); + // Relative luminance, sRGB-linearised. Threshold 0.5 splits dark/light + // backgrounds reliably for both pure-black (#000) and pure-white (#fff). + const luma = 0.2126 * r + 0.7152 * g + 0.0722 * b; + return luma > 0.5 ? "light" : "dark"; +} + +function normalizeChannel(hex: string): number { + // OSC 11 channels can be 1-4 hex digits. Scale into [0,1] regardless. + const max = (1 << (hex.length * 4)) - 1; + const value = parseInt(hex, 16); + return Number.isFinite(value) ? value / max : 0; +} diff --git a/apps/kimi-code/src/tui/types.ts b/apps/kimi-code/src/tui/types.ts new file mode 100644 index 000000000..16be35503 --- /dev/null +++ b/apps/kimi-code/src/tui/types.ts @@ -0,0 +1,149 @@ +import type { + ModelAlias, + PermissionMode, + ProviderConfig, + PromptPart, + ToolInputDisplay, +} from '@moonshot-ai/kimi-code-sdk'; + +import type { NotificationsConfig } from './config'; +import type { PendingApproval, PendingQuestion } from './reverse-rpc/types'; +import type { Theme } from './theme'; + +export interface AppState { + model: string; + workDir: string; + sessionId: string; + yolo: boolean; + permissionMode: PermissionMode; + planMode: boolean; + thinking: boolean; + contextUsage: number; + contextTokens: number; + maxContextTokens: number; + isStreaming: boolean; + isCompacting: boolean; + isReplaying: boolean; + streamingPhase: 'idle' | 'waiting' | 'thinking' | 'composing'; + streamingStartTime: number; + theme: Theme; + version: string; + editorCommand: string | null; + notifications: NotificationsConfig; + availableModels: Record<string, ModelAlias>; + availableProviders: Record<string, ProviderConfig>; + sessionTitle: string | null; +} + +export interface ToolCallBlockData { + id: string; + name: string; + args: Record<string, unknown>; + description?: string; + display?: ToolInputDisplay; + streamingArguments?: string; + streamingStartedAtMs?: number; + result?: ToolResultBlockData; + subagent?: SubagentReplayBlockData; + step?: number; + turnId?: string; + /** Set when the step ended (e.g. max_tokens) before the tool call's + * arguments finished streaming. Renderer flips the header verb to + * "Truncated" and stops showing the in-progress argument preview. */ + truncated?: boolean; +} + +export interface ToolResultBlockData { + tool_call_id: string; + output: string; + is_error?: boolean; + synthetic?: boolean; +} + +export interface SubagentReplayToolCallData { + id: string; + name: string; + args: Record<string, unknown>; + description?: string; + result?: ToolResultBlockData; +} + +export interface SubagentReplayBlockData { + id: string; + name?: string; + text?: string; + toolCalls?: readonly SubagentReplayToolCallData[]; +} + +export interface BackgroundAgentMetadata { + readonly agentId: string; + readonly parentToolCallId: string; + readonly agentName?: string; + readonly description?: string; +} + +export type BackgroundAgentStatusPhase = 'started' | 'completed' | 'failed'; + +export interface BackgroundAgentStatusData { + readonly phase: BackgroundAgentStatusPhase; + readonly headline: string; + readonly detail?: string; +} + +export interface CompactionTranscriptData { + readonly tokensBefore?: number; + readonly tokensAfter?: number; + readonly instruction?: string; +} + +export type TranscriptEntryKind = + | 'welcome' + | 'user' + | 'assistant' + | 'tool_call' + | 'thinking' + | 'status' + | 'skill_activation'; + +export interface TranscriptEntry { + id: string; + kind: TranscriptEntryKind; + turnId?: string; + renderMode: 'markdown' | 'plain' | 'notice'; + content: string; + color?: string; + detail?: string; + toolCallData?: ToolCallBlockData; + backgroundAgentStatus?: BackgroundAgentStatusData; + compactionData?: CompactionTranscriptData; + imageAttachmentIds?: readonly number[]; + skillActivationId?: string; + skillName?: string; + skillArgs?: string; +} + +export type LivePaneMode = + | 'idle' + | 'waiting' + | 'thinking' + | 'tool' + | 'session'; + +export interface LivePaneState { + mode: LivePaneMode; + pendingApproval: PendingApproval | null; + pendingQuestion: PendingQuestion | null; +} + +export interface QueuedMessage { + readonly text: string; + readonly agentId?: string; + readonly parts?: readonly PromptPart[]; + readonly imageAttachmentIds?: readonly number[]; +} + +export const INITIAL_LIVE_PANE: LivePaneState = { + mode: 'idle', + pendingApproval: null, + pendingQuestion: null, +}; diff --git a/apps/kimi-code/src/tui/utils/background-agent-status.ts b/apps/kimi-code/src/tui/utils/background-agent-status.ts new file mode 100644 index 000000000..a54257a97 --- /dev/null +++ b/apps/kimi-code/src/tui/utils/background-agent-status.ts @@ -0,0 +1,40 @@ +import type { + BackgroundAgentMetadata, + BackgroundAgentStatusData, + BackgroundAgentStatusPhase, +} from '#/tui/types'; + +const MAX_BACKGROUND_FIELD_LENGTH = 240; + +function normalizeBackgroundField(value: string | undefined): string | undefined { + if (value === undefined) return undefined; + const collapsed = value.trim().replaceAll(/\s+/g, ' '); + if (collapsed.length === 0) return undefined; + if (collapsed.length <= MAX_BACKGROUND_FIELD_LENGTH) return collapsed; + return `${collapsed.slice(0, MAX_BACKGROUND_FIELD_LENGTH - 3)}...`; +} + +export function formatBackgroundAgentTranscript( + phase: BackgroundAgentStatusPhase, + meta: BackgroundAgentMetadata, + extras: { resultSummary?: string; error?: string } | undefined = undefined, +): BackgroundAgentStatusData { + const normalizedAgentName = normalizeBackgroundField(meta.agentName); + const subject = normalizedAgentName !== undefined ? `${normalizedAgentName} agent` : 'agent'; + const headline = + phase === 'started' + ? `${subject} started in background` + : phase === 'completed' + ? `${subject} completed in background` + : `${subject} failed in background`; + const tail = phase === 'failed' ? normalizeBackgroundField(extras?.error) : undefined; + const detailParts = [normalizeBackgroundField(meta.description), tail].filter( + (part): part is string => part !== undefined, + ); + + return { + phase, + headline, + detail: detailParts.length > 0 ? detailParts.join(' · ') : undefined, + }; +} diff --git a/apps/kimi-code/src/tui/utils/background-task-status.ts b/apps/kimi-code/src/tui/utils/background-task-status.ts new file mode 100644 index 000000000..def7a39c5 --- /dev/null +++ b/apps/kimi-code/src/tui/utils/background-task-status.ts @@ -0,0 +1,104 @@ +/** + * Format a `BackgroundTaskInfo` snapshot into the transcript card data + * consumed by `BackgroundAgentStatusComponent`. + * + * Background tasks have six statuses (running / awaiting_approval / + * completed / failed / killed / lost) but the transcript card only + * renders three visual phases (started / completed / failed). The + * mapping packs the extra nuance — exit code, kill reason, lost-reason + * — into the dim detail line so the user still sees it. + */ + +import type { BackgroundTaskInfo, BackgroundTaskStatus } from '@moonshot-ai/kimi-code-sdk'; + +import type { BackgroundAgentStatusData, BackgroundAgentStatusPhase } from '@/tui/types'; + +const MAX_DETAIL_LENGTH = 240; + +function truncate(value: string | undefined): string | undefined { + if (value === undefined) return undefined; + const collapsed = value.trim().replaceAll(/\s+/g, ' '); + if (collapsed.length === 0) return undefined; + if (collapsed.length <= MAX_DETAIL_LENGTH) return collapsed; + return `${collapsed.slice(0, MAX_DETAIL_LENGTH - 3)}...`; +} + +export type BackgroundTaskTranscriptPhase = 'started' | 'updated' | 'terminal'; + +function phaseFromStatus(status: BackgroundTaskStatus): BackgroundAgentStatusPhase { + switch (status) { + case 'running': + case 'awaiting_approval': + return 'started'; + case 'completed': + return 'completed'; + case 'failed': + case 'killed': + case 'lost': + return 'failed'; + } +} + +function subjectFor(taskId: string): string { + return taskId.startsWith('agent-') ? 'agent task' : 'bash task'; +} + +function headlineFor(info: BackgroundTaskInfo): string { + const subject = subjectFor(info.taskId); + switch (info.status) { + case 'running': + return `${subject} started in background`; + case 'awaiting_approval': + return `${subject} awaiting approval`; + case 'completed': + return `${subject} completed in background`; + case 'failed': + return `${subject} failed in background`; + case 'killed': + return `${subject} stopped`; + case 'lost': + return `${subject} lost`; + } +} + +function detailFor(info: BackgroundTaskInfo): string | undefined { + const parts: string[] = []; + const description = truncate(info.description); + if (description !== undefined) parts.push(description); + + if (info.status === 'completed' || info.status === 'failed') { + if (info.exitCode !== null && info.exitCode !== undefined) { + parts.push(`exit ${info.exitCode}`); + } + } + if (info.status === 'killed') { + const reason = truncate(info.stopReason); + parts.push(reason !== undefined ? `stopped — ${reason}` : 'stopped'); + } + if (info.status === 'awaiting_approval') { + const reason = truncate(info.approvalReason); + if (reason !== undefined) parts.push(`awaiting: ${reason}`); + } + if (info.status === 'lost') { + parts.push('session restarted before completion'); + } + if (info.timedOut === true) parts.push('timed out'); + + return parts.length > 0 ? parts.join(' · ') : undefined; +} + +/** + * Build a transcript card payload for a background task lifecycle + * snapshot. The returned phase drives bullet color in the renderer + * (`BackgroundAgentStatusComponent`); the detail line carries the extra + * status nuance (exit code, kill reason, etc.). + */ +export function formatBackgroundTaskTranscript( + info: BackgroundTaskInfo, +): BackgroundAgentStatusData { + return { + phase: phaseFromStatus(info.status), + headline: headlineFor(info), + detail: detailFor(info), + }; +} diff --git a/apps/kimi-code/src/tui/utils/component-capabilities.ts b/apps/kimi-code/src/tui/utils/component-capabilities.ts new file mode 100644 index 000000000..5f1f0ba97 --- /dev/null +++ b/apps/kimi-code/src/tui/utils/component-capabilities.ts @@ -0,0 +1,40 @@ +export interface Expandable { + setExpanded(expanded: boolean): void; +} + +export interface PlanExpandable { + // Returns true iff the component actually owns a plan preview and + // applied the new state. + setPlanExpanded(expanded: boolean): boolean; +} + +export interface Disposable { + dispose(): void; +} + +export function isExpandable(obj: unknown): obj is Expandable { + return ( + typeof obj === 'object' && + obj !== null && + 'setExpanded' in obj && + typeof (obj as Expandable).setExpanded === 'function' + ); +} + +export function isPlanExpandable(obj: unknown): obj is PlanExpandable { + return ( + typeof obj === 'object' && + obj !== null && + 'setPlanExpanded' in obj && + typeof (obj as PlanExpandable).setPlanExpanded === 'function' + ); +} + +export function hasDispose(value: unknown): value is Disposable { + return ( + typeof value === 'object' && + value !== null && + 'dispose' in value && + typeof (value as Disposable).dispose === 'function' + ); +} diff --git a/apps/kimi-code/src/tui/utils/errors.ts b/apps/kimi-code/src/tui/utils/errors.ts new file mode 100644 index 000000000..f0fd60a2b --- /dev/null +++ b/apps/kimi-code/src/tui/utils/errors.ts @@ -0,0 +1,14 @@ +function isAbortMessage(message: string): boolean { + return message === 'Aborted' || message.endsWith(': Aborted'); +} + +export function isAbortError(error: unknown): boolean { + if (error instanceof Error) { + return error.name === 'AbortError' || isAbortMessage(error.message); + } + if (typeof error === 'object' && error !== null) { + const message = (error as { readonly message?: unknown }).message; + return typeof message === 'string' && isAbortMessage(message); + } + return isAbortMessage(String(error)); +} diff --git a/apps/kimi-code/src/tui/utils/event-payload.ts b/apps/kimi-code/src/tui/utils/event-payload.ts new file mode 100644 index 000000000..195f7d392 --- /dev/null +++ b/apps/kimi-code/src/tui/utils/event-payload.ts @@ -0,0 +1,81 @@ +import { isKimiError } from '@moonshot-ai/kimi-code-sdk'; + +import { STREAMING_ARGS_FIELD_RE } from '#/tui/constant/streaming'; + +function unescapeJsonString(s: string): string { + return s.replaceAll(/\\(["\\/bfnrt])/g, (_, ch: string) => { + switch (ch) { + case 'n': + return '\n'; + case 't': + return '\t'; + case 'r': + return '\r'; + case 'b': + return '\b'; + case 'f': + return '\f'; + case '"': + return '"'; + case '\\': + return '\\'; + case '/': + return '/'; + default: + return ch; + } + }); +} + +export function parseStreamingArgs(argumentsText: string): Record<string, unknown> { + if (argumentsText.trim().length === 0) return {}; + if (argumentsText.trimEnd().endsWith('}')) { + try { + const parsed = JSON.parse(argumentsText) as unknown; + if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) { + return parsed as Record<string, unknown>; + } + } catch { + // fall through to partial scan + } + } + const result: Record<string, unknown> = {}; + for (const match of argumentsText.matchAll(STREAMING_ARGS_FIELD_RE)) { + const key = match[1]; + const rawValue = match[2]; + if (key === undefined || rawValue === undefined) continue; + if (!(key in result)) { + result[key] = unescapeJsonString(rawValue); + } + } + return result; +} + +export function argsRecord(args: unknown): Record<string, unknown> { + return typeof args === 'object' && args !== null && !Array.isArray(args) + ? (args as Record<string, unknown>) + : {}; +} + +export function serializeToolResultOutput(output: unknown): string { + if (typeof output === 'string') return output; + return JSON.stringify(output, null, 2); +} + +export function isTodoItemShape( + value: unknown, +): value is { title: string; status: 'pending' | 'in_progress' | 'done' } { + if (typeof value !== 'object' || value === null) return false; + const rec = value as { title?: unknown; status?: unknown }; + if (typeof rec.title !== 'string' || rec.title.length === 0) return false; + return rec.status === 'pending' || rec.status === 'in_progress' || rec.status === 'done'; +} + +export function formatErrorMessage(error: unknown): string { + if (isKimiError(error)) return `[${error.code}] ${error.message}`; + return error instanceof Error ? error.message : String(error); +} + +export function stringValue(value: unknown): string | undefined { + return typeof value === 'string' ? value : undefined; +} diff --git a/apps/kimi-code/src/tui/utils/image-attachment-store.ts b/apps/kimi-code/src/tui/utils/image-attachment-store.ts new file mode 100644 index 000000000..bac4ab8fb --- /dev/null +++ b/apps/kimi-code/src/tui/utils/image-attachment-store.ts @@ -0,0 +1,120 @@ +/** + * Registry for media pasted into the input box. + * + * Each paste produces an `ImageAttachment` with an auto-incrementing id + * or `VideoAttachment` with a human-readable placeholder (`[image #1 + * (640×480)]` / `[video #2 sample.mov]`). The placeholder is what the + * user sees in the input field; on submit, `extractMediaAttachments` + * walks the text and expands image placeholders to image content parts + * and video placeholders to file-path tags for `ReadMediaFile`. + * + * Scope is per-`KimiTUI` instance. Reloads (`/new`, `/clear`, + * session switch) call `clear()` so ids restart from 1 and stale + * prompt attachments are dropped. We intentionally do NOT persist + * attachments across sessions — coding-agent doesn't either, and + * `--resume` wouldn't know how to materialize the files anyway. + */ + +export interface ImageAttachment { + readonly id: number; + readonly kind: 'image'; + readonly bytes: Uint8Array; + readonly mime: string; + readonly width: number; + readonly height: number; + /** Rendered placeholder string, e.g. `[image #1 (640×480)]`. */ + readonly placeholder: string; +} + +export interface VideoAttachment { + readonly id: number; + readonly kind: 'video'; + readonly mime: string; + readonly filename: string; + readonly sourcePath: string; + readonly label: string; + /** Rendered placeholder string, e.g. `[video #1 sample.mov]`. */ + readonly placeholder: string; +} + +export type MediaAttachment = ImageAttachment | VideoAttachment; + +export class ImageAttachmentStore { + private nextId = 1; + private readonly byId = new Map<number, MediaAttachment>(); + + addImage(bytes: Uint8Array, mime: string, width: number, height: number): ImageAttachment { + const id = this.nextId; + this.nextId += 1; + const attachment: ImageAttachment = { + id, + kind: 'image', + bytes, + mime, + width, + height, + placeholder: formatPlaceholder(id, width, height), + }; + this.byId.set(id, attachment); + return attachment; + } + + addVideo(mime: string, sourcePath: string, filename?: string | undefined): VideoAttachment { + const id = this.nextId; + this.nextId += 1; + const normalizedFilename = basenameLike( + filename !== undefined && filename !== '' ? filename : sourcePath, + ); + const label = sanitizeVideoLabel(normalizedFilename.length > 0 ? normalizedFilename : mime); + const attachment: VideoAttachment = { + id, + kind: 'video', + mime, + filename: normalizedFilename, + sourcePath, + label, + placeholder: formatVideoPlaceholder(id, label), + }; + this.byId.set(id, attachment); + return attachment; + } + + get(id: number): MediaAttachment | undefined { + return this.byId.get(id); + } + + clear(): void { + this.byId.clear(); + this.nextId = 1; + } + + size(): number { + return this.byId.size; + } +} + +export function formatPlaceholder(id: number, width: number, height: number): string { + return `[image #${String(id)} (${String(width)}×${String(height)})]`; +} + +export function formatVideoPlaceholder(id: number, label: string): string { + return `[video #${String(id)} ${sanitizeVideoLabel(label)}]`; +} + +function sanitizeVideoLabel(raw: string): string { + let label = ''; + for (const char of raw) { + const code = char.codePointAt(0); + label += + code === undefined || code < 0x20 || code === 0x7f || char === '[' || char === ']' + ? '_' + : char; + } + label = label.trim(); + return label.length > 0 ? label : 'video'; +} + +function basenameLike(raw: string): string { + const parts = raw.split(/[\\/]/).filter((part) => part.length > 0); + return parts.at(-1) ?? raw; +} diff --git a/apps/kimi-code/src/tui/utils/image-placeholder.ts b/apps/kimi-code/src/tui/utils/image-placeholder.ts new file mode 100644 index 000000000..11c401f2f --- /dev/null +++ b/apps/kimi-code/src/tui/utils/image-placeholder.ts @@ -0,0 +1,126 @@ +/** + * Scan submitted text for media placeholders and produce + * the `PromptPart[]` we'll send to the SDK prompt endpoint. + * + * Rules: + * - Only placeholders that resolve against `store` get extracted. + * A literal `[image #999 ...]` the user typed themselves stays in + * the text (we can't hallucinate files for it). + * - Order is preserved for text/image/video segments. Image placeholders + * expand to image content parts so the prompt reaches the provider + * without relying on a model tool call. Video placeholders still expand + * to file-path tags so `ReadMediaFile` can own video upload behavior. + * - Adjacent text segments are flattened — empty / whitespace-only + * segments drop out so we never emit `{type:'text', text:' '}` + * noise between two media parts. + */ + +import type { PromptPart } from '@moonshot-ai/kimi-code-sdk'; + +import type { + ImageAttachment, + ImageAttachmentStore, + VideoAttachment, +} from './image-attachment-store'; + +const PLACEHOLDER_REGEX = /\[(image|video) #(\d+) (?:(\(\d+×\d+\))|([^\]]+))\]/g; + +export interface ExtractionResult { + /** Flat list of parts in input order; empty array when no media matched. */ + parts: PromptPart[]; + /** + * Did we find at least one matching attachment? When false, callers + * should keep the prompt on the plain text path. + */ + hasMedia: boolean; + /** Image attachment ids matched, in the order they appeared. */ + imageAttachmentIds: number[]; + /** Video attachment ids matched, in the order they appeared. */ + videoAttachmentIds: number[]; +} + +export function extractMediaAttachments( + text: string, + store: ImageAttachmentStore, +): ExtractionResult { + const parts: PromptPart[] = []; + const imageAttachmentIds: number[] = []; + const videoAttachmentIds: number[] = []; + let cursor = 0; + let hasMedia = false; + + PLACEHOLDER_REGEX.lastIndex = 0; + let match: RegExpExecArray | null; + while ((match = PLACEHOLDER_REGEX.exec(text)) !== null) { + const [literal, kind, idStr] = match; + if (kind !== 'image' && kind !== 'video') continue; + if (idStr === undefined) continue; + const id = Number.parseInt(idStr, 10); + const attachment = store.get(id); + if (attachment === undefined) continue; // stale / user-typed — leave as text + if (attachment.kind !== kind) continue; + const before = text.slice(cursor, match.index); + pushText(parts, before); + if (attachment.kind === 'video') { + const mediaText = tagTextForVideo(attachment); + pushText(parts, mediaText); + videoAttachmentIds.push(id); + } else { + parts.push(imagePartForAttachment(attachment)); + imageAttachmentIds.push(id); + } + hasMedia = true; + cursor = match.index + literal.length; + } + const tail = text.slice(cursor); + pushText(parts, tail); + + return { + // Text-only submissions drop the synthesised parts array — the + // caller's contract is "parts is meaningful iff hasMedia", and + // emitting a stray TextPart confuses consumers that branch on + // `parts.length > 0`. + parts: hasMedia ? parts : [], + hasMedia, + imageAttachmentIds, + videoAttachmentIds, + }; +} + +function pushText(parts: PromptPart[], segment: string): void { + if (segment.length === 0) return; + // Keep whitespace-only segments only when they sit between non-empty + // text elsewhere — the simpler rule "drop everything whitespace-only" + // is fine here because the LLM doesn't care about inter-image spaces. + if (segment.trim().length === 0) return; + const last = parts.at(-1); + if (last?.type === 'text') { + parts[parts.length - 1] = { type: 'text', text: last.text + segment }; + return; + } + parts.push({ type: 'text', text: segment }); +} + +function imagePartForAttachment(att: ImageAttachment): PromptPart { + const base64 = Buffer.from(att.bytes).toString('base64'); + return { + type: 'image_url', + imageUrl: { url: `data:${att.mime};base64,${base64}` }, + }; +} + +function tagTextForVideo(att: VideoAttachment): string { + return formatMediaTag('video', att.sourcePath); +} + +function formatMediaTag(tag: 'image' | 'video', path: string): string { + return `<${tag} path="${escapeAttribute(path)}"></${tag}>`; +} + +function escapeAttribute(value: string): string { + return value + .replaceAll('&', '&') + .replaceAll('"', '"') + .replaceAll('<', '<') + .replaceAll('>', '>'); +} diff --git a/apps/kimi-code/src/tui/utils/mcp-oauth.ts b/apps/kimi-code/src/tui/utils/mcp-oauth.ts new file mode 100644 index 000000000..e0c17b043 --- /dev/null +++ b/apps/kimi-code/src/tui/utils/mcp-oauth.ts @@ -0,0 +1,51 @@ +import { + MCP_OAUTH_AUTHORIZATION_URL_TOOL_UPDATE, + type McpOAuthAuthorizationUrlUpdateData, + type ToolProgressEvent, + type ToolUpdate, +} from '@moonshot-ai/kimi-code-sdk'; + +export type OpenUrl = (url: string) => void; + +export class McpOAuthAuthorizationUrlOpener { + private readonly openedAuthorizationUrls = new Set<string>(); + + constructor(private readonly openUrl: OpenUrl) {} + + handleToolProgress(event: Pick<ToolProgressEvent, 'toolCallId' | 'update'>): void { + const update = parseMcpOAuthAuthorizationUrlUpdate(event.update); + if (update === undefined) return; + const key = `${event.toolCallId}\0${update.authorizationUrl}`; + if (this.openedAuthorizationUrls.has(key)) return; + this.openedAuthorizationUrls.add(key); + this.openUrl(update.authorizationUrl); + } +} + +export function parseMcpOAuthAuthorizationUrlUpdate( + update: ToolUpdate, +): McpOAuthAuthorizationUrlUpdateData | undefined { + if (update.kind !== 'custom') return undefined; + if (update.customKind !== MCP_OAUTH_AUTHORIZATION_URL_TOOL_UPDATE) return undefined; + const data = update.customData; + if (!isRecord(data)) return undefined; + const serverName = data['serverName']; + const authorizationUrl = data['authorizationUrl']; + if (typeof serverName !== 'string' || serverName.length === 0) return undefined; + if (typeof authorizationUrl !== 'string' || authorizationUrl.length === 0) return undefined; + if (!isHttpUrl(authorizationUrl)) return undefined; + return { serverName, authorizationUrl }; +} + +function isRecord(value: unknown): value is Record<string, unknown> { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + +function isHttpUrl(value: string): boolean { + try { + const url = new URL(value); + return url.protocol === 'http:' || url.protocol === 'https:'; + } catch { + return false; + } +} diff --git a/apps/kimi-code/src/tui/utils/mcp-server-status.ts b/apps/kimi-code/src/tui/utils/mcp-server-status.ts new file mode 100644 index 000000000..09fda3989 --- /dev/null +++ b/apps/kimi-code/src/tui/utils/mcp-server-status.ts @@ -0,0 +1,73 @@ +import type { McpServerInfo, McpServerStatusEvent } from '@moonshot-ai/kimi-code-sdk'; + +export type McpServerStatusSnapshot = McpServerInfo | McpServerStatusEvent['server']; + +export const MCP_STARTUP_STATUS_ROW_LIMIT = 4; + +function mcpStartupStatusPriority(status: McpServerStatusSnapshot['status']): number { + switch (status) { + case 'failed': + return 0; + case 'needs-auth': + return 1; + case 'pending': + return 2; + case 'connected': + return 3; + case 'disabled': + return 4; + } +} + +export function selectMcpStartupStatusRows( + servers: readonly McpServerStatusSnapshot[], +): McpServerStatusSnapshot[] { + return [...servers] + .filter((server) => server.status !== 'disabled') + .toSorted((a, b) => mcpStartupStatusPriority(a.status) - mcpStartupStatusPriority(b.status)) + .slice(0, MCP_STARTUP_STATUS_ROW_LIMIT); +} + +export function formatMcpStartupStatusSummary( + hidden: readonly McpServerStatusSnapshot[], + visibleCount: number, +): string { + let failed = 0; + let needsAuth = 0; + let connecting = 0; + let connected = 0; + let disabled = 0; + for (const server of hidden) { + switch (server.status) { + case 'failed': + failed++; + break; + case 'needs-auth': + needsAuth++; + break; + case 'pending': + connecting++; + break; + case 'connected': + connected++; + break; + case 'disabled': + disabled++; + break; + } + } + + const parts: string[] = []; + if (failed > 0) parts.push(`${failed} failed`); + if (needsAuth > 0) parts.push(`${needsAuth} need auth`); + if (connecting > 0) parts.push(`${connecting} connecting`); + if (connected > 0) parts.push(`${connected} connected`); + if (disabled > 0) parts.push(`${disabled} disabled`); + const detail = parts.join(', '); + if (visibleCount === 0) return `MCP servers: ${detail}`; + return `MCP servers: ${hidden.length} more (${detail})`; +} + +export function mcpServerStatusKey(server: McpServerStatusSnapshot): string { + return JSON.stringify([server.status, server.transport, server.toolCount, server.error]); +} diff --git a/apps/kimi-code/src/tui/utils/mcp-tool-name.ts b/apps/kimi-code/src/tui/utils/mcp-tool-name.ts new file mode 100644 index 000000000..dd5998a82 --- /dev/null +++ b/apps/kimi-code/src/tui/utils/mcp-tool-name.ts @@ -0,0 +1,16 @@ +// Decodes the `mcp__<server>__<tool>` qualified names produced by kimi-core's +// `qualifyMcpToolName`. Returns null for non-MCP tools and for hash-truncated +// qualified names (where the trailing `__<tool>` segment has been collapsed). +export function decodeMcpToolName( + name: string, +): { readonly serverName: string; readonly toolName: string } | null { + const PREFIX = 'mcp__'; + if (!name.startsWith(PREFIX)) return null; + const rest = name.slice(PREFIX.length); + const sep = rest.indexOf('__'); + if (sep <= 0 || sep === rest.length - 2) return null; + return { + serverName: rest.slice(0, sep), + toolName: rest.slice(sep + 2), + }; +} diff --git a/apps/kimi-code/src/tui/utils/media-url.ts b/apps/kimi-code/src/tui/utils/media-url.ts new file mode 100644 index 000000000..f04edeb29 --- /dev/null +++ b/apps/kimi-code/src/tui/utils/media-url.ts @@ -0,0 +1,51 @@ +export type MediaUrlKind = 'audio' | 'image' | 'video'; + +export function mediaUrlPartToText(kind: MediaUrlKind, url: string): string { + const summary = summarizeDataUrl(url); + if (summary !== undefined) { + const size = summary.bytes !== undefined ? `, ${formatByteSize(summary.bytes)}` : ''; + return `[${kind} ${summary.mime}${size}]`; + } + return `<${kind} url="${escapeAttribute(url)}">`; +} + +export function summarizeDataUrl(url: string): { mime: string; bytes?: number } | undefined { + if (!url.startsWith('data:')) return undefined; + const commaIndex = url.indexOf(','); + const header = + commaIndex >= 0 ? url.slice('data:'.length, commaIndex) : url.slice('data:'.length); + const data = commaIndex >= 0 ? url.slice(commaIndex + 1) : ''; + const [rawMime, ...params] = header.split(';'); + const mime = rawMime !== undefined && rawMime.length > 0 ? rawMime : 'application/octet-stream'; + const isBase64 = params.some((param) => param.toLowerCase() === 'base64'); + return { + mime, + bytes: isBase64 ? estimateBase64Bytes(data) : undefined, + }; +} + +function estimateBase64Bytes(data: string): number { + const compact = data.replaceAll(/\s/g, ''); + if (compact.length === 0) return 0; + const padding = compact.endsWith('==') ? 2 : compact.endsWith('=') ? 1 : 0; + return Math.max(0, Math.floor((compact.length * 3) / 4) - padding); +} + +function formatByteSize(bytes: number): string { + if (bytes < 1024) return `${String(bytes)} B`; + const kib = bytes / 1024; + if (kib < 1024) return `${formatOneDecimal(kib)} KB`; + return `${formatOneDecimal(kib / 1024)} MB`; +} + +function formatOneDecimal(value: number): string { + return value >= 10 ? value.toFixed(0) : value.toFixed(1); +} + +function escapeAttribute(value: string): string { + return value + .replaceAll('&', '&') + .replaceAll('"', '"') + .replaceAll('<', '<') + .replaceAll('>', '>'); +} diff --git a/apps/kimi-code/src/tui/utils/open-url.ts b/apps/kimi-code/src/tui/utils/open-url.ts new file mode 100644 index 000000000..4112d9c6f --- /dev/null +++ b/apps/kimi-code/src/tui/utils/open-url.ts @@ -0,0 +1,11 @@ +import { execFile } from 'node:child_process'; + +export function openUrl(url: string): void { + const command: [string, string[]] = + process.platform === 'darwin' + ? ['open', [url]] + : process.platform === 'win32' + ? ['cmd', ['/c', 'start', '', url]] + : ['xdg-open', [url]]; + execFile(command[0], command[1], () => {}); +} diff --git a/apps/kimi-code/src/tui/utils/printable-key.ts b/apps/kimi-code/src/tui/utils/printable-key.ts new file mode 100644 index 000000000..1b0977369 --- /dev/null +++ b/apps/kimi-code/src/tui/utils/printable-key.ts @@ -0,0 +1,27 @@ +/** + * Decode raw stdin bytes into a comparable printable character. + * + * When a terminal (e.g. the VSCode integrated terminal) enables the Kitty + * keyboard protocol disambiguate flag, ordinary printable keys are sent as + * CSI-u sequences: pressing `r` arrives as "\x1b[114u", pressing `q` as + * "\x1b[113u". A bare `data === 'q'` comparison inside a Container's + * `handleInput` therefore never matches under Kitty-mode terminals. + * + * Rules: + * - Every bare-literal printable-character comparison (letters, digits, + * space, punctuation) must go through this function first. + * - Functional keys (arrows, Enter, Tab, Esc, ...) continue to use + * `matchesKey(data, Key.*)`; pi-tui's `matchesKey` already handles Kitty. + * - Control characters (codepoint < 32, e.g. ctrl-b, ctrl-f) may still + * compare against the raw `data` — `decodeKittyPrintable` rejects them. + * + * The module's existence is itself the "don't forget to decode" constraint: + * `test/tui/printable-key-guard.test.ts` scans every `handleInput` under + * `tui/components/**` and rejects bare-literal comparisons. + */ + +import { decodeKittyPrintable } from '@earendil-works/pi-tui'; + +export function printableChar(data: string): string { + return decodeKittyPrintable(data) ?? data; +} diff --git a/apps/kimi-code/src/tui/utils/proctitle.ts b/apps/kimi-code/src/tui/utils/proctitle.ts new file mode 100644 index 000000000..c9c30d823 --- /dev/null +++ b/apps/kimi-code/src/tui/utils/proctitle.ts @@ -0,0 +1,29 @@ +/** + * Terminal window title synchronization. + * + * Uses the session title when present, capped at 80 characters to keep tabs + * readable. New or unnamed sessions fall back to `Kimi Code`. + * + * Writes both `process.title`, for process listings, and OSC 0/2 escape + * sequences, which most terminals use for window/tab titles. Non-TTY stdout + * skips the OSC write. + */ +import { PRODUCT_NAME } from '#/constant/app'; +import { MAX_PROCESS_TITLE_LENGTH } from '#/tui/constant/terminal'; + +export function setProcessTitle(title: string | null, _sessionId: string): void { + const trimmed = title?.trim() ?? ''; + const label = trimmed.length > 0 ? trimmed.slice(0, MAX_PROCESS_TITLE_LENGTH) : PRODUCT_NAME; + try { + process.title = label; + } catch { + /* noop */ + } + try { + if (process.stdout.isTTY) { + process.stdout.write(`\u001B]0;${label}\u0007`); + } + } catch { + /* noop */ + } +} diff --git a/apps/kimi-code/src/tui/utils/terminal-focus.ts b/apps/kimi-code/src/tui/utils/terminal-focus.ts new file mode 100644 index 000000000..3a3559e60 --- /dev/null +++ b/apps/kimi-code/src/tui/utils/terminal-focus.ts @@ -0,0 +1,44 @@ +import { + DISABLE_TERMINAL_FOCUS_REPORTING, + ENABLE_TERMINAL_FOCUS_REPORTING, + TERMINAL_FOCUS_IN, + TERMINAL_FOCUS_OUT, +} from '#/tui/constant/terminal'; +import type { TUIState } from '#/tui/kimi-tui'; +import type { TerminalState } from '#/tui/utils/terminal-state'; + +export { + DISABLE_TERMINAL_FOCUS_REPORTING, + ENABLE_TERMINAL_FOCUS_REPORTING, + TERMINAL_FOCUS_IN, + TERMINAL_FOCUS_OUT, +} from '#/tui/constant/terminal'; + +export function installTerminalFocusTracking(state: TUIState): () => void { + state.terminalState.focused = true; + const disposeInputListener = state.ui.addInputListener((data) => + handleTerminalFocusInput(state.terminalState, data), + ); + state.terminal.write(ENABLE_TERMINAL_FOCUS_REPORTING); + + return () => { + disposeInputListener(); + state.terminal.write(DISABLE_TERMINAL_FOCUS_REPORTING); + state.terminalState.focused = true; + }; +} + +export function handleTerminalFocusInput( + state: Pick<TerminalState, 'focused'>, + data: string, +): { consume: true } | undefined { + if (data === TERMINAL_FOCUS_IN) { + state.focused = true; + return { consume: true }; + } + if (data === TERMINAL_FOCUS_OUT) { + state.focused = false; + return { consume: true }; + } + return undefined; +} diff --git a/apps/kimi-code/src/tui/utils/terminal-notification.ts b/apps/kimi-code/src/tui/utils/terminal-notification.ts new file mode 100644 index 000000000..a71da9347 --- /dev/null +++ b/apps/kimi-code/src/tui/utils/terminal-notification.ts @@ -0,0 +1,129 @@ +import type { Terminal } from '@earendil-works/pi-tui'; + +import { BEL, ESC, MAX_TERMINAL_NOTIFICATION_MESSAGE_LENGTH, ST } from '#/tui/constant/terminal'; +import type { TUIState } from '#/tui/kimi-tui'; + +export interface TerminalNotification { + readonly title: string; + readonly body?: string | undefined; +} + +export interface EmitOptions { + readonly supportsOsc9?: boolean; + readonly insideTmux?: boolean; +} + +export interface BuildOptions { + readonly supportsOsc9: boolean; + readonly insideTmux: boolean; +} + +export function notifyTerminalOnce( + state: TUIState, + key: string, + notification: TerminalNotification, +): void { + const { enabled, condition } = state.appState.notifications; + if (!enabled) return; + if (state.terminalState.notificationKeys.has(key)) return; + state.terminalState.notificationKeys.add(key); + if (condition === 'unfocused' && state.terminalState.focused) return; + emitTerminalNotification(state.terminal, notification, { + supportsOsc9: state.terminalState.supportsOsc9, + insideTmux: state.terminalState.insideTmux, + }); +} + +export function emitTerminalNotification( + terminal: Pick<Terminal, 'write'>, + notification: TerminalNotification, + options: EmitOptions = {}, +): void { + const sequences = buildTerminalNotificationSequences(notification, { + supportsOsc9: options.supportsOsc9 ?? supportsOsc9Notification(), + insideTmux: options.insideTmux ?? isInsideTmux(), + }); + for (const sequence of sequences) { + terminal.write(sequence); + } +} + +export function formatNotification(notification: TerminalNotification): string { + const title = sanitizeNotificationText(notification.title); + const body = sanitizeNotificationText(notification.body ?? ''); + const message = + title.length > 0 && body.length > 0 ? `${title}: ${body}` : title.length > 0 ? title : body; + return message.slice(0, MAX_TERMINAL_NOTIFICATION_MESSAGE_LENGTH); +} + +/** + * Build the OSC/BEL bytes for a terminal notification. + * + * - `supportsOsc9 === true`: emit a single OSC 9 sequence — the modern + * desktop-notification path used by iTerm2, WezTerm, Kitty, Ghostty + * and Warp. + * - `supportsOsc9 === false`: fall back to a bare BEL so the user still + * gets the system bell on terminals that don't recognize OSC 9. + * + * When `insideTmux === true` and we're emitting OSC 9, wrap the sequence + * in a tmux DCS passthrough (`ESC P tmux ; <payload> ESC \`) and double + * any `ESC` bytes inside the payload — otherwise tmux swallows the OSC. + * BEL is single-byte and passes through tmux unchanged, so no wrap is + * needed in the fallback path. + */ +export function buildTerminalNotificationSequences( + notification: TerminalNotification, + options: BuildOptions, +): string[] { + const message = formatNotification(notification); + if (message.length === 0) return []; + if (!options.supportsOsc9) { + return [BEL]; + } + const osc9 = `${ESC}]9;${message}${BEL}`; + if (options.insideTmux) { + const escaped = osc9.replaceAll(ESC, `${ESC}${ESC}`); + return [`${ESC}Ptmux;${escaped}${ESC}${ST}`]; + } + return [osc9]; +} + +/** + * Best-effort detection of OSC 9 desktop-notification support, driven + * entirely off well-known environment variables. The allow-list is + * intentionally short and conservative because BEL is safe everywhere, + * while shipping OSC 9 to a terminal that doesn't grok it would print + * escape garbage on screen. + */ +export function supportsOsc9Notification(env: NodeJS.ProcessEnv = process.env): boolean { + const termProgram = env['TERM_PROGRAM'] ?? ''; + if ( + termProgram === 'iTerm.app' || + termProgram === 'WezTerm' || + termProgram === 'ghostty' || + termProgram === 'WarpTerminal' + ) { + return true; + } + const term = env['TERM'] ?? ''; + if (term === 'xterm-kitty' || term === 'xterm-ghostty') return true; + return false; +} + +export function isInsideTmux(env: NodeJS.ProcessEnv = process.env): boolean { + const tmux = env['TMUX'] ?? ''; + return tmux.length > 0; +} + +function sanitizeNotificationText(value: string): string { + return Array.from(value) + .map((ch) => (isControlCharacter(ch) ? ' ' : ch)) + .join('') + .replaceAll(/\s+/g, ' ') + .trim(); +} + +function isControlCharacter(ch: string): boolean { + const code = ch.codePointAt(0) ?? 0; + return (code >= 0x00 && code <= 0x1f) || (code >= 0x7f && code <= 0x9f); +} diff --git a/apps/kimi-code/src/tui/utils/terminal-state.ts b/apps/kimi-code/src/tui/utils/terminal-state.ts new file mode 100644 index 000000000..86d2bab9f --- /dev/null +++ b/apps/kimi-code/src/tui/utils/terminal-state.ts @@ -0,0 +1,19 @@ +import { isInsideTmux, supportsOsc9Notification } from './terminal-notification'; + +export interface TerminalState { + notificationKeys: Set<string>; + focused: boolean; + supportsOsc9: boolean; + insideTmux: boolean; + progressActive: boolean; +} + +export function createTerminalState(): TerminalState { + return { + notificationKeys: new Set<string>(), + focused: true, + supportsOsc9: supportsOsc9Notification(), + insideTmux: isInsideTmux(), + progressActive: false, + }; +} diff --git a/apps/kimi-code/src/tui/utils/terminal-theme.ts b/apps/kimi-code/src/tui/utils/terminal-theme.ts new file mode 100644 index 000000000..71c6b9bbf --- /dev/null +++ b/apps/kimi-code/src/tui/utils/terminal-theme.ts @@ -0,0 +1,153 @@ +import { + DISABLE_TERMINAL_THEME_REPORTING, + ENABLE_TERMINAL_THEME_REPORTING, + OSC11_QUERY, + OSC11_RESPONSE, + OSC11_RESPONSE_PREFIX, + OSC11_RESPONSE_PREFIX_NO_ESC, + QUERY_TERMINAL_THEME, + TERMINAL_THEME_INPUT_BUFFER_MAX_LENGTH, + TERMINAL_THEME_DARK, + TERMINAL_THEME_LIGHT, +} from "#/tui/constant/terminal"; +import type { TUIState } from "#/tui/kimi-tui"; +import type { ResolvedTheme } from "#/tui/theme/colors"; +import { parseOsc11BackgroundTheme } from "#/tui/theme/terminal-background"; + +export { + DISABLE_TERMINAL_THEME_REPORTING, + ENABLE_TERMINAL_THEME_REPORTING, + OSC11_QUERY, + QUERY_TERMINAL_THEME, + TERMINAL_THEME_DARK, + TERMINAL_THEME_LIGHT, +} from "#/tui/constant/terminal"; + +export function hasTerminalThemeReport(data: string): boolean { + return data.includes(TERMINAL_THEME_DARK) || data.includes(TERMINAL_THEME_LIGHT); +} + +export interface TerminalThemeInputState { + osc11Buffer: string; +} + +export type TerminalThemeInputResult = + | { + consume?: boolean; + data?: string; + } + | undefined; + +export function createTerminalThemeInputState(): TerminalThemeInputState { + return { osc11Buffer: "" }; +} + +export function handleTerminalThemeInput( + data: string, + terminal: Pick<TUIState["terminal"], "write">, + onTheme: (theme: ResolvedTheme) => void, + inputState: TerminalThemeInputState = createTerminalThemeInputState(), +): TerminalThemeInputResult { + let remaining = data; + + if (inputState.osc11Buffer !== "") { + const candidate = `${inputState.osc11Buffer}${data}`; + const stripped = stripOsc11Reports(candidate, onTheme); + if (stripped !== candidate) { + inputState.osc11Buffer = ""; + return resultFromRemaining(stripped); + } + + inputState.osc11Buffer = + candidate.length > TERMINAL_THEME_INPUT_BUFFER_MAX_LENGTH ? "" : candidate; + return { consume: true }; + } + + remaining = stripOsc11Reports(remaining, onTheme); + remaining = stripTerminalThemeReports(remaining, terminal); + + const partialOsc11Start = findPartialOsc11Start(remaining); + if (partialOsc11Start !== -1) { + inputState.osc11Buffer = remaining.slice(partialOsc11Start); + return resultFromRemaining(remaining.slice(0, partialOsc11Start)); + } + + if (remaining !== data) return resultFromRemaining(remaining); + + return undefined; +} + +function stripOsc11Reports(data: string, onTheme: (theme: ResolvedTheme) => void): string { + let remaining = data; + + for (;;) { + const match = OSC11_RESPONSE.exec(remaining); + if (match === null) return remaining; + + const theme = parseOsc11BackgroundTheme(match[0]); + if (theme !== null) onTheme(theme); + + remaining = `${remaining.slice(0, match.index)}${remaining.slice(match.index + match[0].length)}`; + } +} + +function stripTerminalThemeReports( + data: string, + terminal: Pick<TUIState["terminal"], "write">, +): string { + let remaining = data; + let strippedReport = false; + + for (const report of [TERMINAL_THEME_DARK, TERMINAL_THEME_LIGHT]) { + if (!remaining.includes(report)) continue; + remaining = remaining.split(report).join(""); + strippedReport = true; + } + + if (strippedReport) { + terminal.write(OSC11_QUERY); + } + + return remaining; +} + +function findPartialOsc11Start(data: string): number { + const fullPrefixIndex = data.indexOf(OSC11_RESPONSE_PREFIX); + if (fullPrefixIndex !== -1) return fullPrefixIndex; + + const noEscPrefixIndex = data.indexOf(OSC11_RESPONSE_PREFIX_NO_ESC); + if (noEscPrefixIndex !== -1) return noEscPrefixIndex; + + for (let i = 0; i < data.length; i++) { + const suffix = data.slice(i); + if (OSC11_RESPONSE_PREFIX.startsWith(suffix) && suffix.length > 1) return i; + if (OSC11_RESPONSE_PREFIX_NO_ESC.startsWith(suffix) && suffix.startsWith("]11;")) { + return i; + } + } + + return -1; +} + +function resultFromRemaining(data: string): TerminalThemeInputResult { + if (data.length === 0) return { consume: true }; + return { data }; +} + +export function installTerminalThemeTracking( + state: Pick<TUIState, "terminal" | "ui">, + onTheme: (theme: ResolvedTheme) => void, +): () => void { + const inputState = createTerminalThemeInputState(); + const disposeInputListener = state.ui.addInputListener((data) => + handleTerminalThemeInput(data, state.terminal, onTheme, inputState), + ); + state.terminal.write(ENABLE_TERMINAL_THEME_REPORTING); + state.terminal.write(OSC11_QUERY); + state.terminal.write(QUERY_TERMINAL_THEME); + + return () => { + disposeInputListener(); + state.terminal.write(DISABLE_TERMINAL_THEME_REPORTING); + }; +} diff --git a/apps/kimi-code/src/tui/utils/transcript-id.ts b/apps/kimi-code/src/tui/utils/transcript-id.ts new file mode 100644 index 000000000..149378a9b --- /dev/null +++ b/apps/kimi-code/src/tui/utils/transcript-id.ts @@ -0,0 +1,6 @@ +let transcriptIdCounter = 0; + +export function nextTranscriptId(): string { + transcriptIdCounter += 1; + return `entry-${String(transcriptIdCounter)}`; +} diff --git a/apps/kimi-code/src/utils/clipboard/clipboard-image.ts b/apps/kimi-code/src/utils/clipboard/clipboard-image.ts new file mode 100644 index 000000000..0bb8d3c76 --- /dev/null +++ b/apps/kimi-code/src/utils/clipboard/clipboard-image.ts @@ -0,0 +1,516 @@ +/** + * Read media from the system clipboard with graceful platform fallbacks. + * + * kimi-core's LLM pipeline only accepts PNG/JPEG/GIF/WebP, and the + * clipboard sources we query already emit those formats on supported + * platforms — so we deliberately do not include a BMP→PNG converter. + * + * Lookup order: + * macOS file clipboard -> osascript/AppKit file URLs + * macOS / Windows -> native `@mariozechner/clipboard` + * Linux Wayland -> wl-paste + * Linux X11 -> xclip + * WSL (image not on Linux cb) -> PowerShell fallback via wslpath + * + * Returns `null` when no supported media is available, the format isn't + * supported, or every fallback fails. + */ + +import { spawnSync } from 'node:child_process'; +import { randomUUID } from 'node:crypto'; +import { readFileSync, statSync, unlinkSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { basename, isAbsolute, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { parseImageMeta } from '#/utils/image/image-mime'; + +import { clipboard, type ClipboardModule } from './clipboard-native'; + +export interface ClipboardImage { + kind: 'image'; + bytes: Uint8Array; + mimeType: string; +} + +export interface ClipboardVideo { + kind: 'video'; + mimeType: string; + filename: string; + sourcePath: string; +} + +export type ClipboardMedia = ClipboardImage | ClipboardVideo; + +export class ClipboardMediaError extends Error { + constructor(message: string) { + super(message); + this.name = 'ClipboardMediaError'; + } +} + +type RunCommandOptions = { timeoutMs?: number; env?: NodeJS.ProcessEnv }; +type RunCommand = ( + command: string, + args: string[], + options?: RunCommandOptions, +) => { stdout: Buffer; ok: boolean }; + +const SUPPORTED_IMAGE_MIME_TYPES = ['image/png', 'image/jpeg', 'image/webp', 'image/gif'] as const; +const MAX_VIDEO_BYTES = 100 * 1024 * 1024; + +const VIDEO_MIME_BY_SUFFIX: Readonly<Record<string, string>> = Object.freeze({ + '.mp4': 'video/mp4', + '.mpg': 'video/mpeg', + '.mpeg': 'video/mpeg', + '.mkv': 'video/x-matroska', + '.avi': 'video/x-msvideo', + '.mov': 'video/quicktime', + '.ogv': 'video/ogg', + '.wmv': 'video/x-ms-wmv', + '.webm': 'video/webm', + '.m4v': 'video/x-m4v', + '.flv': 'video/x-flv', + '.3gp': 'video/3gpp', + '.3g2': 'video/3gpp2', +}); + +const DEFAULT_LIST_TIMEOUT_MS = 1000; +const DEFAULT_READ_TIMEOUT_MS = 3000; +const DEFAULT_POWERSHELL_TIMEOUT_MS = 5000; +const DEFAULT_MAX_BUFFER_BYTES = 50 * 1024 * 1024; + +const MACOS_FILE_PATH_SCRIPT = String.raw` +ObjC.import('AppKit'); +ObjC.import('Foundation'); + +const out = []; +const pb = $.NSPasteboard.generalPasteboard; +if (String(pb) !== '[id nil]') { + try { + const options = $.NSMutableDictionary.dictionary; + options.setObjectForKey($.NSNumber.numberWithBool(true), $.NSPasteboardURLReadingFileURLsOnlyKey); + const urls = pb.readObjectsForClassesOptions([$.NSURL], options); + const count = urls ? urls.count : 0; + for (let i = 0; i < count; i++) { + const value = urls.objectAtIndex(i).path; + const path = value ? ObjC.unwrap(value) : ''; + if (path) out.push(path); + } + } catch (error) {} + + if (out.length === 0) { + try { + const files = ObjC.deepUnwrap(pb.propertyListForType('NSFilenamesPboardType')); + if (Array.isArray(files)) { + for (const path of files) { + if (path) out.push(String(path)); + } + } else if (files) { + out.push(String(files)); + } + } catch (error) {} + } +} +out.join('\n'); +`.trim(); + +function isWaylandSession(env: NodeJS.ProcessEnv): boolean { + return Boolean(env['WAYLAND_DISPLAY']) || env['XDG_SESSION_TYPE'] === 'wayland'; +} + +function isWSL(env: NodeJS.ProcessEnv): boolean { + if (env['WSL_DISTRO_NAME'] !== undefined || env['WSLENV'] !== undefined) return true; + try { + return /microsoft|wsl/i.test(readFileSync('/proc/version', 'utf-8')); + } catch { + return false; + } +} + +function baseMimeType(raw: string): string { + return raw.split(';')[0]?.trim().toLowerCase() ?? raw.toLowerCase(); +} + +function isSupportedImageMimeType(mime: string): boolean { + const base = baseMimeType(mime); + return (SUPPORTED_IMAGE_MIME_TYPES as readonly string[]).includes(base); +} + +function selectPreferredImageMimeType(candidates: string[]): string | null { + const normalized = candidates + .map((t) => t.trim()) + .filter((t) => t.length > 0) + .map((raw) => ({ raw, base: baseMimeType(raw) })); + + for (const preferred of SUPPORTED_IMAGE_MIME_TYPES) { + const match = normalized.find((t) => t.base === preferred); + if (match !== undefined) return match.raw; + } + const anyImage = normalized.find((t) => t.base.startsWith('image/')); + return anyImage?.raw ?? null; +} + +function videoMimeFromPath(path: string): string | null { + const dot = path.lastIndexOf('.'); + if (dot < 0) return null; + const suffix = path.slice(dot).toLowerCase(); + return VIDEO_MIME_BY_SUFFIX[suffix] ?? null; +} + +function parseClipboardPaths(text: string): string[] { + return splitClipboardPathLines(text) + .map((line) => line.trim()) + .filter((line) => line.length > 0 && !line.startsWith('#')) + .map((line) => { + if (line.startsWith('file://')) { + try { + return fileURLToPath(line); + } catch { + return ''; + } + } + return line; + }) + .filter((line) => line.length > 0 && isAbsolute(line)); +} + +function splitClipboardPathLines(text: string): string[] { + const lines: string[] = []; + let start = 0; + for (let i = 0; i < text.length; i += 1) { + const char = text[i]; + if (char === '\r' || char === '\n' || text.codePointAt(i) === 0) { + lines.push(text.slice(start, i)); + start = i + 1; + } + } + lines.push(text.slice(start)); + return lines; +} + +function readImagePath(path: string): ClipboardImage | null { + let stat: ReturnType<typeof statSync>; + try { + stat = statSync(path); + } catch { + return null; + } + if (!stat.isFile()) return null; + + let bytes: Buffer; + try { + bytes = readFileSync(path); + } catch { + return null; + } + if (bytes.length === 0) return null; + + const meta = parseImageMeta(bytes); + if (meta === null) return null; + return { kind: 'image', bytes: new Uint8Array(bytes), mimeType: meta.mime }; +} + +function readVideoPath(path: string): ClipboardVideo | null { + const mimeType = videoMimeFromPath(path); + if (mimeType === null) return null; + let stat: ReturnType<typeof statSync>; + try { + stat = statSync(path); + } catch { + return null; + } + if (!stat.isFile()) return null; + if (stat.size > MAX_VIDEO_BYTES) { + throw new ClipboardMediaError( + `Video is ${(stat.size / 1024 / 1024).toFixed(1)} MB; maximum supported size is 100 MB.`, + ); + } + return { + kind: 'video', + mimeType, + filename: basename(path), + sourcePath: path, + }; +} + +function readMediaPath(path: string): ClipboardMedia | null { + // Video files are never opened as images. + const video = readVideoPath(path); + if (video !== null) return video; + return readImagePath(path); +} + +function readMediaFromPaths(paths: readonly string[]): ClipboardMedia | null { + for (const path of paths) { + const media = readMediaPath(path); + if (media !== null) return media; + } + return null; +} + +function readMediaFromText(text: string): ClipboardMedia | null { + return readMediaFromPaths(parseClipboardPaths(text)); +} + +function runCommand( + command: string, + args: string[], + options?: RunCommandOptions, +): { stdout: Buffer; ok: boolean } { + const result = spawnSync(command, args, { + timeout: options?.timeoutMs ?? DEFAULT_READ_TIMEOUT_MS, + maxBuffer: DEFAULT_MAX_BUFFER_BYTES, + env: options?.env, + }); + if (result.error !== undefined || result.status !== 0) { + return { ok: false, stdout: Buffer.alloc(0) }; + } + const stdout = Buffer.isBuffer(result.stdout) ? result.stdout : Buffer.from(result.stdout ?? ''); + return { ok: true, stdout }; +} + +function parseTargetList(output: Buffer): string[] { + return output + .toString('utf-8') + .split(/\r?\n/) + .map((t) => t.trim()) + .filter((t) => t.length > 0); +} + +function readClipboardFileMediaViaWlPaste(): ClipboardMedia | null { + const list = runCommand('wl-paste', ['--list-types'], { + timeoutMs: DEFAULT_LIST_TIMEOUT_MS, + }); + if (!list.ok) return null; + + const types = parseTargetList(list.stdout); + const uriType = types.find((t) => baseMimeType(t) === 'text/uri-list'); + if (uriType === undefined) return null; + + const uris = runCommand('wl-paste', ['--type', uriType, '--no-newline']); + return uris.ok ? readMediaFromText(uris.stdout.toString('utf-8')) : null; +} + +function readClipboardImageViaWlPaste(): ClipboardImage | null { + const list = runCommand('wl-paste', ['--list-types'], { + timeoutMs: DEFAULT_LIST_TIMEOUT_MS, + }); + if (!list.ok) return null; + + const selected = selectPreferredImageMimeType(parseTargetList(list.stdout)); + if (selected === null) return null; + + const data = runCommand('wl-paste', ['--type', selected, '--no-newline']); + if (!data.ok || data.stdout.length === 0) return null; + return { kind: 'image', bytes: data.stdout, mimeType: baseMimeType(selected) }; +} + +function readClipboardFileMediaViaXclip(): ClipboardMedia | null { + const targets = runCommand('xclip', ['-selection', 'clipboard', '-t', 'TARGETS', '-o'], { + timeoutMs: DEFAULT_LIST_TIMEOUT_MS, + }); + if (!targets.ok) return null; + + const candidates = parseTargetList(targets.stdout); + const uriType = candidates.find((t) => baseMimeType(t) === 'text/uri-list'); + if (uriType === undefined) return null; + + const uris = runCommand('xclip', ['-selection', 'clipboard', '-t', uriType, '-o']); + return uris.ok ? readMediaFromText(uris.stdout.toString('utf-8')) : null; +} + +function readClipboardImageViaXclip(): ClipboardImage | null { + const targets = runCommand('xclip', ['-selection', 'clipboard', '-t', 'TARGETS', '-o'], { + timeoutMs: DEFAULT_LIST_TIMEOUT_MS, + }); + + const candidates = targets.ok ? parseTargetList(targets.stdout) : []; + const preferred = candidates.length > 0 ? selectPreferredImageMimeType(candidates) : null; + const tryTypes = + preferred !== null + ? [preferred, ...SUPPORTED_IMAGE_MIME_TYPES] + : [...SUPPORTED_IMAGE_MIME_TYPES]; + + for (const mime of tryTypes) { + const data = runCommand('xclip', ['-selection', 'clipboard', '-t', mime, '-o']); + if (data.ok && data.stdout.length > 0) { + return { kind: 'image', bytes: data.stdout, mimeType: baseMimeType(mime) }; + } + } + return null; +} + +/** + * Windows clipboard images (Win+Shift+S) don't bridge into the WSL + * Linux clipboard. PowerShell reaches the Windows clipboard directly; + * we round-trip via a temp PNG because binary stdout is unreliable + * across the WSL interop boundary. + */ +function readClipboardImageViaPowerShell(): ClipboardImage | null { + const tmpFile = join(tmpdir(), `kimi-wsl-clip-${randomUUID()}.png`); + try { + const winPathResult = runCommand('wslpath', ['-w', tmpFile], { + timeoutMs: DEFAULT_LIST_TIMEOUT_MS, + }); + if (!winPathResult.ok) return null; + const winPath = winPathResult.stdout.toString('utf-8').trim(); + if (winPath.length === 0) return null; + + const psScript = [ + 'Add-Type -AssemblyName System.Windows.Forms', + 'Add-Type -AssemblyName System.Drawing', + '$path = $env:KIMI_WSL_CLIPBOARD_IMAGE_PATH', + '$img = [System.Windows.Forms.Clipboard]::GetImage()', + "if ($img) { $img.Save($path, [System.Drawing.Imaging.ImageFormat]::Png); Write-Output 'ok' } else { Write-Output 'empty' }", + ].join('; '); + + const result = runCommand('powershell.exe', ['-NoProfile', '-Command', psScript], { + timeoutMs: DEFAULT_POWERSHELL_TIMEOUT_MS, + env: { ...process.env, KIMI_WSL_CLIPBOARD_IMAGE_PATH: winPath }, + }); + if (!result.ok) return null; + if (result.stdout.toString('utf-8').trim() !== 'ok') return null; + + const bytes = readFileSync(tmpFile); + if (bytes.length === 0) return null; + return { kind: 'image', bytes: new Uint8Array(bytes), mimeType: 'image/png' }; + } catch { + return null; + } finally { + try { + unlinkSync(tmpFile); + } catch { + // ignore cleanup errors + } + } +} + +function readClipboardFilePathsViaMacOs(run: RunCommand): string[] { + const result = run('osascript', ['-l', 'JavaScript', '-e', MACOS_FILE_PATH_SCRIPT], { + timeoutMs: DEFAULT_LIST_TIMEOUT_MS, + }); + if (!result.ok || result.stdout.length === 0) return []; + return parseClipboardPaths(result.stdout.toString('utf-8')); +} + +function isFileLikeNativeFormat(format: string): boolean { + const f = format.toLowerCase(); + const base = baseMimeType(format); + return ( + f.includes('file-url') || + f.includes('file url') || + f.includes('nsfilenames') || + f.includes('com.apple.finder') || + base === 'text/uri-list' || + base === 'public.url' + ); +} + +function safeAvailableFormats(clip: ClipboardModule | null): string[] { + if (clip?.availableFormats === undefined) return []; + try { + return clip.availableFormats(); + } catch { + return []; + } +} + +async function readClipboardFileMediaViaNativeText( + clip: ClipboardModule | null, +): Promise<{ media: ClipboardMedia | null; lookedFileLike: boolean }> { + if (clip === null) return { media: null, lookedFileLike: false }; + + const formats = safeAvailableFormats(clip); + const lookedFileLike = formats.some(isFileLikeNativeFormat); + if (!lookedFileLike || clip.getText === undefined) { + return { media: null, lookedFileLike }; + } + + try { + return { media: readMediaFromText(await clip.getText()), lookedFileLike }; + } catch (error) { + if (error instanceof ClipboardMediaError) throw error; + return { media: null, lookedFileLike }; + } +} + +async function readClipboardImageViaNative( + clip: ClipboardModule | null = clipboard, +): Promise<ClipboardImage | null> { + if (clip === null) return null; + + let hasImage = false; + try { + hasImage = clip.hasImage(); + } catch { + return null; + } + if (!hasImage) return null; + + try { + const data = await clip.getImageBinary(); + if (data.length === 0) return null; + return { kind: 'image', bytes: Uint8Array.from(data), mimeType: 'image/png' }; + } catch { + return null; + } +} + +export async function readClipboardMedia(options?: { + env?: NodeJS.ProcessEnv; + platform?: NodeJS.Platform; + clipboard?: ClipboardModule | null; + runCommand?: RunCommand; +}): Promise<ClipboardMedia | null> { + const env = options?.env ?? process.env; + const platform = options?.platform ?? process.platform; + const clip = options?.clipboard ?? clipboard; + const run = options?.runCommand ?? runCommand; + + // Termux on Android has no desktop clipboard; skip early rather than + // churn through every fallback. + if (env['TERMUX_VERSION'] !== undefined) return null; + + let image: ClipboardImage | null = null; + if (platform === 'linux') { + const wayland = isWaylandSession(env); + const wsl = isWSL(env); + + if (wayland || wsl) { + const fileMedia = readClipboardFileMediaViaWlPaste() ?? readClipboardFileMediaViaXclip(); + if (fileMedia !== null) return fileMedia; + image = readClipboardImageViaWlPaste() ?? readClipboardImageViaXclip(); + } + if (image === null && wsl) { + image = readClipboardImageViaPowerShell(); + } + if (image === null && !wayland) { + const nativeFileMedia = await readClipboardFileMediaViaNativeText(clip); + if (nativeFileMedia.media !== null) return nativeFileMedia.media; + if (nativeFileMedia.lookedFileLike) return null; + image = await readClipboardImageViaNative(clip); + } + } else { + if (platform === 'darwin') { + const fileMedia = readMediaFromPaths(readClipboardFilePathsViaMacOs(run)); + if (fileMedia !== null) return fileMedia; + } + + const nativeFileMedia = await readClipboardFileMediaViaNativeText(clip); + if (nativeFileMedia.media !== null) return nativeFileMedia.media; + + // Finder exposes file icons/thumbnails as image data. If the clipboard + // looks file-like but we could not read a real file path, do not consume + // that icon as an image attachment. + if (platform === 'darwin' && nativeFileMedia.lookedFileLike) { + return null; + } + + image = await readClipboardImageViaNative(clip); + } + + if (image === null) return null; + if (!isSupportedImageMimeType(image.mimeType)) return null; + return image; +} diff --git a/apps/kimi-code/src/utils/clipboard/clipboard-native.ts b/apps/kimi-code/src/utils/clipboard/clipboard-native.ts new file mode 100644 index 000000000..051e9af46 --- /dev/null +++ b/apps/kimi-code/src/utils/clipboard/clipboard-native.ts @@ -0,0 +1,51 @@ +/** + * Optional native clipboard binding. + * + * `@mariozechner/clipboard` is a native Node binding that can read image + * binaries from the system clipboard on macOS and Windows. It's an + * optional dependency — if the native module fails to load (e.g. on a + * platform where no prebuilt is available) we degrade to shell-based + * fallbacks (wl-paste / xclip / PowerShell) in `clipboard-image.ts`. + */ + +import { createRequire } from 'node:module'; + +import { loadNativePackage } from '#/native/native-require'; + +declare const __KIMI_CODE_NATIVE_BUNDLE__: boolean | undefined; + +export interface ClipboardModule { + availableFormats?(): string[]; + hasText?(): boolean; + getText?(): Promise<string>; + hasImage(): boolean; + getImageBinary(): Promise<Array<number>>; +} + +const nodeRequire = createRequire(import.meta.url); +const isNativeBundle = + typeof __KIMI_CODE_NATIVE_BUNDLE__ === 'boolean' && __KIMI_CODE_NATIVE_BUNDLE__; + +// The native module uses X11/Wayland on Linux; if no display is +// available, skip the load attempt so headless environments don't pay +// the binding cost just to fail later. +const hasDisplay = + process.platform !== 'linux' || Boolean(process.env['DISPLAY'] ?? process.env['WAYLAND_DISPLAY']); + +const clipboard: ClipboardModule | null = (() => { + if (process.env['TERMUX_VERSION'] !== undefined || !hasDisplay) return null; + try { + const bundledClipboard = loadNativePackage<ClipboardModule>('@mariozechner/clipboard'); + if (bundledClipboard !== null) return bundledClipboard; + } catch { + return null; + } + if (isNativeBundle) return null; + try { + return nodeRequire('@mariozechner/clipboard') as ClipboardModule; + } catch { + return null; + } +})(); + +export { clipboard }; diff --git a/apps/kimi-code/src/utils/git/git-ls-files.ts b/apps/kimi-code/src/utils/git/git-ls-files.ts new file mode 100644 index 000000000..a68613284 --- /dev/null +++ b/apps/kimi-code/src/utils/git/git-ls-files.ts @@ -0,0 +1,189 @@ +/** + * Git-aware file listing + relevance signals with a short-TTL cache. + * Used as the cross-directory `@file` completion source when `fd` is + * not installed. + * + * Tracks three things per snapshot, all refreshed atomically: + * - `files` deduped (tracked + untracked-not-ignored), capped at 1000 + * - `mtimeByPath` absolute-path → fs mtime (ms), for recency ranking + * - `recencyOrder` file path → position in recent git history (0-indexed; smaller = more recent) + * + * Rebuild strategy: 2s TTL plus `.git/index` mtime invalidation so + * rapid edits surface without paying the full spawn+stat cost on every + * keystroke. When outside a git worktree, + * `getSnapshot()` returns `null` and callers fall back further. + */ + +import { spawnSync } from 'node:child_process'; +import { existsSync, statSync } from 'node:fs'; +import { join } from 'node:path'; + +const TTL_MS = 2000; +const MAX_ENTRIES = 1000; +// Number of most-recent commits to scan for "recently edited" hotness. +// 200 is enough to cover a week of active work in a typical repo while +// staying fast (<100ms even on large repos). +const RECENT_COMMIT_DEPTH = 200; + +export interface GitSnapshot { + readonly files: readonly string[]; + /** Absolute path → mtime (ms). Missing entries = stat failed. */ + readonly mtimeByPath: ReadonlyMap<string, number>; + /** Path → 0-indexed recency rank (earlier = more recent). */ + readonly recencyOrder: ReadonlyMap<string, number>; +} + +export interface GitLsFilesCache { + /** Full snapshot, or `null` when the work dir is not a git repo. */ + getSnapshot(): GitSnapshot | null; + /** Convenience shortcut; identical to `getSnapshot()?.files ?? null`. */ + list(): string[] | null; + isGitRepo(): boolean; +} + +interface SnapshotState { + snapshot: GitSnapshot; + fetchedAt: number; + indexMtime: number; +} + +export function createGitLsFilesCache(workDir: string): GitLsFilesCache { + const gitRoot = resolveGitRoot(workDir); + const indexPath = gitRoot === null ? null : join(gitRoot, '.git', 'index'); + let state: SnapshotState | undefined; + + return { + isGitRepo: () => gitRoot !== null, + getSnapshot: () => { + if (gitRoot === null) return null; + + const now = Date.now(); + const currentIndexMtime = indexMtime(indexPath); + const fresh = + state !== undefined && + now - state.fetchedAt < TTL_MS && + state.indexMtime === currentIndexMtime; + if (fresh) return state!.snapshot; + + const snapshot = fetchSnapshot(gitRoot); + if (snapshot === null) return null; // transient git failure — retry next call + + state = { snapshot, fetchedAt: now, indexMtime: currentIndexMtime }; + return snapshot; + }, + list: function listCompat() { + return this.getSnapshot()?.files.slice() ?? null; + }, + }; +} + +function resolveGitRoot(workDir: string): string | null { + try { + const result = spawnSync('git', ['-C', workDir, 'rev-parse', '--show-toplevel'], { + encoding: 'utf8', + }); + if (result.status !== 0) return null; + const stdout = result.stdout.trim(); + return stdout.length > 0 ? stdout : null; + } catch { + return null; + } +} + +function indexMtime(indexPath: string | null): number { + if (indexPath === null || !existsSync(indexPath)) return 0; + try { + return statSync(indexPath).mtimeMs; + } catch { + return 0; + } +} + +function fetchSnapshot(gitRoot: string): GitSnapshot | null { + const tracked = runLsFiles(gitRoot, ['-z']); + if (tracked === null) return null; + const untracked = runLsFiles(gitRoot, ['-z', '--others', '--exclude-standard']); + if (untracked === null) return null; + + const seen = new Set<string>(); + for (const path of tracked) seen.add(path); + for (const path of untracked) seen.add(path); + const merged = [...seen].toSorted(); + const files = merged.length > MAX_ENTRIES ? merged.slice(0, MAX_ENTRIES) : merged; + + const mtimeByPath = collectMtimes(gitRoot, files); + const recencyOrder = collectRecencyOrder(gitRoot, new Set(files)); + + return { files, mtimeByPath, recencyOrder }; +} + +function runLsFiles(gitRoot: string, args: readonly string[]): string[] | null { + try { + const result = spawnSync('git', ['-C', gitRoot, 'ls-files', ...args], { + encoding: 'utf8', + maxBuffer: 16 * 1024 * 1024, + }); + if (result.status !== 0) return null; + return result.stdout.split('\0').filter((entry) => entry.length > 0); + } catch { + return null; + } +} + +function collectMtimes(gitRoot: string, files: readonly string[]): Map<string, number> { + const result = new Map<string, number>(); + for (const path of files) { + try { + const stat = statSync(join(gitRoot, path)); + result.set(path, stat.mtimeMs); + } catch { + // File was deleted between ls-files and stat, or permission error. + // Missing entry → ranker treats it as "no mtime signal". + } + } + return result; +} + +/** + * Walk the last RECENT_COMMIT_DEPTH commits and record the first time + * each path is seen (a file touched in HEAD wins over a file touched + * 50 commits ago). Runs on the whole repo, not the work dir, so rename + * tracking stays consistent even when the user cd's into a subdir. + * + * `trackedSet` filters out paths that were renamed away / deleted — we + * only care about files that still appear in `ls-files`, since those + * are the ones we could actually complete. + */ +function collectRecencyOrder(gitRoot: string, trackedSet: Set<string>): Map<string, number> { + const result = new Map<string, number>(); + try { + const proc = spawnSync( + 'git', + [ + '-C', + gitRoot, + 'log', + `-n`, + String(RECENT_COMMIT_DEPTH), + '--name-only', + '--pretty=format:', + '--no-renames', + ], + { encoding: 'utf8', maxBuffer: 16 * 1024 * 1024 }, + ); + if (proc.status !== 0) return result; + let rank = 0; + for (const raw of proc.stdout.split('\n')) { + const line = raw.trim(); + if (line.length === 0) continue; + if (result.has(line)) continue; // keep the earliest (most recent) occurrence + if (!trackedSet.has(line)) continue; // drop deleted / renamed-away paths + result.set(line, rank); + rank += 1; + } + } catch { + // Fall through with whatever we've collected — an incomplete + // recency map just means fewer entries get a hotness boost. + } + return result; +} diff --git a/apps/kimi-code/src/utils/git/git-status.ts b/apps/kimi-code/src/utils/git/git-status.ts new file mode 100644 index 000000000..d78358b11 --- /dev/null +++ b/apps/kimi-code/src/utils/git/git-status.ts @@ -0,0 +1,351 @@ +/** + * Cached git branch + working-tree status for the footer/statusline. + * + * Branch name refreshes every 5s, porcelain status every 15s. Branch + * and status reads stay synchronous with short timeouts. Pull request + * lookup uses an async cache so a slow `gh pr view` never blocks + * footer rendering. + */ + +import { execFile, spawnSync } from 'node:child_process'; + +const BRANCH_TTL_MS = 5_000; +const STATUS_TTL_MS = 15_000; +const PULL_REQUEST_TTL_MS = 60_000; +const SPAWN_TIMEOUT_MS = 500; +const PR_SPAWN_TIMEOUT_MS = 5_000; + +export interface GitStatus { + readonly branch: string; + readonly dirty: boolean; + readonly ahead: number; + readonly behind: number; + readonly diffAdded: number; + readonly diffDeleted: number; + readonly pullRequest: PullRequestInfo | null; +} + +export interface PullRequestInfo { + readonly number: number; + readonly url: string; +} + +export interface GitStatusCache { + /** Returns current status, or `null` when workDir is not a git repo. */ + getStatus(): GitStatus | null; +} + +export interface GitStatusCacheOptions { + readonly onChange?: () => void; +} + +interface BranchState { + value: string | null; + fetchedAt: number; +} + +interface StatusState { + dirty: boolean; + ahead: number; + behind: number; + diffAdded: number; + diffDeleted: number; + fetchedAt: number; +} + +interface PullRequestState { + value: PullRequestInfo | null; + branch: string | null; + fetchedAt: number; + pendingBranch: string | null; + requestId: number; +} + +const AHEAD_BEHIND_RE = /\[(?:ahead (\d+))?(?:, )?(?:behind (\d+))?\]/; + +export function createGitStatusCache( + workDir: string, + options: GitStatusCacheOptions = {}, +): GitStatusCache { + const isRepo = detectGitRepo(workDir); + let branch: BranchState = { value: null, fetchedAt: 0 }; + let status: StatusState = { + dirty: false, + ahead: 0, + behind: 0, + diffAdded: 0, + diffDeleted: 0, + fetchedAt: 0, + }; + let pullRequest: PullRequestState = { + value: null, + branch: null, + fetchedAt: 0, + pendingBranch: null, + requestId: 0, + }; + + return { + getStatus: () => { + if (!isRepo) return null; + + const now = Date.now(); + if (now - branch.fetchedAt >= BRANCH_TTL_MS) { + branch = { value: readBranch(workDir), fetchedAt: now }; + } + if (branch.value === null) return null; + + if (now - status.fetchedAt >= STATUS_TTL_MS) { + status = { ...readStatus(workDir), fetchedAt: now }; + } + refreshPullRequestIfNeeded(branch.value, now); + + return { + branch: branch.value, + dirty: status.dirty, + ahead: status.ahead, + behind: status.behind, + diffAdded: status.diffAdded, + diffDeleted: status.diffDeleted, + pullRequest: pullRequest.branch === branch.value ? pullRequest.value : null, + }; + }, + }; + + function refreshPullRequestIfNeeded(branchName: string, now: number): void { + if (pullRequest.pendingBranch === branchName) return; + const fetchedAt = pullRequest.branch === branchName ? pullRequest.fetchedAt : 0; + if (now - fetchedAt < PULL_REQUEST_TTL_MS) return; + + const requestId = pullRequest.requestId + 1; + pullRequest = { + value: pullRequest.branch === branchName ? pullRequest.value : null, + branch: branchName, + fetchedAt, + pendingBranch: branchName, + requestId, + }; + + void readPullRequest(workDir).then((value) => { + if (pullRequest.requestId !== requestId) return; + + const previous = pullRequest.branch === branchName ? pullRequest.value : null; + const changed = !samePullRequest(previous, value); + pullRequest = { + value, + branch: branchName, + fetchedAt: Date.now(), + pendingBranch: null, + requestId, + }; + if (changed) options.onChange?.(); + }); + } +} + +function detectGitRepo(workDir: string): boolean { + try { + const result = spawnSync('git', ['-C', workDir, 'rev-parse', '--is-inside-work-tree'], { + encoding: 'utf8', + timeout: SPAWN_TIMEOUT_MS, + }); + return result.status === 0 && result.stdout.trim() === 'true'; + } catch { + return false; + } +} + +function readBranch(workDir: string): string | null { + try { + const result = spawnSync('git', ['-C', workDir, 'branch', '--show-current'], { + encoding: 'utf8', + timeout: SPAWN_TIMEOUT_MS, + }); + if (result.status !== 0) return null; + const name = result.stdout.trim(); + return name.length > 0 ? name : null; + } catch { + return null; + } +} + +function readStatus(workDir: string): { + dirty: boolean; + ahead: number; + behind: number; + diffAdded: number; + diffDeleted: number; +} { + try { + const result = spawnSync('git', ['-C', workDir, 'status', '--porcelain', '-b'], { + encoding: 'utf8', + timeout: SPAWN_TIMEOUT_MS, + maxBuffer: 4 * 1024 * 1024, + }); + if (result.status !== 0) { + return { dirty: false, ahead: 0, behind: 0, diffAdded: 0, diffDeleted: 0 }; + } + + let dirty = false; + let ahead = 0; + let behind = 0; + for (const line of result.stdout.split('\n')) { + if (line.startsWith('## ')) { + const m = AHEAD_BEHIND_RE.exec(line); + if (m) { + ahead = Number.parseInt(m[1] ?? '0', 10) || 0; + behind = Number.parseInt(m[2] ?? '0', 10) || 0; + } + } else if (line.trim().length > 0) { + dirty = true; + } + } + const diff = dirty ? readDiffStats(workDir) : { added: 0, deleted: 0 }; + return { + dirty, + ahead, + behind, + diffAdded: diff.added, + diffDeleted: diff.deleted, + }; + } catch { + return { dirty: false, ahead: 0, behind: 0, diffAdded: 0, diffDeleted: 0 }; + } +} + +function readDiffStats(workDir: string): { added: number; deleted: number } { + try { + const result = spawnSync('git', ['-C', workDir, 'diff', '--numstat', 'HEAD', '--'], { + encoding: 'utf8', + timeout: SPAWN_TIMEOUT_MS, + maxBuffer: 4 * 1024 * 1024, + }); + if (result.status !== 0) return { added: 0, deleted: 0 }; + + let added = 0; + let deleted = 0; + for (const line of result.stdout.split('\n')) { + if (!line) continue; + const [addedText, deletedText] = line.split('\t'); + added += parseDiffNumstatCount(addedText); + deleted += parseDiffNumstatCount(deletedText); + } + return { added, deleted }; + } catch { + return { added: 0, deleted: 0 }; + } +} + +function parseDiffNumstatCount(value: string | undefined): number { + if (value === undefined || value === '-') return 0; + const n = Number.parseInt(value, 10); + return Number.isFinite(n) && n > 0 ? n : 0; +} + +function readPullRequest(workDir: string): Promise<PullRequestInfo | null> { + return new Promise((resolve) => { + execFile( + 'gh', + ['pr', 'view', '--json', 'number,url'], + { + cwd: workDir, + encoding: 'utf8', + env: { + ...process.env, + GH_NO_UPDATE_NOTIFIER: '1', + GH_PROMPT_DISABLED: '1', + }, + timeout: PR_SPAWN_TIMEOUT_MS, + maxBuffer: 256 * 1024, + }, + (error, stdout) => { + if (error !== null) { + resolve(null); + return; + } + resolve(parsePullRequest(stdout)); + }, + ); + }); +} + +function samePullRequest(a: PullRequestInfo | null, b: PullRequestInfo | null): boolean { + if (a === null || b === null) return a === b; + return a.number === b.number && a.url === b.url; +} + +function parsePullRequest(stdout: string): PullRequestInfo | null { + try { + const raw = JSON.parse(stdout) as unknown; + if (typeof raw !== 'object' || raw === null) return null; + const record = raw as Record<string, unknown>; + const number = record['number']; + const url = record['url']; + if (typeof number !== 'number' || !Number.isInteger(number) || number <= 0) return null; + if (typeof url !== 'string' || !isSafeHttpUrl(url)) return null; + return { number, url }; + } catch { + return null; + } +} + +function isSafeHttpUrl(value: string): boolean { + if (hasControlChars(value)) return false; + try { + const url = new URL(value); + return url.protocol === 'https:' || url.protocol === 'http:'; + } catch { + return false; + } +} + +function hasControlChars(value: string): boolean { + for (const char of value) { + const code = char.codePointAt(0) ?? 0; + if (code <= 0x1f || code === 0x7f) return true; + } + return false; +} + +export interface FormatGitBadgeOptions { + readonly linkPullRequest?: boolean; +} + +export function formatGitBadgeBase(status: GitStatus): string { + const parts: string[] = []; + const diff = formatDiffStats(status); + if (diff) parts.push(diff); + let sync = ''; + if (status.ahead > 0) sync += `↑${status.ahead}`; + if (status.behind > 0) sync += `↓${status.behind}`; + if (sync) parts.push(sync); + return parts.length === 0 ? status.branch : `${status.branch} [${parts.join(' ')}]`; +} + +export function formatPullRequestBadge( + pullRequest: PullRequestInfo, + options: FormatGitBadgeOptions = {}, +): string { + const prText = `[PR#${String(pullRequest.number)}]`; + return options.linkPullRequest ? toTerminalHyperlink(prText, pullRequest.url) : prText; +} + +export function formatGitBadge(status: GitStatus, options: FormatGitBadgeOptions = {}): string { + const base = formatGitBadgeBase(status); + if (status.pullRequest === null) return base; + + return `${base} ${formatPullRequestBadge(status.pullRequest, options)}`; +} + +function formatDiffStats(status: GitStatus): string | null { + const parts: string[] = []; + if (status.diffAdded > 0) parts.push(`+${String(status.diffAdded)}`); + if (status.diffDeleted > 0) parts.push(`-${String(status.diffDeleted)}`); + if (parts.length > 0) return parts.join(' '); + return status.dirty ? '±' : null; +} + +function toTerminalHyperlink(text: string, url: string): string { + if (!isSafeHttpUrl(url)) return text; + return `\u001B]8;;${url}\u0007${text}\u001B]8;;\u0007`; +} diff --git a/apps/kimi-code/src/utils/history/input-history.ts b/apps/kimi-code/src/utils/history/input-history.ts new file mode 100644 index 000000000..950542563 --- /dev/null +++ b/apps/kimi-code/src/utils/history/input-history.ts @@ -0,0 +1,42 @@ +/** + * User input history persistence — JSONL file with `{"content": "..."}` per line. + * + * Semantics: + * - One JSON object per line (`InputHistoryEntry { content }`) + * - Append-only writes + * - Skip empty entries + * - Skip when same as last entry (consecutive deduplication) + * - Tolerate corrupt lines: log + skip, do not abort load + */ + +import { z } from 'zod'; + +import { appendJsonlLine, readJsonlFile } from '#/utils/persistence'; + +export interface InputHistoryEntry { + content: string; +} + +const InputHistoryEntrySchema: z.ZodType<InputHistoryEntry> = z.object({ + content: z.string(), +}); + +export async function loadInputHistory(file: string): Promise<InputHistoryEntry[]> { + return readJsonlFile(file, InputHistoryEntrySchema); +} + +/** + * Append an entry to the history file. Returns true if written, false if + * skipped (empty or equal to `lastContent`). + */ +export async function appendInputHistory( + file: string, + text: string, + lastContent?: string, +): Promise<boolean> { + const content = text.trim(); + if (content.length === 0) return false; + if (content === lastContent) return false; + await appendJsonlLine(file, InputHistoryEntrySchema, { content }); + return true; +} diff --git a/apps/kimi-code/src/utils/image/image-mime.ts b/apps/kimi-code/src/utils/image/image-mime.ts new file mode 100644 index 000000000..e7ceb97de --- /dev/null +++ b/apps/kimi-code/src/utils/image/image-mime.ts @@ -0,0 +1,189 @@ +/** + * Detect image MIME type + dimensions from raw bytes. + * + * Uses magic-byte sniffing for MIME and minimal format-specific parsing + * for dimensions. Only formats that the kimi-core multimodal pipeline + * accepts are supported: PNG / JPEG / GIF / WebP. + * + * Unsupported or truncated inputs return `null` so the caller can + * decline the paste cleanly. + */ + +export type SupportedImageMime = 'image/png' | 'image/jpeg' | 'image/gif' | 'image/webp'; + +export interface ImageMeta { + mime: SupportedImageMime; + width: number; + height: number; +} + +export function parseImageMeta(bytes: Uint8Array): ImageMeta | null { + if (isPng(bytes)) return parsePng(bytes); + if (isJpeg(bytes)) return parseJpeg(bytes); + if (isGif(bytes)) return parseGif(bytes); + if (isWebp(bytes)) return parseWebp(bytes); + return null; +} + +// ── PNG ───────────────────────────────────────────────────────────── + +function isPng(b: Uint8Array): boolean { + return ( + b.length >= 8 && + b[0] === 0x89 && + b[1] === 0x50 && + b[2] === 0x4e && + b[3] === 0x47 && + b[4] === 0x0d && + b[5] === 0x0a && + b[6] === 0x1a && + b[7] === 0x0a + ); +} + +function parsePng(b: Uint8Array): ImageMeta | null { + // IHDR chunk immediately follows the 8-byte signature: length (4) + + // "IHDR" (4) + width (4 BE) + height (4 BE) + ... + if (b.length < 24) return null; + const width = readUInt32BE(b, 16); + const height = readUInt32BE(b, 20); + if (width <= 0 || height <= 0) return null; + return { mime: 'image/png', width, height }; +} + +// ── JPEG ──────────────────────────────────────────────────────────── + +function isJpeg(b: Uint8Array): boolean { + return b.length >= 3 && b[0] === 0xff && b[1] === 0xd8 && b[2] === 0xff; +} + +function parseJpeg(b: Uint8Array): ImageMeta | null { + // Scan for a Start-Of-Frame marker (SOF0..SOF3, SOF5..SOF7, SOF9..SOF11, + // SOF13..SOF15). After the marker + 2-byte segment length come: + // precision (1), height (2 BE), width (2 BE), components (1). + let i = 2; + while (i < b.length) { + if (b[i] !== 0xff) { + i += 1; + continue; + } + // Skip fill bytes (0xFF padding before a marker). + while (i < b.length && b[i] === 0xff) i += 1; + if (i >= b.length) return null; + const marker = b[i]!; + i += 1; + if (marker === 0xd8 || marker === 0xd9) continue; // SOI / EOI — no length + if (i + 1 >= b.length) return null; + const segLen = readUInt16BE(b, i); + if (isSofMarker(marker)) { + if (i + 7 >= b.length) return null; + const height = readUInt16BE(b, i + 3); + const width = readUInt16BE(b, i + 5); + if (width <= 0 || height <= 0) return null; + return { mime: 'image/jpeg', width, height }; + } + i += segLen; + } + return null; +} + +function isSofMarker(marker: number): boolean { + if (marker < 0xc0 || marker > 0xcf) return false; + // Exclude DHT (0xC4), JPG (0xC8), DAC (0xCC) — these reuse the SOF + // number range but are not frame headers. + return marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc; +} + +// ── GIF ───────────────────────────────────────────────────────────── + +function isGif(b: Uint8Array): boolean { + return ( + b.length >= 6 && + b[0] === 0x47 && // G + b[1] === 0x49 && // I + b[2] === 0x46 && // F + b[3] === 0x38 && // 8 + (b[4] === 0x37 || b[4] === 0x39) && // 7 or 9 + b[5] === 0x61 // a + ); +} + +function parseGif(b: Uint8Array): ImageMeta | null { + // Logical screen width/height are at offsets 6-9, little-endian. + if (b.length < 10) return null; + const width = readUInt16LE(b, 6); + const height = readUInt16LE(b, 8); + if (width <= 0 || height <= 0) return null; + return { mime: 'image/gif', width, height }; +} + +// ── WebP ──────────────────────────────────────────────────────────── + +function isWebp(b: Uint8Array): boolean { + return ( + b.length >= 12 && + b[0] === 0x52 && // R + b[1] === 0x49 && // I + b[2] === 0x46 && // F + b[3] === 0x46 && // F + b[8] === 0x57 && // W + b[9] === 0x45 && // E + b[10] === 0x42 && // B + b[11] === 0x50 // P + ); +} + +function parseWebp(b: Uint8Array): ImageMeta | null { + // WebP has three sub-formats: VP8 (simple), VP8L (lossless), VP8X + // (extended). Offset 12 is the 4-byte chunk identifier. + if (b.length < 30) return null; + const chunk = String.fromCodePoint(b[12]!, b[13]!, b[14]!, b[15]!); + if (chunk === 'VP8 ') { + // Frame data starts at offset 20 + 3-byte start code; dimensions at + // offsets 26-29 (little-endian, 14-bit values with 2 top bits masked). + const widthRaw = readUInt16LE(b, 26); + const heightRaw = readUInt16LE(b, 28); + const width = widthRaw & 0x3fff; + const height = heightRaw & 0x3fff; + if (width <= 0 || height <= 0) return null; + return { mime: 'image/webp', width, height }; + } + if (chunk === 'VP8L') { + // VP8L packs width-1 (14 bits) and height-1 (14 bits) across bytes 21-24 + // starting from the signature byte at offset 20 (0x2F). + if (b[20] !== 0x2f) return null; + const b1 = b[21]!; + const b2 = b[22]!; + const b3 = b[23]!; + const b4 = b[24]!; + const width = 1 + (((b2 & 0x3f) << 8) | b1); + const height = 1 + (((b4 & 0x0f) << 10) | (b3 << 2) | ((b2 & 0xc0) >> 6)); + if (width <= 0 || height <= 0) return null; + return { mime: 'image/webp', width, height }; + } + if (chunk === 'VP8X') { + // Canvas width-1 at offsets 24-26, height-1 at 27-29 (24-bit LE). + const width = 1 + readUInt24LE(b, 24); + const height = 1 + readUInt24LE(b, 27); + if (width <= 0 || height <= 0) return null; + return { mime: 'image/webp', width, height }; + } + return null; +} + +// ── byte helpers ──────────────────────────────────────────────────── + +function readUInt16BE(b: Uint8Array, off: number): number { + return (b[off]! << 8) | b[off + 1]!; +} +function readUInt16LE(b: Uint8Array, off: number): number { + return b[off]! | (b[off + 1]! << 8); +} +function readUInt24LE(b: Uint8Array, off: number): number { + return b[off]! | (b[off + 1]! << 8) | (b[off + 2]! << 16); +} +function readUInt32BE(b: Uint8Array, off: number): number { + return Math.trunc( + b[off]! * 0x100_0000 + (b[off + 1]! << 16) + (b[off + 2]! << 8) + b[off + 3]!, + ); +} diff --git a/apps/kimi-code/src/utils/paths.ts b/apps/kimi-code/src/utils/paths.ts new file mode 100644 index 000000000..b0375d376 --- /dev/null +++ b/apps/kimi-code/src/utils/paths.ts @@ -0,0 +1,55 @@ +/** + * CLI-owned data path helpers. + * + * These paths are for local app data such as logs and input history. Config + * files are owned by Core/SDK and intentionally do not live behind this module. + */ + +import { createHash } from 'node:crypto'; +import { homedir } from 'node:os'; +import { join } from 'node:path'; + +import { + KIMI_CODE_DATA_DIR_NAME, + KIMI_CODE_HOME_ENV, + KIMI_CODE_INPUT_HISTORY_DIR_NAME, + KIMI_CODE_LOG_DIR_NAME, + KIMI_CODE_UPDATE_DIR_NAME, + KIMI_CODE_UPDATE_STATE_FILE_NAME, +} from '#/constant/app'; + +/** + * Return the root data directory for Kimi Code. + * + * Priority: `KIMI_CODE_HOME` env var > `~/.kimi-code`. + */ +export function getDataDir(): string { + const envDir = process.env[KIMI_CODE_HOME_ENV]; + if (envDir) { + return envDir; + } + return join(homedir(), KIMI_CODE_DATA_DIR_NAME); +} + +/** + * Return the diagnostic log directory: `<dataDir>/logs/`. + */ +export function getLogDir(): string { + return join(getDataDir(), KIMI_CODE_LOG_DIR_NAME); +} + +/** + * Return the update cache file: `<dataDir>/updates/latest.json`. + */ +export function getUpdateStateFile(): string { + return join(getDataDir(), KIMI_CODE_UPDATE_DIR_NAME, KIMI_CODE_UPDATE_STATE_FILE_NAME); +} + +/** + * Return the user input history file for a given working directory. + * Layout: `<share_dir>/user-history/<md5(cwd)>.jsonl`. + */ +export function getInputHistoryFile(workDir: string): string { + const hash = createHash('md5').update(workDir, 'utf-8').digest('hex'); + return join(getDataDir(), KIMI_CODE_INPUT_HISTORY_DIR_NAME, `${hash}.jsonl`); +} diff --git a/apps/kimi-code/src/utils/persistence.ts b/apps/kimi-code/src/utils/persistence.ts new file mode 100644 index 000000000..a458ae02a --- /dev/null +++ b/apps/kimi-code/src/utils/persistence.ts @@ -0,0 +1,105 @@ +/** + * Small persistence helpers for CLI-owned data files. + * + * This module is intentionally for non-config files only. User-facing + * configuration is owned by core/SDK; do not route `config.toml` through + * these helpers. + */ + +import { appendFile, mkdir, readFile, rename, unlink, writeFile } from 'node:fs/promises'; +import { basename, dirname, join } from 'node:path'; + +import type { z } from 'zod'; + +function isNotFound(error: unknown): boolean { + return ( + typeof error === 'object' && error !== null && (error as { code?: string }).code === 'ENOENT' + ); +} + +function assertNonConfigWrite(filePath: string): void { + if (basename(filePath) === 'config.toml') { + throw new Error( + 'CLI persistence helpers must not write config.toml; use core/SDK config APIs.', + ); + } +} + +function tempPathFor(filePath: string): string { + const dir = dirname(filePath); + const base = basename(filePath); + const nonce = `${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}`; + return join(dir, `.${base}.${nonce}.tmp`); +} + +export async function readJsonFile<T>( + filePath: string, + schema: z.ZodType<T>, + fallback: T, +): Promise<T> { + let raw: string; + try { + raw = await readFile(filePath, 'utf-8'); + } catch (error) { + if (isNotFound(error)) return fallback; + throw error; + } + const parsed = JSON.parse(raw) as unknown; + return schema.parse(parsed); +} + +export async function writeJsonFile<T>( + filePath: string, + schema: z.ZodType<T>, + value: T, +): Promise<void> { + assertNonConfigWrite(filePath); + const parsed = schema.parse(value); + await mkdir(dirname(filePath), { recursive: true }); + const tmpPath = tempPathFor(filePath); + try { + await writeFile(tmpPath, `${JSON.stringify(parsed, null, 2)}\n`, 'utf-8'); + await rename(tmpPath, filePath); + } catch (error) { + await unlink(tmpPath).catch(() => {}); + throw error; + } +} + +export async function readJsonlFile<T>( + filePath: string, + lineSchema: z.ZodType<T>, +): Promise<T[]> { + let raw: string; + try { + raw = await readFile(filePath, 'utf-8'); + } catch (error) { + if (isNotFound(error)) return []; + throw error; + } + + const entries: T[] = []; + for (const line of raw.split('\n')) { + const trimmed = line.trim(); + if (trimmed.length === 0) continue; + try { + const parsed = JSON.parse(trimmed) as unknown; + const result = lineSchema.safeParse(parsed); + if (result.success) entries.push(result.data); + } catch { + // JSONL is append-only user data; tolerate bad rows and keep the rest. + } + } + return entries; +} + +export async function appendJsonlLine<T>( + filePath: string, + lineSchema: z.ZodType<T>, + value: T, +): Promise<void> { + assertNonConfigWrite(filePath); + const parsed = lineSchema.parse(value); + await mkdir(dirname(filePath), { recursive: true }); + await appendFile(filePath, `${JSON.stringify(parsed)}\n`, 'utf-8'); +} diff --git a/apps/kimi-code/src/utils/process/external-editor.ts b/apps/kimi-code/src/utils/process/external-editor.ts new file mode 100644 index 000000000..7aa895d3c --- /dev/null +++ b/apps/kimi-code/src/utils/process/external-editor.ts @@ -0,0 +1,60 @@ +/** + * External-editor helper — spawn $VISUAL / $EDITOR (or a configured + * command) on a temp file seeded with the current editor buffer, then + * read the edited contents back. + * + * Resolution priority: + * configured (from Core/SDK defaults or `/editor`) > + * $VISUAL > $EDITOR > undefined (caller handles "no editor" toast). + */ + +import { spawn } from 'node:child_process'; +import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +export function resolveEditorCommand(configured?: string | null): string | undefined { + const candidates = [configured, process.env['VISUAL'], process.env['EDITOR']]; + for (const c of candidates) { + if (typeof c === 'string' && c.trim().length > 0) { + return c.trim(); + } + } + return undefined; +} + +/** + * Launch `command` (tokenised via a shell) against a temp file seeded + * with `initialText`. Returns the edited contents on success, or + * `undefined` if the editor exited non-zero / the file disappeared. + * + * The command is passed to `/bin/sh -c "<cmd> <tmpfile>"` so users can + * supply argv-style strings like `"code --wait"` or `"nvim +set ft=markdown"`. + */ +export async function editInExternalEditor( + initialText: string, + command: string, +): Promise<string | undefined> { + const dir = await mkdtemp(join(tmpdir(), 'kimi-edit-')); + const file = join(dir, 'prompt.md'); + await writeFile(file, initialText, 'utf-8'); + try { + const code = await new Promise<number>((resolve, reject) => { + const shellCmd = `${command} ${shellQuote(file)}`; + const child = spawn('/bin/sh', ['-c', shellCmd], { stdio: 'inherit' }); + child.on('exit', (c) =>{ resolve(c ?? 0); }); + child.on('error', reject); + }); + if (code !== 0) return undefined; + return await readFile(file, 'utf-8'); + } finally { + await rm(dir, { recursive: true, force: true }).catch(() => { + // best-effort cleanup + }); + } +} + +function shellQuote(path: string): string { + // Single-quote and escape any embedded single quotes. + return `'${path.replaceAll('\'', "'\\''")}'`; +} diff --git a/apps/kimi-code/src/utils/process/fd-detect.ts b/apps/kimi-code/src/utils/process/fd-detect.ts new file mode 100644 index 000000000..d8fc662b5 --- /dev/null +++ b/apps/kimi-code/src/utils/process/fd-detect.ts @@ -0,0 +1,28 @@ +/** + * Probe for the `fd` binary so the pi-tui `CombinedAutocompleteProvider` + * can enable its cross-directory fuzzy file search. + * + * Naming differs across distros: + * - Homebrew / Arch / most Linuxes: `fd` + * - Debian / Ubuntu: `fdfind` + * + * We use `spawnSync(..., { stdio: 'ignore' })` rather than shelling out + * to `which` so the check doesn't depend on the parent shell's PATH + * resolution semantics and stays cheap (~ms) on startup. + */ + +import { spawnSync } from 'node:child_process'; + +const CANDIDATES = ['fd', 'fdfind']; + +export function detectFdPath(): string | null { + for (const name of CANDIDATES) { + try { + const result = spawnSync(name, ['--version'], { stdio: 'ignore' }); + if (result.status === 0) return name; + } catch { + // ENOENT, EACCES, etc. — try next candidate + } + } + return null; +} diff --git a/apps/kimi-code/src/utils/process/proctitle.ts b/apps/kimi-code/src/utils/process/proctitle.ts new file mode 100644 index 000000000..b731233af --- /dev/null +++ b/apps/kimi-code/src/utils/process/proctitle.ts @@ -0,0 +1,31 @@ +/** + * Early-startup process name initialization. + * + * Sets the process title so `ps`/`top` and the terminal tab show + * `Kimi Code` from the moment the binary launches — before Commander + * parses argv, before any preflight, even on `--help`/`--version`. + * + * OSC is written to stderr (not stdout) so it still reaches the terminal + * when stdout is piped, e.g. `kimi --print | grep ...`. + */ +import { PRODUCT_NAME } from '#/constant/app'; +import { BEL, ESC } from '#/constant/terminal'; + +export function setProcessTitle(label: string): void { + try { + process.title = label; + } catch { + /* noop */ + } + try { + if (process.stderr.isTTY) { + process.stderr.write(`${ESC}]0;${label}${BEL}`); + } + } catch { + /* noop */ + } +} + +export function initProcessName(name: string = PRODUCT_NAME): void { + setProcessTitle(name); +} diff --git a/apps/kimi-code/src/utils/process/stdin.ts b/apps/kimi-code/src/utils/process/stdin.ts new file mode 100644 index 000000000..c59792f2a --- /dev/null +++ b/apps/kimi-code/src/utils/process/stdin.ts @@ -0,0 +1,18 @@ +import { createInterface } from 'node:readline'; + +export function readStdinText(): Promise<string> { + return new Promise((resolve, reject) => { + const chunks: Buffer[] = []; + process.stdin.on('data', (chunk: Buffer) => chunks.push(chunk)); + process.stdin.on('end', () =>{ resolve(Buffer.concat(chunks).toString('utf-8').trim()); }); + process.stdin.on('error', reject); + process.stdin.resume(); + }); +} + +export async function* createStdinLineReader(): AsyncIterable<string> { + const rl = createInterface({ input: process.stdin, crlfDelay: Infinity }); + for await (const line of rl) { + yield line; + } +} diff --git a/apps/kimi-code/src/utils/usage/usage-format.ts b/apps/kimi-code/src/utils/usage/usage-format.ts new file mode 100644 index 000000000..34db897a2 --- /dev/null +++ b/apps/kimi-code/src/utils/usage/usage-format.ts @@ -0,0 +1,37 @@ +/** + * Formatting helpers for the `/usage` slash command. + * + * Kept pure + ANSI-free so they're trivial to unit-test; the slash + * command itself chalks the colour afterwards. + */ + +export function formatTokenCount(n: number): string { + if (!Number.isFinite(n) || n < 0) return '0'; + if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`; + if (n >= 1_000) return `${(n / 1_000).toFixed(1)}k`; + return String(Math.round(n)); +} + +/** + * Build a `[███░░░░░░░]` style bar. Returns a plain-ASCII string with + * `filled`/`empty` glyphs — colouring is the caller's responsibility. + */ +export function renderProgressBar(ratio: number, width = 20, filled = '█', empty = '░'): string { + const clamped = safeUsageRatio(ratio); + const filledCount = Math.round(clamped * width); + return filled.repeat(filledCount) + empty.repeat(Math.max(0, width - filledCount)); +} + +export function safeUsageRatio(ratio: number): number { + return Number.isFinite(ratio) ? Math.max(0, Math.min(ratio, 1)) : 0; +} + +/** + * Map a usage ratio to a semantic colour token — the `/usage` renderer + * translates these into palette hex values. + */ +export function ratioSeverity(ratio: number): 'ok' | 'warn' | 'danger' { + if (ratio >= 0.85) return 'danger'; + if (ratio >= 0.5) return 'warn'; + return 'ok'; +} diff --git a/apps/kimi-code/test/cli/export.test.ts b/apps/kimi-code/test/cli/export.test.ts new file mode 100644 index 000000000..4a124cc80 --- /dev/null +++ b/apps/kimi-code/test/cli/export.test.ts @@ -0,0 +1,446 @@ +/** + * `kimi export` + * + * Verifies the CLI layer: argument handling, previous-session confirmation, + * error reporting, and delegation to the session export implementation. + */ + +import { mkdtempSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +import type { createKimiDeviceId as createKimiDeviceIdFn } from '@moonshot-ai/kimi-code-oauth'; +import { Command } from 'commander'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import { handleExport, registerExportCommand } from '#/cli/sub/export'; +import type { ExportDeps } from '#/cli/sub/export'; +import type { + ExportSessionInput, + ExportSessionManifest, + ExportSessionResult, + SessionSummary, +} from '@moonshot-ai/kimi-code-sdk'; + +let tmp: string; + +type CreateKimiDeviceId = typeof createKimiDeviceIdFn; + +const mocks = vi.hoisted(() => ({ + kimiHarnessConstructor: vi.fn(), + harnessEnsureConfigFile: vi.fn(), + harnessGetConfig: vi.fn(async () => ({ + providers: {}, + defaultModel: 'k2', + telemetry: true, + })), + harnessGetCachedAccessToken: vi.fn(), + harnessExportSession: vi.fn(), + createKimiDeviceId: vi.fn<CreateKimiDeviceId>(() => 'device-1'), + initializeTelemetry: vi.fn(), + shutdownTelemetry: vi.fn(), + telemetryTrack: vi.fn(), + setTelemetryContext: vi.fn(), + withTelemetryContext: vi.fn(), +})); + +vi.mock('@moonshot-ai/kimi-code-sdk', async (importOriginal) => { + const actual = await importOriginal<typeof import('@moonshot-ai/kimi-code-sdk')>(); + return { + ...actual, + KimiHarness: class { + homeDir = '/tmp/kimi-export-home'; + auth = { + getCachedAccessToken: mocks.harnessGetCachedAccessToken, + }; + ensureConfigFile = mocks.harnessEnsureConfigFile; + getConfig = mocks.harnessGetConfig; + constructor(...args: unknown[]) { + mocks.kimiHarnessConstructor(...args); + } + + exportSession = mocks.harnessExportSession; + }, + }; +}); + +vi.mock('@moonshot-ai/kimi-code-oauth', async () => { + const actual = await vi.importActual<typeof import('@moonshot-ai/kimi-code-oauth')>( + '@moonshot-ai/kimi-code-oauth', + ); + return { + ...actual, + createKimiDeviceId: mocks.createKimiDeviceId, + KIMI_CODE_PROVIDER_NAME: 'kimi-code', + }; +}); + +vi.mock('@moonshot-ai/kimi-telemetry', () => ({ + initializeTelemetry: mocks.initializeTelemetry, + shutdownTelemetry: mocks.shutdownTelemetry, + track: mocks.telemetryTrack, + setTelemetryContext: mocks.setTelemetryContext, + withTelemetryContext: mocks.withTelemetryContext, +})); + +beforeEach(() => { + tmp = mkdtempSync(join(tmpdir(), 'kimi-export-')); +}); + +afterEach(() => { + rmSync(tmp, { recursive: true, force: true }); + vi.clearAllMocks(); + mocks.harnessGetConfig.mockResolvedValue({ + providers: {}, + defaultModel: 'k2', + telemetry: true, + }); +}); + +function makeSummary(id: string, overrides: Partial<SessionSummary> = {}): SessionSummary { + return { + id, + workDir: tmp, + sessionDir: join(tmp, 'sessions', id), + createdAt: 1, + updatedAt: 2, + ...overrides, + }; +} + +function makeResult(id: string, zipPath: string): ExportSessionResult { + const manifest: ExportSessionManifest = { + sessionId: id, + exportedAt: '2026-04-18T12:00:00.000Z', + kimiCodeVersion: '1.27.0', + wireProtocolVersion: '1.0', + os: 'test', + nodejsVersion: '22.0.0', + workspaceDir: tmp, + }; + return { + zipPath, + entries: ['manifest.json', 'wire.jsonl'], + sessionDir: join(tmp, 'sessions', id), + manifest, + }; +} + +function makeDeps(overrides: Partial<ExportDeps> = {}): { + deps: ExportDeps; + stdout: string[]; + stderr: string[]; + exitCodes: number[]; + exportInputs: ExportSessionInput[]; + listedWorkDirs: string[]; +} { + const stdout: string[] = []; + const stderr: string[] = []; + const exitCodes: number[] = []; + const exportInputs: ExportSessionInput[] = []; + const listedWorkDirs: string[] = []; + const deps: ExportDeps = { + listSessions: async (workDir) => { + listedWorkDirs.push(workDir); + return []; + }, + exportSession: async (input) => { + exportInputs.push(input); + return makeResult(input.id, input.outputPath ?? join(tmp, `${input.id}.zip`)); + }, + confirmPreviousSession: async () => true, + version: '1.0.0-test', + cwd: () => tmp, + stdout: { + write: (chunk: string) => { + stdout.push(chunk); + return true; + }, + }, + stderr: { + write: (chunk: string) => { + stderr.push(chunk); + return true; + }, + }, + exit: ((code: number) => { + exitCodes.push(code); + throw new ExitCalled(code); + }) as ExportDeps['exit'], + ...overrides, + }; + return { deps, stdout, stderr, exitCodes, exportInputs, listedWorkDirs }; +} + +class ExitCalled extends Error { + constructor(public readonly code: number) { + super(`exit(${code})`); + } +} + +async function runExport( + deps: ExportDeps, + args: { + sessionId?: string; + output?: string; + yes?: boolean; + includeGlobalLog?: boolean; + } = {}, +): Promise<void> { + try { + await handleExport(deps, args.sessionId, args.output, { + yes: args.yes ?? false, + includeGlobalLog: args.includeGlobalLog ?? true, + }); + } catch (error) { + if (error instanceof ExitCalled) return; + throw error; + } +} + +describe('kimi export', () => { + it('delegates a named session export and prints the resulting zip path', async () => { + const output = join(tmp, 'out.zip'); + const { deps, stdout, stderr, exitCodes, exportInputs, listedWorkDirs } = makeDeps(); + + await runExport(deps, { sessionId: 'ses_test123456', output }); + + expect(exitCodes).toEqual([]); + expect(stderr).toEqual([]); + expect(listedWorkDirs).toEqual([]); + expect(exportInputs).toEqual([{ id: 'ses_test123456', outputPath: output, includeGlobalLog: true, version: '1.0.0-test' }]); + expect(stdout.join('').trim()).toBe(output); + }); + + it('omits outputPath when the caller does not provide --output', async () => { + const { deps, stdout, exportInputs } = makeDeps(); + + await runExport(deps, { sessionId: 'session_default_output' }); + + expect(exportInputs).toEqual([{ id: 'session_default_output', includeGlobalLog: true, version: '1.0.0-test' }]); + expect(stdout.join('').trim()).toBe(join(tmp, 'session_default_output.zip')); + }); + + it('exits 1 when no session-id is provided and no previous session exists', async () => { + const { deps, stderr, exitCodes, exportInputs, listedWorkDirs } = makeDeps(); + + await runExport(deps); + + expect(listedWorkDirs).toEqual([tmp]); + expect(exportInputs).toEqual([]); + expect(exitCodes).toContain(1); + expect(stderr.join('').toLowerCase()).toContain('no previous session'); + }); + + it('surfaces export errors for a named session', async () => { + const { deps, stderr, exitCodes } = makeDeps({ + exportSession: async () => { + throw new Error('Session "ses_does_not_exist" was not found'); + }, + }); + + await runExport(deps, { sessionId: 'ses_does_not_exist' }); + + expect(exitCodes).toContain(1); + expect(stderr.join('').toLowerCase()).toContain('not found'); + }); + + it('falls back to the most-recent session when no id is supplied', async () => { + const previous = makeSummary('ses_fallback'); + const output = join(tmp, 'fallback.zip'); + const { deps, stdout, exitCodes, exportInputs } = makeDeps({ + listSessions: async () => [previous], + }); + + await runExport(deps, { output }); + + expect(exitCodes).toEqual([]); + expect(exportInputs).toEqual([{ id: 'ses_fallback', outputPath: output, includeGlobalLog: true, version: '1.0.0-test' }]); + expect(stdout.join('').trim()).toBe(output); + }); + + it('confirms before exporting the previous session when no id is supplied', async () => { + const previous = makeSummary('ses_confirm', { title: 'Prod debug' }); + const summaries: unknown[] = []; + const { deps, stdout, exitCodes, exportInputs } = makeDeps({ + listSessions: async () => [previous], + confirmPreviousSession: async (summary) => { + summaries.push(summary); + return false; + }, + }); + + await runExport(deps, { output: join(tmp, 'cancelled.zip') }); + + expect(exitCodes).toEqual([]); + expect(exportInputs).toEqual([]); + expect(stdout.join('')).toContain('Export cancelled.'); + expect(summaries).toEqual([ + { + workDir: tmp, + sessionId: 'ses_confirm', + sessionDir: join(tmp, 'sessions', 'ses_confirm'), + title: 'Prod debug', + }, + ]); + }); + + it('skips previous-session confirmation with --yes', async () => { + const previous = makeSummary('ses_yes'); + const { deps, exitCodes, exportInputs } = makeDeps({ + listSessions: async () => [previous], + confirmPreviousSession: async () => { + throw new Error('confirm should not be called'); + }, + }); + + await runExport(deps, { output: join(tmp, 'yes.zip'), yes: true }); + + expect(exitCodes).toEqual([]); + expect(exportInputs).toEqual([{ id: 'ses_yes', outputPath: join(tmp, 'yes.zip'), includeGlobalLog: true, version: '1.0.0-test' }]); + }); + + it('describes the user-facing command without implementation details', () => { + const program = new Command('kimi'); + const { deps } = makeDeps(); + + registerExportCommand(program, deps); + + const command = program.commands.find((item) => item.name() === 'export'); + expect(command?.description()).toBe('Export a session as a ZIP archive.'); + expect(command?.description()).not.toMatch(/sdk/i); + }); + + it('parses --no-include-global-log as an option when no session id is given', async () => { + const previous = makeSummary('ses_global_log'); + const { deps, stdout, exitCodes, exportInputs } = makeDeps({ + listSessions: async () => [previous], + confirmPreviousSession: async () => true, + }); + const program = new Command('kimi'); + registerExportCommand(program, deps); + + await program.parseAsync(['node', 'kimi', 'export', '--no-include-global-log', '-y']); + + expect(exitCodes).toEqual([]); + expect(exportInputs).toEqual([{ id: 'ses_global_log', version: '1.0.0-test' }]); + expect(stdout.join('').trim()).toBe(join(tmp, 'ses_global_log.zip')); + }); + + it('parses options after an explicit session id', async () => { + const output = join(tmp, 'after-id.zip'); + const { deps, exitCodes, exportInputs } = makeDeps(); + const program = new Command('kimi'); + registerExportCommand(program, deps); + + await program.parseAsync([ + 'node', + 'kimi', + 'export', + 'ses_after_id', + '-o', + output, + '-y', + '--no-include-global-log', + ]); + + expect(exitCodes).toEqual([]); + expect(exportInputs).toEqual([ + { id: 'ses_after_id', outputPath: output, version: '1.0.0-test' }, + ]); + }); + + it('initializes and flushes telemetry around default export tracking', async () => { + const program = new Command('kimi'); + const output = join(tmp, 'telemetry.zip'); + mocks.harnessExportSession.mockResolvedValue(makeResult('ses_telemetry', output)); + + registerExportCommand(program, { + cwd: () => tmp, + stdout: { + write: () => true, + }, + stderr: { + write: () => true, + }, + exit: ((code: number) => { + throw new ExitCalled(code); + }) as ExportDeps['exit'], + }); + + await program.parseAsync(['node', 'kimi', 'export', 'ses_telemetry', '--output', output], { + from: 'node', + }); + + expect(mocks.kimiHarnessConstructor).toHaveBeenCalledWith( + expect.objectContaining({ + telemetry: { + track: mocks.telemetryTrack, + setContext: mocks.setTelemetryContext, + withContext: mocks.withTelemetryContext, + }, + }), + ); + expect(mocks.harnessEnsureConfigFile).toHaveBeenCalledOnce(); + expect(mocks.harnessGetConfig).toHaveBeenCalledOnce(); + expect(mocks.createKimiDeviceId).toHaveBeenCalledWith('/tmp/kimi-export-home'); + expect(mocks.initializeTelemetry).toHaveBeenCalledWith({ + homeDir: '/tmp/kimi-export-home', + deviceId: 'device-1', + enabled: true, + appName: 'kimi-code-cli', + version: expect.any(String), + uiMode: 'shell', + model: 'k2', + getAccessToken: expect.any(Function), + }); + expect(mocks.initializeTelemetry.mock.invocationCallOrder[0]).toBeLessThan( + mocks.harnessExportSession.mock.invocationCallOrder[0]!, + ); + expect(mocks.harnessExportSession).toHaveBeenCalledWith({ + id: 'ses_telemetry', + outputPath: output, + version: expect.any(String), + includeGlobalLog: true, + }); + expect(mocks.shutdownTelemetry).toHaveBeenCalledWith({ timeoutMs: 3000 }); + expect(mocks.harnessExportSession.mock.invocationCallOrder[0]).toBeLessThan( + mocks.shutdownTelemetry.mock.invocationCallOrder[0]!, + ); + }); + + it('passes enabled false when default export config disables telemetry', async () => { + const program = new Command('kimi'); + const output = join(tmp, 'telemetry-disabled.zip'); + mocks.harnessGetConfig.mockResolvedValue({ + providers: {}, + defaultModel: 'k2', + telemetry: false, + }); + mocks.harnessExportSession.mockResolvedValue(makeResult('ses_disabled', output)); + + registerExportCommand(program, { + cwd: () => tmp, + stdout: { + write: () => true, + }, + stderr: { + write: () => true, + }, + exit: ((code: number) => { + throw new ExitCalled(code); + }) as ExportDeps['exit'], + }); + + await program.parseAsync(['node', 'kimi', 'export', 'ses_disabled', '--output', output], { + from: 'node', + }); + + expect(mocks.initializeTelemetry).toHaveBeenCalledWith( + expect.objectContaining({ + enabled: false, + }), + ); + expect(mocks.shutdownTelemetry).toHaveBeenCalledWith({ timeoutMs: 3000 }); + }); +}); diff --git a/apps/kimi-code/test/cli/main.test.ts b/apps/kimi-code/test/cli/main.test.ts new file mode 100644 index 000000000..8ab1ea976 --- /dev/null +++ b/apps/kimi-code/test/cli/main.test.ts @@ -0,0 +1,211 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { ErrorCodes, KimiError } from '@moonshot-ai/kimi-code-sdk'; + +import { validateOptions } from '#/cli/options'; +import type { CLIOptions } from '#/cli/options'; +import type * as OptionsModule from '#/cli/options'; +import { runPrompt } from '#/cli/run-prompt'; +import { runShell } from '#/cli/run-shell'; +import { formatStartupError } from '#/cli/startup-error'; +import { runUpdatePreflight } from '#/cli/update/preflight'; +import { handleMainCommand, main } from '#/main'; + +const mocks = vi.hoisted(() => { + const parse = vi.fn(); + return { + parse, + createProgram: vi.fn(() => ({ parse })), + getVersion: vi.fn(() => '0.0.1-alpha.2'), + validateOptions: vi.fn(), + runUpdatePreflight: vi.fn(), + runShell: vi.fn(), + runPrompt: vi.fn(), + installCrashHandlers: vi.fn(), + }; +}); + +vi.mock('@moonshot-ai/kimi-telemetry', () => ({ + installCrashHandlers: mocks.installCrashHandlers, + track: vi.fn(), +})); + +vi.mock('../../src/cli/commands', () => ({ + createProgram: mocks.createProgram, +})); + +vi.mock('../../src/cli/version', () => ({ + getVersion: mocks.getVersion, +})); + +vi.mock('../../src/cli/options', async () => { + const actual = await vi.importActual<typeof OptionsModule>('../../src/cli/options.js'); + return { + ...actual, + validateOptions: mocks.validateOptions, + }; +}); + +vi.mock('../../src/cli/update/preflight', () => ({ + runUpdatePreflight: mocks.runUpdatePreflight, +})); + +vi.mock('../../src/cli/run-shell', () => ({ + runShell: mocks.runShell, +})); + +vi.mock('../../src/cli/run-prompt', () => ({ + runPrompt: mocks.runPrompt, +})); + +class ExitCalled extends Error { + constructor(readonly code: number) { + super(`exit(${code})`); + } +} + +function defaultOpts(): CLIOptions { + return { + session: undefined, + continue: false, + yolo: false, + plan: false, + model: undefined, + outputFormat: undefined, + prompt: undefined, + skillsDirs: [], + }; +} + +async function runHandleMainCommand(opts: CLIOptions): Promise<number | null> { + const exitSpy = vi.spyOn(process, 'exit').mockImplementation((code?: string | number | null) => { + throw new ExitCalled(Number(code ?? 0)); + }); + try { + await handleMainCommand(opts, '0.0.1-alpha.2'); + return null; + } catch (error) { + if (error instanceof ExitCalled) { + return error.code; + } + throw error; + } finally { + exitSpy.mockRestore(); + } +} + +describe('main entry command handling', () => { + afterEach(() => { + vi.clearAllMocks(); + }); + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('runs update preflight before starting the shell', async () => { + const opts = defaultOpts(); + mocks.validateOptions.mockReturnValue({ options: opts, uiMode: 'shell' }); + mocks.runUpdatePreflight.mockResolvedValue('continue'); + mocks.runShell.mockResolvedValue(void 0); + + const exitCode = await runHandleMainCommand(opts); + + expect(exitCode).toBeNull(); + expect(validateOptions).toHaveBeenCalledWith(opts); + expect(runUpdatePreflight).toHaveBeenCalledWith('0.0.1-alpha.2', { track: expect.any(Function) }); + expect(mocks.runUpdatePreflight.mock.invocationCallOrder[0]).toBeLessThan( + mocks.runShell.mock.invocationCallOrder[0]!, + ); + expect(runShell).toHaveBeenCalledWith(opts, '0.0.1-alpha.2'); + }); + + it('runs prompt mode without interactive update preflight', async () => { + const opts: CLIOptions = { + ...defaultOpts(), + prompt: 'explain the repo', + }; + mocks.validateOptions.mockReturnValue({ options: opts, uiMode: 'print' }); + mocks.runUpdatePreflight.mockResolvedValue('continue'); + mocks.runPrompt.mockResolvedValue(void 0); + + const exitCode = await runHandleMainCommand(opts); + + expect(exitCode).toBeNull(); + expect(runUpdatePreflight).toHaveBeenCalledWith('0.0.1-alpha.2', { + track: expect.any(Function), + isTTY: false, + }); + expect(runPrompt).toHaveBeenCalledWith(opts, '0.0.1-alpha.2'); + expect(runShell).not.toHaveBeenCalled(); + }); + + it('keeps shell mode update preflight interactive by default', async () => { + const opts = defaultOpts(); + mocks.validateOptions.mockReturnValue({ options: opts, uiMode: 'shell' }); + mocks.runUpdatePreflight.mockResolvedValue('continue'); + mocks.runShell.mockResolvedValue(void 0); + + const exitCode = await runHandleMainCommand(opts); + + expect(exitCode).toBeNull(); + expect(runUpdatePreflight).toHaveBeenCalledWith('0.0.1-alpha.2', { + track: expect.any(Function), + }); + expect(runShell).toHaveBeenCalledWith(opts, '0.0.1-alpha.2'); + }); + + it('installs crash handlers before parsing CLI arguments', () => { + main(); + + expect(mocks.installCrashHandlers).toHaveBeenCalledTimes(1); + expect(mocks.installCrashHandlers.mock.invocationCallOrder[0]).toBeLessThan( + mocks.createProgram.mock.invocationCallOrder[0]!, + ); + expect(mocks.parse).toHaveBeenCalledWith(process.argv); + }); + + it('exits early when update preflight requests process exit', async () => { + const opts = defaultOpts(); + mocks.validateOptions.mockReturnValue({ options: opts, uiMode: 'shell' }); + mocks.runUpdatePreflight.mockResolvedValue('exit'); + mocks.runShell.mockResolvedValue(void 0); + + const exitCode = await runHandleMainCommand(opts); + + expect(exitCode).toBe(0); + expect(runShell).not.toHaveBeenCalled(); + }); + + it('formats Kimi startup errors with structured fields', () => { + const error = new KimiError( + ErrorCodes.SHELL_GIT_BASH_NOT_FOUND, + 'Git Bash was not found on this Windows host. Checked: C:\\Program Files\\Git\\bin\\bash.exe.', + ); + const red = (text: string): string => `\u001B[31m${text}\u001B[39m`; + + expect(formatStartupError(error, { errorStyle: red })).toBe( + [ + '\u001B[31merror: Git Bash not found\u001B[39m', + '', + '\u001B[31mmessage:\u001B[39m', + '\u001B[31mGit Bash was not found on this Windows host. Checked: C:\\Program Files\\Git\\bin\\bash.exe.\u001B[39m', + '', + ].join('\n'), + ); + }); + + it('keeps generic startup errors on the legacy fallback path', () => { + expect(formatStartupError(new Error('Provider not set'), { errorStyle: (text) => text })).toBe( + 'error: failed to start shell: Provider not set\n', + ); + }); + + it('formats generic prompt mode errors without saying shell', () => { + expect( + formatStartupError(new Error('Provider not set'), { + errorStyle: (text) => text, + operation: 'run prompt', + }), + ).toBe('error: failed to run prompt: Provider not set\n'); + }); +}); diff --git a/apps/kimi-code/test/cli/options.test.ts b/apps/kimi-code/test/cli/options.test.ts new file mode 100644 index 000000000..a98268ced --- /dev/null +++ b/apps/kimi-code/test/cli/options.test.ts @@ -0,0 +1,256 @@ +import { describe, expect, it } from 'vitest'; + +import { createProgram } from '#/cli/commands'; +import type { CLIOptions } from '#/cli/options'; +import { OptionConflictError, validateOptions } from '#/cli/options'; + +function parse(argv: string[]): CLIOptions { + let captured: CLIOptions | undefined; + + const program = createProgram( + '0.1.0-test', + (opts) => { + captured = opts; + }, + () => {}, + ); + + program.exitOverride(); + program.configureOutput({ + writeOut: () => {}, + writeErr: () => {}, + }); + + program.parse(['node', 'kimi', ...argv]); + + if (captured === undefined) { + throw new Error('Main action handler was not called'); + } + return captured; +} + +describe('CLI options parsing', () => { + describe('defaults', () => { + it('returns defaults when no arguments are given', () => { + const opts = parse([]); + expect(opts.yolo).toBe(false); + expect(opts.plan).toBe(false); + expect(opts.continue).toBe(false); + expect(opts.session).toBeUndefined(); + expect(opts.model).toBeUndefined(); + expect(opts.outputFormat).toBeUndefined(); + expect(opts.prompt).toBeUndefined(); + expect(opts.skillsDirs).toEqual([]); + }); + }); + + describe('--version', () => { + it('prints the version string and exits', () => { + let output = ''; + const program = createProgram('1.2.3', () => {}, () => {}); + program.exitOverride(); + program.configureOutput({ + writeOut: (s) => { + output += s; + }, + }); + + expect(() => program.parse(['node', 'kimi', '--version'])).toThrow(); + expect(output).toContain('1.2.3'); + }); + + it('supports -V as a short alias', () => { + let output = ''; + const program = createProgram('4.5.6', () => {}, () => {}); + program.exitOverride(); + program.configureOutput({ + writeOut: (s) => { + output += s; + }, + }); + + expect(() => program.parse(['node', 'kimi', '-V'])).toThrow(); + expect(output).toContain('4.5.6'); + }); + }); + + describe('--yolo family', () => { + it('--yolo sets yolo to true', () => { + expect(parse(['--yolo']).yolo).toBe(true); + }); + + it('-y sets yolo to true', () => { + expect(parse(['-y']).yolo).toBe(true); + }); + + it('--yes sets yolo to true (hidden alias)', () => { + expect(parse(['--yes']).yolo).toBe(true); + }); + + it('--auto-approve sets yolo to true (hidden alias)', () => { + expect(parse(['--auto-approve']).yolo).toBe(true); + }); + }); + + describe('--session / --resume / --continue', () => { + it('-S sets session', () => { + expect(parse(['-S', 'sess-123']).session).toBe('sess-123'); + }); + + it('-r is an alias for --session', () => { + expect(parse(['-r', 'sess-456']).session).toBe('sess-456'); + }); + + it('--resume is an alias for --session', () => { + expect(parse(['--resume', 'sess-789']).session).toBe('sess-789'); + }); + + it('bare -S (no id) yields empty string — triggers the picker', () => { + expect(parse(['-S']).session).toBe(''); + }); + + it('-C sets continue', () => { + expect(parse(['-C']).continue).toBe(true); + }); + + it('--continue and --session combined raises a conflict', () => { + const opts = parse(['--continue', '--session', 'abc123']); + expect(() => validateOptions(opts)).toThrow(OptionConflictError); + expect(() => validateOptions(opts)).toThrow('Cannot combine --continue, --session.'); + }); + }); + + describe('--plan', () => { + it('sets plan mode flag', () => { + expect(parse(['--plan']).plan).toBe(true); + }); + }); + + describe('--model / -m', () => { + it('parses -m as a model override', () => { + expect(parse(['-m', 'kimi-code/k2']).model).toBe('kimi-code/k2'); + }); + + it('parses --model=value as a model override', () => { + expect(parse(['--model=kimi-code/k2.5']).model).toBe('kimi-code/k2.5'); + }); + + it('rejects empty model values', () => { + const opts = parse(['--model', ' ']); + expect(() => validateOptions(opts)).toThrow(OptionConflictError); + expect(() => validateOptions(opts)).toThrow('Model cannot be empty.'); + }); + }); + + describe('--prompt / -p', () => { + it('parses -p as prompt mode', () => { + const opts = parse(['-p', 'explain this repo']); + expect(opts.prompt).toBe('explain this repo'); + expect(validateOptions(opts).uiMode).toBe('print'); + }); + + it('parses --prompt=value as prompt mode', () => { + const opts = parse(['--prompt=explain this repo']); + expect(opts.prompt).toBe('explain this repo'); + expect(validateOptions(opts).uiMode).toBe('print'); + }); + + it('rejects empty prompt values before reaching the SDK', () => { + const opts = parse(['-p', ' ']); + expect(() => validateOptions(opts)).toThrow(OptionConflictError); + expect(() => validateOptions(opts)).toThrow('Prompt cannot be empty.'); + }); + + it('allows prompt mode with --continue', () => { + const opts = parse(['-p', 'continue here', '--continue']); + expect(opts.continue).toBe(true); + expect(validateOptions(opts).uiMode).toBe('print'); + }); + + it('allows prompt mode with a concrete session id', () => { + const opts = parse(['-p', 'resume here', '--session', 'ses_123']); + expect(opts.session).toBe('ses_123'); + expect(validateOptions(opts).uiMode).toBe('print'); + }); + + it('rejects prompt mode with bare --session picker', () => { + const opts = parse(['-p', 'resume here', '--session']); + expect(() => validateOptions(opts)).toThrow(OptionConflictError); + expect(() => validateOptions(opts)).toThrow('Cannot use --session without an id in prompt mode.'); + }); + + it('rejects prompt mode with --yolo because prompt mode always uses auto permission', () => { + const opts = parse(['-p', 'run this', '--yolo']); + expect(() => validateOptions(opts)).toThrow(OptionConflictError); + expect(() => validateOptions(opts)).toThrow('Cannot combine --prompt with --yolo.'); + }); + + it('rejects prompt mode with --plan', () => { + const opts = parse(['-p', 'run this', '--plan']); + expect(() => validateOptions(opts)).toThrow(OptionConflictError); + expect(() => validateOptions(opts)).toThrow('Cannot combine --prompt with --plan.'); + }); + + it('parses --output-format=stream-json in prompt mode', () => { + const opts = parse(['-p', 'run this', '--output-format=stream-json']); + expect(opts.outputFormat).toBe('stream-json'); + expect(validateOptions(opts).uiMode).toBe('print'); + }); + + it('parses --output-format text in prompt mode', () => { + const opts = parse(['-p', 'run this', '--output-format', 'text']); + expect(opts.outputFormat).toBe('text'); + }); + + it('rejects --output-format outside prompt mode', () => { + const opts = parse(['--output-format=stream-json']); + expect(() => validateOptions(opts)).toThrow(OptionConflictError); + expect(() => validateOptions(opts)).toThrow( + 'Output format is only supported in prompt mode.', + ); + }); + }); + + describe('--skills-dir', () => { + it('collects repeated skill directories', () => { + expect(parse(['--skills-dir', '/one', '--skills-dir=/two']).skillsDirs).toEqual([ + '/one', + '/two', + ]); + }); + }); + + describe('sub-commands', () => { + it('registers the diagnostic sub-commands during alpha', () => { + const program = createProgram('0.0.0', () => {}, () => {}); + const commandNames: string[] = program.commands.map((command) => command.name()); + expect(commandNames).toEqual(['export', 'migrate']); + }); + }); + + describe('rejected flags', () => { + it('any removed flag is unknown to Commander', () => { + for (const arg of [ + '--verbose', + '--debug', + '--work-dir=/', + '--config=x', + '--thinking', + '--print', + '--wire', + '--agent=default', + '--add-dir=/', + '--raw-model', + '--config-file=x', + '--quiet', + '--final-message-only', + '--input-format=text', + '--agent-file=x', + '--mcp-config={}', + '--mcp-config-file=/', + ]) { + expect(() => parse([arg])).toThrow(); + } + }); + }); +}); diff --git a/apps/kimi-code/test/cli/run-prompt.test.ts b/apps/kimi-code/test/cli/run-prompt.test.ts new file mode 100644 index 000000000..0169acba3 --- /dev/null +++ b/apps/kimi-code/test/cli/run-prompt.test.ts @@ -0,0 +1,689 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; + +import { runPrompt } from '#/cli/run-prompt'; + +const mocks = vi.hoisted(() => { + const eventHandlers = new Set<(event: any) => void>(); + const agentEvent = (agentId: string, event: Record<string, unknown>) => ({ + sessionId: 'ses_prompt', + agentId, + ...event, + }); + const mainEvent = (event: Record<string, unknown>) => agentEvent('main', event); + const session = { + id: 'ses_prompt', + setModel: vi.fn(), + setPermission: vi.fn(), + setApprovalHandler: vi.fn(), + setQuestionHandler: vi.fn(), + getStatus: vi.fn( + async (): Promise<{ readonly permission: string; readonly model?: string }> => ({ + permission: 'manual', + }), + ), + onEvent: vi.fn((handler: (event: any) => void) => { + eventHandlers.add(handler); + return () => eventHandlers.delete(handler); + }), + prompt: vi.fn(async () => { + for (const handler of eventHandlers) { + handler( + mainEvent({ type: 'turn.started', turnId: 1, origin: { kind: 'user' } }), + ); + handler(mainEvent({ type: 'assistant.delta', turnId: 1, delta: 'hello' })); + handler(mainEvent({ type: 'assistant.delta', turnId: 1, delta: ' world' })); + handler(mainEvent({ type: 'turn.ended', turnId: 1, reason: 'completed' })); + } + }), + }; + + return { + session, + eventHandlers, + agentEvent, + mainEvent, + kimiHarnessConstructor: vi.fn(), + harnessEnsureConfigFile: vi.fn(), + harnessGetConfig: vi.fn( + async (): Promise<{ providers: {}; defaultModel?: string; telemetry: boolean }> => ({ + providers: {}, + defaultModel: 'k2', + telemetry: true, + }), + ), + harnessCreateSession: vi.fn(async () => session), + harnessResumeSession: vi.fn(async () => session), + harnessListSessions: vi.fn(async () => [{ id: 'ses_previous' }]), + harnessClose: vi.fn(), + harnessTrack: vi.fn(), + harnessGetCachedAccessToken: vi.fn(), + initializeTelemetry: vi.fn(), + setCrashPhase: vi.fn(), + shutdownTelemetry: vi.fn(), + telemetryTrack: vi.fn(), + setTelemetryContext: vi.fn(), + lifecycleTrack: vi.fn(), + withTelemetryContext: vi.fn(() => ({ track: vi.fn() })), + }; +}); + +vi.mock('@moonshot-ai/kimi-code-sdk', async (importOriginal) => { + const actual = await importOriginal<typeof import('@moonshot-ai/kimi-code-sdk')>(); + return { + ...actual, + KimiHarness: class { + homeDir = '/tmp/kimi-code-test-home'; + auth = { getCachedAccessToken: mocks.harnessGetCachedAccessToken }; + ensureConfigFile = mocks.harnessEnsureConfigFile; + getConfig = mocks.harnessGetConfig; + createSession = mocks.harnessCreateSession; + resumeSession = mocks.harnessResumeSession; + listSessions = mocks.harnessListSessions; + close = mocks.harnessClose; + track = mocks.harnessTrack; + + constructor(...args: unknown[]) { + mocks.kimiHarnessConstructor(...args); + } + }, + }; +}); + +vi.mock('@moonshot-ai/kimi-code-oauth', async () => { + const actual = await vi.importActual<typeof import('@moonshot-ai/kimi-code-oauth')>( + '@moonshot-ai/kimi-code-oauth', + ); + return { + ...actual, + createKimiDeviceId: vi.fn(() => 'device-1'), + KIMI_CODE_PROVIDER_NAME: 'kimi-code', + }; +}); + +vi.mock('@moonshot-ai/kimi-telemetry', () => ({ + initializeTelemetry: mocks.initializeTelemetry, + setCrashPhase: mocks.setCrashPhase, + shutdownTelemetry: mocks.shutdownTelemetry, + track: mocks.telemetryTrack, + setTelemetryContext: mocks.setTelemetryContext, + withTelemetryContext: mocks.withTelemetryContext, +})); + +function opts(overrides: Partial<Parameters<typeof runPrompt>[0]> = {}) { + return { + session: undefined, + continue: false, + yolo: false, + plan: false, + model: undefined, + outputFormat: undefined, + prompt: 'say hello', + skillsDirs: [], + ...overrides, + }; +} + +function writer(columns?: number) { + let text = ''; + return { + columns, + write: vi.fn((chunk: string) => { + text += chunk; + return true; + }), + text: () => text, + }; +} + +function fakeProcess() { + const listeners = new Map<NodeJS.Signals, () => Promise<void> | void>(); + return { + once: vi.fn((signal: NodeJS.Signals, listener: () => Promise<void> | void) => { + listeners.set(signal, listener); + }), + off: vi.fn((signal: NodeJS.Signals, listener: () => Promise<void> | void) => { + if (listeners.get(signal) === listener) { + listeners.delete(signal); + } + }), + exit: vi.fn(), + listener: (signal: NodeJS.Signals) => listeners.get(signal), + }; +} + +async function waitForAssertion(assertion: () => void): Promise<void> { + let lastError: unknown; + for (let attempt = 0; attempt < 20; attempt += 1) { + try { + assertion(); + return; + } catch (error) { + lastError = error; + await new Promise((resolve) => setTimeout(resolve, 0)); + } + } + throw lastError; +} + +describe('runPrompt', () => { + afterEach(() => { + vi.clearAllMocks(); + mocks.eventHandlers.clear(); + }); + + it('creates a fresh auto-permission session and streams assistant output to stdout', async () => { + const stdout = writer(); + const stderr = writer(); + + await runPrompt(opts({ skillsDirs: ['/skills'] }), '1.2.3-test', { stdout, stderr }); + + expect(mocks.kimiHarnessConstructor).toHaveBeenCalledWith( + expect.objectContaining({ skillDirs: ['/skills'], uiMode: 'print' }), + ); + expect(mocks.harnessCreateSession).toHaveBeenCalledWith({ + workDir: process.cwd(), + model: 'k2', + permission: 'auto', + }); + expect(mocks.session.setPermission).not.toHaveBeenCalled(); + expect(mocks.session.setApprovalHandler).toHaveBeenCalledWith(expect.any(Function)); + expect(mocks.session.setQuestionHandler).toHaveBeenCalledWith(expect.any(Function)); + expect(mocks.session.prompt).toHaveBeenCalledWith('say hello'); + expect(stdout.text()).toBe('• hello world\n\n'); + expect(stderr.text()).toBe('To resume this session: kimi -r ses_prompt\n'); + expect(mocks.shutdownTelemetry).toHaveBeenCalled(); + expect(mocks.harnessClose).toHaveBeenCalled(); + }); + + it('uses the CLI model override when creating a fresh prompt session', async () => { + await runPrompt(opts({ model: 'kimi-code/k2.5' }), '1.2.3-test', { + stdout: { write: vi.fn(() => true) }, + stderr: { write: vi.fn(() => true) }, + }); + + expect(mocks.harnessCreateSession).toHaveBeenCalledWith({ + workDir: process.cwd(), + model: 'kimi-code/k2.5', + permission: 'auto', + }); + expect(mocks.initializeTelemetry).toHaveBeenCalledWith( + expect.objectContaining({ model: 'kimi-code/k2.5' }), + ); + }); + + it('formats thinking and assistant output as transcript blocks', async () => { + mocks.session.prompt.mockImplementationOnce(async () => { + for (const handler of mocks.eventHandlers) { + handler( + mocks.mainEvent({ type: 'turn.started', turnId: 3, origin: { kind: 'user' } }), + ); + handler( + mocks.mainEvent({ + type: 'thinking.delta', + turnId: 3, + delta: 'The user wants an exact reply.', + }), + ); + handler( + mocks.mainEvent({ + type: 'thinking.delta', + turnId: 3, + delta: '\nNo tools are needed.', + }), + ); + handler(mocks.mainEvent({ type: 'assistant.delta', turnId: 3, delta: 'prompt-mode-ok' })); + handler(mocks.mainEvent({ type: 'turn.ended', turnId: 3, reason: 'completed' })); + } + }); + const stdout = writer(); + const stderr = writer(); + + await runPrompt(opts(), '1.2.3-test', { stdout, stderr }); + + expect(stderr.text()).toBe( + '• The user wants an exact reply.\n No tools are needed.\n\nTo resume this session: kimi -r ses_prompt\n', + ); + expect(stdout.text()).toBe('• prompt-mode-ok\n\n'); + expect(stderr.write).toHaveBeenNthCalledWith(1, '• The user wants an exact reply.'); + expect(stderr.write).toHaveBeenNthCalledWith(2, '\n No tools are needed.'); + expect(stdout.write).toHaveBeenNthCalledWith(1, '• prompt-mode-ok'); + }); + + it('formats hook results as their own transcript block', async () => { + mocks.session.prompt.mockImplementationOnce(async () => { + for (const handler of mocks.eventHandlers) { + handler( + mocks.mainEvent({ type: 'turn.started', turnId: 3, origin: { kind: 'user' } }), + ); + handler( + mocks.mainEvent({ + type: 'hook.result', + turnId: 3, + hookEvent: 'UserPromptSubmit', + content: '{}', + }), + ); + handler(mocks.mainEvent({ type: 'assistant.delta', turnId: 3, delta: 'answer' })); + handler(mocks.mainEvent({ type: 'turn.ended', turnId: 3, reason: 'completed' })); + } + }); + const stdout = writer(); + const stderr = writer(); + + await runPrompt(opts(), '1.2.3-test', { stdout, stderr }); + + expect(stdout.text()).toBe('• UserPromptSubmit hook\n\n {}\n\n• answer\n\n'); + expect(stderr.text()).toBe('To resume this session: kimi -r ses_prompt\n'); + }); + + it('wraps transcript blocks with hanging indentation when terminal width is known', async () => { + mocks.session.prompt.mockImplementationOnce(async () => { + for (const handler of mocks.eventHandlers) { + handler( + mocks.mainEvent({ type: 'turn.started', turnId: 4, origin: { kind: 'user' } }), + ); + handler(mocks.mainEvent({ type: 'thinking.delta', turnId: 4, delta: 'thinking-wrap' })); + handler(mocks.mainEvent({ type: 'assistant.delta', turnId: 4, delta: 'answer-wrap' })); + handler(mocks.mainEvent({ type: 'turn.ended', turnId: 4, reason: 'completed' })); + } + }); + const stdout = writer(10); + const stderr = writer(10); + + await runPrompt(opts(), '1.2.3-test', { stdout, stderr }); + + expect(stderr.text()).toBe('• thinking\n -wrap\n\nTo resume this session: kimi -r ses_prompt\n'); + expect(stdout.text()).toBe('• answer-w\n rap\n\n'); + }); + + it('filters prompt output and completion to the main agent turn', async () => { + mocks.session.prompt.mockImplementationOnce(async () => { + const emit = (event: Record<string, unknown>) => { + for (const handler of Array.from(mocks.eventHandlers)) { + handler(event); + } + }; + + emit(mocks.mainEvent({ type: 'turn.started', turnId: 1, origin: { kind: 'user' } })); + emit( + mocks.agentEvent('child-agent', { + type: 'turn.started', + turnId: 1, + origin: { kind: 'user' }, + }), + ); + emit( + mocks.agentEvent('child-agent', { + type: 'assistant.delta', + turnId: 1, + delta: 'sub answer', + }), + ); + emit(mocks.agentEvent('child-agent', { type: 'turn.ended', turnId: 1, reason: 'completed' })); + await Promise.resolve(); + emit(mocks.mainEvent({ type: 'assistant.delta', turnId: 1, delta: 'main answer' })); + emit(mocks.mainEvent({ type: 'turn.ended', turnId: 1, reason: 'completed' })); + }); + const stdout = writer(); + const stderr = writer(); + + await runPrompt(opts(), '1.2.3-test', { stdout, stderr }); + + expect(stdout.text()).toBe('• main answer\n\n'); + expect(stderr.text()).toBe('To resume this session: kimi -r ses_prompt\n'); + }); + + it('ignores child-agent error events while the main turn continues', async () => { + mocks.session.prompt.mockImplementationOnce(async () => { + const emit = (event: Record<string, unknown>) => { + for (const handler of Array.from(mocks.eventHandlers)) { + handler(event); + } + }; + + emit(mocks.mainEvent({ type: 'turn.started', turnId: 1, origin: { kind: 'user' } })); + emit( + mocks.agentEvent('child-agent', { + type: 'error', + code: 'subagent.failed', + message: 'child failed', + }), + ); + await Promise.resolve(); + emit(mocks.mainEvent({ type: 'assistant.delta', turnId: 1, delta: 'main recovered' })); + emit(mocks.mainEvent({ type: 'turn.ended', turnId: 1, reason: 'completed' })); + }); + const stdout = writer(); + const stderr = writer(); + + await runPrompt(opts(), '1.2.3-test', { stdout, stderr }); + + expect(stdout.text()).toBe('• main recovered\n\n'); + expect(stderr.text()).toBe('To resume this session: kimi -r ses_prompt\n'); + }); + + it('resumes a concrete session and forces auto permission before prompting', async () => { + await runPrompt(opts({ session: 'ses_existing' }), '1.2.3-test', { + stdout: { write: vi.fn(() => true) }, + stderr: { write: vi.fn(() => true) }, + }); + + expect(mocks.harnessResumeSession).toHaveBeenCalledWith({ id: 'ses_existing' }); + expect(mocks.session.getStatus).toHaveBeenCalled(); + expect(mocks.session.setPermission).toHaveBeenNthCalledWith(1, 'auto'); + expect(mocks.session.setPermission).toHaveBeenNthCalledWith(2, 'manual'); + }); + + it('applies the CLI model override to resumed prompt sessions', async () => { + await runPrompt(opts({ session: 'ses_existing', model: 'kimi-code/k2.5' }), '1.2.3-test', { + stdout: { write: vi.fn(() => true) }, + stderr: { write: vi.fn(() => true) }, + }); + + expect(mocks.harnessResumeSession).toHaveBeenCalledWith({ id: 'ses_existing' }); + expect(mocks.session.setModel).toHaveBeenCalledWith('kimi-code/k2.5'); + expect(mocks.initializeTelemetry).toHaveBeenCalledWith( + expect.objectContaining({ model: 'kimi-code/k2.5' }), + ); + }); + + it('writes stream-json output as assistant JSONL without transcript bullets', async () => { + const stdout = writer(); + const stderr = writer(); + + await runPrompt(opts({ outputFormat: 'stream-json' }), '1.2.3-test', { stdout, stderr }); + + expect(stdout.text()).toBe('{"role":"assistant","content":"hello world"}\n'); + expect(stderr.text()).toBe('To resume this session: kimi -r ses_prompt\n'); + }); + + it('writes stream-json tool calls and tool results as JSONL messages', async () => { + mocks.session.prompt.mockImplementationOnce(async () => { + for (const handler of mocks.eventHandlers) { + handler( + mocks.mainEvent({ type: 'turn.started', turnId: 8, origin: { kind: 'user' } }), + ); + handler(mocks.mainEvent({ type: 'assistant.delta', turnId: 8, delta: 'checking' })); + handler( + mocks.mainEvent({ + type: 'tool.call.started', + turnId: 8, + toolCallId: 'tc_1', + name: 'Shell', + args: { command: 'ls' }, + }), + ); + handler( + mocks.mainEvent({ + type: 'tool.result', + turnId: 8, + toolCallId: 'tc_1', + output: 'file1.py\nfile2.py', + }), + ); + handler(mocks.mainEvent({ type: 'assistant.delta', turnId: 8, delta: 'done' })); + handler(mocks.mainEvent({ type: 'turn.ended', turnId: 8, reason: 'completed' })); + } + }); + const stdout = writer(); + const stderr = writer(); + + await runPrompt(opts({ outputFormat: 'stream-json' }), '1.2.3-test', { stdout, stderr }); + + expect(stdout.text()).toBe( + [ + '{"role":"assistant","content":"checking","tool_calls":[{"type":"function","id":"tc_1","function":{"name":"Shell","arguments":"{\\"command\\":\\"ls\\"}"}}]}', + '{"role":"tool","tool_call_id":"tc_1","content":"file1.py\\nfile2.py"}', + '{"role":"assistant","content":"done"}', + '', + ].join('\n'), + ); + }); + + it('resumes a concrete session without a configured default model', async () => { + mocks.harnessGetConfig.mockResolvedValueOnce({ providers: {}, telemetry: true }); + mocks.session.getStatus.mockResolvedValueOnce({ permission: 'manual', model: 'saved-model' }); + + await runPrompt(opts({ session: 'ses_existing' }), '1.2.3-test', { + stdout: { write: vi.fn(() => true) }, + stderr: { write: vi.fn(() => true) }, + }); + + expect(mocks.harnessResumeSession).toHaveBeenCalledWith({ id: 'ses_existing' }); + expect(mocks.harnessCreateSession).not.toHaveBeenCalled(); + expect(mocks.initializeTelemetry).toHaveBeenCalledWith( + expect.objectContaining({ model: 'saved-model' }), + ); + expect(mocks.session.setPermission).toHaveBeenNthCalledWith(1, 'auto'); + expect(mocks.session.setPermission).toHaveBeenNthCalledWith(2, 'manual'); + }); + + it('continues the previous workdir session when --continue is used', async () => { + await runPrompt(opts({ continue: true }), '1.2.3-test', { + stdout: { write: vi.fn(() => true) }, + stderr: { write: vi.fn(() => true) }, + }); + + expect(mocks.harnessListSessions).toHaveBeenCalledWith({ workDir: process.cwd() }); + expect(mocks.harnessResumeSession).toHaveBeenCalledWith({ id: 'ses_previous' }); + expect(mocks.session.setPermission).toHaveBeenNthCalledWith(1, 'auto'); + expect(mocks.session.setPermission).toHaveBeenNthCalledWith(2, 'manual'); + }); + + it('continues a previous session without a configured default model', async () => { + mocks.harnessGetConfig.mockResolvedValueOnce({ providers: {}, telemetry: true }); + mocks.session.getStatus.mockResolvedValueOnce({ permission: 'manual', model: 'saved-model' }); + + await runPrompt(opts({ continue: true }), '1.2.3-test', { + stdout: { write: vi.fn(() => true) }, + stderr: { write: vi.fn(() => true) }, + }); + + expect(mocks.harnessListSessions).toHaveBeenCalledWith({ workDir: process.cwd() }); + expect(mocks.harnessResumeSession).toHaveBeenCalledWith({ id: 'ses_previous' }); + expect(mocks.harnessCreateSession).not.toHaveBeenCalled(); + expect(mocks.initializeTelemetry).toHaveBeenCalledWith( + expect.objectContaining({ model: 'saved-model' }), + ); + }); + + it('restores resumed session permission even when the turn fails', async () => { + mocks.session.prompt.mockImplementationOnce(async () => { + for (const handler of mocks.eventHandlers) { + handler( + mocks.mainEvent({ type: 'turn.started', turnId: 5, origin: { kind: 'user' } }), + ); + handler( + mocks.mainEvent({ + type: 'turn.ended', + turnId: 5, + reason: 'failed', + error: { code: 'provider.error', message: 'model failed' }, + }), + ); + } + }); + + await expect( + runPrompt(opts({ session: 'ses_existing' }), '1.2.3-test', { + stdout: { write: vi.fn(() => true) }, + stderr: { write: vi.fn(() => true) }, + }), + ).rejects.toThrow('provider.error: model failed'); + + expect(mocks.session.setPermission).toHaveBeenNthCalledWith(1, 'auto'); + expect(mocks.session.setPermission).toHaveBeenNthCalledWith(2, 'manual'); + expect(mocks.session.setPermission.mock.invocationCallOrder[1]).toBeLessThan( + mocks.harnessClose.mock.invocationCallOrder[0]!, + ); + }); + + it('restores resumed session permission before exiting on SIGINT', async () => { + let releasePrompt!: () => void; + mocks.session.prompt.mockImplementationOnce(async () => { + for (const handler of mocks.eventHandlers) { + handler( + mocks.mainEvent({ type: 'turn.started', turnId: 6, origin: { kind: 'user' } }), + ); + } + await new Promise<void>((resolve) => { + releasePrompt = resolve; + }); + }); + const processMock = fakeProcess(); + const run = runPrompt(opts({ session: 'ses_existing' }), '1.2.3-test', { + stdout: { write: vi.fn(() => true) }, + stderr: { write: vi.fn(() => true) }, + process: processMock, + } as Parameters<typeof runPrompt>[2] & { process: ReturnType<typeof fakeProcess> }); + + await waitForAssertion(() => { + expect(mocks.session.setPermission).toHaveBeenCalledWith('auto'); + expect(processMock.listener('SIGINT')).toBeDefined(); + }); + + await processMock.listener('SIGINT')?.(); + + expect(mocks.session.setPermission).toHaveBeenNthCalledWith(2, 'manual'); + expect(mocks.session.setPermission.mock.invocationCallOrder[1]).toBeLessThan( + processMock.exit.mock.invocationCallOrder[0]!, + ); + expect(mocks.shutdownTelemetry).toHaveBeenCalled(); + expect(mocks.harnessClose).toHaveBeenCalled(); + expect(processMock.exit).toHaveBeenCalledWith(130); + + for (const handler of mocks.eventHandlers) { + handler(mocks.mainEvent({ type: 'turn.ended', turnId: 6, reason: 'completed' })); + } + releasePrompt(); + await run; + + expect(mocks.harnessClose).toHaveBeenCalledTimes(1); + }); + + it('waits for the pending auto permission write before signal restore', async () => { + let releaseAutoPermission!: () => void; + let releasePrompt!: () => void; + mocks.session.setPermission.mockImplementationOnce(async () => { + await new Promise<void>((resolve) => { + releaseAutoPermission = resolve; + }); + }); + mocks.session.prompt.mockImplementationOnce(async () => { + for (const handler of mocks.eventHandlers) { + handler( + mocks.mainEvent({ type: 'turn.started', turnId: 7, origin: { kind: 'user' } }), + ); + } + await new Promise<void>((resolve) => { + releasePrompt = resolve; + }); + }); + const processMock = fakeProcess(); + const run = runPrompt(opts({ session: 'ses_existing' }), '1.2.3-test', { + stdout: { write: vi.fn(() => true) }, + stderr: { write: vi.fn(() => true) }, + process: processMock, + } as Parameters<typeof runPrompt>[2] & { process: ReturnType<typeof fakeProcess> }); + + await waitForAssertion(() => { + expect(processMock.listener('SIGINT')).toBeDefined(); + expect(mocks.session.setPermission).toHaveBeenCalledWith('auto'); + }); + expect(processMock.once.mock.invocationCallOrder[0]).toBeLessThan( + mocks.session.setPermission.mock.invocationCallOrder[0]!, + ); + + const signalCleanup = processMock.listener('SIGINT')?.(); + await Promise.resolve(); + + expect(mocks.session.setPermission).toHaveBeenCalledTimes(1); + + releaseAutoPermission(); + await signalCleanup; + + expect(mocks.session.setPermission).toHaveBeenNthCalledWith(2, 'manual'); + expect(processMock.exit).toHaveBeenCalledWith(130); + + await waitForAssertion(() => { + expect(mocks.session.prompt).toHaveBeenCalledWith('say hello'); + }); + for (const handler of mocks.eventHandlers) { + handler(mocks.mainEvent({ type: 'turn.ended', turnId: 7, reason: 'completed' })); + } + releasePrompt(); + await run; + }); + + it('uses auto permission so headless mode can bypass plan approval and questions', async () => { + await runPrompt(opts(), '1.2.3-test', { + stdout: { write: vi.fn(() => true) }, + stderr: { write: vi.fn(() => true) }, + }); + + expect(mocks.harnessCreateSession).toHaveBeenCalledWith( + expect.objectContaining({ permission: 'auto' }), + ); + }); + + it('throws when no default model is configured', async () => { + mocks.harnessGetConfig.mockResolvedValueOnce({ providers: {}, telemetry: true }); + + await expect( + runPrompt(opts(), '1.2.3-test', { + stdout: { write: vi.fn(() => true) }, + stderr: { write: vi.fn(() => true) }, + }), + ).rejects.toThrow('No model configured. Set default_model in config.toml.'); + + expect(mocks.harnessClose).toHaveBeenCalled(); + }); + + it('rejects when the turn fails and still closes resources', async () => { + mocks.session.prompt.mockImplementationOnce(async () => { + for (const handler of mocks.eventHandlers) { + handler( + mocks.mainEvent({ type: 'turn.started', turnId: 2, origin: { kind: 'user' } }), + ); + handler( + mocks.mainEvent({ + type: 'turn.ended', + turnId: 2, + reason: 'failed', + error: { code: 'provider.error', message: 'model failed' }, + }), + ); + } + }); + + await expect( + runPrompt(opts(), '1.2.3-test', { + stdout: { write: vi.fn(() => true) }, + stderr: { write: vi.fn(() => true) }, + }), + ).rejects.toThrow('provider.error: model failed'); + + expect(mocks.shutdownTelemetry).toHaveBeenCalled(); + expect(mocks.harnessClose).toHaveBeenCalled(); + }); + + it('approval fallback approves if an unexpected approval request reaches SDK', async () => { + await runPrompt(opts(), '1.2.3-test', { + stdout: { write: vi.fn(() => true) }, + stderr: { write: vi.fn(() => true) }, + }); + + const handler = mocks.session.setApprovalHandler.mock.calls[0]![0] as () => unknown; + expect(handler()).toEqual({ decision: 'approved' }); + }); + + it('question fallback returns null so prompt mode never opens a question UI', async () => { + await runPrompt(opts(), '1.2.3-test', { + stdout: { write: vi.fn(() => true) }, + stderr: { write: vi.fn(() => true) }, + }); + + const handler = mocks.session.setQuestionHandler.mock.calls[0]![0] as () => unknown; + expect(handler()).toBeNull(); + }); +}); diff --git a/apps/kimi-code/test/cli/run-shell.test.ts b/apps/kimi-code/test/cli/run-shell.test.ts new file mode 100644 index 000000000..64f9e02aa --- /dev/null +++ b/apps/kimi-code/test/cli/run-shell.test.ts @@ -0,0 +1,535 @@ +import { execSync } from 'node:child_process'; + +import type { createKimiDeviceId as createKimiDeviceIdFn } from '@moonshot-ai/kimi-code-oauth'; +import { afterEach, describe, expect, it, vi } from 'vitest'; + +import { runShell } from '#/cli/run-shell'; + +import { captureProcessWrite, ExitCalled, mockProcessExit } from '../helpers/process'; + +type CreateKimiDeviceId = typeof createKimiDeviceIdFn; + +const mocks = vi.hoisted(() => { + type TuiConfigFallback = { + theme: 'dark' | 'light' | 'auto'; + editorCommand: string | null; + notifications: { enabled: boolean; condition: 'unfocused' | 'always' }; + }; + + class TuiConfigParseError extends Error { + readonly fallback: TuiConfigFallback; + + constructor(fallback: TuiConfigFallback) { + super('Invalid TUI config in ~/.kimi-code/tui.toml; using defaults.'); + this.fallback = fallback; + } + } + + const lifecycleTrack = vi.fn(); + + return { + loadTuiConfig: vi.fn(), + detectTerminalTheme: vi.fn(), + kimiHarnessConstructor: vi.fn(), + harnessEnsureConfigFile: vi.fn(), + harnessGetConfig: vi.fn(async () => ({ + providers: {}, + defaultModel: 'k2', + telemetry: true, + })), + harnessGetCachedAccessToken: vi.fn(), + harnessClose: vi.fn(), + detectPendingMigration: vi.fn<() => Promise<unknown>>(async () => null), + harnessTrack: vi.fn(), + kimiTuiConstructor: vi.fn(), + tuiStart: vi.fn(), + tuiGetStartupMcpMs: vi.fn(async () => 0), + tuiGetCurrentSessionId: vi.fn(() => ''), + tuiHasSessionContent: vi.fn(() => false), + createKimiDeviceId: vi.fn<CreateKimiDeviceId>(() => 'device-1'), + initializeTelemetry: vi.fn(), + setCrashPhase: vi.fn(), + shutdownTelemetry: vi.fn(), + telemetryTrack: vi.fn(), + setTelemetryContext: vi.fn(), + lifecycleTrack, + withTelemetryContext: vi.fn(() => ({ + track: lifecycleTrack, + })), + execSync: vi.fn(), + TuiConfigParseError, + }; +}); + +vi.mock('@moonshot-ai/kimi-code-sdk', async (importOriginal) => { + const actual = await importOriginal<typeof import('@moonshot-ai/kimi-code-sdk')>(); + return { + ...actual, + KimiHarness: class { + homeDir = '/tmp/kimi-code-test-home'; + auth = { + getCachedAccessToken: mocks.harnessGetCachedAccessToken, + }; + ensureConfigFile = mocks.harnessEnsureConfigFile; + getConfig = mocks.harnessGetConfig; + close = mocks.harnessClose; + track = mocks.harnessTrack; + + constructor(...args: unknown[]) { + mocks.kimiHarnessConstructor(...args); + } + }, + }; +}); + +vi.mock('@moonshot-ai/kimi-code-oauth', async () => { + const actual = await vi.importActual<typeof import('@moonshot-ai/kimi-code-oauth')>( + '@moonshot-ai/kimi-code-oauth', + ); + return { + ...actual, + createKimiDeviceId: mocks.createKimiDeviceId, + KIMI_CODE_PROVIDER_NAME: 'kimi-code', + }; +}); + +vi.mock('@moonshot-ai/kimi-telemetry', () => ({ + initializeTelemetry: mocks.initializeTelemetry, + setCrashPhase: mocks.setCrashPhase, + shutdownTelemetry: mocks.shutdownTelemetry, + track: mocks.telemetryTrack, + setTelemetryContext: mocks.setTelemetryContext, + withTelemetryContext: mocks.withTelemetryContext, +})); + +vi.mock('../../src/tui/config', () => ({ + loadTuiConfig: mocks.loadTuiConfig, + TuiConfigParseError: mocks.TuiConfigParseError, +})); + +vi.mock('../../src/tui/index', () => ({ + KimiTUI: class { + onExit?: () => Promise<void>; + + constructor(...args: unknown[]) { + mocks.kimiTuiConstructor(this, ...args); + } + + start = mocks.tuiStart; + getStartupMcpMs = mocks.tuiGetStartupMcpMs; + getCurrentSessionId = mocks.tuiGetCurrentSessionId; + hasSessionContent = mocks.tuiHasSessionContent; + }, +})); + +vi.mock('../../src/tui/theme/detect', () => ({ + detectTerminalTheme: mocks.detectTerminalTheme, +})); + +vi.mock('../../src/migration/index', () => ({ + detectPendingMigration: mocks.detectPendingMigration, +})); + +vi.mock('node:child_process', () => ({ + execSync: mocks.execSync, +})); + +describe('runShell', () => { + afterEach(() => { + vi.clearAllMocks(); + mocks.harnessGetConfig.mockResolvedValue({ + providers: {}, + defaultModel: 'k2', + telemetry: true, + }); + mocks.tuiGetStartupMcpMs.mockResolvedValue(0); + mocks.tuiGetCurrentSessionId.mockReturnValue(''); + mocks.tuiHasSessionContent.mockReturnValue(false); + }); + + it('constructs KimiHarness and KimiTUI with startup input', async () => { + mocks.loadTuiConfig.mockResolvedValue({ + theme: 'dark', + editorCommand: null, + notifications: { enabled: true, condition: 'unfocused' }, + }); + mocks.tuiStart.mockResolvedValue(undefined); + mocks.tuiGetStartupMcpMs.mockResolvedValue(47); + mocks.tuiGetCurrentSessionId.mockReturnValue('ses-startup'); + + const cliOptions = { + session: undefined, + continue: false, + yolo: true, + plan: true, + model: undefined, + outputFormat: undefined, + prompt: undefined, + skillsDirs: [], + }; + + await runShell(cliOptions, '1.2.3-test'); + + expect(mocks.kimiHarnessConstructor).toHaveBeenCalledWith( + expect.objectContaining({ + identity: expect.objectContaining({ + userAgentProduct: 'kimi-code-cli', + version: '1.2.3-test', + }), + }), + ); + expect(mocks.harnessEnsureConfigFile).toHaveBeenCalledOnce(); + expect(mocks.harnessEnsureConfigFile.mock.invocationCallOrder[0]).toBeLessThan( + mocks.harnessGetConfig.mock.invocationCallOrder[0]!, + ); + expect(execSync).toHaveBeenCalledWith('stty -ixon', { stdio: 'ignore' }); + expect(mocks.kimiTuiConstructor).toHaveBeenCalledTimes(1); + expect(mocks.createKimiDeviceId).toHaveBeenCalledWith( + '/tmp/kimi-code-test-home', + expect.any(Object), + ); + expect(mocks.initializeTelemetry).toHaveBeenCalledWith({ + homeDir: '/tmp/kimi-code-test-home', + deviceId: 'device-1', + enabled: true, + appName: 'kimi-code-cli', + version: '1.2.3-test', + uiMode: 'shell', + model: 'k2', + getAccessToken: expect.any(Function), + }); + expect(mocks.setCrashPhase).toHaveBeenCalledWith('runtime'); + + const [, harness, startupInput] = mocks.kimiTuiConstructor.mock.calls[0]!; + expect(harness).toBeTypeOf('object'); + expect(startupInput).toMatchObject({ + cliOptions, + tuiConfig: { + theme: 'dark', + editorCommand: null, + notifications: { enabled: true, condition: 'unfocused' }, + }, + version: '1.2.3-test', + workDir: process.cwd(), + resolvedTheme: 'dark', + }); + expect(mocks.tuiStart).toHaveBeenCalledOnce(); + expect(mocks.harnessTrack).not.toHaveBeenCalledWith('started', expect.anything()); + expect(mocks.withTelemetryContext).toHaveBeenCalledWith({ sessionId: 'ses-startup' }); + expect(mocks.lifecycleTrack).toHaveBeenCalledWith('started', { + resumed: false, + yolo: true, + plan: true, + afk: false, + }); + expect(mocks.lifecycleTrack).toHaveBeenCalledWith('startup_perf', { + duration_ms: expect.any(Number), + config_ms: expect.any(Number), + init_ms: expect.any(Number), + mcp_ms: 47, + }); + }); + + it('tracks first launch when device id creation reports first launch', async () => { + mocks.loadTuiConfig.mockResolvedValue({ + theme: 'dark', + editorCommand: null, + notifications: { enabled: true, condition: 'unfocused' }, + }); + mocks.tuiStart.mockResolvedValue(undefined); + mocks.createKimiDeviceId.mockImplementationOnce((homeDir, options) => { + const deviceId = `device-for-${homeDir}`; + options?.onFirstLaunch?.(deviceId); + return deviceId; + }); + + await runShell( + { + session: undefined, + continue: false, + yolo: false, + plan: false, + model: undefined, + outputFormat: undefined, + prompt: undefined, + skillsDirs: [], + }, + '1.2.3-test', + ); + + expect(mocks.createKimiDeviceId).toHaveBeenCalledWith( + '/tmp/kimi-code-test-home', + expect.objectContaining({ onFirstLaunch: expect.any(Function) }), + ); + expect(mocks.harnessTrack).toHaveBeenCalledWith('first_launch'); + }); + + it('marks resumed lifecycle starts from session flags', async () => { + mocks.loadTuiConfig.mockResolvedValue({ + theme: 'dark', + editorCommand: null, + notifications: { enabled: true, condition: 'unfocused' }, + }); + mocks.tuiStart.mockResolvedValue(undefined); + mocks.tuiGetCurrentSessionId.mockReturnValue('ses-1'); + + await runShell( + { + session: 'ses-1', + continue: false, + yolo: false, + plan: false, + model: undefined, + outputFormat: undefined, + prompt: undefined, + skillsDirs: [], + }, + '1.2.3-test', + ); + + expect(mocks.lifecycleTrack).toHaveBeenCalledWith('started', { + resumed: true, + yolo: false, + plan: false, + afk: false, + }); + }); + + it('binds startup_perf to the session captured before MCP metrics resolve', async () => { + mocks.loadTuiConfig.mockResolvedValue({ + theme: 'dark', + editorCommand: null, + notifications: { enabled: true, condition: 'unfocused' }, + }); + mocks.tuiStart.mockResolvedValue(undefined); + let currentSessionId = 'ses-startup'; + mocks.tuiGetCurrentSessionId.mockImplementation(() => currentSessionId); + mocks.tuiGetStartupMcpMs.mockImplementation(async () => { + currentSessionId = 'ses-later'; + return 47; + }); + + await runShell( + { + session: undefined, + continue: false, + yolo: false, + plan: false, + model: undefined, + outputFormat: undefined, + prompt: undefined, + skillsDirs: [], + }, + '1.2.3-test', + ); + + expect(mocks.withTelemetryContext).toHaveBeenNthCalledWith(1, { sessionId: 'ses-startup' }); + expect(mocks.withTelemetryContext).toHaveBeenNthCalledWith(2, { sessionId: 'ses-startup' }); + expect(mocks.lifecycleTrack).toHaveBeenNthCalledWith(2, 'startup_perf', { + duration_ms: expect.any(Number), + config_ms: expect.any(Number), + init_ms: expect.any(Number), + mcp_ms: 47, + }); + }); + + it('bridges OAuth refresh outcomes to telemetry', async () => { + mocks.loadTuiConfig.mockResolvedValue({ + theme: 'dark', + editorCommand: null, + notifications: { enabled: true, condition: 'unfocused' }, + }); + mocks.tuiStart.mockResolvedValue(undefined); + + await runShell( + { + session: undefined, + continue: false, + yolo: false, + plan: false, + model: undefined, + outputFormat: undefined, + prompt: undefined, + skillsDirs: [], + }, + '1.2.3-test', + ); + + const [harnessOptions] = mocks.kimiHarnessConstructor.mock.calls[0] as [ + { + readonly onOAuthRefresh: ( + outcome: + | { readonly success: true } + | { readonly success: false; readonly reason: 'unauthorized' | 'network_or_other' }, + ) => void; + }, + ]; + + harnessOptions.onOAuthRefresh({ success: true }); + harnessOptions.onOAuthRefresh({ success: false, reason: 'unauthorized' }); + harnessOptions.onOAuthRefresh({ success: false, reason: 'network_or_other' }); + + expect(mocks.telemetryTrack).toHaveBeenCalledWith('oauth_refresh', { success: true }); + expect(mocks.telemetryTrack).toHaveBeenCalledWith('oauth_refresh', { + success: false, + reason: 'unauthorized', + }); + expect(mocks.telemetryTrack).toHaveBeenCalledWith('oauth_refresh', { + success: false, + reason: 'network_or_other', + }); + }); + + it('detects auto theme and forwards config parse warnings as startup notice', async () => { + mocks.loadTuiConfig.mockRejectedValue( + new mocks.TuiConfigParseError({ + theme: 'auto', + editorCommand: 'vim', + notifications: { enabled: true, condition: 'always' }, + }), + ); + mocks.detectTerminalTheme.mockResolvedValue('light'); + mocks.tuiStart.mockResolvedValue(undefined); + + await runShell( + { + session: '', + continue: false, + yolo: false, + plan: false, + model: undefined, + outputFormat: undefined, + prompt: undefined, + skillsDirs: [], + }, + '1.2.3-test', + ); + + expect(mocks.detectTerminalTheme).toHaveBeenCalledOnce(); + const [, , startupInput] = mocks.kimiTuiConstructor.mock.calls[0]!; + expect(startupInput).toMatchObject({ + startupNotice: 'Invalid TUI config in ~/.kimi-code/tui.toml; using defaults.', + resolvedTheme: 'light', + tuiConfig: { + theme: 'auto', + editorCommand: 'vim', + notifications: { enabled: true, condition: 'always' }, + }, + }); + }); + + it('closes the harness when TUI startup fails', async () => { + mocks.loadTuiConfig.mockResolvedValue({ + theme: 'dark', + editorCommand: null, + notifications: { enabled: true, condition: 'unfocused' }, + }); + mocks.tuiStart.mockRejectedValue(new Error('boom')); + + await expect( + runShell( + { + session: undefined, + continue: false, + yolo: false, + plan: false, + model: undefined, + outputFormat: undefined, + prompt: undefined, + skillsDirs: [], + }, + '1.2.3-test', + ), + ).rejects.toThrow('boom'); + + expect(mocks.setCrashPhase).toHaveBeenCalledWith('shutdown'); + expect(mocks.harnessTrack).toHaveBeenCalledWith('exit', { duration_s: expect.any(Number) }); + expect(mocks.shutdownTelemetry).toHaveBeenCalledOnce(); + expect(mocks.harnessClose).toHaveBeenCalledOnce(); + }); + + it('tracks exit and prints resume instructions from the TUI exit handler', async () => { + mocks.loadTuiConfig.mockResolvedValue({ + theme: 'dark', + editorCommand: null, + notifications: { enabled: true, condition: 'unfocused' }, + }); + mocks.tuiStart.mockResolvedValue(undefined); + mocks.tuiGetCurrentSessionId.mockReturnValue('ses-1'); + mocks.tuiHasSessionContent.mockReturnValue(true); + + const stdout = captureProcessWrite('stdout'); + const stderr = captureProcessWrite('stderr'); + const exitSpy = mockProcessExit(); + + try { + await runShell( + { + session: undefined, + continue: false, + yolo: false, + plan: false, + model: undefined, + outputFormat: undefined, + prompt: undefined, + skillsDirs: [], + }, + '1.2.3-test', + ); + const [tui] = mocks.kimiTuiConstructor.mock.calls[0]!; + mocks.harnessTrack.mockClear(); + mocks.lifecycleTrack.mockClear(); + mocks.withTelemetryContext.mockClear(); + + await expect((tui as { onExit: () => Promise<void> }).onExit()).rejects.toBeInstanceOf( + ExitCalled, + ); + + expect(mocks.setCrashPhase).toHaveBeenCalledWith('shutdown'); + expect(mocks.withTelemetryContext).toHaveBeenCalledWith({ sessionId: 'ses-1' }); + expect(mocks.lifecycleTrack).toHaveBeenCalledWith('exit', { + duration_s: expect.any(Number), + }); + expect(mocks.harnessTrack).not.toHaveBeenCalledWith('exit', expect.anything()); + expect(mocks.shutdownTelemetry).toHaveBeenCalledOnce(); + expect(stdout.text()).toBe(' Bye!\n'); + expect(stderr.text()).toContain(' To resume this session: kimi -r ses-1'); + } finally { + exitSpy.mockRestore(); + stdout.restore(); + stderr.restore(); + } + }); + + it('surfaces an invalid target config as an error for kimi migrate, not silently', async () => { + mocks.loadTuiConfig.mockResolvedValue({ + theme: 'dark', + editorCommand: null, + notifications: { enabled: true, condition: 'unfocused' }, + }); + mocks.detectPendingMigration.mockResolvedValue({ totalSessions: 1 }); + mocks.harnessGetConfig.mockRejectedValue( + new Error('Invalid configuration in ~/.kimi-code/config.toml'), + ); + + // A broken config.toml must fail loudly — `kimi migrate` must not swallow + // it and proceed, or the user never learns their config is broken. + await expect( + runShell( + { + session: undefined, + continue: false, + yolo: false, + plan: false, + model: undefined, + outputFormat: undefined, + prompt: undefined, + skillsDirs: [], + }, + '1.2.3-test', + { migrateOnly: true }, + ), + ).rejects.toThrow('Invalid configuration'); + expect(mocks.tuiStart).not.toHaveBeenCalled(); + }); +}); diff --git a/apps/kimi-code/test/cli/session-flag-picker.test.ts b/apps/kimi-code/test/cli/session-flag-picker.test.ts new file mode 100644 index 000000000..1f24560fa --- /dev/null +++ b/apps/kimi-code/test/cli/session-flag-picker.test.ts @@ -0,0 +1,78 @@ +import { describe, expect, it } from 'vitest'; + +import { createProgram } from '#/cli/commands'; +import type { CLIOptions } from '#/cli/options'; +import { OptionConflictError, validateOptions } from '#/cli/options'; + +function parse(argv: string[]): CLIOptions { + let captured: CLIOptions | undefined; + const program = createProgram( + '0.0.0-test', + (opts) => { + captured = opts; + }, + () => {}, + ); + program.exitOverride(); + program.configureOutput({ + writeOut: () => {}, + writeErr: () => {}, + }); + program.parse(['node', 'kimi', ...argv]); + if (captured === undefined) { + throw new Error('Main action handler was not called'); + } + return captured; +} + +describe('--session / -r / -S picker routing', () => { + describe('argParser: no-arg forms coerce to empty string', () => { + it('--session with no id → session === ""', () => { + const opts = parse(['--session']); + expect(opts.session).toBe(''); + }); + + it('-S with no id → session === ""', () => { + const opts = parse(['-S']); + expect(opts.session).toBe(''); + }); + + it('-r with no id → session === ""', () => { + const opts = parse(['-r']); + expect(opts.session).toBe(''); + }); + }); + + describe('argParser: id forms keep the id verbatim', () => { + it('--session foo → session === "foo"', () => { + const opts = parse(['--session', 'foo']); + expect(opts.session).toBe('foo'); + }); + + it('-S foo → session === "foo"', () => { + const opts = parse(['-S', 'foo']); + expect(opts.session).toBe('foo'); + }); + + it('-r foo → session === "foo" (hidden alias)', () => { + const opts = parse(['-r', 'foo']); + expect(opts.session).toBe('foo'); + }); + }); + + describe('validateOptions: empty session vs ui mode', () => { + it('empty session + shell mode → OK, session stays ""', () => { + const { options, uiMode } = validateOptions(parse(['--session'])); + expect(uiMode).toBe('shell'); + expect(options.session).toBe(''); + }); + + // Note: --print / --wire are held back from the first release, so + // the "picker + print/wire" combinations can't be constructed via + // Commander anymore. The validateOptions guard still lives in + // source for when those flags return. + }); + + // Silence unused-import warning kept for the preserved guard tests. + void OptionConflictError; +}); diff --git a/apps/kimi-code/test/cli/update/cache.test.ts b/apps/kimi-code/test/cli/update/cache.test.ts new file mode 100644 index 000000000..a1ba436bc --- /dev/null +++ b/apps/kimi-code/test/cli/update/cache.test.ts @@ -0,0 +1,62 @@ +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; + +import { readUpdateCache, writeUpdateCache } from '#/cli/update/cache'; +import { emptyUpdateCache } from '#/cli/update/types'; +import { getUpdateStateFile } from '#/utils/paths'; + +const originalEnv = { ...process.env }; + +let dir: string; + +beforeEach(() => { + dir = mkdtempSync(join(tmpdir(), 'kimi-update-cache-')); + process.env['KIMI_CODE_HOME'] = dir; +}); + +afterEach(() => { + rmSync(dir, { recursive: true, force: true }); + process.env = { ...originalEnv }; +}); + +describe('update cache', () => { + it('returns an empty cache when the file is missing', async () => { + await expect(readUpdateCache()).resolves.toEqual(emptyUpdateCache()); + }); + + it('falls back to an empty cache when the file is corrupt', async () => { + mkdirSync(join(dir, 'updates'), { recursive: true }); + writeFileSync(getUpdateStateFile(), '{"broken"', 'utf-8'); + await expect(readUpdateCache()).resolves.toEqual(emptyUpdateCache()); + }); + + it('falls back to an empty cache when the file has the old npm.json shape', async () => { + mkdirSync(join(dir, 'updates'), { recursive: true }); + writeFileSync( + getUpdateStateFile(), + JSON.stringify({ + packageName: '@moonshot-ai/kimi-code', + checkedAt: '2026-04-23T08:00:00.000Z', + distTags: { beta: '0.0.1-beta.1' }, + }), + 'utf-8', + ); + await expect(readUpdateCache()).resolves.toEqual(emptyUpdateCache()); + }); + + it('writes and reads back the cache from updates/latest.json', async () => { + const cache = { + source: 'cdn', + checkedAt: '2026-04-23T08:00:00.000Z', + latest: '0.5.0', + } as const; + + await writeUpdateCache(cache); + + expect(getUpdateStateFile()).toBe(join(dir, 'updates', 'latest.json')); + await expect(readUpdateCache()).resolves.toEqual(cache); + }); +}); diff --git a/apps/kimi-code/test/cli/update/cdn.test.ts b/apps/kimi-code/test/cli/update/cdn.test.ts new file mode 100644 index 000000000..3127bd71d --- /dev/null +++ b/apps/kimi-code/test/cli/update/cdn.test.ts @@ -0,0 +1,49 @@ +import { describe, expect, it, vi } from 'vitest'; + +import { fetchLatestVersionFromCdn } from '#/cli/update/cdn'; +import { KIMI_CODE_CDN_LATEST_URL } from '#/constant/app'; + +function mockFetchOk(body: string): typeof fetch { + return vi.fn(async () => ({ + ok: true, + status: 200, + text: async () => body, + })) as unknown as typeof fetch; +} + +function mockFetchStatus(status: number): typeof fetch { + return vi.fn(async () => ({ + ok: status >= 200 && status < 300, + status, + text: async () => '', + })) as unknown as typeof fetch; +} + +describe('fetchLatestVersionFromCdn', () => { + it('returns the trimmed semver returned by CDN /latest', async () => { + const f = mockFetchOk(' 0.5.0\n'); + await expect(fetchLatestVersionFromCdn(f)).resolves.toBe('0.5.0'); + expect(f).toHaveBeenCalledWith(KIMI_CODE_CDN_LATEST_URL); + }); + + it('throws when response is non-2xx', async () => { + await expect(fetchLatestVersionFromCdn(mockFetchStatus(404))).rejects.toThrow(/HTTP 404/); + }); + + it('throws when body is not valid semver', async () => { + await expect(fetchLatestVersionFromCdn(mockFetchOk('not-a-version'))).rejects.toThrow( + /invalid semver/, + ); + }); + + it('throws when body is empty', async () => { + await expect(fetchLatestVersionFromCdn(mockFetchOk(' '))).rejects.toThrow(/invalid semver/); + }); + + it('propagates the underlying fetch error', async () => { + const f = vi.fn(async () => { + throw new Error('network down'); + }) as unknown as typeof fetch; + await expect(fetchLatestVersionFromCdn(f)).rejects.toThrow(/network down/); + }); +}); diff --git a/apps/kimi-code/test/cli/update/preflight.test.ts b/apps/kimi-code/test/cli/update/preflight.test.ts new file mode 100644 index 000000000..dc115c9e4 --- /dev/null +++ b/apps/kimi-code/test/cli/update/preflight.test.ts @@ -0,0 +1,269 @@ +import type * as ChildProcess from 'node:child_process'; +import { EventEmitter } from 'node:events'; + +import { afterEach, describe, expect, it, vi } from 'vitest'; + +import { readUpdateCache } from '#/cli/update/cache'; +import { runUpdatePreflight } from '#/cli/update/preflight'; +import { promptForInstallConfirmation } from '#/cli/update/prompt'; +import type * as PromptModule from '#/cli/update/prompt'; +import { refreshUpdateCache } from '#/cli/update/refresh'; +import type * as RefreshModule from '#/cli/update/refresh'; +import { detectInstallSource } from '#/cli/update/source'; +import { emptyUpdateCache, type UpdateCache } from '#/cli/update/types'; + +const mocks = vi.hoisted(() => ({ + readUpdateCache: vi.fn(), + detectInstallSource: vi.fn(), + promptForInstallConfirmation: vi.fn(), + refreshUpdateCache: vi.fn(), + spawn: vi.fn(), +})); + +vi.mock('../../../src/cli/update/cache', () => ({ + readUpdateCache: mocks.readUpdateCache, +})); + +vi.mock('../../../src/cli/update/source', () => ({ + detectInstallSource: mocks.detectInstallSource, +})); + +vi.mock('../../../src/cli/update/prompt', async () => { + const actual = await vi.importActual<typeof PromptModule>('../../../src/cli/update/prompt.js'); + return { + ...actual, + promptForInstallConfirmation: mocks.promptForInstallConfirmation, + }; +}); + +vi.mock('../../../src/cli/update/refresh', async () => { + const actual = await vi.importActual<typeof RefreshModule>('../../../src/cli/update/refresh.js'); + return { + ...actual, + refreshUpdateCache: mocks.refreshUpdateCache, + }; +}); + +vi.mock('node:child_process', async () => { + const actual = await vi.importActual<typeof ChildProcess>('node:child_process'); + return { + ...actual, + spawn: mocks.spawn, + }; +}); + +function cacheWith(version: string): UpdateCache { + return { + source: 'cdn', + checkedAt: '2026-04-23T08:00:00.000Z', + latest: version, + }; +} + +function captureOutput(): { + stdout: string[]; + stderr: string[]; + options: { + stdout: { write(chunk: string): boolean }; + stderr: { write(chunk: string): boolean }; + isTTY: boolean; + }; +} { + const stdout: string[] = []; + const stderr: string[] = []; + return { + stdout, + stderr, + options: { + stdout: { write: (chunk: string) => { stdout.push(chunk); return true; } }, + stderr: { write: (chunk: string) => { stderr.push(chunk); return true; } }, + isTTY: true, + }, + }; +} + +function mockSpawnExit(code: number, signal: NodeJS.Signals | null = null): void { + mocks.spawn.mockImplementation(() => { + const child = new EventEmitter(); + queueMicrotask(() => { child.emit('exit', code, signal); }); + return child; + }); +} + +describe('runUpdatePreflight', () => { + afterEach(() => { vi.clearAllMocks(); }); + + it('continues on first launch with empty cache, still refreshes in background', async () => { + mocks.readUpdateCache.mockResolvedValue(emptyUpdateCache()); + mocks.refreshUpdateCache.mockResolvedValue(emptyUpdateCache()); + const { options } = captureOutput(); + + await expect(runUpdatePreflight('0.4.0', options)).resolves.toBe('continue'); + expect(readUpdateCache).toHaveBeenCalledTimes(1); + expect(refreshUpdateCache).toHaveBeenCalledTimes(1); + expect(detectInstallSource).not.toHaveBeenCalled(); + }); + + it('skips when non-interactive', async () => { + mocks.readUpdateCache.mockResolvedValue(cacheWith('0.5.0')); + mocks.refreshUpdateCache.mockResolvedValue(cacheWith('0.5.0')); + const { options } = captureOutput(); + await expect( + runUpdatePreflight('0.4.0', { ...options, isTTY: false }), + ).resolves.toBe('continue'); + expect(detectInstallSource).not.toHaveBeenCalled(); + }); + + it('npm-global: prompts and spawns npm install -g', async () => { + mocks.readUpdateCache.mockResolvedValue(cacheWith('0.5.0')); + mocks.refreshUpdateCache.mockResolvedValue(cacheWith('0.5.0')); + mocks.detectInstallSource.mockResolvedValue('npm-global'); + mocks.promptForInstallConfirmation.mockResolvedValue(true); + mockSpawnExit(0); + const { stdout, options } = captureOutput(); + + await expect(runUpdatePreflight('0.4.0', options)).resolves.toBe('exit'); + expect(mocks.promptForInstallConfirmation).toHaveBeenCalledWith( + expect.objectContaining({ + installCommand: 'npm install -g @moonshot-ai/kimi-code@0.5.0', + installSource: 'npm-global', + }), + ); + expect(mocks.spawn).toHaveBeenCalledWith( + expect.stringMatching(/^npm(\.cmd)?$/), + ['install', '-g', '@moonshot-ai/kimi-code@0.5.0'], + { stdio: 'inherit' }, + ); + expect(stdout.join('')).toContain('Updated @moonshot-ai/kimi-code to 0.5.0'); + }); + + it('pnpm-global: spawns pnpm add -g', async () => { + mocks.readUpdateCache.mockResolvedValue(cacheWith('0.5.0')); + mocks.refreshUpdateCache.mockResolvedValue(cacheWith('0.5.0')); + mocks.detectInstallSource.mockResolvedValue('pnpm-global'); + mocks.promptForInstallConfirmation.mockResolvedValue(true); + mockSpawnExit(0); + const { options } = captureOutput(); + await runUpdatePreflight('0.4.0', options); + expect(mocks.spawn).toHaveBeenCalledWith( + expect.stringMatching(/^pnpm(\.cmd)?$/), + ['add', '-g', '@moonshot-ai/kimi-code@0.5.0'], + { stdio: 'inherit' }, + ); + }); + + it('yarn-global: spawns yarn global add', async () => { + mocks.readUpdateCache.mockResolvedValue(cacheWith('0.5.0')); + mocks.refreshUpdateCache.mockResolvedValue(cacheWith('0.5.0')); + mocks.detectInstallSource.mockResolvedValue('yarn-global'); + mocks.promptForInstallConfirmation.mockResolvedValue(true); + mockSpawnExit(0); + const { options } = captureOutput(); + await runUpdatePreflight('0.4.0', options); + expect(mocks.spawn).toHaveBeenCalledWith( + expect.stringMatching(/^yarn(\.cmd)?$/), + ['global', 'add', '@moonshot-ai/kimi-code@0.5.0'], + { stdio: 'inherit' }, + ); + }); + + it('bun-global: spawns bun add -g', async () => { + mocks.readUpdateCache.mockResolvedValue(cacheWith('0.5.0')); + mocks.refreshUpdateCache.mockResolvedValue(cacheWith('0.5.0')); + mocks.detectInstallSource.mockResolvedValue('bun-global'); + mocks.promptForInstallConfirmation.mockResolvedValue(true); + mockSpawnExit(0); + const { options } = captureOutput(); + await runUpdatePreflight('0.4.0', options); + expect(mocks.spawn).toHaveBeenCalledWith( + expect.stringMatching(/^bun(\.exe)?$/), + ['add', '-g', '@moonshot-ai/kimi-code@0.5.0'], + { stdio: 'inherit' }, + ); + }); + + it('native on darwin: spawns bash -c curl|bash', async () => { + mocks.readUpdateCache.mockResolvedValue(cacheWith('0.5.0')); + mocks.refreshUpdateCache.mockResolvedValue(cacheWith('0.5.0')); + mocks.detectInstallSource.mockResolvedValue('native'); + mocks.promptForInstallConfirmation.mockResolvedValue(true); + mockSpawnExit(0); + const originalPlatform = process.platform; + Object.defineProperty(process, 'platform', { value: 'darwin' }); + try { + const { options } = captureOutput(); + await runUpdatePreflight('0.4.0', options); + expect(mocks.spawn).toHaveBeenCalledWith( + 'bash', + ['-c', expect.stringContaining('curl -fsSL https://code.kimi.com/kimi-code/install.sh')], + { stdio: 'inherit' }, + ); + } finally { + Object.defineProperty(process, 'platform', { value: originalPlatform }); + } + }); + + it('native on win32: prints manual powershell command, does not spawn', async () => { + mocks.readUpdateCache.mockResolvedValue(cacheWith('0.5.0')); + mocks.refreshUpdateCache.mockResolvedValue(cacheWith('0.5.0')); + mocks.detectInstallSource.mockResolvedValue('native'); + const originalPlatform = process.platform; + Object.defineProperty(process, 'platform', { value: 'win32' }); + try { + const { stdout, options } = captureOutput(); + await expect(runUpdatePreflight('0.4.0', options)).resolves.toBe('continue'); + expect(stdout.join('')).toContain('irm https://code.kimi.com/kimi-code/install.ps1 | iex'); + expect(promptForInstallConfirmation).not.toHaveBeenCalled(); + expect(mocks.spawn).not.toHaveBeenCalled(); + } finally { + Object.defineProperty(process, 'platform', { value: originalPlatform }); + } + }); + + it('unsupported: prints fallback npm command', async () => { + mocks.readUpdateCache.mockResolvedValue(cacheWith('0.5.0')); + mocks.refreshUpdateCache.mockResolvedValue(cacheWith('0.5.0')); + mocks.detectInstallSource.mockResolvedValue('unsupported'); + const { stdout, options } = captureOutput(); + await expect(runUpdatePreflight('0.4.0', options)).resolves.toBe('continue'); + expect(stdout.join('')).toContain('npm install -g @moonshot-ai/kimi-code@0.5.0'); + expect(mocks.spawn).not.toHaveBeenCalled(); + }); + + it('declined install continues without spawn', async () => { + mocks.readUpdateCache.mockResolvedValue(cacheWith('0.5.0')); + mocks.refreshUpdateCache.mockResolvedValue(cacheWith('0.5.0')); + mocks.detectInstallSource.mockResolvedValue('npm-global'); + mocks.promptForInstallConfirmation.mockResolvedValue(false); + const { options } = captureOutput(); + await expect(runUpdatePreflight('0.4.0', options)).resolves.toBe('continue'); + expect(mocks.spawn).not.toHaveBeenCalled(); + }); + + it('warns and continues when spawn exits non-zero', async () => { + mocks.readUpdateCache.mockResolvedValue(cacheWith('0.5.0')); + mocks.refreshUpdateCache.mockResolvedValue(cacheWith('0.5.0')); + mocks.detectInstallSource.mockResolvedValue('npm-global'); + mocks.promptForInstallConfirmation.mockResolvedValue(true); + mockSpawnExit(1); + const { stderr, options } = captureOutput(); + await expect(runUpdatePreflight('0.4.0', options)).resolves.toBe('continue'); + expect(stderr.join('')).toContain('warning: failed to install'); + }); + + it('tracks update_prompted telemetry', async () => { + mocks.readUpdateCache.mockResolvedValue(cacheWith('0.5.0')); + mocks.refreshUpdateCache.mockResolvedValue(cacheWith('0.5.0')); + mocks.detectInstallSource.mockResolvedValue('npm-global'); + mocks.promptForInstallConfirmation.mockResolvedValue(false); + const { options } = captureOutput(); + const track = vi.fn(); + await runUpdatePreflight('0.4.0', { ...options, track }); + expect(track).toHaveBeenCalledWith('update_prompted', expect.objectContaining({ + current: '0.4.0', + latest: '0.5.0', + decision: 'prompt-install', + source: 'npm-global', + })); + }); +}); diff --git a/apps/kimi-code/test/cli/update/prompt.test.ts b/apps/kimi-code/test/cli/update/prompt.test.ts new file mode 100644 index 000000000..b898ab654 --- /dev/null +++ b/apps/kimi-code/test/cli/update/prompt.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, it } from 'vitest'; + +import { + createInstallPromptChoices, + getDefaultInstallPromptSelection, + moveInstallPromptSelection, +} from '#/cli/update/prompt'; + +describe('install prompt helpers', () => { + it('defaults the selection to "Install update now"', () => { + const choices = createInstallPromptChoices({ version: '0.0.2-beta.1' }); + + expect(getDefaultInstallPromptSelection(choices)).toBe(0); + expect(choices[0]).toEqual({ + value: 'install', + label: 'Install update now (0.0.2-beta.1)', + }); + expect(choices[1]).toEqual({ + value: 'skip', + label: 'Continue with current version', + }); + }); + + it('moves selection with arrow directions and clamps at the edges', () => { + expect(moveInstallPromptSelection(1, 'up', 2)).toBe(0); + expect(moveInstallPromptSelection(0, 'up', 2)).toBe(0); + expect(moveInstallPromptSelection(0, 'down', 2)).toBe(1); + expect(moveInstallPromptSelection(1, 'down', 2)).toBe(1); + }); +}); diff --git a/apps/kimi-code/test/cli/update/refresh.test.ts b/apps/kimi-code/test/cli/update/refresh.test.ts new file mode 100644 index 000000000..16a18d2f3 --- /dev/null +++ b/apps/kimi-code/test/cli/update/refresh.test.ts @@ -0,0 +1,40 @@ +import { describe, expect, it, vi } from 'vitest'; + +import { refreshUpdateCache } from '#/cli/update/refresh'; + +describe('refreshUpdateCache', () => { + it('writes a fresh cache on successful fetch', async () => { + const writeCache = vi.fn(async () => {}); + const result = await refreshUpdateCache({ + fetchLatest: async () => '0.5.0', + writeCache, + now: () => new Date('2026-05-20T12:34:56.000Z'), + }); + + expect(result).toEqual({ + source: 'cdn', + checkedAt: '2026-05-20T12:34:56.000Z', + latest: '0.5.0', + }); + expect(writeCache).toHaveBeenCalledWith({ + source: 'cdn', + checkedAt: '2026-05-20T12:34:56.000Z', + latest: '0.5.0', + }); + }); + + it('propagates fetch errors and skips writeCache so the cache is preserved', async () => { + const writeCache = vi.fn(async () => {}); + await expect( + refreshUpdateCache({ + fetchLatest: async () => { + throw new Error('network down'); + }, + writeCache, + now: () => new Date(), + }), + ).rejects.toThrow(/network down/); + + expect(writeCache).not.toHaveBeenCalled(); + }); +}); diff --git a/apps/kimi-code/test/cli/update/select.test.ts b/apps/kimi-code/test/cli/update/select.test.ts new file mode 100644 index 000000000..a616a4867 --- /dev/null +++ b/apps/kimi-code/test/cli/update/select.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, it } from 'vitest'; + +import { selectUpdateTarget } from '#/cli/update/select'; + +describe('selectUpdateTarget', () => { + it('returns the latest version when it is newer than current', () => { + expect(selectUpdateTarget('0.4.0', '0.5.0')).toEqual({ version: '0.5.0' }); + }); + + it('returns null when latest equals current', () => { + expect(selectUpdateTarget('0.5.0', '0.5.0')).toBeNull(); + }); + + it('returns null when latest is older than current', () => { + expect(selectUpdateTarget('0.6.0', '0.5.0')).toBeNull(); + }); + + it('returns null when latest is null (cache empty)', () => { + expect(selectUpdateTarget('0.5.0', null)).toBeNull(); + }); + + it('returns null when current is not a valid semver', () => { + expect(selectUpdateTarget('not-a-version', '0.5.0')).toBeNull(); + }); + + it('returns null when latest is not a valid semver', () => { + expect(selectUpdateTarget('0.5.0', 'not-a-version')).toBeNull(); + }); + + it('handles prerelease semver comparisons correctly', () => { + expect(selectUpdateTarget('0.5.0-rc.1', '0.5.0')).toEqual({ version: '0.5.0' }); + expect(selectUpdateTarget('0.5.0', '0.5.0-rc.1')).toBeNull(); + }); +}); diff --git a/apps/kimi-code/test/cli/update/source.test.ts b/apps/kimi-code/test/cli/update/source.test.ts new file mode 100644 index 000000000..20bba5cd1 --- /dev/null +++ b/apps/kimi-code/test/cli/update/source.test.ts @@ -0,0 +1,149 @@ +import { describe, expect, it } from 'vitest'; + +import { + classifyByPathHeuristic, + classifyInstallSource, + detectInstallSource, +} from '#/cli/update/source'; + +describe('classifyByPathHeuristic', () => { + it('returns null for an npm-style global path (handled by classifyInstallSource)', () => { + expect(classifyByPathHeuristic('/usr/local/lib/node_modules/@moonshot-ai/kimi-code')).toBeNull(); + }); + + it('detects pnpm global on macOS', () => { + expect( + classifyByPathHeuristic('/Users/me/Library/pnpm/global/5/node_modules/@moonshot-ai/kimi-code'), + ).toBe('pnpm-global'); + }); + + it('detects pnpm global on Linux', () => { + expect( + classifyByPathHeuristic('/home/me/.local/share/pnpm/global/5/node_modules/@moonshot-ai/kimi-code'), + ).toBe('pnpm-global'); + }); + + it('detects pnpm global on Windows (normalized backslashes)', () => { + expect( + classifyByPathHeuristic('C:\\Users\\me\\AppData\\Local\\pnpm\\global\\5\\node_modules\\@moonshot-ai/kimi-code'), + ).toBe('pnpm-global'); + }); + + it('detects yarn classic global', () => { + expect( + classifyByPathHeuristic('/Users/me/.config/yarn/global/node_modules/@moonshot-ai/kimi-code'), + ).toBe('yarn-global'); + }); + + it('detects yarn berry global (~/.yarn/global)', () => { + expect( + classifyByPathHeuristic('/Users/me/.yarn/global/node_modules/@moonshot-ai/kimi-code'), + ).toBe('yarn-global'); + }); + + it('detects bun global', () => { + expect( + classifyByPathHeuristic('/Users/me/.bun/install/global/node_modules/@moonshot-ai/kimi-code'), + ).toBe('bun-global'); + }); + + it('returns null for an unknown layout', () => { + expect(classifyByPathHeuristic('/Users/me/dev/@moonshot-ai/kimi-code')).toBeNull(); + }); +}); + +describe('classifyInstallSource (npm prefix matching)', () => { + it('matches a macOS/Linux npm global package path', () => { + expect( + classifyInstallSource('/usr/local/lib/node_modules/@moonshot-ai/kimi-code', '/usr/local', 'darwin'), + ).toBe('npm-global'); + }); + + it('returns unsupported when the package path does not match the prefix', () => { + expect( + classifyInstallSource('/Users/me/dev/@moonshot-ai/kimi-code', '/usr/local', 'darwin'), + ).toBe('unsupported'); + }); +}); + +describe('detectInstallSource', () => { + it('returns pnpm-global when packageRoot matches pnpm heuristic', async () => { + await expect( + detectInstallSource({ + getPackageRoot: () => + '/Users/me/Library/pnpm/global/5/node_modules/@moonshot-ai/kimi-code', + getGlobalPrefix: async () => '/usr/local', + detectNative: () => false, + platform: 'darwin', + }), + ).resolves.toBe('pnpm-global'); + }); + + it('returns yarn-global when packageRoot matches yarn heuristic', async () => { + await expect( + detectInstallSource({ + getPackageRoot: () => '/Users/me/.config/yarn/global/node_modules/@moonshot-ai/kimi-code', + getGlobalPrefix: async () => '/usr/local', + detectNative: () => false, + platform: 'darwin', + }), + ).resolves.toBe('yarn-global'); + }); + + it('returns bun-global when packageRoot matches bun heuristic', async () => { + await expect( + detectInstallSource({ + getPackageRoot: () => '/Users/me/.bun/install/global/node_modules/@moonshot-ai/kimi-code', + getGlobalPrefix: async () => '/usr/local', + detectNative: () => false, + platform: 'darwin', + }), + ).resolves.toBe('bun-global'); + }); + + it('returns npm-global when packageRoot matches npm prefix', async () => { + await expect( + detectInstallSource({ + getPackageRoot: () => '/usr/local/lib/node_modules/@moonshot-ai/kimi-code', + getGlobalPrefix: async () => '/usr/local', + detectNative: () => false, + platform: 'darwin', + }), + ).resolves.toBe('npm-global'); + }); + + it('returns native when SEA isSea() is true (highest priority)', async () => { + await expect( + detectInstallSource({ + getPackageRoot: () => '/usr/local/lib/node_modules/@moonshot-ai/kimi-code', + getGlobalPrefix: async () => '/usr/local', + detectNative: () => true, + platform: 'darwin', + }), + ).resolves.toBe('native'); + }); + + it('returns unsupported when nothing matches', async () => { + await expect( + detectInstallSource({ + getPackageRoot: () => '/Users/me/dev/@moonshot-ai/kimi-code', + getGlobalPrefix: async () => '/usr/local', + detectNative: () => false, + platform: 'darwin', + }), + ).resolves.toBe('unsupported'); + }); + + it('returns unsupported when npm prefix lookup throws', async () => { + await expect( + detectInstallSource({ + getPackageRoot: () => '/Users/me/dev/@moonshot-ai/kimi-code', + getGlobalPrefix: async () => { + throw new Error('prefix failed'); + }, + detectNative: () => false, + platform: 'darwin', + }), + ).resolves.toBe('unsupported'); + }); +}); diff --git a/apps/kimi-code/test/cli/version.test.ts b/apps/kimi-code/test/cli/version.test.ts new file mode 100644 index 000000000..f17240eff --- /dev/null +++ b/apps/kimi-code/test/cli/version.test.ts @@ -0,0 +1,28 @@ +import { readFileSync } from 'node:fs'; +import { dirname } from 'node:path'; + +import { describe, expect, it } from 'vitest'; + +import { + buildKimiDefaultHeaders, + getHostPackageJsonPath, + getHostPackageRoot, + getVersion, +} from '#/cli/version'; + +describe('cli version helpers', () => { + it('resolves the host package manifest near apps/kimi-code and reads its version', () => { + const pkgPath = getHostPackageJsonPath(); + const pkg = JSON.parse(readFileSync(pkgPath, 'utf8')) as { version: string }; + + expect(pkgPath.endsWith('/apps/kimi-code/package.json')).toBe(true); + expect(getHostPackageRoot()).toBe(dirname(pkgPath)); + expect(getVersion()).toBe(pkg.version); + }); + + it('builds default headers with the kimi-code-cli user-agent', () => { + const headers = buildKimiDefaultHeaders('1.2.3'); + + expect(headers['User-Agent']).toBe('kimi-code-cli/1.2.3'); + }); +}); diff --git a/apps/kimi-code/test/e2e/local-logging-export.e2e.test.ts b/apps/kimi-code/test/e2e/local-logging-export.e2e.test.ts new file mode 100644 index 000000000..8421105f0 --- /dev/null +++ b/apps/kimi-code/test/e2e/local-logging-export.e2e.test.ts @@ -0,0 +1,143 @@ +import { readFile, mkdtemp, rm } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import * as zlib from 'node:zlib'; + +import { Command } from 'commander'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; + +import { registerExportCommand } from '#/cli/sub/export'; +import { createKimiCodeHostIdentity } from '#/cli/version'; +import { KimiHarness, log } from '@moonshot-ai/kimi-code-sdk'; +import { __resetRootLoggerForTest } from '../../../../packages/agent-core/src/logging/logger'; + +const SESSION_LOG = 'logs/kimi-code.log'; +const GLOBAL_LOG = 'logs/global/kimi-code.log'; +const MAIN_WIRE = 'agents/main/wire.jsonl'; +const ENABLED = process.env['KIMI_E2E'] === '1'; + +let homeDir: string; +let workDir: string; +let oldHome: string | undefined; + +beforeEach(async () => { + await __resetRootLoggerForTest(); + homeDir = await mkdtemp(join(tmpdir(), 'kimi-cli-log-home-')); + workDir = await mkdtemp(join(tmpdir(), 'kimi-cli-log-work-')); + oldHome = process.env['KIMI_CODE_HOME']; + process.env['KIMI_CODE_HOME'] = homeDir; +}); + +afterEach(async () => { + await __resetRootLoggerForTest(); + if (oldHome === undefined) { + delete process.env['KIMI_CODE_HOME']; + } else { + process.env['KIMI_CODE_HOME'] = oldHome; + } + await rm(homeDir, { recursive: true, force: true }); + await rm(workDir, { recursive: true, force: true }); +}); + +describe.skipIf(!ENABLED)('local logging export e2e', () => { + it('exports session log and global log by default, and allows skipping global log', async () => { + const harness = new KimiHarness({ + homeDir, + identity: createKimiCodeHostIdentity('0.1.1'), + }); + try { + const session = await harness.createSession({ + id: 'ses_cli_logging_export', + workDir, + }); + log.warn('cli logging export marker', { sessionId: session.id }); + log.warn('cli global marker'); + + const defaultZip = join(workDir, 'default.zip'); + await runKimiExport([session.id, '-o', defaultZip]); + const defaultEntries = readZipEntries(await readFile(defaultZip)); + expect(defaultEntries.has(MAIN_WIRE)).toBe(true); + expect(defaultEntries.has(SESSION_LOG)).toBe(true); + expect(defaultEntries.has(GLOBAL_LOG)).toBe(true); + expect(defaultEntries.get(SESSION_LOG)!.toString('utf-8')).toContain( + 'cli logging export marker', + ); + expect(defaultEntries.get(GLOBAL_LOG)!.toString('utf-8')).toContain('cli global marker'); + const defaultManifest = JSON.parse( + defaultEntries.get('manifest.json')!.toString('utf-8'), + ) as Record<string, unknown>; + expect(defaultManifest['sessionLogPath']).toBe(SESSION_LOG); + expect(defaultManifest['globalLogPath']).toBe(GLOBAL_LOG); + + const noGlobalZip = join(workDir, 'no-global.zip'); + await runKimiExport([session.id, '-o', noGlobalZip, '--no-include-global-log']); + const noGlobalEntries = readZipEntries(await readFile(noGlobalZip)); + expect(noGlobalEntries.has(GLOBAL_LOG)).toBe(false); + const noGlobalManifest = JSON.parse( + noGlobalEntries.get('manifest.json')!.toString('utf-8'), + ) as Record<string, unknown>; + expect(noGlobalManifest['globalLogPath']).toBeUndefined(); + } finally { + await harness.close().catch(() => {}); + } + }, 15_000); +}); + +async function runKimiExport(args: string[]): Promise<void> { + const program = new Command('kimi'); + const stdout: string[] = []; + const stderr: string[] = []; + registerExportCommand(program, { + stdout: { + write: (chunk) => { + stdout.push(chunk); + return true; + }, + }, + stderr: { + write: (chunk) => { + stderr.push(chunk); + return true; + }, + }, + exit: (code: number): never => { + throw new Error(`kimi export exited ${code}: ${stderr.join('')}`); + }, + }); + await program.parseAsync(['node', 'kimi', 'export', ...args]); +} + +function readZipEntries(buf: Buffer): Map<string, Buffer> { + let eocd = -1; + for (let i = buf.length - 22; i >= Math.max(0, buf.length - 65_557); i--) { + if (buf.readUInt32LE(i) === 0x06054b50) { + eocd = i; + break; + } + } + if (eocd === -1) throw new Error('zip eocd not found'); + const entryCount = buf.readUInt16LE(eocd + 10); + const cdOffset = buf.readUInt32LE(eocd + 16); + const entries = new Map<string, Buffer>(); + let pos = cdOffset; + for (let i = 0; i < entryCount; i++) { + if (buf.readUInt32LE(pos) !== 0x02014b50) throw new Error('bad cd entry'); + const method = buf.readUInt16LE(pos + 10); + const compressedSize = buf.readUInt32LE(pos + 20); + const fnameLen = buf.readUInt16LE(pos + 28); + const extraLen = buf.readUInt16LE(pos + 30); + const commentLen = buf.readUInt16LE(pos + 32); + const lfhOffset = buf.readUInt32LE(pos + 42); + const filename = buf.toString('utf8', pos + 46, pos + 46 + fnameLen); + if (buf.readUInt32LE(lfhOffset) !== 0x04034b50) throw new Error('bad lfh'); + const lfhFnameLen = buf.readUInt16LE(lfhOffset + 26); + const lfhExtraLen = buf.readUInt16LE(lfhOffset + 28); + const dataStart = lfhOffset + 30 + lfhFnameLen + lfhExtraLen; + const compressed = buf.subarray(dataStart, dataStart + compressedSize); + const data = method === 0 ? compressed : method === 8 ? zlib.inflateRawSync(compressed) : null; + if (data === null) throw new Error('unsupported compression'); + entries.set(filename, data); + pos += 46 + fnameLen + extraLen + commentLen; + } + return entries; +} diff --git a/apps/kimi-code/test/e2e/real-llm-smoke.e2e.test.ts b/apps/kimi-code/test/e2e/real-llm-smoke.e2e.test.ts new file mode 100644 index 000000000..ffa93ba06 --- /dev/null +++ b/apps/kimi-code/test/e2e/real-llm-smoke.e2e.test.ts @@ -0,0 +1,81 @@ +/** + * Real-LLM smoke. Opt-in only — runs when `KIMI_E2E_REAL=1` is set, so + * `make test` / CI never touches the network. Goes through the SDK's + * `KimiHarness` entry and round-trips one real prompt. Requires provider + * credentials on disk. + * + * Env knobs: + * KIMI_E2E_REAL — set to "1" to enable this suite + * KIMI_E2E_MODEL — model alias override (default: config's default) + * KIMI_E2E_PROMPT — prompt text (default: "Reply with a single word: hi") + * KIMI_E2E_WORKDIR — workspace directory (default: /tmp/kimi-e2e) + */ + +import { mkdirSync } from 'node:fs'; +import process from 'node:process'; + +import { KimiHarness, type Event } from '@moonshot-ai/kimi-code-sdk'; +import { describe, expect, test } from 'vitest'; + +import { createKimiCodeHostIdentity, getVersion } from '#/cli/version'; + +const DEFAULT_PROMPT = 'Reply with a single word: hi'; +const DEFAULT_WORKDIR = '/tmp/kimi-e2e'; +const TURN_TIMEOUT_MS = 60_000; + +const ENABLED = process.env['KIMI_E2E_REAL'] === '1'; +type TurnEndedEvent = Extract<Event, { readonly type: 'turn.ended' }>; + +describe.skipIf(!ENABLED)('SDK e2e — real LLM smoke', () => { + test( + 'round-trips a single prompt through KimiHarness', + async () => { + const workDir = process.env['KIMI_E2E_WORKDIR'] ?? DEFAULT_WORKDIR; + const prompt = process.env['KIMI_E2E_PROMPT'] ?? DEFAULT_PROMPT; + const modelAlias = process.env['KIMI_E2E_MODEL']; + mkdirSync(workDir, { recursive: true }); + + const version = getVersion(); + process.stderr.write( + `[smoke] workDir=${workDir}${modelAlias !== undefined ? ` model=${modelAlias}` : ''}\n` + + `[smoke] prompt=${JSON.stringify(prompt)}\n`, + ); + + const harness = new KimiHarness({ + identity: createKimiCodeHostIdentity(version), + }); + + try { + const session = await harness.createSession({ + workDir, + model: modelAlias, + }); + process.stderr.write(`[smoke] session created: ${session.id}\n`); + + const turnEnded = new Promise<TurnEndedEvent>((resolve) => { + const off = session.onEvent((event) => { + const payload = JSON.stringify(event).slice(0, 500); + process.stdout.write(`[smoke][event:${event.type}] ${payload}\n`); + if (event.type === 'turn.ended') { + off(); + resolve(event); + } + }); + }); + + await session.prompt(prompt); + process.stderr.write('[smoke] prompt dispatched\n'); + + const event = await turnEnded; + process.stderr.write(`[smoke] turn.ended reason=${event.reason}\n`); + if (event.error !== undefined) { + process.stderr.write(`[smoke] error=${event.error.message}\n`); + } + expect(event.reason).toBe('completed'); + } finally { + await harness.close().catch(() => {}); + } + }, + TURN_TIMEOUT_MS + 10_000, + ); +}); diff --git a/apps/kimi-code/test/helpers/process.ts b/apps/kimi-code/test/helpers/process.ts new file mode 100644 index 000000000..5a861724b --- /dev/null +++ b/apps/kimi-code/test/helpers/process.ts @@ -0,0 +1,32 @@ +import { vi } from 'vitest'; + +export class ExitCalled extends Error { + constructor(public readonly code: number) { + super(`exit(${code})`); + } +} + +export function mockProcessExit(): { mockRestore(): void } { + return vi.spyOn(process, 'exit').mockImplementation(((code?: string | number | null) => { + throw new ExitCalled(Number(code ?? 0)); + }) as never); +} + +export function captureProcessWrite(stream: 'stdout' | 'stderr'): { + readonly chunks: string[]; + readonly text: () => string; + readonly restore: () => void; +} { + const chunks: string[] = []; + const target = process[stream]; + const spy = vi.spyOn(target, 'write').mockImplementation(((chunk: string | Uint8Array) => { + chunks.push(typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString('utf8')); + return true; + }) as never); + + return { + chunks, + text: () => chunks.join(''), + restore: () =>{ spy.mockRestore(); }, + }; +} diff --git a/apps/kimi-code/test/migration/badge.test.ts b/apps/kimi-code/test/migration/badge.test.ts new file mode 100644 index 000000000..68fb72bf3 --- /dev/null +++ b/apps/kimi-code/test/migration/badge.test.ts @@ -0,0 +1,47 @@ +import { describe, expect, it } from 'vitest'; + +import { formatSessionLabel } from '#/migration/badge'; + +describe('formatSessionLabel', () => { + it('prepends [imported] when metadata.imported_from_kimi_cli === true', () => { + const label = formatSessionLabel({ + title: 'Refactor sessions list', + metadata: { imported_from_kimi_cli: true }, + }); + expect(label).toBe('[imported] Refactor sessions list'); + }); + + it('does not prepend [imported] when metadata is missing', () => { + const label = formatSessionLabel({ title: 'Plain session' }); + expect(label).toBe('Plain session'); + }); + + it('does not prepend [imported] when metadata is empty', () => { + const label = formatSessionLabel({ title: 'Plain session', metadata: {} }); + expect(label).toBe('Plain session'); + }); + + it('only triggers on the literal boolean true (not truthy values)', () => { + const label = formatSessionLabel({ + title: 'truthy but not true', + metadata: { imported_from_kimi_cli: 'yes' as unknown }, + }); + expect(label).toBe('truthy but not true'); + }); + + it('does not prepend [imported] when flag is false', () => { + const label = formatSessionLabel({ + title: 'native session', + metadata: { imported_from_kimi_cli: false }, + }); + expect(label).toBe('native session'); + }); + + it('preserves the title even when it is empty', () => { + const label = formatSessionLabel({ + title: '', + metadata: { imported_from_kimi_cli: true }, + }); + expect(label).toBe('[imported] '); + }); +}); diff --git a/apps/kimi-code/test/migration/command.test.ts b/apps/kimi-code/test/migration/command.test.ts new file mode 100644 index 000000000..2124dd70b --- /dev/null +++ b/apps/kimi-code/test/migration/command.test.ts @@ -0,0 +1,29 @@ +/** + * `kimi migrate` — a bare, flagless subcommand that delegates to a host + * handler. The migration UI is the native pi-tui screen, covered separately + * by `migration-screen.test.ts`. + */ + +import { Command } from 'commander'; +import { describe, expect, it, vi } from 'vitest'; + +import { registerMigrateCommand } from '#/migration/command'; + +describe('registerMigrateCommand', () => { + it('adds a flagless migrate subcommand to the program', () => { + const program = new Command('kimi'); + registerMigrateCommand(program, () => {}); + const sub = program.commands.find((c) => c.name() === 'migrate'); + expect(sub).toBeDefined(); + expect(sub!.description()).toContain('Migrate'); + expect(sub!.options).toHaveLength(0); + }); + + it('invokes the host handler when `migrate` runs', () => { + const program = new Command('kimi'); + const onMigrate = vi.fn(); + registerMigrateCommand(program, onMigrate); + program.parse(['migrate'], { from: 'user' }); + expect(onMigrate).toHaveBeenCalledTimes(1); + }); +}); diff --git a/apps/kimi-code/test/migration/detect-pending.test.ts b/apps/kimi-code/test/migration/detect-pending.test.ts new file mode 100644 index 000000000..c674d054d --- /dev/null +++ b/apps/kimi-code/test/migration/detect-pending.test.ts @@ -0,0 +1,79 @@ +import { describe, expect, it, beforeEach, afterEach } from 'vitest'; +import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { detectPendingMigration } from '#/migration/detect-pending'; + +let src: string; +let tgt: string; +beforeEach(async () => { + src = await mkdtemp(join(tmpdir(), 'detect-pending-src-')); + tgt = await mkdtemp(join(tmpdir(), 'detect-pending-tgt-')); +}); +afterEach(async () => { + await rm(src, { recursive: true, force: true }); + await rm(tgt, { recursive: true, force: true }); +}); + +describe('detectPendingMigration', () => { + it('returns null when source dir does not exist', async () => { + const plan = await detectPendingMigration({ sourceHome: join(src, 'nope'), targetHome: tgt }); + expect(plan).toBeNull(); + }); + + it('returns null when the migrated marker exists', async () => { + await writeFile(join(src, '.migrated-to-kimi-code'), '{}', 'utf-8'); + const plan = await detectPendingMigration({ sourceHome: src, targetHome: tgt }); + expect(plan).toBeNull(); + }); + + it('returns null when the skip marker exists in target', async () => { + await writeFile(join(src, 'config.toml'), '', 'utf-8'); + await writeFile(join(tgt, '.skip-migration-from-kimi-cli'), '', 'utf-8'); + const plan = await detectPendingMigration({ sourceHome: src, targetHome: tgt }); + expect(plan).toBeNull(); + }); + + it('returns null when source has nothing worth migrating', async () => { + // empty source dir, no config/mcp/credentials/sessions + const plan = await detectPendingMigration({ sourceHome: src, targetHome: tgt }); + expect(plan).toBeNull(); + }); + + it('returns a MigrationPlan when source has migratable data', async () => { + await writeFile(join(src, 'config.toml'), 'default_thinking = true\n', 'utf-8'); + const plan = await detectPendingMigration({ sourceHome: src, targetHome: tgt }); + expect(plan).not.toBeNull(); + expect(plan?.hasConfig).toBe(true); + }); + + it('returns a MigrationPlan when source has only user-history', async () => { + await mkdir(join(src, 'user-history'), { recursive: true }); + await writeFile(join(src, 'user-history', 'shell.txt'), 'ls\n', 'utf-8'); + const plan = await detectPendingMigration({ sourceHome: src, targetHome: tgt }); + expect(plan).not.toBeNull(); + expect(plan?.hasUserHistory).toBe(true); + }); + + it('does not suppress when the marker targeted a different home', async () => { + await writeFile(join(src, 'config.toml'), 'default_thinking = true\n', 'utf-8'); + await writeFile( + join(src, '.migrated-to-kimi-code'), + JSON.stringify({ version: 1, target_path: '/some/other/home' }), + 'utf-8', + ); + const plan = await detectPendingMigration({ sourceHome: src, targetHome: tgt }); + expect(plan).not.toBeNull(); // this target was never migrated → still offer + }); + + it('suppresses when the marker targeted this home', async () => { + await writeFile(join(src, 'config.toml'), 'default_thinking = true\n', 'utf-8'); + await writeFile( + join(src, '.migrated-to-kimi-code'), + JSON.stringify({ version: 1, target_path: tgt }), + 'utf-8', + ); + const plan = await detectPendingMigration({ sourceHome: src, targetHome: tgt }); + expect(plan).toBeNull(); + }); +}); diff --git a/apps/kimi-code/test/migration/migration-screen.test.ts b/apps/kimi-code/test/migration/migration-screen.test.ts new file mode 100644 index 000000000..90b34dca3 --- /dev/null +++ b/apps/kimi-code/test/migration/migration-screen.test.ts @@ -0,0 +1,617 @@ +import { describe, expect, it, vi } from 'vitest'; +import { + MigrationScreenComponent, + type MigrationScreenResult, +} from '#/migration/migration-screen'; +import { darkColors } from '#/tui/theme/colors'; +import type { + MigrationPlan, + MigrationReport, + RunMigrationInput, +} from '@moonshot-ai/migration-legacy'; + +function makePlan(over: Partial<MigrationPlan> = {}): MigrationPlan { + return { + sourceHome: '/x/.kimi', + hasConfig: true, + hasMcp: true, + hasUserHistory: true, + oauthCredentials: ['kimi-code.json'], + workdirs: [], + detectedPlugins: [], + detectedMcpOauthServers: [], + totalSessions: 1365, + ...over, + }; +} + +function render(c: MigrationScreenComponent): string { + return c.render(80).join('\n'); +} + +describe('MigrationScreenComponent — ask phase', () => { + it('ask1 renders the intro block and three options', () => { + const c = new MigrationScreenComponent({ + plan: makePlan(), + sourceHome: '/x/.kimi', + targetHome: '/y/.kimi-code', + colors: darkColors, + onComplete: () => {}, + }); + const out = render(c); + expect(out).toContain('Migrate from kimi-cli'); + expect(out).toContain('1365 sessions'); + expect(out).toContain('Migrate now'); + expect(out).toContain('Ask me later'); + expect(out).toContain('Never ask again'); + }); + + it('ask1 summary shows the kimi-cli login hint when OAuth credentials are detected', async () => { + // OAuth credentials are detected but not migrated — surface that up front + // so the pre-migration summary is consistent with the result screen's + // "⚠ kimi-cli login not migrated — run /login" line. + const c = new MigrationScreenComponent({ + plan: makePlan(), + sourceHome: '/x/.kimi', + targetHome: '/y/.kimi-code', + colors: darkColors, + onComplete: () => {}, + }); + const out = render(c); + expect(out).toContain('kimi-cli login'); + }); + + it('ask1 renders a non-empty summary when the only detected data is an OAuth login', async () => { + // Legacy install with nothing but `credentials/*.json` would otherwise + // render the summary line as blank — `summarizePlan` no longer treats + // OAuth as a migrated kind. The OAuth hint must keep that line useful. + const c = new MigrationScreenComponent({ + plan: makePlan({ + hasConfig: false, + hasMcp: false, + hasUserHistory: false, + totalSessions: 0, + workdirs: [], + }), + sourceHome: '/x/.kimi', + targetHome: '/y/.kimi-code', + colors: darkColors, + onComplete: () => {}, + }); + const out = render(c); + expect(out).toContain('kimi-cli login'); + // ...and the summary line is not blank — the OAuth hint is its content. + expect(out).toMatch(/Found an existing kimi-cli installation:\n\s+\S/); + }); + + it('picking "Ask me later" at ask1 completes with decision=later', () => { + let result: { decision: string } | undefined; + const c = new MigrationScreenComponent({ + plan: makePlan(), + sourceHome: '/x/.kimi', + targetHome: '/y/.kimi-code', + colors: darkColors, + onComplete: (r) => { + result = r; + }, + }); + c.handleInput('\u001B[B'); // Down -> "Ask me later" + c.handleInput('\r'); // Enter + expect(result?.decision).toBe('later'); + }); + + it('"Migrate now" -> "Config only" advances ask1 -> ask2 and resolves scope.sessions=false', async () => { + let captured: RunMigrationInput | undefined; + const c = new MigrationScreenComponent({ + plan: makePlan(), + sourceHome: '/x/.kimi', + targetHome: '/y/.kimi-code', + colors: darkColors, + runMigration: async (input) => { + captured = input; + return makeReport(); + }, + onComplete: () => {}, + }); + c.handleInput('\r'); // ask1: "Migrate now" + c.handleInput('\r'); // ask2: "Config only" (first option) + await new Promise((r) => setTimeout(r, 0)); + expect(captured?.scope.sessions).toBe(false); + }); + + it('"Migrate now" -> "Config + sessions" begins migration immediately with sessions=true', async () => { + let captured: RunMigrationInput | undefined; + const c = new MigrationScreenComponent({ + plan: makePlan(), + sourceHome: '/x/.kimi', + targetHome: '/y/.kimi-code', + colors: darkColors, + runMigration: async (input) => { + captured = input; + return makeReport(); + }, + onComplete: () => {}, + }); + c.handleInput('\r'); // ask1: Migrate now + c.handleInput('\u001B[B'); // ask2: down -> Also migrate sessions + c.handleInput('\r'); // ask2 select -> "Config + N sessions" begins migration immediately + await new Promise((r) => setTimeout(r, 0)); + expect(captured?.scope.sessions).toBe(true); + }); + + it('ask2 shows the detected session count alongside the "config only" option', () => { + const c = new MigrationScreenComponent({ + plan: makePlan({ totalSessions: 1365 }), + sourceHome: '/x/.kimi', + targetHome: '/y/.kimi-code', + colors: darkColors, + onComplete: () => {}, + }); + c.handleInput('\r'); // ask1: Migrate now -> ask2 + const out = render(c); + expect(out).toContain('Config only'); + // Concrete count so the user sees the cost of "+ sessions" up front. + expect(out).toContain('Config + 1365 sessions'); + expect(out).not.toContain('Most recent'); + expect(out).not.toContain('Migrate now'); + }); + + it('ask2 falls back to "Config + all sessions" when no sessions were detected', () => { + const c = new MigrationScreenComponent({ + plan: makePlan({ totalSessions: 0 }), + sourceHome: '/x/.kimi', + targetHome: '/y/.kimi-code', + colors: darkColors, + onComplete: () => {}, + }); + c.handleInput('\r'); // ask1 -> ask2 + const out = render(c); + expect(out).toContain('Config + all sessions'); + // "Config + 0 sessions" would read as an obvious dead-end. + expect(out).not.toContain('Config + 0 sessions'); + }); + + it('skipDecisionStep starts at the scope question with the now/later/never gate hidden', () => { + const c = new MigrationScreenComponent({ + plan: makePlan(), + sourceHome: '/x/.kimi', + targetHome: '/y/.kimi-code', + colors: darkColors, + skipDecisionStep: true, + onComplete: () => {}, + }); + const out = render(c); + expect(out).toContain('Migrate chat sessions too?'); + expect(out).not.toContain('Migrate now'); + expect(out).not.toContain('Never ask again'); + }); + + it('skipDecisionStep -> "Config only" resolves scope without the decision step', async () => { + let captured: RunMigrationInput | undefined; + const c = new MigrationScreenComponent({ + plan: makePlan(), + sourceHome: '/x/.kimi', + targetHome: '/y/.kimi-code', + colors: darkColors, + skipDecisionStep: true, + runMigration: async (input) => { + captured = input; + return makeReport(); + }, + onComplete: () => {}, + }); + c.handleInput('\r'); // ask2: "Config only" (first option) — no ask1 gate + await new Promise((r) => setTimeout(r, 0)); + expect(captured?.scope.sessions).toBe(false); + }); +}); + +describe('MigrationScreenComponent — progress phase', () => { + it('renders a step checklist and the session counter when in progress', () => { + const c = new MigrationScreenComponent({ + plan: makePlan(), + sourceHome: '/x/.kimi', + targetHome: '/y/.kimi-code', + colors: darkColors, + onComplete: () => {}, + }); + // expose progress rendering via the test hook (see Step 5.2) + c._testEnterProgress(); + c._testUpdateStep('config done'); + c._testUpdateSessionProgress(32, 50); + const out = c.render(80).join('\n'); + expect(out).toContain('Migrating from kimi-cli'); + expect(out).toContain('32 / 50'); + expect(out).toContain('Config'); + }); + + it('animates the progress spinner while a migration step runs', async () => { + vi.useFakeTimers(); + try { + const c = new MigrationScreenComponent({ + plan: makePlan(), + sourceHome: '/x/.kimi', + targetHome: '/y/.kimi-code', + colors: darkColors, + skipDecisionStep: true, + // A migration that never settles keeps the screen in the progress + // phase so the spinner animation can be observed. + runMigration: () => new Promise<MigrationReport>(() => {}), + onComplete: () => {}, + }); + c.handleInput('\r'); // ask2: "Config only" -> migration begins + c._testUpdateSessionProgress(1, 3); // surface the spinner line + const before = c.render(80).join('\n'); + vi.advanceTimersByTime(400); // several spinner frames + const after = c.render(80).join('\n'); + // Before the fix nothing advanced the spinner — the frame, and the whole + // progress render, stayed frozen on the first braille glyph. + expect(after).not.toBe(before); + } finally { + vi.useRealTimers(); + } + }); + + it('tracks Config and MCP as independent steps', () => { + const c = new MigrationScreenComponent({ + plan: makePlan(), + sourceHome: '/x/.kimi', + targetHome: '/y/.kimi-code', + colors: darkColors, + onComplete: () => {}, + }); + c._testEnterProgress(); + c._testUpdateStep('config done'); // config finished; MCP has not started + const out = c.render(80).join('\n'); + // Four checklist rows (config, mcp, user-history, sessions). With only + // config done, exactly one shows ✓ and the other three show ◐ — MCP is + // its own step and stays pending. + expect((out.match(/✓/g) ?? []).length).toBe(1); + expect((out.match(/◐/g) ?? []).length).toBe(3); + }); +}); + +function makeReport( + over: Partial<MigrationReport['summary']['sessions']> = {}, + summaryOver: Partial<MigrationReport['summary']> = {}, + noticesOver: Partial<MigrationReport['notices']> = {}, +): MigrationReport { + return { + startedAt: 's', + completedAt: 'e', + migratorVersion: '0.1.1', + source: '/x/.kimi', + target: '/y/.kimi-code', + summary: { + config: { + migrated: true, + tuiExtracted: false, + droppedProviders: [], + droppedModels: [], + droppedKeys: [], + configConflicts: [], + wroteSiblingDueToConflict: false, + wroteTuiSibling: false, + migratedHooks: 0, + droppedHooks: 0, + siblingContents: { providers: [], models: [], hooks: 0 }, + }, + mcp: { mergedServers: [], keptNewForConflicts: [], droppedServers: [], wroteSiblingDueToConflict: false }, + userHistory: { copied: 12, skippedExisting: 0 }, + sessions: { + scope: 'all', + bucketsScanned: 0, + bucketsSkippedNonlocalKaos: 0, + bucketsSkippedNoWorkdirFound: 0, + sessionsAttempted: 50, + sessionsMigrated: 50, + sessionsAlreadyMigrated: 0, + sessionsSkippedPlaceholder: 0, + sessionsSkippedEmpty: 0, + sessionsSkippedMalformed: 0, + sessionsFailed: [], + sessionsConflicts: [], + ...over, + }, + ...summaryOver, + }, + notices: { + mcpOauthServersRequiringReauth: [], + oauthLoginsRequiringRelogin: [], + detectedPlugins: ['p1', 'p2'], + configConflictNotice: null, + tuiConflictNotice: null, + ...noticesOver, + }, + }; +} + +describe('MigrationScreenComponent — result phase', () => { + it('renders the report summary including plugin notices', () => { + const c = new MigrationScreenComponent({ + plan: makePlan(), + sourceHome: '/x/.kimi', + targetHome: '/y/.kimi-code', + colors: darkColors, + onComplete: () => {}, + }); + c._testShowResult(makeReport()); + const out = c.render(80).join('\n'); + expect(out).toContain('Migration complete'); + expect(out).toContain('50 sessions migrated'); + expect(out).toContain('2 kimi-cli plugins'); + }); + + it('renders migrated hooks in the ✓ line and dropped hooks as a warning', () => { + const c = new MigrationScreenComponent({ + plan: makePlan(), + sourceHome: '/x/.kimi', + targetHome: '/y/.kimi-code', + colors: darkColors, + onComplete: () => {}, + }); + c._testShowResult( + makeReport( + {}, + { + config: { + migrated: true, + tuiExtracted: false, + droppedProviders: [], + droppedModels: [], + droppedKeys: [], + configConflicts: [], + wroteSiblingDueToConflict: false, + wroteTuiSibling: false, + migratedHooks: 2, + droppedHooks: 1, + siblingContents: { providers: [], models: [], hooks: 0 }, + }, + }, + ), + ); + const out = c.render(80).join('\n'); + expect(out).toContain('· hooks'); // appears in the ✓ migrated-kinds line + expect(out).toContain('1 hooks dropped'); + }); + + it('Enter on the result screen completes with the prior decision', () => { + let result: MigrationScreenResult | undefined; + const c = new MigrationScreenComponent({ + plan: makePlan(), + sourceHome: '/x/.kimi', + targetHome: '/y/.kimi-code', + colors: darkColors, + onComplete: (r) => { + result = r; + }, + }); + c._testShowResult(makeReport()); + c.handleInput('\r'); + expect(result?.decision).toBe('now'); + expect(result?.migrated).toBe(true); + }); + + it('omits a data class from the result when it was not migrated', () => { + const c = new MigrationScreenComponent({ + plan: makePlan(), + sourceHome: '/x/.kimi', + targetHome: '/y/.kimi-code', + colors: darkColors, + onComplete: () => {}, + }); + // config skipped (e.g. a malformed legacy config.toml). + c._testShowResult( + makeReport( + {}, + { + config: { + migrated: false, + tuiExtracted: false, + droppedProviders: [], + droppedModels: [], + droppedKeys: [], + configConflicts: [], + wroteSiblingDueToConflict: false, + wroteTuiSibling: false, + migratedHooks: 0, + droppedHooks: 0, + siblingContents: { providers: [], models: [], hooks: 0 }, + }, + }, + ), + ); + const out = c.render(80).join('\n'); + // REPL history (copied) is still shown... + expect(out).toContain('REPL history'); + // ...but config must not be claimed as migrated. + expect(out).not.toContain('config'); + }); + + it('surfaces conflict and failure warnings on the result screen', () => { + const c = new MigrationScreenComponent({ + plan: makePlan(), + sourceHome: '/x/.kimi', + targetHome: '/y/.kimi-code', + colors: darkColors, + onComplete: () => {}, + }); + c._testShowResult( + makeReport( + { sessionsFailed: [{ sourcePath: '/s', reason: 'bad' }] }, + { + config: { + migrated: true, + tuiExtracted: false, + droppedProviders: [], + droppedModels: [], + droppedKeys: [], + configConflicts: [], + wroteSiblingDueToConflict: true, + wroteTuiSibling: false, + migratedHooks: 0, + droppedHooks: 0, + siblingContents: { providers: [], models: [], hooks: 0 }, + }, + mcp: { mergedServers: ['m'], keptNewForConflicts: [], droppedServers: [], wroteSiblingDueToConflict: true }, + }, + ), + ); + const out = c.render(80).join('\n'); + expect(out).toContain('config.migrated-from-kimi-cli.toml'); + expect(out).toContain('mcp.migrated-from-kimi-cli.json'); + expect(out).toContain('1 sessions failed'); + }); + + it('lists sibling-file contents in the config-fallback warning so the user knows what to merge', () => { + // When the target's `config.toml` could not be parsed and migration writes + // to `config.migrated-from-kimi-cli.toml` instead, the result screen must + // (a) name the sibling, (b) say what's in it so the user knows what to + // merge by hand, and (c) describe the trigger accurately (parse failure, + // not "unreadable"). Otherwise users have to crack the file open to find + // out — and they may not realize hooks landed in there at all. + const c = new MigrationScreenComponent({ + plan: makePlan(), + sourceHome: '/x/.kimi', + targetHome: '/y/.kimi-code', + colors: darkColors, + onComplete: () => {}, + }); + c._testShowResult( + makeReport( + {}, + { + config: { + migrated: true, + tuiExtracted: false, + droppedProviders: [], + droppedModels: [], + droppedKeys: [], + configConflicts: [], + wroteSiblingDueToConflict: true, + wroteTuiSibling: false, + migratedHooks: 0, + droppedHooks: 0, + siblingContents: { + providers: ['openai', 'managed:kimi-code'], + models: ['gpt4'], + hooks: 3, + }, + }, + }, + ), + ); + const out = c.render(80).join('\n'); + expect(out).toContain('config.migrated-from-kimi-cli.toml'); + // Accurate trigger description (file parses, not "unreadable"). + expect(out).toContain('could not be parsed'); + // Enumeration of what's inside the sibling. + expect(out).toContain('2 providers'); + expect(out).toContain('1 model'); + expect(out).toContain('3 hooks'); + }); + + it('shows skipped empty sessions as a muted line, not a failure', () => { + const c = new MigrationScreenComponent({ + plan: makePlan(), + sourceHome: '/x/.kimi', + targetHome: '/y/.kimi-code', + colors: darkColors, + onComplete: () => {}, + }); + c._testShowResult(makeReport({ sessionsSkippedEmpty: 3 })); + const out = c.render(80).join('\n'); + expect(out).toContain('3 empty sessions skipped'); + // It is informational, not a failure. + expect(out).not.toContain('3 sessions failed'); + }); + + it('lists kept config settings on the result screen when kimi-cli differed', () => { + const c = new MigrationScreenComponent({ + plan: makePlan(), + sourceHome: '/x/.kimi', + targetHome: '/y/.kimi-code', + colors: darkColors, + onComplete: () => {}, + }); + c._testShowResult( + makeReport( + {}, + { + config: { + migrated: true, + tuiExtracted: false, + droppedProviders: [], + droppedModels: [], + droppedKeys: [], + configConflicts: ['default_model', 'providers.kimi'], + wroteSiblingDueToConflict: false, + wroteTuiSibling: false, + migratedHooks: 0, + droppedHooks: 0, + siblingContents: { providers: [], models: [], hooks: 0 }, + }, + }, + ), + ); + const out = c.render(80).join('\n'); + expect(out).toContain('2 config conflicts kept yours'); + expect(out).toContain('default_model · providers.kimi'); + }); + + it('surfaces MCP servers that need re-authentication', () => { + const c = new MigrationScreenComponent({ + plan: makePlan(), + sourceHome: '/x/.kimi', + targetHome: '/y/.kimi-code', + colors: darkColors, + onComplete: () => {}, + }); + c._testShowResult(makeReport({}, {}, { mcpOauthServersRequiringReauth: ['srv-a', 'srv-b'] })); + const out = c.render(80).join('\n'); + expect(out).toContain('2 MCP servers need re-authentication'); + }); +}); + +describe('MigrationScreenComponent — execution wiring', () => { + it('runs migration after the ask phase and lands on the result phase', async () => { + const fakeReport = makeReport(); + let onCompleteResult: MigrationScreenResult | undefined; + const c = new MigrationScreenComponent({ + plan: makePlan(), + sourceHome: '/x/.kimi', + targetHome: '/y/.kimi-code', + colors: darkColors, + onComplete: (r) => { + onCompleteResult = r; + }, + // injected runner for testability — no filesystem access + runMigration: async (_input) => fakeReport, + }); + c.handleInput('\r'); // ask1: Migrate now + c.handleInput('\r'); // ask2: Config only -> begins migration + // migration is async; wait a tick + await new Promise((res) => setTimeout(res, 0)); + expect(c.render(80).join('\n')).toContain('Migration complete'); + c.handleInput('\r'); // dismiss result + expect(onCompleteResult?.decision).toBe('now'); + expect(onCompleteResult?.migrated).toBe(true); + }); + + it('lands on the failure screen when the runner rejects', async () => { + const c = new MigrationScreenComponent({ + plan: makePlan(), + sourceHome: '/x/.kimi', + targetHome: '/y/.kimi-code', + colors: darkColors, + onComplete: () => {}, + runMigration: async () => { + throw new Error('boom'); + }, + }); + c.handleInput('\r'); // ask1: Migrate now + c.handleInput('\r'); // ask2: Config only -> begins migration + await new Promise((res) => setTimeout(res, 0)); + expect(c.render(80).join('\n')).toContain('Migration failed'); + }); +}); diff --git a/apps/kimi-code/test/native/cache-base.test.ts b/apps/kimi-code/test/native/cache-base.test.ts new file mode 100644 index 000000000..896aefabc --- /dev/null +++ b/apps/kimi-code/test/native/cache-base.test.ts @@ -0,0 +1,51 @@ +import { describe, expect, it } from 'vitest'; + +import { getNativeCacheBase } from '#/native/native-assets'; + +describe('getNativeCacheBase precedence', () => { + const baseOptions = { homeDir: '/home/u' }; + + it('uses KIMI_CODE_CACHE_DIR when set (highest precedence)', () => { + const got = getNativeCacheBase({ + ...baseOptions, + env: { KIMI_CODE_CACHE_DIR: '/custom/cache' }, + }); + expect(got).toBe('/custom/cache'); + }); + + it('ignores KIMI_CODE_HOME (no longer affects native cache)', () => { + const got = getNativeCacheBase({ + ...baseOptions, + env: { KIMI_CODE_HOME: '/legacy' }, + platform: 'darwin', + }); + expect(got).toBe('/home/u/Library/Caches/kimi-code'); + }); + + it('uses platform default on macOS when no env set', () => { + const got = getNativeCacheBase({ + ...baseOptions, + env: {}, + platform: 'darwin', + }); + expect(got).toBe('/home/u/Library/Caches/kimi-code'); + }); + + it('uses XDG_CACHE_HOME on Linux when set', () => { + const got = getNativeCacheBase({ + ...baseOptions, + env: { XDG_CACHE_HOME: '/xdg' }, + platform: 'linux', + }); + expect(got).toBe('/xdg/kimi-code'); + }); + + it('uses LOCALAPPDATA on Windows when set', () => { + const got = getNativeCacheBase({ + ...baseOptions, + env: { LOCALAPPDATA: 'C:\\Users\\u\\AppData\\Local' }, + platform: 'win32', + }); + expect(got).toBe('C:\\Users\\u\\AppData\\Local\\kimi-code'); + }); +}); diff --git a/apps/kimi-code/test/native/cache-gc.test.ts b/apps/kimi-code/test/native/cache-gc.test.ts new file mode 100644 index 000000000..4f9203a15 --- /dev/null +++ b/apps/kimi-code/test/native/cache-gc.test.ts @@ -0,0 +1,103 @@ +import { mkdirSync, statSync, utimesSync, writeFileSync, readdirSync, mkdtempSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; + +import { cleanupStaleNativeCache } from '#/native/native-assets'; + +describe('cleanupStaleNativeCache', () => { + let root: string; + + beforeEach(() => { + root = mkdtempSync(join(tmpdir(), 'kimi-cache-gc-')); + }); + + afterEach(() => { + rmSync(root, { recursive: true, force: true }); + }); + + function makeHashDir(version: string, target: string, hash: string, mtimeOffset = 0): string { + const dir = join(root, 'native', version, target, hash); + mkdirSync(dir, { recursive: true }); + writeFileSync(join(dir, 'marker.txt'), hash); + const now = Math.floor(Date.now() / 1000); + utimesSync(dir, now + mtimeOffset, now + mtimeOffset); + return dir; + } + + it('keeps currentRoot + most recently modified sibling, deletes the rest', () => { + const v = '1.0.0'; + const t = 'darwin-arm64'; + const oldest = makeHashDir(v, t, 'aaaa', -300); + const middle = makeHashDir(v, t, 'bbbb', -200); + const recent = makeHashDir(v, t, 'cccc', -100); + const current = makeHashDir(v, t, 'dddd', 0); + + const result = cleanupStaleNativeCache({ + cacheBase: root, + version: v, + target: t, + currentRoot: current, + }); + + expect(result.kept.toSorted()).toEqual([recent, current].toSorted()); + expect(result.removed.toSorted()).toEqual([oldest, middle].toSorted()); + + expect(readdirSync(join(root, 'native', v, t)).toSorted()).toEqual(['cccc', 'dddd']); + }); + + it('no-op when only currentRoot exists', () => { + const v = '1.0.0'; + const t = 'darwin-arm64'; + const current = makeHashDir(v, t, 'dddd'); + + const result = cleanupStaleNativeCache({ + cacheBase: root, + version: v, + target: t, + currentRoot: current, + }); + + expect(result.kept).toEqual([current]); + expect(result.removed).toEqual([]); + }); + + it('no-op when target dir does not exist', () => { + const result = cleanupStaleNativeCache({ + cacheBase: root, + version: '1.0.0', + target: 'darwin-arm64', + currentRoot: join(root, 'native', '1.0.0', 'darwin-arm64', 'absent'), + }); + expect(result.kept).toEqual([]); + expect(result.removed).toEqual([]); + }); + + it('does not touch other targets or versions', () => { + const current = makeHashDir('1.0.0', 'darwin-arm64', 'dddd'); + const otherTarget = makeHashDir('1.0.0', 'linux-x64', 'eeee'); + const otherVersion = makeHashDir('0.9.0', 'darwin-arm64', 'ffff'); + + cleanupStaleNativeCache({ + cacheBase: root, + version: '1.0.0', + target: 'darwin-arm64', + currentRoot: current, + }); + + expect(statSync(otherTarget).isDirectory()).toBe(true); + expect(statSync(otherVersion).isDirectory()).toBe(true); + }); + + it('errors array is empty on clean run', () => { + const current = makeHashDir('1.0.0', 'darwin-arm64', 'dddd'); + const result = cleanupStaleNativeCache({ + cacheBase: root, + version: '1.0.0', + target: 'darwin-arm64', + currentRoot: current, + }); + expect(result.errors).toEqual([]); + }); +}); diff --git a/apps/kimi-code/test/native/native-assets.test.ts b/apps/kimi-code/test/native/native-assets.test.ts new file mode 100644 index 000000000..1946a6ccc --- /dev/null +++ b/apps/kimi-code/test/native/native-assets.test.ts @@ -0,0 +1,128 @@ +import { createHash } from 'node:crypto'; +import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +import { describe, expect, it } from 'vitest'; + +import { + getNativeCacheBase, + getNativePackageRoot, + NATIVE_ASSET_MANIFEST_VERSION, + type NativeAssetManifest, + type NativeAssetSource, +} from '#/native/native-assets'; +import { loadNativePackage } from '#/native/native-require'; + +function sha256(bytes: Buffer | string): string { + return createHash('sha256').update(bytes).digest('hex'); +} + +function fakeManifest(files: Record<string, string>): { + manifest: NativeAssetManifest; + source: NativeAssetSource; +} { + const assetEntries = Object.entries(files).map(([relativePath, content]) => { + const assetKey = `native/test-target/${relativePath}`; + return { + assetKey, + relativePath, + sha256: sha256(content), + }; + }); + const manifest: NativeAssetManifest = { + version: NATIVE_ASSET_MANIFEST_VERSION, + target: 'test-target', + packages: [ + { + name: 'fake-native', + root: 'node_modules/fake-native', + files: assetEntries, + }, + ], + }; + const manifestKey = 'native/test-target/manifest.json'; + const assets = new Map<string, Buffer>([ + [manifestKey, Buffer.from(JSON.stringify(manifest))], + ...Object.entries(files).map(([relativePath, content]) => [ + `native/test-target/${relativePath}`, + Buffer.from(content), + ] as const), + ]); + return { + manifest, + source: { + getAssetKeys: () => [...assets.keys()], + getRawAsset: (assetKey) => { + const asset = assets.get(assetKey); + if (asset === undefined) throw new Error(`missing test asset: ${assetKey}`); + return asset; + }, + }, + }; +} + +describe('native assets', () => { + it('uses KIMI_CODE_CACHE_DIR as the native cache base when present', () => { + expect( + getNativeCacheBase({ + env: { KIMI_CODE_CACHE_DIR: '/tmp/kimi-cache' }, + homeDir: '/home/kimi', + platform: 'linux', + }), + ).toBe('/tmp/kimi-cache'); + }); + + it('extracts package assets and repairs corrupted cache files', () => { + const dir = mkdtempSync(join(tmpdir(), 'kimi-native-assets-')); + try { + const { manifest, source } = fakeManifest({ + 'node_modules/fake-native/package.json': '{"main":"index.js"}', + 'node_modules/fake-native/index.js': "module.exports = { value: 'ok' };\n", + }); + + const packageRoot = getNativePackageRoot('fake-native', { + cacheBase: dir, + manifest, + source, + version: 'test', + }); + expect(packageRoot).toBe(join(dir, 'native', 'test', 'test-target', sha256(JSON.stringify(manifest)), 'node_modules', 'fake-native')); + expect(readFileSync(join(packageRoot ?? '', 'index.js'), 'utf-8')).toContain("value: 'ok'"); + + writeFileSync(join(packageRoot ?? '', 'index.js'), 'broken'); + const repairedRoot = getNativePackageRoot('fake-native', { + cacheBase: dir, + manifest, + source, + version: 'test', + }); + expect(repairedRoot).toBe(packageRoot); + expect(readFileSync(join(repairedRoot ?? '', 'index.js'), 'utf-8')).toContain("value: 'ok'"); + expect(existsSync(join(dir, 'native', 'test', 'test-target'))).toBe(true); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); + + it('loads a package from extracted native assets', () => { + const dir = mkdtempSync(join(tmpdir(), 'kimi-native-require-')); + try { + const { manifest, source } = fakeManifest({ + 'node_modules/fake-native/package.json': '{"main":"index.js"}', + 'node_modules/fake-native/index.js': "module.exports = { value: 'ok' };\n", + }); + + const pkg = loadNativePackage<{ value: string }>('fake-native', { + cacheBase: dir, + manifest, + source, + version: 'test', + }); + + expect(pkg).toEqual({ value: 'ok' }); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); +}); diff --git a/apps/kimi-code/test/scripts/native/exec.test.ts b/apps/kimi-code/test/scripts/native/exec.test.ts new file mode 100644 index 000000000..87f581191 --- /dev/null +++ b/apps/kimi-code/test/scripts/native/exec.test.ts @@ -0,0 +1,46 @@ +import { describe, expect, it } from 'vitest'; + +import { commandForExecFile } from '../../../scripts/native/exec.mjs'; + +describe('commandForExecFile', () => { + it('returns command as-is on non-Windows', () => { + const result = commandForExecFile('postject', ['kimi', 'NODE_SEA_BLOB', './blob'], 'darwin'); + expect(result).toEqual({ command: 'postject', args: ['kimi', 'NODE_SEA_BLOB', './blob'] }); + }); + + it('returns command as-is on Windows for non-batch files', () => { + const result = commandForExecFile('postject.exe', ['kimi.exe'], 'win32'); + expect(result).toEqual({ command: 'postject.exe', args: ['kimi.exe'] }); + }); + + it('wraps .cmd files through cmd.exe on Windows', () => { + const result = commandForExecFile('postject.cmd', ['kimi.exe', 'NODE_SEA_BLOB'], 'win32', { + ComSpec: 'C:\\Windows\\System32\\cmd.exe', + }); + expect(result.command).toBe('C:\\Windows\\System32\\cmd.exe'); + expect(result.args).toEqual([ + '/d', + '/s', + '/c', + '""postject.cmd" "kimi.exe" "NODE_SEA_BLOB""', + ]); + expect(result.options?.windowsVerbatimArguments).toBe(true); + }); + + it('wraps .bat files through cmd.exe on Windows', () => { + const result = commandForExecFile('foo.bat', [], 'win32', { ComSpec: 'cmd.exe' }); + expect(result.command).toBe('cmd.exe'); + }); + + it('escapes embedded double quotes in args', () => { + const result = commandForExecFile('foo.cmd', ['hello "world"'], 'win32', { + ComSpec: 'cmd.exe', + }); + expect(result.args[3]).toBe('""foo.cmd" "hello ""world""""'); + }); + + it('falls back to cmd.exe when ComSpec missing', () => { + const result = commandForExecFile('foo.cmd', [], 'win32', {}); + expect(result.command).toBe('cmd.exe'); + }); +}); diff --git a/apps/kimi-code/test/scripts/native/manifest.test.ts b/apps/kimi-code/test/scripts/native/manifest.test.ts new file mode 100644 index 000000000..3f8b6892f --- /dev/null +++ b/apps/kimi-code/test/scripts/native/manifest.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, it } from 'vitest'; + +import { + NATIVE_ASSET_MANIFEST_VERSION, + buildManifestKey, + isManifestVersionSupported, +} from '../../../scripts/native/manifest.mjs'; + +describe('NATIVE_ASSET_MANIFEST_VERSION', () => { + it('is a positive integer', () => { + expect(Number.isInteger(NATIVE_ASSET_MANIFEST_VERSION)).toBe(true); + expect(NATIVE_ASSET_MANIFEST_VERSION).toBeGreaterThan(0); + }); +}); + +describe('buildManifestKey', () => { + it('namespaces by target', () => { + expect(buildManifestKey('darwin-arm64')).toBe('native/darwin-arm64/manifest.json'); + expect(buildManifestKey('linux-x64')).toBe('native/linux-x64/manifest.json'); + }); +}); + +describe('isManifestVersionSupported', () => { + it('accepts current version', () => { + expect(isManifestVersionSupported(NATIVE_ASSET_MANIFEST_VERSION)).toBe(true); + }); + + it('rejects other versions', () => { + expect(isManifestVersionSupported(NATIVE_ASSET_MANIFEST_VERSION + 1)).toBe(false); + expect(isManifestVersionSupported(0)).toBe(false); + }); +}); diff --git a/apps/kimi-code/test/scripts/native/native-deps.test.ts b/apps/kimi-code/test/scripts/native/native-deps.test.ts new file mode 100644 index 000000000..c96b642e6 --- /dev/null +++ b/apps/kimi-code/test/scripts/native/native-deps.test.ts @@ -0,0 +1,90 @@ +import { describe, expect, it } from 'vitest'; + +import { + nativeDeps, + resolveTargetDeps, + isSupportedTarget, + SUPPORTED_TARGETS, +} from '../../../scripts/native/native-deps.mjs'; + +describe('SUPPORTED_TARGETS', () => { + it('contains the six published targets', () => { + expect([...SUPPORTED_TARGETS].toSorted()).toEqual( + [ + 'darwin-arm64', + 'darwin-x64', + 'linux-arm64', + 'linux-x64', + 'win32-arm64', + 'win32-x64', + ].toSorted(), + ); + }); +}); + +describe('isSupportedTarget', () => { + it('accepts every supported target', () => { + for (const t of SUPPORTED_TARGETS) { + expect(isSupportedTarget(t)).toBe(true); + } + }); + + it('rejects unknown targets', () => { + expect(isSupportedTarget('linux-x64-musl')).toBe(false); + expect(isSupportedTarget('darwin-arm')).toBe(false); + }); +}); + +describe('resolveTargetDeps', () => { + it('returns one descriptor per package for darwin-arm64', () => { + const deps = resolveTargetDeps('darwin-arm64'); + const names = deps.map((d) => d.resolvedName); + expect(names).toContain('@mariozechner/clipboard'); + expect(names).toContain('@mariozechner/clipboard-darwin-arm64'); + expect(names).toContain('koffi'); + }); + + it('picks the right clipboard subpackage per target', () => { + expect( + resolveTargetDeps('linux-x64').map((d) => d.resolvedName), + ).toContain('@mariozechner/clipboard-linux-x64-gnu'); + expect( + resolveTargetDeps('win32-x64').map((d) => d.resolvedName), + ).toContain('@mariozechner/clipboard-win32-x64-msvc'); + expect( + resolveTargetDeps('win32-arm64').map((d) => d.resolvedName), + ).toContain('@mariozechner/clipboard-win32-arm64-msvc'); + }); + + it('encodes koffi native file path with target triplet', () => { + const linuxKoffi = resolveTargetDeps('linux-arm64').find((d) => d.resolvedName === 'koffi'); + expect(linuxKoffi?.nativeFileRelatives).toEqual(['build/koffi/linux_arm64/koffi.node']); + const macKoffi = resolveTargetDeps('darwin-x64').find((d) => d.resolvedName === 'koffi'); + expect(macKoffi?.nativeFileRelatives).toEqual(['build/koffi/darwin_x64/koffi.node']); + const winArmKoffi = resolveTargetDeps('win32-arm64').find((d) => d.resolvedName === 'koffi'); + expect(winArmKoffi?.nativeFileRelatives).toEqual(['build/koffi/win32_arm64/koffi.node']); + }); + + it('throws on unsupported target', () => { + expect(() => resolveTargetDeps('linux-x64-musl')).toThrow(/unsupported/i); + }); +}); + +describe('nativeDeps registry shape', () => { + it('has clipboard host (collect=js-only)', () => { + const host = nativeDeps.find((d) => d.id === 'clipboard-host'); + expect(host?.collect).toBe('js-only'); + }); + + it('has clipboard-target (collect=native-files, parent=clipboard-host)', () => { + const target = nativeDeps.find((d) => d.id === 'clipboard-target'); + expect(target?.collect).toBe('native-files'); + expect(target?.parent).toBe('clipboard-host'); + }); + + it('has koffi (collect=js-and-native-file, parent=pi-tui)', () => { + const koffi = nativeDeps.find((d) => d.id === 'koffi'); + expect(koffi?.collect).toBe('js-and-native-file'); + expect(koffi?.parent).toBe('pi-tui'); + }); +}); diff --git a/apps/kimi-code/test/scripts/native/paths.test.ts b/apps/kimi-code/test/scripts/native/paths.test.ts new file mode 100644 index 000000000..80209f457 --- /dev/null +++ b/apps/kimi-code/test/scripts/native/paths.test.ts @@ -0,0 +1,101 @@ +import { describe, expect, it } from 'vitest'; + +import { + appRoot, + executableName, + nativeIntermediatesDir, + nativeBinDir, + nativeBinPath, + nativeBlobPath, + nativeJsBundlePath, + nativeManifestKey, + nativeSeaConfigPath, + targetTriple, + nativeDistRoot, + nativeManifestDir, + nativeArtifactsDir, + nativeSmokeHome, + SEA_SENTINEL_FUSE, +} from '../../../scripts/native/paths.mjs'; + +describe('targetTriple', () => { + it('returns platform-arch when env unset', () => { + expect(targetTriple({ platform: 'darwin', arch: 'arm64', env: {} })).toBe('darwin-arm64'); + expect(targetTriple({ platform: 'linux', arch: 'x64', env: {} })).toBe('linux-x64'); + expect(targetTriple({ platform: 'win32', arch: 'x64', env: {} })).toBe('win32-x64'); + }); + + it('honors KIMI_CODE_BUILD_TARGET override', () => { + expect( + targetTriple({ + platform: 'darwin', + arch: 'arm64', + env: { KIMI_CODE_BUILD_TARGET: 'linux-arm64' }, + }), + ).toBe('linux-arm64'); + }); +}); + +describe('executableName', () => { + it('returns kimi.exe on win32', () => { + expect(executableName('win32')).toBe('kimi.exe'); + }); + + it('returns kimi on other platforms', () => { + expect(executableName('darwin')).toBe('kimi'); + expect(executableName('linux')).toBe('kimi'); + }); +}); + +describe('path helpers', () => { + it('returns absolute intermediates dir under app root', () => { + expect(nativeIntermediatesDir()).toBe(`${appRoot}/dist-native/intermediates`); + }); + + it('returns absolute bin dir per target', () => { + expect(nativeBinDir('darwin-arm64')).toBe(`${appRoot}/dist-native/bin/darwin-arm64`); + }); + + it('returns absolute bin path with executable name', () => { + expect(nativeBinPath('darwin-arm64', 'darwin')).toBe( + `${appRoot}/dist-native/bin/darwin-arm64/kimi`, + ); + expect(nativeBinPath('win32-x64', 'win32')).toBe( + `${appRoot}/dist-native/bin/win32-x64/kimi.exe`, + ); + }); + + it('returns intermediate artifact paths', () => { + expect(nativeJsBundlePath()).toBe(`${appRoot}/dist-native/intermediates/main.cjs`); + expect(nativeBlobPath()).toBe(`${appRoot}/dist-native/intermediates/kimi.blob`); + expect(nativeSeaConfigPath()).toBe( + `${appRoot}/dist-native/intermediates/sea-config.json`, + ); + }); + + it('returns manifest key for target', () => { + expect(nativeManifestKey('darwin-arm64')).toBe('native/darwin-arm64/manifest.json'); + }); + + it('returns native dist root', () => { + expect(nativeDistRoot()).toBe(`${appRoot}/dist-native`); + }); + + it('returns manifest dir for target', () => { + expect(nativeManifestDir('darwin-arm64')).toBe( + `${appRoot}/dist-native/intermediates/native-assets/darwin-arm64`, + ); + }); + + it('returns artifacts dir', () => { + expect(nativeArtifactsDir()).toBe(`${appRoot}/dist-native/artifacts`); + }); + + it('returns smoke home', () => { + expect(nativeSmokeHome()).toBe(`${appRoot}/dist-native/smoke-home`); + }); + + it('has correct SEA sentinel fuse value', () => { + expect(SEA_SENTINEL_FUSE).toBe('NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2'); + }); +}); diff --git a/apps/kimi-code/test/scripts/native/release-artifacts.test.ts b/apps/kimi-code/test/scripts/native/release-artifacts.test.ts new file mode 100644 index 000000000..791fd14e0 --- /dev/null +++ b/apps/kimi-code/test/scripts/native/release-artifacts.test.ts @@ -0,0 +1,150 @@ +import { createHash } from 'node:crypto'; +import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { mkdtemp, readFile, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join, resolve } from 'node:path'; +import { execFile } from 'node:child_process'; +import { promisify } from 'node:util'; +import { inflateRawSync } from 'node:zlib'; + +import { afterEach, describe, expect, it } from 'vitest'; + +import { appRoot } from '../../../scripts/native/paths.mjs'; + +const execFileAsync = promisify(execFile); +const packageScript = resolve(appRoot, 'scripts/native/package.mjs'); +const manifestScript = resolve(appRoot, 'scripts/native/produce-manifest.mjs'); +const artifactsDir = resolve(appRoot, 'dist-native/artifacts'); +const target = 'test-zip-artifact'; +const executableName = process.platform === 'win32' ? 'kimi.exe' : 'kimi'; +const fakeBinary = resolve(appRoot, 'dist-native/bin', target, executableName); + +function sha256(bytes: Buffer | string): string { + return createHash('sha256').update(bytes).digest('hex'); +} + +function zipEntryNames(zipPath: string): readonly string[] { + const zip = readFileSync(zipPath); + const eocdOffset = findEndOfCentralDirectory(zip); + const entryCount = zip.readUInt16LE(eocdOffset + 10); + let offset = zip.readUInt32LE(eocdOffset + 16); + const names: string[] = []; + + for (let i = 0; i < entryCount; i += 1) { + expect(zip.readUInt32LE(offset)).toBe(0x02014b50); + const nameLength = zip.readUInt16LE(offset + 28); + const extraLength = zip.readUInt16LE(offset + 30); + const commentLength = zip.readUInt16LE(offset + 32); + names.push(zip.subarray(offset + 46, offset + 46 + nameLength).toString('utf-8')); + offset += 46 + nameLength + extraLength + commentLength; + } + + return names; +} + +function readZipEntry(zipPath: string, expectedName: string): Buffer { + const zip = readFileSync(zipPath); + const eocdOffset = findEndOfCentralDirectory(zip); + const entryCount = zip.readUInt16LE(eocdOffset + 10); + let offset = zip.readUInt32LE(eocdOffset + 16); + + for (let i = 0; i < entryCount; i += 1) { + expect(zip.readUInt32LE(offset)).toBe(0x02014b50); + const method = zip.readUInt16LE(offset + 10); + const compressedSize = zip.readUInt32LE(offset + 20); + const nameLength = zip.readUInt16LE(offset + 28); + const extraLength = zip.readUInt16LE(offset + 30); + const commentLength = zip.readUInt16LE(offset + 32); + const localHeaderOffset = zip.readUInt32LE(offset + 42); + const name = zip.subarray(offset + 46, offset + 46 + nameLength).toString('utf-8'); + if (name === expectedName) { + return readLocalEntry(zip, localHeaderOffset, method, compressedSize); + } + offset += 46 + nameLength + extraLength + commentLength; + } + + throw new Error(`zip entry not found: ${expectedName}`); +} + +function readLocalEntry( + zip: Buffer, + localHeaderOffset: number, + method: number, + compressedSize: number, +): Buffer { + expect(zip.readUInt32LE(localHeaderOffset)).toBe(0x04034b50); + const nameLength = zip.readUInt16LE(localHeaderOffset + 26); + const extraLength = zip.readUInt16LE(localHeaderOffset + 28); + const dataStart = localHeaderOffset + 30 + nameLength + extraLength; + const compressed = zip.subarray(dataStart, dataStart + compressedSize); + if (method === 0) return compressed; + if (method === 8) return inflateRawSync(compressed); + throw new Error(`unsupported zip compression method: ${String(method)}`); +} + +function findEndOfCentralDirectory(zip: Buffer): number { + for (let offset = zip.length - 22; offset >= 0; offset -= 1) { + if (zip.readUInt32LE(offset) === 0x06054b50) return offset; + } + throw new Error('end of central directory not found'); +} + +describe('native release artifacts', () => { + afterEach(() => { + rmSync(resolve(appRoot, 'dist-native/bin', target), { recursive: true, force: true }); + rmSync(resolve(artifactsDir, `kimi-code-${target}.zip`), { force: true }); + rmSync(resolve(artifactsDir, `kimi-code-${target}.zip.sha256`), { force: true }); + }); + + it('packages the native binary as a zip archive and checksums the archive', async () => { + const binaryContent = 'native binary payload\n'; + mkdirSync(resolve(appRoot, 'dist-native/bin', target), { recursive: true }); + writeFileSync(fakeBinary, binaryContent, { mode: 0o755 }); + + await execFileAsync(process.execPath, [packageScript], { + cwd: appRoot, + env: { ...process.env, KIMI_CODE_BUILD_TARGET: target }, + }); + + const archivePath = resolve(artifactsDir, `kimi-code-${target}.zip`); + const checksumPath = `${archivePath}.sha256`; + expect(existsSync(archivePath)).toBe(true); + expect(existsSync(checksumPath)).toBe(true); + expect(zipEntryNames(archivePath)).toEqual([executableName]); + expect(readZipEntry(archivePath, executableName).toString('utf-8')).toBe(binaryContent); + expect(readFileSync(checksumPath, 'utf-8')).toBe( + `${sha256(readFileSync(archivePath))} kimi-code-${target}.zip\n`, + ); + }); + + it('produces a manifest from zip archive checksums', async () => { + const releaseDir = await mkdtemp(join(tmpdir(), 'kimi-manifest-zip-')); + const archiveBytes = Buffer.from('fake zip bytes'); + const checksum = sha256(archiveBytes); + await writeFile(join(releaseDir, 'kimi-code-darwin-arm64.zip'), archiveBytes); + await writeFile( + join(releaseDir, 'kimi-code-darwin-arm64.zip.sha256'), + `${checksum} kimi-code-darwin-arm64.zip\n`, + ); + + await execFileAsync(process.execPath, [manifestScript, releaseDir, '@moonshot-ai/kimi-code@0.5.0']); + + const manifest = JSON.parse( + await readFile(join(releaseDir, 'manifest.json'), 'utf-8'), + ) as { + version: string; + tag: string; + platforms: Record<string, { filename: string; checksum: string }>; + }; + expect(manifest).toEqual({ + version: '0.5.0', + tag: '@moonshot-ai/kimi-code@0.5.0', + platforms: { + 'darwin-arm64': { + filename: 'kimi-code-darwin-arm64.zip', + checksum, + }, + }, + }); + }); +}); diff --git a/apps/kimi-code/test/scripts/native/sign-args.test.ts b/apps/kimi-code/test/scripts/native/sign-args.test.ts new file mode 100644 index 000000000..803f1820a --- /dev/null +++ b/apps/kimi-code/test/scripts/native/sign-args.test.ts @@ -0,0 +1,48 @@ +import { describe, expect, it } from 'vitest'; + +import { buildCodesignArgs } from '../../../scripts/native/04-sign.mjs'; + +describe('buildCodesignArgs', () => { + it('returns ad-hoc args for identity "-"', () => { + const args = buildCodesignArgs({ + identity: '-', + executable: '/path/kimi', + entitlementsPath: '/path/entitlements.plist', + keychainPath: null, + }); + expect(args).toEqual(['--sign', '-', '/path/kimi']); + }); + + it('returns hardened-runtime args for Developer ID identity', () => { + const args = buildCodesignArgs({ + identity: 'Developer ID Application: Moonshot AI (ABCD1234)', + executable: '/path/kimi', + entitlementsPath: '/path/entitlements.plist', + keychainPath: '/tmp/sign.keychain-db', + }); + expect(args).toEqual([ + '--sign', + 'Developer ID Application: Moonshot AI (ABCD1234)', + '--options', + 'runtime', + '--entitlements', + '/path/entitlements.plist', + '--timestamp', + '--keychain', + '/tmp/sign.keychain-db', + '--force', + '/path/kimi', + ]); + }); + + it('omits --keychain when keychainPath is null but uses Developer ID otherwise', () => { + const args = buildCodesignArgs({ + identity: 'Developer ID Application: Moonshot AI (ABCD1234)', + executable: '/path/kimi', + entitlementsPath: '/path/entitlements.plist', + keychainPath: null, + }); + expect(args).toContain('--entitlements'); + expect(args).not.toContain('--keychain'); + }); +}); diff --git a/apps/kimi-code/test/scripts/nix/update-pnpm-deps.test.ts b/apps/kimi-code/test/scripts/nix/update-pnpm-deps.test.ts new file mode 100644 index 000000000..87aaf27ec --- /dev/null +++ b/apps/kimi-code/test/scripts/nix/update-pnpm-deps.test.ts @@ -0,0 +1,152 @@ +import { execFile } from 'node:child_process'; +import { chmod, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join, resolve } from 'node:path'; +import { promisify } from 'node:util'; + +import { afterEach, describe, expect, it } from 'vitest'; + +const execFileAsync = promisify(execFile); + +const repoRoot = resolve(import.meta.dirname, '../../../../..'); +const scriptPath = join(repoRoot, 'build/nix/update-pnpm-deps.sh'); + +const currentHash = 'sha256-LZ9Bkm3pG2ib7NcdqcP/kmoYWsNjXQ8PoEIlg/94oVo='; +const fakeHash = 'sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA='; +const newHash = 'sha256-NNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNN='; + +const tempRoots: string[] = []; + +describe('update-pnpm-deps.sh', () => { + afterEach(async () => { + await Promise.all(tempRoots.splice(0).map((dir) => rm(dir, { recursive: true, force: true }))); + }); + + it('exits without patching fakeHash when the current pnpmDeps hash still builds', async () => { + const fixture = await createFixtureRepo(currentHash); + const result = await runUpdateScript(fixture, { currentValid: true }); + + await expect(readFile(join(fixture.root, 'flake.nix'), 'utf-8')).resolves.toContain(currentHash); + await expect(readFile(join(fixture.root, 'flake.nix'), 'utf-8')).resolves.not.toContain(fakeHash); + await expect(readFile(fixture.nixLogPath, 'utf-8')).resolves.toBe('current\n'); + expect(result.stdout).toContain('pnpmDeps hash still valid'); + + const cache = JSON.parse( + await readFile(join(fixture.root, '.git/kimi-code/pnpm-deps-hashes-v1.json'), 'utf-8'), + ) as Record<string, { hash: string }>; + expect(Object.values(cache)).toContainEqual(expect.objectContaining({ hash: currentHash })); + }); + + it('writes a local cache after discovery and verifies the cached hash on the next run', async () => { + const fixture = await createFixtureRepo(currentHash); + + await runUpdateScript(fixture, { currentValid: false }); + await expect(readFile(join(fixture.root, 'flake.nix'), 'utf-8')).resolves.toContain(newHash); + + const cache = JSON.parse( + await readFile(join(fixture.root, '.git/kimi-code/pnpm-deps-hashes-v1.json'), 'utf-8'), + ) as Record<string, { hash: string }>; + expect(Object.values(cache)).toContainEqual(expect.objectContaining({ hash: newHash })); + + await writeFile(join(fixture.root, 'flake.nix'), flakeWithHash(currentHash)); + await writeFile(fixture.nixLogPath, ''); + + const result = await runUpdateScript(fixture, { currentValid: false }); + + await expect(readFile(join(fixture.root, 'flake.nix'), 'utf-8')).resolves.toContain(newHash); + await expect(readFile(fixture.nixLogPath, 'utf-8')).resolves.toBe('current\nnew\n'); + expect(result.stdout).toContain('cache hit'); + }); +}); + +async function createFixtureRepo(hash: string): Promise<{ binDir: string; nixLogPath: string; root: string }> { + const root = await mkdtemp(join(tmpdir(), 'kimi-update-pnpm-deps-')); + tempRoots.push(root); + + await writeFile(join(root, 'flake.nix'), flakeWithHash(hash)); + await writeFile(join(root, 'flake.lock'), '{}\n'); + await writeFile(join(root, '.npmrc'), 'engine-strict=true\n'); + await writeFile(join(root, 'package.json'), '{"name":"root"}\n'); + await writeFile(join(root, 'pnpm-lock.yaml'), 'lockfileVersion: 9.0\n'); + await writeFile(join(root, 'pnpm-workspace.yaml'), 'packages:\n - "apps/*"\n'); + await mkdir(join(root, 'apps/example'), { recursive: true }); + await writeFile(join(root, 'apps/example/package.json'), '{"name":"example"}\n'); + + await execFileAsync('git', ['init'], { cwd: root }); + + const binDir = join(root, 'bin'); + const nixLogPath = join(root, 'nix.log'); + await mkdir(binDir); + await writeFile(nixLogPath, ''); + await writeFile( + join(binDir, 'nix'), + `#!/usr/bin/env bash +set -euo pipefail + +state=unknown +if grep -Fq "$CURRENT_HASH" flake.nix; then + state=current +elif grep -Fq "$FAKE_HASH" flake.nix; then + state=fake +elif grep -Fq "$NEW_HASH" flake.nix; then + state=new +fi + +printf '%s\\n' "$state" >> "$NIX_LOG" + +case "$state" in + current) + if [ "\${CURRENT_VALID:-0}" = "1" ]; then + exit 0 + fi + echo "current hash invalid" >&2 + exit 1 + ;; + fake) + echo "error: hash mismatch" >&2 + echo " got: $NEW_HASH" >&2 + exit 1 + ;; + new) + exit 0 + ;; + *) + echo "unexpected hash state: $state" >&2 + exit 1 + ;; +esac +`, + ); + await chmod(join(binDir, 'nix'), 0o755); + + return { binDir, nixLogPath, root }; +} + +async function runUpdateScript( + fixture: { binDir: string; nixLogPath: string; root: string }, + options: { currentValid: boolean }, +): Promise<{ stdout: string; stderr: string }> { + return execFileAsync('bash', [scriptPath], { + cwd: fixture.root, + env: { + ...process.env, + CURRENT_HASH: currentHash, + CURRENT_VALID: options.currentValid ? '1' : '0', + FAKE_HASH: fakeHash, + NEW_HASH: newHash, + NIX_LOG: fixture.nixLogPath, + PATH: `${fixture.binDir}:${process.env['PATH'] ?? ''}`, + }, + }); +} + +function flakeWithHash(hash: string): string { + return `{ + outputs = { self, nixpkgs }: { + packages.x86_64-linux.kimi-code-pnpm-deps = { + hash = "${hash}"; + }; + }; +} +`; +} diff --git a/apps/kimi-code/test/tui/actions/replay-hydrate-agent-group.test.ts b/apps/kimi-code/test/tui/actions/replay-hydrate-agent-group.test.ts new file mode 100644 index 000000000..31197ace8 --- /dev/null +++ b/apps/kimi-code/test/tui/actions/replay-hydrate-agent-group.test.ts @@ -0,0 +1,352 @@ + +import { Text } from '@earendil-works/pi-tui'; +import type { Session } from '@moonshot-ai/kimi-code-sdk'; +import { describe, expect, it, vi } from 'vitest'; + +import { hydrateProjectedEntries, hydrateTranscriptFromReplay } from '#/tui/actions/replay-ops'; +import { AgentGroupComponent } from '#/tui/components/messages/agent-group'; +import { ToolCallComponent } from '#/tui/components/messages/tool-call'; +import { createTUIState, type KimiTUIOptions, type TUIState } from '#/tui/kimi-tui'; +import type { AppState, ToolCallBlockData, TranscriptEntry } from '#/tui/types'; + +function makeAppState(): AppState { + return { + model: 'k2', + workDir: '/tmp/proj-a', + sessionId: 'sess-1', + yolo: false, + permissionMode: 'manual', + planMode: false, + thinking: false, + contextUsage: 0, + contextTokens: 0, + maxContextTokens: 100, + isStreaming: false, + isCompacting: false, + isReplaying: false, + streamingPhase: 'idle', + streamingStartTime: 0, + theme: 'dark', + version: '0.0.0-test', + editorCommand: null, + notifications: { enabled: true, condition: 'unfocused' }, + availableModels: {}, + availableProviders: {}, + sessionTitle: null, + }; +} + +function makeTuiState(): TUIState { + const options: KimiTUIOptions = { + initialAppState: makeAppState(), + startup: { + continueLast: false, + yolo: false, + plan: false, + }, + resolvedTheme: 'dark', + }; + const state = createTUIState(options); + vi.spyOn(state.ui, 'requestRender').mockImplementation(() => {}); + return state; +} + +function appendEntry(state: TUIState, entry: TranscriptEntry): void { + state.transcriptEntries.push(entry); + if (entry.toolCallData !== undefined) { + const tc = new ToolCallComponent( + entry.toolCallData, + entry.toolCallData.result, + state.theme.colors, + state.ui, + state.theme.markdownTheme, + ); + state.pendingToolComponents.set(entry.toolCallData.id, tc); + state.transcriptContainer.addChild(tc); + return; + } + state.transcriptContainer.addChild(new Text(entry.content, 0, 0)); +} + +function hydrate(state: TUIState, entries: readonly TranscriptEntry[]): void { + hydrateProjectedEntries(state, entries, (entry) => { + appendEntry(state, entry); + }); +} + +function setTodoList( + state: TUIState, + todos: Parameters<TUIState['todoPanel']['setTodos']>[0], +): void { + state.todoPanel.setTodos(todos); + state.todoPanelContainer.clear(); + if (!state.todoPanel.isEmpty()) { + state.todoPanelContainer.addChild(state.todoPanel); + } +} + +function sessionWithToolStore(toolStore: Record<string, unknown>): Session { + return { + getResumeState: () => ({ + sessionMetadata: {}, + agents: { + main: { + type: 'main', + config: { + modelAlias: 'k2', + provider: undefined, + modelCapabilities: { max_context_tokens: 100 }, + }, + context: { history: [], tokenCount: 0 }, + replay: [], + permission: { mode: 'manual' }, + plan: null, + usage: {}, + tools: [], + toolStore, + background: [], + }, + }, + }), + } as unknown as Session; +} + +let entryIdSeq = 0; +function makeAgentEntry( + id: string, + step: number, + turnId: string, + result?: { is_error?: boolean }, +): TranscriptEntry { + entryIdSeq += 1; + const tc: ToolCallBlockData = { + id, + name: 'Agent', + args: { description: id }, + step, + turnId, + ...(result !== undefined + ? { result: { tool_call_id: id, output: 'done', is_error: result.is_error ?? false } } + : {}), + }; + return { + id: `e${String(entryIdSeq)}`, + kind: 'tool_call', + turnId, + renderMode: 'plain', + content: '', + toolCallData: tc, + }; +} + +function makeBashEntry(id: string, step: number, turnId: string): TranscriptEntry { + entryIdSeq += 1; + const tc: ToolCallBlockData = { + id, + name: 'Bash', + args: { command: 'pwd' }, + step, + turnId, + }; + return { + id: `e${String(entryIdSeq)}`, + kind: 'tool_call', + turnId, + renderMode: 'plain', + content: '', + toolCallData: tc, + }; +} + +function makeUserEntry(turnId: string, content: string): TranscriptEntry { + entryIdSeq += 1; + return { + id: `e${String(entryIdSeq)}`, + kind: 'user', + turnId, + renderMode: 'plain', + content, + }; +} + +describe('hydrateProjectedEntries', () => { + it('hydrates the visible todo panel from resumed tool store state', async () => { + const state = makeTuiState(); + const errors: string[] = []; + + const ok = await hydrateTranscriptFromReplay( + state, + { + setAppState: (patch) => { + state.appState = { ...state.appState, ...patch }; + }, + appendEntry: (entry) => { + appendEntry(state, entry); + }, + setTodoList: (todos) => { + setTodoList(state, todos); + }, + emitError: (message) => { + errors.push(message); + }, + }, + sessionWithToolStore({ + todo: [ + { title: 'Review resume snapshot', status: 'done' }, + { title: 'Render todo panel', status: 'in_progress' }, + { title: '', status: 'pending' }, + ], + }), + ); + + expect(ok).toBe(true); + expect(errors).toEqual([]); + expect(state.todoPanel.getTodos()).toEqual([ + { title: 'Review resume snapshot', status: 'done' }, + { title: 'Render todo panel', status: 'in_progress' }, + ]); + expect(state.todoPanelContainer.children).toContain(state.todoPanel); + }); + + it('clears the todo panel when resumed state has no todo store entry', async () => { + const state = makeTuiState(); + setTodoList(state, [{ title: 'stale todo', status: 'pending' }]); + + const ok = await hydrateTranscriptFromReplay( + state, + { + setAppState: (patch) => { + state.appState = { ...state.appState, ...patch }; + }, + appendEntry: (entry) => { + appendEntry(state, entry); + }, + setTodoList: (todos) => { + setTodoList(state, todos); + }, + emitError: () => {}, + }, + sessionWithToolStore({}), + ); + + expect(ok).toBe(true); + expect(state.todoPanel.getTodos()).toEqual([]); + expect(state.todoPanelContainer.children).not.toContain(state.todoPanel); + }); + + it('groups 2 adjacent same-step Agents into a single AgentGroupComponent', () => { + const state = makeTuiState(); + const entries: TranscriptEntry[] = [ + makeAgentEntry('a1', 1, 't1', { is_error: false }), + makeAgentEntry('a2', 1, 't1', { is_error: false }), + ]; + + hydrate(state, entries); + + const children = state.transcriptContainer.children; + expect(children.length).toBe(1); + expect(children[0]).toBeInstanceOf(AgentGroupComponent); + expect((children[0] as AgentGroupComponent).size()).toBe(2); + // ToolCallComponent still registers in pendingToolComponents because + // later wire event routing depends on this mapping. + expect(state.pendingToolComponents.has('a1')).toBe(true); + expect(state.pendingToolComponents.has('a2')).toBe(true); + }); + + it('keeps cross-step Agents independent', () => { + const state = makeTuiState(); + const entries: TranscriptEntry[] = [ + makeAgentEntry('a1', 1, 't1'), + makeAgentEntry('a2', 2, 't1'), + ]; + + hydrate(state, entries); + const children = state.transcriptContainer.children; + expect(children.length).toBe(2); + expect(children[0]).toBeInstanceOf(ToolCallComponent); + expect(children[1]).toBeInstanceOf(ToolCallComponent); + }); + + it('does not group when a non-Agent tool sits between Agents', () => { + const state = makeTuiState(); + const entries: TranscriptEntry[] = [ + makeAgentEntry('a1', 1, 't1'), + makeBashEntry('b1', 1, 't1'), + makeAgentEntry('a2', 1, 't1'), + ]; + + hydrate(state, entries); + const children = state.transcriptContainer.children; + expect(children.length).toBe(3); + for (const child of children) expect(child).toBeInstanceOf(ToolCallComponent); + }); + + it('a single Agent is left as a standalone ToolCallComponent', () => { + const state = makeTuiState(); + hydrate(state, [makeAgentEntry('a1', 1, 't1')]); + const children = state.transcriptContainer.children; + expect(children.length).toBe(1); + expect(children[0]).toBeInstanceOf(ToolCallComponent); + }); + + it('user message between Agents prevents grouping', () => { + const state = makeTuiState(); + const entries: TranscriptEntry[] = [ + makeAgentEntry('a1', 1, 't1'), + makeUserEntry('t1', 'hello'), + makeAgentEntry('a2', 1, 't1'), + ]; + + hydrate(state, entries); + const children = state.transcriptContainer.children; + expect(children.length).toBe(3); + expect(children[0]).toBeInstanceOf(ToolCallComponent); + expect(children[2]).toBeInstanceOf(ToolCallComponent); + }); + + it('replay does not write pendingAgentGroup (it stays null after hydration)', () => { + const state = makeTuiState(); + hydrate(state, [makeAgentEntry('a1', 1, 't1'), makeAgentEntry('a2', 1, 't1')]); + expect(state.pendingAgentGroup).toBeNull(); + }); + + it('background Agent replay hydrates only background status rows, not AgentGroupComponent', () => { + const state = makeTuiState(); + hydrate(state, [ + { + id: 'e-background-started', + kind: 'status', + renderMode: 'plain', + content: 'explore agent started in background', + }, + { + id: 'e-background-completed', + kind: 'status', + renderMode: 'plain', + content: 'explore agent completed in background', + }, + ]); + + expect( + state.transcriptContainer.children.some((child) => child instanceof AgentGroupComponent), + ).toBe(false); + expect( + state.transcriptContainer.children.some((child) => child instanceof ToolCallComponent), + ).toBe(false); + }); + + it('group with mixed completed/failed children renders correct phase tails', () => { + const state = makeTuiState(); + hydrate(state, [ + makeAgentEntry('a1', 1, 't1', { is_error: false }), + makeAgentEntry('a2', 1, 't1', { is_error: true }), + ]); + const group = state.transcriptContainer.children[0] as AgentGroupComponent; + const out = group + .render(120) + .join('\n') + .replaceAll(/\[[0-9;]*m/g, ''); + expect(out).toContain('✓ Completed'); + expect(out).toContain('✗ Failed'); + }); +}); diff --git a/apps/kimi-code/test/tui/activity-pane.test.ts b/apps/kimi-code/test/tui/activity-pane.test.ts new file mode 100644 index 000000000..d0eef5d08 --- /dev/null +++ b/apps/kimi-code/test/tui/activity-pane.test.ts @@ -0,0 +1,112 @@ +import { describe, expect, it, vi } from 'vitest'; + +import { KimiTUI, type KimiTUIStartupInput, type TUIState } from '#/tui/kimi-tui'; + +interface ActivityDriver { + state: TUIState; + updateActivityPane(): void; +} + +function makeStartupInput(): KimiTUIStartupInput { + return { + cliOptions: { + session: undefined, + continue: false, + yolo: false, + plan: false, + model: undefined, + outputFormat: undefined, + prompt: undefined, + skillsDirs: [], + }, + tuiConfig: { + theme: 'dark', + editorCommand: null, + notifications: { enabled: true, condition: 'unfocused' }, + }, + version: '0.0.0-test', + workDir: '/tmp/proj-a', + resolvedTheme: 'dark', + }; +} + +function makeDriverWithTerminalProgress(): { + driver: ActivityDriver; + state: TUIState; + setProgress: ReturnType<typeof vi.fn<(active: boolean) => void>>; +} { + const setProgress = vi.fn<(active: boolean) => void>(); + const driver = new KimiTUI({} as never, makeStartupInput()) as unknown as ActivityDriver; + vi.spyOn(driver.state.ui, 'requestRender').mockImplementation(() => {}); + driver.state.terminal = { columns: 80, setProgress } as unknown as TUIState['terminal']; + return { driver, state: driver.state, setProgress }; +} + +describe('updateActivityPane terminal progress', () => { + it('toggles terminal progress when the activity pane enters and leaves work mode', () => { + vi.useFakeTimers(); + try { + const { driver, state, setProgress } = makeDriverWithTerminalProgress(); + + state.livePane = { ...state.livePane, mode: 'waiting' }; + driver.updateActivityPane(); + + expect(setProgress).toHaveBeenCalledTimes(1); + expect(setProgress).toHaveBeenLastCalledWith(true); + expect(state.terminalState.progressActive).toBe(true); + + state.livePane = { ...state.livePane, mode: 'idle' }; + driver.updateActivityPane(); + + expect(setProgress).toHaveBeenCalledTimes(2); + expect(setProgress).toHaveBeenLastCalledWith(false); + expect(state.terminalState.progressActive).toBe(false); + } finally { + vi.useRealTimers(); + } + }); + + it('keeps compaction visible as terminal progress even though the pane is hidden', () => { + const { driver, state, setProgress } = makeDriverWithTerminalProgress(); + state.appState.isCompacting = true; + state.appState.streamingPhase = 'waiting'; + + driver.updateActivityPane(); + driver.updateActivityPane(); + + expect(setProgress).toHaveBeenCalledTimes(1); + expect(setProgress).toHaveBeenLastCalledWith(true); + + state.appState.isCompacting = false; + state.appState.streamingPhase = 'idle'; + driver.updateActivityPane(); + + expect(setProgress).toHaveBeenCalledTimes(2); + expect(setProgress).toHaveBeenLastCalledWith(false); + }); + + it('keeps terminal progress active without showing a thinking spinner', () => { + vi.useFakeTimers(); + try { + const { driver, state, setProgress } = makeDriverWithTerminalProgress(); + state.livePane = { ...state.livePane, mode: 'idle' }; + state.appState.streamingPhase = 'thinking'; + + driver.updateActivityPane(); + + expect(setProgress).toHaveBeenCalledTimes(1); + expect(setProgress).toHaveBeenLastCalledWith(true); + expect(state.activitySpinner).toBeUndefined(); + expect(state.activityContainer.children).toHaveLength(0); + + state.appState.streamingPhase = 'idle'; + driver.updateActivityPane(); + + expect(setProgress).toHaveBeenCalledTimes(2); + expect(setProgress).toHaveBeenLastCalledWith(false); + expect(state.activitySpinner).toBeUndefined(); + } finally { + vi.useRealTimers(); + } + }); +}); diff --git a/apps/kimi-code/test/tui/background-task-status.test.ts b/apps/kimi-code/test/tui/background-task-status.test.ts new file mode 100644 index 000000000..e8033d45f --- /dev/null +++ b/apps/kimi-code/test/tui/background-task-status.test.ts @@ -0,0 +1,104 @@ +import type { BackgroundTaskInfo, BackgroundTaskStatus } from '@moonshot-ai/kimi-code-sdk'; +import { describe, expect, it } from 'vitest'; + +import { formatBackgroundTaskTranscript } from '@/tui/utils/background-task-status'; + +function task(overrides: Partial<BackgroundTaskInfo> = {}): BackgroundTaskInfo { + return { + taskId: 'bash-abcd1234', + command: 'npm run dev', + description: 'dev server', + status: 'running', + pid: 1234, + exitCode: null, + startedAt: Date.now() - 1000, + endedAt: null, + ...overrides, + }; +} + +describe('formatBackgroundTaskTranscript', () => { + it('renders a bash started entry', () => { + const data = formatBackgroundTaskTranscript(task({ status: 'running' })); + expect(data.phase).toBe('started'); + expect(data.headline).toContain('bash task started'); + expect(data.detail).toBe('dev server'); + }); + + it('renders an agent started entry', () => { + const data = formatBackgroundTaskTranscript( + task({ taskId: 'agent-deadbeef', status: 'running' }), + ); + expect(data.headline).toContain('agent task started'); + }); + + it('renders a completed entry with exit code in detail', () => { + const data = formatBackgroundTaskTranscript( + task({ status: 'completed', exitCode: 0, endedAt: Date.now() }), + ); + expect(data.phase).toBe('completed'); + expect(data.headline).toContain('completed'); + expect(data.detail).toContain('exit 0'); + }); + + it('renders a failed entry with non-zero exit', () => { + const data = formatBackgroundTaskTranscript( + task({ status: 'failed', exitCode: 2, endedAt: Date.now() }), + ); + expect(data.phase).toBe('failed'); + expect(data.headline).toContain('failed'); + expect(data.detail).toContain('exit 2'); + }); + + it('renders a killed entry with stop reason', () => { + const data = formatBackgroundTaskTranscript( + task({ status: 'killed', stopReason: 'user', endedAt: Date.now() }), + ); + expect(data.phase).toBe('failed'); + expect(data.headline).toContain('stopped'); + expect(data.detail).toContain('user'); + }); + + it('renders a lost entry with restart note', () => { + const data = formatBackgroundTaskTranscript(task({ status: 'lost', endedAt: Date.now() })); + expect(data.phase).toBe('failed'); + expect(data.headline).toContain('lost'); + expect(data.detail).toContain('session restarted'); + }); + + it('surfaces awaiting_approval reason', () => { + const data = formatBackgroundTaskTranscript( + task({ status: 'awaiting_approval', approvalReason: 'needs network' }), + ); + expect(data.phase).toBe('started'); + expect(data.headline).toContain('awaiting'); + expect(data.detail).toContain('needs network'); + }); + + it('surfaces timedOut for agent deadlines', () => { + const data = formatBackgroundTaskTranscript( + task({ + taskId: 'agent-aaaaaaaa', + status: 'failed', + exitCode: 1, + timedOut: true, + endedAt: Date.now(), + }), + ); + expect(data.detail).toContain('timed out'); + }); + + it('handles every BackgroundTaskStatus without throwing', () => { + const statuses: BackgroundTaskStatus[] = [ + 'running', + 'awaiting_approval', + 'completed', + 'failed', + 'killed', + 'lost', + ]; + for (const status of statuses) { + expect(() => formatBackgroundTaskTranscript(task({ status }))).not.toThrow(); + } + }); +}); diff --git a/apps/kimi-code/test/tui/commands/registry.test.ts b/apps/kimi-code/test/tui/commands/registry.test.ts new file mode 100644 index 000000000..3685857b8 --- /dev/null +++ b/apps/kimi-code/test/tui/commands/registry.test.ts @@ -0,0 +1,105 @@ +import { + BUILTIN_SLASH_COMMANDS, + findBuiltInSlashCommand, + parseSlashInput, + resolveSlashCommandAvailability, + sortSlashCommands, + type KimiSlashCommand, +} from '#/tui/commands/index'; +import { describe, expect, it } from 'vitest'; + +describe('parseSlashInput', () => { + it('parses command names and trimmed args', () => { + expect(parseSlashInput('/help')).toEqual({ name: 'help', args: '' }); + expect(parseSlashInput('/model kimi-k2 ')).toEqual({ + name: 'model', + args: 'kimi-k2', + }); + }); + + it('returns null for non-commands and path-like input', () => { + expect(parseSlashInput('hello')).toBeNull(); + expect(parseSlashInput('/')).toBeNull(); + expect(parseSlashInput('/ ')).toBeNull(); + expect(parseSlashInput('/some/path')).toBeNull(); + expect(parseSlashInput('/some/path with args')).toBeNull(); + }); +}); + +describe('built-in slash command registry', () => { + it('finds built-ins by name or alias', () => { + expect(findBuiltInSlashCommand('exit')?.name).toBe('exit'); + expect(findBuiltInSlashCommand('quit')?.name).toBe('exit'); + expect(findBuiltInSlashCommand('q')?.name).toBe('exit'); + expect(findBuiltInSlashCommand('clear')?.name).toBe('new'); + expect(findBuiltInSlashCommand('mcp')?.name).toBe('mcp'); + expect(findBuiltInSlashCommand('status')?.name).toBe('status'); + expect(findBuiltInSlashCommand('usage')?.aliases).not.toContain('status'); + expect(findBuiltInSlashCommand('unknown')).toBeUndefined(); + }); + + it('marks plan clear as idle-only while normal plan toggles are always available', () => { + const plan = findBuiltInSlashCommand('plan'); + expect(plan).toBeDefined(); + expect(resolveSlashCommandAvailability(plan!, '')).toBe('always'); + expect(resolveSlashCommandAvailability(plan!, 'on')).toBe('always'); + expect(resolveSlashCommandAvailability(plan!, 'clear')).toBe('idle-only'); + }); + + it('defaults commands without explicit availability to idle-only', () => { + const command: KimiSlashCommand = { + name: 'example', + aliases: [], + description: 'Example command', + }; + + expect(resolveSlashCommandAvailability(command, '')).toBe('idle-only'); + }); + + it('sorts commands by priority descending and name ascending', () => { + const commands: KimiSlashCommand[] = [ + { name: 'zebra', aliases: [], description: 'Z', priority: 100 }, + { name: 'alpha', aliases: [], description: 'A', priority: 100 }, + { name: 'middle', aliases: [], description: 'M', priority: 50 }, + { name: 'plain', aliases: [], description: 'P' }, + ]; + + expect(sortSlashCommands(commands).map((command) => command.name)).toEqual([ + 'alpha', + 'zebra', + 'middle', + 'plain', + ]); + }); + + it('contains the expected command names once', () => { + const names = BUILTIN_SLASH_COMMANDS.map((command) => command.name); + + expect(new Set(names).size).toBe(names.length); + expect(names).toEqual( + expect.arrayContaining([ + 'compact', + 'editor', + 'exit', + 'fork', + 'help', + 'init', + 'login', + 'logout', + 'mcp', + 'model', + 'new', + 'permission', + 'plan', + 'sessions', + 'settings', + 'status', + 'theme', + 'title', + 'usage', + 'version', + 'yolo', + ]), + ); + }); +}); diff --git a/apps/kimi-code/test/tui/commands/resolve.test.ts b/apps/kimi-code/test/tui/commands/resolve.test.ts new file mode 100644 index 000000000..ae043d5f4 --- /dev/null +++ b/apps/kimi-code/test/tui/commands/resolve.test.ts @@ -0,0 +1,117 @@ +import { + resolveSkillCommand, + resolveSlashCommandInput, + slashBusyMessage, + slashCommandBusyReason, +} from '#/tui/commands/index'; +import { describe, expect, it } from 'vitest'; + +function resolve( + input: string, + overrides: Partial<Parameters<typeof resolveSlashCommandInput>[0]> = {}, +) { + return resolveSlashCommandInput({ + input, + skillCommandMap: new Map<string, string>(), + isStreaming: false, + isCompacting: false, + ...overrides, + }); +} + +describe('resolveSlashCommandInput', () => { + it('returns not-command for normal text', () => { + expect(resolve('hello')).toEqual({ kind: 'not-command' }); + }); + + it('resolves built-in commands by name and alias', () => { + expect(resolve('/help')).toMatchObject({ kind: 'builtin', name: 'help', args: '' }); + expect(resolve('/q')).toMatchObject({ kind: 'builtin', name: 'exit', args: '' }); + expect(resolve('/clear')).toMatchObject({ kind: 'builtin', name: 'new', args: '' }); + expect(resolve('/fork')).toMatchObject({ kind: 'builtin', name: 'fork', args: '' }); + expect(resolve('/title New title')).toMatchObject({ + kind: 'builtin', + name: 'title', + args: 'New title', + }); + expect(resolve('/init')).toMatchObject({ kind: 'builtin', name: 'init', args: '' }); + }); + + it('blocks idle-only built-ins while streaming', () => { + expect(resolve('/new', { isStreaming: true })).toEqual({ + kind: 'blocked', + commandName: 'new', + reason: 'streaming', + }); + expect(resolve('/init', { isStreaming: true })).toEqual({ + kind: 'blocked', + commandName: 'init', + reason: 'streaming', + }); + }); + + it('allows always-available built-ins while streaming', () => { + expect(resolve('/plan on', { isStreaming: true })).toMatchObject({ + kind: 'builtin', + name: 'plan', + args: 'on', + }); + expect(resolve('/mcp', { isStreaming: true })).toMatchObject({ + kind: 'builtin', + name: 'mcp', + args: '', + }); + expect(resolve('/mcp', { isCompacting: true })).toMatchObject({ + kind: 'builtin', + name: 'mcp', + args: '', + }); + }); + + it('blocks plan clear while compacting because it is idle-only', () => { + expect(resolve('/plan clear', { isCompacting: true })).toEqual({ + kind: 'blocked', + commandName: 'plan', + reason: 'compacting', + }); + }); + + it('resolves skill commands and blocks them while busy', () => { + const skillCommandMap = new Map([['skill:review', 'review']]); + + expect(resolve('/skill:review src/app.ts', { skillCommandMap })).toEqual({ + kind: 'skill', + commandName: 'skill:review', + skillName: 'review', + args: 'src/app.ts', + }); + expect(resolve('/skill:review src/app.ts', { skillCommandMap, isStreaming: true })).toEqual({ + kind: 'blocked', + commandName: 'skill:review', + reason: 'streaming', + }); + }); + + it('returns message for unknown slash input', () => { + expect(resolve('/does-not-exist arg')).toEqual({ + kind: 'message', + input: '/does-not-exist arg', + }); + }); +}); + +describe('slash command busy helpers', () => { + it('resolves skill command aliases with and without skill prefix', () => { + const map = new Map([['skill:review', 'review']]); + + expect(resolveSkillCommand(map, 'skill:review')).toBe('review'); + expect(resolveSkillCommand(map, 'review')).toBe('review'); + }); + + it('formats busy messages', () => { + expect(slashCommandBusyReason({ isStreaming: true, isCompacting: false })).toBe('streaming'); + expect(slashCommandBusyReason({ isStreaming: false, isCompacting: true })).toBe('compacting'); + expect(slashBusyMessage('new', 'streaming')).toContain('Cannot /new while streaming'); + expect(slashBusyMessage('new', 'compacting')).toContain('Cannot /new while compacting'); + }); +}); diff --git a/apps/kimi-code/test/tui/commands/skills.test.ts b/apps/kimi-code/test/tui/commands/skills.test.ts new file mode 100644 index 000000000..81a176813 --- /dev/null +++ b/apps/kimi-code/test/tui/commands/skills.test.ts @@ -0,0 +1,57 @@ +import { buildSkillSlashCommands, isUserActivatableSkill } from '#/tui/commands/index'; +import type { SkillSummary } from '@moonshot-ai/kimi-code-sdk'; +import { describe, expect, it } from 'vitest'; + +function skill( + name: string, + type?: SkillSummary['type'], + extra: Partial<SkillSummary> = {}, +): SkillSummary { + return { + name, + type, + description: `${name} skill`, + ...extra, + } as SkillSummary; +} + +describe('skill slash commands', () => { + it('allows user-activatable skill types', () => { + expect(isUserActivatableSkill(skill('default'))).toBe(true); + expect(isUserActivatableSkill(skill('prompt', 'prompt'))).toBe(true); + expect(isUserActivatableSkill(skill('inline', 'inline'))).toBe(true); + expect(isUserActivatableSkill(skill('flow', 'flow'))).toBe(true); + }); + + it('filters non-user-activatable skill types', () => { + expect(isUserActivatableSkill(skill('agent', 'agent'))).toBe(false); + }); + + it('builds slash commands and command map entries with skill prefixes', () => { + const built = buildSkillSlashCommands([ + skill('review', 'prompt'), + skill('agent-only', 'agent'), + skill('commit', 'flow'), + ]); + + expect(built.commands.map((command) => command.name)).toEqual(['skill:review', 'skill:commit']); + expect(built.commands[0]).toMatchObject({ + name: 'skill:review', + aliases: [], + description: 'review skill', + }); + expect([...built.commandMap.entries()]).toEqual([ + ['skill:review', 'review'], + ['skill:commit', 'commit'], + ]); + }); + + it('keeps disableModelInvocation skills slash-invocable', () => { + const built = buildSkillSlashCommands([ + skill('mcp-config', 'inline', { disableModelInvocation: true, source: 'builtin' }), + ]); + + expect(built.commands.map((command) => command.name)).toEqual(['skill:mcp-config']); + expect(built.commandMap.get('skill:mcp-config')).toBe('mcp-config'); + }); +}); diff --git a/apps/kimi-code/test/tui/components/chrome/device-code-box.test.ts b/apps/kimi-code/test/tui/components/chrome/device-code-box.test.ts new file mode 100644 index 000000000..f7cea9b92 --- /dev/null +++ b/apps/kimi-code/test/tui/components/chrome/device-code-box.test.ts @@ -0,0 +1,66 @@ +import { describe, expect, it } from 'vitest'; + +import { DeviceCodeBoxComponent } from '#/tui/components/chrome/device-code-box'; +import { darkColors } from '#/tui/theme/colors'; + +function strip(text: string): string { + return text.replaceAll(/\[[0-9;]*m/g, ''); +} + +const url = 'https://www.kimi.com/code/authorize_device?user_code=N32D-W3YD'; +const code = 'N32D-W3YD'; +const title = 'Sign in to Kimi Code'; +const hint = 'Press Ctrl-C to cancel'; + +describe('DeviceCodeBoxComponent', () => { + it('renders a rounded border that frames the title, url and code', () => { + const component = new DeviceCodeBoxComponent({ + title, + url, + code, + hint, + colors: darkColors, + }); + + const lines = component.render(80).map(strip); + const joined = lines.join('\n'); + + expect(lines[1]?.startsWith('╭')).toBe(true); + expect(lines[1]?.endsWith('╮')).toBe(true); + expect(lines.at(-2)?.startsWith('╰')).toBe(true); + expect(lines.at(-2)?.endsWith('╯')).toBe(true); + + expect(joined).toContain(title); + expect(joined).toContain(url); + expect(joined).toContain(code); + expect(joined).toContain(hint); + expect(joined).toContain('Verification code'); + }); + + it('truncates long urls when the terminal is narrow', () => { + const component = new DeviceCodeBoxComponent({ + title, + url, + code, + colors: darkColors, + }); + + const lines = component.render(40).map(strip); + const urlLine = lines.find((line) => line.includes('https://')); + expect(urlLine).toBeDefined(); + expect(urlLine).toContain('…'); + expect(urlLine?.length).toBeLessThanOrEqual(40); + }); + + it('omits the hint row when no hint is provided', () => { + const component = new DeviceCodeBoxComponent({ + title, + url, + code, + colors: darkColors, + }); + + const joined = component.render(80).map(strip).join('\n'); + expect(joined).not.toContain('Press Ctrl-C'); + }); +}); diff --git a/apps/kimi-code/test/tui/components/chrome/gutter-container.test.ts b/apps/kimi-code/test/tui/components/chrome/gutter-container.test.ts new file mode 100644 index 000000000..c26e97a17 --- /dev/null +++ b/apps/kimi-code/test/tui/components/chrome/gutter-container.test.ts @@ -0,0 +1,57 @@ +import type { Component } from '@earendil-works/pi-tui'; +import { describe, expect, it, vi } from 'vitest'; + +import { GutterContainer } from '#/tui/components/chrome/gutter-container'; + +class FakeChild implements Component { + constructor( + private readonly lines: (innerWidth: number) => string[], + ) {} + invalidate(): void {} + render(width: number): string[] { + return this.lines(width); + } +} + +describe('GutterContainer', () => { + it('prefixes every child line with `left` spaces', () => { + const c = new GutterContainer(2, 2); + c.addChild(new FakeChild(() => ['hello', 'world'])); + expect(c.render(20)).toEqual([' hello', ' world']); + }); + + it('shrinks the width passed to children by left + right', () => { + const seenWidth = vi.fn<(w: number) => string[]>(() => ['x']); + const c = new GutterContainer(2, 3); + c.addChild(new FakeChild(seenWidth)); + c.render(20); + expect(seenWidth).toHaveBeenCalledWith(15); + }); + + it('clamps inner width to at least 1 when gutters would otherwise consume it', () => { + const seenWidth = vi.fn<(w: number) => string[]>(() => ['x']); + const c = new GutterContainer(5, 5); + c.addChild(new FakeChild(seenWidth)); + c.render(2); + expect(seenWidth).toHaveBeenCalledWith(1); + }); + + it('stacks lines from multiple children in order', () => { + const c = new GutterContainer(1, 0); + c.addChild(new FakeChild(() => ['a1', 'a2'])); + c.addChild(new FakeChild(() => ['b1'])); + expect(c.render(10)).toEqual([' a1', ' a2', ' b1']); + }); + + it('returns an empty array when there are no children', () => { + const c = new GutterContainer(2, 2); + expect(c.render(20)).toEqual([]); + }); + + it('preserves ANSI sequences within child lines (only the leading pad is plain)', () => { + const colored = 'red'; + const c = new GutterContainer(2, 2); + c.addChild(new FakeChild(() => [colored])); + expect(c.render(20)).toEqual([` ${colored}`]); + }); +}); diff --git a/apps/kimi-code/test/tui/components/dialogs/approval-panel.test.ts b/apps/kimi-code/test/tui/components/dialogs/approval-panel.test.ts new file mode 100644 index 000000000..451c08ba5 --- /dev/null +++ b/apps/kimi-code/test/tui/components/dialogs/approval-panel.test.ts @@ -0,0 +1,414 @@ +import { CURSOR_MARKER } from '@earendil-works/pi-tui'; +import { describe, expect, it } from 'vitest'; + +import { ApprovalPanelComponent } from '#/tui/components/dialogs/approval-panel'; +import type { PendingApproval } from '#/tui/reverse-rpc/types'; +import { getColorPalette } from '#/tui/theme/colors'; + +import { captureProcessWrite } from '../../../helpers/process'; + +const COLORS = getColorPalette('dark'); + +function strip(text: string): string { + return text.replaceAll(/\u001B\[[0-9;]*m/g, ''); +} + +function makePending(): PendingApproval { + return { + data: { + id: 'approval_1', + tool_call_id: 'tool_1', + tool_name: 'WriteFile', + action: 'write a file', + description: 'Update README.md', + display: [], + choices: [ + { label: 'Approve once', response: 'approved' }, + { label: 'Approve for this session', response: 'approved_for_session' }, + { label: 'Reject', response: 'rejected' }, + { label: 'Reject with feedback', response: 'rejected', requires_feedback: true }, + ], + }, + }; +} + +function makeDialog(): { + dialog: ApprovalPanelComponent; + responses: Array<{ + response: string; + feedback?: string | undefined; + selected_label?: string | undefined; + }>; +} { + const responses: Array<{ + response: string; + feedback?: string | undefined; + selected_label?: string | undefined; + }> = []; + const dialog = new ApprovalPanelComponent( + makePending(), + (response) => responses.push(response), + COLORS, + ); + return { dialog, responses }; +} + +describe('ApprovalPanelComponent', () => { + it('renders only numeric approval shortcuts in the hint', () => { + const { dialog } = makeDialog(); + const out = strip(dialog.render(80).join('\n')); + expect(out).toContain('1/2/3/4 choose'); + expect(out).not.toContain('y/a/n/f'); + }); + + it('renders dangerous shell warnings with simple copy and no icon', () => { + const pending: PendingApproval = { + data: { + id: 'approval_danger', + tool_call_id: 'tool_danger', + tool_name: 'Bash', + action: 'run', + description: '', + display: [ + { + type: 'shell', + language: 'bash', + command: 'rm -rf /tmp/cache', + danger: 'recursive delete', + }, + ], + choices: [{ label: 'Approve once', response: 'approved' }], + }, + }; + const dialog = new ApprovalPanelComponent(pending, () => {}, COLORS); + + const out = strip(dialog.render(80).join('\n')); + expect(out).toContain('Dangerous: recursive delete'); + expect(out).not.toContain('potentially destructive'); + expect(out).not.toContain('⚠'); + }); + + it('numeric shortcuts still drive approval actions', () => { + const { dialog, responses } = makeDialog(); + dialog.handleInput('2'); + expect(responses).toEqual([{ response: 'approved_for_session', feedback: undefined }]); + }); + + it('shortcut 4 enters feedback mode and submits the typed feedback', () => { + const { dialog, responses } = makeDialog(); + dialog.handleInput('4'); + dialog.handleInput('n'); + dialog.handleInput('o'); + dialog.handleInput('\r'); + expect(responses).toEqual([{ response: 'rejected', feedback: 'no' }]); + }); + + it('renders feedback input inline with the selected choice', () => { + const { dialog } = makeDialog(); + dialog.handleInput('4'); + + const out = strip(dialog.render(80).join('\n')); + expect(out).toContain('▶ 4. Reject with feedback'); + expect(out).not.toContain('\n > '); + }); + + it('legacy y/a/n/f shortcuts no longer trigger approval actions', () => { + for (const key of ['y', 'a', 'n', 'f']) { + const { dialog, responses } = makeDialog(); + dialog.handleInput(key); + expect(responses).toEqual([]); + } + }); + + it('feedback input supports left/right cursor editing', () => { + const { dialog, responses } = makeDialog(); + dialog.handleInput('4'); + dialog.handleInput('n'); + dialog.handleInput('o'); + dialog.handleInput('\u001B[D'); + dialog.handleInput('!'); + dialog.handleInput('\r'); + expect(responses).toEqual([{ response: 'rejected', feedback: 'n!o' }]); + }); + + it('feedback input keeps editor shortcuts like ctrl+b / ctrl+f', () => { + const { dialog, responses } = makeDialog(); + dialog.handleInput('4'); + dialog.handleInput('a'); + dialog.handleInput('b'); + dialog.handleInput('c'); + dialog.handleInput('\u0002'); + dialog.handleInput('\u0002'); + dialog.handleInput('X'); + dialog.handleInput('\u0006'); + dialog.handleInput('Y'); + dialog.handleInput('\r'); + expect(responses).toEqual([{ response: 'rejected', feedback: 'aXbYc' }]); + }); + + it('renders an IME cursor marker while editing feedback', () => { + const { dialog } = makeDialog(); + dialog.focused = true; + dialog.handleInput('4'); + + const out = dialog.render(80).join('\n'); + expect(out).toContain(CURSOR_MARKER); + }); + + it.each(['\u0003', '\u0004', '\u001B'])( + 'shortcut %j rejects approval immediately', + (key) => { + const { dialog, responses } = makeDialog(); + dialog.handleInput(key); + expect(responses).toEqual([{ response: 'rejected' }]); + }, + ); + + it('renders ExitPlanMode with plan-specific header and plan-review choices', () => { + const pending: PendingApproval = { + data: { + id: 'approval_plan', + tool_call_id: 'tool_plan', + tool_name: 'ExitPlanMode', + action: 'review plan', + description: '', + display: [], + choices: [ + { label: 'Approve', response: 'approved' }, + { label: 'Reject', response: 'rejected' }, + { label: 'Revise', response: 'rejected', requires_feedback: true }, + ], + }, + }; + const dialog = new ApprovalPanelComponent(pending, () => {}, COLORS); + + const out = strip(dialog.render(80).join('\n')); + expect(out).toContain('Ready to build with this plan?'); + expect(out).not.toContain('Approve ExitPlanMode?'); + expect(out).toContain('Approve'); + expect(out).toContain('Reject'); + expect(out).toContain('Revise'); + expect(out).not.toContain('Approve for this session'); + expect(out).not.toContain('Investigate'); + }); + + it('renders an Edit diff collapsed by default and expands on ctrl+e', () => { + const responses: Array<{ response: string }> = []; + const oldLines: string[] = []; + const newLines: string[] = []; + for (let i = 1; i <= 30; i++) { + oldLines.push(`old${String(i)}`); + newLines.push(`new${String(i)}`); + } + const pending: PendingApproval = { + data: { + id: 'approval_diff', + tool_call_id: 'tool_diff', + tool_name: 'Edit', + action: 'edit', + description: '', + display: [ + { + type: 'diff', + path: 'src/foo.ts', + old_text: oldLines.join('\n'), + new_text: newLines.join('\n'), + }, + ], + choices: [{ label: 'Approve once', response: 'approved' }], + }, + }; + let globalToggleCalls = 0; + const dialog = new ApprovalPanelComponent( + pending, + (r) => responses.push(r), + COLORS, + () => globalToggleCalls++, + ); + + const collapsed = strip(dialog.render(120).join('\n')); + expect(collapsed).not.toMatch(/\bedit\s+src\/foo\.ts\b/); + expect(collapsed).toContain('+30'); + expect(collapsed).toContain('-30'); + expect(collapsed).toContain('ctrl+e expand'); + expect(collapsed).toContain('ctrl+e to expand'); + expect(collapsed).toMatch(/old\d+|new\d+/); + expect(collapsed).not.toContain('new30'); + + dialog.handleInput('\u0005'); // Ctrl+E — local toggle, no global callback. + + const expanded = strip(dialog.render(120).join('\n')); + expect(expanded).toContain('new30'); + expect(expanded).toContain('ctrl+e collapse'); + expect(expanded).not.toContain('more changes hidden'); + expect(globalToggleCalls).toBe(0); + expect(responses).toEqual([]); + }); + + it('forwards ctrl+o to the global tool-output toggle without changing local expansion', () => { + const pending: PendingApproval = { + data: { + id: 'approval_forward', + tool_call_id: 'tool_forward', + tool_name: 'Edit', + action: 'edit', + description: '', + display: [ + { + type: 'diff', + path: 'src/foo.ts', + old_text: Array.from({ length: 30 }, (_, i) => `old${String(i + 1)}`).join('\n'), + new_text: Array.from({ length: 30 }, (_, i) => `new${String(i + 1)}`).join('\n'), + }, + ], + choices: [{ label: 'Approve once', response: 'approved' }], + }, + }; + let globalToggleCalls = 0; + const dialog = new ApprovalPanelComponent(pending, () => {}, COLORS, () => globalToggleCalls++); + + dialog.handleInput('\u000F'); // Ctrl+O — forwarded; local stays collapsed. + + const after = strip(dialog.render(120).join('\n')); + expect(globalToggleCalls).toBe(1); + expect(after).toContain('ctrl+e expand'); + expect(after).not.toContain('new30'); + }); + + it('also forwards ctrl+e to the global plan-expand toggle while toggling local content', () => { + const pending: PendingApproval = { + data: { + id: 'approval_plan_forward', + tool_call_id: 'tool_plan_forward', + tool_name: 'Edit', + action: 'edit', + description: '', + display: [ + { + type: 'diff', + path: 'src/foo.ts', + old_text: Array.from({ length: 30 }, (_, i) => `old${String(i + 1)}`).join('\n'), + new_text: Array.from({ length: 30 }, (_, i) => `new${String(i + 1)}`).join('\n'), + }, + ], + choices: [{ label: 'Approve once', response: 'approved' }], + }, + }; + let planToggles = 0; + const dialog = new ApprovalPanelComponent( + pending, + () => {}, + COLORS, + undefined, + () => planToggles++, + ); + + dialog.handleInput('\u0005'); // Ctrl+E + const out = strip(dialog.render(120).join('\n')); + expect(planToggles).toBe(1); + expect(out).toContain('ctrl+e collapse'); // local also expanded + expect(out).toContain('new30'); + }); + + it('renders Write as a syntax-highlighted code block (file_content), not a diff', () => { + const responses: Array<{ response: string }> = []; + const lines: string[] = []; + for (let i = 1; i <= 30; i++) lines.push(`const x${String(i)} = ${String(i)};`); + const pending: PendingApproval = { + data: { + id: 'approval_write', + tool_call_id: 'tool_write', + tool_name: 'Write', + action: 'write', + description: '', + display: [{ type: 'file_content', path: 'src/new.ts', content: lines.join('\n') }], + choices: [{ label: 'Approve once', response: 'approved' }], + }, + }; + const dialog = new ApprovalPanelComponent(pending, (r) => responses.push(r), COLORS); + + const collapsed = strip(dialog.render(120).join('\n')); + // No diff markers, no +N -M header. + expect(collapsed).not.toMatch(/^\s*\+\d+/m); + expect(collapsed).not.toMatch(/^\s*-\d+/m); + expect(collapsed).toContain('src/new.ts'); + expect(collapsed).toContain('const x1 = 1;'); + expect(collapsed).toContain('const x10 = 10;'); + expect(collapsed).not.toContain('const x25 = 25;'); + expect(collapsed).toContain('20 more lines hidden (ctrl+e to expand)'); + expect(collapsed).toContain('ctrl+e expand'); + + dialog.handleInput('\u0005'); // Ctrl+E + const expanded = strip(dialog.render(120).join('\n')); + expect(expanded).toContain('const x30 = 30;'); + expect(expanded).not.toContain('more lines hidden'); + expect(responses).toEqual([]); + }); + + it('renders unknown file_content extensions as plain text without stderr noise', () => { + const pending: PendingApproval = { + data: { + id: 'approval_unknown_write', + tool_call_id: 'tool_unknown_write', + tool_name: 'Write', + action: 'write', + description: '', + display: [{ type: 'file_content', path: 'demo.abcxyz', content: 'hello\nworld' }], + choices: [{ label: 'Approve once', response: 'approved' }], + }, + }; + const stderr = captureProcessWrite('stderr'); + try { + const dialog = new ApprovalPanelComponent(pending, () => {}, COLORS); + const collapsed = strip(dialog.render(120).join('\n')); + expect(collapsed).toContain('hello'); + + dialog.handleInput('\u0005'); // Ctrl+E + const expanded = strip(dialog.render(120).join('\n')); + expect(expanded).toContain('world'); + expect(stderr.text()).not.toContain('Could not find the language'); + } finally { + stderr.restore(); + } + }); + + it('returns feedback for plan-review revise choice', () => { + const responses: Array<{ + response: string; + feedback?: string | undefined; + selected_label?: string | undefined; + }> = []; + const pending: PendingApproval = { + data: { + id: 'approval_plan', + tool_call_id: 'tool_plan', + tool_name: 'ExitPlanMode', + action: 'review plan', + description: '', + display: [], + choices: [ + { label: 'Approve', response: 'approved' }, + { + label: 'Revise', + response: 'rejected', + selected_label: 'Revise', + requires_feedback: true, + }, + ], + }, + }; + const dialog = new ApprovalPanelComponent( + pending, + (response) => responses.push(response), + COLORS, + ); + + dialog.handleInput('2'); + dialog.handleInput('n'); + dialog.handleInput('o'); + dialog.handleInput('\r'); + expect(responses).toEqual([ + { response: 'rejected', feedback: 'no', selected_label: 'Revise' }, + ]); + }); +}); diff --git a/apps/kimi-code/test/tui/components/dialogs/choice-picker.test.ts b/apps/kimi-code/test/tui/components/dialogs/choice-picker.test.ts new file mode 100644 index 000000000..9f6d453c1 --- /dev/null +++ b/apps/kimi-code/test/tui/components/dialogs/choice-picker.test.ts @@ -0,0 +1,202 @@ +import { describe, expect, it, vi } from 'vitest'; + +import { ChoicePickerComponent } from '#/tui/components/dialogs/choice-picker'; +import { EditorSelectorComponent } from '#/tui/components/dialogs/editor-selector'; +import { ModelSelectorComponent } from '#/tui/components/dialogs/model-selector'; +import { PermissionSelectorComponent } from '#/tui/components/dialogs/permission-selector'; +import { SettingsSelectorComponent } from '#/tui/components/dialogs/settings-selector'; +import { ThemeSelectorComponent } from '#/tui/components/dialogs/theme-selector'; +import { darkColors } from '#/tui/theme/colors'; + +const ANSI_SGR = /\u001B\[[0-9;]*m/g; + +function strip(text: string): string { + return text.replaceAll(ANSI_SGR, ''); +} + +describe('ChoicePickerComponent', () => { + it('renders optional descriptions below choice labels', () => { + const picker = new ChoicePickerComponent({ + title: 'Select permission mode', + options: [ + { + value: 'manual', + label: 'Manual', + description: 'Ask before commands, edits, and other risky actions.', + }, + { + value: 'auto', + label: 'Auto', + description: 'Automatically approve tool actions and plan transitions.', + }, + ], + currentValue: 'manual', + colors: darkColors, + onSelect: vi.fn(), + onCancel: vi.fn(), + }); + + const out = picker.render(120).map(strip); + + expect(out).toContain(' ❯ Manual ← current'); + expect(out).toContain(' Ask before commands, edits, and other risky actions.'); + expect(out).toContain(' Automatically approve tool actions and plan transitions.'); + }); + + it('renders domain selector wrappers with their configured options', () => { + const onSelect = vi.fn(); + const onCancel = vi.fn(); + + const editor = new EditorSelectorComponent({ + currentValue: 'vim', + colors: darkColors, + onSelect, + onCancel, + }); + expect(editor.render(120).map(strip)).toContain(' ❯ Vim ← current'); + + const model = new ModelSelectorComponent({ + models: { + kimi: { + provider: 'managed:kimi-code', + model: 'kimi-k2', + maxContextSize: 200_000, + displayName: 'Kimi K2', + capabilities: ['thinking'], + }, + }, + currentValue: 'kimi', + currentThinking: true, + colors: darkColors, + onSelect, + onCancel, + }); + const modelOutput = model.render(120).map(strip); + expect(modelOutput).toContain(' ❯ Kimi K2 (Kimi Code) ← current'); + expect(modelOutput).toContain(' Thinking'); + expect(modelOutput).toContain(' [ On ] Off '); + + const theme = new ThemeSelectorComponent({ + currentValue: 'light', + colors: darkColors, + onSelect, + onCancel, + }); + expect(theme.render(120).map(strip)).toContain(' ❯ Light ← current'); + + const permission = new PermissionSelectorComponent({ + currentValue: 'manual', + colors: darkColors, + onSelect, + onCancel, + }); + expect(permission.render(120).map(strip)).toContain(' ❯ Manual ← current'); + + const settings = new SettingsSelectorComponent({ + colors: darkColors, + onSelect, + onCancel, + }); + const settingsOutput = settings.render(120).map(strip); + expect(settingsOutput).toContain(' ❯ Model'); + expect(settingsOutput).toContain(' Switch the active model and thinking mode.'); + }); + + it('submits the selected model and inline thinking state', () => { + const onSelect = vi.fn(); + const picker = new ModelSelectorComponent({ + models: { + kimi: { + provider: 'managed:kimi-code', + model: 'kimi-k2', + maxContextSize: 200_000, + displayName: 'Kimi K2', + capabilities: ['thinking'], + }, + }, + currentValue: 'kimi', + currentThinking: true, + colors: darkColors, + onSelect, + onCancel: vi.fn(), + }); + + picker.handleInput('\u001B[C'); + picker.handleInput('\r'); + + expect(onSelect).toHaveBeenCalledWith({ alias: 'kimi', thinking: false }); + }); + + it('forces always-thinking models on and unsupported models off', () => { + const onSelect = vi.fn(); + const picker = new ModelSelectorComponent({ + models: { + always: { + provider: 'managed:kimi-code', + model: 'kimi-thinking', + maxContextSize: 200_000, + displayName: 'Kimi Thinking', + capabilities: ['always_thinking'], + }, + plain: { + provider: 'managed:kimi-code', + model: 'kimi-plain', + maxContextSize: 200_000, + displayName: 'Kimi Plain', + capabilities: ['tool_use'], + }, + }, + currentValue: 'always', + currentThinking: false, + colors: darkColors, + onSelect, + onCancel: vi.fn(), + }); + + expect(picker.render(120).map(strip)).toContain(' [ Always on ]'); + picker.handleInput('\u001B[C'); + picker.handleInput('\r'); + expect(onSelect).toHaveBeenLastCalledWith({ alias: 'always', thinking: true }); + + picker.handleInput('\u001B[B'); + expect(picker.render(120).map(strip)).toContain(' [ Off ] unsupported'); + picker.handleInput('\u001B[D'); + picker.handleInput('\r'); + expect(onSelect).toHaveBeenLastCalledWith({ alias: 'plain', thinking: false }); + }); + + it('keeps the thinking draft when moving across models', () => { + const onSelect = vi.fn(); + const picker = new ModelSelectorComponent({ + models: { + plain: { + provider: 'managed:kimi-code', + model: 'kimi-plain', + maxContextSize: 200_000, + displayName: 'Kimi Plain', + capabilities: ['tool_use'], + }, + thinking: { + provider: 'managed:kimi-code', + model: 'kimi-thinking', + maxContextSize: 200_000, + displayName: 'Kimi Thinking', + capabilities: ['thinking'], + }, + }, + currentValue: 'plain', + currentThinking: false, + colors: darkColors, + onSelect, + onCancel: vi.fn(), + }); + + picker.handleInput('\u001B[B'); + picker.handleInput('\u001B[D'); + picker.handleInput('\u001B[A'); + picker.handleInput('\u001B[B'); + picker.handleInput('\r'); + + expect(onSelect).toHaveBeenCalledWith({ alias: 'thinking', thinking: true }); + }); +}); diff --git a/apps/kimi-code/test/tui/components/dialogs/compaction.test.ts b/apps/kimi-code/test/tui/components/dialogs/compaction.test.ts new file mode 100644 index 000000000..b49501978 --- /dev/null +++ b/apps/kimi-code/test/tui/components/dialogs/compaction.test.ts @@ -0,0 +1,39 @@ +import { describe, expect, it } from 'vitest'; + +import { CompactionComponent } from '#/tui/components/dialogs/compaction'; +import { darkColors } from '#/tui/theme/colors'; + +function strip(text: string): string { + return text.replaceAll(/\u001B\[[0-9;]*m/g, ''); +} + +describe('CompactionComponent', () => { + it('renders the custom instruction below the compacting label', () => { + const component = new CompactionComponent(darkColors, undefined, 'keep the recent files only'); + + try { + const lines = component.render(120).map(strip); + const text = lines.join('\n'); + + expect(text).toContain('Compacting context...'); + expect(text).toContain(' keep the recent files only'); + } finally { + component.dispose(); + } + }); + + it('renders a cancelled terminal state', () => { + const component = new CompactionComponent(darkColors); + + try { + component.markCanceled(); + const lines = component.render(120).map(strip); + const text = lines.join('\n'); + + expect(text).toContain('Compaction cancelled'); + expect(text).not.toContain('Compacting context...'); + } finally { + component.dispose(); + } + }); +}); diff --git a/apps/kimi-code/test/tui/components/dialogs/feedback-input-dialog.test.ts b/apps/kimi-code/test/tui/components/dialogs/feedback-input-dialog.test.ts new file mode 100644 index 000000000..13fcdd589 --- /dev/null +++ b/apps/kimi-code/test/tui/components/dialogs/feedback-input-dialog.test.ts @@ -0,0 +1,108 @@ +import chalk from 'chalk'; +import { beforeAll, describe, expect, it } from 'vitest'; + +import { + FeedbackInputDialogComponent, + type FeedbackInputDialogResult, +} from '#/tui/components/dialogs/feedback-input-dialog'; +import { darkColors } from '#/tui/theme/colors'; + +const ESC = String.fromCodePoint(27); +const CTRL_C = String.fromCodePoint(3); +const ANSI_RE = new RegExp(`${ESC}\\[[0-9;]*m`, 'g'); + +function strip(text: string): string { + return text.replaceAll(ANSI_RE, ''); +} + +beforeAll(() => { + chalk.level = 3; +}); + +function makeDialog(): { + dialog: FeedbackInputDialogComponent; + collected: FeedbackInputDialogResult[]; +} { + const collected: FeedbackInputDialogResult[] = []; + const dialog = new FeedbackInputDialogComponent((result) => { + collected.push(result); + }, darkColors); + dialog.focused = true; + return { dialog, collected }; +} + +describe('FeedbackInputDialogComponent', () => { + it('renders a blue rounded box with title, subtitle, and footer', () => { + const { dialog } = makeDialog(); + const text = strip(dialog.render(60).join('\n')); + + expect(text).toContain('╭'); + expect(text).toContain('╮'); + expect(text).toContain('╰'); + expect(text).toContain('╯'); + expect(text).toContain('Send feedback to Kimi Code'); + expect(text).toContain("Tell us what's working or what's not."); + expect(text).toContain('Enter to submit'); + expect(text).toContain('Esc to cancel'); + }); + + it('uses the primary color for the border', () => { + const { dialog } = makeDialog(); + const rendered = dialog.render(60).join('\n'); + const sample = chalk.hex(darkColors.primary)('x'); + const ansiOpen = sample.split('x')[0]!; + expect(rendered).toContain(`${ansiOpen}╭`); + expect(rendered).toContain(`${ansiOpen}╰`); + }); + + it('typing then pressing Enter submits the trimmed value', () => { + const { dialog, collected } = makeDialog(); + for (const ch of 'hello world ') { + dialog.handleInput(ch); + } + dialog.handleInput('\r'); + expect(collected).toEqual([{ kind: 'ok', value: 'hello world' }]); + }); + + it('Enter on an empty input does not submit and shows an inline empty hint', () => { + const { dialog, collected } = makeDialog(); + dialog.handleInput('\r'); + expect(collected).toEqual([]); + const text = strip(dialog.render(60).join('\n')); + expect(text).toContain('Feedback cannot be empty.'); + expect(text).not.toContain("Tell us what's working or what's not."); + }); + + it('typing again after the empty hint clears the hint', () => { + const { dialog, collected } = makeDialog(); + dialog.handleInput('\r'); + expect(strip(dialog.render(60).join('\n'))).toContain('Feedback cannot be empty.'); + dialog.handleInput('a'); + const text = strip(dialog.render(60).join('\n')); + expect(text).toContain("Tell us what's working or what's not."); + expect(text).not.toContain('Feedback cannot be empty.'); + expect(collected).toEqual([]); + }); + + it('Esc cancels the dialog', () => { + const { dialog, collected } = makeDialog(); + dialog.handleInput(ESC); + expect(collected).toEqual([{ kind: 'cancel' }]); + }); + + it('Ctrl-C cancels the dialog', () => { + const { dialog, collected } = makeDialog(); + dialog.handleInput(CTRL_C); + expect(collected).toEqual([{ kind: 'cancel' }]); + }); + + it('does not fire onDone twice', () => { + const { dialog, collected } = makeDialog(); + dialog.handleInput('h'); + dialog.handleInput('i'); + dialog.handleInput('\r'); + dialog.handleInput('\r'); + dialog.handleInput(ESC); + expect(collected).toEqual([{ kind: 'ok', value: 'hi' }]); + }); +}); diff --git a/apps/kimi-code/test/tui/components/dialogs/question-dialog.test.ts b/apps/kimi-code/test/tui/components/dialogs/question-dialog.test.ts new file mode 100644 index 000000000..6a492f406 --- /dev/null +++ b/apps/kimi-code/test/tui/components/dialogs/question-dialog.test.ts @@ -0,0 +1,439 @@ +import { CURSOR_MARKER } from '@earendil-works/pi-tui'; +import chalk from 'chalk'; +import { beforeAll, describe, expect, it } from 'vitest'; + +import { QuestionDialogComponent } from '#/tui/components/dialogs/question-dialog'; +import type { PendingQuestion } from '#/tui/reverse-rpc/types'; +import { darkColors } from '#/tui/theme/colors'; + +function strip(text: string): string { + return text.replaceAll(/\u001B\[[0-9;]*m/g, ''); +} + +beforeAll(() => { + chalk.level = 3; +}); + +function makePending( + questions: PendingQuestion['data']['questions'], + requestId = 'q_1', +): PendingQuestion { + return { + data: { + id: requestId, + tool_call_id: 'tc_1', + questions, + }, + }; +} + +function makeDialog( + pending: PendingQuestion, + onToggleToolOutput?: () => void, + onTogglePlanExpand?: () => void, +): { + dialog: QuestionDialogComponent; + collected: string[][]; + methods: Array<string | undefined>; +} { + const collected: string[][] = []; + const methods: Array<string | undefined> = []; + const dialog = new QuestionDialogComponent( + pending, + (response) => { + collected.push(response.answers); + methods.push(response.method); + }, + darkColors, + 6, + onToggleToolOutput, + onTogglePlanExpand, + ); + return { dialog, collected, methods }; +} + +describe('QuestionDialogComponent', () => { + it('single-select answers auto-advance and only submit from the review tab', () => { + const pending = makePending([ + { + question: 'Q1?', + multi_select: false, + options: [{ label: 'A1' }, { label: 'B1' }], + }, + { + question: 'Q2?', + multi_select: false, + options: [{ label: 'A2' }, { label: 'B2' }], + }, + ]); + const { dialog, collected, methods } = makeDialog(pending); + + dialog.handleInput('2'); + expect(collected).toEqual([]); + expect(strip(dialog.render(80).join('\n'))).toMatch(/Q2\?/); + + dialog.handleInput('\r'); + expect(collected).toEqual([]); + + const reviewRaw = dialog.render(80).join('\n'); + const review = strip(reviewRaw); + expect(review).toContain('Review your answer before submit'); + expect(review).toContain('Ready to submit your answers?'); + expect(review).not.toContain('? Ready to submit your answers?'); + expect(review).not.toContain('Please answer all questions before submitting.'); + expect(reviewRaw).toContain( + chalk.hex(darkColors.text).bold(' Review your answer before submit'), + ); + expect(reviewRaw).toContain(chalk.hex(darkColors.text)(' Ready to submit your answers?')); + expect(review).toContain('B1'); + expect(review).toContain('A2'); + + dialog.handleInput('1'); + expect(collected).toEqual([['B1', 'A2']]); + expect(methods).toEqual(['enter']); + }); + + it('last single-select question goes straight to review instead of wrapping back', () => { + const pending = makePending([ + { + question: 'Q1?', + multi_select: false, + options: [{ label: 'A1' }, { label: 'B1' }], + }, + { + question: 'Q2?', + multi_select: false, + options: [{ label: 'A2' }, { label: 'B2' }], + }, + ]); + const { dialog, collected } = makeDialog(pending); + + dialog.handleInput('\t'); + dialog.handleInput('2'); + + const review = strip(dialog.render(80).join('\n')); + expect(review).toContain('Review your answer before submit'); + expect(review).toContain('Some questions are still unanswered.'); + expect(review).toContain('B2'); + expect(review).toContain('Not answered'); + expect(collected).toEqual([]); + }); + + it('renders optional body text above options', () => { + const pending = makePending([ + { + question: 'Approve this plan?', + body: '# Plan\n\n1. Make the focused change.', + multi_select: false, + options: [{ label: 'Approve' }, { label: 'Reject' }], + }, + ]); + const { dialog } = makeDialog(pending); + const out = strip(dialog.render(80).join('\n')); + expect(out).toContain('# Plan'); + expect(out).toContain('1. Make the focused change.'); + expect(out).toContain('Approve'); + expect(out).toContain('Other'); + }); + + it('multi-select uses space and number keys to toggle choices', () => { + const pending = makePending([ + { + question: 'Pick many?', + multi_select: true, + options: [{ label: 'A' }, { label: 'B' }, { label: 'C' }], + }, + ]); + const { dialog, collected } = makeDialog(pending); + + dialog.handleInput(' '); + dialog.handleInput('\u001B[B'); + dialog.handleInput('\u001B[B'); + dialog.handleInput('3'); + dialog.handleInput('\t'); + + const review = strip(dialog.render(80).join('\n')); + expect(review).toContain('A, C'); + expect(review).not.toContain('Not answered'); + expect(collected).toEqual([]); + }); + + it('review shows an unanswered warning and still allows submit', () => { + const pending = makePending([ + { + question: 'Q1?', + multi_select: false, + options: [{ label: 'A1' }, { label: 'B1' }], + }, + { + question: 'Q2?', + multi_select: false, + options: [{ label: 'A2' }, { label: 'B2' }], + }, + ]); + const { dialog, collected } = makeDialog(pending); + + dialog.handleInput('\t'); + dialog.handleInput('2'); + + const before = strip(dialog.render(80).join('\n')); + expect(before).toContain('Not answered'); + expect(before).toContain('Some questions are still unanswered.'); + + dialog.handleInput('\r'); + expect(collected).toHaveLength(1); + expect(collected[0]?.[0]).toBeUndefined(); + expect(collected[0]?.[1]).toBe('B2'); + }); + + it('review cancel dismisses the whole request', () => { + const pending = makePending([ + { + question: 'Pick one?', + multi_select: false, + options: [{ label: 'A' }, { label: 'B' }], + }, + ]); + const { dialog, collected } = makeDialog(pending); + + dialog.handleInput('\t'); + dialog.handleInput('2'); + + expect(collected).toEqual([[]]); + }); + + it('single-select Other input is inline and auto-advances after commit', () => { + const pending = makePending([ + { + question: 'Pick one?', + multi_select: false, + other_label: 'Custom', + other_description: 'Type your own answer', + options: [{ label: 'A' }, { label: 'B' }], + }, + ]); + const { dialog, collected, methods } = makeDialog(pending); + + dialog.handleInput('3'); + let out = strip(dialog.render(80).join('\n')); + expect(out).toContain('→ [3] Custom:'); + expect(out).not.toContain('Type your own answer'); + + dialog.handleInput('H'); + dialog.handleInput('i'); + dialog.handleInput('\r'); + + out = strip(dialog.render(80).join('\n')); + expect(out).toContain('Review your answer before submit'); + expect(out).toContain('Ready to submit your answers?'); + expect(out).not.toContain('? Ready to submit your answers?'); + expect(out).toContain('Hi'); + + dialog.handleInput('1'); + expect(collected).toEqual([['Hi']]); + expect(methods).toEqual(['enter']); + }); + + it('Other input supports left/right cursor editing before commit', () => { + const pending = makePending([ + { + question: 'Pick one?', + multi_select: false, + other_label: 'Custom', + options: [{ label: 'A' }, { label: 'B' }], + }, + ]); + const { dialog } = makeDialog(pending); + + dialog.handleInput('3'); + dialog.handleInput('H'); + dialog.handleInput('i'); + dialog.handleInput('\u001B[D'); + dialog.handleInput('!'); + dialog.handleInput('\r'); + + const out = strip(dialog.render(80).join('\n')); + expect(out).toContain('H!i'); + expect(out).toContain('Review your answer before submit'); + }); + + it('renders an IME cursor marker while editing Other when focused', () => { + const pending = makePending([ + { + question: 'Pick one?', + multi_select: false, + other_label: 'Custom', + options: [{ label: 'A' }, { label: 'B' }], + }, + ]); + const { dialog } = makeDialog(pending); + + dialog.focused = true; + dialog.handleInput('3'); + + const out = dialog.render(80).join('\n'); + expect(out).toContain(CURSOR_MARKER); + }); + + it('keeps selected options green even when the cursor returns to them', () => { + const pending = makePending([ + { + question: 'Pick one?', + multi_select: false, + options: [{ label: 'A' }, { label: 'B' }], + }, + ]); + const { dialog } = makeDialog(pending); + + dialog.handleInput('\r'); + dialog.handleInput('\u001B[D'); + + const out = dialog.render(80).join('\n'); + expect(out).toContain(chalk.hex(darkColors.success).bold(' → [1] A')); + expect(out).not.toContain(chalk.hex(darkColors.primary)(' → [1] A')); + }); + + it('stretches the border to the full available width', () => { + const pending = makePending([ + { + question: 'Pick one?', + multi_select: false, + options: [{ label: 'A' }, { label: 'B' }], + }, + ]); + const { dialog } = makeDialog(pending); + + const lines = dialog.render(80); + expect(strip(lines[0] ?? '')).toHaveLength(80); + expect(strip(lines.at(-1) ?? '')).toHaveLength(80); + }); + + it('does not show the submit tab as completed when all questions are answered', () => { + const pending = makePending([ + { + question: 'Q1?', + multi_select: false, + options: [{ label: 'A1' }, { label: 'B1' }], + }, + { + question: 'Q2?', + multi_select: false, + options: [{ label: 'A2' }, { label: 'B2' }], + }, + ]); + const { dialog } = makeDialog(pending); + + dialog.handleInput('\r'); + dialog.handleInput('\r'); + + const out = strip(dialog.render(80).join('\n')); + expect(out).toContain(' Submit '); + expect(out).not.toContain('(✓) Submit'); + expect(out).not.toContain('(○) Submit'); + }); + + it('renders the active tab with a highlighted background instead of the circle marker', () => { + const pending = makePending([ + { + question: 'Q1?', + header: 'First', + multi_select: false, + options: [{ label: 'A1' }, { label: 'B1' }], + }, + { + question: 'Q2?', + header: 'Second', + multi_select: false, + options: [{ label: 'A2' }, { label: 'B2' }], + }, + ]); + const { dialog } = makeDialog(pending); + + const out = dialog.render(80).join('\n'); + expect(out).toContain(chalk.bgHex(darkColors.primary).hex(darkColors.text).bold(' First ')); + expect(out).not.toContain('(●) First'); + }); + + it('preserves Other drafts across tabs and question navigation', () => { + const pending = makePending([ + { + question: 'Pick toppings?', + multi_select: true, + options: [{ label: 'Cheese' }, { label: 'Pepperoni' }], + }, + ]); + const { dialog } = makeDialog(pending); + + dialog.handleInput('3'); + dialog.handleInput('M'); + dialog.handleInput('u'); + dialog.handleInput('s'); + dialog.handleInput('h'); + dialog.handleInput('r'); + dialog.handleInput('o'); + dialog.handleInput('o'); + dialog.handleInput('m'); + dialog.handleInput('\t'); + + let out = strip(dialog.render(80).join('\n')); + expect(out).toContain('Not answered'); + + dialog.handleInput('\u001B[D'); + out = strip(dialog.render(80).join('\n')); + expect(out).toContain('Other: Mushroom'); + + dialog.handleInput('\r'); + dialog.handleInput('\r'); + dialog.handleInput('\t'); + out = strip(dialog.render(80).join('\n')); + expect(out).toContain('Mushroom'); + }); + + it('escape dismisses with empty answers array', () => { + const pending = makePending([ + { + question: 'Pick one?', + multi_select: false, + options: [{ label: 'A' }, { label: 'B' }], + }, + ]); + const { dialog, collected } = makeDialog(pending); + dialog.handleInput('\u001B'); + expect(collected).toEqual([[]]); + }); + + it.each(['\u0003', '\u0004'])('ctrl shortcut %j dismisses question dialog', (key) => { + const pending = makePending([ + { + question: 'Pick one?', + multi_select: false, + options: [{ label: 'A' }, { label: 'B' }], + }, + ]); + const { dialog, collected } = makeDialog(pending); + dialog.handleInput(key); + expect(collected).toEqual([[]]); + }); + it('forwards ctrl+o to the global tool-output toggle without answering', () => { + let toggles = 0; + const pending = makePending([ + { question: 'Q?', multi_select: false, options: [{ label: 'A' }] }, + ]); + const { dialog, collected } = makeDialog(pending, () => toggles++); + dialog.handleInput('\u000F'); // Ctrl+O + expect(toggles).toBe(1); + expect(collected).toEqual([]); + }); + + it('forwards ctrl+e to the global plan-expand toggle without answering', () => { + let planToggles = 0; + const pending = makePending([ + { question: 'Q?', multi_select: false, options: [{ label: 'A' }] }, + ]); + const { dialog, collected } = makeDialog(pending, undefined, () => planToggles++); + dialog.handleInput('\u0005'); // Ctrl+E + expect(planToggles).toBe(1); + expect(collected).toEqual([]); + }); + +}); diff --git a/apps/kimi-code/test/tui/components/dialogs/session-picker.test.ts b/apps/kimi-code/test/tui/components/dialogs/session-picker.test.ts new file mode 100644 index 000000000..c8e919637 --- /dev/null +++ b/apps/kimi-code/test/tui/components/dialogs/session-picker.test.ts @@ -0,0 +1,264 @@ +import { visibleWidth } from '@earendil-works/pi-tui'; +import { afterEach, describe, expect, it, vi } from 'vitest'; + +import { SessionPickerComponent } from '#/tui/components/dialogs/session-picker'; +import { getColorPalette } from '#/tui/theme/colors'; + +function stripAnsi(text: string): string { + return text.replaceAll(/\[[0-?]*[ -/]*[@-~]/g, ''); +} + +function renderPlain(component: SessionPickerComponent, width = 120): string { + return stripAnsi(component.render(width).join('\n')); +} + +describe('SessionPickerComponent', () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('renders millisecond updated_at timestamps as relative times', () => { + const now = new Date('2026-05-11T12:00:00.000Z').getTime(); + vi.spyOn(Date, 'now').mockReturnValue(now); + + const component = new SessionPickerComponent({ + sessions: [ + { + id: 'ses_minutes', + title: 'minutes old', + work_dir: '/tmp/project', + updated_at: now - 2 * 60 * 1000, + }, + { + id: 'ses_hours', + title: 'hours old', + work_dir: '/tmp/project', + updated_at: now - 3 * 60 * 60 * 1000, + }, + ], + loading: false, + currentSessionId: 'ses_other', + colors: getColorPalette('dark'), + onSelect: vi.fn(), + onCancel: vi.fn(), + }); + + const output = renderPlain(component); + + expect(output).toContain('2m ago'); + expect(output).toContain('3h ago'); + expect(output).not.toContain('just now'); + }); + + it('renders title, full session id, work_dir, and last_prompt for each session', () => { + const now = new Date('2026-05-11T12:00:00.000Z').getTime(); + vi.spyOn(Date, 'now').mockReturnValue(now); + + const component = new SessionPickerComponent({ + sessions: [ + { + id: 'ses_01HXYABCDEFGHIJK', + title: 'Refactor sessions list', + last_prompt: 'please redesign the picker UI', + work_dir: '/tmp/project', + updated_at: now - 60 * 1000, + }, + ], + loading: false, + currentSessionId: 'ses_other', + colors: getColorPalette('dark'), + onSelect: vi.fn(), + onCancel: vi.fn(), + }); + + const output = renderPlain(component); + + expect(output).toContain('Refactor sessions list'); + // Session id is rendered in full, never abbreviated with an ellipsis. + expect(output).toContain('ses_01HXYABCDEFGHIJK'); + expect(output).not.toMatch(/ses_01\S*…/); + expect(output).toContain('/tmp/project'); + expect(output).toContain('please redesign the picker UI'); + }); + + it('omits the last-prompt row when last_prompt is missing', () => { + const now = new Date('2026-05-11T12:00:00.000Z').getTime(); + vi.spyOn(Date, 'now').mockReturnValue(now); + + const component = new SessionPickerComponent({ + sessions: [ + { + id: 'ses_no_prompt', + title: 'no prompt yet', + work_dir: '/tmp/project', + updated_at: now - 60 * 1000, + }, + ], + loading: false, + currentSessionId: 'ses_other', + colors: getColorPalette('dark'), + onSelect: vi.fn(), + onCancel: vi.fn(), + }); + + const output = renderPlain(component); + + expect(output).not.toMatch(/^\s*›/m); + }); + + it('truncates overly long last_prompt content', () => { + const now = new Date('2026-05-11T12:00:00.000Z').getTime(); + vi.spyOn(Date, 'now').mockReturnValue(now); + + const longPrompt = 'a'.repeat(500); + const component = new SessionPickerComponent({ + sessions: [ + { + id: 'ses_long', + title: 'long prompt', + last_prompt: longPrompt, + work_dir: '/tmp/project', + updated_at: now - 60 * 1000, + }, + ], + loading: false, + currentSessionId: 'ses_other', + colors: getColorPalette('dark'), + onSelect: vi.fn(), + onCancel: vi.fn(), + }); + + const lines = component.render(60).map((line) => stripAnsi(line)); + const promptLine = lines.find((line) => line.trimStart().startsWith('›')); + expect(promptLine).toBeDefined(); + expect(promptLine!.length).toBeLessThanOrEqual(60); + expect(promptLine!.endsWith('…')).toBe(true); + expect(promptLine).not.toContain(longPrompt); + }); + + it('marks the current session with a (current) badge', () => { + const now = new Date('2026-05-11T12:00:00.000Z').getTime(); + vi.spyOn(Date, 'now').mockReturnValue(now); + + const component = new SessionPickerComponent({ + sessions: [ + { + id: 'ses_current', + title: 'this is current', + work_dir: '/tmp/project', + updated_at: now, + }, + { + id: 'ses_other', + title: 'not current', + work_dir: '/tmp/project', + updated_at: now - 60 * 1000, + }, + ], + loading: false, + currentSessionId: 'ses_current', + colors: getColorPalette('dark'), + onSelect: vi.fn(), + onCancel: vi.fn(), + }); + + const lines = component.render(120).map((line) => stripAnsi(line)); + const currentLine = lines.find((line) => line.includes('this is current')); + const otherLine = lines.find((line) => line.includes('not current')); + expect(currentLine).toContain('(current)'); + expect(otherLine).not.toContain('(current)'); + }); + + it('places the relative time on the same line as the title, not right-aligned', () => { + const now = new Date('2026-05-11T12:00:00.000Z').getTime(); + vi.spyOn(Date, 'now').mockReturnValue(now); + + const component = new SessionPickerComponent({ + sessions: [ + { + id: 'ses_inline_time', + title: 'Short title', + work_dir: '/tmp/project', + updated_at: now - 5 * 60 * 1000, + }, + ], + loading: false, + currentSessionId: 'ses_other', + colors: getColorPalette('dark'), + onSelect: vi.fn(), + onCancel: vi.fn(), + }); + + const lines = component.render(120).map((line) => stripAnsi(line)); + const headerLine = lines.find((line) => line.includes('Short title')); + expect(headerLine).toBeDefined(); + // Title and time sit side-by-side with only the small inline separator. + expect(headerLine).toMatch(/Short title\s{1,4}5m ago/); + // No long run of trailing spaces, i.e. not right-aligned. + expect(headerLine).not.toMatch(/Short title\s{8,}/); + }); + + it('prepends [imported] badge before the title for sessions migrated from kimi-cli', () => { + const now = new Date('2026-05-11T12:00:00.000Z').getTime(); + vi.spyOn(Date, 'now').mockReturnValue(now); + + const component = new SessionPickerComponent({ + sessions: [ + { + id: 'ses_imported', + title: 'Migrated session', + work_dir: '/tmp/project', + updated_at: now - 60 * 1000, + metadata: { imported_from_kimi_cli: true }, + }, + { + id: 'ses_native', + title: 'Fresh session', + work_dir: '/tmp/project', + updated_at: now - 60 * 1000, + }, + ], + loading: false, + currentSessionId: 'ses_other', + colors: getColorPalette('dark'), + onSelect: vi.fn(), + onCancel: vi.fn(), + }); + + const lines = component.render(120).map((line) => stripAnsi(line)); + const importedLine = lines.find((line) => line.includes('Migrated session')); + const nativeLine = lines.find((line) => line.includes('Fresh session')); + expect(importedLine).toContain('[imported] Migrated session'); + expect(nativeLine).not.toContain('[imported]'); + }); + + it('keeps every rendered line within the terminal width even for CJK content', () => { + const now = new Date('2026-05-11T12:00:00.000Z').getTime(); + vi.spyOn(Date, 'now').mockReturnValue(now); + + const component = new SessionPickerComponent({ + sessions: [ + { + id: 'ses_cjk_long_session_id_value', + title: '现在要重构一下 TUI 的 sessions 列表,要渲染几个字段,让 UI 更好看', + last_prompt: + '我们要渲染几个:sessionid title lastPrompt。工作目录,修改时间。需要重新设计下 UI。', + work_dir: '/Users/someone/Desktop/中文目录/very-long-project-folder-name', + updated_at: now - 5 * 60 * 1000, + }, + ], + loading: false, + currentSessionId: 'ses_cjk_long_session_id_value', + colors: getColorPalette('dark'), + onSelect: vi.fn(), + onCancel: vi.fn(), + }); + + for (const width of [40, 80, 120, 238]) { + const lines = component.render(width); + for (const line of lines) { + expect(visibleWidth(line)).toBeLessThanOrEqual(width); + } + } + }); +}); diff --git a/apps/kimi-code/test/tui/components/editor/custom-editor.test.ts b/apps/kimi-code/test/tui/components/editor/custom-editor.test.ts new file mode 100644 index 000000000..58469bf21 --- /dev/null +++ b/apps/kimi-code/test/tui/components/editor/custom-editor.test.ts @@ -0,0 +1,102 @@ +import type { + AutocompleteItem, + AutocompleteProvider, + AutocompleteSuggestions, + TUI, +} from '@earendil-works/pi-tui'; +import { describe, expect, it, vi } from 'vitest'; + +import { CustomEditor } from '#/tui/components/editor/custom-editor'; +import { getColorPalette } from '#/tui/theme/index'; + +function makeEditor(): CustomEditor { + const tui = { + requestRender: vi.fn(), + } as unknown as TUI; + return new CustomEditor(tui, { ...getColorPalette('dark') }); +} + +async function flushAutocomplete(): Promise<void> { + await Promise.resolve(); + await Promise.resolve(); +} + +function providerReturning(items: AutocompleteItem[]): AutocompleteProvider { + return { + getSuggestions: vi.fn(async () => ({ items, prefix: '/' })), + applyCompletion: vi.fn((lines, cursorLine, cursorCol) => ({ lines, cursorLine, cursorCol })), + }; +} + +describe('CustomEditor autocomplete Escape handling', () => { + it('escape closes a visible slash command menu without firing app-level escape', async () => { + const editor = makeEditor(); + const onEscape = vi.fn(); + editor.onEscape = onEscape; + editor.setAutocompleteProvider(providerReturning([{ value: 'help', label: 'help' }])); + + editor.handleInput('/'); + await flushAutocomplete(); + + expect(editor.isShowingAutocomplete()).toBe(true); + + editor.handleInput('\u001B'); + + expect(editor.isShowingAutocomplete()).toBe(false); + expect(onEscape).not.toHaveBeenCalled(); + }); + + it('escape cancels an in-flight slash command menu request', async () => { + const editor = makeEditor(); + const onEscape = vi.fn(); + let resolveSuggestions: (items: AutocompleteItem[]) => void = () => {}; + const provider: AutocompleteProvider = { + getSuggestions: vi.fn( + () => + new Promise<AutocompleteSuggestions | null>((resolve) => { + resolveSuggestions = (items) =>{ resolve({ items, prefix: '/' }); }; + }), + ), + applyCompletion: vi.fn((lines, cursorLine, cursorCol) => ({ lines, cursorLine, cursorCol })), + }; + editor.onEscape = onEscape; + editor.setAutocompleteProvider(provider); + + editor.handleInput('/'); + await flushAutocomplete(); + editor.handleInput('\u001B'); + resolveSuggestions([{ value: 'help', label: 'help' }]); + await flushAutocomplete(); + + expect(editor.isShowingAutocomplete()).toBe(false); + expect(onEscape).not.toHaveBeenCalled(); + }); +}); + +describe('CustomEditor Kitty key release handling', () => { + it('ignores Kitty key release events instead of inserting their CSI-u payload', () => { + const editor = makeEditor(); + + editor.handleInput('\u001B[47;1:3u'); + editor.handleInput('\u001B[110;1:3u'); + + expect(editor.getText()).toBe(''); + }); +}); + +describe('CustomEditor shortcut telemetry hooks', () => { + it('reports newline and undo shortcuts before delegating to the base editor', () => { + const editor = makeEditor(); + const onInsertNewline = vi.fn(); + const onUndo = vi.fn(); + editor.onInsertNewline = onInsertNewline; + editor.onUndo = onUndo; + + editor.handleInput('a'); + editor.handleInput('\n'); + editor.handleInput('\u001F'); + + expect(onInsertNewline).toHaveBeenCalledOnce(); + expect(onUndo).toHaveBeenCalledOnce(); + }); +}); diff --git a/apps/kimi-code/test/tui/components/editor/file-mention-provider.test.ts b/apps/kimi-code/test/tui/components/editor/file-mention-provider.test.ts new file mode 100644 index 000000000..496aa4ce3 --- /dev/null +++ b/apps/kimi-code/test/tui/components/editor/file-mention-provider.test.ts @@ -0,0 +1,215 @@ +import { describe, it, expect } from 'vitest'; + +import { FileMentionProvider } from '#/tui/components/editor/file-mention-provider'; +import type { GitLsFilesCache, GitSnapshot } from '#/utils/git/git-ls-files'; + +function stubGitCache( + files: string[] | null, + opts: { mtimes?: Record<string, number>; recency?: string[] } = {}, +): GitLsFilesCache { + const snapshot: GitSnapshot | null = + files === null + ? null + : { + files, + mtimeByPath: new Map(Object.entries(opts.mtimes ?? {})), + recencyOrder: new Map((opts.recency ?? []).map((p, i) => [p, i])), + }; + return { + isGitRepo: () => files !== null, + getSnapshot: () => snapshot, + list: () => (files === null ? null : files.slice()), + }; +} + +function ctrl(): AbortSignal { + return new AbortController().signal; +} + +const NO_FD = null; + +describe('FileMentionProvider — @ prefix detection + git-backed suggestions', () => { + it('returns null when there is no @ mention and the dir is empty', async () => { + const provider = new FileMentionProvider([], '/nonexistent', NO_FD, stubGitCache([])); + const result = await provider.getSuggestions(['hello world'], 0, 11, { signal: ctrl() }); + // pi-tui inner will also return null for non-path plain text. + expect(result).toBeNull(); + }); + + it('bare @ surfaces the first files as a starting list', async () => { + const files = ['a.ts', 'b.ts', 'src/c.ts']; + const provider = new FileMentionProvider([], '/repo', NO_FD, stubGitCache(files)); + const result = await provider.getSuggestions(['@'], 0, 1, { signal: ctrl() }); + expect(result).not.toBeNull(); + expect(result!.prefix).toBe('@'); + expect(result!.items.map((i) => i.value)).toEqual(['@a.ts', '@b.ts', '@src/c.ts']); + }); + + it('ranks basename-prefix > substring > fuzzy', async () => { + const files = [ + 'docs/readme.md', // basename starts with "read" + 'src/readability.ts', // basename starts with "read" + 'lib/threader.ts', // basename contains "read" (substring) + ]; + const provider = new FileMentionProvider([], '/repo', NO_FD, stubGitCache(files)); + const result = await provider.getSuggestions(['@read'], 0, 5, { signal: ctrl() }); + expect(result).not.toBeNull(); + const values = result!.items.map((i) => i.value); + const readabilityIdx = values.indexOf('@src/readability.ts'); + const readmeIdx = values.indexOf('@docs/readme.md'); + const threadIdx = values.indexOf('@lib/threader.ts'); + // Both starts-with entries rank ahead of the substring entry. + expect(readabilityIdx).toBeGreaterThanOrEqual(0); + expect(readmeIdx).toBeGreaterThanOrEqual(0); + expect(threadIdx).toBeGreaterThan(Math.max(readabilityIdx, readmeIdx)); + }); + + it('empty query prefers recently-edited files over everything else', async () => { + const files = ['a.ts', 'b.ts', 'c.ts', 'd.ts', 'e.ts']; + const provider = new FileMentionProvider( + [], + '/repo', + NO_FD, + stubGitCache(files, { + recency: ['d.ts', 'b.ts'], // d most recent, then b + }), + ); + const result = await provider.getSuggestions(['@'], 0, 1, { signal: ctrl() }); + const values = result!.items.map((i) => i.value); + // Recency layer fills first, then alphabetical layer. + expect(values.slice(0, 2)).toEqual(['@d.ts', '@b.ts']); + expect(values.slice(2)).toEqual(['@a.ts', '@c.ts', '@e.ts']); + }); + + it('empty query falls back to mtime when no recency info', async () => { + const files = ['old.ts', 'newer.ts', 'newest.ts']; + const provider = new FileMentionProvider( + [], + '/repo', + NO_FD, + stubGitCache(files, { + mtimes: { 'old.ts': 1000, 'newer.ts': 2000, 'newest.ts': 3000 }, + }), + ); + const result = await provider.getSuggestions(['@'], 0, 1, { signal: ctrl() }); + const values = result!.items.map((i) => i.value); + expect(values).toEqual(['@newest.ts', '@newer.ts', '@old.ts']); + }); + + it('empty query falls back to basename alphabetical when no signals', async () => { + const files = ['zoo/apple.ts', 'banana.ts', 'cherry.ts']; + const provider = new FileMentionProvider([], '/repo', NO_FD, stubGitCache(files)); + const result = await provider.getSuggestions(['@'], 0, 1, { signal: ctrl() }); + const values = result!.items.map((i) => i.value); + // Sorted by basename alphabetical: apple, banana, cherry + expect(values).toEqual(['@zoo/apple.ts', '@banana.ts', '@cherry.ts']); + }); + + it('hides dot-dir files from the default list', async () => { + const files = ['.agents/skills/x.md', '.github/workflows/y.yml', 'src/a.ts', 'README.md']; + const provider = new FileMentionProvider([], '/repo', NO_FD, stubGitCache(files)); + const result = await provider.getSuggestions(['@'], 0, 1, { signal: ctrl() }); + const values = result!.items.map((i) => i.value); + expect(values).toContain('@README.md'); + expect(values).toContain('@src/a.ts'); + expect(values).not.toContain('@.agents/skills/x.md'); + expect(values).not.toContain('@.github/workflows/y.yml'); + }); + + it('shows dot-dir files when the query explicitly opts in (starts with .)', async () => { + const files = ['.agents/skills/foo.md', '.agents/README.md', 'src/a.ts']; + const provider = new FileMentionProvider([], '/repo', NO_FD, stubGitCache(files)); + const result = await provider.getSuggestions(['@.agents'], 0, 8, { signal: ctrl() }); + expect(result).not.toBeNull(); + const values = result!.items.map((i) => i.value); + expect(values.some((v) => v.startsWith('@.agents/'))).toBe(true); + }); + + it('within a category, recency ranks higher than mtime', async () => { + const files = ['older-recent.ts', 'never-touched-but-new.ts']; + const provider = new FileMentionProvider( + [], + '/repo', + NO_FD, + stubGitCache(files, { + mtimes: { 'older-recent.ts': 1000, 'never-touched-but-new.ts': 9999 }, + recency: ['older-recent.ts'], + }), + ); + // Query hits both via fuzzy (they both contain letters from 'nr'). + // Use basename-startswith shared prefix to force cat 0 tie. + const tied = ['aa-recent.ts', 'aa-newer.ts']; + const provider2 = new FileMentionProvider( + [], + '/repo', + NO_FD, + stubGitCache(tied, { + mtimes: { 'aa-recent.ts': 1000, 'aa-newer.ts': 9999 }, + recency: ['aa-recent.ts'], + }), + ); + const result = await provider2.getSuggestions(['@aa'], 0, 3, { signal: ctrl() }); + const values = result!.items.map((i) => i.value); + expect(values[0]).toBe('@aa-recent.ts'); + expect(values[1]).toBe('@aa-newer.ts'); + void provider; // silence unused + }); + + it('scoped @src/ limits to files under src/', async () => { + const files = ['src/a.ts', 'src/b.ts', 'lib/c.ts']; + const provider = new FileMentionProvider([], '/repo', NO_FD, stubGitCache(files)); + const result = await provider.getSuggestions(['@src/'], 0, 5, { signal: ctrl() }); + expect(result).not.toBeNull(); + const values = result!.items.map((i) => i.value); + // Empty query after src/ shouldn't match lib/c.ts via basename ranking; + // but our git-backed path doesn't apply scope directly — the query is + // "src/" and we fall back to fuzzy on the raw path. Both src/ paths + // contain "src/" and rank higher than lib/c.ts. + expect(values[0]).toMatch(/^@src\//); + expect(values[1]).toMatch(/^@src\//); + }); + + it('does not trigger the @ branch when @ is preceded by a non-delimiter', async () => { + // "email@example" — @ is not at a token boundary; our extractAtPrefix + // returns null and the inner provider handles the text. + const provider = new FileMentionProvider([], '/nonexistent', NO_FD, stubGitCache(['a.ts'])); + const result = await provider.getSuggestions(['email@example'], 0, 13, { signal: ctrl() }); + // Inner provider returns null for this kind of free text. + expect(result).toBeNull(); + }); + + it('handles multiple @ mentions on one line by completing the last one', async () => { + const files = ['alpha.ts', 'beta.ts']; + const provider = new FileMentionProvider([], '/repo', NO_FD, stubGitCache(files)); + // "read @alpha.ts and @bet" — cursor at end, inside the second @. + const line = 'read @alpha.ts and @bet'; + const result = await provider.getSuggestions([line], 0, line.length, { signal: ctrl() }); + expect(result).not.toBeNull(); + expect(result!.prefix).toBe('@bet'); + const values = result!.items.map((i) => i.value); + expect(values).toContain('@beta.ts'); + expect(values).not.toContain('@alpha.ts'); + }); + + it('applyCompletion delegates to inner (replaces prefix with value)', () => { + const provider = new FileMentionProvider([], '/repo', NO_FD, stubGitCache(['src/a.ts'])); + const out = provider.applyCompletion( + ['hey @src'], + 0, + 8, // cursor just after @src + { value: '@src/a.ts', label: 'a.ts' }, + '@src', + ); + // pi-tui appends a trailing space after a non-directory completion + // so the user can type the next token immediately. + expect(out.lines[0]).toBe('hey @src/a.ts '); + }); + + it('falls through to inner when the git cache is null (non-git dir)', async () => { + const provider = new FileMentionProvider([], '/nonexistent', NO_FD, stubGitCache(null)); + // No files visible via readdir either, but it shouldn't throw. + const result = await provider.getSuggestions(['@foo'], 0, 4, { signal: ctrl() }); + // pi-tui readdir on a nonexistent basePath returns [] → null. + expect(result).toBeNull(); + }); +}); diff --git a/apps/kimi-code/test/tui/components/editor/normalize-caps-locked-ctrl.test.ts b/apps/kimi-code/test/tui/components/editor/normalize-caps-locked-ctrl.test.ts new file mode 100644 index 000000000..2d2151f72 --- /dev/null +++ b/apps/kimi-code/test/tui/components/editor/normalize-caps-locked-ctrl.test.ts @@ -0,0 +1,76 @@ +import { describe, it, expect } from 'vitest'; + +import { normalizeCapsLockedCtrl } from '#/tui/components/editor/custom-editor'; + +// Kitty keyboard protocol emits `ESC[<codepoint>;<modifier+1>[:eventType]u`. +// Modifier mask bits: shift=1, alt=2, ctrl=4, super=8, hyper=16, meta=32, +// caps_lock=64, num_lock=128. The pi-tui bug this helper works around is +// that when caps_lock is on, terminals report the codepoint of ctrl+letter +// as the *uppercase* ASCII letter (e.g. 68 = 'D' instead of 100 = 'd'), +// which pi-tui's matcher compares literally and fails. We rewrite the +// sequence back to the unlocked form before dispatching. + +describe('normalizeCapsLockedCtrl', () => { + it('rewrites ctrl+D reported with caps_lock back to ctrl+d', () => { + // ctrl(4) + caps_lock(64) = 68, reported = 69 + expect(normalizeCapsLockedCtrl('\u001B[68;69u')).toBe('\u001B[100;5u'); + }); + + it('rewrites ctrl+C / ctrl+O / ctrl+S under caps_lock', () => { + expect(normalizeCapsLockedCtrl('\u001B[67;69u')).toBe('\u001B[99;5u'); + expect(normalizeCapsLockedCtrl('\u001B[79;69u')).toBe('\u001B[111;5u'); + expect(normalizeCapsLockedCtrl('\u001B[83;69u')).toBe('\u001B[115;5u'); + }); + + it('preserves the trailing event-type sub-parameter', () => { + // `:3u` = key release event + expect(normalizeCapsLockedCtrl('\u001B[68;69:3u')).toBe('\u001B[100;5:3u'); + // multiple sub-parameters still survive + expect(normalizeCapsLockedCtrl('\u001B[68;69:3:1u')).toBe('\u001B[100;5:3:1u'); + }); + + it('leaves plain uppercase typed with only caps_lock alone', () => { + // User intentionally typing an uppercase letter — do not downgrade. + // caps_lock(64) alone = 64, reported = 65 + expect(normalizeCapsLockedCtrl('\u001B[68;65u')).toBe('\u001B[68;65u'); + }); + + it('leaves ctrl+letter without caps_lock alone', () => { + // ctrl(4) alone = 4, reported = 5 + expect(normalizeCapsLockedCtrl('\u001B[68;5u')).toBe('\u001B[68;5u'); + expect(normalizeCapsLockedCtrl('\u001B[100;5u')).toBe('\u001B[100;5u'); + }); + + it('leaves ctrl+shift+letter with caps_lock alone', () => { + // shift(1) + ctrl(4) + caps_lock(64) = 69, reported = 70. + // User explicitly wants the shifted form — don't rewrite. + expect(normalizeCapsLockedCtrl('\u001B[68;70u')).toBe('\u001B[68;70u'); + }); + + it('rewrites ctrl+alt+letter under caps_lock (shift not involved)', () => { + // alt(2) + ctrl(4) + caps_lock(64) = 70, reported = 71 + expect(normalizeCapsLockedCtrl('\u001B[68;71u')).toBe('\u001B[100;7u'); + }); + + it('ignores codepoints outside A-Z even with ctrl+caps_lock', () => { + // Digit '1' (49) — uppercase mapping would be nonsense. + expect(normalizeCapsLockedCtrl('\u001B[49;69u')).toBe('\u001B[49;69u'); + // Symbol '[' (91) — just past 'Z'. + expect(normalizeCapsLockedCtrl('\u001B[91;69u')).toBe('\u001B[91;69u'); + // '@' (64) — just before 'A'. + expect(normalizeCapsLockedCtrl('\u001B[64;69u')).toBe('\u001B[64;69u'); + }); + + it('passes through non-CSI-u input unchanged', () => { + // Plain printable character + expect(normalizeCapsLockedCtrl('H')).toBe('H'); + // Legacy ctrl+d control byte + expect(normalizeCapsLockedCtrl('\u0004')).toBe('\u0004'); + // Legacy ctrl+c control byte + expect(normalizeCapsLockedCtrl('\u0003')).toBe('\u0003'); + // Arrow key CSI (not CSI-u) + expect(normalizeCapsLockedCtrl('\u001B[A')).toBe('\u001B[A'); + // Empty string + expect(normalizeCapsLockedCtrl('')).toBe(''); + }); +}); diff --git a/apps/kimi-code/test/tui/components/editor/prompt-symbol.test.ts b/apps/kimi-code/test/tui/components/editor/prompt-symbol.test.ts new file mode 100644 index 000000000..c7537551d --- /dev/null +++ b/apps/kimi-code/test/tui/components/editor/prompt-symbol.test.ts @@ -0,0 +1,37 @@ +import { describe, it, expect } from 'vitest'; + +import { injectPromptSymbol } from '#/tui/components/editor/custom-editor'; + +describe('injectPromptSymbol', () => { + it('places a "> " prompt at columns 2-3 (col 0 = border, col 1 = single-space gap)', () => { + expect(injectPromptSymbol(' hello world')).toBe(' > hello world'); + }); + + it('preserves overall visible width (prompt occupies padding slots)', () => { + const original = ' hello '; + expect(injectPromptSymbol(original)).toHaveLength(original.length); + }); + + it('preserves trailing ANSI escapes (e.g. cursor inverse marker)', () => { + const line = ' [7m [0m '; + const out = injectPromptSymbol(line); + expect(out).toBe(' > [7m [0m '); + }); + + it('emits no SGR (terminal default foreground renders the symbol)', () => { + const out = injectPromptSymbol(' hello'); + expect(out).not.toMatch(/\[/); + }); + + it('returns undefined when the line is too short', () => { + expect(injectPromptSymbol(' ')).toBeUndefined(); + expect(injectPromptSymbol('')).toBeUndefined(); + }); + + it('returns undefined when the leading four characters are not all spaces', () => { + expect(injectPromptSymbol('x hello')).toBeUndefined(); + expect(injectPromptSymbol(' x hello')).toBeUndefined(); + expect(injectPromptSymbol(' x hello')).toBeUndefined(); + expect(injectPromptSymbol(' xhello')).toBeUndefined(); + }); +}); diff --git a/apps/kimi-code/test/tui/components/editor/side-borders.test.ts b/apps/kimi-code/test/tui/components/editor/side-borders.test.ts new file mode 100644 index 000000000..11801aa71 --- /dev/null +++ b/apps/kimi-code/test/tui/components/editor/side-borders.test.ts @@ -0,0 +1,70 @@ +import { describe, it, expect } from 'vitest'; + +import { wrapWithSideBorders } from '#/tui/components/editor/custom-editor'; + +const id = (s: string): string => s; + +describe('wrapWithSideBorders', () => { + it('turns the top horizontal border into a ╭…╮ run', () => { + const out = wrapWithSideBorders(['──────────', ' hi ', '──────────'], id); + expect(out[0]).toBe('╭────────╮'); + }); + + it('turns the bottom horizontal border into a ╰…╯ run', () => { + const out = wrapWithSideBorders(['──────────', ' hi ', '──────────'], id); + expect(out[2]).toBe('╰────────╯'); + }); + + it('wraps content lines with │ … │, replacing the outer padding columns', () => { + // ' hi ' is 10 chars; first/last spaces become │, middle keeps its length + const out = wrapWithSideBorders(['──────────', ' hi ', '──────────'], id); + expect(out[1]).toBe('│ hi │'); + expect(out[1]).toHaveLength(' hi '.length); + }); + + it('treats scroll-indicator lines (── ↑ N more ──) as horizontal borders', () => { + const top = '─── ↑ 5 more ────'; + const bot = '─── ↓ 3 more ────'; + const out = wrapWithSideBorders([top, ' x ', bot], id); + expect(out[0]?.startsWith('╭')).toBe(true); + expect(out[0]?.endsWith('╮')).toBe(true); + expect(out[2]?.startsWith('╰')).toBe(true); + expect(out[2]?.endsWith('╯')).toBe(true); + // body of the indicator is preserved + expect(out[0]).toContain('↑ 5 more'); + expect(out[2]).toContain('↓ 3 more'); + }); + + it('handles autocomplete rows that come after the bottom border (still wrap with │)', () => { + const lines = [ + '──────────', + ' q ', + '──────────', + ' item1 ', + ' item2 ', + ]; + const out = wrapWithSideBorders(lines, id); + expect(out[0]?.startsWith('╭')).toBe(true); + expect(out[2]?.startsWith('╰')).toBe(true); + // ' item1 ' → first/last spaces become │ + expect(out[3]).toBe('│ item1 │'); + expect(out[4]).toBe('│ item2 │'); + }); + + it('paints corners and side borders through the provided borderColor', () => { + const paint = (s: string): string => `<${s}>`; + const out = wrapWithSideBorders(['─────', ' x ', '─────'], paint); + // corners and horizontals routed through paint + expect(out[0]).toBe('<╭───╮>'); + expect(out[2]).toBe('<╰───╯>'); + // side bars on content lines also painted + expect(out[1]).toBe('<│> x <│>'); + }); + + it('does not clobber non-space content sitting in the outer column (e.g. cursor overflow)', () => { + // last column holds a non-space character — leave it as-is rather than overwrite with │ + const out = wrapWithSideBorders(['─────', ' abc', '─────'], id); + // first column was a space → replaced with │; last column was 'c' → kept + expect(out[1]).toBe('│ abc'); + }); +}); diff --git a/apps/kimi-code/test/tui/components/editor/slash-highlight.test.ts b/apps/kimi-code/test/tui/components/editor/slash-highlight.test.ts new file mode 100644 index 000000000..04826e604 --- /dev/null +++ b/apps/kimi-code/test/tui/components/editor/slash-highlight.test.ts @@ -0,0 +1,66 @@ +import chalk from 'chalk'; +import { describe, it, expect, beforeAll } from 'vitest'; + +import { highlightFirstSlashToken } from '#/tui/components/editor/custom-editor'; + +beforeAll(() => { + // Vitest runs without a TTY so chalk auto-detects colour support as + // 0 (no colours). Force full colour so the highlighter actually + // emits the SGR escapes we're asserting on. + chalk.level = 3; +}); + +function strip(s: string): string { + return s.replaceAll(/\u001B\[[0-9;]*m/g, ''); +} + +describe('highlightFirstSlashToken', () => { + it('colours /cmd when line starts with a slash', () => { + const out = highlightFirstSlashToken(' /help rest of input', '#ff00aa'); + expect(out).toBeDefined(); + // Visible text unchanged + expect(strip(out!)).toBe(' /help rest of input'); + // SGR escapes surround /help + expect(out!).toMatch(/\u001B\[.*m\/help\u001B\[/); + }); + + it('returns undefined when the line has no slash', () => { + expect(highlightFirstSlashToken('hello world', '#ff00aa')).toBeUndefined(); + }); + + it('returns undefined when slash is not at the leading position', () => { + expect(highlightFirstSlashToken(' hello /not-cmd', '#ff00aa')).toBeUndefined(); + }); + + it('returns undefined for path-like slash tokens', () => { + expect(highlightFirstSlashToken('/user/desktop/ foo', '#ff00aa')).toBeUndefined(); + }); + + it('handles /token at end of line (no trailing whitespace)', () => { + const out = highlightFirstSlashToken('/exit', '#ff00aa'); + expect(out).toBeDefined(); + expect(strip(out!)).toBe('/exit'); + }); + + it('passes through pre-existing ANSI (e.g. cursor inverse) in the tail', () => { + // Simulate pi-tui Editor inserting an inverse-video cursor marker + // somewhere after the slash token. + const line = '/help x\u001B[7m \u001B[0m'; + const out = highlightFirstSlashToken(line, '#ff00aa'); + expect(out).toBeDefined(); + // Stripped visible content unchanged + expect(strip(out!)).toBe(strip(line)); + // Inverse cursor SGR is still present afterwards + expect(out!.includes('\u001B[7m')).toBe(true); + }); + + it('only paints the first token, not other slashes further along', () => { + const out = highlightFirstSlashToken('/a /b', '#ff00aa'); + expect(out).toBeDefined(); + // Count the SGR opens — should be exactly one for /a. + const opens = (out!.match(/\u001B\[[0-9;]+m/g) ?? []).length; + expect(opens).toBeGreaterThanOrEqual(2); // chalk hex+bold open and reset(s) + // /b should remain plain — the substring " /b" exists verbatim. + expect(out!).toContain(' /b'); + }); +}); diff --git a/apps/kimi-code/test/tui/components/media/code-highlight.test.ts b/apps/kimi-code/test/tui/components/media/code-highlight.test.ts new file mode 100644 index 000000000..b6df1cf22 --- /dev/null +++ b/apps/kimi-code/test/tui/components/media/code-highlight.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, it } from 'vitest'; + +import { highlightLines, langFromPath } from '#/tui/components/media/code-highlight'; + +import { captureProcessWrite } from '../../../helpers/process'; + +describe('code-highlight', () => { + it('maps known file extensions to supported highlight languages', () => { + expect(langFromPath('src/foo.ts')).toBe('typescript'); + expect(langFromPath('src/foo.TS')).toBe('typescript'); + }); + + it('treats unsupported file extensions as plain text', () => { + expect(langFromPath('src/foo.abcxyz')).toBeUndefined(); + }); + + it('does not call cli-highlight for unsupported languages', () => { + const stderr = captureProcessWrite('stderr'); + try { + expect(highlightLines('hello\nworld', 'abcxyz')).toEqual(['hello', 'world']); + expect(stderr.text()).not.toContain('Could not find the language'); + } finally { + stderr.restore(); + } + }); +}); diff --git a/apps/kimi-code/test/tui/components/media/diff-preview.test.ts b/apps/kimi-code/test/tui/components/media/diff-preview.test.ts new file mode 100644 index 000000000..f2a43aabd --- /dev/null +++ b/apps/kimi-code/test/tui/components/media/diff-preview.test.ts @@ -0,0 +1,180 @@ +import { describe, expect, it } from 'vitest'; + +import { + computeDiffLines, + renderDiffLines, + renderDiffLinesClustered, +} from '#/tui/components/media/diff-preview'; +import { getColorPalette } from '#/tui/theme/colors'; + +const COLORS = getColorPalette('dark'); + +function stripAnsi(text: string): string { + return text.replaceAll(/\u001B\[[0-9;]*m/g, ''); +} + +describe('computeDiffLines', () => { + it('renders a complete diff when isIncomplete is false', () => { + const lines = computeDiffLines('A\nB\nC\nD', 'A\nB', 1, 1, false); + const kinds = lines.map((l) => l.kind); + expect(kinds).toEqual(['context', 'context', 'delete', 'delete']); + }); + + it('suppresses trailing deletes when isIncomplete is true', () => { + const lines = computeDiffLines('A\nB\nC\nD', 'A\nB', 1, 1, true); + const kinds = lines.map((l) => l.kind); + expect(kinds).toEqual(['context', 'context']); + }); + + it('suppresses all deletes when everything would be deleted and incomplete', () => { + const lines = computeDiffLines('A\nB\nC', '', 1, 1, true); + expect(lines).toEqual([]); + }); + + it('keeps trailing adds when isIncomplete is true', () => { + const lines = computeDiffLines('A\nB\nC', 'A\nB\nX', 1, 1, true); + const kinds = lines.map((l) => l.kind); + expect(kinds).toEqual(['context', 'context', 'delete', 'add']); + }); + + it('keeps internal delete blocks that are not trailing', () => { + const lines = computeDiffLines('A\nB\nC\nD', 'A\nC', 1, 1, true); + const kinds = lines.map((l) => l.kind); + expect(kinds).toEqual(['context', 'delete', 'context']); + }); +}); + +describe('renderDiffLines', () => { + it('does not show removed count for suppressed trailing deletes', () => { + const output = renderDiffLines('A\nB\nC\nD', 'A\nB', 'test.ts', COLORS, true, 1, 1); + const text = stripAnsi(output.join('\n')); + expect(text).toContain('test.ts'); + expect(text).not.toContain('-2'); + expect(text).not.toContain('C'); + expect(text).not.toContain('D'); + // When trailing deletes are suppressed, only context lines remain; + // renderDiffLines only emits changed lines, so the body is empty. + expect(text).not.toContain('A'); + expect(text).not.toContain('B'); + }); + + it('shows removed count for complete diffs', () => { + const output = renderDiffLines('A\nB\nC\nD', 'A\nB', 'test.ts', COLORS, false, 1, 1); + const text = stripAnsi(output.join('\n')); + expect(text).toContain('-2'); + expect(text).toContain('C'); + expect(text).toContain('D'); + }); +}); + +describe('renderDiffLinesClustered', () => { + it('renders header with file path and counts', () => { + const out = renderDiffLinesClustered('A\nB\nC', 'A\nX\nC', 'foo.ts', COLORS); + const text = stripAnsi(out[0]!); + expect(text).toContain('+1'); + expect(text).toContain('-1'); + expect(text).toContain('foo.ts'); + }); + + it('returns header only when there are no changes', () => { + const out = renderDiffLinesClustered('A\nB', 'A\nB', 'foo.ts', COLORS); + expect(out).toHaveLength(1); + expect(stripAnsi(out[0]!)).toContain('foo.ts'); + }); + + it('shows context lines around a single change cluster', () => { + // Five lines, change line 3 only — context is 1 each side. + const oldText = ['L1', 'L2', 'L3', 'L4', 'L5'].join('\n'); + const newText = ['L1', 'L2', 'L3X', 'L4', 'L5'].join('\n'); + const text = stripAnsi( + renderDiffLinesClustered(oldText, newText, 'f.ts', COLORS, { contextLines: 1 }).join('\n'), + ); + expect(text).toContain('L2'); + expect(text).toContain('L3'); + expect(text).toContain('L3X'); + expect(text).toContain('L4'); + expect(text).not.toContain('L1'); + expect(text).not.toContain('L5'); + }); + + it('elides unchanged middle between two clusters with a separator', () => { + const oldLines: string[] = []; + for (let i = 1; i <= 30; i++) oldLines.push(`L${String(i)}`); + const newLines = oldLines.slice(); + newLines[1] = 'L2X'; // change near top + newLines[28] = 'L29X'; // change near bottom + const text = stripAnsi( + renderDiffLinesClustered(oldLines.join('\n'), newLines.join('\n'), 'f.ts', COLORS, { + contextLines: 2, + }).join('\n'), + ); + expect(text).toContain('L2X'); + expect(text).toContain('L29X'); + expect(text).toMatch(/… \d+ unchanged lines? …/); + // Middle untouched lines (e.g. L15) should not appear. + expect(text).not.toContain('L15'); + }); + + it('merges nearby change clusters when the gap is within context window', () => { + const oldLines: string[] = []; + for (let i = 1; i <= 10; i++) oldLines.push(`L${String(i)}`); + const newLines = oldLines.slice(); + newLines[2] = 'L3X'; + newLines[5] = 'L6X'; // gap of 2 lines between change indices 2 and 5 → merges with contextLines=2 (mergeGap=4) + const out = renderDiffLinesClustered(oldLines.join('\n'), newLines.join('\n'), 'f.ts', COLORS, { + contextLines: 2, + }).join('\n'); + const text = stripAnsi(out); + expect(text).not.toMatch(/unchanged lines? …/); + expect(text).toContain('L3X'); + expect(text).toContain('L6X'); + }); + + it('emits a partial body even when a single cluster exceeds maxLines', () => { + // Worst case from prod: 100 lines fully replaced inline → single huge + // cluster of ~200 diff entries. With maxLines=10 the renderer must + // still emit ~10 leading body rows, not just the truncation footer. + const oldLines: string[] = []; + const newLines: string[] = []; + for (let i = 1; i <= 100; i++) { + oldLines.push(`old${String(i)}`); + newLines.push(`new${String(i)}`); + } + const out = renderDiffLinesClustered( + oldLines.join('\n'), + newLines.join('\n'), + 'big.ts', + COLORS, + { + contextLines: 3, + maxLines: 10, + }, + ); + // header + 10 body rows + truncation footer + expect(out.length).toBe(12); + const text = stripAnsi(out.join('\n')); + expect(text).toContain('+100'); + expect(text).toContain('-100'); + expect(text).toMatch(/old\d+|new\d+/); + expect(text).toContain('ctrl+o to expand'); + }); + + it('truncates at cluster boundary and appends the ctrl+o footer when maxLines is set', () => { + const oldLines: string[] = []; + for (let i = 1; i <= 50; i++) oldLines.push(`L${String(i)}`); + const newLines = oldLines.slice(); + newLines[1] = 'L2X'; + newLines[20] = 'L21X'; + newLines[40] = 'L41X'; + const text = stripAnsi( + renderDiffLinesClustered(oldLines.join('\n'), newLines.join('\n'), 'f.ts', COLORS, { + contextLines: 2, + maxLines: 6, + }).join('\n'), + ); + expect(text).toContain('L2X'); + expect(text).toMatch(/more change/); + expect(text).toContain('ctrl+o to expand'); + expect(text).not.toContain('L41X'); + }); +}); diff --git a/apps/kimi-code/test/tui/components/messages/assistant-message.test.ts b/apps/kimi-code/test/tui/components/messages/assistant-message.test.ts new file mode 100644 index 000000000..47d0836a9 --- /dev/null +++ b/apps/kimi-code/test/tui/components/messages/assistant-message.test.ts @@ -0,0 +1,53 @@ +import { visibleWidth } from '@earendil-works/pi-tui'; +import { describe, expect, it } from 'vitest'; + +import { AssistantMessageComponent } from '#/tui/components/messages/assistant-message'; +import { STATUS_BULLET } from '#/tui/constant/symbols'; +import { darkColors } from '#/tui/theme/colors'; +import { createMarkdownTheme } from '#/tui/theme/pi-tui-theme'; + +import { captureProcessWrite } from '../../../helpers/process'; + +function strip(text: string): string { + return text.replaceAll(/\u001B\[[0-9;]*m/g, ''); +} + +describe('AssistantMessageComponent', () => { + it('defines the shared status bullet as a stable non-emoji glyph', () => { + expect(STATUS_BULLET).toBe('● '); + expect(visibleWidth(STATUS_BULLET)).toBe(2); + }); + + it('uses the stable status bullet without stealing content width', () => { + const component = new AssistantMessageComponent(createMarkdownTheme(darkColors), darkColors); + + component.updateContent('abcdef'); + + const lines = component.render(8).map(strip); + expect(lines).toEqual(['', `${STATUS_BULLET}abcdef`]); + expect(visibleWidth(lines[1] ?? '')).toBe(8); + }); + + it('renders unknown markdown fence languages as plain text without stderr noise', () => { + const stderr = captureProcessWrite('stderr'); + try { + const theme = createMarkdownTheme(darkColors); + expect(theme.highlightCode?.('hello\nworld', 'abcxyz')).toEqual(['hello', 'world']); + expect(stderr.text()).not.toContain('Could not find the language'); + } finally { + stderr.restore(); + } + }); + + it('preserves literal hook result XML in normal assistant text', () => { + const component = new AssistantMessageComponent(createMarkdownTheme(darkColors), darkColors); + + component.updateContent('<hook_result hook_event="UserPromptSubmit">\n{}\n</hook_result>'); + + const text = component.render(80).map(strip).join('\n'); + expect(text).toContain('<hook_result hook_event="UserPromptSubmit">'); + expect(text).toContain('{}'); + expect(text).toContain('</hook_result>'); + expect(text).not.toContain('UserPromptSubmit hook'); + }); +}); diff --git a/apps/kimi-code/test/tui/components/messages/background-agent-status.test.ts b/apps/kimi-code/test/tui/components/messages/background-agent-status.test.ts new file mode 100644 index 000000000..89def6690 --- /dev/null +++ b/apps/kimi-code/test/tui/components/messages/background-agent-status.test.ts @@ -0,0 +1,56 @@ +import { describe, expect, it } from 'vitest'; + +import { BackgroundAgentStatusComponent } from '#/tui/components/messages/background-agent-status'; +import { STATUS_BULLET } from '#/tui/constant/symbols'; +import { darkColors } from '#/tui/theme/colors'; + +function strip(text: string): string { + return text.replaceAll(/\u001B\[[0-9;]*m/g, ''); +} + +describe('BackgroundAgentStatusComponent', () => { + it('renders started/completed with the shared bullet and failed with a red x marker', () => { + const started = new BackgroundAgentStatusComponent( + { + phase: 'started', + headline: 'explore agent started in background', + detail: 'Explore project structure', + }, + darkColors, + ); + const completed = new BackgroundAgentStatusComponent( + { + phase: 'completed', + headline: 'explore agent completed in background', + detail: 'Explore project structure', + }, + darkColors, + ); + const failed = new BackgroundAgentStatusComponent( + { + phase: 'failed', + headline: 'explore agent failed in background', + detail: 'Explore project structure · boom', + }, + darkColors, + ); + + const startedLines = started.render(120).map((line) => strip(line).trimEnd()); + const completedLines = completed.render(120).map((line) => strip(line).trimEnd()); + const failedLines = failed.render(120).map((line) => strip(line).trimEnd()); + + expect(startedLines[0]).toBe(''); + expect(completedLines[0]).toBe(''); + expect(failedLines[0]).toBe(''); + + expect(startedLines[1]).toBe( + `${STATUS_BULLET}explore agent started in background (Explore project structure)`, + ); + expect(completedLines[1]).toBe( + `${STATUS_BULLET}explore agent completed in background (Explore project structure)`, + ); + expect(failedLines[1]).toBe( + '✗ explore agent failed in background (Explore project structure · boom)', + ); + }); +}); diff --git a/apps/kimi-code/test/tui/components/messages/notice.test.ts b/apps/kimi-code/test/tui/components/messages/notice.test.ts new file mode 100644 index 000000000..a7a9f05e3 --- /dev/null +++ b/apps/kimi-code/test/tui/components/messages/notice.test.ts @@ -0,0 +1,23 @@ +import { describe, expect, it } from 'vitest'; + +import { NoticeMessageComponent } from '#/tui/components/messages/status-message'; +import { darkColors } from '#/tui/theme/colors'; + +function strip(text: string): string { + return text.replaceAll(/\u001B\[[0-9;]*m/g, ''); +} + +describe('NoticeComponent', () => { + it('renders top and bottom spacing around the notice copy', () => { + const component = new NoticeMessageComponent( + 'Plan mode: ON', + 'Plan will be created here: /tmp/plans/test-plan.md', + darkColors, + ); + + const lines = component.render(120).map((line) => strip(line)); + expect(lines[0]).toBe(''); + expect(lines[1]).toContain('Plan mode: ON'); + expect(lines[2]).toContain('Plan will be created here: /tmp/plans/test-plan.md'); + }); +}); diff --git a/apps/kimi-code/test/tui/components/messages/shell-execution.test.ts b/apps/kimi-code/test/tui/components/messages/shell-execution.test.ts new file mode 100644 index 000000000..86e31ded1 --- /dev/null +++ b/apps/kimi-code/test/tui/components/messages/shell-execution.test.ts @@ -0,0 +1,55 @@ +import { describe, expect, it } from 'vitest'; + +import { ShellExecutionComponent } from '#/tui/components/messages/shell-execution'; +import { darkColors } from '#/tui/theme/colors'; + +function strip(text: string): string { + return text.replaceAll(/\u001B\[[0-9;]*m/g, ''); +} + +describe('ShellExecutionComponent', () => { + it('renders shell command previews with prompt indentation', () => { + const component = new ShellExecutionComponent({ + command: 'printf hello\nprintf world', + colors: darkColors, + showCommand: true, + }); + + const output = component.render(100).map((line) => strip(line).trimEnd()); + + expect(output).toContain(' $ printf hello'); + expect(output).toContain(' printf world'); + }); + + it('keeps collapsed shell output short and expands on demand', () => { + const collapsed = new ShellExecutionComponent({ + result: { + tool_call_id: 'call_shell', + output: ['line1', 'line2', 'line3', 'line4', 'line5'].join('\n'), + is_error: false, + }, + colors: darkColors, + }); + + const collapsedOutput = collapsed.render(100).map(strip).join('\n'); + expect(collapsedOutput).toContain('line1'); + expect(collapsedOutput).toContain('line3'); + expect(collapsedOutput).not.toContain('line4'); + expect(collapsedOutput).toContain('... (2 more lines, ctrl+o to expand)'); + + const expanded = new ShellExecutionComponent({ + result: { + tool_call_id: 'call_shell', + output: ['line1', 'line2', 'line3', 'line4', 'line5'].join('\n'), + is_error: false, + }, + colors: darkColors, + expanded: true, + }); + + const expandedOutput = expanded.render(100).map(strip).join('\n'); + expect(expandedOutput).toContain('line4'); + expect(expandedOutput).toContain('line5'); + expect(expandedOutput).not.toContain('ctrl+o to expand'); + }); +}); diff --git a/apps/kimi-code/test/tui/components/messages/status-panel.test.ts b/apps/kimi-code/test/tui/components/messages/status-panel.test.ts new file mode 100644 index 000000000..0fec0bddf --- /dev/null +++ b/apps/kimi-code/test/tui/components/messages/status-panel.test.ts @@ -0,0 +1,97 @@ +import { describe, expect, it } from 'vitest'; + +import { buildStatusReportLines } from '#/tui/components/messages/status-panel'; +import { darkColors } from '#/tui/theme/colors'; + +function strip(text: string): string { + return text.replaceAll(/\u001B\[[0-9;]*m/g, ''); +} + +describe('status panel report lines', () => { + it('formats runtime status, context, and managed usage without account or AGENTS.md rows', () => { + const lines = buildStatusReportLines({ + colors: darkColors, + version: '1.2.3', + model: 'k2', + workDir: '/tmp/project', + sessionId: 'ses-1', + sessionTitle: 'Implement status', + thinking: true, + permissionMode: 'manual', + planMode: false, + contextUsage: 0.25, + contextTokens: 2500, + maxContextTokens: 10000, + availableModels: { + k2: { + provider: 'managed:kimi-code', + model: 'kimi-k2', + maxContextSize: 10000, + displayName: 'Kimi K2', + }, + }, + status: { + model: 'k2', + thinkingLevel: 'high', + permission: 'auto', + planMode: true, + contextTokens: 3000, + maxContextTokens: 12000, + contextUsage: 0.25, + }, + managedUsage: { + summary: null, + limits: [ + { + label: '5h limit', + used: 8, + limit: 100, + resetHint: 'resets in 1h', + }, + ], + }, + }).map(strip); + + const output = lines.join('\n'); + expect(output).toContain('>_ Kimi Code (v1.2.3)'); + expect(output).toContain('Model Kimi K2 (thinking on)'); + expect(output).toContain('Directory /tmp/project'); + expect(output).toContain('Permissions auto'); + expect(output).toContain('Plan mode on'); + expect(output).toContain('Session ses-1'); + expect(output).toContain('Title Implement status'); + expect(output).toContain('Context window'); + expect(output).toContain('25.0%'); + expect(output).toContain('(3.0k / 12.0k)'); + expect(output).toContain('Plan usage'); + expect(output).toContain('92% left'); + expect(output).not.toContain('Account'); + expect(output).not.toContain('AGENTS.md'); + expect(output).not.toContain('Runtime'); + }); + + it('falls back to app state and shows status load errors as warnings', () => { + const lines = buildStatusReportLines({ + colors: darkColors, + version: '1.2.3', + model: '', + workDir: '/tmp/project', + sessionId: '', + sessionTitle: null, + thinking: false, + permissionMode: 'manual', + planMode: false, + contextUsage: 0, + contextTokens: 0, + maxContextTokens: 0, + availableModels: {}, + statusError: 'No active session', + }).map(strip); + + const output = lines.join('\n'); + expect(output).toContain('Model not set'); + expect(output).toContain('Session none'); + expect(output).toContain('Warning No active session'); + expect(output).toContain('No context window data available.'); + }); +}); diff --git a/apps/kimi-code/test/tui/components/messages/thinking.test.ts b/apps/kimi-code/test/tui/components/messages/thinking.test.ts new file mode 100644 index 000000000..9aa0688a8 --- /dev/null +++ b/apps/kimi-code/test/tui/components/messages/thinking.test.ts @@ -0,0 +1,82 @@ +import type { TUI } from '@earendil-works/pi-tui'; +import { describe, expect, it, vi } from 'vitest'; + +import { ThinkingComponent } from '#/tui/components/messages/thinking'; +import { STATUS_BULLET } from '#/tui/constant/symbols'; +import { darkColors } from '#/tui/theme/colors'; + +function strip(text: string): string { + return text.replaceAll(/\u001B\[[0-9;]*m/g, ''); +} + +const longThinking = ['line1', 'line2', 'line3', 'line4', 'line5', 'line6', 'line7'].join('\n'); + +describe('ThinkingComponent', () => { + it('shows the live spinner header before thinking content', () => { + const component = new ThinkingComponent('working it out', darkColors, true, 'live'); + const out = strip(component.render(80).join('\n')); + + expect(out).toContain('⠋ thinking...'); + expect(out).not.toContain(' ⠋ thinking...'); + expect(out).not.toContain(`${STATUS_BULLET}⠋`); + expect(out).toContain(' working it out'); + }); + + it('keeps live thinking height-limited to the tail', () => { + const component = new ThinkingComponent(longThinking, darkColors, true, 'live'); + const out = strip(component.render(80).join('\n')); + + expect(out).not.toContain('line1'); + expect(out).not.toContain('line4'); + expect(out).toContain('line5'); + expect(out).toContain('line7'); + expect(out).not.toContain('ctrl+o to expand'); + }); + + it('animates the live spinner and stops on finalize', () => { + vi.useFakeTimers(); + const requestRender = vi.fn(); + const component = new ThinkingComponent('step', darkColors, true, 'live', { + requestRender, + } as unknown as TUI); + + expect(strip(component.render(80).join('\n'))).toContain('⠋ thinking...'); + + vi.advanceTimersByTime(80); + expect(requestRender).toHaveBeenCalled(); + expect(strip(component.render(80).join('\n'))).toContain('⠙ thinking...'); + + component.finalize(); + requestRender.mockClear(); + vi.advanceTimersByTime(160); + expect(requestRender).not.toHaveBeenCalled(); + vi.useRealTimers(); + }); + + it('finalizes in place into a collapsed preview', () => { + const component = new ThinkingComponent(longThinking, darkColors, true, 'live'); + + component.finalize(); + + const out = strip(component.render(80).join('\n')); + expect(out).toContain('line1'); + expect(out).toContain('line3'); + expect(out).not.toContain('line4'); + expect(out).toContain('... (4 more lines, ctrl+o to expand)'); + }); + + it('expands and collapses after finalization', () => { + const component = new ThinkingComponent(longThinking, darkColors, true, 'live'); + component.finalize(); + + component.setExpanded(true); + const expanded = strip(component.render(80).join('\n')); + expect(expanded).toContain('line7'); + expect(expanded).not.toContain('ctrl+o to expand'); + + component.setExpanded(false); + const collapsed = strip(component.render(80).join('\n')); + expect(collapsed).not.toContain('line7'); + expect(collapsed).toContain('ctrl+o to expand'); + }); +}); diff --git a/apps/kimi-code/test/tui/components/messages/tool-call.test.ts b/apps/kimi-code/test/tui/components/messages/tool-call.test.ts new file mode 100644 index 000000000..a71c6619b --- /dev/null +++ b/apps/kimi-code/test/tui/components/messages/tool-call.test.ts @@ -0,0 +1,1068 @@ +import type { TUI } from '@earendil-works/pi-tui'; +import { afterEach, describe, expect, it, vi } from 'vitest'; + +import { ToolCallComponent } from '#/tui/components/messages/tool-call'; +import { STATUS_BULLET } from '#/tui/constant/symbols'; +import { darkColors } from '#/tui/theme/colors'; +import { createMarkdownTheme } from '#/tui/theme/pi-tui-theme'; + +import { captureProcessWrite } from '../../../helpers/process'; + +const ESC = String.fromCodePoint(0x1b); +const BEL = String.fromCodePoint(0x07); + +function strip(text: string): string { + return text + .replaceAll(/\u001B\[[0-9;]*m/g, '') + .replaceAll(new RegExp(`${ESC}\\]8;;[^${BEL}]*${BEL}`, 'g'), ''); +} + +function stubTui(rows: number): TUI { + return { + terminal: { rows }, + requestRender: () => {}, + } as unknown as TUI; +} + +describe('ToolCallComponent', () => { + afterEach(() => { + vi.useRealTimers(); + }); + + it('uses the shared non-emoji tool status bullet', () => { + const component = new ToolCallComponent( + { + id: 'call_read_marker', + name: 'Read', + args: { path: 'foo.ts' }, + }, + { + tool_call_id: 'call_read_marker', + output: 'content', + is_error: false, + }, + darkColors, + ); + + const out = strip(component.render(100).join('\n')); + expect(out).toContain(`${STATUS_BULLET}Used Read`); + expect(out).not.toContain(`\u23FA Used Read`); + expect(out).not.toContain(`${String.fromCodePoint(0x23fa, 0xfe0e)} Used Read`); + }); + + it('keeps collapsed tool results short and expands on demand', () => { + const component = new ToolCallComponent( + { + id: 'call_shell', + name: 'Bash', + args: { command: 'printf output' }, + }, + { + tool_call_id: 'call_shell', + output: ['line1', 'line2', 'line3', 'line4', 'line5'].join('\n'), + is_error: false, + }, + darkColors, + ); + + const collapsed = strip(component.render(100).join('\n')); + expect(collapsed).toContain('line1'); + expect(collapsed).toContain('line2'); + expect(collapsed).toContain('line3'); + expect(collapsed).not.toContain('line4'); + expect(collapsed).toContain('... (2 more lines, ctrl+o to expand)'); + + component.setExpanded(true); + + const expanded = strip(component.render(100).join('\n')); + expect(expanded).toContain('line4'); + expect(expanded).toContain('line5'); + expect(expanded).not.toContain('ctrl+o to expand'); + }); + + it('hides tool output bodies that start with a <system tag', () => { + const reminderOutput = + '<system-reminder>\nThe task tools have not been used recently.\n</system-reminder>'; + const component = new ToolCallComponent( + { + id: 'call_hidden', + name: 'Bash', + args: { command: 'echo hi' }, + }, + { + tool_call_id: 'call_hidden', + output: reminderOutput, + is_error: false, + }, + darkColors, + ); + + const collapsed = strip(component.render(100).join('\n')); + expect(collapsed).toContain(`${STATUS_BULLET}Used Bash`); + expect(collapsed).not.toContain('system-reminder'); + expect(collapsed).not.toContain('task tools'); + + component.setExpanded(true); + const expanded = strip(component.render(100).join('\n')); + expect(expanded).not.toContain('system-reminder'); + expect(expanded).not.toContain('task tools'); + }); + + it('hides <system-prefixed output even when the tool result is an error', () => { + const component = new ToolCallComponent( + { + id: 'call_hidden_err', + name: 'Bash', + args: { command: 'false' }, + }, + { + tool_call_id: 'call_hidden_err', + output: '<system-reminder>do not show</system-reminder>', + is_error: true, + }, + darkColors, + ); + + const out = strip(component.render(100).join('\n')); + expect(out).not.toContain('system-reminder'); + expect(out).not.toContain('do not show'); + }); + + it('still renders tool output when the body merely contains <system later on', () => { + const component = new ToolCallComponent( + { + id: 'call_inline', + name: 'Bash', + args: { command: 'echo hi' }, + }, + { + tool_call_id: 'call_inline', + output: 'first line\n<system-reminder>nope</system-reminder>', + is_error: false, + }, + darkColors, + ); + + const out = strip(component.render(100).join('\n')); + expect(out).toContain('first line'); + }); + + it('renders ExitPlanMode plan from result output when args.plan is absent', () => { + const component = new ToolCallComponent( + { + id: 'call_exit', + name: 'ExitPlanMode', + args: {}, + }, + { + tool_call_id: 'call_exit', + output: + 'Exited plan mode. Plan mode deactivated. All tools are now available.\n' + + 'Plan saved to: /tmp/plan.md\n\n' + + '## Approved Plan:\n# File Plan\n\n1. Do the focused fix.', + is_error: false, + }, + darkColors, + ); + + const out = strip(component.render(100).join('\n')); + expect(out).toContain('Current plan'); + expect(out).toContain('# File Plan'); + expect(out).toContain('1. Do the focused fix.'); + expect(out).not.toContain('Plan saved to: /tmp/plan.md'); + }); + + it('setPlanInfo injects plan body when args.plan is empty (plan-file mode)', () => { + const component = new ToolCallComponent( + { + id: 'call_exit_async', + name: 'ExitPlanMode', + args: {}, + }, + undefined, + darkColors, + undefined, + createMarkdownTheme(darkColors), + ); + + // A fresh tool card only shows the 'Current plan' title; no plan box renders yet. + const before = strip(component.render(100).join('\n')); + expect(before).toContain('Current plan'); + expect(before).not.toContain('Refactor session'); + + component.setPlanInfo({ plan: '# Refactor session\n\n- step', path: '/tmp/refactor.md' }); + + const after = strip(component.render(100).join('\n')); + expect(after).toContain('Refactor session'); + expect(after).toContain('plan:'); + expect(after).toContain('refactor.md'); + // Directory portion of the path must not leak into the visible header. + expect(after).not.toContain('/tmp/refactor.md'); + }); + + it('caps the plan preview to the terminal height and expands on ctrl+e', () => { + const longPlan = `# Refactor session\n\n${Array.from({ length: 40 }, (_, i) => `- step ${String(i + 1)}`).join('\n')}`; + const component = new ToolCallComponent( + { + id: 'call_exit_long', + name: 'ExitPlanMode', + args: { plan: longPlan }, + }, + undefined, + darkColors, + stubTui(24), + createMarkdownTheme(darkColors), + ); + + const collapsed = strip(component.render(100).join('\n')); + expect(collapsed).toContain('step 1'); + expect(collapsed).toMatch(/\.\.\. \(\d+ more lines, ctrl\+e to expand\)/); + expect(collapsed).not.toContain('step 40'); + + expect(component.setPlanExpanded(true)).toBe(true); + const expanded = strip(component.render(100).join('\n')); + expect(expanded).toContain('step 40'); + expect(expanded).not.toContain('ctrl+e to expand'); + }); + + it('plan preview controls are no-ops for non-ExitPlanMode tool calls', () => { + const component = new ToolCallComponent( + { + id: 'call_bash_plan', + name: 'Bash', + args: { command: 'echo hi' }, + }, + undefined, + darkColors, + undefined, + createMarkdownTheme(darkColors), + ); + + expect(component.setPlanExpanded(true)).toBe(false); + component.setPlanInfo({ plan: 'should be ignored', path: '/etc/hosts' }); + + const out = strip(component.render(100).join('\n')); + expect(out).not.toContain('should be ignored'); + expect(out).not.toContain('plan:'); + }); + + it('ctrl+o does not affect the plan preview cap', () => { + const longPlan = `# P\n\n${Array.from({ length: 40 }, (_, i) => `- step ${String(i + 1)}`).join('\n')}`; + const component = new ToolCallComponent( + { + id: 'call_exit_isolation', + name: 'ExitPlanMode', + args: { plan: longPlan }, + }, + undefined, + darkColors, + stubTui(24), + createMarkdownTheme(darkColors), + ); + component.setExpanded(true); + const out = strip(component.render(100).join('\n')); + expect(out).toContain('ctrl+e to expand'); + expect(out).not.toContain('step 40'); + }); + + it('header chips an Approved status when ExitPlanMode result indicates approval', () => { + const component = new ToolCallComponent( + { + id: 'call_exit_approved', + name: 'ExitPlanMode', + args: {}, + }, + { + tool_call_id: 'call_exit_approved', + output: + 'Exited plan mode. Plan mode deactivated. All tools are now available.\n' + + 'Plan saved to: /tmp/plan.md\n\n' + + '## Approved Plan:\n# Plan body', + is_error: false, + }, + darkColors, + ); + + const header = strip(component.render(100).join('\n')).split('\n')[1] ?? ''; + expect(header).toMatch(/Current plan · Approved\s*$/); + }); + + it('header chips approved option label when the user picked one', () => { + const component = new ToolCallComponent( + { + id: 'call_exit_chosen', + name: 'ExitPlanMode', + args: {}, + }, + { + tool_call_id: 'call_exit_chosen', + output: + 'Exited plan mode. Selected approach: Pragmatic refactor\n' + + 'Execute ONLY the selected approach. Do not execute any unselected alternatives.\n\n' + + 'Plan mode deactivated. All tools are now available.\n' + + 'Plan saved to: /tmp/plan.md\n\n' + + '## Approved Plan:\n# body', + is_error: false, + }, + darkColors, + ); + + const header = strip(component.render(100).join('\n')).split('\n')[1] ?? ''; + expect(header).toContain('Current plan · Approved: Pragmatic refactor'); + }); + + it('header chips Rejected and renders feedback for reject-with-suggestion', () => { + const component = new ToolCallComponent( + { + id: 'call_exit_reject_fb', + name: 'ExitPlanMode', + args: {}, + }, + { + tool_call_id: 'call_exit_reject_fb', + output: 'User rejected the plan. Feedback:\n\nplease rethink step 2', + is_error: false, + }, + darkColors, + ); + + const out = strip(component.render(100).join('\n')); + expect(out).toContain('Current plan · Rejected'); + expect(out).toContain('↪ Suggestion'); + expect(out).toContain('please rethink step 2'); + }); + + it('header chips Rejected without feedback when user rejected outright', () => { + const component = new ToolCallComponent( + { + id: 'call_exit_reject', + name: 'ExitPlanMode', + args: {}, + }, + { + tool_call_id: 'call_exit_reject', + output: 'Plan rejected by user. Plan mode remains active.', + is_error: false, + }, + darkColors, + ); + + const out = strip(component.render(100).join('\n')); + expect(out).toContain('Current plan · Rejected'); + expect(out).not.toContain('Plan mode remains active.'); + }); + + it('suppresses EnterPlanMode success body so prompt scaffolding does not leak into the transcript', () => { + const component = new ToolCallComponent( + { + id: 'call_enter', + name: 'EnterPlanMode', + args: { reason: 'plan a refactor' }, + }, + { + tool_call_id: 'call_enter', + output: + 'Plan mode is now active. Your workflow:\n\n' + + 'Plan file: /tmp/plan.md\n\n' + + '1. Use read-only tools (Read, Grep, Glob) to investigate the codebase.\n' + + '2. Design a concrete, step-by-step plan.\n' + + '3. Write the plan to the plan file with Write or Edit.\n' + + '4. When the plan is ready, call ExitPlanMode for user approval.\n\n' + + 'Do NOT edit files other than the plan file while plan mode is active.', + is_error: false, + }, + darkColors, + ); + + const out = strip(component.render(100).join('\n')); + expect(out).toContain('Used EnterPlanMode'); + expect(out).not.toContain('Plan mode is now active'); + expect(out).not.toContain('Plan file:'); + expect(out).not.toContain('read-only tools'); + }); + + it('still surfaces EnterPlanMode error output', () => { + const component = new ToolCallComponent( + { + id: 'call_enter_err', + name: 'EnterPlanMode', + args: {}, + }, + { + tool_call_id: 'call_enter_err', + output: 'Plan mode is already active. Use ExitPlanMode when the plan is ready.', + is_error: true, + }, + darkColors, + ); + + const out = strip(component.render(100).join('\n')); + expect(out).toContain('Plan mode is already active'); + }); + + it('renders AskUserQuestion with a friendly header instead of the raw tool name', () => { + const component = new ToolCallComponent( + { + id: 'call_question', + name: 'AskUserQuestion', + args: {}, + }, + { + tool_call_id: 'call_question', + output: JSON.stringify({ + answers: { + 'Favorite editor?': 'Vim', + }, + }), + is_error: false, + }, + darkColors, + ); + + const out = strip(component.render(100).join('\n')); + expect(out).toContain('Collected your answers'); + expect(out).toContain('Favorite editor?'); + expect(out).toContain('Vim'); + expect(out).not.toContain('AskUserQuestion'); + }); + + it('appends a chip to the header once a result arrives', () => { + const component = new ToolCallComponent( + { + id: 'call_read', + name: 'Read', + args: { path: 'foo.ts' }, + }, + { + tool_call_id: 'call_read', + output: '1\tfoo\n2\tbar\n3\tbaz', + is_error: false, + }, + darkColors, + ); + + const out = strip(component.render(100).join('\n')); + expect(out).toContain('Used Read'); + expect(out).toContain('· 3 lines'); + }); + + it('truncates a long file path from the head so the filename stays visible', () => { + const longPath = + 'apps/kimi-code/src/tui/components/messages/tool-renderers/long-path/example/final-file.ts'; + const component = new ToolCallComponent( + { + id: 'call_long_path', + name: 'Read', + args: { path: longPath }, + }, + undefined, + darkColors, + ); + + const out = strip(component.render(100).join('\n')); + expect(out).toContain('final-file.ts'); + expect(out).toContain('…'); + expect(out).not.toContain('apps/kimi-code/src/tui/components/messages/tool-renderers/long-pa…'); + }); + + it('shows Read paths relative to the active workspace', () => { + const component = new ToolCallComponent( + { + id: 'call_workspace_read', + name: 'Read', + args: { path: '/tmp/proj-a/apps/kimi-code/src/main.ts' }, + }, + { + tool_call_id: 'call_workspace_read', + output: '1\tcontent', + is_error: false, + }, + darkColors, + undefined, + undefined, + '/tmp/proj-a', + ); + + const out = strip(component.render(100).join('\n')); + expect(out).toContain('Used Read (apps/kimi-code/src/main.ts)'); + expect(out).not.toContain('/tmp/proj-a/apps'); + expect(component.getReadSnapshot().filePath).toBe('apps/kimi-code/src/main.ts'); + }); + + it('keeps Read paths outside the active workspace absolute', () => { + const component = new ToolCallComponent( + { + id: 'call_external_read', + name: 'Read', + args: { path: '/tmp/proj-ab/src/main.ts' }, + }, + undefined, + darkColors, + undefined, + undefined, + '/tmp/proj-a', + ); + + const out = strip(component.render(100).join('\n')); + expect(out).toContain('Using Read (/tmp/proj-ab/src/main.ts)'); + expect(component.getReadSnapshot().filePath).toBe('/tmp/proj-ab/src/main.ts'); + }); + + it('does not append a chip while a tool is still running', () => { + const component = new ToolCallComponent( + { + id: 'call_pending', + name: 'Read', + args: { path: 'foo.ts' }, + }, + undefined, + darkColors, + ); + + const out = strip(component.render(100).join('\n')); + expect(out).toContain('Using Read'); + expect(out).not.toContain('lines'); + }); + + it('renders a single foreground subagent without the generic Agent tool header', () => { + vi.useFakeTimers(); + vi.setSystemTime(10_000); + const component = new ToolCallComponent( + { + id: 'call_agent', + name: 'Agent', + args: { description: 'explore project xxx' }, + }, + undefined, + darkColors, + ); + + component.onSubagentSpawned({ + agentId: 'sub_explore_123456', + agentName: 'explore', + runInBackground: false, + }); + + let out = strip(component.render(120).join('\n')); + expect(out).toContain('Explore Agent Starting (explore project xxx) · 0 tools · 0s'); + expect(out).not.toContain('Using Agent'); + expect(out).not.toContain('Used Agent'); + + vi.setSystemTime(20_000); + component.appendSubagentText('think1\nthink2\nthink3', 'thinking'); + component.appendSubagentText('answer1\nanswer2\nanswer3', 'text'); + component.appendSubToolCall({ + id: 'sub_explore_123456:read', + name: 'Read', + args: { path: 'apps/kimi-code/src/tui/utils/background-agent-status.ts' }, + }); + + out = strip(component.render(120).join('\n')); + expect(out).toContain('Explore Agent Running (explore project xxx) · 1 tool · 10s'); + expect(out).toContain('Using Read (apps/kimi-code/src/tui/utils/background-agent-status.ts)'); + expect(out).not.toContain('think1'); + expect(out).not.toContain('think2'); + expect(out).toContain('think3'); + expect(out).toContain('◌ think3'); + expect(out).not.toContain('answer1'); + expect(out).not.toContain('answer2'); + expect(out).toContain('answer3'); + expect(out).toContain('└ answer3'); + + vi.setSystemTime(22_000); + component.onSubagentCompleted({ resultSummary: 'summary fallback' }); + component.setResult({ + tool_call_id: 'call_agent', + output: 'parent duplicate result', + is_error: false, + }); + vi.setSystemTime(30_000); + + out = strip(component.render(120).join('\n')); + expect(out).toContain('Explore Agent Completed (explore project xxx) · 1 tool · 12s'); + expect(out).not.toContain('think3'); + expect(out).toContain('└ answer3'); + expect(out).not.toContain('Used Agent'); + expect(out).not.toContain('parent duplicate result'); + expect(out).not.toContain('summary fallback'); + }); + + it('keeps the single subagent tool area to the latest four activities', () => { + vi.useFakeTimers(); + vi.setSystemTime(0); + const component = new ToolCallComponent( + { + id: 'call_agent_tools', + name: 'Agent', + args: { description: 'inspect tools' }, + }, + undefined, + darkColors, + ); + component.onSubagentSpawned({ + agentId: 'sub_tools', + agentName: 'explore', + runInBackground: false, + }); + + for (let i = 1; i <= 4; i++) { + const id = `sub_tools:read-${String(i)}`; + component.appendSubToolCall({ id, name: 'Read', args: { path: `file${String(i)}.ts` } }); + component.finishSubToolCall({ tool_call_id: id, output: 'ok', is_error: false }); + } + component.appendSubToolCall({ + id: 'sub_tools:grep', + name: 'Grep', + args: { pattern: 'auth' }, + }); + + const out = strip(component.render(120).join('\n')); + expect(out).toContain('Explore Agent Running (inspect tools) · 5 tools · 0s'); + expect(out).not.toContain('file1.ts'); + expect(out).toContain('Used Read (file2.ts)'); + expect(out).toContain('Used Read (file3.ts)'); + expect(out).toContain('Used Read (file4.ts)'); + expect(out).not.toContain('… Using Grep (auth)'); + expect(out).toContain('• Using Grep (auth)'); + expect(out).toContain('Using Grep (auth)'); + }); + + it('keeps the single subagent tool window stable when older tools update', () => { + vi.useFakeTimers(); + vi.setSystemTime(0); + const component = new ToolCallComponent( + { + id: 'call_agent_stable_tools', + name: 'Agent', + args: { description: 'inspect tools' }, + }, + undefined, + darkColors, + ); + component.onSubagentSpawned({ + agentId: 'sub_tools', + agentName: 'explore', + runInBackground: false, + }); + + for (let i = 1; i <= 5; i++) { + component.appendSubToolCall({ + id: `sub_tools:read-${String(i)}`, + name: 'Read', + args: { path: `file${String(i)}.ts` }, + }); + } + component.appendSubToolCallDelta({ + id: 'sub_tools:read-1', + name: 'Read', + argumentsPart: '{"path":"file1-updated.ts"}', + }); + component.finishSubToolCall({ + tool_call_id: 'sub_tools:read-1', + output: 'ok', + is_error: false, + }); + + const out = strip(component.render(120).join('\n')); + expect(out).not.toContain('file1-updated.ts'); + expect(out).toContain('Using Read (file2.ts)'); + expect(out).toContain('Using Read (file3.ts)'); + expect(out).toContain('Using Read (file4.ts)'); + expect(out).toContain('Using Read (file5.ts)'); + }); + + it('wraps single subagent thinking and output with hanging indentation', () => { + vi.useFakeTimers(); + vi.setSystemTime(0); + const component = new ToolCallComponent( + { + id: 'call_agent_wrapped_text', + name: 'Agent', + args: { description: 'inspect wrapping' }, + }, + undefined, + darkColors, + ); + component.onSubagentSpawned({ + agentId: 'sub_wrapped', + agentName: 'explore', + runInBackground: false, + }); + component.appendSubagentText( + 'thinking words that should wrap with a clean hanging indent', + 'thinking', + ); + component.appendSubagentText( + 'output words that should also wrap with a clean hanging indent', + 'text', + ); + + const lines = strip(component.render(34).join('\n')).split('\n'); + expect(lines).toContain(' ◌ thinking words that should '); + expect(lines).toContain(' wrap with a clean hanging '); + expect(lines).toContain(' indent '); + expect(lines).toContain(' └ output words that should also '); + expect(lines).toContain(' wrap with a clean hanging '); + }); + + it('renders failed single subagents with the dedicated header and error text', () => { + vi.useFakeTimers(); + vi.setSystemTime(1000); + const component = new ToolCallComponent( + { + id: 'call_agent_failed', + name: 'Agent', + args: { description: 'check failure' }, + }, + undefined, + darkColors, + ); + component.onSubagentSpawned({ + agentId: 'sub_failed', + agentName: 'explore', + runInBackground: false, + }); + + vi.setSystemTime(4000); + component.onSubagentFailed({ error: 'subagent exceeded max_steps' }); + + const out = strip(component.render(120).join('\n')); + expect(out).toContain('Explore Agent Failed (check failure) · 0 tools · 3s'); + expect(out).toContain('└ subagent exceeded max_steps'); + expect(out).not.toContain('Using Agent'); + expect(out).not.toContain('Used Agent'); + }); + + it('renders the full Write content during the streaming delta window (no cap)', () => { + const lines: string[] = []; + for (let i = 1; i <= 30; i++) lines.push(`line${String(i)}`); + const escaped = lines.join('\\n'); + const component = new ToolCallComponent( + { + id: 'call_write_stream', + name: 'Write', + args: { file_path: 'foo.ts', content: lines.join('\n') }, + streamingArguments: `{"file_path":"foo.ts","content":"${escaped}`, + }, + undefined, + darkColors, + ); + + const out = strip(component.render(100).join('\n')); + expect(out).toContain('Using Write'); + // All 30 lines must be present — no 10-line cap during streaming. + expect(out).toContain('line1'); + expect(out).toContain('line25'); + expect(out).toContain('line30'); + expect(out).not.toContain('ctrl+o to expand'); + }); + + it('switches a streaming tool call to Truncated when the step ended with max_tokens', () => { + const lines: string[] = []; + for (let i = 1; i <= 10; i++) lines.push(`line${String(i)}`); + const escaped = lines.join('\\n'); + const component = new ToolCallComponent( + { + id: 'call_write_truncated', + name: 'Write', + args: { file_path: 'foo.ts', content: lines.join('\n') }, + streamingArguments: `{"file_path":"foo.ts","content":"${escaped}`, + truncated: true, + }, + undefined, + darkColors, + ); + + const out = strip(component.render(100).join('\n')); + expect(out).toContain('Truncated Write'); + expect(out).not.toContain('Preparing Write'); + expect(out).toContain('Tool call arguments truncated by max_tokens'); + // The live argument preview must NOT render once the call is + // truncated — leaving the half-streamed Write content on screen + // was the original "preparing write" bug. + expect(out).not.toContain('line1'); + expect(out).not.toContain('line10'); + }); + + it('renders a stable Edit progress placeholder during the streaming delta window', () => { + vi.useFakeTimers(); + vi.setSystemTime(4000); + const oldLines: string[] = []; + const newLines: string[] = []; + for (let i = 1; i <= 20; i++) { + oldLines.push(`old${String(i)}`); + newLines.push(`new${String(i)}`); + } + const oldEscaped = oldLines.join('\\n'); + const newEscaped = newLines.join('\\n'); + const streaming = `{"file_path":"foo.ts","old_string":"${oldEscaped}","new_string":"${newEscaped}`; + const component = new ToolCallComponent( + { + id: 'call_edit_stream', + name: 'Edit', + args: { + file_path: 'foo.ts', + old_string: oldLines.join('\n'), + new_string: newLines.join('\n'), + }, + streamingArguments: streaming, + streamingStartedAtMs: 0, + }, + undefined, + darkColors, + ); + + const out = strip(component.render(100).join('\n')); + expect(out).toContain('Using Edit'); + expect(out).toContain('foo.ts'); + expect(out).toContain('Preparing changes for foo.ts...'); + expect(out).toContain('4s elapsed'); + expect(out).toMatch(/\d+(?:\.\d+)? (?:B|KB|MB)/); + expect(out).not.toContain('old20'); + expect(out).not.toContain('new20'); + expect(out).not.toMatch(/^\s*\d+\s+[+-]\s/m); + expect(out).not.toContain('ctrl+o to expand'); + }); + + it('keeps Write preview uncapped between finalized args and result (approval window)', () => { + // The wire sequence is: tool.call.delta → ... → tool.call (final + // args, no streamingArguments) → approval pending ... → tool.result. + // Between tool.call and tool.result the user may sit in an approval + // panel; the preview must stay fully visible during that gap, only + // collapsing once setResult fires (bullet turns green). + const lines: string[] = []; + for (let i = 1; i <= 30; i++) lines.push(`line${String(i)}`); + const component = new ToolCallComponent( + { + id: 'call_write_pending', + name: 'Write', + args: { file_path: 'foo.ts', content: lines.join('\n') }, + // No streamingArguments → finalized args; no result yet. + }, + undefined, + darkColors, + ); + const out = strip(component.render(100).join('\n')); + expect(out).toContain('line1'); + expect(out).toContain('line25'); + expect(out).toContain('line30'); + expect(out).not.toContain('ctrl+o to expand'); + }); + + it('snaps a long Write preview to the collapsed cap when the result arrives', () => { + const lines: string[] = []; + for (let i = 1; i <= 30; i++) lines.push(`line${String(i)}`); + const escaped = lines.join('\\n'); + const component = new ToolCallComponent( + { + id: 'call_write_snap', + name: 'Write', + args: { file_path: 'big.txt', content: lines.join('\n') }, + streamingArguments: `{"file_path":"big.txt","content":"${escaped}"}`, + }, + undefined, + darkColors, + ); + expect(strip(component.render(100).join('\n'))).toContain('line25'); + + component.setResult({ + tool_call_id: 'call_write_snap', + output: 'Wrote big.txt', + is_error: false, + }); + + const after = strip(component.render(100).join('\n')); + expect(after).toContain('line1'); + expect(after).not.toContain('line25'); + expect(after).toContain('ctrl+o to expand'); + }); + + it('refreshes the header when file_path arrives in a later streaming delta', () => { + // First delta: only an opening brace, no file_path yet. + const component = new ToolCallComponent( + { + id: 'call_write_path', + name: 'Write', + args: {}, + streamingArguments: '{', + }, + undefined, + darkColors, + ); + const before = strip(component.render(100).join('\n')); + expect(before).toContain('Using Write'); + expect(before).not.toContain('foo.ts'); + + // Later delta: file_path is now parseable from streamingArguments. + component.updateToolCall({ + id: 'call_write_path', + name: 'Write', + args: { file_path: 'foo.ts' }, + streamingArguments: '{"file_path":"foo.ts","content":"hello', + }); + const after = strip(component.render(100).join('\n')); + expect(after).toContain('foo.ts'); + }); + + it('builds the call preview when finalized args arrive after streaming', () => { + // Mimic the wire sequence: tool.call.delta → ... → tool.call (finalized). + const component = new ToolCallComponent( + { + id: 'call_write_seq', + name: 'Write', + args: { file_path: 'foo.ts', content: 'a\nb' }, + streamingArguments: '{"file_path":"foo.ts","content":"a\\nb', + }, + undefined, + darkColors, + ); + // While streaming, body is rendered live from streamingArguments. + expect(strip(component.render(100).join('\n'))).toMatch(/^\s*1\s+a\s*$/m); + + // Finalized tool.call: streamingArguments is undefined; the body + // re-renders from finalized args, content unchanged. + component.updateToolCall({ + id: 'call_write_seq', + name: 'Write', + args: { file_path: 'foo.ts', content: 'a\nb' }, + }); + const out = strip(component.render(100).join('\n')); + expect(out).toMatch(/^\s*1\s+a\s*$/m); + expect(out).toMatch(/^\s*2\s+b\s*$/m); + }); + + it('builds the Edit diff when finalized args arrive after streaming', () => { + const component = new ToolCallComponent( + { + id: 'call_edit_seq', + name: 'Edit', + args: { file_path: 'foo.ts' }, + streamingArguments: '{"file_path":"foo.ts","old_string":"a\\nb","new_string":"a\\nB', + streamingStartedAtMs: Date.now(), + }, + undefined, + darkColors, + ); + expect(strip(component.render(100).join('\n'))).toContain('Preparing changes'); + expect(strip(component.render(100).join('\n'))).not.toMatch(/^\s*\d+\s+[+-]\s/m); + + component.updateToolCall({ + id: 'call_edit_seq', + name: 'Edit', + args: { file_path: 'foo.ts', old_string: 'a\nb', new_string: 'a\nB' }, + }); + const out = strip(component.render(100).join('\n')); + expect(out).toContain('foo.ts'); + expect(out).toMatch(/^\s*2\s+- b\s*$/m); + expect(out).toMatch(/^\s*2\s+\+ B\s*$/m); + }); + + it('refreshes and stops the Edit streaming progress timer', () => { + vi.useFakeTimers(); + vi.setSystemTime(0); + const ui = { requestRender: vi.fn() }; + const component = new ToolCallComponent( + { + id: 'call_edit_timer', + name: 'Edit', + args: { file_path: 'foo.ts' }, + streamingArguments: '{"file_path":"foo.ts","old_string":"a', + streamingStartedAtMs: 0, + }, + undefined, + darkColors, + ui as never, + ); + + expect(strip(component.render(100).join('\n'))).toContain('0s elapsed'); + vi.advanceTimersByTime(1000); + expect(ui.requestRender).toHaveBeenCalled(); + expect(strip(component.render(100).join('\n'))).toContain('1s elapsed'); + + ui.requestRender.mockClear(); + component.setResult({ + tool_call_id: 'call_edit_timer', + output: 'Replaced 1 occurrence in foo.ts', + is_error: false, + }); + vi.advanceTimersByTime(1000); + expect(ui.requestRender).not.toHaveBeenCalled(); + + const componentToDispose = new ToolCallComponent( + { + id: 'call_edit_dispose', + name: 'Edit', + args: { file_path: 'bar.ts' }, + streamingArguments: '{"file_path":"bar.ts","old_string":"a', + streamingStartedAtMs: 0, + }, + undefined, + darkColors, + ui as never, + ); + ui.requestRender.mockClear(); + componentToDispose.dispose(); + vi.advanceTimersByTime(1000); + expect(ui.requestRender).not.toHaveBeenCalled(); + }); + + it('expands the Write call preview when ctrl+o expansion is set', () => { + const lines: string[] = []; + for (let i = 1; i <= 30; i++) lines.push(`line${String(i)}`); + const component = new ToolCallComponent( + { + id: 'call_write_done', + name: 'Write', + args: { file_path: 'big.txt', content: lines.join('\n') }, + }, + { + tool_call_id: 'call_write_done', + output: 'Wrote big.txt', + is_error: false, + }, + darkColors, + ); + + const collapsed = strip(component.render(100).join('\n')); + expect(collapsed).toContain('line1'); + expect(collapsed).toContain('line10'); + expect(collapsed).not.toContain('line25'); + expect(collapsed).toContain('ctrl+o to expand'); + + component.setExpanded(true); + + const expanded = strip(component.render(100).join('\n')); + expect(expanded).toContain('line25'); + expect(expanded).toContain('line30'); + expect(expanded).not.toContain('ctrl+o to expand'); + }); + + it('renders unknown Write file extensions as plain text without stderr noise', () => { + const stderr = captureProcessWrite('stderr'); + try { + const component = new ToolCallComponent( + { + id: 'call_write_unknown_ext', + name: 'Write', + args: { file_path: 'demo.abcxyz', content: 'hello\nworld' }, + }, + { + tool_call_id: 'call_write_unknown_ext', + output: 'Wrote demo.abcxyz', + is_error: false, + }, + darkColors, + ); + + const collapsed = strip(component.render(100).join('\n')); + expect(collapsed).toContain('hello'); + + component.setExpanded(true); + const expanded = strip(component.render(100).join('\n')); + expect(expanded).toContain('world'); + expect(stderr.text()).not.toContain('Could not find the language'); + } finally { + stderr.restore(); + } + }); +}); diff --git a/apps/kimi-code/test/tui/components/messages/tool-renderers/chip.test.ts b/apps/kimi-code/test/tui/components/messages/tool-renderers/chip.test.ts new file mode 100644 index 000000000..99d253af7 --- /dev/null +++ b/apps/kimi-code/test/tui/components/messages/tool-renderers/chip.test.ts @@ -0,0 +1,124 @@ +import { describe, expect, it } from 'vitest'; + +import { + computeEditStats, + computeWriteStats, + pickChip, +} from '#/tui/components/messages/tool-renderers/chip'; +import type { ToolCallBlockData, ToolResultBlockData } from '#/tui/types'; + +function strip(text: string): string { + return text.replaceAll(/\[[0-9;]*m/g, ''); +} + +function call(name: string, args: Record<string, unknown> = {}): ToolCallBlockData { + return { id: 'tc', name, args }; +} + +function result(output: string, isError = false): ToolResultBlockData { + return { tool_call_id: 'tc', output, is_error: isError }; +} + +function chipFor(name: string, args: Record<string, unknown>, out: ToolResultBlockData): string { + const provider = pickChip(name); + return strip(provider?.(call(name, args), out) ?? ''); +} + +describe('chip registry', () => { + it('Bash has no chip (exit code is not surfaced)', () => { + expect(pickChip('Bash')).toBeUndefined(); + }); + + it('Edit chip shows +N -M from args diff', () => { + const c = chipFor( + 'Edit', + { path: 'foo.ts', old_string: 'a\nb\nc', new_string: 'a\nB\nc\nd' }, + result('Replaced 1 occurrence in foo.ts'), + ); + expect(c).toMatch(/\+\d+/); + expect(c).toMatch(/-\d+/); + }); + + it('Write chip shows N lines from content arg', () => { + expect(chipFor('Write', { path: 'a.txt', content: 'a\nb\nc\n' }, result('Wrote a.txt'))).toBe( + '3 lines', + ); + }); + + it('Read chip shows line count', () => { + expect(chipFor('Read', { path: 'a.ts' }, result('1\tfoo\n2\tbar\n3\tbaz'))).toBe('3 lines'); + }); + + it('Read chip handles single line as singular', () => { + expect(chipFor('Read', { path: 'a.ts' }, result('1\tfoo'))).toBe('1 line'); + }); + + it('Grep chip shows match count', () => { + expect(chipFor('Grep', { pattern: 'foo' }, result('a.ts\nb.ts\nc.ts'))).toBe('3 matches'); + }); + + it('Grep chip says "no matches" on empty result', () => { + expect(chipFor('Grep', { pattern: 'foo' }, result(''))).toBe('no matches'); + }); + + it('Glob chip shows file count', () => { + expect(chipFor('Glob', { pattern: '**/*.ts' }, result('a.ts\nb.ts'))).toBe('2 files'); + }); + + it('FetchURL chip shows size and is non-empty', () => { + const out = chipFor('FetchURL', { url: 'https://example.com' }, result('hello world')); + expect(out).toMatch(/\d+\s*B/); + }); + + it('WebSearch chip shows result count', () => { + expect(chipFor('WebSearch', { query: 'kimi' }, result('1. Alpha\n2. Beta\n3. Gamma'))).toBe( + '3 results', + ); + }); + + it('Think tool has no chip', () => { + expect(pickChip('Think')).toBeUndefined(); + }); + + it('Unknown tools have no chip', () => { + expect(pickChip('SomethingElse')).toBeUndefined(); + }); +}); + +describe('computeWriteStats', () => { + it('returns zero lines for empty content', () => { + expect(computeWriteStats({})).toEqual({ lines: 0 }); + expect(computeWriteStats({ content: '' })).toEqual({ lines: 0 }); + }); + + it('counts a single line with no trailing newline', () => { + expect(computeWriteStats({ content: 'hello' })).toEqual({ lines: 1 }); + }); + + it('ignores trailing newline so "a\\nb\\n" is 2 lines', () => { + expect(computeWriteStats({ content: 'a\nb\n' })).toEqual({ lines: 2 }); + expect(computeWriteStats({ content: 'a\nb' })).toEqual({ lines: 2 }); + }); +}); + +describe('computeEditStats', () => { + it('returns zero when both strings are empty', () => { + expect(computeEditStats({})).toEqual({ added: 0, removed: 0 }); + expect(computeEditStats({ old_string: '', new_string: '' })).toEqual({ + added: 0, + removed: 0, + }); + }); + + it('counts added and removed lines for a replacement', () => { + const stats = computeEditStats({ old_string: 'a\nb\nc', new_string: 'a\nB\nc\nd' }); + expect(stats.added).toBeGreaterThan(0); + expect(stats.removed).toBeGreaterThan(0); + }); + + it('counts only adds when old is empty', () => { + const stats = computeEditStats({ old_string: '', new_string: 'x\ny\nz' }); + expect(stats.added).toBe(3); + expect(stats.removed).toBe(0); + }); +}); diff --git a/apps/kimi-code/test/tui/components/messages/tool-renderers/media.test.ts b/apps/kimi-code/test/tui/components/messages/tool-renderers/media.test.ts new file mode 100644 index 000000000..ab4a53fd6 --- /dev/null +++ b/apps/kimi-code/test/tui/components/messages/tool-renderers/media.test.ts @@ -0,0 +1,150 @@ +import type { Component } from '@earendil-works/pi-tui'; +import { describe, expect, it } from 'vitest'; + +import { + parseReadMediaOutput, + readMediaChip, + readMediaSummary, +} from '#/tui/components/messages/tool-renderers/media'; +import { darkColors } from '#/tui/theme/colors'; +import type { ToolCallBlockData, ToolResultBlockData } from '#/tui/types'; + +function strip(text: string): string { + return text.replaceAll(/\[[0-9;]*m/g, ''); +} + +function joinRender(components: Component[], width = 100): string { + return components.flatMap((c) => c.render(width)).join('\n'); +} + +function call(name: string, args: Record<string, unknown> = {}): ToolCallBlockData { + return { id: 'tc', name, args }; +} + +function result(output: string, isError = false): ToolResultBlockData { + return { tool_call_id: 'tc', output, is_error: isError }; +} + +const ctx = { expanded: false, colors: darkColors }; +const expandedCtx = { expanded: true, colors: darkColors }; + +// 1x1 transparent png base64 (≈70 bytes once decoded) +const PNG_B64 = + 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII='; +const PNG_DATA_URL = `data:image/png;base64,${PNG_B64}`; + +function imageOutput(path: string, b64 = PNG_B64, mime = 'image/png'): string { + return JSON.stringify([ + { type: 'text', text: `<image path="${path}">` }, + { type: 'image_url', imageUrl: { url: `data:${mime};base64,${b64}` } }, + { type: 'text', text: '</image>' }, + { type: 'text', text: `Loaded image file "${path}" (${mime}, 70 bytes, original size 1x1px).` }, + ]); +} + +function videoOutput(path: string, mime = 'video/mp4'): string { + return JSON.stringify([ + { type: 'text', text: `<video path="${path}">` }, + { type: 'video_url', videoUrl: { url: `data:${mime};base64,YWJj` } }, + { type: 'text', text: '</video>' }, + ]); +} + +describe('parseReadMediaOutput', () => { + it('extracts kind, path, mime type, and bytes from an image data URL', () => { + const m = parseReadMediaOutput(imageOutput('/tmp/a.png')); + expect(m).not.toBeNull(); + expect(m?.kind).toBe('image'); + expect(m?.path).toBe('/tmp/a.png'); + expect(m?.mimeType).toBe('image/png'); + expect(m?.bytes).toBeGreaterThan(0); + expect(m?.originalSize).toBe('1x1px'); + }); + + it('extracts video kind and mime', () => { + const m = parseReadMediaOutput(videoOutput('/tmp/a.mp4')); + expect(m?.kind).toBe('video'); + expect(m?.mimeType).toBe('video/mp4'); + }); + + it('captures non-data video URL when uploader was used', () => { + const out = JSON.stringify([ + { type: 'text', text: `<video path="/tmp/a.mp4">` }, + { type: 'video_url', videoUrl: { url: 'https://cdn.example/v/abc' } }, + { type: 'text', text: '</video>' }, + ]); + const m = parseReadMediaOutput(out); + expect(m?.kind).toBe('video'); + expect(m?.url).toBe('https://cdn.example/v/abc'); + expect(m?.bytes).toBeUndefined(); + }); + + it('returns null for non-JSON output', () => { + expect(parseReadMediaOutput('not json')).toBeNull(); + }); + + it('returns null when no media part is present', () => { + expect(parseReadMediaOutput(JSON.stringify([{ type: 'text', text: 'hi' }]))).toBeNull(); + }); +}); + +describe('readMediaChip', () => { + it('returns a compact summary for an image', () => { + const text = strip(readMediaChip(call('ReadMediaFile'), result(imageOutput('/tmp/a.png')))); + expect(text).toMatch(/image/); + expect(text).toContain('image/png'); + expect(text).toMatch(/B|KB|MB/); + }); + + it('returns empty string on error so the truncated body shows the error', () => { + expect(readMediaChip(call('ReadMediaFile'), result('boom', true))).toBe(''); + }); + + it('returns empty string when output is unparseable', () => { + expect(readMediaChip(call('ReadMediaFile'), result('garbage'))).toBe(''); + }); +}); + +describe('readMediaSummary renderer', () => { + it('renders an empty body when collapsed (chip carries the info)', () => { + const out = joinRender( + readMediaSummary(call('ReadMediaFile'), result(imageOutput('/tmp/a.png')), ctx), + ); + expect(out.trim()).toBe(''); + }); + + it('renders path + meta line when expanded — never the base64 blob', () => { + const out = strip( + joinRender( + readMediaSummary(call('ReadMediaFile'), result(imageOutput('/tmp/a.png')), expandedCtx), + ), + ); + expect(out).toContain('/tmp/a.png'); + expect(out).toContain('image/png'); + // Crucially: the base64 must never reach the screen. + expect(out).not.toContain(PNG_B64); + expect(out).not.toContain(PNG_DATA_URL); + }); + + it('falls back to truncated renderer for errors', () => { + const out = strip( + joinRender( + readMediaSummary( + call('ReadMediaFile', { path: '/tmp/x.png' }), + result('File not found', true), + ctx, + ), + ), + ); + expect(out).toContain('File not found'); + }); + + it('falls back to truncated renderer when the output is not the media envelope', () => { + const out = strip( + joinRender( + readMediaSummary(call('ReadMediaFile'), result('"some plain string output"'), ctx), + ), + ); + expect(out).toContain('some plain string output'); + }); +}); diff --git a/apps/kimi-code/test/tui/components/messages/tool-renderers/registry.test.ts b/apps/kimi-code/test/tui/components/messages/tool-renderers/registry.test.ts new file mode 100644 index 000000000..aebe87435 --- /dev/null +++ b/apps/kimi-code/test/tui/components/messages/tool-renderers/registry.test.ts @@ -0,0 +1,165 @@ +import type { Component } from '@earendil-works/pi-tui'; +import { describe, expect, it } from 'vitest'; + +import { pickResultRenderer } from '#/tui/components/messages/tool-renderers/registry'; +import { darkColors } from '#/tui/theme/colors'; +import type { ToolCallBlockData, ToolResultBlockData } from '#/tui/types'; + +function strip(text: string): string { + return text.replaceAll(/\[[0-9;]*m/g, ''); +} + +function joinRender(components: Component[], width = 100): string { + return components.flatMap((c) => c.render(width)).join('\n'); +} + +function call(name: string, args: Record<string, unknown> = {}): ToolCallBlockData { + return { id: 'tc', name, args }; +} + +function result(output: string, isError = false): ToolResultBlockData { + return { tool_call_id: 'tc', output, is_error: isError }; +} + +const ctx = { expanded: false, colors: darkColors }; +const expandedCtx = { expanded: true, colors: darkColors }; + +describe('tool-result registry', () => { + it('falls back to truncated renderer for unknown tools', () => { + const renderer = pickResultRenderer('SomethingUnknown'); + const out = strip(joinRender(renderer(call('SomethingUnknown'), result('a\nb\nc\nd\ne'), ctx))); + expect(out).toContain('a'); + expect(out).toContain('b'); + expect(out).toContain('c'); + expect(out).not.toContain('\nd'); + expect(out).toContain('... (2 more lines, ctrl+o to expand)'); + }); + + it('uses truncated renderer for Bash to preserve raw output UX', () => { + const renderer = pickResultRenderer('Bash'); + const out = strip(joinRender(renderer(call('Bash'), result('one\ntwo\nthree\nfour'), ctx))); + expect(out).toContain('one'); + expect(out).toContain('... (1 more lines, ctrl+o to expand)'); + }); + + it('Read renders no body when collapsed (header chip carries the count)', () => { + const renderer = pickResultRenderer('Read'); + const out = joinRender( + renderer(call('Read', { path: 'foo.ts' }), result('1\tfoo\n2\tbar'), ctx), + ); + expect(out.trim()).toBe(''); + }); + + it('Read expands to the raw file content when expanded', () => { + const renderer = pickResultRenderer('Read'); + const out = strip( + joinRender(renderer(call('Read', { path: 'foo.ts' }), result('1\tfoo\n2\tbar'), expandedCtx)), + ); + expect(out).toContain('foo'); + expect(out).toContain('bar'); + }); + + it('Grep glance lists path samples below the chip', () => { + const renderer = pickResultRenderer('Grep'); + const out = strip( + joinRender( + renderer( + call('Grep', { pattern: 'foo' }), + result('src/a.ts\nsrc/b.ts\nsrc/c.ts\nsrc/d.ts\nsrc/e.ts'), + ctx, + ), + ), + ); + expect(out).toContain('src/a.ts'); + expect(out).toContain('src/b.ts'); + expect(out).toContain('src/c.ts'); + expect(out).toContain('+2 more'); + expect(out).not.toContain('src/d.ts'); + }); + + it('Grep glance strips trailing :line:text in content mode', () => { + const renderer = pickResultRenderer('Grep'); + const out = strip( + joinRender( + renderer( + call('Grep', { pattern: 'foo' }), + result('src/a.ts:42: foo()\nsrc/b.ts:7:foo'), + ctx, + ), + ), + ); + expect(out).toContain('src/a.ts:42'); + expect(out).not.toContain('foo()'); + }); + + it('Grep with empty result renders nothing in collapsed state', () => { + const renderer = pickResultRenderer('Grep'); + const out = joinRender(renderer(call('Grep', { pattern: 'foo' }), result(''), ctx)); + expect(out.trim()).toBe(''); + }); + + it('Glob glance lists path samples', () => { + const renderer = pickResultRenderer('Glob'); + const out = strip( + joinRender( + renderer(call('Glob', { pattern: '**/*.ts' }), result('a.ts\nb.ts\nc.ts\nd.ts'), ctx), + ), + ); + expect(out).toContain('a.ts'); + expect(out).toContain('b.ts'); + expect(out).toContain('c.ts'); + expect(out).toContain('+1 more'); + }); + + it('FetchURL renders no body when collapsed', () => { + const renderer = pickResultRenderer('FetchURL'); + const out = joinRender( + renderer(call('FetchURL', { url: 'https://example.com/x' }), result('<body>...'), ctx), + ); + expect(out.trim()).toBe(''); + }); + + it('WebSearch renders no body when collapsed', () => { + const renderer = pickResultRenderer('WebSearch'); + const out = joinRender( + renderer(call('WebSearch', { query: 'kimi' }), result('1. Alpha\n2. Beta'), ctx), + ); + expect(out.trim()).toBe(''); + }); + + it('Edit renders no body when collapsed', () => { + const renderer = pickResultRenderer('Edit'); + const out = joinRender( + renderer( + call('Edit', { path: 'foo.ts', old_string: 'a', new_string: 'b' }), + result('Replaced 1 occurrence in foo.ts'), + ctx, + ), + ); + expect(out.trim()).toBe(''); + }); + + it('Write renders no body when collapsed', () => { + const renderer = pickResultRenderer('Write'); + const out = joinRender( + renderer(call('Write', { path: 'a.txt', content: 'a\nb\n' }), result('Wrote'), ctx), + ); + expect(out.trim()).toBe(''); + }); + + it('Think renders no body even with a thought arg', () => { + const renderer = pickResultRenderer('Think'); + const out = joinRender(renderer(call('Think', { thought: 'hello' }), result('Recorded.'), ctx)); + expect(out.trim()).toBe(''); + }); + + it('Errors always fall back to truncated renderer regardless of tool', () => { + const renderer = pickResultRenderer('Read'); + const out = strip( + joinRender( + renderer(call('Read', { path: 'foo.ts' }), result('ENOENT: foo.ts not found', true), ctx), + ), + ); + expect(out).toContain('ENOENT: foo.ts not found'); + }); +}); diff --git a/apps/kimi-code/test/tui/components/messages/usage-panel.test.ts b/apps/kimi-code/test/tui/components/messages/usage-panel.test.ts new file mode 100644 index 000000000..e7d6c2c79 --- /dev/null +++ b/apps/kimi-code/test/tui/components/messages/usage-panel.test.ts @@ -0,0 +1,67 @@ +import { visibleWidth } from '@earendil-works/pi-tui'; +import { describe, expect, it } from 'vitest'; + +import { buildUsageReportLines, UsagePanelComponent } from '#/tui/components/messages/usage-panel'; +import { darkColors } from '#/tui/theme/colors'; + +function strip(text: string): string { + return text.replaceAll(/\u001B\[[0-9;]*m/g, ''); +} + +describe('UsagePanelComponent', () => { + it('formats session, context, and managed usage sections', () => { + const lines = buildUsageReportLines({ + colors: darkColors, + sessionUsage: { + byModel: { + kimi: { + inputOther: 1000, + inputCacheRead: 500, + inputCacheCreation: 500, + output: 250, + }, + }, + } as never, + contextUsage: 0.25, + contextTokens: 2500, + maxContextTokens: 10000, + managedUsage: { + summary: { + label: 'daily', + used: 20, + limit: 100, + resetHint: 'resets tomorrow', + }, + limits: [], + }, + }).map(strip); + + expect(lines).toContain('Session usage'); + expect(lines).toContain(' kimi input 2.0k output 250 total 2.3k'); + expect(lines).toContain('Context window'); + expect(lines.join('\n')).toContain('25.0%'); + expect(lines).toContain('Plan usage'); + expect(lines.join('\n')).toContain('80% left'); + expect(lines.join('\n')).toContain('(resets tomorrow)'); + }); + + it('wraps preformatted usage lines in a bordered panel', () => { + const component = new UsagePanelComponent(['Session usage'], darkColors.primary); + const output = component.render(80).map(strip); + + expect(output[0]).toContain(' Usage '); + expect(output[1]).toContain('Session usage'); + }); + + it('truncates lines wider than the terminal so the panel never overflows', () => { + const longLine = 'error: ' + 'x'.repeat(200); + const component = new UsagePanelComponent([longLine], darkColors.primary); + const width = 60; + + const output = component.render(width); + + for (const line of output) { + expect(visibleWidth(line)).toBeLessThanOrEqual(width); + } + }); +}); diff --git a/apps/kimi-code/test/tui/components/messages/user-message.test.ts b/apps/kimi-code/test/tui/components/messages/user-message.test.ts new file mode 100644 index 000000000..ab1d5752b --- /dev/null +++ b/apps/kimi-code/test/tui/components/messages/user-message.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, it } from 'vitest'; + +import { UserMessageComponent } from '#/tui/components/messages/user-message'; +import { darkColors } from '#/tui/theme/colors'; + +function stripAnsi(text: string): string { + return text.replaceAll(/\u001B\[[0-9;]*m/g, ''); +} + +describe('UserMessageComponent', () => { + it('renders video placeholders as plain text, not inline image escapes', () => { + const component = new UserMessageComponent( + 'please inspect [video #1 sample.mov]', + darkColors, + [], + ); + + const out = stripAnsi(component.render(80).join('\n')); + + expect(out).toContain('[video #1 sample.mov]'); + expect(out).not.toContain('\u001B_G'); + expect(out).not.toContain('\u001B]1337;File='); + }); +}); diff --git a/apps/kimi-code/test/tui/components/panels/footer-bg-agents.test.ts b/apps/kimi-code/test/tui/components/panels/footer-bg-agents.test.ts new file mode 100644 index 000000000..e66e7f11f --- /dev/null +++ b/apps/kimi-code/test/tui/components/panels/footer-bg-agents.test.ts @@ -0,0 +1,108 @@ +import { describe, expect, it } from 'vitest'; + +import { FooterComponent } from '#/tui/components/chrome/footer'; +import { darkColors } from '#/tui/theme/colors'; +import type { AppState } from '#/tui/types'; + +const ANSI_SGR = /\[[0-9;]*m/g; +function strip(text: string): string { + return text.replaceAll(ANSI_SGR, ''); +} + +function baseState(overrides: Partial<AppState> = {}): AppState { + return { + model: 'k2', + workDir: '/tmp/proj', + sessionId: 'sess_1', + yolo: false, + permissionMode: 'manual', + planMode: false, + thinking: false, + contextUsage: 0, + contextTokens: 0, + maxContextTokens: 200_000, + isStreaming: false, + isCompacting: false, + isReplaying: false, + streamingPhase: 'idle', + streamingStartTime: 0, + theme: 'dark', + version: 'test', + editorCommand: null, + notifications: { enabled: true, condition: 'unfocused' }, + availableModels: {}, + ...overrides, + } as AppState; +} + +describe('FooterComponent — background task / agent badges', () => { + it('omits both badges when counts are 0', () => { + const footer = new FooterComponent(baseState(), darkColors); + const [line1] = footer.render(120); + expect(line1).toBeDefined(); + expect(strip(line1!)).not.toMatch(/tasks? running/); + expect(strip(line1!)).not.toMatch(/agents? running/); + }); + + it('renders the task badge alone when only bash tasks are running', () => { + const footer = new FooterComponent(baseState(), darkColors); + footer.setBackgroundCounts({ bashTasks: 1, agentTasks: 0 }); + const out = strip(footer.render(120)[0]!); + expect(out).toMatch(/\[1 task running\]/); + expect(out).not.toMatch(/agents? running/); + }); + + it('renders the agent badge alone when only agent tasks are running', () => { + const footer = new FooterComponent(baseState(), darkColors); + footer.setBackgroundCounts({ bashTasks: 0, agentTasks: 1 }); + const out = strip(footer.render(120)[0]!); + expect(out).toMatch(/\[1 agent running\]/); + expect(out).not.toMatch(/tasks? running/); + }); + + it('renders both badges side by side when both are non-zero', () => { + const footer = new FooterComponent(baseState(), darkColors); + footer.setBackgroundCounts({ bashTasks: 2, agentTasks: 3 }); + const out = strip(footer.render(120)[0]!); + expect(out).toMatch(/\[2 tasks running\]/); + expect(out).toMatch(/\[3 agents running\]/); + // Task badge appears before agent badge in the line. + expect(out.indexOf('2 tasks')).toBeLessThan(out.indexOf('3 agents')); + }); + + it('pluralizes correctly across both badges', () => { + const footer = new FooterComponent(baseState(), darkColors); + footer.setBackgroundCounts({ bashTasks: 1, agentTasks: 1 }); + const out = strip(footer.render(120)[0]!); + expect(out).toMatch(/\[1 task running\]/); + expect(out).toMatch(/\[1 agent running\]/); + }); + + it('updates badges live via setBackgroundCounts', () => { + const footer = new FooterComponent(baseState(), darkColors); + footer.setBackgroundCounts({ bashTasks: 2, agentTasks: 1 }); + expect(strip(footer.render(120)[0]!)).toMatch(/\[2 tasks running\]/); + footer.setBackgroundCounts({ bashTasks: 0, agentTasks: 0 }); + const after = strip(footer.render(120)[0]!); + expect(after).not.toMatch(/tasks? running/); + expect(after).not.toMatch(/agents? running/); + }); + + it('clamps negative counts to 0', () => { + const footer = new FooterComponent(baseState(), darkColors); + footer.setBackgroundCounts({ bashTasks: -5, agentTasks: -2 }); + const out = strip(footer.render(120)[0]!); + expect(out).not.toMatch(/tasks? running/); + expect(out).not.toMatch(/agents? running/); + }); + + it('drops the badges when terminal is too narrow to fit them', () => { + const footer = new FooterComponent(baseState(), darkColors); + footer.setBackgroundCounts({ bashTasks: 4, agentTasks: 3 }); + // Extremely narrow width: footer primary content fills the line, so leftLine wins. + const [line1] = footer.render(20); + expect(line1).toBeDefined(); + expect(strip(line1!)).not.toMatch(/\[4 tasks running\]/); + expect(strip(line1!)).not.toMatch(/\[3 agents running\]/); + }); +}); diff --git a/apps/kimi-code/test/tui/components/panels/footer-context.test.ts b/apps/kimi-code/test/tui/components/panels/footer-context.test.ts new file mode 100644 index 000000000..4861b68cd --- /dev/null +++ b/apps/kimi-code/test/tui/components/panels/footer-context.test.ts @@ -0,0 +1,150 @@ +import { describe, it, expect } from 'vitest'; +import chalk from 'chalk'; + +import { FooterComponent, formatFooterGitBadge } from '#/tui/components/chrome/footer'; +import { darkColors } from '#/tui/theme/colors'; +import type { AppState } from '#/tui/types'; + +const ANSI_SGR = /\u001B\[[0-9;]*m/g; +function strip(text: string): string { + return text.replaceAll(ANSI_SGR, ''); +} + +function hexToSgr(hex: string): string { + const value = hex.replace(/^#/, ''); + const r = Number.parseInt(value.slice(0, 2), 16); + const g = Number.parseInt(value.slice(2, 4), 16); + const b = Number.parseInt(value.slice(4, 6), 16); + return `\u001B[38;2;${String(r)};${String(g)};${String(b)}m`; +} + +function baseState(overrides: Partial<AppState> = {}): AppState { + return { + model: 'k2', + workDir: '/tmp', + sessionId: 'sess_1', + yolo: false, + permissionMode: 'manual', + planMode: false, + thinking: false, + contextUsage: 0, + contextTokens: 0, + maxContextTokens: 0, + isStreaming: false, + isCompacting: false, + isReplaying: false, + streamingPhase: 'idle', + streamingStartTime: 0, + theme: 'dark', + version: 'test', + editorCommand: null, + notifications: { enabled: true, condition: 'unfocused' }, + availableModels: {}, + ...overrides, + } as AppState; +} + +describe('FooterComponent — context NaN resilience', () => { + it('NaN usage → renders 0.0% (never literal "NaN%")', () => { + const fc = new FooterComponent(baseState({ contextUsage: Number.NaN }), darkColors); + const out = strip(fc.render(120).join('')); + expect(out).not.toMatch(/NaN/); + expect(out).toMatch(/context: 0\.0%/); + }); + + it('undefined-ish (coerced) usage → renders 0.0%', () => { + const fc = new FooterComponent( + baseState({ contextUsage: undefined as unknown as number }), + darkColors, + ); + const out = strip(fc.render(120).join('')); + expect(out).not.toMatch(/NaN/); + expect(out).toMatch(/context: 0\.0%/); + }); + + it('clamps ratios above 1.0 → renders 100.0%', () => { + const fc = new FooterComponent(baseState({ contextUsage: 1.5 }), darkColors); + const out = strip(fc.render(120).join('')); + expect(out).toMatch(/context: 100\.0%/); + }); + + it('ratio 0.427 → renders 42.7%', () => { + const fc = new FooterComponent(baseState({ contextUsage: 0.427 }), darkColors); + const out = strip(fc.render(200).join('')); + expect(out).toMatch(/context: 42\.7%/); + }); + + it('tokens provided but max=0 → falls back to percent-only, no division-by-zero artefact', () => { + const fc = new FooterComponent( + baseState({ contextUsage: 0, contextTokens: 500, maxContextTokens: 0 }), + darkColors, + ); + const out = strip(fc.render(200).join('')); + expect(out).not.toMatch(/Infinity|NaN/); + expect(out).toMatch(/context: 0\.0%/); + // With maxTokens=0, token-count annotation is suppressed. + expect(out).not.toMatch(/\(500\//); + }); + + it('setState updates visible model and context values', () => { + const footer = new FooterComponent(baseState({ model: 'k2', contextUsage: 0 }), darkColors); + + footer.setState(baseState({ model: 'kimi-k2-5', contextUsage: 0.5 })); + + const out = strip(footer.render(200).join('')); + expect(out).toContain('kimi-k2-5'); + expect(out).not.toContain(' k2 '); + expect(out).toMatch(/context: 50\.0%/); + }); + + it('shows "thinking" label when thinking is enabled, hides it when disabled', () => { + const on = new FooterComponent(baseState({ model: 'k2', thinking: true }), darkColors); + const off = new FooterComponent(baseState({ model: 'k2', thinking: false }), darkColors); + + expect(strip(on.render(120)[0]!)).toContain('thinking'); + expect(strip(off.render(120)[0]!)).not.toContain('thinking'); + }); + + it('renders transient hints on the context line', () => { + const footer = new FooterComponent(baseState(), darkColors); + + footer.setTransientHint('Press Ctrl-C again to exit'); + + const [, line2] = footer.render(120); + expect(strip(line2 ?? '')).toContain('Press Ctrl-C again to exit'); + expect(strip(line2 ?? '')).toContain('context: 0.0%'); + }); + + it('highlights the pull request badge separately from git status text', () => { + const previousLevel = chalk.level; + chalk.level = 3; + try { + const out = formatFooterGitBadge( + { + branch: 'feature/footer', + dirty: false, + ahead: 0, + behind: 0, + diffAdded: 0, + diffDeleted: 0, + pullRequest: { + number: 6, + url: 'https://github.com/acme/repo/pull/6', + }, + }, + darkColors, + ); + + const primaryIndex = out.indexOf(hexToSgr(darkColors.primary)); + const statusIndex = out.indexOf(hexToSgr(darkColors.status)); + const badgeIndex = out.indexOf('[PR#6]'); + expect(statusIndex).toBeGreaterThanOrEqual(0); + expect(primaryIndex).toBeGreaterThanOrEqual(0); + expect(primaryIndex).toBeLessThan(badgeIndex); + expect(strip(out)).toContain('feature/footer '); + expect(strip(out)).toContain('[PR#6]'); + } finally { + chalk.level = previousLevel; + } + }); +}); diff --git a/apps/kimi-code/test/tui/components/panels/help-panel.test.ts b/apps/kimi-code/test/tui/components/panels/help-panel.test.ts new file mode 100644 index 000000000..8b3489899 --- /dev/null +++ b/apps/kimi-code/test/tui/components/panels/help-panel.test.ts @@ -0,0 +1,102 @@ +import { describe, it, expect, vi } from 'vitest'; + +import type { KimiSlashCommand } from '#/tui/commands/index'; +import { HelpPanelComponent } from '#/tui/components/dialogs/help-panel'; +import { darkColors } from '#/tui/theme/colors'; + +function cmd(name: string, description: string, aliases: string[] = []): KimiSlashCommand { + return { + name, + aliases, + description, + }; +} + +function strip(text: string): string { + return text.replaceAll(/\u001B\[[0-9;]*m/g, ''); +} + +describe('HelpPanelComponent', () => { + it('renders keyboard shortcuts + slash commands sections', () => { + const panel = new HelpPanelComponent({ + commands: [cmd('exit', 'Exit', ['quit', 'q'])], + colors: darkColors, + onClose: () => {}, + }); + const out = strip(panel.render(80).join('\n')); + expect(out).toMatch(/help/); + expect(out).toMatch(/Keyboard shortcuts/); + expect(out).toMatch(/Shift-Tab/); + expect(out).toMatch(/Ctrl-O/); + expect(out).toMatch(/Slash commands/); + expect(out).toMatch(/\/exit \(\/quit, \/q\)/); + expect(out).toMatch(/Exit/); + }); + + it('sorts slash commands by name', () => { + const panel = new HelpPanelComponent({ + commands: [cmd('zebra', 'Z'), cmd('alpha', 'A'), cmd('mango', 'M')], + colors: darkColors, + onClose: () => {}, + }); + const out = strip(panel.render(80).join('\n')); + const alphaIdx = out.indexOf('/alpha'); + const mangoIdx = out.indexOf('/mango'); + const zebraIdx = out.indexOf('/zebra'); + expect(alphaIdx).toBeGreaterThan(-1); + expect(alphaIdx).toBeLessThan(mangoIdx); + expect(mangoIdx).toBeLessThan(zebraIdx); + }); + + it('Escape fires onClose', () => { + const onClose = vi.fn(); + const panel = new HelpPanelComponent({ + commands: [], + colors: darkColors, + onClose, + }); + panel.handleInput('\u001B'); // Esc + expect(onClose).toHaveBeenCalledTimes(1); + }); + + it('q / Enter also close the panel', () => { + const onClose = vi.fn(); + const panel = new HelpPanelComponent({ + commands: [], + colors: darkColors, + onClose, + }); + panel.handleInput('q'); + panel.handleInput('\r'); + expect(onClose).toHaveBeenCalledTimes(2); + }); + + it('clips to maxVisible with a "showing X-Y of Z" tail', () => { + const many = Array.from({ length: 30 }, (_, i) => cmd(`cmd${String(i)}`, `Desc ${String(i)}`)); + const panel = new HelpPanelComponent({ + commands: many, + colors: darkColors, + onClose: () => {}, + maxVisible: 6, + }); + const out = strip(panel.render(80).join('\n')); + expect(out).toMatch(/showing 1-6 of/); + }); + + it('arrow keys shift the scroll window', () => { + const many = Array.from({ length: 30 }, (_, i) => cmd(`cmd${String(i)}`, 'd')); + const panel = new HelpPanelComponent({ + commands: many, + colors: darkColors, + onClose: () => {}, + maxVisible: 6, + }); + panel.handleInput('\u001B[B'); // ↓ + panel.handleInput('\u001B[B'); // ↓ + const out = strip(panel.render(80).join('\n')); + expect(out).toMatch(/showing 3-8 of/); + panel.handleInput('\u001B[A'); // ↑ + const out2 = strip(panel.render(80).join('\n')); + expect(out2).toMatch(/showing 2-7 of/); + }); +}); diff --git a/apps/kimi-code/test/tui/components/panels/plan-box.test.ts b/apps/kimi-code/test/tui/components/panels/plan-box.test.ts new file mode 100644 index 000000000..04f1f84bb --- /dev/null +++ b/apps/kimi-code/test/tui/components/panels/plan-box.test.ts @@ -0,0 +1,106 @@ +import { describe, expect, it } from 'vitest'; + +import { PlanBoxComponent } from '#/tui/components/messages/plan-box'; +import { darkColors } from '#/tui/theme/colors'; +import { createMarkdownTheme } from '#/tui/theme/pi-tui-theme'; + +const ESC = String.fromCodePoint(0x1b); +const BEL = String.fromCodePoint(0x07); + +// Strip CSI styling and OSC 8 hyperlink sequences from rendered output so +// visible text can be matched directly. +function strip(text: string): string { + return text + .replaceAll(new RegExp(`${ESC}\\[[0-9;]*m`, 'g'), '') + .replaceAll(new RegExp(`${ESC}\\]8;;[^${BEL}]*${BEL}`, 'g'), ''); +} + +const theme = createMarkdownTheme(darkColors); + +describe('PlanBoxComponent', () => { + it('falls back to bare " plan " title when no path is provided', () => { + const box = new PlanBoxComponent('# Hello', theme, darkColors.success); + const out = strip(box.render(60).join('\n')); + const top = out.split('\n')[0]!; + expect(top).toContain('┌ plan '); + expect(top).not.toContain('plan:'); + }); + + it('renders " plan: <basename> " in the top border without the directory prefix', () => { + const box = new PlanBoxComponent( + '# Hello', + theme, + darkColors.success, + '/tmp/projects/foo/.kimi-code/plans/very-long-slug-name.md', + ); + const out = strip(box.render(80).join('\n')); + const top = out.split('\n')[0]!; + expect(top).toContain(' plan: very-long-slug-name.md '); + expect(top).not.toContain('/tmp/'); + expect(top).not.toContain('…/'); + }); + + it('wraps the basename in an OSC 8 hyperlink targeting file://', () => { + const box = new PlanBoxComponent('# Hello', theme, darkColors.success, '/tmp/plan.md'); + const top = box.render(60)[0]!; + expect(top).toContain(`${ESC}]8;;file:///tmp/plan.md${BEL}plan.md${ESC}]8;;${BEL}`); + // After stripping OSC + CSI, visible width must respect the requested render width. + expect(strip(top).length).toBeLessThanOrEqual(60); + }); + + it('skips the hyperlink for non-absolute paths but still shows the basename', () => { + const box = new PlanBoxComponent('# Hello', theme, darkColors.success, 'relative/plan.md'); + const top = box.render(60)[0]!; + expect(top).not.toContain(`${ESC}]8;`); + expect(strip(top)).toContain(' plan: plan.md '); + }); + + it('degrades to bare " plan " when even the basename does not fit', () => { + const box = new PlanBoxComponent('# Hello', theme, darkColors.success, '/tmp/plan.md'); + const out = strip(box.render(14).join('\n')); + const top = out.split('\n')[0]!; + expect(top).toContain(' plan '); + expect(top).not.toContain('plan:'); + }); + + it('renders all lines when content fits under maxContentLines', () => { + const plan = Array.from({ length: 5 }, (_, i) => `- step ${String(i + 1)}`).join('\n'); + const box = new PlanBoxComponent(plan, theme, darkColors.success, undefined, { + maxContentLines: 20, + }); + const out = strip(box.render(80).join('\n')); + expect(out).toContain('step 1'); + expect(out).toContain('step 5'); + expect(out).not.toContain('ctrl+e to expand'); + }); + + it('truncates content over maxContentLines with a footer inside the box', () => { + const plan = Array.from({ length: 30 }, (_, i) => `- step ${String(i + 1)}`).join('\n'); + const box = new PlanBoxComponent(plan, theme, darkColors.success, undefined, { + maxContentLines: 10, + }); + const rendered = box.render(80); + const out = strip(rendered.join('\n')); + expect(out).toContain('step 1'); + expect(out).toContain('step 9'); + expect(out).not.toContain('step 10'); + expect(out).toMatch(/\.\.\. \(\d+ more lines, ctrl\+e to expand\)/); + // Footer must live inside the bordered box, not after it. + const footerIdx = rendered.findIndex((line) => strip(line).includes('ctrl+e to expand')); + const bottomIdx = rendered.findIndex((line) => strip(line).includes('└')); + expect(footerIdx).toBeGreaterThan(-1); + expect(footerIdx).toBeLessThan(bottomIdx); + expect(strip(rendered[footerIdx]!)).toMatch(/^\s+│.*│\s*$/); + }); + + it('renders the full plan when expanded is true, ignoring maxContentLines', () => { + const plan = Array.from({ length: 30 }, (_, i) => `- step ${String(i + 1)}`).join('\n'); + const box = new PlanBoxComponent(plan, theme, darkColors.success, undefined, { + maxContentLines: 10, + expanded: true, + }); + const out = strip(box.render(80).join('\n')); + expect(out).toContain('step 30'); + expect(out).not.toContain('ctrl+e to expand'); + }); +}); diff --git a/apps/kimi-code/test/tui/components/panels/todo-panel.test.ts b/apps/kimi-code/test/tui/components/panels/todo-panel.test.ts new file mode 100644 index 000000000..373d2d0c4 --- /dev/null +++ b/apps/kimi-code/test/tui/components/panels/todo-panel.test.ts @@ -0,0 +1,58 @@ +import { describe, it, expect } from 'vitest'; + +import { TodoPanelComponent, type TodoItem } from '#/tui/components/chrome/todo-panel'; +import { darkColors } from '#/tui/theme/colors'; + +function strip(text: string): string { + return text.replaceAll(/\u001B\[[0-9;]*m/g, ''); +} + +describe('TodoPanelComponent', () => { + it('returns no lines when empty (so the layout slot collapses)', () => { + const panel = new TodoPanelComponent(darkColors); + expect(panel.render(80)).toEqual([]); + expect(panel.isEmpty()).toBe(true); + }); + + it('renders a Todo header + one row per entry', () => { + const panel = new TodoPanelComponent(darkColors); + panel.setTodos([ + { title: 'Investigate parser', status: 'done' }, + { title: 'Add tests', status: 'in_progress' }, + { title: 'Open PR', status: 'pending' }, + ]); + const lines = panel.render(80).map(strip); + const joined = lines.join('\n'); + expect(joined).toMatch(/Todo/); + expect(joined).toMatch(/✓ Investigate parser/); + expect(joined).toMatch(/● Add tests/); + expect(joined).toMatch(/○ Open PR/); + }); + + it('setTodos replaces the list (not appends)', () => { + const panel = new TodoPanelComponent(darkColors); + panel.setTodos([{ title: 'old', status: 'pending' }]); + panel.setTodos([{ title: 'new', status: 'in_progress' }]); + const out = strip(panel.render(80).join('\n')); + expect(out).toMatch(/● new/); + expect(out).not.toMatch(/old/); + }); + + it('clear() wipes the list and reverts to empty', () => { + const panel = new TodoPanelComponent(darkColors); + panel.setTodos([{ title: 'x', status: 'pending' }]); + panel.clear(); + expect(panel.isEmpty()).toBe(true); + expect(panel.render(80)).toEqual([]); + }); + + it('defensive copy: external mutation does not leak into the panel', () => { + const panel = new TodoPanelComponent(darkColors); + const source: TodoItem[] = [{ title: 'foo', status: 'pending' }]; + panel.setTodos(source); + source[0] = { title: 'hacked', status: 'done' }; + const out = strip(panel.render(80).join('\n')); + expect(out).toMatch(/○ foo/); + expect(out).not.toMatch(/hacked/); + }); +}); diff --git a/apps/kimi-code/test/tui/components/panes/activity-pane.test.ts b/apps/kimi-code/test/tui/components/panes/activity-pane.test.ts new file mode 100644 index 000000000..383c43693 --- /dev/null +++ b/apps/kimi-code/test/tui/components/panes/activity-pane.test.ts @@ -0,0 +1,29 @@ +import { Text } from '@earendil-works/pi-tui'; +import { describe, expect, it } from 'vitest'; + +import { ActivityPaneComponent } from '#/tui/components/panes/activity-pane'; + +describe('ActivityPaneComponent', () => { + it('renders waiting loader after a spacer', () => { + const component = new ActivityPaneComponent({ + mode: 'waiting', + spinner: new Text('loading', 0, 0) as never, + }); + + expect(component.render(80).map((line) => line.trimEnd())).toEqual(['', 'loading']); + }); + + it('renders composing spinner after a spacer', () => { + const component = new ActivityPaneComponent({ + mode: 'composing', + spinner: new Text('working', 0, 0) as never, + }); + + expect(component.render(80).map((line) => line.trimEnd())).toEqual(['', 'working']); + }); + + it('renders nothing for hidden and thinking modes', () => { + expect(new ActivityPaneComponent({ mode: 'hidden' }).render(80)).toEqual([]); + expect(new ActivityPaneComponent({ mode: 'thinking' }).render(80)).toEqual([]); + }); +}); diff --git a/apps/kimi-code/test/tui/components/panes/queue-pane.test.ts b/apps/kimi-code/test/tui/components/panes/queue-pane.test.ts new file mode 100644 index 000000000..fbb5e4979 --- /dev/null +++ b/apps/kimi-code/test/tui/components/panes/queue-pane.test.ts @@ -0,0 +1,58 @@ +import { describe, expect, it } from 'vitest'; + +import { QueuePaneComponent } from '#/tui/components/panes/queue-pane'; +import { darkColors } from '#/tui/theme/colors'; + +function stripAnsi(text: string): string { + return text.replaceAll(/\u001B\[[0-9;]*m/g, ''); +} + +describe('QueuePaneComponent', () => { + it('renders queued messages with the steer hint', () => { + const component = new QueuePaneComponent({ + colors: darkColors, + isCompacting: false, + isStreaming: true, + canSteerImmediately: true, + messages: [ + { text: 'first message' }, + { text: '/skill:review src/app.ts' }, + ], + }); + + const output = stripAnsi(component.render(120).join('\n')); + + expect(output).toContain('❯ first message'); + expect(output).toContain('❯ /skill:review src/app.ts'); + expect(output).toContain('ctrl-s to steer immediately'); + }); + + it('renders compaction hint when waiting for compaction', () => { + const component = new QueuePaneComponent({ + colors: darkColors, + isCompacting: true, + isStreaming: false, + canSteerImmediately: true, + messages: [{ text: 'after compact' }], + }); + + const output = stripAnsi(component.render(120).join('\n')); + + expect(output).toContain('will send after compaction'); + }); + + it('omits the steer hint when immediate steering is disabled', () => { + const component = new QueuePaneComponent({ + colors: darkColors, + isCompacting: false, + isStreaming: true, + canSteerImmediately: false, + messages: [{ text: 'after init' }], + }); + + const output = stripAnsi(component.render(120).join('\n')); + + expect(output).toContain('will send after current task'); + expect(output).not.toContain('ctrl-s to steer immediately'); + }); +}); diff --git a/apps/kimi-code/test/tui/config.test.ts b/apps/kimi-code/test/tui/config.test.ts new file mode 100644 index 000000000..a7ddf4509 --- /dev/null +++ b/apps/kimi-code/test/tui/config.test.ts @@ -0,0 +1,111 @@ +import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; + +import { + DEFAULT_TUI_CONFIG, + INVALID_TUI_CONFIG_MESSAGE, + loadTuiConfig, + parseTuiConfig, + saveTuiConfig, + TuiConfigParseError, +} from '#/tui/config'; + +let dir: string; +let filePath: string; + +beforeEach(() => { + dir = join(tmpdir(), `kimi-tui-config-${Date.now()}-${Math.random().toString(36).slice(2)}`); + mkdirSync(dir, { recursive: true }); + filePath = join(dir, 'tui.toml'); +}); + +afterEach(() => { + rmSync(dir, { recursive: true, force: true }); +}); + +describe('TUI config', () => { + it('creates the default config when the file does not exist', async () => { + const result = await loadTuiConfig(filePath); + + expect(result).toEqual(DEFAULT_TUI_CONFIG); + const text = readFileSync(filePath, 'utf-8'); + expect(text).toContain('Terminal UI preferences for kimi-code.'); + expect(text).toContain('theme = "auto"'); + expect(text).toContain('command = ""'); + expect(text).toContain('[notifications]'); + expect(text).toContain('enabled = true'); + expect(text).toContain('notification_condition = "unfocused"'); + }); + + it('parses valid TOML', () => { + const config = parseTuiConfig(` +theme = "light" + +[editor] +command = "code --wait" + +[notifications] +enabled = false +notification_condition = "always" +`); + + expect(config).toEqual({ + theme: 'light', + editorCommand: 'code --wait', + notifications: { enabled: false, condition: 'always' }, + }); + }); + + it('normalizes an empty editor command to auto-detect', () => { + const config = parseTuiConfig(` +[editor] +command = " " +`); + + expect(config).toEqual({ + theme: 'auto', + editorCommand: null, + notifications: { enabled: true, condition: 'unfocused' }, + }); + }); + + it('falls back to default notifications when the section is omitted', () => { + const config = parseTuiConfig(`theme = "dark"`); + + expect(config.notifications).toEqual({ enabled: true, condition: 'unfocused' }); + }); + + it('throws TuiConfigParseError with fallback when parsing fails, leaving the file untouched', async () => { + writeFileSync(filePath, '[[[', 'utf-8'); + + const error = await loadTuiConfig(filePath).then( + () => null, + (error: unknown) => error, + ); + + expect(error).toBeInstanceOf(TuiConfigParseError); + expect((error as TuiConfigParseError).message).toBe(INVALID_TUI_CONFIG_MESSAGE); + expect((error as TuiConfigParseError).fallback).toEqual(DEFAULT_TUI_CONFIG); + expect(readFileSync(filePath, 'utf-8')).toBe('[[['); + }); + + it('saves and reloads the normalized config', async () => { + await saveTuiConfig( + { + theme: 'light', + editorCommand: 'vim', + notifications: { enabled: false, condition: 'always' }, + }, + filePath, + ); + + expect(await loadTuiConfig(filePath)).toEqual({ + theme: 'light', + editorCommand: 'vim', + notifications: { enabled: false, condition: 'always' }, + }); + }); +}); diff --git a/apps/kimi-code/test/tui/create-tui-state.test.ts b/apps/kimi-code/test/tui/create-tui-state.test.ts new file mode 100644 index 000000000..4da743841 --- /dev/null +++ b/apps/kimi-code/test/tui/create-tui-state.test.ts @@ -0,0 +1,103 @@ + +import { describe, it, expect } from 'vitest'; + +import { createTUIState, type KimiTUIOptions } from '#/tui/kimi-tui'; +import type { AppState } from '#/tui/types'; + +function fakeInitialAppState(): AppState { + return { + model: 'test-model', + workDir: '/tmp/kimi-test', + sessionId: 'sess-1', + yolo: false, + permissionMode: 'manual', + planMode: false, + thinking: false, + contextUsage: 0, + contextTokens: 0, + maxContextTokens: 0, + isStreaming: false, + isCompacting: false, + isReplaying: false, + streamingPhase: 'idle', + streamingStartTime: 0, + theme: 'dark', + version: '0.0.0-test', + editorCommand: null, + notifications: { enabled: true, condition: 'unfocused' }, + availableModels: {}, + availableProviders: {}, + sessionTitle: null, + }; +} + +describe('createTUIState', () => { + it('initializes all fields with sensible defaults', () => { + const opts: KimiTUIOptions = { + initialAppState: fakeInitialAppState(), + startup: { + continueLast: false, + yolo: false, + plan: false, + }, + }; + const state = createTUIState(opts); + + // UI objects are created. + expect(state.ui).toBeDefined(); + expect(state.terminal).toBeDefined(); + expect(state.transcriptContainer).toBeDefined(); + expect(state.activityContainer).toBeDefined(); + expect(state.todoPanelContainer).toBeDefined(); + expect(state.queueContainer).toBeDefined(); + expect(state.editorContainer).toBeDefined(); + expect(state.editor).toBeDefined(); + expect(state.footer).toBeDefined(); + expect(state.todoPanel).toBeDefined(); + expect(state.theme.colors).toBeDefined(); + expect(state.theme.markdownTheme).toBeDefined(); + + // App state is cloned from initialAppState, not reused by reference. + expect(state.appState).not.toBe(opts.initialAppState); + expect(state.appState.model).toBe('test-model'); + expect(state.appState.sessionId).toBe('sess-1'); + expect(state.startupState).toBe('pending'); + expect(state.startupNotice).toBeUndefined(); + + // LivePane defaults. + expect(state.livePane.mode).toBe('idle'); + expect(state.livePane.pendingApproval).toBeNull(); + expect(state.livePane.pendingQuestion).toBeNull(); + + // Empty collections. + expect(state.transcriptEntries).toHaveLength(0); + expect(state.queuedMessages).toHaveLength(0); + expect(state.pendingToolComponents.size).toBe(0); + expect(state.activeToolCalls.size).toBe(0); + expect(state.streamingToolCallArguments.size).toBe(0); + expect(state.backgroundAgents.size).toBe(0); + expect(state.backgroundAgentMetadata.size).toBe(0); + expect(state.renderedSkillActivationIds.size).toBe(0); + + // Boolean, counter, and optional-field defaults. + expect(state.toolOutputExpanded).toBe(false); + expect(state.showingSessionPicker).toBe(false); + expect(state.showingHelpPanel).toBe(false); + expect(state.externalEditorRunning).toBe(false); + expect(state.loadingSessions).toBe(false); + expect(state.currentTurnId).toBeUndefined(); + expect(state.currentStep).toBe(0); + expect(state.assistantStreamActive).toBe(false); + expect(state.assistantDraft).toBe(''); + expect(state.thinkingDraft).toBe(''); + expect(state.lastHistoryContent).toBeUndefined(); + expect(state.lastActivityMode).toBeUndefined(); + expect(state.activitySpinner).toBeUndefined(); + expect(state.activitySpinnerStyle).toBeUndefined(); + expect(state.streamingComponent).toBeUndefined(); + expect(state.streamingTranscriptEntry).toBeUndefined(); + expect(state.activeCompactionBlock).toBeUndefined(); + expect(state.pendingAgentGroup).toBeNull(); + expect(state.pendingReadGroup).toBeNull(); + }); +}); diff --git a/apps/kimi-code/test/tui/input/image-attachment-store.test.ts b/apps/kimi-code/test/tui/input/image-attachment-store.test.ts new file mode 100644 index 000000000..cb3e88c59 --- /dev/null +++ b/apps/kimi-code/test/tui/input/image-attachment-store.test.ts @@ -0,0 +1,62 @@ +import { describe, it, expect } from 'vitest'; + +import { + ImageAttachmentStore, + formatPlaceholder, + formatVideoPlaceholder, +} from '#/tui/utils/image-attachment-store'; + +describe('ImageAttachmentStore', () => { + it('assigns monotonically increasing ids starting at 1', () => { + const s = new ImageAttachmentStore(); + const a = s.addImage(new Uint8Array([1]), 'image/png', 10, 20); + const b = s.addVideo('video/quicktime', '/tmp/sample.mov'); + expect(a.id).toBe(1); + expect(b.id).toBe(2); + }); + + it('builds the canonical placeholder string', () => { + expect(formatPlaceholder(1, 640, 480)).toBe('[image #1 (640×480)]'); + expect(formatPlaceholder(42, 3840, 2160)).toBe('[image #42 (3840×2160)]'); + }); + + it('builds video placeholders with sanitized labels', () => { + expect(formatVideoPlaceholder(1, 'sample.mov')).toBe('[video #1 sample.mov]'); + expect(formatVideoPlaceholder(2, 'bad[name]\u0000.mov')).toBe('[video #2 bad_name__.mov]'); + }); + + it('uses the video filename basename as the placeholder label', () => { + const s = new ImageAttachmentStore(); + const att = s.addVideo('video/mp4', '/tmp/clips/sample.mp4'); + expect(att.filename).toBe('sample.mp4'); + expect(att.sourcePath).toBe('/tmp/clips/sample.mp4'); + expect(att.placeholder).toBe('[video #1 sample.mp4]'); + }); + + it('get() returns stored attachment', () => { + const s = new ImageAttachmentStore(); + const bytes = new Uint8Array([9, 8, 7]); + const att = s.addImage(bytes, 'image/jpeg', 100, 200); + expect(s.get(att.id)).toBe(att); + expect(s.get(99)).toBeUndefined(); + }); + + it('keeps pasted image bytes in memory', () => { + const s = new ImageAttachmentStore(); + const bytes = new Uint8Array([9, 8, 7]); + const att = s.addImage(bytes, 'image/jpeg', 100, 200); + expect(att.bytes).toBe(bytes); + expect(att.mime).toBe('image/jpeg'); + }); + + it('clear() resets ids and empties storage', () => { + const s = new ImageAttachmentStore(); + s.addImage(new Uint8Array(), 'image/png', 10, 10); + s.addImage(new Uint8Array(), 'image/png', 10, 10); + expect(s.size()).toBe(2); + s.clear(); + expect(s.size()).toBe(0); + const next = s.addImage(new Uint8Array(), 'image/png', 10, 10); + expect(next.id).toBe(1); + }); +}); diff --git a/apps/kimi-code/test/tui/input/image-placeholder.test.ts b/apps/kimi-code/test/tui/input/image-placeholder.test.ts new file mode 100644 index 000000000..cdc74e913 --- /dev/null +++ b/apps/kimi-code/test/tui/input/image-placeholder.test.ts @@ -0,0 +1,104 @@ +import { describe, it, expect } from 'vitest'; + +import { ImageAttachmentStore } from '#/tui/utils/image-attachment-store'; +import { extractMediaAttachments } from '#/tui/utils/image-placeholder'; + +function storeWith( + bytes: Uint8Array, + width = 640, + height = 480, +): { store: ImageAttachmentStore; placeholder: string } { + const store = new ImageAttachmentStore(); + const att = store.addImage(bytes, 'image/png', width, height); + return { store, placeholder: att.placeholder }; +} + +describe('extractMediaAttachments', () => { + it('returns no parts and hasMedia=false for plain text', () => { + const store = new ImageAttachmentStore(); + const r = extractMediaAttachments('hello world', store); + expect(r.hasMedia).toBe(false); + expect(r.parts).toEqual([]); + expect(r.imageAttachmentIds).toEqual([]); + expect(r.videoAttachmentIds).toEqual([]); + }); + + it('extracts a single matching placeholder into an image content part', () => { + const { store, placeholder } = storeWith(new Uint8Array([0xaa, 0xbb])); + const r = extractMediaAttachments(`describe ${placeholder} please`, store); + expect(r.hasMedia).toBe(true); + expect(r.imageAttachmentIds).toEqual([1]); + expect(r.parts).toEqual([ + { type: 'text', text: 'describe ' }, + { type: 'image_url', imageUrl: { url: 'data:image/png;base64,qrs=' } }, + { type: 'text', text: ' please' }, + ]); + }); + + it('keeps matched-placeholder order with multiple images', () => { + const store = new ImageAttachmentStore(); + const a = store.addImage(new Uint8Array([1]), 'image/png', 10, 10); + const b = store.addImage(new Uint8Array([2]), 'image/png', 20, 20); + const text = `first ${a.placeholder} then ${b.placeholder} end`; + const r = extractMediaAttachments(text, store); + expect(r.imageAttachmentIds).toEqual([1, 2]); + expect(r.parts).toEqual([ + { type: 'text', text: 'first ' }, + { type: 'image_url', imageUrl: { url: 'data:image/png;base64,AQ==' } }, + { type: 'text', text: ' then ' }, + { type: 'image_url', imageUrl: { url: 'data:image/png;base64,Ag==' } }, + { type: 'text', text: ' end' }, + ]); + }); + + it('keeps matched-placeholder order with mixed image and video attachments', () => { + const store = new ImageAttachmentStore(); + const img = store.addImage(new Uint8Array([1]), 'image/png', 10, 10); + const vid = store.addVideo('video/quicktime', '/tmp/clip.mov'); + const text = `first ${img.placeholder} then ${vid.placeholder} end`; + const r = extractMediaAttachments(text, store); + expect(r.imageAttachmentIds).toEqual([1]); + expect(r.videoAttachmentIds).toEqual([2]); + expect(r.parts).toEqual([ + { type: 'text', text: 'first ' }, + { type: 'image_url', imageUrl: { url: 'data:image/png;base64,AQ==' } }, + { type: 'text', text: ' then <video path="/tmp/clip.mov"></video> end' }, + ]); + }); + + it('leaves unresolved (typed by hand) placeholders as literal text', () => { + const store = new ImageAttachmentStore(); + const r = extractMediaAttachments('try [image #999 (1×1)] and [video #42 clip.mov] now', store); + expect(r.hasMedia).toBe(false); + expect(r.parts).toEqual([]); + }); + + it('uses pasted image bytes in data URLs', () => { + const bytes = new Uint8Array([0x89, 0x50, 0x4e, 0x47]); + const { store, placeholder } = storeWith(bytes); + const r = extractMediaAttachments(placeholder, store); + expect(r.parts).toHaveLength(1); + expect(r.parts[0]).toEqual({ + type: 'image_url', + imageUrl: { url: 'data:image/png;base64,iVBORw==' }, + }); + }); + + it('escapes media paths in generated tags', () => { + const store = new ImageAttachmentStore(); + const att = store.addVideo('video/mp4', '/tmp/a&"<>.mp4', 'sample.mp4'); + const r = extractMediaAttachments(att.placeholder, store); + expect(r.parts).toEqual([ + { type: 'text', text: '<video path="/tmp/a&"<>.mp4"></video>' }, + ]); + }); + + it('expands video placeholders backed by local files to readMediaFile video tags', () => { + const store = new ImageAttachmentStore(); + const att = store.addVideo('video/mp4', '/tmp/sample.mp4'); + const r = extractMediaAttachments(att.placeholder, store); + expect(r.hasMedia).toBe(true); + expect(r.videoAttachmentIds).toEqual([1]); + expect(r.parts).toEqual([{ type: 'text', text: '<video path="/tmp/sample.mp4"></video>' }]); + }); +}); diff --git a/apps/kimi-code/test/tui/kimi-tui-message-flow.test.ts b/apps/kimi-code/test/tui/kimi-tui-message-flow.test.ts new file mode 100644 index 000000000..f1d9dc17d --- /dev/null +++ b/apps/kimi-code/test/tui/kimi-tui-message-flow.test.ts @@ -0,0 +1,1172 @@ +import { mkdtemp, rm } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +import { + deleteAllKittyImages, + resetCapabilitiesCache, + setCapabilities, +} from '@earendil-works/pi-tui'; +import type { ApprovalRequest, ApprovalResponse, Event } from '@moonshot-ai/kimi-code-sdk'; +import { afterEach, describe, expect, it, vi } from 'vitest'; + +import { ModelSelectorComponent } from '#/tui/components/dialogs/model-selector'; +import { KimiTUI, type KimiTUIStartupInput, type TUIState } from '#/tui/kimi-tui'; +import type { QueuedMessage } from '#/tui/types'; +import type { ImageAttachmentStore } from '#/tui/utils/image-attachment-store'; + +function stripSgr(text: string): string { + return text.replaceAll(/\u001B\[[0-9;]*m/g, ''); +} + +interface MessageDriver { + state: TUIState; + init(): Promise<boolean>; + handleUserInput(text: string): void; + handleEvent(event: Event, sendQueued: (item: QueuedMessage) => void): void; + persistInputHistory(text: string): Promise<void>; + startSessionEventSubscription(): void; + getCurrentSessionId(): string; +} + +interface FeedbackDriver extends MessageDriver { + handleFeedbackCommand(): Promise<void>; + promptFeedbackInput(): Promise<string | undefined>; +} + +function makeStartupInput(): KimiTUIStartupInput { + return { + cliOptions: { + session: undefined, + continue: false, + yolo: false, + plan: false, + model: undefined, + outputFormat: undefined, + prompt: undefined, + skillsDirs: [], + }, + tuiConfig: { + theme: 'dark', + editorCommand: null, + notifications: { enabled: true, condition: 'unfocused' }, + }, + version: '0.0.0-test', + workDir: '/tmp/proj-a', + resolvedTheme: 'dark', + }; +} + +function makeSession(overrides: Record<string, unknown> = {}) { + return { + id: 'ses-1', + model: 'k2', + summary: { title: null }, + prompt: vi.fn(async () => {}), + steer: vi.fn(async () => {}), + init: vi.fn(async () => {}), + cancel: vi.fn(async () => {}), + cancelCompaction: vi.fn(async () => {}), + getStatus: vi.fn(async () => ({ + model: 'k2', + thinkingLevel: 'off', + permission: 'manual', + planMode: false, + contextTokens: 0, + maxContextTokens: 100, + contextUsage: 0, + })), + setApprovalHandler: vi.fn(), + setQuestionHandler: vi.fn(), + setModel: vi.fn(async () => {}), + setThinking: vi.fn(async () => {}), + setPermission: vi.fn(async () => {}), + setPlanMode: vi.fn(async () => {}), + onEvent: vi.fn(() => vi.fn()), + listMcpServers: vi.fn(async () => []), + listSkills: vi.fn(async () => []), + getResumeState: vi.fn(() => ({ + sessionMetadata: {}, + agents: { + main: { + status: { + model: 'k2', + thinkingLevel: 'off', + permission: 'manual', + planMode: false, + contextTokens: 0, + maxContextTokens: 100, + contextUsage: 0, + }, + context: { history: [] }, + replay: [], + }, + }, + })), + close: vi.fn(async () => {}), + ...overrides, + }; +} + +function makeHarness(session = makeSession(), overrides: Record<string, unknown> = {}) { + return { + getConfig: vi.fn(async () => ({ + models: { + k2: { model: 'moonshot-v1', maxContextSize: 100 }, + }, + })), + createSession: vi.fn(async () => session), + resumeSession: vi.fn(async () => session), + forkSession: vi.fn(async () => session), + listSessions: vi.fn(async () => []), + close: vi.fn(async () => {}), + track: vi.fn(), + setTelemetryContext: vi.fn(), + interactiveAgentId: 'main', + auth: { + status: vi.fn(), + login: vi.fn(), + logout: vi.fn(), + getManagedUsage: vi.fn(), + submitFeedback: vi.fn(async () => ({ kind: 'ok' })), + }, + ...overrides, + }; +} + +async function makeDriver( + session = makeSession(), + harnessOverrides: Record<string, unknown> = {}, +): Promise<{ + driver: MessageDriver; + session: ReturnType<typeof makeSession>; + harness: ReturnType<typeof makeHarness>; +}> { + const harness = makeHarness(session, harnessOverrides); + const driver = new KimiTUI(harness as never, makeStartupInput()) as unknown as MessageDriver; + vi.spyOn(driver.state.ui, 'requestRender').mockImplementation(() => {}); + vi.spyOn(driver.state.terminal, 'setProgress').mockImplementation(() => {}); + driver.persistInputHistory = vi.fn(async () => {}); + await driver.init(); + return { driver, session, harness }; +} + +function renderTranscript(driver: MessageDriver): string { + return driver.state.transcriptContainer.render(120).join('\n'); +} + +function countOccurrences(haystack: string, needle: string): number { + return haystack.split(needle).length - 1; +} + +const tempDirs: string[] = []; +const originalKimiCodeHome = process.env['KIMI_CODE_HOME']; +const originalVisual = process.env['VISUAL']; +const originalEditor = process.env['EDITOR']; + +async function makeTempHome(): Promise<string> { + const dir = await mkdtemp(join(tmpdir(), 'kimi-code-tui-')); + tempDirs.push(dir); + return dir; +} + +afterEach(async () => { + resetCapabilitiesCache(); + for (const dir of tempDirs.splice(0)) { + await rm(dir, { recursive: true, force: true }); + } + if (originalKimiCodeHome === undefined) { + delete process.env['KIMI_CODE_HOME']; + } else { + process.env['KIMI_CODE_HOME'] = originalKimiCodeHome; + } + if (originalVisual === undefined) { + delete process.env['VISUAL']; + } else { + process.env['VISUAL'] = originalVisual; + } + if (originalEditor === undefined) { + delete process.env['EDITOR']; + } else { + process.env['EDITOR'] = originalEditor; + } +}); + +describe('KimiTUI message flow', () => { + it('tracks editor shortcut and paste hooks', async () => { + const { driver, harness } = await makeDriver(); + harness.track.mockClear(); + + driver.state.editor.handleInput('\n'); + driver.state.editor.handleInput('\u001F'); + delete process.env['VISUAL']; + delete process.env['EDITOR']; + driver.state.editor.onOpenExternalEditor?.(); + driver.state.editor.onToggleToolExpand?.(); + driver.state.editor.onTextPaste?.(); + + expect(harness.track).toHaveBeenCalledWith('shortcut_newline', undefined); + expect(harness.track).toHaveBeenCalledWith('undo', undefined); + expect(harness.track).toHaveBeenCalledWith('shortcut_editor', undefined); + expect(harness.track).toHaveBeenCalledWith('shortcut_expand', undefined); + expect(harness.track).toHaveBeenCalledWith('shortcut_paste', { kind: 'text' }); + }); + + it('tracks /clear as the clear alias for /new', async () => { + const { driver, harness } = await makeDriver(makeSession({ id: 'ses-1' })); + const nextSession = makeSession({ id: 'ses-2' }); + harness.createSession.mockResolvedValueOnce(nextSession); + harness.track.mockClear(); + + driver.handleUserInput('/clear'); + + await vi.waitFor(() => { + expect(driver.getCurrentSessionId()).toBe('ses-2'); + }); + expect(harness.track).toHaveBeenCalledWith('input_command', { command: 'new' }); + expect(harness.track).toHaveBeenCalledWith('clear', undefined); + }); + + it('tracks theme changes from slash commands', async () => { + process.env['KIMI_CODE_HOME'] = await makeTempHome(); + const { driver, harness } = await makeDriver(); + harness.track.mockClear(); + + driver.handleUserInput('/theme light'); + + await vi.waitFor(() => { + expect(driver.state.appState.theme).toBe('light'); + }); + expect(harness.track).toHaveBeenCalledWith('input_command', { command: 'theme' }); + expect(harness.track).toHaveBeenCalledWith('theme_switch', { theme: 'light' }); + }); + + it('tracks successful feedback submissions only after the request succeeds', async () => { + const { driver, harness } = await makeDriver( + makeSession(), + { + getConfig: vi.fn(async () => ({ + models: { + k2: { + model: 'moonshot-v1', + maxContextSize: 100, + provider: 'managed:kimi-code', + }, + }, + })), + }, + ); + const feedbackDriver = driver as unknown as FeedbackDriver; + feedbackDriver.promptFeedbackInput = vi.fn(async () => 'useful feedback'); + harness.auth.submitFeedback.mockResolvedValueOnce({ kind: 'ok' }); + harness.track.mockClear(); + + await feedbackDriver.handleFeedbackCommand(); + + expect(harness.auth.submitFeedback).toHaveBeenCalledWith( + expect.objectContaining({ + content: 'useful feedback', + sessionId: 'ses-1', + version: 'kimi-code-0.0.0-test', + model: 'k2', + }), + ); + expect(harness.track).toHaveBeenCalledWith('feedback_submitted', undefined); + }); + + it('does not track feedback when the dialog is cancelled', async () => { + const { driver, harness } = await makeDriver( + makeSession(), + { + getConfig: vi.fn(async () => ({ + models: { + k2: { + model: 'moonshot-v1', + maxContextSize: 100, + provider: 'managed:kimi-code', + }, + }, + })), + }, + ); + const feedbackDriver = driver as unknown as FeedbackDriver; + feedbackDriver.promptFeedbackInput = vi.fn(async () => undefined); + harness.track.mockClear(); + + await feedbackDriver.handleFeedbackCommand(); + + expect(harness.auth.submitFeedback).not.toHaveBeenCalled(); + expect(harness.track).not.toHaveBeenCalledWith('feedback_submitted', undefined); + }); + + it('tracks blocked slash commands as invalid without counting them as executed commands', async () => { + const { driver, harness } = await makeDriver(); + driver.state.appState.isStreaming = true; + harness.track.mockClear(); + + driver.handleUserInput('/new'); + await Promise.resolve(); + + expect(harness.track).toHaveBeenCalledWith('input_command_invalid', { + reason: 'blocked', + command: 'new', + }); + expect(harness.track).not.toHaveBeenCalledWith('input_command', { command: 'new' }); + }); + + it('tracks Shift-Tab mode switches through the editor handler', async () => { + const { driver, session, harness } = await makeDriver(); + harness.track.mockClear(); + + driver.state.editor.onShiftTab?.(); + + await vi.waitFor(() => { + expect(session.setPlanMode).toHaveBeenCalledWith(true); + }); + expect(harness.track).toHaveBeenCalledWith('shortcut_plan_toggle', { enabled: true }); + expect(harness.track).toHaveBeenCalledWith('shortcut_mode_switch', { to_mode: 'plan' }); + }); + + it('routes /yolo through session permission state without app-layer telemetry duplication', async () => { + const { driver, session, harness } = await makeDriver(); + harness.track.mockClear(); + + driver.handleUserInput('/yolo on'); + + await vi.waitFor(() => { + expect(session.setPermission).toHaveBeenCalledWith('yolo'); + }); + expect(driver.state.appState).toMatchObject({ + yolo: true, + permissionMode: 'yolo', + }); + expect(harness.track).toHaveBeenCalledWith('input_command', { command: 'yolo' }); + expect(harness.track).not.toHaveBeenCalledWith('yolo_toggle', expect.anything()); + }); + + it('hydrates MCP server status after subscribing to session events', async () => { + const session = makeSession({ + listMcpServers: vi.fn(async () => [ + { + name: 'local-tools', + transport: 'stdio', + status: 'connected', + toolCount: 2, + }, + { + name: 'remote-tools', + transport: 'http', + status: 'failed', + toolCount: 0, + error: 'connection refused', + }, + ]), + }); + const { driver } = await makeDriver(session); + + driver.startSessionEventSubscription(); + await Promise.resolve(); + + expect(session.onEvent).toHaveBeenCalledOnce(); + expect(session.listMcpServers).toHaveBeenCalledOnce(); + const subscribeOrder = session.onEvent.mock.invocationCallOrder[0]; + const snapshotOrder = session.listMcpServers.mock.invocationCallOrder[0]; + if (subscribeOrder === undefined || snapshotOrder === undefined) { + throw new Error('Expected MCP status sync to subscribe and fetch a snapshot.'); + } + expect(subscribeOrder).toBeLessThan(snapshotOrder); + const transcript = renderTranscript(driver); + expect(transcript).toContain('MCP server "local-tools" connected'); + expect(transcript).toContain('2 tools (stdio)'); + expect(transcript).toContain('MCP server "remote-tools" failed: connection refused'); + }); + + it('deduplicates identical MCP status updates while allowing reconnect transitions', async () => { + const eventListeners: Array<(event: Event) => void> = []; + const connectedServer = { + name: 'local-tools', + transport: 'stdio', + status: 'connected', + toolCount: 2, + }; + const session = makeSession({ + onEvent: vi.fn((listener: (event: Event) => void) => { + eventListeners.push(listener); + return vi.fn(); + }), + listMcpServers: vi.fn(async () => [connectedServer]), + }); + const { driver } = await makeDriver(session); + + driver.startSessionEventSubscription(); + await Promise.resolve(); + eventListeners[0]?.({ + type: 'mcp.server.status', + agentId: 'main', + sessionId: 'ses-1', + server: connectedServer, + } as Event); + + expect(countOccurrences(renderTranscript(driver), 'MCP server "local-tools" connected')).toBe( + 1, + ); + + eventListeners[0]?.({ + type: 'mcp.server.status', + agentId: 'main', + sessionId: 'ses-1', + server: { + ...connectedServer, + status: 'pending', + toolCount: 0, + }, + } as Event); + eventListeners[0]?.({ + type: 'mcp.server.status', + agentId: 'main', + sessionId: 'ses-1', + server: connectedServer, + } as Event); + + expect(countOccurrences(renderTranscript(driver), 'MCP server "local-tools" connected')).toBe( + 2, + ); + }); + + it('does not let a late MCP snapshot overwrite a live status event', async () => { + const eventListeners: Array<(event: Event) => void> = []; + let resolveSnapshot: ( + servers: Array<{ + name: string; + transport: 'stdio' | 'http'; + status: 'pending' | 'connected' | 'failed' | 'disabled'; + toolCount: number; + error?: string; + }>, + ) => void = () => {}; + const snapshot = new Promise((resolve) => { + resolveSnapshot = resolve; + }); + const session = makeSession({ + onEvent: vi.fn((listener: (event: Event) => void) => { + eventListeners.push(listener); + return vi.fn(); + }), + listMcpServers: vi.fn(() => snapshot), + }); + const { driver } = await makeDriver(session); + + driver.startSessionEventSubscription(); + eventListeners[0]?.({ + type: 'mcp.server.status', + agentId: 'main', + sessionId: 'ses-1', + server: { + name: 'local-tools', + transport: 'stdio', + status: 'connected', + toolCount: 2, + }, + } as Event); + resolveSnapshot([ + { + name: 'local-tools', + transport: 'stdio', + status: 'failed', + toolCount: 0, + error: 'stale failure', + }, + ]); + await Promise.resolve(); + + const transcript = renderTranscript(driver); + expect(transcript).toContain('MCP server "local-tools" connected'); + expect(transcript).not.toContain('stale failure'); + }); + + it('sends normal editor input to the active session and marks the turn as waiting', async () => { + const { driver, session } = await makeDriver(); + + driver.handleUserInput('hello'); + + expect(session.prompt).toHaveBeenCalledWith('hello'); + expect(driver.state.appState.isStreaming).toBe(true); + expect(driver.state.appState.streamingPhase).toBe('waiting'); + expect(driver.state.livePane.mode).toBe('waiting'); + expect(driver.state.transcriptEntries).toEqual([ + expect.objectContaining({ + kind: 'user', + content: 'hello', + }), + ]); + }); + + it('sends pasted image placeholders as image content parts', async () => { + const { driver, session } = await makeDriver(); + const imageStore = (driver as unknown as { imageStore: ImageAttachmentStore }).imageStore; + const attachment = imageStore.addImage(new Uint8Array([0xaa, 0xbb]), 'image/png', 1, 1); + + driver.handleUserInput(`describe ${attachment.placeholder}`); + + expect(session.prompt).toHaveBeenCalledWith([ + { type: 'text', text: 'describe ' }, + { type: 'image_url', imageUrl: { url: 'data:image/png;base64,qrs=' } }, + ]); + expect(driver.state.transcriptEntries).toEqual([ + expect.objectContaining({ + kind: 'user', + content: `describe ${attachment.placeholder}`, + imageAttachmentIds: [attachment.id], + }), + ]); + }); + + it('queues editor input instead of prompting while a turn is already streaming', async () => { + const { driver, session, harness } = await makeDriver(); + driver.state.appState.isStreaming = true; + harness.track.mockClear(); + + driver.handleUserInput('queued message'); + + expect(session.prompt).not.toHaveBeenCalled(); + expect(driver.state.queuedMessages).toEqual([{ text: 'queued message', agentId: 'main' }]); + expect(driver.state.queueContainer.children.length).toBeGreaterThan(0); + expect(harness.track).toHaveBeenCalledWith('input_queue', undefined); + }); + + it('cancels active streaming from Escape and Ctrl-C editor shortcuts', async () => { + const { driver, session } = await makeDriver(); + + driver.state.appState.isStreaming = true; + driver.state.editor.onEscape?.(); + + expect(session.cancel).toHaveBeenCalledTimes(1); + + session.cancel.mockClear(); + driver.state.appState.isStreaming = true; + driver.state.editor.onCtrlC?.(); + + expect(session.cancel).toHaveBeenCalledTimes(1); + }); + + it('dispatches the next queued message after the active turn ends', async () => { + vi.useFakeTimers(); + try { + const { driver } = await makeDriver(); + const sendQueued = vi.fn(); + driver.state.appState.isStreaming = true; + driver.state.appState.streamingStartTime = 1; + driver.state.currentTurnId = '1'; + driver.state.queuedMessages = [{ text: 'next' }]; + + driver.handleEvent( + { + type: 'turn.ended', + agentId: 'main', + sessionId: 'ses-1', + turnId: 1, + reason: 'completed', + } as Event, + sendQueued, + ); + await vi.runAllTimersAsync(); + + expect(sendQueued).toHaveBeenCalledWith({ text: 'next' }); + expect(driver.state.queuedMessages).toEqual([]); + expect(driver.state.appState.isStreaming).toBe(false); + } finally { + vi.useRealTimers(); + } + }); + + it('cancels manual compaction from the editor', async () => { + const { driver, session } = await makeDriver(); + driver.handleEvent( + { + type: 'compaction.started', + agentId: 'main', + sessionId: 'ses-1', + trigger: 'manual', + } as Event, + vi.fn(), + ); + + driver.state.editor.onEscape?.(); + + expect(session.cancelCompaction).toHaveBeenCalledTimes(1); + + session.cancelCompaction.mockClear(); + driver.state.appState.isCompacting = true; + driver.state.editor.onCtrlC?.(); + + expect(session.cancelCompaction).toHaveBeenCalledTimes(1); + }); + + it('dispatches the next queued message after compaction is cancelled', async () => { + vi.useFakeTimers(); + try { + const { driver } = await makeDriver(); + const sendQueued = vi.fn(); + driver.handleEvent( + { + type: 'compaction.started', + agentId: 'main', + sessionId: 'ses-1', + trigger: 'manual', + } as Event, + sendQueued, + ); + driver.state.queuedMessages = [{ text: 'next' }]; + + driver.handleEvent( + { + type: 'compaction.cancelled', + agentId: 'main', + sessionId: 'ses-1', + } as Event, + sendQueued, + ); + await vi.runAllTimersAsync(); + + expect(driver.state.appState.isCompacting).toBe(false); + expect(driver.state.appState.streamingPhase).toBe('idle'); + expect(driver.state.queuedMessages).toEqual([]); + expect(sendQueued).toHaveBeenCalledWith({ text: 'next' }); + expect(driver.state.transcriptContainer.render(120).map(stripSgr).join('\n')).toContain( + 'Compaction cancelled', + ); + } finally { + vi.useRealTimers(); + } + }); + + it('renders an error instead of prompting when no model is selected', async () => { + const { driver, session } = await makeDriver(); + driver.state.appState.model = ''; + + driver.handleUserInput('hello'); + + expect(session.prompt).not.toHaveBeenCalled(); + expect(driver.state.transcriptContainer.render(120).join('\n')).toContain('LLM not set'); + }); + + it('dispatches /init to the active session and clears busy state after completion', async () => { + let resolveInit: (() => void) | undefined; + const session = makeSession({ + init: vi.fn( + () => + new Promise<void>((resolve) => { + resolveInit = resolve; + }), + ), + }); + const { driver, harness } = await makeDriver(session); + harness.track.mockClear(); + + driver.handleUserInput('/init'); + + await vi.waitFor(() => { + expect(session.init).toHaveBeenCalledTimes(1); + }); + expect(session.prompt).not.toHaveBeenCalled(); + expect(driver.state.appState.isStreaming).toBe(true); + expect(driver.state.livePane.mode).toBe('waiting'); + + resolveInit?.(); + + await vi.waitFor(() => { + expect(driver.state.appState.isStreaming).toBe(false); + }); + expect(driver.state.livePane.mode).toBe('idle'); + expect(harness.track).toHaveBeenCalledWith('init_complete', undefined); + }); + + it('queues Ctrl-S input instead of steering while /init is running', async () => { + let resolveInit: (() => void) | undefined; + const session = makeSession({ + init: vi.fn( + () => + new Promise<void>((resolve) => { + resolveInit = resolve; + }), + ), + }); + const { driver } = await makeDriver(session); + + driver.handleUserInput('/init'); + await vi.waitFor(() => { + expect(session.init).toHaveBeenCalledTimes(1); + }); + + driver.state.editor.setText('apply after init'); + driver.state.editor.onCtrlS?.(); + + expect(session.steer).not.toHaveBeenCalled(); + expect(driver.state.queuedMessages).toEqual([{ text: 'apply after init', agentId: 'main' }]); + expect(stripSgr(driver.state.queueContainer.render(120).join('\n'))).not.toContain( + 'ctrl-s to steer immediately', + ); + + resolveInit?.(); + + await vi.waitFor(() => { + expect(session.prompt).toHaveBeenCalledWith('apply after init'); + }); + expect(driver.state.queuedMessages).toEqual([]); + }); + + it('cancels the active /init request through the session', async () => { + let resolveInit: (() => void) | undefined; + const session = makeSession({ + init: vi.fn( + () => + new Promise<void>((resolve) => { + resolveInit = resolve; + }), + ), + }); + const { driver } = await makeDriver(session); + + driver.handleUserInput('/init'); + await vi.waitFor(() => { + expect(session.init).toHaveBeenCalledTimes(1); + }); + + driver.state.editor.onEscape?.(); + + await vi.waitFor(() => { + expect(session.cancel).toHaveBeenCalledTimes(1); + }); + + resolveInit?.(); + }); + + it('does not run /init when no model is selected', async () => { + const { driver, session } = await makeDriver(); + driver.state.appState.model = ''; + + driver.handleUserInput('/init'); + + expect(session.init).not.toHaveBeenCalled(); + expect(driver.state.transcriptContainer.render(120).join('\n')).toContain('LLM not set'); + }); + + it('shows the login prompt for auth.login_required session errors', async () => { + const { driver } = await makeDriver(); + + driver.handleEvent( + { + type: 'error', + agentId: 'main', + sessionId: 'ses-1', + code: 'auth.login_required', + message: 'OAuth provider credentials were rejected.', + retryable: false, + } as Event, + vi.fn(), + ); + + const transcript = stripSgr(renderTranscript(driver)); + expect(transcript).toContain('OAuth login expired. Send /login to login.'); + expect(transcript).not.toContain('[auth.login_required]'); + expect(transcript).not.toContain('kimi export'); + }); + + it('appends the kimi export hint beneath session error messages', async () => { + const { driver } = await makeDriver(); + + driver.handleEvent( + { + type: 'error', + agentId: 'main', + sessionId: 'ses-1', + code: 'compaction.failed', + message: "APIStatusError: 400 the message at position 82 with role 'assistant' must not be empty", + retryable: false, + } as Event, + vi.fn(), + ); + + const transcript = stripSgr(driver.state.transcriptContainer.render(200).join('\n')); + expect(transcript).toContain('Error: [compaction.failed]'); + expect(transcript).toContain('If this persists, run `kimi export ses-1`'); + expect(transcript).toContain("Please don't share it publicly"); + }); + + it('skips the kimi export hint when no active session id is set', async () => { + const { driver } = await makeDriver(); + driver.state.appState.sessionId = ''; + + driver.handleEvent( + { + type: 'error', + agentId: 'main', + sessionId: '', + code: 'compaction.failed', + message: 'boom', + retryable: false, + } as Event, + vi.fn(), + ); + + const transcript = stripSgr(renderTranscript(driver)); + expect(transcript).toContain('Error: [compaction.failed]'); + expect(transcript).not.toContain('kimi export'); + }); + + it('shows ExitPlanMode plan only in the current-plan card during approval', async () => { + const planContent = '# No Duplicate Plan\n\n- Do the non-duplicated plan work'; + const session = makeSession({ + getPlan: vi.fn(async () => ({ + id: 'no-duplicate-plan', + content: planContent, + path: '/tmp/no-duplicate-plan.md', + })), + }); + const { driver } = await makeDriver(session); + + driver.handleEvent( + { + type: 'tool.call.started', + agentId: 'main', + sessionId: 'ses-1', + turnId: 1, + toolCallId: 'call_exit_plan', + name: 'ExitPlanMode', + args: {}, + } as Event, + vi.fn(), + ); + + await vi.waitFor(() => { + const transcript = stripSgr(renderTranscript(driver)); + expect(transcript).toContain('Current plan'); + expect(countOccurrences(transcript, 'non-duplicated plan work')).toBe(1); + }); + + const approvalHandler = vi.mocked(session.setApprovalHandler).mock.calls[0]?.[0] as + | ((request: ApprovalRequest) => Promise<ApprovalResponse>) + | undefined; + if (approvalHandler === undefined) throw new Error('expected approval handler'); + void approvalHandler({ + turnId: 1, + toolCallId: 'call_exit_plan', + toolName: 'ExitPlanMode', + action: 'Review plan', + display: { + kind: 'plan_review', + plan: planContent, + path: '/tmp/no-duplicate-plan.md', + }, + }); + + await vi.waitFor(() => { + const approval = stripSgr(driver.state.editorContainer.render(120).join('\n')); + expect(approval).toContain('Ready to build with this plan?'); + expect(approval).not.toContain('non-duplicated plan work'); + expect(approval).not.toContain('/tmp/no-duplicate-plan.md'); + }); + }); + + it('renders /status using the active session runtime status', async () => { + const session = makeSession({ + getStatus: vi.fn(async () => ({ + model: 'k2', + thinkingLevel: 'high', + permission: 'auto', + planMode: true, + contextTokens: 25, + maxContextTokens: 100, + contextUsage: 0.25, + })), + }); + const { driver } = await makeDriver(session); + const getStatus = vi.mocked(session.getStatus); + const previousStatusCalls = getStatus.mock.calls.length; + + driver.handleUserInput('/status'); + + await vi.waitFor(() => { + expect(getStatus).toHaveBeenCalledTimes(previousStatusCalls + 1); + const output = stripSgr(driver.state.transcriptContainer.render(120).join('\n')); + expect(output).toContain(' Status '); + expect(output).toContain('>_ Kimi Code'); + expect(output).toContain('Model'); + expect(output).toContain('thinking on'); + expect(output).toContain('Permissions auto'); + expect(output).toContain('Plan mode on'); + expect(output).toContain('Context window'); + expect(output).toContain('25.0%'); + }); + }); + + it('renders /mcp using a fresh MCP server snapshot', async () => { + const session = makeSession({ + listMcpServers: vi.fn(async () => [ + { + name: 'local-tools', + transport: 'stdio', + status: 'connected', + toolCount: 2, + }, + { + name: 'remote-tools', + transport: 'http', + status: 'failed', + toolCount: 0, + error: 'connection refused', + }, + { + name: 'linear', + transport: 'http', + status: 'needs-auth', + toolCount: 0, + }, + { + name: 'disabled-tools', + transport: 'stdio', + status: 'disabled', + toolCount: 0, + }, + ]), + }); + const { driver } = await makeDriver(session); + const listMcpServers = vi.mocked(session.listMcpServers); + const previousCalls = listMcpServers.mock.calls.length; + + driver.handleUserInput('/mcp'); + + await vi.waitFor(() => { + expect(listMcpServers).toHaveBeenCalledTimes(previousCalls + 1); + const output = stripSgr(driver.state.transcriptContainer.render(140).join('\n')); + expect(output).toContain(' MCP (4) '); + expect(output).toContain('Servers'); + expect(output).toContain('local-tools'); + expect(output).toContain('connected'); + expect(output).toContain('stdio'); + expect(output).toContain('2 tools'); + expect(output).toContain('remote-tools'); + expect(output).toContain('failed'); + expect(output).toContain('connection refused'); + expect(output).toContain('linear'); + expect(output).toContain('needs auth'); + expect(output).toContain('/mcp-config login linear'); + expect(output).toContain('disabled-tools'); + expect(output).toContain('disabled'); + expect(output).toContain('1 connected · 1 needs auth · 1 failed · 1 disabled · 2 tools available'); + }); + }); + + it('renders an empty /mcp state when no MCP servers are configured', async () => { + const session = makeSession({ + listMcpServers: vi.fn(async () => []), + }); + const { driver } = await makeDriver(session); + + driver.handleUserInput('/mcp'); + + await vi.waitFor(() => { + const output = stripSgr(driver.state.transcriptContainer.render(120).join('\n')); + expect(output).toContain('No MCP servers configured. Run /mcp-config to add one.'); + }); + }); + + it('renders /mcp list failures as command boundary errors', async () => { + const session = makeSession({ + listMcpServers: vi.fn(async () => { + throw new Error('rpc unavailable'); + }), + }); + const { driver } = await makeDriver(session); + + driver.handleUserInput('/mcp'); + + await vi.waitFor(() => { + const output = stripSgr(driver.state.transcriptContainer.render(120).join('\n')); + expect(output).toContain('Error: Failed to load MCP servers: rpc unavailable'); + }); + }); + + it('applies /model selection with inline thinking state', async () => { + const session = makeSession(); + const { driver } = await makeDriver(session, { + getConfig: vi.fn(async () => ({ + models: { + k2: { + provider: 'managed:kimi-code', + model: 'kimi-k2', + maxContextSize: 100, + displayName: 'Kimi K2', + capabilities: ['thinking'], + }, + turbo: { + provider: 'managed:kimi-code', + model: 'kimi-turbo', + maxContextSize: 100, + displayName: 'Kimi Turbo', + capabilities: ['thinking'], + }, + }, + })), + }); + + driver.handleUserInput('/model turbo'); + + const picker = driver.state.editorContainer.children[0]; + expect(picker).toBeInstanceOf(ModelSelectorComponent); + const pickerOutput = stripSgr((picker as ModelSelectorComponent).render(120).join('\n')); + expect(pickerOutput).toContain('Kimi K2 (Kimi Code) ← current'); + expect(pickerOutput).toContain('❯ Kimi Turbo (Kimi Code)'); + (picker as ModelSelectorComponent).handleInput('\u001B[D'); + (picker as ModelSelectorComponent).handleInput('\r'); + + await vi.waitFor(() => { + expect(session.setModel).toHaveBeenCalledWith('turbo'); + expect(session.setThinking).toHaveBeenCalledWith('on'); + }); + expect(driver.state.appState.model).toBe('turbo'); + expect(driver.state.appState.thinking).toBe(true); + }); + + it('deletes Kitty inline images when /new clears the transcript', async () => { + setCapabilities({ images: 'kitty', trueColor: true, hyperlinks: true }); + const { driver, harness } = await makeDriver(makeSession({ id: 'ses-1' })); + const nextSession = makeSession({ id: 'ses-2' }); + harness.createSession.mockResolvedValueOnce(nextSession); + const write = vi.spyOn(driver.state.terminal, 'write').mockImplementation(() => {}); + + driver.handleUserInput('/new'); + + await vi.waitFor(() => { + expect(harness.createSession).toHaveBeenCalledTimes(2); + expect(driver.getCurrentSessionId()).toBe('ses-2'); + }); + expect(write).toHaveBeenCalledWith(deleteAllKittyImages()); + }); + + it('forks the active session and switches to the returned session', async () => { + const originalTitle = process.title; + const source = makeSession({ + id: 'ses-source', + summary: { title: 'Source title' }, + }); + const forked = makeSession({ + id: 'ses-fork', + summary: { title: 'Fork: Source title' }, + }); + const forkSession = vi.fn(async () => forked); + const { driver, harness } = await makeDriver(source, { forkSession }); + + try { + driver.handleUserInput('/fork ignored args'); + + await vi.waitFor(() => { + expect(forkSession).toHaveBeenCalledWith({ + id: 'ses-source', + title: 'Fork: Source title', + }); + expect(driver.getCurrentSessionId()).toBe('ses-fork'); + }); + expect(process.title).toBe('Fork: Source title'); + expect(source.close).toHaveBeenCalledOnce(); + expect(forked.onEvent).toHaveBeenCalledOnce(); + expect(harness.resumeSession).not.toHaveBeenCalled(); + expect(driver.state.transcriptContainer.render(120).join('\n')).toContain( + 'Session forked (ses-fork).', + ); + } finally { + process.title = originalTitle; + } + }); + + it('keeps the current session when fork fails', async () => { + const forkSession = vi.fn(async () => { + throw new Error('fork unavailable'); + }); + const { driver } = await makeDriver(makeSession({ id: 'ses-source' }), { forkSession }); + + driver.handleUserInput('/fork'); + + await vi.waitFor(() => { + expect(forkSession).toHaveBeenCalledWith({ + id: 'ses-source', + title: 'Fork: ses-source', + }); + expect(driver.getCurrentSessionId()).toBe('ses-source'); + expect(driver.state.transcriptContainer.render(120).join('\n')).toContain( + 'Failed to fork session: fork unavailable', + ); + }); + }); + + it('renders newly streamed thinking expanded when ctrl+o toggle was already active', async () => { + const { driver } = await makeDriver(); + driver.state.toolOutputExpanded = true; + + const longThinking = ['t1', 't2', 't3', 't4', 't5', 't6', 't7'].join('\n'); + driver.handleEvent( + { + type: 'thinking.delta', + agentId: 'main', + sessionId: 'ses-1', + delta: longThinking, + } as Event, + vi.fn(), + ); + driver.handleEvent( + { + type: 'assistant.delta', + agentId: 'main', + sessionId: 'ses-1', + delta: 'answer', + } as Event, + vi.fn(), + ); + + const transcript = stripSgr(renderTranscript(driver)); + expect(transcript).toContain('t7'); + expect(transcript).not.toContain('ctrl+o to expand'); + }); + + it('renders hook results without XML tags', async () => { + const { driver } = await makeDriver(); + + driver.handleEvent( + { + type: 'hook.result', + agentId: 'main', + sessionId: 'ses-1', + turnId: 1, + hookEvent: 'UserPromptSubmit', + content: '{}', + } as Event, + vi.fn(), + ); + + const transcript = stripSgr(renderTranscript(driver)); + expect(transcript).toContain('UserPromptSubmit hook'); + expect(transcript).toContain('{}'); + expect(transcript).not.toContain('<hook_result'); + }); + + it('renders empty hook results as empty status text', async () => { + const { driver } = await makeDriver(); + + driver.handleEvent( + { + type: 'hook.result', + agentId: 'main', + sessionId: 'ses-1', + turnId: 1, + hookEvent: 'UserPromptSubmit', + content: '', + } as Event, + vi.fn(), + ); + + const transcript = stripSgr(renderTranscript(driver)); + expect(transcript).toContain('UserPromptSubmit hook'); + expect(transcript).toContain('(empty)'); + expect(transcript).not.toContain('<hook_result'); + }); +}); diff --git a/apps/kimi-code/test/tui/kimi-tui-startup.test.ts b/apps/kimi-code/test/tui/kimi-tui-startup.test.ts new file mode 100644 index 000000000..9d93ebfd5 --- /dev/null +++ b/apps/kimi-code/test/tui/kimi-tui-startup.test.ts @@ -0,0 +1,678 @@ +import { describe, expect, it, vi } from "vitest"; + +import type { MigrationPlan } from "@moonshot-ai/migration-legacy"; +import { log } from "@moonshot-ai/kimi-code-sdk"; + +import { KimiTUI, type KimiTUIStartupInput, type TUIState } from "#/tui/kimi-tui"; +import { + DISABLE_TERMINAL_THEME_REPORTING, + ENABLE_TERMINAL_THEME_REPORTING, + OSC11_QUERY, + QUERY_TERMINAL_THEME, + TERMINAL_THEME_LIGHT, +} from "#/tui/utils/terminal-theme"; + +interface StartupDriver { + state: TUIState; + init(): Promise<boolean>; + handleLoginCommand(): Promise<void>; + handleLogoutCommand(): Promise<void>; +} + +interface ThemeTrackingDriver extends StartupDriver { + refreshTerminalThemeTracking(): void; +} + +interface MigrateExitDriver extends StartupDriver { + start(): Promise<void>; + onExit?: (code?: number) => Promise<void>; + runMigrationScreen(plan: unknown): Promise<unknown>; + initMainTui(): Promise<boolean>; + terminalFocusTrackingDispose?: () => void; +} + +const MIGRATION_PLAN: MigrationPlan = { + sourceHome: "/x/.kimi", + hasConfig: false, + hasMcp: false, + hasUserHistory: false, + oauthCredentials: [], + workdirs: [], + detectedPlugins: [], + detectedMcpOauthServers: [], + totalSessions: 0, +}; + +function makeStartupInput( + cliOptions: Partial<KimiTUIStartupInput["cliOptions"]> = {}, + tuiConfig: Partial<KimiTUIStartupInput["tuiConfig"]> = {}, + resolvedTheme: KimiTUIStartupInput["resolvedTheme"] = "dark", +): KimiTUIStartupInput { + return { + cliOptions: { + session: undefined, + continue: false, + yolo: false, + plan: false, + model: undefined, + outputFormat: undefined, + prompt: undefined, + skillsDirs: [], + ...cliOptions, + }, + tuiConfig: { + theme: "dark", + editorCommand: null, + notifications: { enabled: true, condition: "unfocused" }, + ...tuiConfig, + }, + version: "0.0.0-test", + workDir: "/tmp/proj-a", + resolvedTheme, + }; +} + +function makeSession(overrides: Record<string, unknown> = {}) { + return { + id: "ses-1", + model: "k2", + summary: { title: "Session title" }, + getStatus: vi.fn(async () => ({ + model: "k2", + thinkingLevel: "off", + permission: "manual", + planMode: false, + contextTokens: 10, + maxContextTokens: 100, + contextUsage: 0.1, + })), + setApprovalHandler: vi.fn(), + setQuestionHandler: vi.fn(), + setModel: vi.fn(async () => {}), + setThinking: vi.fn(async () => {}), + setPermission: vi.fn(async () => {}), + setPlanMode: vi.fn(async () => {}), + onEvent: vi.fn(() => () => {}), + listSkills: vi.fn(async () => []), + close: vi.fn(async () => {}), + ...overrides, + }; +} + +function loginRequiredError(): Error & { readonly code: string } { + return Object.assign(new Error('OAuth provider "managed:kimi-code" requires login.'), { + code: "auth.login_required", + }); +} + +function makeHarness(session = makeSession(), overrides: Record<string, unknown> = {}) { + return { + getConfig: vi.fn(async () => ({ + models: { + k2: { model: "moonshot-v1", maxContextSize: 100 }, + }, + })), + createSession: vi.fn(async () => session), + resumeSession: vi.fn(async () => session), + listSessions: vi.fn(async () => []), + close: vi.fn(async () => {}), + track: vi.fn(), + setTelemetryContext: vi.fn(), + auth: { + status: vi.fn(async () => ({ providers: [] })), + login: vi.fn(async () => {}), + logout: vi.fn(), + getManagedUsage: vi.fn(), + }, + ...overrides, + }; +} + +function makeDriver(harness: ReturnType<typeof makeHarness>, input: KimiTUIStartupInput) { + const driver = new KimiTUI(harness as never, input) as unknown as StartupDriver; + vi.spyOn(driver.state.ui, "requestRender").mockImplementation(() => {}); + vi.spyOn(driver.state.terminal, "setProgress").mockImplementation(() => {}); + return driver; +} + +type InputListener = Parameters<TUIState["ui"]["addInputListener"]>[0]; +const DARK_OSC11_REPORT = "\u001B]11;rgb:2828/2c2c/3434\u0007"; +const LIGHT_OSC11_REPORT = "\u001B]11;rgb:fafa/fbfb/fcfc\u0007"; + +function captureInputListeners(driver: StartupDriver) { + const listeners: InputListener[] = []; + const removeInputListener = vi.fn<() => void>(); + const write = vi.spyOn(driver.state.terminal, "write").mockImplementation(() => {}); + const addInputListener = vi + .spyOn(driver.state.ui, "addInputListener") + .mockImplementation((listener: InputListener) => { + listeners.push(listener); + return removeInputListener; + }); + + return { listeners, removeInputListener, write, addInputListener }; +} + +describe("KimiTUI startup", () => { + it("creates a fresh session from startup flags and syncs runtime state", async () => { + const session = makeSession({ + getStatus: vi.fn(async () => ({ + model: "k2", + thinkingLevel: "off", + permission: "yolo", + planMode: true, + contextTokens: 25, + maxContextTokens: 200, + contextUsage: 0.125, + })), + }); + const harness = makeHarness(session); + const driver = makeDriver(harness, makeStartupInput({ yolo: true, plan: true })); + + await expect(driver.init()).resolves.toBe(false); + + expect(harness.createSession).toHaveBeenCalledWith({ + workDir: "/tmp/proj-a", + permission: "yolo", + planMode: true, + }); + expect(session.setApprovalHandler).toHaveBeenCalledOnce(); + expect(session.setQuestionHandler).toHaveBeenCalledOnce(); + expect(harness.setTelemetryContext).toHaveBeenCalledWith({ sessionId: null }); + expect(harness.setTelemetryContext).toHaveBeenLastCalledWith({ sessionId: "ses-1" }); + expect(driver.state.startupState).toBe("ready"); + expect(driver.state.appState).toMatchObject({ + sessionId: "ses-1", + model: "k2", + permissionMode: "yolo", + yolo: true, + planMode: true, + contextTokens: 25, + maxContextTokens: 200, + contextUsage: 0.125, + sessionTitle: "Session title", + }); + }); + + it("resumes the latest session for --continue and marks history for replay", async () => { + const session = makeSession({ id: "ses-latest" }); + const harness = makeHarness(session, { + listSessions: vi.fn(async () => [{ id: "ses-latest" }, { id: "ses-old" }]), + }); + const driver = makeDriver(harness, makeStartupInput({ continue: true })); + + await expect(driver.init()).resolves.toBe(true); + + expect(harness.resumeSession).toHaveBeenCalledWith({ id: "ses-latest" }); + expect(harness.createSession).not.toHaveBeenCalled(); + expect(driver.state.startupState).toBe("ready"); + expect(driver.state.appState.sessionId).toBe("ses-latest"); + }); + + it("passes the CLI model override when creating a fresh startup session", async () => { + const harness = makeHarness(); + const driver = makeDriver(harness, makeStartupInput({ model: "kimi-code/k2.5" })); + + await expect(driver.init()).resolves.toBe(false); + + expect(harness.createSession).toHaveBeenCalledWith({ + workDir: "/tmp/proj-a", + model: "kimi-code/k2.5", + permission: undefined, + planMode: undefined, + }); + }); + + it("applies the CLI model override when resuming a startup session", async () => { + let model = "k2"; + const session = makeSession({ + setModel: vi.fn(async (nextModel: string) => { + model = nextModel; + }), + getStatus: vi.fn(async () => ({ + model, + thinkingLevel: "off", + permission: "manual", + planMode: false, + contextTokens: 10, + maxContextTokens: 100, + contextUsage: 0.1, + })), + }); + const harness = makeHarness(session, { + listSessions: vi.fn(async () => [{ id: "ses-latest" }]), + }); + const driver = makeDriver( + harness, + makeStartupInput({ continue: true, model: "kimi-code/k2.5" }), + ); + + await expect(driver.init()).resolves.toBe(true); + + expect(session.setModel).toHaveBeenCalledWith("kimi-code/k2.5"); + expect(driver.state.appState.model).toBe("kimi-code/k2.5"); + }); + + it("enters picker startup for bare --session without creating a session", async () => { + const harness = makeHarness(); + const driver = makeDriver(harness, makeStartupInput({ session: "" })); + + await expect(driver.init()).resolves.toBe(false); + + expect(harness.createSession).not.toHaveBeenCalled(); + expect(harness.resumeSession).not.toHaveBeenCalled(); + expect(driver.state.startupState).toBe("picker"); + }); + + it("tracks terminal theme reports while auto theme is active", () => { + const harness = makeHarness(); + const driver = makeDriver( + harness, + makeStartupInput({}, { theme: "auto" }, "dark"), + ) as unknown as ThemeTrackingDriver; + const { listeners, write, addInputListener } = captureInputListeners(driver); + + driver.refreshTerminalThemeTracking(); + + expect(addInputListener).toHaveBeenCalledOnce(); + expect(write).toHaveBeenCalledWith(ENABLE_TERMINAL_THEME_REPORTING); + expect(write).toHaveBeenCalledWith(OSC11_QUERY); + expect(write).toHaveBeenCalledWith(QUERY_TERMINAL_THEME); + expect(listeners).toHaveLength(1); + + write.mockClear(); + expect(listeners[0]?.(TERMINAL_THEME_LIGHT)).toEqual({ consume: true }); + expect(write).toHaveBeenCalledWith(OSC11_QUERY); + expect(driver.state.appState.theme).toBe("auto"); + expect(driver.state.theme.resolvedTheme).toBe("dark"); + expect(driver.state.ui.requestRender).not.toHaveBeenCalled(); + + expect(listeners[0]?.(DARK_OSC11_REPORT)).toEqual({ consume: true }); + expect(driver.state.appState.theme).toBe("auto"); + expect(driver.state.theme.resolvedTheme).toBe("dark"); + expect(driver.state.ui.requestRender).not.toHaveBeenCalled(); + + expect(listeners[0]?.(LIGHT_OSC11_REPORT)).toEqual({ consume: true }); + expect(driver.state.appState.theme).toBe("auto"); + expect(driver.state.theme.resolvedTheme).toBe("light"); + expect(driver.state.ui.requestRender).toHaveBeenCalled(); + }); + + it("does not track terminal theme reports for explicit themes", () => { + const harness = makeHarness(); + const driver = makeDriver(harness, makeStartupInput()) as unknown as ThemeTrackingDriver; + const { write, addInputListener } = captureInputListeners(driver); + + driver.refreshTerminalThemeTracking(); + + expect(addInputListener).not.toHaveBeenCalled(); + expect(write).not.toHaveBeenCalled(); + }); + + it("disables terminal theme reports after leaving auto theme", () => { + const harness = makeHarness(); + const driver = makeDriver( + harness, + makeStartupInput({}, { theme: "auto" }, "dark"), + ) as unknown as ThemeTrackingDriver; + const { write, removeInputListener } = captureInputListeners(driver); + + driver.refreshTerminalThemeTracking(); + driver.state.appState.theme = "dark"; + driver.refreshTerminalThemeTracking(); + + expect(removeInputListener).toHaveBeenCalledOnce(); + expect(write).toHaveBeenCalledWith(DISABLE_TERMINAL_THEME_REPORTING); + }); + + it("starts TUI without a session when fresh startup needs OAuth login", async () => { + const harness = makeHarness(makeSession(), { + createSession: vi.fn(async () => { + throw loginRequiredError(); + }), + }); + const driver = makeDriver(harness, makeStartupInput()); + + await expect(driver.init()).resolves.toBe(false); + + expect(driver.state.startupState).toBe("ready"); + expect(driver.state.startupNotice).toContain("OAuth login expired"); + expect(driver.state.appState).toMatchObject({ + sessionId: "", + model: "", + thinking: false, + contextTokens: 0, + maxContextTokens: 0, + contextUsage: 0, + sessionTitle: null, + }); + }); + + it("preserves fresh startup yolo and plan intent after OAuth login", async () => { + const session = makeSession({ + getStatus: vi.fn(async () => ({ + model: "k2", + thinkingLevel: "off", + permission: "yolo", + planMode: true, + contextTokens: 10, + maxContextTokens: 100, + contextUsage: 0.1, + })), + }); + const createSession = vi + .fn() + .mockRejectedValueOnce(loginRequiredError()) + .mockResolvedValueOnce(session); + const harness = makeHarness(session, { + getConfig: vi.fn(async () => ({ + defaultModel: "k2", + defaultThinking: false, + models: { + k2: { model: "moonshot-v1", maxContextSize: 100 }, + }, + })), + createSession, + }); + const driver = makeDriver(harness, makeStartupInput({ yolo: true, plan: true })); + + await expect(driver.init()).resolves.toBe(false); + + expect(driver.state.appState).toMatchObject({ + sessionId: "", + model: "", + permissionMode: "yolo", + yolo: true, + planMode: true, + }); + + vi.spyOn(driver as any, 'promptPlatformSelection').mockResolvedValue('kimi-code'); + await driver.handleLoginCommand(); + + expect(createSession).toHaveBeenNthCalledWith(1, { + workDir: "/tmp/proj-a", + permission: "yolo", + planMode: true, + }); + expect(createSession).toHaveBeenNthCalledWith(2, { + workDir: "/tmp/proj-a", + model: "k2", + thinking: "off", + permission: "yolo", + planMode: true, + }); + expect(driver.state.appState).toMatchObject({ + sessionId: "ses-1", + model: "k2", + permissionMode: "yolo", + yolo: true, + planMode: true, + }); + }); + + it("does not force manual permission after OAuth login without --yolo", async () => { + const session = makeSession({ + getStatus: vi.fn(async () => ({ + model: "k2", + thinkingLevel: "off", + permission: "auto", + planMode: false, + contextTokens: 10, + maxContextTokens: 100, + contextUsage: 0.1, + })), + }); + const createSession = vi + .fn() + .mockRejectedValueOnce(loginRequiredError()) + .mockResolvedValueOnce(session); + const harness = makeHarness(session, { + getConfig: vi.fn(async () => ({ + defaultModel: "k2", + defaultThinking: false, + models: { + k2: { model: "moonshot-v1", maxContextSize: 100 }, + }, + })), + createSession, + }); + const driver = makeDriver(harness, makeStartupInput()); + + await expect(driver.init()).resolves.toBe(false); + vi.spyOn(driver as any, 'promptPlatformSelection').mockResolvedValue('kimi-code'); + await driver.handleLoginCommand(); + + expect(createSession).toHaveBeenNthCalledWith(2, { + workDir: "/tmp/proj-a", + model: "k2", + thinking: "off", + permission: undefined, + planMode: undefined, + }); + expect(driver.state.appState).toMatchObject({ + permissionMode: "auto", + yolo: false, + }); + }); + + it("syncs configured thinking after OAuth login refreshes an active session", async () => { + const session = makeSession(); + const harness = makeHarness(session, { + getConfig: vi.fn(async () => ({ + defaultModel: "k2", + defaultThinking: true, + models: { + k2: { model: "moonshot-v1", maxContextSize: 100 }, + }, + })), + }); + const driver = makeDriver(harness, makeStartupInput()); + + await expect(driver.init()).resolves.toBe(false); + expect(driver.state.appState.thinking).toBe(false); + + vi.spyOn(driver as any, 'promptPlatformSelection').mockResolvedValue('kimi-code'); + await driver.handleLoginCommand(); + + expect(session.setModel).toHaveBeenCalledWith("k2"); + expect(session.setThinking).toHaveBeenCalledWith("on"); + expect(driver.state.appState).toMatchObject({ + model: "k2", + thinking: true, + maxContextTokens: 100, + }); + expect(harness.track).toHaveBeenCalledWith("login", { + provider: "managed:kimi-code", + already_logged_in: false, + }); + }); + + it("tracks login with already_logged_in when a token already exists", async () => { + const session = makeSession(); + const harness = makeHarness(session, { + auth: { + status: vi.fn(async () => ({ + providers: [{ providerName: "managed:kimi-code", hasToken: true }], + })), + login: vi.fn(async () => {}), + logout: vi.fn(), + getManagedUsage: vi.fn(), + }, + }); + const driver = makeDriver(harness, makeStartupInput()); + + await expect(driver.init()).resolves.toBe(false); + harness.track.mockClear(); + + vi.spyOn(driver as any, 'promptPlatformSelection').mockResolvedValue('kimi-code'); + await driver.handleLoginCommand(); + + expect(harness.auth.login).toHaveBeenCalledWith( + "managed:kimi-code", + expect.objectContaining({ + signal: expect.any(AbortSignal), + onDeviceCode: expect.any(Function), + }), + ); + expect(harness.track).toHaveBeenCalledWith("login", { + provider: "managed:kimi-code", + already_logged_in: true, + }); + }); + + it("logs login failures with session context", async () => { + const warn = vi.spyOn(log, "warn").mockImplementation(() => {}); + const session = makeSession(); + const loginError = new Error("Failed to list Kimi Code models (HTTP 402)."); + const harness = makeHarness(session, { + auth: { + status: vi.fn(async () => ({ providers: [] })), + login: vi.fn(async () => { + throw loginError; + }), + logout: vi.fn(), + getManagedUsage: vi.fn(), + }, + }); + const driver = makeDriver(harness, makeStartupInput()); + + try { + await expect(driver.init()).resolves.toBe(false); + + vi.spyOn(driver as any, 'promptPlatformSelection').mockResolvedValue('kimi-code'); + await driver.handleLoginCommand(); + + expect(harness.auth.login).toHaveBeenCalledWith( + "managed:kimi-code", + expect.objectContaining({ + signal: expect.any(AbortSignal), + onDeviceCode: expect.any(Function), + }), + ); + expect(warn).toHaveBeenCalledWith( + "login failed", + expect.objectContaining({ + providerName: "managed:kimi-code", + alreadyLoggedIn: false, + sessionId: "ses-1", + error: expect.objectContaining({ + message: "Failed to list Kimi Code models (HTTP 402).", + }), + }), + ); + } finally { + warn.mockRestore(); + } + }); + + it("tracks logout after managed credentials and session state are cleared", async () => { + const session = makeSession(); + const harness = makeHarness(session); + const driver = makeDriver(harness, makeStartupInput()); + + await expect(driver.init()).resolves.toBe(false); + harness.track.mockClear(); + + await driver.handleLogoutCommand(); + + expect(harness.auth.logout).toHaveBeenCalledWith("managed:kimi-code"); + expect(session.close).toHaveBeenCalledOnce(); + expect(driver.state.appState).toMatchObject({ + sessionId: "", + model: "", + sessionTitle: null, + }); + expect(harness.track).toHaveBeenCalledWith("logout", { provider: "managed:kimi-code" }); + }); + + it("starts TUI without replaying when --continue needs OAuth login", async () => { + const harness = makeHarness(makeSession(), { + listSessions: vi.fn(async () => [{ id: "ses-latest" }]), + resumeSession: vi.fn(async () => { + throw loginRequiredError(); + }), + }); + const driver = makeDriver(harness, makeStartupInput({ continue: true })); + + await expect(driver.init()).resolves.toBe(false); + + expect(harness.resumeSession).toHaveBeenCalledWith({ id: "ses-latest" }); + expect(harness.createSession).not.toHaveBeenCalled(); + expect(driver.state.startupState).toBe("ready"); + expect(driver.state.appState.sessionId).toBe(""); + }); + + it("starts TUI without replaying when an explicit resume needs OAuth login", async () => { + const harness = makeHarness(makeSession(), { + listSessions: vi.fn(async () => [{ id: "ses-target" }]), + resumeSession: vi.fn(async () => { + throw loginRequiredError(); + }), + }); + const driver = makeDriver(harness, makeStartupInput({ session: "ses-target" })); + + await expect(driver.init()).resolves.toBe(false); + + expect(harness.resumeSession).toHaveBeenCalledWith({ id: "ses-target" }); + expect(driver.state.startupState).toBe("ready"); + expect(driver.state.appState.sessionId).toBe(""); + }); + + it("disposes terminal focus/theme tracking on the kimi migrate exit", async () => { + const harness = makeHarness(); + const driver = makeDriver(harness, { + ...makeStartupInput(), + migrationPlan: MIGRATION_PLAN, + migrateOnly: true, + }) as unknown as MigrateExitDriver; + // pi-tui start/stop and focus tracking touch the real TTY — stub the I/O. + vi.spyOn(driver.state.ui, "start").mockImplementation(() => {}); + vi.spyOn(driver.state.ui, "stop").mockImplementation(() => {}); + vi.spyOn(driver.state.terminal, "write").mockImplementation(() => {}); + // The migration screen would await user input; resolve it immediately. + vi.spyOn(driver, "runMigrationScreen").mockResolvedValue({ decision: "later" }); + const onExit = vi.fn(async () => {}); + driver.onExit = onExit; + + await driver.start(); + + // `kimi migrate` exits via process.exit; startEventLoop() installed focus + // tracking, so the exit path must dispose it — otherwise the terminal + // keeps emitting focus/OSC sequences after the command finishes. + expect(driver.terminalFocusTrackingDispose).toBeUndefined(); + expect(onExit).toHaveBeenCalledWith(0); + }); + + it("disposes terminal tracking when post-migration startup fails", async () => { + const harness = makeHarness(); + const driver = makeDriver(harness, { + ...makeStartupInput(), + migrationPlan: MIGRATION_PLAN, + migrateOnly: false, + }) as unknown as MigrateExitDriver; + vi.spyOn(driver.state.ui, "start").mockImplementation(() => {}); + vi.spyOn(driver.state.ui, "stop").mockImplementation(() => {}); + vi.spyOn(driver.state.terminal, "write").mockImplementation(() => {}); + // The migration screen resolves "later"; startup then continues into + // initMainTui(), which fails (e.g. a session-resume error). + vi.spyOn(driver, "runMigrationScreen").mockResolvedValue({ decision: "later" }); + vi.spyOn(driver, "initMainTui").mockRejectedValue(new Error("resume boom")); + + await expect(driver.start()).rejects.toThrow("resume boom"); + + // The focus tracking installed by startEventLoop() must be torn down + // before the error propagates — not left active after the process exits. + expect(driver.terminalFocusTrackingDispose).toBeUndefined(); + }); + + it("keeps non-login startup session errors fatal", async () => { + const harness = makeHarness(makeSession(), { + createSession: vi.fn(async () => { + throw new Error("provider config is invalid"); + }), + }); + const driver = makeDriver(harness, makeStartupInput()); + + await expect(driver.init()).rejects.toThrow("provider config is invalid"); + }); +}); diff --git a/apps/kimi-code/test/tui/media-url.test.ts b/apps/kimi-code/test/tui/media-url.test.ts new file mode 100644 index 000000000..79c73b9f9 --- /dev/null +++ b/apps/kimi-code/test/tui/media-url.test.ts @@ -0,0 +1,37 @@ +import { describe, expect, it } from 'vitest'; + +import { mediaUrlPartToText, summarizeDataUrl } from '#/tui/utils/media-url'; + +describe('mediaUrlPartToText', () => { + it('keeps non-data URLs as escaped XML-like references', () => { + expect(mediaUrlPartToText('image', 'file:///tmp/a&b".png')).toBe( + '<image url="file:///tmp/a&b".png">', + ); + }); + + it('summarizes base64 data URLs without returning the payload', () => { + expect(mediaUrlPartToText('image', 'data:image/png;base64,qrs=')).toBe( + '[image image/png, 2 B]', + ); + }); + + it('formats larger base64 payload sizes compactly', () => { + const oneKib = 'A'.repeat(1368); + expect(mediaUrlPartToText('video', `data:video/mp4;base64,${oneKib}`)).toBe( + '[video video/mp4, 1.0 KB]', + ); + }); +}); + +describe('summarizeDataUrl', () => { + it('returns undefined for regular URLs', () => { + expect(summarizeDataUrl('https://example.com/a.png')).toBeUndefined(); + }); + + it('parses MIME and decoded byte count for base64 data URLs', () => { + expect(summarizeDataUrl('data:image/png;base64,AQIDBA==')).toEqual({ + mime: 'image/png', + bytes: 4, + }); + }); +}); diff --git a/apps/kimi-code/test/tui/printable-key-guard.test.ts b/apps/kimi-code/test/tui/printable-key-guard.test.ts new file mode 100644 index 000000000..f7be25b1c --- /dev/null +++ b/apps/kimi-code/test/tui/printable-key-guard.test.ts @@ -0,0 +1,69 @@ +/** + * Guard test: scan every TUI component and reject `data === '<printable>'` + * bare-literal comparisons. When the terminal enables the Kitty keyboard + * protocol (e.g. the VSCode integrated terminal), printable keys arrive as + * CSI-u sequences, so a bare comparison silently disables the shortcut. + * See `apps/kimi-code/src/tui/utils/printable-key.ts`. + * + * Every printable-character comparison must first go through + * `printableChar(data)`. Control characters (codepoint < 32) should use + * `matchesKey` with `Key.*` or stay as escape literals (`'\t'`, ...); + * those are exempted by the guard's regex. + */ + +import { readdirSync, readFileSync, statSync } from 'node:fs'; +import { join } from 'node:path'; + +import { describe, expect, it } from 'vitest'; + +const COMPONENTS_ROOT = join(__dirname, '..', '..', 'src', 'tui', 'components'); + +function walk(dir: string): string[] { + const out: string[] = []; + for (const entry of readdirSync(dir)) { + const p = join(dir, entry); + const s = statSync(p); + if (s.isDirectory()) out.push(...walk(p)); + else if (entry.endsWith('.ts') || entry.endsWith('.tsx')) out.push(p); + } + return out; +} + +// Single-character visible-ASCII (codepoint 32-126) bare-literal +// comparisons, e.g. `data === 'q'`, `data === ' '`. The regex deliberately +// permits escape sequences (`data === '\t'`, control-char literals), +// multi-character sequences (`data === '[A'`), and comparisons on +// variables other than `data` (the decoded value is usually `k` or +// `printable`). +const BARE_PRINTABLE = /\bdata\s*===\s*'([\u0020-\u007E])'/g; + +describe('TUI handleInput — printable-key guard', () => { + it('forbids bare-literal printable comparisons on `data` (use printableChar)', () => { + const offenders: { file: string; line: number; snippet: string }[] = []; + for (const file of walk(COMPONENTS_ROOT)) { + const content = readFileSync(file, 'utf8'); + const lines = content.split('\n'); + for (let i = 0; i < lines.length; i++) { + const line = lines[i] ?? ''; + if (line.trimStart().startsWith('//')) continue; + BARE_PRINTABLE.lastIndex = 0; + let m: RegExpExecArray | null; + while ((m = BARE_PRINTABLE.exec(line)) !== null) { + offenders.push({ + file: file.slice(file.indexOf('src/')), + line: i + 1, + snippet: line.trim(), + }); + } + } + } + expect( + offenders, + `Found bare-literal \`data === '...'\` comparisons. ` + + `In VSCode/Kitty terminals these never match because keys arrive as ` + + `CSI-u sequences. Use \`printableChar(data)\` from ` + + `\`@/tui/utils/printable-key\` and compare the decoded value instead.\n` + + offenders.map((o) => ` ${o.file}:${String(o.line)} ${o.snippet}`).join('\n'), + ).toEqual([]); + }); +}); diff --git a/apps/kimi-code/test/tui/replay-ops.test.ts b/apps/kimi-code/test/tui/replay-ops.test.ts new file mode 100644 index 000000000..2a366e6cc --- /dev/null +++ b/apps/kimi-code/test/tui/replay-ops.test.ts @@ -0,0 +1,566 @@ +import type { + AgentReplayRecord, + BackgroundTaskInfo, + ContentPart, + PromptOrigin, + Role, + ToolCall, +} from '@moonshot-ai/kimi-code-sdk'; +import { describe, expect, it } from 'vitest'; + +import { projectReplayRecords } from '#/tui/actions/replay-ops'; + +interface ReplayMessageExtra { + readonly toolCalls?: readonly ToolCall[]; + readonly toolCallId?: string; + readonly origin?: PromptOrigin; + readonly isError?: boolean; +} + +function message( + role: Role, + content: readonly ContentPart[], + extra: ReplayMessageExtra = {}, +): AgentReplayRecord { + return { + type: 'message', + message: { + role, + content: [...content], + toolCalls: [...(extra.toolCalls ?? [])], + toolCallId: extra.toolCallId, + origin: extra.origin, + isError: extra.isError, + }, + }; +} + +function backgroundTask( + taskId: string, + description: string, + status: BackgroundTaskInfo['status'] = 'running', +): BackgroundTaskInfo { + return { + taskId, + command: `[agent] ${description}`, + description, + status, + pid: 0, + exitCode: status === 'completed' ? 0 : null, + startedAt: 1, + endedAt: status === 'running' || status === 'awaiting_approval' ? null : 2, + }; +} + +describe('projectReplayRecords', () => { + it('projects only the most recent ten visible user turns from agent replay', () => { + const projected = projectReplayRecords( + Array.from({ length: 12 }, (_, index) => [ + message('user', [{ type: 'text', text: `prompt ${index}` }]), + message('assistant', [{ type: 'text', text: `answer ${index}` }]), + ]).flat(), + ); + + expect( + projected.entries.filter((entry) => entry.kind === 'user').map((entry) => entry.content), + ).toEqual([ + 'prompt 2', + 'prompt 3', + 'prompt 4', + 'prompt 5', + 'prompt 6', + 'prompt 7', + 'prompt 8', + 'prompt 9', + 'prompt 10', + 'prompt 11', + ]); + expect( + projected.entries.filter((entry) => entry.kind === 'assistant').map((entry) => entry.content), + ).toEqual([ + 'answer 2', + 'answer 3', + 'answer 4', + 'answer 5', + 'answer 6', + 'answer 7', + 'answer 8', + 'answer 9', + 'answer 10', + 'answer 11', + ]); + }); + + it('does not count model-triggered skill activations as user turns', () => { + const records: AgentReplayRecord[] = Array.from({ length: 9 }, (_, index) => [ + message('user', [{ type: 'text', text: `prompt ${index}` }]), + message('assistant', [{ type: 'text', text: `answer ${index}` }]), + ]).flat(); + for (const index of [0, 1, 2, 3]) { + records.push( + message('user', [{ type: 'text', text: `Skill body ${index}` }], { + origin: { + kind: 'skill_activation', + activationId: `act-${index}`, + skillName: 'review', + trigger: 'model-tool', + }, + }), + ); + } + + const projected = projectReplayRecords(records); + + expect( + projected.entries.filter((entry) => entry.kind === 'user').map((entry) => entry.content), + ).toEqual([ + 'prompt 0', + 'prompt 1', + 'prompt 2', + 'prompt 3', + 'prompt 4', + 'prompt 5', + 'prompt 6', + 'prompt 7', + 'prompt 8', + ]); + expect(projected.entries.filter((entry) => entry.kind === 'skill_activation')).toHaveLength(4); + }); + + it('projects UserPromptSubmit hook results as assistant transcript entries', () => { + const hookResult = + '<hook_result hook_event="UserPromptSubmit">\nhook response 1\n</hook_result>\n<hook_result hook_event="UserPromptSubmit">\nhook response 2\n</hook_result>'; + const projected = projectReplayRecords([ + message('user', [{ type: 'text', text: 'prompt' }]), + message('user', [{ type: 'text', text: hookResult }], { + origin: { kind: 'hook_result', event: 'UserPromptSubmit' }, + }), + message('assistant', [{ type: 'text', text: 'model response' }]), + ]); + + expect(projected.entries.map((e) => [e.kind, e.content])).toEqual([ + ['user', 'prompt'], + [ + 'assistant', + '*UserPromptSubmit hook*\n\nhook response 1\n\n*UserPromptSubmit hook*\n\nhook response 2', + ], + ['assistant', 'model response'], + ]); + }); + + it('projects blocking UserPromptSubmit hook results from replayed assistant entries', () => { + const hookResult = + '<hook_result hook_event="UserPromptSubmit">\nblocked reason\n</hook_result>'; + const projected = projectReplayRecords([ + message('user', [{ type: 'text', text: 'blocked prompt' }]), + message('assistant', [{ type: 'text', text: hookResult }], { + origin: { kind: 'hook_result', event: 'UserPromptSubmit', blocked: true }, + }), + ]); + + expect(projected.entries.map((e) => [e.kind, e.content])).toEqual([ + ['user', 'blocked prompt'], + ['assistant', '*UserPromptSubmit hook blocked*\n\nblocked reason'], + ]); + }); + + it('does not infer blocked UserPromptSubmit hook results from assistant role alone', () => { + const hookResult = + '<hook_result hook_event="UserPromptSubmit">\nlegacy hook response\n</hook_result>'; + const projected = projectReplayRecords([ + message('user', [{ type: 'text', text: 'prompt' }]), + message('assistant', [{ type: 'text', text: hookResult }], { + origin: { kind: 'hook_result', event: 'UserPromptSubmit' }, + }), + ]); + + expect(projected.entries.map((e) => [e.kind, e.content])).toEqual([ + ['user', 'prompt'], + ['assistant', '*UserPromptSubmit hook*\n\nlegacy hook response'], + ]); + }); + + it('preserves literal hook result XML from normal assistant replies', () => { + const hookResult = + '<hook_result hook_event="UserPromptSubmit">\nhook response\n</hook_result>'; + const projected = projectReplayRecords([ + message('user', [{ type: 'text', text: 'show me the hook XML' }]), + message('assistant', [{ type: 'text', text: hookResult }]), + ]); + + expect(projected.entries.map((e) => [e.kind, e.content])).toEqual([ + ['user', 'show me the hook XML'], + ['assistant', hookResult], + ]); + }); + + it('projects user messages plus thinking and assistant content', () => { + const projected = projectReplayRecords([ + message('user', [{ type: 'text', text: 'hello' }]), + message('assistant', [ + { type: 'think', think: 'thinking...' }, + { type: 'text', text: 'answer' }, + ]), + ]); + + expect(projected.entries.map((e) => [e.kind, e.content])).toEqual([ + ['user', 'hello'], + ['thinking', 'thinking...'], + ['assistant', 'answer'], + ]); + }); + + it('projects skill activation origin metadata without exposing the full prompt', () => { + const projected = projectReplayRecords([ + message( + 'user', + [{ type: 'text', text: 'Review the requested file.\n\nUser request:\nsrc/app.ts' }], + { + origin: { + kind: 'skill_activation', + activationId: 'act-1', + skillName: 'review', + skillArgs: 'src/app.ts', + trigger: 'user-slash', + }, + }, + ), + ]); + + expect(projected.entries).toHaveLength(1); + expect(projected.entries[0]).toEqual( + expect.objectContaining({ + kind: 'skill_activation', + content: 'Activated skill: review', + skillActivationId: 'act-1', + skillName: 'review', + skillArgs: 'src/app.ts', + }), + ); + expect(JSON.stringify(projected.entries)).not.toContain('Review the requested file'); + }); + + it('deduplicates replayed skill activation cards by activation id', () => { + const record = message('user', [{ type: 'text', text: 'Skill body' }], { + origin: { + kind: 'skill_activation', + activationId: 'act-1', + skillName: 'review', + trigger: 'user-slash', + }, + }); + + const projected = projectReplayRecords([record, record]); + + expect(projected.entries).toHaveLength(1); + expect(projected.entries[0]).toEqual( + expect.objectContaining({ + kind: 'skill_activation', + skillActivationId: 'act-1', + skillName: 'review', + }), + ); + }); + + it('projects background task notifications as status rows', () => { + const notificationXml = [ + '<notification id="task:agent-bg123:completed" category="task" type="task.completed" source_kind="background_task" source_id="agent-bg123">', + 'Title: Background agent completed', + 'Severity: info', + 'Optimize summary completed.', + '<task-notification>', + 'Subagent detailed output should stay out of the transcript row.', + '</task-notification>', + '</notification>', + ].join('\n'); + const projected = projectReplayRecords([ + message('assistant', [], { + toolCalls: [ + { + type: 'function', + id: 'call_agent', + function: { + name: 'Agent', + arguments: JSON.stringify({ + description: 'Optimize summary', + subagent_type: 'coder', + run_in_background: true, + }), + }, + }, + ], + }), + message('tool', [ + { + type: 'text', + text: [ + 'task_id: agent-bg123', + 'status: running', + 'agent_id: agent-child123', + 'actual_subagent_type: coder', + 'automatic_notification: true', + '', + 'description: Optimize summary', + ].join('\n'), + }, + ], { + toolCallId: 'call_agent', + }), + message('user', [{ type: 'text', text: notificationXml }], { + origin: { + kind: 'background_task', + taskId: 'agent-bg123', + status: 'completed', + notificationId: 'task:agent-bg123:completed', + }, + }), + ], [backgroundTask('agent-bg123', 'Optimize summary', 'completed')]); + + expect(projected.entries.map((entry) => [entry.kind, entry.content])).toEqual([ + ['tool_call', ''], + ['status', 'agent completed in background'], + ]); + expect(projected.entries[1]?.backgroundAgentStatus).toMatchObject({ + phase: 'completed', + headline: 'agent completed in background', + detail: 'Optimize summary', + }); + expect(JSON.stringify(projected.entries)).not.toContain('<notification'); + expect(JSON.stringify(projected.entries)).not.toContain('Subagent detailed output'); + }); + + it('uses background notification origin over XML attributes', () => { + const projected = projectReplayRecords([ + message('user', [ + { + type: 'text', + text: [ + '<notification id="task:wrong:completed" category="task" type="task.completed" source_kind="background_task" source_id="wrong">', + 'Title: Background agent completed', + 'Severity: info', + 'Optimize ch03 lost.', + '</notification>', + ].join('\n'), + }, + ], { + origin: { + kind: 'background_task', + taskId: 'agent-real', + status: 'lost', + notificationId: 'task:agent-real:lost', + }, + }), + ], [backgroundTask('agent-real', 'Real task description', 'lost')]); + + expect(projected.entries).toHaveLength(1); + expect(projected.entries[0]).toEqual( + expect.objectContaining({ + kind: 'status', + content: 'agent lost in background', + backgroundAgentStatus: expect.objectContaining({ + phase: 'failed', + detail: 'Real task description', + }), + }), + ); + }); + + it('renders multimodal user parts as stable placeholders', () => { + const projected = projectReplayRecords([ + message('user', [ + { type: 'text', text: 'look ' }, + { type: 'image_url', imageUrl: { url: 'file:///tmp/a.png' } }, + { type: 'video_url', videoUrl: { url: 'file:///tmp/a.mov' } }, + ]), + ]); + + expect(projected.entries[0]?.content).toBe( + 'look <image url="file:///tmp/a.png"><video url="file:///tmp/a.mov">', + ); + }); + + it('summarizes data URLs in resumed multimodal user parts', () => { + const projected = projectReplayRecords([ + message('user', [ + { type: 'text', text: 'look ' }, + { type: 'image_url', imageUrl: { url: 'data:image/png;base64,qrs=' } }, + { type: 'video_url', videoUrl: { url: 'data:video/mp4;base64,AQIDBA==' } }, + ]), + ]); + + expect(projected.entries[0]?.content).toBe('look [image image/png, 2 B][video video/mp4, 4 B]'); + expect(projected.entries[0]?.content).not.toContain('qrs='); + expect(projected.entries[0]?.content).not.toContain('AQIDBA=='); + }); + + it('pairs tool results with their tool call entry', () => { + const projected = projectReplayRecords([ + message('assistant', [], { + toolCalls: [ + { + type: 'function', + id: 'tc_1', + function: { + name: 'Bash', + arguments: '{"command":"pwd"}', + }, + }, + ], + }), + message('tool', [{ type: 'text', text: 'done' }], { + toolCallId: 'tc_1', + }), + ]); + + expect(projected.entries).toHaveLength(1); + expect(projected.entries[0]?.toolCallData).toMatchObject({ + id: 'tc_1', + name: 'Bash', + result: { tool_call_id: 'tc_1', output: 'done' }, + }); + }); + + it('preserves failed tool result state', () => { + const projected = projectReplayRecords([ + message('assistant', [], { + toolCalls: [ + { + type: 'function', + id: 'tc_1', + function: { + name: 'Bash', + arguments: '{"command":"false"}', + }, + }, + ], + }), + message('tool', [{ type: 'text', text: 'failed' }], { + toolCallId: 'tc_1', + isError: true, + }), + ]); + + expect(projected.entries[0]?.toolCallData?.result).toMatchObject({ + tool_call_id: 'tc_1', + output: 'failed', + is_error: true, + }); + }); + + it('projects resumed assistant text, tool call, and tool result records in order', () => { + const projected = projectReplayRecords([ + message('user', [{ type: 'text', text: 'try call a tool' }]), + message( + 'assistant', + [ + { type: 'think', think: 'I should call Bash.' }, + { type: 'text', text: 'Calling Bash now.' }, + ], + { + toolCalls: [ + { + type: 'function', + id: 'call_resume_bash', + function: { + name: 'Bash', + arguments: '{"command":"echo ok"}', + }, + }, + ], + }, + ), + message('tool', [{ type: 'text', text: 'ok' }], { + toolCallId: 'call_resume_bash', + }), + ]); + + expect(projected.entries.map((entry) => [entry.kind, entry.content])).toEqual([ + ['user', 'try call a tool'], + ['thinking', 'I should call Bash.'], + ['assistant', 'Calling Bash now.'], + ['tool_call', ''], + ]); + expect(projected.entries[3]?.toolCallData).toMatchObject({ + id: 'call_resume_bash', + name: 'Bash', + args: { command: 'echo ok' }, + result: { tool_call_id: 'call_resume_bash', output: 'ok' }, + }); + }); + + it('keeps media-bearing tool results as a JSON envelope', () => { + const mediaContent: ContentPart[] = [ + { type: 'text', text: '<image path="/tmp/a.png">' }, + { type: 'image_url', imageUrl: { url: 'data:image/png;base64,QUJD' } }, + { type: 'text', text: '</image>' }, + ]; + const projected = projectReplayRecords([ + message('assistant', [], { + toolCalls: [ + { + type: 'function', + id: 'tc_media', + function: { + name: 'ReadMediaFile', + arguments: '{"path":"/tmp/a.png"}', + }, + }, + ], + }), + message('tool', mediaContent, { + toolCallId: 'tc_media', + }), + ]); + + const output = projected.entries[0]?.toolCallData?.result?.output ?? ''; + expect(JSON.parse(output)).toEqual(mediaContent); + }); + + it('projects plan, permission, and approval replay records as notices', () => { + const projected = projectReplayRecords([ + { type: 'plan_updated', enabled: true }, + { type: 'permission_updated', mode: 'auto' }, + { type: 'permission_updated', mode: 'yolo' }, + { type: 'permission_updated', mode: 'manual' }, + { + type: 'approval_result', + record: { + turnId: 0, + toolCallId: 'call_bash', + action: 'run command', + toolName: 'Bash', + result: { + decision: 'approved', + scope: 'session', + selectedLabel: 'Approve for this session', + }, + }, + }, + { type: 'plan_updated', enabled: false }, + ]); + + expect(projected.entries.map((e) => [e.kind, e.renderMode, e.content])).toEqual([ + ['status', 'notice', 'Plan mode: ON'], + ['status', 'notice', 'Permission mode: auto'], + ['status', 'notice', 'YOLO mode: ON'], + ['status', 'notice', 'YOLO mode: OFF'], + ['status', 'notice', 'Approved for session: run command'], + ['status', 'notice', 'Plan mode: OFF'], + ]); + expect(projected.entries[2]?.detail).toBe( + 'All actions will be approved automatically. Use with caution.', + ); + }); + + it('ignores config replay records and system injections', () => { + const projected = projectReplayRecords([ + { type: 'config_updated', config: { thinkingLevel: 'off' } }, + message('user', [{ type: 'text', text: 'ignore by origin' }], { + origin: { kind: 'injection', variant: 'plan_mode' }, + }), + message('user', [{ type: 'text', text: 'visible' }]), + ]); + + expect(projected.entries.map((e) => [e.kind, e.content])).toEqual([['user', 'visible']]); + }); +}); diff --git a/apps/kimi-code/test/tui/reverse-rpc/approval-adapter.test.ts b/apps/kimi-code/test/tui/reverse-rpc/approval-adapter.test.ts new file mode 100644 index 000000000..72c2b6141 --- /dev/null +++ b/apps/kimi-code/test/tui/reverse-rpc/approval-adapter.test.ts @@ -0,0 +1,162 @@ +import { describe, expect, it } from 'vitest'; + +import { adaptApprovalRequest, adaptPanelResponse } from '#/tui/reverse-rpc/approval/adapter'; + +describe('approval adapter', () => { + it('adapts generic command displays into shell blocks with approval choices', () => { + const adapted = adaptApprovalRequest( + { + toolCallId: 'tc-1', + toolName: 'EnterPlanMode', + action: 'run', + display: { + kind: 'generic', + summary: 'run', + detail: { + command: 'sudo rm -rf /tmp/cache', + cwd: '/tmp', + }, + }, + }, + ); + + expect(adapted).toMatchObject({ + id: 'tc-1', + tool_call_id: 'tc-1', + tool_name: 'EnterPlanMode', + display: [ + { + type: 'shell', + language: 'bash', + command: 'sudo rm -rf /tmp/cache', + cwd: '/tmp', + danger: 'recursive delete', + }, + ], + }); + expect(adapted.choices.map((choice) => choice.label)).toEqual([ + 'Approve once', + 'Approve for this session', + 'Reject', + 'Reject with feedback', + ]); + }); + + it('emits only a diff block for Edit — no separate file_op title row', () => { + const adapted = adaptApprovalRequest( + { + toolCallId: 'tc-edit', + toolName: 'Edit', + action: 'edit', + display: { + kind: 'generic', + summary: 'edit', + detail: { + file_path: 'src/foo.ts', + old_string: 'a\nb\nc', + new_string: 'a\nB\nc', + }, + }, + }, + ); + + expect(adapted.display).toEqual([ + { type: 'diff', path: 'src/foo.ts', old_text: 'a\nb\nc', new_text: 'a\nB\nc' }, + ]); + }); + + it('emits a file_content block for Write so the new file previews as code, not diff', () => { + const adapted = adaptApprovalRequest( + { + toolCallId: 'tc-write', + toolName: 'Write', + action: 'write', + display: { + kind: 'generic', + summary: 'write', + detail: { + file_path: 'src/new.ts', + content: 'export const x = 1;\nexport const y = 2;', + }, + }, + }, + ); + + expect(adapted.display).toEqual([ + { + type: 'file_content', + path: 'src/new.ts', + content: 'export const x = 1;\nexport const y = 2;', + }, + ]); + }); + + it('omits plan review content from the approval panel while keeping Python-style choices', () => { + const adapted = adaptApprovalRequest({ + toolCallId: 'tc-plan', + toolName: 'ExitPlanMode', + action: 'Review plan', + display: { + kind: 'plan_review', + plan: '# Plan\n\n- Inspect\n- Change\n- Verify', + path: '/tmp/kimi-plan.md', + }, + }); + + expect(adapted.display).toEqual([]); + expect(adapted.choices).toEqual([ + { label: 'Approve', response: 'approved', selected_label: 'Approve' }, + { label: 'Reject', response: 'rejected', selected_label: 'Reject' }, + { + label: 'Revise', + response: 'rejected', + selected_label: 'Revise', + requires_feedback: true, + }, + ]); + }); + + it('renders multi-option plan review choices ahead of reject controls', () => { + const adapted = adaptApprovalRequest({ + toolCallId: 'tc-plan-options', + toolName: 'ExitPlanMode', + action: 'Review plan and choose an option', + display: { + kind: 'plan_review', + plan: '# Plan', + path: '/tmp/kimi-plan.md', + options: [ + { label: 'Approach A', description: 'Small refactor' }, + { label: 'Approach B', description: 'Full refactor' }, + ], + }, + }); + + expect(adapted.choices).toEqual([ + { label: 'Approach A', response: 'approved', selected_label: 'Approach A' }, + { label: 'Approach B', response: 'approved', selected_label: 'Approach B' }, + { label: 'Reject', response: 'rejected', selected_label: 'Reject' }, + { + label: 'Revise', + response: 'rejected', + selected_label: 'Revise', + requires_feedback: true, + }, + ]); + }); + + it('maps approved-for-session responses into core approval payloads', () => { + expect( + adaptPanelResponse({ + response: 'approved_for_session', + feedback: 'looks good', + selected_label: 'Approve for this session', + }), + ).toEqual({ + decision: 'approved', + scope: 'session', + feedback: 'looks good', + selectedLabel: 'Approve for this session', + }); + }); +}); diff --git a/apps/kimi-code/test/tui/reverse-rpc/approval.test.ts b/apps/kimi-code/test/tui/reverse-rpc/approval.test.ts new file mode 100644 index 000000000..c2e29c6c2 --- /dev/null +++ b/apps/kimi-code/test/tui/reverse-rpc/approval.test.ts @@ -0,0 +1,142 @@ +import type { ApprovalRequest } from '@moonshot-ai/kimi-code-sdk'; +import { describe, expect, it, vi } from 'vitest'; + +import { ApprovalController } from '#/tui/reverse-rpc/approval/controller'; +import { createApprovalRequestHandler } from '#/tui/reverse-rpc/approval/handler'; + +function approvalEvent(overrides: Partial<ApprovalRequest> = {}): ApprovalRequest { + return { + toolCallId: 'tc-1', + toolName: 'Bash', + action: 'run command', + display: { + kind: 'generic', + summary: 'run command', + detail: { + command: 'rm -rf /tmp/cache', + cwd: '/tmp', + }, + }, + ...overrides, + }; +} + +describe('approval reverse-rpc', () => { + it('auto-approves queued requests with the same action when the current is approved for session', async () => { + const controller = new ApprovalController(); + const panel = (id: string, action: string) => ({ + id, + tool_call_id: id, + tool_name: 'Bash', + action, + description: '', + display: [], + choices: [], + }); + + const first = controller.show(panel('tc-1', 'run command: ls')); + const second = controller.show(panel('tc-2', 'run command: ls')); + const third = controller.show(panel('tc-3', 'edit src/x.ts')); + const fourth = controller.show(panel('tc-4', 'run command: ls')); + + controller.respond({ decision: 'approved', scope: 'session', feedback: 'ok' }); + + await expect(first).resolves.toEqual({ + decision: 'approved', + scope: 'session', + feedback: 'ok', + }); + // Queued same-action requests inherit a session-scoped approval without + // surfacing another panel. The user's feedback is not carried over — + // it described the first request only. + await expect(second).resolves.toEqual({ decision: 'approved', scope: 'session' }); + await expect(fourth).resolves.toEqual({ decision: 'approved', scope: 'session' }); + // A different-action request still waits for an explicit decision. + expect(controller.hasPending()).toBe(true); + + controller.respond({ decision: 'rejected' }); + await expect(third).resolves.toEqual({ decision: 'rejected' }); + }); + + it('does not auto-approve queued requests when only approved-once is chosen', async () => { + const controller = new ApprovalController(); + const panel = (id: string) => ({ + id, + tool_call_id: id, + tool_name: 'Bash', + action: 'run command: ls', + description: '', + display: [], + choices: [], + }); + + const first = controller.show(panel('tc-1')); + const second = controller.show(panel('tc-2')); + + controller.respond({ decision: 'approved' }); + + await expect(first).resolves.toEqual({ decision: 'approved' }); + // The second same-action request must NOT be auto-resolved — approve-once + // is a one-shot decision, not a session rule. + expect(controller.hasPending()).toBe(true); + controller.respond({ decision: 'approved' }); + await expect(second).resolves.toEqual({ decision: 'approved' }); + }); + + it('ApprovalController cancels pending requests with a cancelled response', async () => { + const controller = new ApprovalController(); + const pending = controller.show({ + id: 'req-1', + tool_call_id: 'tc-1', + tool_name: 'Bash', + action: 'run', + description: '', + display: [], + choices: [], + }); + + controller.cancelAll('closed'); + + await expect(pending).resolves.toEqual({ + decision: 'cancelled', + feedback: 'closed', + }); + }); + + it('adapts approval payloads through the handler and falls back on failure', async () => { + const controller = new ApprovalController(); + const show = vi.spyOn(controller, 'show').mockResolvedValue({ + decision: 'approved', + scope: 'session', + feedback: 'looks good', + }); + const handler = createApprovalRequestHandler(controller); + + await expect(handler(approvalEvent())).resolves.toEqual({ + decision: 'approved', + scope: 'session', + feedback: 'looks good', + }); + expect(show).toHaveBeenCalledWith( + expect.objectContaining({ + id: 'tc-1', + tool_call_id: 'tc-1', + tool_name: 'Bash', + display: [ + expect.objectContaining({ + type: 'shell', + command: 'rm -rf /tmp/cache', + cwd: '/tmp', + danger: 'recursive delete', + }), + ], + }), + ); + + show.mockRejectedValueOnce(new Error('boom')); + await expect(handler(approvalEvent())).resolves.toEqual({ + decision: 'cancelled', + feedback: 'approval handler failed', + }); + }); +}); diff --git a/apps/kimi-code/test/tui/reverse-rpc/base-controller.test.ts b/apps/kimi-code/test/tui/reverse-rpc/base-controller.test.ts new file mode 100644 index 000000000..c755c4dd9 --- /dev/null +++ b/apps/kimi-code/test/tui/reverse-rpc/base-controller.test.ts @@ -0,0 +1,124 @@ +import { describe, expect, it, vi } from 'vitest'; + +import { ReverseRpcController } from '#/tui/reverse-rpc/base-controller'; + +class TestController extends ReverseRpcController<string, string> { + protected createCancelResponse(reason: string): string { + return `cancel:${reason}`; + } +} + +describe('ReverseRpcController', () => { + it('shows a payload, resolves the pending promise on respond, and hides the panel', async () => { + const controller = new TestController(); + const showPanel = vi.fn(); + const hidePanel = vi.fn(); + controller.setUIHooks({ showPanel, hidePanel }); + + const pending = controller.show('payload'); + expect(controller.hasPending()).toBe(true); + expect(showPanel).toHaveBeenCalledWith('payload'); + + controller.respond('approved'); + + await expect(pending).resolves.toBe('approved'); + expect(controller.hasPending()).toBe(false); + expect(hidePanel).toHaveBeenCalledOnce(); + }); + + it('queues concurrent show() requests and presents them one at a time', async () => { + const controller = new TestController(); + const showPanel = vi.fn(); + const hidePanel = vi.fn(); + controller.setUIHooks({ showPanel, hidePanel }); + + const first = controller.show('first'); + const second = controller.show('second'); + const third = controller.show('third'); + + // Only the first is presented; the rest stay queued. + expect(showPanel).toHaveBeenCalledTimes(1); + expect(showPanel).toHaveBeenLastCalledWith('first'); + expect(controller.hasPending()).toBe(true); + + controller.respond('answer-first'); + await expect(first).resolves.toBe('answer-first'); + // Advancing to the next queued request reuses the same panel without + // hiding it in between. + expect(hidePanel).not.toHaveBeenCalled(); + expect(showPanel).toHaveBeenCalledTimes(2); + expect(showPanel).toHaveBeenLastCalledWith('second'); + + controller.respond('answer-second'); + await expect(second).resolves.toBe('answer-second'); + expect(showPanel).toHaveBeenCalledTimes(3); + expect(showPanel).toHaveBeenLastCalledWith('third'); + + controller.respond('answer-third'); + await expect(third).resolves.toBe('answer-third'); + expect(controller.hasPending()).toBe(false); + expect(hidePanel).toHaveBeenCalledTimes(1); + }); + + it('auto-resolves matching queued requests via the autoResolveFor hook', async () => { + class AutoController extends ReverseRpcController< + { action: string; id: string }, + string + > { + protected createCancelResponse(reason: string): string { + return `cancel:${reason}`; + } + protected override autoResolveFor( + resolved: { action: string; id: string }, + response: string, + queued: { action: string; id: string }, + ): string | undefined { + if (response === 'approve_all_same' && resolved.action === queued.action) { + return `auto:${queued.id}`; + } + return undefined; + } + } + const controller = new AutoController(); + const showPanel = vi.fn(); + const hidePanel = vi.fn(); + controller.setUIHooks({ showPanel, hidePanel }); + + const first = controller.show({ action: 'run', id: 'a' }); + const second = controller.show({ action: 'run', id: 'b' }); + const third = controller.show({ action: 'edit', id: 'c' }); + const fourth = controller.show({ action: 'run', id: 'd' }); + + controller.respond('approve_all_same'); + + await expect(first).resolves.toBe('approve_all_same'); + await expect(second).resolves.toBe('auto:b'); + await expect(fourth).resolves.toBe('auto:d'); + // The non-matching request advances to the panel and stays pending. + expect(showPanel).toHaveBeenLastCalledWith({ action: 'edit', id: 'c' }); + expect(controller.hasPending()).toBe(true); + + controller.respond('approve_all_same'); + await expect(third).resolves.toBe('approve_all_same'); + expect(controller.hasPending()).toBe(false); + expect(hidePanel).toHaveBeenCalledTimes(1); + }); + + it('cancelAll cancels the current request and every queued request', async () => { + const controller = new TestController(); + const hidePanel = vi.fn(); + controller.setUIHooks({ showPanel: vi.fn(), hidePanel }); + + const first = controller.show('first'); + const second = controller.show('second'); + const third = controller.show('third'); + + controller.cancelAll('shutdown'); + + await expect(first).resolves.toBe('cancel:shutdown'); + await expect(second).resolves.toBe('cancel:shutdown'); + await expect(third).resolves.toBe('cancel:shutdown'); + expect(controller.hasPending()).toBe(false); + expect(hidePanel).toHaveBeenCalledTimes(1); + }); +}); diff --git a/apps/kimi-code/test/tui/reverse-rpc/index.test.ts b/apps/kimi-code/test/tui/reverse-rpc/index.test.ts new file mode 100644 index 000000000..790fd00dc --- /dev/null +++ b/apps/kimi-code/test/tui/reverse-rpc/index.test.ts @@ -0,0 +1,214 @@ +import { describe, expect, it, vi } from 'vitest'; + +import { ApprovalController } from '#/tui/reverse-rpc/approval/controller'; +import { registerReverseRPCHandlers } from '#/tui/reverse-rpc/index'; +import { QuestionController } from '#/tui/reverse-rpc/question/controller'; + +describe('registerReverseRPCHandlers', () => { + it('wires controller UI hooks without registering wire request handlers', async () => { + const approvalController = new ApprovalController(); + const questionController = new QuestionController(); + const uiHooks = { + showApprovalPanel: vi.fn(), + hideApprovalPanel: vi.fn(), + showQuestionDialog: vi.fn(), + hideQuestionDialog: vi.fn(), + }; + + registerReverseRPCHandlers(approvalController, questionController, uiHooks); + + const approvalPending = approvalController.show({ + id: 'req-1', + tool_call_id: 'tc-1', + tool_name: 'Bash', + action: 'run', + description: '', + display: [], + choices: [], + }); + expect(uiHooks.showApprovalPanel).toHaveBeenCalledWith( + expect.objectContaining({ id: 'req-1' }), + ); + approvalController.cancelAll('bye'); + await expect(approvalPending).resolves.toEqual({ + decision: 'cancelled', + feedback: 'bye', + }); + expect(uiHooks.hideApprovalPanel).toHaveBeenCalledOnce(); + + const questionPending = questionController.show({ + id: 'q-1', + tool_call_id: 'tc-1', + questions: [], + }); + expect(uiHooks.showQuestionDialog).toHaveBeenCalledWith(expect.objectContaining({ id: 'q-1' })); + questionController.cancelAll('bye'); + await expect(questionPending).resolves.toEqual({ answers: [] }); + expect(uiHooks.hideQuestionDialog).toHaveBeenCalledOnce(); + }); + + it('queues question dialogs behind active approval panels', async () => { + const approvalController = new ApprovalController(); + const questionController = new QuestionController(); + const uiHooks = { + showApprovalPanel: vi.fn(), + hideApprovalPanel: vi.fn(), + showQuestionDialog: vi.fn(), + hideQuestionDialog: vi.fn(), + }; + + registerReverseRPCHandlers(approvalController, questionController, uiHooks); + + const approvalPending = approvalController.show({ + id: 'approval-1', + tool_call_id: 'tc-1', + tool_name: 'Bash', + action: 'run', + description: '', + display: [], + choices: [], + }); + const questionPending = questionController.show({ + id: 'question-1', + tool_call_id: 'tq-1', + questions: [], + }); + + expect(uiHooks.showApprovalPanel).toHaveBeenCalledWith( + expect.objectContaining({ id: 'approval-1' }), + ); + expect(uiHooks.showQuestionDialog).not.toHaveBeenCalled(); + + approvalController.respond({ decision: 'approved' }); + await expect(approvalPending).resolves.toEqual({ decision: 'approved' }); + expect(uiHooks.hideApprovalPanel).toHaveBeenCalledOnce(); + expect(uiHooks.showQuestionDialog).toHaveBeenCalledWith( + expect.objectContaining({ id: 'question-1' }), + ); + + questionController.respond({ answers: ['answer'] }); + await expect(questionPending).resolves.toEqual({ answers: ['answer'] }); + expect(uiHooks.hideQuestionDialog).toHaveBeenCalledOnce(); + }); + + it('queues approval panels behind active question dialogs', async () => { + const approvalController = new ApprovalController(); + const questionController = new QuestionController(); + const uiHooks = { + showApprovalPanel: vi.fn(), + hideApprovalPanel: vi.fn(), + showQuestionDialog: vi.fn(), + hideQuestionDialog: vi.fn(), + }; + + registerReverseRPCHandlers(approvalController, questionController, uiHooks); + + const questionPending = questionController.show({ + id: 'question-1', + tool_call_id: 'tq-1', + questions: [], + }); + const approvalPending = approvalController.show({ + id: 'approval-1', + tool_call_id: 'tc-1', + tool_name: 'Bash', + action: 'run', + description: '', + display: [], + choices: [], + }); + + expect(uiHooks.showQuestionDialog).toHaveBeenCalledWith( + expect.objectContaining({ id: 'question-1' }), + ); + expect(uiHooks.showApprovalPanel).not.toHaveBeenCalled(); + + questionController.respond({ answers: ['answer'] }); + await expect(questionPending).resolves.toEqual({ answers: ['answer'] }); + expect(uiHooks.hideQuestionDialog).toHaveBeenCalledOnce(); + expect(uiHooks.showApprovalPanel).toHaveBeenCalledWith( + expect.objectContaining({ id: 'approval-1' }), + ); + + approvalController.respond({ decision: 'approved' }); + await expect(approvalPending).resolves.toEqual({ decision: 'approved' }); + expect(uiHooks.hideApprovalPanel).toHaveBeenCalledOnce(); + }); + + it('removes queued modals when their controller is cancelled', async () => { + const approvalController = new ApprovalController(); + const questionController = new QuestionController(); + const uiHooks = { + showApprovalPanel: vi.fn(), + hideApprovalPanel: vi.fn(), + showQuestionDialog: vi.fn(), + hideQuestionDialog: vi.fn(), + }; + + registerReverseRPCHandlers(approvalController, questionController, uiHooks); + + const approvalPending = approvalController.show({ + id: 'approval-1', + tool_call_id: 'tc-1', + tool_name: 'Bash', + action: 'run', + description: '', + display: [], + choices: [], + }); + const questionPending = questionController.show({ + id: 'question-1', + tool_call_id: 'tq-1', + questions: [], + }); + + questionController.cancelAll('closed'); + await expect(questionPending).resolves.toEqual({ answers: [] }); + expect(uiHooks.hideQuestionDialog).not.toHaveBeenCalled(); + + approvalController.respond({ decision: 'approved' }); + await expect(approvalPending).resolves.toEqual({ decision: 'approved' }); + expect(uiHooks.showQuestionDialog).not.toHaveBeenCalled(); + }); + + it('clears active and queued modals without showing queued entries', async () => { + const approvalController = new ApprovalController(); + const questionController = new QuestionController(); + const uiHooks = { + showApprovalPanel: vi.fn(), + hideApprovalPanel: vi.fn(), + showQuestionDialog: vi.fn(), + hideQuestionDialog: vi.fn(), + }; + + const disposers = registerReverseRPCHandlers(approvalController, questionController, uiHooks); + + const approvalPending = approvalController.show({ + id: 'approval-1', + tool_call_id: 'tc-1', + tool_name: 'Bash', + action: 'run', + description: '', + display: [], + choices: [], + }); + const questionPending = questionController.show({ + id: 'question-1', + tool_call_id: 'tq-1', + questions: [], + }); + + for (const dispose of disposers) dispose(); + expect(uiHooks.hideApprovalPanel).toHaveBeenCalledOnce(); + expect(uiHooks.showQuestionDialog).not.toHaveBeenCalled(); + + approvalController.cancelAll('closed'); + questionController.cancelAll('closed'); + await expect(approvalPending).resolves.toEqual({ + decision: 'cancelled', + feedback: 'closed', + }); + await expect(questionPending).resolves.toEqual({ answers: [] }); + expect(uiHooks.hideQuestionDialog).not.toHaveBeenCalled(); + }); +}); diff --git a/apps/kimi-code/test/tui/reverse-rpc/question.test.ts b/apps/kimi-code/test/tui/reverse-rpc/question.test.ts new file mode 100644 index 000000000..41de65fff --- /dev/null +++ b/apps/kimi-code/test/tui/reverse-rpc/question.test.ts @@ -0,0 +1,134 @@ +import type { QuestionRequest } from '@moonshot-ai/kimi-code-sdk'; +import { describe, expect, it, vi } from 'vitest'; + +import { QuestionController } from '#/tui/reverse-rpc/question/controller'; +import { createQuestionAskHandler } from '#/tui/reverse-rpc/question/handler'; + +function questionEvent(overrides: Partial<QuestionRequest> = {}): QuestionRequest { + return { + toolCallId: 'q-1', + questions: [ + { + question: 'Q1?', + options: [{ label: 'Alpha' }], + }, + ], + ...overrides, + }; +} + +describe('question reverse-rpc', () => { + it('QuestionController cancels pending requests with an empty answer list', async () => { + const controller = new QuestionController(); + const pending = controller.show({ + id: 'req-1', + tool_call_id: 'tc-1', + questions: [], + }); + + controller.cancelAll('closed'); + + await expect(pending).resolves.toEqual({ answers: [] }); + }); + + it('normalizes question payloads and returns the selected answer', async () => { + const controller = new QuestionController(); + const show = vi + .spyOn(controller, 'show') + .mockResolvedValue({ answers: ['Alpha'], method: 'number_key' }); + const handler = createQuestionAskHandler(controller); + const event = questionEvent({ + questions: [ + { + question: 'Q1?', + header: 'Pick', + body: 'Choose one', + multiSelect: true, + otherLabel: 'Other', + otherDescription: 'Type a custom answer', + options: [{ label: 'Alpha', description: 'First option' }], + }, + ], + }); + + await expect(handler(event)).resolves.toEqual({ + answers: { 'Q1?': 'Alpha' }, + method: 'number_key', + }); + expect(show).toHaveBeenCalledWith({ + id: 'q-1', + tool_call_id: 'q-1', + questions: [ + { + question: 'Q1?', + header: 'Pick', + body: 'Choose one', + multi_select: true, + other_label: 'Other', + other_description: 'Type a custom answer', + options: [{ label: 'Alpha', description: 'First option' }], + }, + ], + }); + + show.mockResolvedValueOnce({ answers: [''] }); + await expect(handler(questionEvent())).resolves.toBeNull(); + + show.mockRejectedValueOnce(new Error('boom')); + await expect(handler(questionEvent())).resolves.toBeNull(); + }); + + it('maps multiple question answers by question text', async () => { + const controller = new QuestionController(); + const show = vi + .spyOn(controller, 'show') + .mockResolvedValue({ answers: ['Alpha', 'SQLite'], method: 'enter' }); + const handler = createQuestionAskHandler(controller); + const event = questionEvent({ + toolCallId: 'call_question', + questions: [ + { + question: 'Q1?', + options: [{ label: 'Alpha' }], + }, + { + question: 'Storage?', + header: 'Store', + options: [{ label: 'SQLite' }], + }, + ], + }); + + await expect(handler(event)).resolves.toEqual({ + answers: { + 'Q1?': 'Alpha', + 'Storage?': 'SQLite', + }, + method: 'enter', + }); + expect(show).toHaveBeenCalledWith({ + id: 'call_question', + tool_call_id: 'call_question', + questions: [ + { + question: 'Q1?', + header: undefined, + body: undefined, + multi_select: false, + other_label: undefined, + other_description: undefined, + options: [{ label: 'Alpha', description: undefined }], + }, + { + question: 'Storage?', + header: 'Store', + body: undefined, + multi_select: false, + other_label: undefined, + other_description: undefined, + options: [{ label: 'SQLite', description: undefined }], + }, + ], + }); + }); +}); diff --git a/apps/kimi-code/test/tui/task-output-viewer.test.ts b/apps/kimi-code/test/tui/task-output-viewer.test.ts new file mode 100644 index 000000000..2cb998b42 --- /dev/null +++ b/apps/kimi-code/test/tui/task-output-viewer.test.ts @@ -0,0 +1,260 @@ +import type { Terminal } from '@earendil-works/pi-tui'; +import type { BackgroundTaskInfo } from '@moonshot-ai/kimi-code-sdk'; +import { describe, expect, it, vi } from 'vitest'; + +import { TaskOutputViewer } from '@/tui/components/dialogs/task-output-viewer'; +import { darkColors } from '@/tui/theme/colors'; + +const ANSI_SGR = /\[[0-9;]*m/g; +function strip(text: string): string { + return text.replaceAll(ANSI_SGR, ''); +} + +function fakeTerminal(rows: number, columns = 120): Terminal { + return { + start: () => {}, + stop: () => {}, + drainInput: () => Promise.resolve(), + write: () => {}, + get columns() { + return columns; + }, + get rows() { + return rows; + }, + get kittyProtocolActive() { + return false; + }, + moveBy: () => {}, + hideCursor: () => {}, + showCursor: () => {}, + clearLine: () => {}, + clearFromCursor: () => {}, + clearScreen: () => {}, + setTitle: () => {}, + setProgress: () => {}, + }; +} + +function info(overrides: Partial<BackgroundTaskInfo> = {}): BackgroundTaskInfo { + return { + taskId: 'bash-aaaaaaaa', + command: 'npm run dev', + description: 'dev server', + status: 'running', + pid: 1234, + exitCode: null, + startedAt: Date.now() - 60_000, + endedAt: null, + ...overrides, + }; +} + +function makeViewer(opts: { + output: string; + taskInfo?: BackgroundTaskInfo; + rows?: number; + columns?: number; + onClose?: () => void; +}): TaskOutputViewer { + return new TaskOutputViewer( + { + taskId: opts.taskInfo?.taskId ?? 'bash-aaaaaaaa', + info: opts.taskInfo ?? info(), + output: opts.output, + colors: darkColors, + onClose: opts.onClose ?? (() => {}), + }, + fakeTerminal(opts.rows ?? 30, opts.columns ?? 120), + ); +} + +describe('TaskOutputViewer — rendering', () => { + it('fills exactly terminal.rows lines', () => { + const lines = makeViewer({ output: 'hello\nworld', rows: 24 }).render(120); + expect(lines.length).toBe(24); + }); + + it('shows the task header (id + status + description)', () => { + const out = strip( + makeViewer({ + output: '', + taskInfo: info({ taskId: 'bash-zzzzzzzz', status: 'running', description: 'demo svc' }), + }) + .render(120) + .join('\n'), + ); + expect(out).toContain('Task output'); + expect(out).toContain('bash-zzzzzzzz'); + expect(out).toContain('running'); + expect(out).toContain('demo svc'); + }); + + it('shows the empty-state body when output is empty', () => { + const out = strip(makeViewer({ output: '' }).render(120).join('\n')); + expect(out).toContain('[no output captured]'); + }); + + it('shows position indicator in the footer', () => { + const lines = ['line 1', 'line 2', 'line 3'].join('\n'); + const out = strip(makeViewer({ output: lines, rows: 20 }).render(120).join('\n')); + expect(out).toMatch(/1-3 \/ 3/); + expect(out).toContain('100%'); + }); + + it('renders all visible body lines when output fits the body height', () => { + const lines = ['alpha', 'bravo', 'charlie', 'delta', 'echo'].join('\n'); + const out = strip(makeViewer({ output: lines, rows: 20 }).render(120).join('\n')); + expect(out).toContain('alpha'); + expect(out).toContain('bravo'); + expect(out).toContain('charlie'); + expect(out).toContain('delta'); + expect(out).toContain('echo'); + }); +}); + +describe('TaskOutputViewer — scrolling', () => { + function bigOutput(n: number): string { + return Array.from({ length: n }, (_, i) => `line-${String(i + 1).padStart(3, '0')}`).join('\n'); + } + + it('renders the top of the buffer initially', () => { + const viewer = makeViewer({ output: bigOutput(100), rows: 20 }); + const out = strip(viewer.render(120).join('\n')); + expect(out).toContain('line-001'); + expect(out).not.toContain('line-100'); + }); + + it('down arrow scrolls forward by one line', () => { + const viewer = makeViewer({ output: bigOutput(50), rows: 12 }); + viewer.handleInput(''); + const out = strip(viewer.render(120).join('\n')); + expect(out).toContain('line-002'); + expect(out).not.toContain('line-001'); + }); + + it('PageDown scrolls a page', () => { + const viewer = makeViewer({ output: bigOutput(50), rows: 12 }); + // PageDown via prompt-toolkit-style sequence "[6~" + viewer.handleInput('[6~'); + const out = strip(viewer.render(120).join('\n')); + // body has 12 - 2 (header/footer) - 2 (top/bot border) = 8 viewable rows. + // PageDown shifts by (body - 1) = 7 lines. + expect(out).toContain('line-008'); + expect(out).not.toContain('line-001'); + }); + + it('G jumps to the bottom', () => { + const viewer = makeViewer({ output: bigOutput(100), rows: 14 }); + viewer.handleInput('G'); + const out = strip(viewer.render(120).join('\n')); + expect(out).toContain('line-100'); + // Footer reads 100% at the end. + expect(out).toContain('100%'); + }); + + it('g jumps back to the top', () => { + const viewer = makeViewer({ output: bigOutput(100), rows: 14 }); + viewer.handleInput('G'); + viewer.handleInput('g'); + const out = strip(viewer.render(120).join('\n')); + expect(out).toContain('line-001'); + }); + + it('scrolling clamps at the start and end', () => { + const viewer = makeViewer({ output: bigOutput(5), rows: 20 }); + // Scrolling down on a buffer smaller than the body should not advance. + viewer.handleInput(''); + viewer.handleInput(''); + const out = strip(viewer.render(120).join('\n')); + expect(out).toContain('line-001'); + expect(out).toContain('line-005'); + }); +}); + +describe('TaskOutputViewer — input', () => { + it('Esc invokes onClose', () => { + const onClose = vi.fn(); + makeViewer({ output: 'x', onClose }).handleInput(''); + expect(onClose).toHaveBeenCalledTimes(1); + }); + + it('q invokes onClose', () => { + const onClose = vi.fn(); + makeViewer({ output: 'x', onClose }).handleInput('q'); + expect(onClose).toHaveBeenCalledTimes(1); + }); + + // Under the Kitty keyboard protocol (e.g. VSCode integrated terminal), + // ordinary printable keys arrive as CSI-u sequences. + it('Kitty-encoded q invokes onClose', () => { + const onClose = vi.fn(); + makeViewer({ output: 'x', onClose }).handleInput('\u001B[113u'); + expect(onClose).toHaveBeenCalledTimes(1); + }); + + it('Kitty-encoded G scrolls to bottom', () => { + const viewer = makeViewer({ + output: Array.from({ length: 200 }, (_, i) => `line-${String(i)}`).join('\n'), + rows: 10, + }); + viewer.handleInput('\u001B[71u'); // G (uppercase) + const rendered = strip(viewer.render(120).join('\n')); + expect(rendered).toContain('line-199'); + }); +}); + +describe('TaskOutputViewer — live tail via setProps', () => { + function makeOutput(n: number): string { + return Array.from({ length: n }, (_, i) => `line-${String(i + 1).padStart(3, '0')}`).join('\n'); + } + + it('follows new lines when the viewer is already parked at the bottom', () => { + // 5 lines, body has ~26 viewable rows → user is at bottom by default. + const viewer = makeViewer({ output: makeOutput(5), rows: 30 }); + viewer.handleInput('G'); + // Append more lines. + viewer.setProps({ + taskId: 'bash-aaaaaaaa', + info: info(), + output: makeOutput(50), + colors: darkColors, + onClose: () => {}, + }); + const out = strip(viewer.render(120).join('\n')); + expect(out).toContain('line-050'); + }); + + it('preserves scroll position when the user has scrolled up', () => { + const viewer = makeViewer({ output: makeOutput(100), rows: 14 }); + // Body has 14 - 4 = 10 viewable rows; max scroll = 90. Stay at top (0). + expect(strip(viewer.render(120).join('\n'))).toContain('line-001'); + // Output grows. Since user is at scroll=0 (not bottom), keep them at top. + viewer.setProps({ + taskId: 'bash-aaaaaaaa', + info: info(), + output: makeOutput(200), + colors: darkColors, + onClose: () => {}, + }); + const out = strip(viewer.render(120).join('\n')); + expect(out).toContain('line-001'); + expect(out).not.toContain('line-200'); + }); + + it('skips re-split when output is unchanged', () => { + const same = makeOutput(20); + const viewer = makeViewer({ output: same, rows: 30 }); + viewer.handleInput('G'); + const before = strip(viewer.render(120).join('\n')); + viewer.setProps({ + taskId: 'bash-aaaaaaaa', + info: info(), + output: same, + colors: darkColors, + onClose: () => {}, + }); + const after = strip(viewer.render(120).join('\n')); + expect(after).toBe(before); + }); +}); diff --git a/apps/kimi-code/test/tui/tasks-browser.test.ts b/apps/kimi-code/test/tui/tasks-browser.test.ts new file mode 100644 index 000000000..fd973c8fa --- /dev/null +++ b/apps/kimi-code/test/tui/tasks-browser.test.ts @@ -0,0 +1,434 @@ +import type { Terminal } from '@earendil-works/pi-tui'; +import type { BackgroundTaskInfo, BackgroundTaskStatus } from '@moonshot-ai/kimi-code-sdk'; +import { describe, expect, it, vi } from 'vitest'; + +import { + TasksBrowserApp, + type TasksBrowserProps, + type TasksFilter, +} from '@/tui/components/dialogs/tasks-browser'; +import { darkColors } from '@/tui/theme/colors'; + +const ANSI_SGR = /\[[0-9;]*m/g; +function strip(text: string): string { + return text.replaceAll(ANSI_SGR, ''); +} + +/** Minimal Terminal stub — only `rows` is read by the component. */ +function fakeTerminal(rows: number, columns = 120): Terminal { + return { + start: () => {}, + stop: () => {}, + drainInput: () => Promise.resolve(), + write: () => {}, + get columns() { + return columns; + }, + get rows() { + return rows; + }, + get kittyProtocolActive() { + return false; + }, + moveBy: () => {}, + hideCursor: () => {}, + showCursor: () => {}, + clearLine: () => {}, + clearFromCursor: () => {}, + clearScreen: () => {}, + setTitle: () => {}, + setProgress: () => {}, + }; +} + +function task(overrides: Partial<BackgroundTaskInfo> = {}): BackgroundTaskInfo { + return { + taskId: 'bash-abcd1234', + command: 'npm run dev', + description: 'dev server', + status: 'running', + pid: 1234, + exitCode: null, + startedAt: Date.now() - 60_000, + endedAt: null, + ...overrides, + }; +} + +function makeProps(overrides: Partial<TasksBrowserProps> = {}): TasksBrowserProps { + return { + tasks: [], + filter: 'all', + selectedTaskId: undefined, + tailOutput: undefined, + tailLoading: false, + flashMessage: undefined, + colors: darkColors, + onSelect: vi.fn(), + onToggleFilter: vi.fn(), + onRefresh: vi.fn(), + onCancel: vi.fn(), + onStopConfirmed: vi.fn(), + onOpenOutput: vi.fn(), + onStopIgnored: vi.fn(), + ...overrides, + } as TasksBrowserProps; +} + +function makeApp( + props: Partial<TasksBrowserProps> = {}, + rows = 30, + columns = 120, +): TasksBrowserApp { + return new TasksBrowserApp(makeProps(props), fakeTerminal(rows, columns)); +} + +describe('TasksBrowserApp — full-screen rendering', () => { + it('fills exactly terminal.rows lines (height takeover)', () => { + const rows = 30; + const lines = makeApp({}, rows).render(120); + expect(lines.length).toBe(rows); + }); + + it('reacts to terminal height changes', () => { + const props = makeProps({ + tasks: [task({ taskId: 'bash-aaaaaaaa', status: 'running' })], + selectedTaskId: 'bash-aaaaaaaa', + }); + // Two terminals with different heights — verify render adapts. + const small = new TasksBrowserApp(props, fakeTerminal(15, 120)).render(120); + const big = new TasksBrowserApp(props, fakeTerminal(40, 120)).render(120); + expect(small.length).toBe(15); + expect(big.length).toBe(40); + }); + + it('shows the header row with TASK BROWSER title and counts', () => { + const props: Partial<TasksBrowserProps> = { + tasks: [ + task({ taskId: 'bash-aaaaaaaa', status: 'running' }), + task({ taskId: 'agent-bbbbbbbb', status: 'completed' }), + ], + }; + const out = strip(makeApp(props).render(120).join('\n')); + expect(out).toContain('TASK BROWSER'); + expect(out).toContain('filter=ALL'); + expect(out).toContain('1 running'); + expect(out).toContain('1 completed'); + expect(out).toContain('2 total'); + }); + + it('renders three framed panes: Tasks / Detail / Preview Output', () => { + const out = strip( + makeApp({ + tasks: [task({ taskId: 'bash-aaaaaaaa', status: 'running' })], + selectedTaskId: 'bash-aaaaaaaa', + }) + .render(120) + .join('\n'), + ); + expect(out).toContain('Tasks [all]'); + expect(out).toContain('Detail'); + expect(out).toContain('Preview Output'); + }); + + it('shows the selected task details in the Detail pane', () => { + const out = strip( + makeApp({ + tasks: [ + task({ + taskId: 'bash-aaaaaaaa', + status: 'running', + description: 'long running task', + pid: 9999, + }), + ], + selectedTaskId: 'bash-aaaaaaaa', + }) + .render(120) + .join('\n'), + ); + expect(out).toContain('Task ID:'); + expect(out).toContain('bash-aaaaaaaa'); + expect(out).toContain('long running task'); + }); + + it('renders tail output in the Preview Output pane', () => { + const out = strip( + makeApp({ + tasks: [task({ taskId: 'bash-aaaaaaaa' })], + selectedTaskId: 'bash-aaaaaaaa', + tailOutput: 'ready in 432ms\nlistening on :3000', + }) + .render(120) + .join('\n'), + ); + expect(out).toContain('ready in 432ms'); + expect(out).toContain('listening on :3000'); + }); + + it('shows a loading state when tail is loading', () => { + const out = strip( + makeApp({ + tasks: [task({ taskId: 'bash-aaaaaaaa' })], + selectedTaskId: 'bash-aaaaaaaa', + tailLoading: true, + }) + .render(120) + .join('\n'), + ); + expect(out).toContain('[loading'); + }); + + it('shows empty-state copy in the Tasks pane when no tasks', () => { + const out = strip(makeApp().render(120).join('\n')); + expect(out).toContain('No background tasks'); + }); + + it('filters out terminal tasks when filter=active', () => { + const tasks = [ + task({ taskId: 'bash-aaaaaaaa', status: 'running' }), + task({ taskId: 'bash-bbbbbbbb', status: 'completed' }), + ]; + const out = strip(makeApp({ tasks, filter: 'active' }).render(120).join('\n')); + expect(out).toContain('bash-aaaaaaaa'); + expect(out).not.toContain('bash-bbbbbbbb'); + }); + + it('renders without throwing for every BackgroundTaskStatus', () => { + const statuses: BackgroundTaskStatus[] = [ + 'running', + 'awaiting_approval', + 'completed', + 'failed', + 'killed', + 'lost', + ]; + for (const status of statuses) { + const props = makeProps({ + tasks: [task({ taskId: 'bash-aaaaaaaa', status })], + selectedTaskId: 'bash-aaaaaaaa', + }); + expect(() => new TasksBrowserApp(props, fakeTerminal(30)).render(120)).not.toThrow(); + } + }); + + it('falls back to a single line when the terminal is too small', () => { + const out = strip(makeApp({}, 5, 30).render(30).join('\n')); + expect(out).toContain('too small'); + }); +}); + +describe('TasksBrowserApp — input handling', () => { + it('Esc invokes onCancel', () => { + const onCancel = vi.fn(); + const app = makeApp({ onCancel }); + app.handleInput(''); + expect(onCancel).toHaveBeenCalledTimes(1); + }); + + it('q invokes onCancel', () => { + const onCancel = vi.fn(); + makeApp({ onCancel }).handleInput('q'); + expect(onCancel).toHaveBeenCalledTimes(1); + }); + + it('Tab invokes onToggleFilter', () => { + const onToggleFilter = vi.fn(); + makeApp({ onToggleFilter }).handleInput('\t'); + expect(onToggleFilter).toHaveBeenCalledTimes(1); + }); + + it('R invokes onRefresh', () => { + const onRefresh = vi.fn(); + makeApp({ onRefresh }).handleInput('r'); + expect(onRefresh).toHaveBeenCalledTimes(1); + }); + + it('arrow keys move selection and invoke onSelect', () => { + const onSelect = vi.fn(); + const tasks = [ + task({ taskId: 'bash-aaaaaaaa', status: 'running', startedAt: 1 }), + task({ taskId: 'bash-bbbbbbbb', status: 'running', startedAt: 2 }), + task({ taskId: 'bash-cccccccc', status: 'running', startedAt: 3 }), + ]; + const app = makeApp({ tasks, selectedTaskId: 'bash-aaaaaaaa', onSelect }); + app.handleInput(''); // ↓ + expect(onSelect).toHaveBeenLastCalledWith('bash-bbbbbbbb'); + app.handleInput('j'); + expect(onSelect).toHaveBeenLastCalledWith('bash-cccccccc'); + app.handleInput(''); // ↑ + expect(onSelect).toHaveBeenLastCalledWith('bash-bbbbbbbb'); + }); + + it('Enter and O both invoke onOpenOutput', () => { + const onOpenOutput = vi.fn(); + const app = makeApp({ + tasks: [task({ taskId: 'bash-aaaaaaaa' })], + selectedTaskId: 'bash-aaaaaaaa', + onOpenOutput, + }); + app.handleInput('o'); + app.handleInput('\r'); + expect(onOpenOutput).toHaveBeenCalledTimes(2); + expect(onOpenOutput).toHaveBeenCalledWith('bash-aaaaaaaa'); + }); +}); + +// When a terminal (e.g. the VSCode integrated terminal) enables the Kitty +// keyboard protocol disambiguate flag, ordinary printable keys arrive as +// CSI-u sequences: `r` → "\x1b[114u", `q` → "\x1b[113u". These tests pin +// down that the tasks panel's literal-character shortcuts still fire +// under Kitty mode. +describe('TasksBrowserApp — Kitty CSI-u printable input', () => { + const kitty = (ch: string): string => `\u001B[${String(ch.codePointAt(0) ?? 0)}u`; + + it('Kitty-encoded q invokes onCancel', () => { + const onCancel = vi.fn(); + makeApp({ onCancel }).handleInput(kitty('q')); + expect(onCancel).toHaveBeenCalledTimes(1); + }); + + it('Kitty-encoded r invokes onRefresh', () => { + const onRefresh = vi.fn(); + makeApp({ onRefresh }).handleInput(kitty('r')); + expect(onRefresh).toHaveBeenCalledTimes(1); + }); + + it('Kitty-encoded j moves selection down', () => { + const onSelect = vi.fn(); + const tasks = [ + task({ taskId: 'bash-aaaaaaaa', status: 'running', startedAt: 1 }), + task({ taskId: 'bash-bbbbbbbb', status: 'running', startedAt: 2 }), + ]; + const app = makeApp({ tasks, selectedTaskId: 'bash-aaaaaaaa', onSelect }); + app.handleInput(kitty('j')); + expect(onSelect).toHaveBeenLastCalledWith('bash-bbbbbbbb'); + }); + + it('Kitty-encoded o invokes onOpenOutput', () => { + const onOpenOutput = vi.fn(); + const app = makeApp({ + tasks: [task({ taskId: 'bash-aaaaaaaa' })], + selectedTaskId: 'bash-aaaaaaaa', + onOpenOutput, + }); + app.handleInput(kitty('o')); + expect(onOpenOutput).toHaveBeenCalledWith('bash-aaaaaaaa'); + }); + + it('Kitty-encoded s → y confirms a stop', () => { + const onStopConfirmed = vi.fn(); + const app = makeApp({ + tasks: [task({ taskId: 'bash-aaaaaaaa', status: 'running' })], + selectedTaskId: 'bash-aaaaaaaa', + onStopConfirmed, + }); + app.handleInput(kitty('s')); + app.handleInput(kitty('y')); + expect(onStopConfirmed).toHaveBeenCalledWith('bash-aaaaaaaa'); + }); +}); + +describe('TasksBrowserApp — stop confirmation', () => { + it('S → y confirms a stop and invokes onStopConfirmed', () => { + const onStopConfirmed = vi.fn(); + const app = makeApp({ + tasks: [task({ taskId: 'bash-aaaaaaaa', status: 'running' })], + selectedTaskId: 'bash-aaaaaaaa', + onStopConfirmed, + }); + app.handleInput('s'); + const after = strip(app.render(120).join('\n')); + expect(after).toContain('Stop bash-aaaaaaaa?'); + app.handleInput('y'); + expect(onStopConfirmed).toHaveBeenCalledWith('bash-aaaaaaaa'); + expect(strip(app.render(120).join('\n'))).not.toContain('Stop bash-aaaaaaaa?'); + }); + + it('S → n cancels without firing onStopConfirmed', () => { + const onStopConfirmed = vi.fn(); + const app = makeApp({ + tasks: [task({ taskId: 'bash-aaaaaaaa', status: 'running' })], + selectedTaskId: 'bash-aaaaaaaa', + onStopConfirmed, + }); + app.handleInput('s'); + app.handleInput('n'); + expect(onStopConfirmed).not.toHaveBeenCalled(); + expect(strip(app.render(120).join('\n'))).not.toContain('Stop bash-aaaaaaaa?'); + }); + + it('S → Esc cancels the confirm without closing the panel', () => { + const onStopConfirmed = vi.fn(); + const onCancel = vi.fn(); + const app = makeApp({ + tasks: [task({ taskId: 'bash-aaaaaaaa', status: 'running' })], + selectedTaskId: 'bash-aaaaaaaa', + onStopConfirmed, + onCancel, + }); + app.handleInput('s'); + app.handleInput(''); + expect(onStopConfirmed).not.toHaveBeenCalled(); + expect(onCancel).not.toHaveBeenCalled(); + }); + + it('S on a terminal task invokes onStopIgnored and stays out of confirm mode', () => { + const onStopConfirmed = vi.fn(); + const onStopIgnored = vi.fn(); + const app = makeApp({ + tasks: [task({ taskId: 'bash-aaaaaaaa', status: 'completed', exitCode: 0 })], + selectedTaskId: 'bash-aaaaaaaa', + onStopConfirmed, + onStopIgnored, + }); + app.handleInput('s'); + expect(onStopIgnored).toHaveBeenCalledWith('bash-aaaaaaaa', 'terminal'); + expect(onStopConfirmed).not.toHaveBeenCalled(); + expect(strip(app.render(120).join('\n'))).not.toContain('Stop bash-aaaaaaaa?'); + }); + + it('navigation during confirm mode is locked out', () => { + const onSelect = vi.fn(); + const onStopConfirmed = vi.fn(); + const tasks = [ + task({ taskId: 'bash-aaaaaaaa', status: 'running', startedAt: 1 }), + task({ taskId: 'bash-bbbbbbbb', status: 'running', startedAt: 2 }), + ]; + const app = makeApp({ tasks, selectedTaskId: 'bash-aaaaaaaa', onSelect, onStopConfirmed }); + app.handleInput('s'); + onSelect.mockClear(); + app.handleInput(''); // ↓ arrow should be swallowed + expect(onSelect).not.toHaveBeenCalled(); + expect(strip(app.render(120).join('\n'))).not.toContain('Stop bash-aaaaaaaa?'); + }); +}); + +describe('TasksBrowserApp — setProps', () => { + it('keeps selection across prop updates when the task still exists', () => { + const tasks = [ + task({ taskId: 'bash-aaaaaaaa', status: 'running' }), + task({ taskId: 'bash-bbbbbbbb', status: 'running' }), + ]; + const app = makeApp({ tasks, selectedTaskId: 'bash-bbbbbbbb' }); + app.setProps({ + ...makeProps({ + tasks: [...tasks, task({ taskId: 'bash-cccccccc', status: 'completed' })], + selectedTaskId: 'bash-bbbbbbbb', + }), + }); + const out = strip(app.render(120).join('\n')); + expect(out).toContain('bash-bbbbbbbb'); + }); + + it('switches the filter via setProps without throwing', () => { + const tasks = [task({ status: 'completed' })]; + const filters: TasksFilter[] = ['all', 'active', 'all']; + const app = makeApp({ tasks }); + for (const filter of filters) { + expect(() => { + app.setProps(makeProps({ tasks, filter })); + }).not.toThrow(); + } + }); +}); diff --git a/apps/kimi-code/test/tui/terminal-focus.test.ts b/apps/kimi-code/test/tui/terminal-focus.test.ts new file mode 100644 index 000000000..71246b785 --- /dev/null +++ b/apps/kimi-code/test/tui/terminal-focus.test.ts @@ -0,0 +1,59 @@ +import { describe, expect, it, vi } from 'vitest'; + +import type { TUIState } from '#/tui/kimi-tui'; +import { + DISABLE_TERMINAL_FOCUS_REPORTING, + ENABLE_TERMINAL_FOCUS_REPORTING, + TERMINAL_FOCUS_IN, + TERMINAL_FOCUS_OUT, + handleTerminalFocusInput, + installTerminalFocusTracking, +} from '#/tui/utils/terminal-focus'; + +describe('terminal focus tracking', () => { + it('updates focus state from terminal focus reporting sequences', () => { + const state = { focused: true }; + + expect(handleTerminalFocusInput(state, TERMINAL_FOCUS_OUT)).toEqual({ consume: true }); + expect(state.focused).toBe(false); + + expect(handleTerminalFocusInput(state, TERMINAL_FOCUS_IN)).toEqual({ consume: true }); + expect(state.focused).toBe(true); + + expect(handleTerminalFocusInput(state, 'x')).toBeUndefined(); + }); + + it('enables focus reporting and removes the listener on dispose', () => { + const listeners: Array<(data: string) => { consume: true } | undefined> = []; + const removeInputListener = vi.fn(); + const state = { + terminalState: { + focused: false, + }, + terminal: { + write: vi.fn(), + }, + ui: { + addInputListener: vi.fn((listener) => { + listeners.push(listener); + return removeInputListener; + }), + }, + } as unknown as TUIState; + + const dispose = installTerminalFocusTracking(state); + + expect(state.terminalState.focused).toBe(true); + expect(state.terminal.write).toHaveBeenCalledWith(ENABLE_TERMINAL_FOCUS_REPORTING); + expect(listeners).toHaveLength(1); + + listeners[0]?.(TERMINAL_FOCUS_OUT); + expect(state.terminalState.focused).toBe(false); + + dispose(); + + expect(removeInputListener).toHaveBeenCalledOnce(); + expect(state.terminal.write).toHaveBeenCalledWith(DISABLE_TERMINAL_FOCUS_REPORTING); + expect(state.terminalState.focused).toBe(true); + }); +}); diff --git a/apps/kimi-code/test/tui/terminal-notification.test.ts b/apps/kimi-code/test/tui/terminal-notification.test.ts new file mode 100644 index 000000000..2d5d904ee --- /dev/null +++ b/apps/kimi-code/test/tui/terminal-notification.test.ts @@ -0,0 +1,227 @@ +import { describe, expect, it, vi } from 'vitest'; + +import type { TUIState } from '#/tui/kimi-tui'; +import { + buildTerminalNotificationSequences, + emitTerminalNotification, + formatNotification, + isInsideTmux, + notifyTerminalOnce, + supportsOsc9Notification, +} from '#/tui/utils/terminal-notification'; + +function makeNotificationState(args: { + readonly enabled?: boolean; + readonly condition?: 'unfocused' | 'always'; + readonly focused?: boolean; + readonly supportsOsc9?: boolean; + readonly insideTmux?: boolean; +} = {}): TUIState { + return { + appState: { + notifications: { + enabled: args.enabled ?? true, + condition: args.condition ?? 'unfocused', + }, + }, + terminalState: { + notificationKeys: new Set<string>(), + focused: args.focused ?? false, + supportsOsc9: args.supportsOsc9 ?? true, + insideTmux: args.insideTmux ?? false, + }, + terminal: { + write: vi.fn(), + }, + } as unknown as TUIState; +} + +describe('terminal notification helpers', () => { + it('emits OSC 9 only when the terminal supports it', () => { + const terminal = { write: vi.fn() }; + + emitTerminalNotification( + terminal, + { title: 'Kimi Code', body: 'Approval\nrequired' }, + { supportsOsc9: true, insideTmux: false }, + ); + + expect(terminal.write).toHaveBeenCalledTimes(1); + expect(terminal.write).toHaveBeenCalledWith(']9;Kimi Code: Approval required'); + }); + + it('falls back to a bare BEL when the terminal does not support OSC 9', () => { + const terminal = { write: vi.fn() }; + + emitTerminalNotification( + terminal, + { title: 'Kimi Code', body: 'Approval required' }, + { supportsOsc9: false, insideTmux: false }, + ); + + expect(terminal.write).toHaveBeenCalledTimes(1); + expect(terminal.write).toHaveBeenCalledWith(''); + }); + + it('wraps OSC 9 in a tmux DCS passthrough when running inside tmux', () => { + const terminal = { write: vi.fn() }; + + emitTerminalNotification( + terminal, + { title: 'Kimi Code', body: 'Approval required' }, + { supportsOsc9: true, insideTmux: true }, + ); + + expect(terminal.write).toHaveBeenCalledTimes(1); + expect(terminal.write).toHaveBeenCalledWith('Ptmux;]9;Kimi Code: Approval required\\'); + }); + + it('skips the tmux wrap when falling back to BEL', () => { + const terminal = { write: vi.fn() }; + + emitTerminalNotification( + terminal, + { title: 'Kimi Code', body: 'Approval required' }, + { supportsOsc9: false, insideTmux: true }, + ); + + expect(terminal.write).toHaveBeenCalledWith(''); + }); + + it('emits nothing when the formatted message is empty', () => { + const terminal = { write: vi.fn() }; + + emitTerminalNotification( + terminal, + { title: '', body: '' }, + { supportsOsc9: true, insideTmux: false }, + ); + + expect(terminal.write).not.toHaveBeenCalled(); + }); + + it('deduplicates notifications by key on TUI state', () => { + const state = makeNotificationState(); + + notifyTerminalOnce(state, 'approval:req-1', { title: 'Approval required' }); + notifyTerminalOnce(state, 'approval:req-1', { title: 'Approval required' }); + notifyTerminalOnce(state, 'approval:req-2', { title: 'Approval required' }); + + expect(state.terminal.write).toHaveBeenCalledTimes(2); + }); + + it('suppresses notifications while the terminal is focused', () => { + const state = makeNotificationState({ focused: true }); + + notifyTerminalOnce(state, 'approval:req-1', { title: 'Approval required' }); + state.terminalState.focused = false; + notifyTerminalOnce(state, 'approval:req-1', { title: 'Approval required' }); + notifyTerminalOnce(state, 'approval:req-2', { title: 'Approval required' }); + + expect(state.terminal.write).toHaveBeenCalledTimes(1); + expect(state.terminalState.notificationKeys.has('approval:req-1')).toBe(true); + }); + + it('skips emission entirely when notifications.enabled is false', () => { + const state = makeNotificationState({ enabled: false }); + + notifyTerminalOnce(state, 'approval:req-1', { title: 'Approval required' }); + + expect(state.terminal.write).not.toHaveBeenCalled(); + expect(state.terminalState.notificationKeys.has('approval:req-1')).toBe(false); + }); + + it('emits even while focused when condition is "always"', () => { + const state = makeNotificationState({ condition: 'always', focused: true }); + + notifyTerminalOnce(state, 'approval:req-1', { title: 'Approval required' }); + + expect(state.terminal.write).toHaveBeenCalledTimes(1); + expect(state.terminalState.notificationKeys.has('approval:req-1')).toBe(true); + }); + + it('uses the tmux-wrapped sequence when state.insideTmux is true', () => { + const state = makeNotificationState({ insideTmux: true }); + + notifyTerminalOnce(state, 'approval:req-1', { title: 'Approval required' }); + + expect(state.terminal.write).toHaveBeenCalledTimes(1); + expect(state.terminal.write).toHaveBeenCalledWith('Ptmux;]9;Approval required\\'); + }); + + it('falls back to BEL on a TUI state that did not detect OSC 9 support', () => { + const state = makeNotificationState({ supportsOsc9: false }); + + notifyTerminalOnce(state, 'approval:req-1', { title: 'Approval required' }); + + expect(state.terminal.write).toHaveBeenCalledTimes(1); + expect(state.terminal.write).toHaveBeenCalledWith(''); + }); + + it('falls back to body when the title is empty', () => { + expect(formatNotification({ title: '', body: 'Question?' })).toBe('Question?'); + }); + + it('returns OSC 9 / BEL based on capability flag', () => { + expect( + buildTerminalNotificationSequences( + { title: 'A', body: 'B' }, + { supportsOsc9: true, insideTmux: false }, + ), + ).toEqual([']9;A: B']); + expect( + buildTerminalNotificationSequences( + { title: 'A', body: 'B' }, + { supportsOsc9: false, insideTmux: false }, + ), + ).toEqual(['']); + }); + + it('doubles ESC bytes inside the tmux DCS payload', () => { + const sequences = buildTerminalNotificationSequences( + { title: 'A', body: 'B' }, + { supportsOsc9: true, insideTmux: true }, + ); + + expect(sequences).toHaveLength(1); + const wrapped = sequences[0]!; + expect(wrapped.startsWith('Ptmux;')).toBe(true); + expect(wrapped.endsWith('\\')).toBe(true); + expect(wrapped).toContain(']9;A: B'); + }); +}); + +describe('supportsOsc9Notification', () => { + it('detects iTerm2 / WezTerm / Ghostty / Warp via TERM_PROGRAM', () => { + expect(supportsOsc9Notification({ TERM_PROGRAM: 'iTerm.app' })).toBe(true); + expect(supportsOsc9Notification({ TERM_PROGRAM: 'WezTerm' })).toBe(true); + expect(supportsOsc9Notification({ TERM_PROGRAM: 'ghostty' })).toBe(true); + expect(supportsOsc9Notification({ TERM_PROGRAM: 'WarpTerminal' })).toBe(true); + }); + + it('detects Kitty / Ghostty via TERM', () => { + expect(supportsOsc9Notification({ TERM: 'xterm-kitty' })).toBe(true); + expect(supportsOsc9Notification({ TERM: 'xterm-ghostty' })).toBe(true); + }); + + it('returns false for terminals known not to support OSC 9', () => { + expect(supportsOsc9Notification({ TERM_PROGRAM: 'Apple_Terminal' })).toBe(false); + expect(supportsOsc9Notification({ TERM_PROGRAM: 'vscode' })).toBe(false); + expect(supportsOsc9Notification({ TERM_PROGRAM: 'tabby' })).toBe(false); + expect(supportsOsc9Notification({ WT_SESSION: 'abc-123' })).toBe(false); + expect(supportsOsc9Notification({ ConEmuANSI: 'ON' })).toBe(false); + expect(supportsOsc9Notification({ TERM: 'xterm-256color' })).toBe(false); + expect(supportsOsc9Notification({})).toBe(false); + }); +}); + +describe('isInsideTmux', () => { + it('detects tmux via the TMUX env var', () => { + expect(isInsideTmux({ TMUX: '/private/tmp/tmux-501/default,1234,0' })).toBe(true); + }); + + it('returns false when TMUX is empty or unset', () => { + expect(isInsideTmux({ TMUX: '' })).toBe(false); + expect(isInsideTmux({})).toBe(false); + }); +}); diff --git a/apps/kimi-code/test/tui/terminal-theme.test.ts b/apps/kimi-code/test/tui/terminal-theme.test.ts new file mode 100644 index 000000000..68a1818b2 --- /dev/null +++ b/apps/kimi-code/test/tui/terminal-theme.test.ts @@ -0,0 +1,160 @@ +import { describe, expect, it, vi } from "vitest"; + +import type { TUIState } from "#/tui/kimi-tui"; +import { + DISABLE_TERMINAL_THEME_REPORTING, + ENABLE_TERMINAL_THEME_REPORTING, + OSC11_QUERY, + QUERY_TERMINAL_THEME, + TERMINAL_THEME_DARK, + TERMINAL_THEME_LIGHT, + createTerminalThemeInputState, + hasTerminalThemeReport, + handleTerminalThemeInput, + installTerminalThemeTracking, +} from "#/tui/utils/terminal-theme"; + +type InputListener = Parameters<TUIState["ui"]["addInputListener"]>[0]; +const DARK_OSC11_REPORT = "\u001B]11;rgb:2828/2c2c/3434\u0007"; +const LIGHT_OSC11_REPORT = "\u001B]11;rgb:fafa/fbfb/fcfc\u0007"; + +describe("terminal theme tracking", () => { + it("recognizes terminal theme reports as change notifications", () => { + expect(hasTerminalThemeReport(TERMINAL_THEME_DARK)).toBe(true); + expect(hasTerminalThemeReport(TERMINAL_THEME_LIGHT)).toBe(true); + expect(hasTerminalThemeReport(`x${TERMINAL_THEME_LIGHT}y`)).toBe(true); + expect(hasTerminalThemeReport("x")).toBe(false); + }); + + it("consumes theme reports by querying the terminal background", () => { + const terminal = { write: vi.fn() }; + const onTheme = vi.fn(); + + expect(handleTerminalThemeInput(TERMINAL_THEME_DARK, terminal, onTheme)).toEqual({ + consume: true, + }); + expect(handleTerminalThemeInput(TERMINAL_THEME_LIGHT, terminal, onTheme)).toEqual({ + consume: true, + }); + + expect(terminal.write).toHaveBeenCalledTimes(2); + expect(terminal.write).toHaveBeenCalledWith(OSC11_QUERY); + expect(onTheme).not.toHaveBeenCalled(); + }); + + it("strips terminal theme reports without dropping coalesced input", () => { + const terminal = { write: vi.fn() }; + const onTheme = vi.fn(); + + expect(handleTerminalThemeInput(`a${TERMINAL_THEME_LIGHT}b`, terminal, onTheme)).toEqual({ + data: "ab", + }); + + expect(terminal.write).toHaveBeenCalledWith(OSC11_QUERY); + expect(onTheme).not.toHaveBeenCalled(); + }); + + it("consumes OSC 11 background reports and forwards resolved themes", () => { + const terminal = { write: vi.fn() }; + const onTheme = vi.fn(); + + expect(handleTerminalThemeInput(DARK_OSC11_REPORT, terminal, onTheme)).toEqual({ + consume: true, + }); + expect(onTheme).toHaveBeenLastCalledWith("dark"); + + expect(handleTerminalThemeInput(LIGHT_OSC11_REPORT, terminal, onTheme)).toEqual({ + consume: true, + }); + expect(onTheme).toHaveBeenLastCalledWith("light"); + + expect(handleTerminalThemeInput("x", terminal, onTheme)).toBeUndefined(); + expect(handleTerminalThemeInput("]", terminal, onTheme)).toBeUndefined(); + expect(onTheme).toHaveBeenCalledTimes(2); + expect(terminal.write).not.toHaveBeenCalled(); + }); + + it("strips OSC 11 background reports without dropping coalesced input", () => { + const terminal = { write: vi.fn() }; + const onTheme = vi.fn(); + + expect(handleTerminalThemeInput(`a${DARK_OSC11_REPORT}b`, terminal, onTheme)).toEqual({ + data: "ab", + }); + + expect(onTheme).toHaveBeenCalledWith("dark"); + expect(terminal.write).not.toHaveBeenCalled(); + }); + + it("accumulates fragmented OSC 11 background reports", () => { + const terminal = { write: vi.fn() }; + const onTheme = vi.fn(); + const inputState = createTerminalThemeInputState(); + + expect( + handleTerminalThemeInput("\u001B]11;rgb:2828/2c2c/3", terminal, onTheme, inputState), + ).toEqual({ consume: true }); + expect(onTheme).not.toHaveBeenCalled(); + + expect(handleTerminalThemeInput("434\u0007", terminal, onTheme, inputState)).toEqual({ + consume: true, + }); + expect(onTheme).toHaveBeenCalledWith("dark"); + expect(inputState.osc11Buffer).toBe(""); + }); + + it("forwards input that follows a fragmented OSC 11 background report", () => { + const terminal = { write: vi.fn() }; + const onTheme = vi.fn(); + const inputState = createTerminalThemeInputState(); + + expect( + handleTerminalThemeInput("\u001B]11;rgb:2828/2c2c/3", terminal, onTheme, inputState), + ).toEqual({ consume: true }); + + expect(handleTerminalThemeInput("434\u0007x", terminal, onTheme, inputState)).toEqual({ + data: "x", + }); + expect(onTheme).toHaveBeenCalledWith("dark"); + expect(inputState.osc11Buffer).toBe(""); + }); + + it("enables reporting, queries current theme, and disables on dispose", () => { + const listeners: InputListener[] = []; + const removeInputListener = vi.fn(); + const onTheme = vi.fn(); + const state = { + terminal: { + write: vi.fn(), + }, + ui: { + addInputListener: vi.fn((listener: InputListener) => { + listeners.push(listener); + return removeInputListener; + }), + }, + } as unknown as Pick<TUIState, "terminal" | "ui">; + + const dispose = installTerminalThemeTracking(state, onTheme); + + expect(state.terminal.write).toHaveBeenCalledWith(ENABLE_TERMINAL_THEME_REPORTING); + expect(state.terminal.write).toHaveBeenCalledWith(OSC11_QUERY); + expect(state.terminal.write).toHaveBeenCalledWith(QUERY_TERMINAL_THEME); + expect(listeners).toHaveLength(1); + + expect(listeners[0]?.(TERMINAL_THEME_LIGHT)).toEqual({ consume: true }); + expect(state.terminal.write).toHaveBeenCalledWith(OSC11_QUERY); + expect(onTheme).not.toHaveBeenCalled(); + + expect(listeners[0]?.("\u001B]11;rgb:2828/2c2c/3")).toEqual({ consume: true }); + expect(onTheme).not.toHaveBeenCalled(); + + expect(listeners[0]?.("434\u0007")).toEqual({ consume: true }); + expect(onTheme).toHaveBeenCalledWith("dark"); + + dispose(); + + expect(removeInputListener).toHaveBeenCalledOnce(); + expect(state.terminal.write).toHaveBeenCalledWith(DISABLE_TERMINAL_THEME_REPORTING); + }); +}); diff --git a/apps/kimi-code/test/tui/utils/mcp-oauth.test.ts b/apps/kimi-code/test/tui/utils/mcp-oauth.test.ts new file mode 100644 index 000000000..1e1808508 --- /dev/null +++ b/apps/kimi-code/test/tui/utils/mcp-oauth.test.ts @@ -0,0 +1,106 @@ +import { + MCP_OAUTH_AUTHORIZATION_URL_TOOL_UPDATE, + type ToolUpdate, +} from '@moonshot-ai/kimi-code-sdk'; +import { describe, expect, it, vi } from 'vitest'; + +import { + McpOAuthAuthorizationUrlOpener, + type OpenUrl, + parseMcpOAuthAuthorizationUrlUpdate, +} from '#/tui/utils/mcp-oauth'; + +describe('parseMcpOAuthAuthorizationUrlUpdate', () => { + it('extracts MCP OAuth authorization URLs from structured tool updates', () => { + const update: ToolUpdate = { + kind: 'custom', + customKind: MCP_OAUTH_AUTHORIZATION_URL_TOOL_UPDATE, + customData: { + serverName: 'linear', + authorizationUrl: 'https://linear.example/oauth?state=abc', + }, + }; + + expect(parseMcpOAuthAuthorizationUrlUpdate(update)).toEqual({ + serverName: 'linear', + authorizationUrl: 'https://linear.example/oauth?state=abc', + }); + }); + + it('ignores unrelated or malformed updates', () => { + const unrelated: ToolUpdate = { + kind: 'status', + text: 'https://linear.example/oauth?state=abc', + }; + const malformed: ToolUpdate = { + kind: 'custom', + customKind: MCP_OAUTH_AUTHORIZATION_URL_TOOL_UPDATE, + customData: { + serverName: 'linear', + authorizationUrl: 'file:///tmp/callback', + }, + }; + + expect(parseMcpOAuthAuthorizationUrlUpdate(unrelated)).toBeUndefined(); + expect(parseMcpOAuthAuthorizationUrlUpdate(malformed)).toBeUndefined(); + }); +}); + +describe('McpOAuthAuthorizationUrlOpener', () => { + it('opens authorization URLs from structured tool progress updates', () => { + const openUrl = vi.fn<OpenUrl>(); + const opener = new McpOAuthAuthorizationUrlOpener(openUrl); + + opener.handleToolProgress({ + toolCallId: 'tool-1', + update: authorizationUrlUpdate('https://linear.example/oauth?state=abc'), + }); + + expect(openUrl).toHaveBeenCalledTimes(1); + expect(openUrl).toHaveBeenCalledWith('https://linear.example/oauth?state=abc'); + }); + + it('opens each authorization URL once per tool call', () => { + const openUrl = vi.fn<OpenUrl>(); + const opener = new McpOAuthAuthorizationUrlOpener(openUrl); + const update = authorizationUrlUpdate('https://linear.example/oauth?state=abc'); + + opener.handleToolProgress({ toolCallId: 'tool-1', update }); + opener.handleToolProgress({ toolCallId: 'tool-1', update }); + opener.handleToolProgress({ toolCallId: 'tool-2', update }); + + expect(openUrl).toHaveBeenCalledTimes(2); + expect(openUrl).toHaveBeenNthCalledWith(1, 'https://linear.example/oauth?state=abc'); + expect(openUrl).toHaveBeenNthCalledWith(2, 'https://linear.example/oauth?state=abc'); + }); + + it('ignores progress updates that do not contain an MCP OAuth authorization URL', () => { + const openUrl = vi.fn<OpenUrl>(); + const opener = new McpOAuthAuthorizationUrlOpener(openUrl); + + opener.handleToolProgress({ + toolCallId: 'tool-1', + update: { + kind: 'status', + text: 'https://linear.example/oauth?state=abc', + }, + }); + opener.handleToolProgress({ + toolCallId: 'tool-1', + update: authorizationUrlUpdate('file:///tmp/callback'), + }); + + expect(openUrl).not.toHaveBeenCalled(); + }); +}); + +function authorizationUrlUpdate(authorizationUrl: string): ToolUpdate { + return { + kind: 'custom', + customKind: MCP_OAUTH_AUTHORIZATION_URL_TOOL_UPDATE, + customData: { + serverName: 'linear', + authorizationUrl, + }, + }; +} diff --git a/apps/kimi-code/test/utils/clipboard/clipboard-image.test.ts b/apps/kimi-code/test/utils/clipboard/clipboard-image.test.ts new file mode 100644 index 000000000..7e802db6a --- /dev/null +++ b/apps/kimi-code/test/utils/clipboard/clipboard-image.test.ts @@ -0,0 +1,161 @@ +import { mkdtempSync, rmSync, truncateSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join, resolve } from 'node:path'; +import { pathToFileURL } from 'node:url'; + +import { describe, expect, it, vi } from 'vitest'; + +import { ClipboardMediaError, readClipboardMedia } from '#/utils/clipboard/clipboard-image'; +import type { ClipboardModule } from '#/utils/clipboard/clipboard-native'; + +function png(width: number, height: number): Uint8Array { + const bytes = new Uint8Array(24); + bytes.set([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a], 0); + bytes.set([0x00, 0x00, 0x00, 0x0d], 8); + bytes.set([0x49, 0x48, 0x44, 0x52], 12); + bytes[16] = (width >>> 24) & 0xff; + bytes[17] = (width >>> 16) & 0xff; + bytes[18] = (width >>> 8) & 0xff; + bytes[19] = width & 0xff; + bytes[20] = (height >>> 24) & 0xff; + bytes[21] = (height >>> 16) & 0xff; + bytes[22] = (height >>> 8) & 0xff; + bytes[23] = height & 0xff; + return bytes; +} + +function fakeClipboard(overrides: Partial<ClipboardModule>): ClipboardModule { + return { + hasImage: vi.fn(() => false), + getImageBinary: vi.fn(async () => []), + ...overrides, + }; +} + +function noMacOsPaths(): { stdout: Buffer; ok: boolean } { + return { stdout: Buffer.alloc(0), ok: false }; +} + +describe('readClipboardMedia', () => { + it('reads a copied image file from its real path instead of the Finder preview icon', async () => { + const dir = mkdtempSync(join(tmpdir(), 'kimi-code-clip-')); + try { + const imagePath = join(dir, 'photo.png'); + const imageBytes = png(12, 34); + writeFileSync(imagePath, imageBytes); + const getImageBinary = vi.fn(async () => Array.from(png(1, 1))); + const clip = fakeClipboard({ + availableFormats: vi.fn(() => ['public.file-url', 'public.png']), + hasImage: vi.fn(() => true), + getImageBinary, + }); + const runCommand = vi.fn(() => ({ stdout: Buffer.from(`${imagePath}\n`), ok: true })); + + const media = await readClipboardMedia({ platform: 'darwin', clipboard: clip, runCommand }); + + expect(media).toEqual({ + kind: 'image', + bytes: imageBytes, + mimeType: 'image/png', + }); + expect(getImageBinary).not.toHaveBeenCalled(); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); + + it('prefers a video file URL over an available image preview', async () => { + const dir = mkdtempSync(join(tmpdir(), 'kimi-code-clip-')); + try { + const videoPath = join(dir, 'sample.mov'); + writeFileSync(videoPath, new Uint8Array([0, 1, 2])); + const getImageBinary = vi.fn(async () => [0x89, 0x50, 0x4e, 0x47]); + const clip = fakeClipboard({ + availableFormats: vi.fn(() => ['public.file-url', 'public.png']), + hasText: vi.fn(() => true), + getText: vi.fn(async () => pathToFileURL(videoPath).toString()), + hasImage: vi.fn(() => true), + getImageBinary, + }); + + const media = await readClipboardMedia({ + platform: 'darwin', + clipboard: clip, + runCommand: noMacOsPaths, + }); + + expect(media?.kind).toBe('video'); + expect(media).toMatchObject({ + mimeType: 'video/quicktime', + filename: 'sample.mov', + sourcePath: videoPath, + }); + expect(getImageBinary).not.toHaveBeenCalled(); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); + + it('falls back to native image bytes when no video file is present', async () => { + const clip = fakeClipboard({ + availableFormats: vi.fn(() => ['public.png']), + hasText: vi.fn(() => false), + hasImage: vi.fn(() => true), + getImageBinary: vi.fn(async () => [0x89, 0x50, 0x4e, 0x47]), + }); + + const media = await readClipboardMedia({ + platform: 'darwin', + clipboard: clip, + runCommand: noMacOsPaths, + }); + + expect(media).toEqual({ + kind: 'image', + bytes: new Uint8Array([0x89, 0x50, 0x4e, 0x47]), + mimeType: 'image/png', + }); + }); + + it('does not consume file-like clipboard image previews when no real file path is readable', async () => { + const getImageBinary = vi.fn(async () => Array.from(png(1, 1))); + const clip = fakeClipboard({ + availableFormats: vi.fn(() => ['public.file-url', 'public.png']), + hasImage: vi.fn(() => true), + getImageBinary, + }); + + const media = await readClipboardMedia({ + platform: 'darwin', + clipboard: clip, + runCommand: noMacOsPaths, + }); + + expect(media).toBeNull(); + expect(getImageBinary).not.toHaveBeenCalled(); + }); + + it('rejects pasted videos larger than 100 MB', async () => { + const dir = mkdtempSync(join(tmpdir(), 'kimi-code-clip-')); + try { + const videoPath = resolve(dir, 'too-big.mp4'); + writeFileSync(videoPath, new Uint8Array([0])); + truncateSync(videoPath, 101 * 1024 * 1024); + const clip = fakeClipboard({ + availableFormats: vi.fn(() => ['public.file-url']), + hasText: vi.fn(() => true), + getText: vi.fn(async () => pathToFileURL(videoPath).toString()), + }); + + await expect( + readClipboardMedia({ + platform: 'darwin', + clipboard: clip, + runCommand: noMacOsPaths, + }), + ).rejects.toThrow(ClipboardMediaError); + } finally { + rmSync(dir, { recursive: true, force: true }); + } + }); +}); diff --git a/apps/kimi-code/test/utils/git/git-ls-files.test.ts b/apps/kimi-code/test/utils/git/git-ls-files.test.ts new file mode 100644 index 000000000..d7b193a5c --- /dev/null +++ b/apps/kimi-code/test/utils/git/git-ls-files.test.ts @@ -0,0 +1,113 @@ +import { execFileSync } from 'node:child_process'; +import { mkdtempSync, rmSync, writeFileSync, mkdirSync, utimesSync, statSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; + +import { createGitLsFilesCache } from '#/utils/git/git-ls-files'; + +function git(cwd: string, ...args: string[]): void { + execFileSync('git', args, { cwd, stdio: 'ignore' }); +} + +function commit(cwd: string, message: string): void { + git(cwd, '-c', 'user.email=test@example.com', '-c', 'user.name=Test', 'commit', '-m', message); +} + +describe('createGitLsFilesCache', () => { + let dir: string; + + beforeEach(() => { + dir = mkdtempSync(join(tmpdir(), 'git-ls-files-test-')); + }); + + afterEach(() => { + rmSync(dir, { recursive: true, force: true }); + }); + + it('returns null for a non-git directory', () => { + const cache = createGitLsFilesCache(dir); + expect(cache.isGitRepo()).toBe(false); + expect(cache.list()).toBeNull(); + }); + + it('lists tracked files in a git repo', () => { + git(dir, 'init'); + writeFileSync(join(dir, 'a.ts'), ''); + mkdirSync(join(dir, 'src')); + writeFileSync(join(dir, 'src/b.ts'), ''); + git(dir, 'add', '.'); + commit(dir, 'init'); + + const cache = createGitLsFilesCache(dir); + expect(cache.isGitRepo()).toBe(true); + const snap = cache.getSnapshot(); + expect(snap).not.toBeNull(); + expect(snap!.files).toContain('a.ts'); + expect(snap!.files).toContain('src/b.ts'); + expect(snap!.mtimeByPath.has('a.ts')).toBe(true); + expect(snap!.mtimeByPath.get('a.ts')!).toBeGreaterThan(0); + expect(cache.getSnapshot()).toBe(snap); + }); + + it('includes untracked-but-not-ignored files', () => { + git(dir, 'init'); + writeFileSync(join(dir, '.gitignore'), 'ignored.ts\n'); + writeFileSync(join(dir, 'tracked.ts'), ''); + git(dir, 'add', 'tracked.ts'); + commit(dir, 'init'); + + // Create both an untracked file and one that matches .gitignore. + writeFileSync(join(dir, 'new.ts'), ''); + writeFileSync(join(dir, 'ignored.ts'), ''); + + const cache = createGitLsFilesCache(dir); + const files = cache.list()!; + expect(files).toContain('tracked.ts'); + expect(files).toContain('new.ts'); + expect(files).not.toContain('ignored.ts'); + }); + + it('builds a recency order from recent commits', () => { + git(dir, 'init'); + writeFileSync(join(dir, 'old.ts'), ''); + git(dir, 'add', 'old.ts'); + commit(dir, 'old'); + writeFileSync(join(dir, 'new.ts'), ''); + git(dir, 'add', 'new.ts'); + commit(dir, 'new'); + + const cache = createGitLsFilesCache(dir); + const snap = cache.getSnapshot()!; + const newRank = snap.recencyOrder.get('new.ts'); + const oldRank = snap.recencyOrder.get('old.ts'); + expect(newRank).toBeDefined(); + expect(oldRank).toBeDefined(); + expect(newRank!).toBeLessThan(oldRank!); + }); + + it('invalidates when .git/index mtime changes', () => { + git(dir, 'init'); + writeFileSync(join(dir, 'a.ts'), ''); + git(dir, 'add', '.'); + commit(dir, 'init'); + + const cache = createGitLsFilesCache(dir); + const first = cache.list()!; + expect(first).toContain('a.ts'); + + // Touch .git/index forward to simulate a new commit / add. + const indexPath = join(dir, '.git', 'index'); + const s = statSync(indexPath); + const future = new Date(s.mtimeMs + 5000); + utimesSync(indexPath, future, future); + + writeFileSync(join(dir, 'b.ts'), ''); + git(dir, 'add', 'b.ts'); + + const second = cache.list()!; + expect(second).not.toBe(first); // new snapshot + expect(second).toContain('b.ts'); + }); +}); diff --git a/apps/kimi-code/test/utils/git/git-status.test.ts b/apps/kimi-code/test/utils/git/git-status.test.ts new file mode 100644 index 000000000..fed27dd87 --- /dev/null +++ b/apps/kimi-code/test/utils/git/git-status.test.ts @@ -0,0 +1,200 @@ +/* eslint-disable import/first -- vi.mock setup must run before the imports it stubs out. */ +import { afterEach, describe, expect, it, vi } from 'vitest'; + +const mocks = vi.hoisted(() => ({ + spawnSync: vi.fn(), + execFile: vi.fn(), +})); + +vi.mock('node:child_process', () => ({ + execFile: mocks.execFile, + spawnSync: mocks.spawnSync, +})); + +import { createGitStatusCache, formatGitBadge } from '#/utils/git/git-status'; + +afterEach(() => { + vi.useRealTimers(); + vi.clearAllMocks(); +}); + +describe('git status cache', () => { + it('caches branch and status reads until their TTL expires', async () => { + vi.useFakeTimers(); + vi.setSystemTime(new Date('2026-04-24T00:00:00Z')); + mocks.execFile.mockImplementation( + ( + _cmd: string, + _args: string[], + _options: unknown, + callback: (error: Error | null, stdout: string, stderr: string) => void, + ) => { + callback(new Error('no pull request'), '', ''); + }, + ); + mocks.spawnSync.mockImplementation((_cmd: string, args: string[]) => { + if (args.includes('rev-parse')) { + return { status: 0, stdout: 'true\n' }; + } + if (args.includes('branch')) { + return { status: 0, stdout: 'main\n' }; + } + if (args.includes('status')) { + return { + status: 0, + stdout: '## main...origin/main [ahead 2, behind 1]\n M src/app.ts\n', + }; + } + if (args.includes('diff')) { + return { status: 0, stdout: '4\t1\tsrc/app.ts\n' }; + } + return { status: 1, stdout: '' }; + }); + + const cache = createGitStatusCache('/tmp/repo'); + + expect(cache.getStatus()).toEqual({ + branch: 'main', + dirty: true, + ahead: 2, + behind: 1, + diffAdded: 4, + diffDeleted: 1, + pullRequest: null, + }); + expect(cache.getStatus()).toEqual({ + branch: 'main', + dirty: true, + ahead: 2, + behind: 1, + diffAdded: 4, + diffDeleted: 1, + pullRequest: null, + }); + expect(mocks.spawnSync).toHaveBeenCalledTimes(4); + expect(mocks.execFile).toHaveBeenCalledTimes(1); + + await Promise.resolve(); + + vi.setSystemTime(new Date('2026-04-24T00:00:06Z')); + cache.getStatus(); + expect(mocks.spawnSync).toHaveBeenCalledTimes(5); + expect(mocks.execFile).toHaveBeenCalledTimes(1); + + vi.setSystemTime(new Date('2026-04-24T00:00:16Z')); + cache.getStatus(); + expect(mocks.spawnSync).toHaveBeenCalledTimes(8); + expect(mocks.execFile).toHaveBeenCalledTimes(1); + }); + + it('reads uncommitted diff line counts and current pull request metadata', async () => { + const onChange = vi.fn(); + mocks.execFile.mockImplementation( + ( + _cmd: string, + _args: string[], + _options: unknown, + callback: (error: Error | null, stdout: string, stderr: string) => void, + ) => { + callback(null, '{"number":12,"url":"https://github.com/acme/repo/pull/12"}\n', ''); + }, + ); + mocks.spawnSync.mockImplementation((_cmd: string, args: string[]) => { + if (args.includes('rev-parse')) { + return { status: 0, stdout: 'true\n' }; + } + if (args.includes('branch')) { + return { status: 0, stdout: 'feature/footer\n' }; + } + if (args.includes('status')) { + return { + status: 0, + stdout: '## feature/footer...origin/feature/footer\n M src/app.ts\n', + }; + } + if (args.includes('diff')) { + return { + status: 0, + stdout: '10\t3\tsrc/app.ts\n-\t-\timage.png\n0\t5\tdeleted.ts\n', + }; + } + return { status: 1, stdout: '' }; + }); + + const cache = createGitStatusCache('/tmp/repo', { onChange }); + expect(cache.getStatus()).toEqual({ + branch: 'feature/footer', + dirty: true, + ahead: 0, + behind: 0, + diffAdded: 10, + diffDeleted: 8, + pullRequest: null, + }); + + await Promise.resolve(); + + expect(onChange).toHaveBeenCalledTimes(1); + expect(cache.getStatus()).toEqual({ + branch: 'feature/footer', + dirty: true, + ahead: 0, + behind: 0, + diffAdded: 10, + diffDeleted: 8, + pullRequest: { + number: 12, + url: 'https://github.com/acme/repo/pull/12', + }, + }); + }); + + it('returns null when the working directory is not a git repo and formats badges', () => { + mocks.spawnSync.mockReturnValue({ status: 1, stdout: '' }); + expect(createGitStatusCache('/tmp/not-a-repo').getStatus()).toBeNull(); + expect( + formatGitBadge({ + branch: 'main', + dirty: true, + ahead: 2, + behind: 1, + diffAdded: 12, + diffDeleted: 3, + pullRequest: null, + }), + ).toBe('main [+12 -3 ↑2↓1]'); + expect( + formatGitBadge({ + branch: 'main', + dirty: true, + ahead: 0, + behind: 0, + diffAdded: 0, + diffDeleted: 0, + pullRequest: null, + }), + ).toBe('main [±]'); + }); + + it('formats pull request badges as terminal hyperlinks when requested', () => { + const linked = formatGitBadge( + { + branch: 'feature/footer', + dirty: false, + ahead: 0, + behind: 0, + diffAdded: 0, + diffDeleted: 0, + pullRequest: { + number: 12, + url: 'https://github.com/acme/repo/pull/12', + }, + }, + { linkPullRequest: true }, + ); + + expect(linked).toContain('[PR#12]'); + expect(linked).toContain('\u001B]8;;https://github.com/acme/repo/pull/12\u0007'); + expect(linked).toContain('\u001B]8;;\u0007'); + }); +}); diff --git a/apps/kimi-code/test/utils/history/input-history.test.ts b/apps/kimi-code/test/utils/history/input-history.test.ts new file mode 100644 index 000000000..43269f3d7 --- /dev/null +++ b/apps/kimi-code/test/utils/history/input-history.test.ts @@ -0,0 +1,84 @@ +import { mkdtempSync, rmSync, writeFileSync, readFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +import { afterEach, beforeEach, describe, it, expect } from 'vitest'; + +import { loadInputHistory, appendInputHistory } from '#/utils/history/input-history'; + +let dir: string; +let file: string; + +beforeEach(() => { + dir = mkdtempSync(join(tmpdir(), 'input-history-')); + file = join(dir, 'history.jsonl'); +}); + +afterEach(() => { + rmSync(dir, { recursive: true, force: true }); +}); + +describe('input-history', () => { + it('returns empty when file does not exist', async () => { + expect(await loadInputHistory(file)).toEqual([]); + }); + + it('parses valid jsonl entries in order', async () => { + writeFileSync( + file, + [ + JSON.stringify({ content: 'first' }), + JSON.stringify({ content: 'second' }), + '', + JSON.stringify({ content: 'third' }), + ].join('\n'), + 'utf-8', + ); + const entries = await loadInputHistory(file); + expect(entries).toEqual([{ content: 'first' }, { content: 'second' }, { content: 'third' }]); + }); + + it('skips malformed lines without aborting the load', async () => { + writeFileSync( + file, + [ + JSON.stringify({ content: 'good' }), + 'not json', + JSON.stringify({ wrong_field: 'x' }), + JSON.stringify({ content: 'tail' }), + ].join('\n'), + 'utf-8', + ); + const entries = await loadInputHistory(file); + expect(entries).toEqual([{ content: 'good' }, { content: 'tail' }]); + }); + + it('appends a new entry and creates the parent directory', async () => { + const nested = join(dir, 'nested', 'sub', 'history.jsonl'); + const written = await appendInputHistory(nested, 'hello'); + expect(written).toBe(true); + const raw = readFileSync(nested, 'utf-8').trim().split('\n'); + expect(raw).toHaveLength(1); + expect(JSON.parse(raw[0]!)).toEqual({ content: 'hello' }); + }); + + it('skips empty / whitespace-only entries', async () => { + expect(await appendInputHistory(file, '')).toBe(false); + expect(await appendInputHistory(file, ' ')).toBe(false); + expect(await loadInputHistory(file)).toEqual([]); + }); + + it('skips when content equals the previous entry (consecutive dedup)', async () => { + expect(await appendInputHistory(file, 'a')).toBe(true); + expect(await appendInputHistory(file, 'a', 'a')).toBe(false); + expect(await appendInputHistory(file, 'b', 'a')).toBe(true); + const entries = await loadInputHistory(file); + expect(entries).toEqual([{ content: 'a' }, { content: 'b' }]); + }); + + it('trims whitespace before persisting', async () => { + expect(await appendInputHistory(file, ' hi ')).toBe(true); + const entries = await loadInputHistory(file); + expect(entries).toEqual([{ content: 'hi' }]); + }); +}); diff --git a/apps/kimi-code/test/utils/image/image-mime.test.ts b/apps/kimi-code/test/utils/image/image-mime.test.ts new file mode 100644 index 000000000..bcc426bdf --- /dev/null +++ b/apps/kimi-code/test/utils/image/image-mime.test.ts @@ -0,0 +1,98 @@ +import { describe, it, expect } from 'vitest'; + +import { parseImageMeta } from '#/utils/image/image-mime'; + +function png(width: number, height: number): Uint8Array { + // 8-byte PNG signature + IHDR length (4) + 'IHDR' + width (4 BE) + height (4 BE) + ... + const bytes = new Uint8Array(24); + bytes.set([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a], 0); + // length = 13 (IHDR body) + bytes.set([0x00, 0x00, 0x00, 0x0d], 8); + bytes.set([0x49, 0x48, 0x44, 0x52], 12); // "IHDR" + // width + bytes[16] = (width >>> 24) & 0xff; + bytes[17] = (width >>> 16) & 0xff; + bytes[18] = (width >>> 8) & 0xff; + bytes[19] = width & 0xff; + // height + bytes[20] = (height >>> 24) & 0xff; + bytes[21] = (height >>> 16) & 0xff; + bytes[22] = (height >>> 8) & 0xff; + bytes[23] = height & 0xff; + return bytes; +} + +function jpeg(width: number, height: number): Uint8Array { + // SOI + SOF0 marker with the minimal segment body. + // Layout: FFD8 FFC0 <len16 BE> <precision 08> <height BE> <width BE> <components 03> + const body = new Uint8Array([ + 0xff, + 0xd8, // SOI + 0xff, + 0xc0, // SOF0 + 0x00, + 0x11, // segment length (17 bytes including itself) + 0x08, // precision + (height >> 8) & 0xff, + height & 0xff, + (width >> 8) & 0xff, + width & 0xff, + 0x03, // components + // 3 component spec blocks (3 bytes each) — not parsed but required for realism + 0x01, + 0x22, + 0x00, + 0x02, + 0x11, + 0x01, + 0x03, + 0x11, + 0x01, + ]); + return body; +} + +function gif(width: number, height: number): Uint8Array { + const bytes = new Uint8Array(10); + // "GIF89a" + bytes.set([0x47, 0x49, 0x46, 0x38, 0x39, 0x61], 0); + bytes[6] = width & 0xff; + bytes[7] = (width >> 8) & 0xff; + bytes[8] = height & 0xff; + bytes[9] = (height >> 8) & 0xff; + return bytes; +} + +describe('parseImageMeta', () => { + it('recognises PNG with correct dimensions', () => { + const meta = parseImageMeta(png(640, 480)); + expect(meta).toEqual({ mime: 'image/png', width: 640, height: 480 }); + }); + + it('recognises JPEG with correct dimensions', () => { + const meta = parseImageMeta(jpeg(1280, 720)); + expect(meta).toEqual({ mime: 'image/jpeg', width: 1280, height: 720 }); + }); + + it('recognises GIF (89a)', () => { + const meta = parseImageMeta(gif(100, 200)); + expect(meta).toEqual({ mime: 'image/gif', width: 100, height: 200 }); + }); + + it('recognises GIF (87a)', () => { + const bytes = gif(50, 75); + bytes[4] = 0x37; // '7' + const meta = parseImageMeta(bytes); + expect(meta).toEqual({ mime: 'image/gif', width: 50, height: 75 }); + }); + + it('returns null for non-image bytes', () => { + expect(parseImageMeta(new Uint8Array([1, 2, 3, 4]))).toBeNull(); + expect(parseImageMeta(new Uint8Array([]))).toBeNull(); + }); + + it('returns null for truncated PNG', () => { + const full = png(10, 10); + expect(parseImageMeta(full.slice(0, 20))).toBeNull(); + }); +}); diff --git a/apps/kimi-code/test/utils/paths.test.ts b/apps/kimi-code/test/utils/paths.test.ts new file mode 100644 index 000000000..5c9d3dc03 --- /dev/null +++ b/apps/kimi-code/test/utils/paths.test.ts @@ -0,0 +1,73 @@ +import { createHash } from 'node:crypto'; +import { homedir } from 'node:os'; +import { join } from 'node:path'; + +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; + +import { getDataDir, getInputHistoryFile, getLogDir, getUpdateStateFile } from '#/utils/paths'; + +const originalEnv = { ...process.env }; + +beforeEach(() => { + delete process.env['KIMI_CODE_HOME']; +}); + +afterEach(() => { + process.env = { ...originalEnv }; +}); + +describe('getDataDir', () => { + it('returns ~/.kimi-code when KIMI_CODE_HOME is not set', () => { + expect(getDataDir()).toBe(join(homedir(), '.kimi-code')); + }); + + it('returns KIMI_CODE_HOME when set', () => { + process.env['KIMI_CODE_HOME'] = '/tmp/kimi-test-data'; + expect(getDataDir()).toBe('/tmp/kimi-test-data'); + }); + + it('returns KIMI_CODE_HOME even if it is a relative path', () => { + process.env['KIMI_CODE_HOME'] = 'relative/path'; + expect(getDataDir()).toBe('relative/path'); + }); +}); + +describe('getLogDir', () => { + it('returns <dataDir>/logs', () => { + expect(getLogDir()).toBe(join(homedir(), '.kimi-code', 'logs')); + }); + + it('respects KIMI_CODE_HOME', () => { + process.env['KIMI_CODE_HOME'] = '/z'; + expect(getLogDir()).toBe(join('/z', 'logs')); + }); +}); + +describe('getUpdateStateFile', () => { + it('returns <dataDir>/updates/latest.json', () => { + expect(getUpdateStateFile()).toBe(join(homedir(), '.kimi-code', 'updates', 'latest.json')); + }); + + it('respects KIMI_CODE_HOME', () => { + process.env['KIMI_CODE_HOME'] = '/updates-home'; + expect(getUpdateStateFile()).toBe(join('/updates-home', 'updates', 'latest.json')); + }); +}); + +describe('getInputHistoryFile', () => { + it('returns <dataDir>/user-history/<md5(workDir)>.jsonl', () => { + const workDir = '/home/user/project'; + const hash = createHash('md5').update(workDir, 'utf-8').digest('hex'); + expect(getInputHistoryFile(workDir)).toBe( + join(homedir(), '.kimi-code', 'user-history', `${hash}.jsonl`), + ); + }); + + it('respects KIMI_CODE_HOME', () => { + process.env['KIMI_CODE_HOME'] = '/custom/data'; + const hash = createHash('md5').update('/proj', 'utf-8').digest('hex'); + expect(getInputHistoryFile('/proj')).toBe( + join('/custom/data', 'user-history', `${hash}.jsonl`), + ); + }); +}); diff --git a/apps/kimi-code/test/utils/persistence.test.ts b/apps/kimi-code/test/utils/persistence.test.ts new file mode 100644 index 000000000..9ebe3f55a --- /dev/null +++ b/apps/kimi-code/test/utils/persistence.test.ts @@ -0,0 +1,116 @@ +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { z } from 'zod'; + +import { appendJsonlLine, readJsonFile, readJsonlFile, writeJsonFile } from '#/utils/persistence'; + +interface TestJson { + name: string; + count: number; +} + +const TestJsonSchema: z.ZodType<TestJson> = z.object({ + name: z.string(), + count: z.number().int(), +}); + +interface TestLine { + content: string; +} + +const TestLineSchema: z.ZodType<TestLine> = z.object({ + content: z.string(), +}); + +let dir: string; + +beforeEach(() => { + dir = mkdtempSync(join(tmpdir(), 'kimi-persistence-')); +}); + +afterEach(() => { + rmSync(dir, { recursive: true, force: true }); +}); + +describe('persistence helpers', () => { + it('readJsonFile returns fallback when file is missing', async () => { + const fallback = { name: 'fallback', count: 1 }; + await expect( + readJsonFile(join(dir, 'missing.json'), TestJsonSchema, fallback), + ).resolves.toEqual(fallback); + }); + + it('writeJsonFile writes schema-valid JSON atomically', async () => { + const file = join(dir, 'nested', 'state.json'); + await writeJsonFile(file, TestJsonSchema, { name: 'ok', count: 2 }); + + expect(JSON.parse(readFileSync(file, 'utf-8'))).toEqual({ name: 'ok', count: 2 }); + await expect( + readJsonFile(file, TestJsonSchema, { name: 'fallback', count: 0 }), + ).resolves.toEqual({ name: 'ok', count: 2 }); + }); + + it('readJsonFile rejects schema-invalid JSON', async () => { + const file = join(dir, 'bad.json'); + writeFileSync(file, JSON.stringify({ name: 'bad', count: 'nope' }), 'utf-8'); + + await expect( + readJsonFile(file, TestJsonSchema, { name: 'fallback', count: 0 }), + ).rejects.toThrow(); + }); + + it('writeJsonFile refuses to write config.toml', async () => { + await expect( + writeJsonFile(join(dir, 'config.toml'), TestJsonSchema, { name: 'bad', count: 1 }), + ).rejects.toThrow(/config\.toml/); + }); + + it('readJsonlFile preserves valid line order', async () => { + const file = join(dir, 'history.jsonl'); + writeFileSync( + file, + [ + JSON.stringify({ content: 'first' }), + JSON.stringify({ content: 'second' }), + JSON.stringify({ content: 'third' }), + ].join('\n'), + 'utf-8', + ); + + await expect(readJsonlFile(file, TestLineSchema)).resolves.toEqual([ + { content: 'first' }, + { content: 'second' }, + { content: 'third' }, + ]); + }); + + it('readJsonlFile skips malformed and schema-invalid lines', async () => { + const file = join(dir, 'history.jsonl'); + writeFileSync( + file, + [ + JSON.stringify({ content: 'good' }), + 'not json', + JSON.stringify({ wrong: 'shape' }), + '', + JSON.stringify({ content: 'tail' }), + ].join('\n'), + 'utf-8', + ); + + await expect(readJsonlFile(file, TestLineSchema)).resolves.toEqual([ + { content: 'good' }, + { content: 'tail' }, + ]); + }); + + it('appendJsonlLine creates the parent directory', async () => { + const file = join(dir, 'nested', 'history.jsonl'); + await appendJsonlLine(file, TestLineSchema, { content: 'hello' }); + + expect(readFileSync(file, 'utf-8').trim()).toBe(JSON.stringify({ content: 'hello' })); + }); +}); diff --git a/apps/kimi-code/test/utils/process/external-editor.test.ts b/apps/kimi-code/test/utils/process/external-editor.test.ts new file mode 100644 index 000000000..3e135d019 --- /dev/null +++ b/apps/kimi-code/test/utils/process/external-editor.test.ts @@ -0,0 +1,79 @@ +/* eslint-disable import/first -- vi.mock setup must run before the imports it stubs out. */ +import { EventEmitter } from 'node:events'; +import { writeFile } from 'node:fs/promises'; +import type * as FsPromises from 'node:fs/promises'; + +import { afterEach, describe, expect, it, vi } from 'vitest'; + +const mocks = vi.hoisted(() => ({ + spawn: vi.fn(), + rmCalls: vi.fn(), +})); + +vi.mock('node:child_process', () => ({ + spawn: mocks.spawn, +})); + +vi.mock('node:fs/promises', async () => { + const actual = await vi.importActual<typeof FsPromises>('node:fs/promises'); + return { + ...actual, + rm: (...args: Parameters<typeof actual.rm>) => { + mocks.rmCalls(...args); + return actual.rm(...args); + }, + }; +}); + +import { editInExternalEditor, resolveEditorCommand } from '#/utils/process/external-editor'; + +function shellPath(cmd: string): string { + const match = cmd.match(/'([^']+)'$/); + if (!match) throw new Error(`Could not parse temp path from: ${cmd}`); + return match[1]!; +} + +afterEach(() => { + vi.unstubAllEnvs(); + vi.clearAllMocks(); +}); + +describe('external-editor helpers', () => { + it('prefers configured editor, then VISUAL, then EDITOR', () => { + vi.stubEnv('VISUAL', 'nvim'); + vi.stubEnv('EDITOR', 'vim'); + + expect(resolveEditorCommand('code --wait')).toBe('code --wait'); + expect(resolveEditorCommand(null)).toBe('nvim'); + vi.stubEnv('VISUAL', ''); + expect(resolveEditorCommand()).toBe('vim'); + }); + + it('returns the edited contents on success and cleans up the temp directory', async () => { + mocks.spawn.mockImplementation((_cmd: string, args: string[]) => { + const child = new EventEmitter(); + void writeFile(shellPath(args[1]!), 'edited text', 'utf8').then(() => { + child.emit('exit', 0); + }); + return child as never; + }); + + await expect(editInExternalEditor('seed', 'code --wait')).resolves.toBe('edited text'); + expect(mocks.spawn).toHaveBeenCalledWith( + '/bin/sh', + ['-c', expect.stringMatching(/^code --wait /)], + { stdio: 'inherit' }, + ); + expect(mocks.rmCalls).toHaveBeenCalled(); + }); + + it('returns undefined when the editor exits non-zero', async () => { + mocks.spawn.mockImplementation(() => { + const child = new EventEmitter(); + queueMicrotask(() => child.emit('exit', 1)); + return child as never; + }); + + await expect(editInExternalEditor('seed', 'false')).resolves.toBeUndefined(); + }); +}); diff --git a/apps/kimi-code/test/utils/process/stdin.test.ts b/apps/kimi-code/test/utils/process/stdin.test.ts new file mode 100644 index 000000000..072c48eb0 --- /dev/null +++ b/apps/kimi-code/test/utils/process/stdin.test.ts @@ -0,0 +1,59 @@ +/* eslint-disable import/first -- vi.mock setup must run before the imports it stubs out. */ +import { describe, expect, it, vi, afterEach } from 'vitest'; + +const mocks = vi.hoisted(() => ({ + createInterface: vi.fn(), +})); + +vi.mock('node:readline', () => ({ + createInterface: mocks.createInterface, +})); + +import { createStdinLineReader, readStdinText } from '#/utils/process/stdin'; + +afterEach(() => { + vi.clearAllMocks(); +}); + +describe('stdin helpers', () => { + it('reads stdin text until end and trims the result', async () => { + const listeners = new Map<string, (...args: unknown[]) => void>(); + const onSpy = vi.spyOn(process.stdin, 'on').mockImplementation((( + event: string, + handler: (...args: unknown[]) => void, + ) => { + listeners.set(event, handler); + return process.stdin; + }) as never); + const resumeSpy = vi.spyOn(process.stdin, 'resume').mockImplementation(() => process.stdin); + + const pending = readStdinText(); + listeners.get('data')?.(Buffer.from(' hello world \n')); + listeners.get('end')?.(); + + await expect(pending).resolves.toBe('hello world'); + + onSpy.mockRestore(); + resumeSpy.mockRestore(); + }); + + it('yields lines from readline', async () => { + mocks.createInterface.mockReturnValue( + (async function* () { + yield 'one'; + yield 'two'; + })(), + ); + + const lines: string[] = []; + for await (const line of createStdinLineReader()) { + lines.push(line); + } + + expect(lines).toEqual(['one', 'two']); + expect(mocks.createInterface).toHaveBeenCalledWith({ + input: process.stdin, + crlfDelay: Infinity, + }); + }); +}); diff --git a/apps/kimi-code/test/utils/usage/usage-format.test.ts b/apps/kimi-code/test/utils/usage/usage-format.test.ts new file mode 100644 index 000000000..264bb5c55 --- /dev/null +++ b/apps/kimi-code/test/utils/usage/usage-format.test.ts @@ -0,0 +1,77 @@ +import { describe, it, expect } from 'vitest'; + +import { + formatTokenCount, + renderProgressBar, + ratioSeverity, + safeUsageRatio, +} from '#/utils/usage/usage-format'; + +describe('formatTokenCount', () => { + it('passes small values through unchanged', () => { + expect(formatTokenCount(0)).toBe('0'); + expect(formatTokenCount(1)).toBe('1'); + expect(formatTokenCount(999)).toBe('999'); + }); + + it('rounds integers over 1k to 1 decimal', () => { + expect(formatTokenCount(1_000)).toBe('1.0k'); + expect(formatTokenCount(1_234)).toBe('1.2k'); + expect(formatTokenCount(9_876)).toBe('9.9k'); + }); + + it('switches to M above a million', () => { + expect(formatTokenCount(1_000_000)).toBe('1.0M'); + expect(formatTokenCount(2_500_000)).toBe('2.5M'); + }); + + it('clamps negatives and NaN to 0', () => { + expect(formatTokenCount(-1)).toBe('0'); + expect(formatTokenCount(Number.NaN)).toBe('0'); + expect(formatTokenCount(Number.POSITIVE_INFINITY)).toBe('0'); + }); +}); + +describe('renderProgressBar', () => { + it('empty bar at ratio 0', () => { + expect(renderProgressBar(0, 10)).toBe('░'.repeat(10)); + }); + it('full bar at ratio 1', () => { + expect(renderProgressBar(1, 10)).toBe('█'.repeat(10)); + }); + it('half bar at ratio 0.5', () => { + expect(renderProgressBar(0.5, 10)).toBe('█'.repeat(5) + '░'.repeat(5)); + }); + it('clamps ratios outside [0,1]', () => { + expect(renderProgressBar(-1, 8)).toBe('░'.repeat(8)); + expect(renderProgressBar(2, 8)).toBe('█'.repeat(8)); + }); + it('coerces NaN to 0', () => { + expect(renderProgressBar(Number.NaN, 6)).toBe('░'.repeat(6)); + }); +}); + +describe('safeUsageRatio', () => { + it('matches footer context usage clamping semantics', () => { + expect(safeUsageRatio(Number.NaN)).toBe(0); + expect(safeUsageRatio(-1)).toBe(0); + expect(safeUsageRatio(0.427)).toBe(0.427); + expect(safeUsageRatio(1.5)).toBe(1); + }); +}); + +describe('ratioSeverity', () => { + it('green below 0.5', () => { + expect(ratioSeverity(0)).toBe('ok'); + expect(ratioSeverity(0.49)).toBe('ok'); + }); + it('yellow in [0.5, 0.85)', () => { + expect(ratioSeverity(0.5)).toBe('warn'); + expect(ratioSeverity(0.7)).toBe('warn'); + expect(ratioSeverity(0.849)).toBe('warn'); + }); + it('red at or above 0.85', () => { + expect(ratioSeverity(0.85)).toBe('danger'); + expect(ratioSeverity(1)).toBe('danger'); + }); +}); diff --git a/apps/kimi-code/tsconfig.json b/apps/kimi-code/tsconfig.json new file mode 100644 index 000000000..ea6828176 --- /dev/null +++ b/apps/kimi-code/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "allowJs": true, + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["src", "test", "../../packages/agent-core/src/prompt-modules.d.ts"] +} diff --git a/apps/kimi-code/tsdown.config.ts b/apps/kimi-code/tsdown.config.ts new file mode 100644 index 000000000..93b0659d0 --- /dev/null +++ b/apps/kimi-code/tsdown.config.ts @@ -0,0 +1,31 @@ +import { resolve } from 'node:path'; + +import { defineConfig } from 'tsdown'; + +import { rawTextPlugin } from '../../build/raw-text-plugin.mjs'; + +const appRoot = import.meta.dirname; + +export default defineConfig({ + entry: ['./src/main.ts'], + format: ['esm'], + outDir: 'dist', + clean: true, + banner: { + js: [ + '#!/usr/bin/env node', + "import { fileURLToPath as __cjsShimFileURLToPath } from 'node:url';", + "import { dirname as __cjsShimDirname } from 'node:path';", + 'const __filename = __cjsShimFileURLToPath(import.meta.url);', + 'const __dirname = __cjsShimDirname(__filename);', + ].join('\n'), + }, + plugins: [rawTextPlugin()], + alias: { + '@': resolve(appRoot, 'src'), + }, + deps: { + alwaysBundle: [/^@moonshot-ai\//], + neverBundle: [], + }, +}); diff --git a/apps/kimi-code/tsdown.native.config.ts b/apps/kimi-code/tsdown.native.config.ts new file mode 100644 index 000000000..eec94d3ba --- /dev/null +++ b/apps/kimi-code/tsdown.native.config.ts @@ -0,0 +1,64 @@ +import { readFileSync } from 'node:fs'; +import { builtinModules } from 'node:module'; +import { resolve } from 'node:path'; + +import { defineConfig } from 'tsdown'; + +import { rawTextPlugin } from '../../build/raw-text-plugin.mjs'; + +const appRoot = import.meta.dirname; +const packageJson = JSON.parse( + readFileSync(new URL('./package.json', import.meta.url), 'utf-8'), +) as { version: string }; + +const builtins = new Set([ + ...builtinModules, + ...builtinModules.map((name) => `node:${name}`), +]); +const optionalNativeDependencies = new Set(['cpu-features']); + +function shouldAlwaysBundle(id: string): boolean { + if (builtins.has(id) || id.startsWith('node:')) return false; + if (optionalNativeDependencies.has(id)) return false; + return true; +} + +function buildTarget(): string { + return process.env['KIMI_CODE_BUILD_TARGET'] ?? `${process.platform}-${process.arch}`; +} + +export default defineConfig({ + entry: ['./src/main.ts'], + format: ['cjs'], + outDir: 'dist-native/intermediates', + clean: true, + dts: false, + fixedExtension: true, + hash: false, + platform: 'node', + target: 'node24', + banner: { js: '#!/usr/bin/env node' }, + plugins: [rawTextPlugin()], + alias: { + '@': resolve(appRoot, 'src'), + }, + define: { + __KIMI_CODE_VERSION__: JSON.stringify(packageJson.version), + __KIMI_CODE_CHANNEL__: JSON.stringify(process.env['KIMI_CODE_CHANNEL'] ?? ''), + __KIMI_CODE_COMMIT__: JSON.stringify(process.env['KIMI_CODE_COMMIT'] ?? ''), + __KIMI_CODE_BUILD_TARGET__: JSON.stringify(buildTarget()), + __KIMI_CODE_NATIVE_BUNDLE__: 'true', + }, + deps: { + alwaysBundle: shouldAlwaysBundle, + neverBundle: [...optionalNativeDependencies], + onlyBundle: false, + }, + outputOptions: { + codeSplitting: false, + entryFileNames: 'main.cjs', + }, + checks: { + legacyCjs: false, + }, +}); diff --git a/apps/kimi-code/vitest.config.ts b/apps/kimi-code/vitest.config.ts new file mode 100644 index 000000000..49cedd3dd --- /dev/null +++ b/apps/kimi-code/vitest.config.ts @@ -0,0 +1,20 @@ +import { resolve } from 'node:path'; + +import { defineConfig } from 'vitest/config'; + +import { rawTextPlugin } from '../../build/raw-text-plugin.mjs'; + +const appRoot = import.meta.dirname; + +export default defineConfig({ + plugins: [rawTextPlugin()], + resolve: { + alias: { + '@': resolve(appRoot, 'src'), + }, + }, + test: { + name: 'cli', + include: ['test/**/*.test.ts', 'test/**/*.test.tsx'], + }, +}); diff --git a/apps/vis/package.json b/apps/vis/package.json new file mode 100644 index 000000000..837569fcb --- /dev/null +++ b/apps/vis/package.json @@ -0,0 +1,19 @@ +{ + "name": "@moonshot-ai/vis", + "version": "0.1.1", + "private": true, + "description": "Debug visualization tool for kimi-code sessions", + "license": "MIT", + "type": "module", + "scripts": { + "build:deps": "pnpm --filter @moonshot-ai/agent-core... build", + "predev": "pnpm run build:deps", + "dev": "concurrently -k -n server,web -c cyan,magenta \"pnpm --filter @moonshot-ai/vis-server dev\" \"pnpm --filter @moonshot-ai/vis-web dev\"", + "build": "pnpm run build:deps && pnpm --filter @moonshot-ai/vis-server build && pnpm --filter @moonshot-ai/vis-web build && node scripts/copy-web-dist.mjs", + "prestart": "pnpm run build", + "start": "node server/dist/server.mjs" + }, + "devDependencies": { + "concurrently": "^9.1.2" + } +} diff --git a/apps/vis/scripts/copy-web-dist.mjs b/apps/vis/scripts/copy-web-dist.mjs new file mode 100644 index 000000000..18978247f --- /dev/null +++ b/apps/vis/scripts/copy-web-dist.mjs @@ -0,0 +1,26 @@ +#!/usr/bin/env node +// Copy web/dist/** into server/dist/public/ so the bundled server can serve +// the SPA out of a single deploy artifact. + +import { cp, mkdir, rm, stat } from 'node:fs/promises'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const here = dirname(fileURLToPath(import.meta.url)); +const root = join(here, '..'); +const src = join(root, 'web', 'dist'); +const dst = join(root, 'server', 'dist', 'public'); + +try { + await stat(src); +} catch { + process.stderr.write(`[copy-web-dist] source missing: ${src}\n`); + process.stderr.write('Did you run `pnpm --filter @moonshot-ai/vis-web build` first?\n'); + process.exit(1); +} + +await rm(dst, { recursive: true, force: true }); +await mkdir(dirname(dst), { recursive: true }); +await cp(src, dst, { recursive: true }); + +process.stdout.write(`[copy-web-dist] copied ${src} -> ${dst}\n`); diff --git a/apps/vis/server/package.json b/apps/vis/server/package.json new file mode 100644 index 000000000..65232652c --- /dev/null +++ b/apps/vis/server/package.json @@ -0,0 +1,27 @@ +{ + "name": "@moonshot-ai/vis-server", + "version": "0.1.1", + "private": true, + "license": "MIT", + "type": "module", + "imports": { + "#/*": [ + "./src/*.ts", + "./src/*/index.ts" + ] + }, + "scripts": { + "dev": "tsx watch src/index.ts", + "build": "tsdown", + "test": "vitest run" + }, + "dependencies": { + "@hono/node-server": "^1.13.7", + "@moonshot-ai/agent-core": "workspace:^", + "hono": "^4.7.7" + }, + "devDependencies": { + "tsx": "^4.21.0", + "vitest": "4.1.4" + } +} diff --git a/apps/vis/server/src/app.ts b/apps/vis/server/src/app.ts new file mode 100644 index 000000000..41b9abbc5 --- /dev/null +++ b/apps/vis/server/src/app.ts @@ -0,0 +1,138 @@ +import { timingSafeEqual } from 'node:crypto'; +import { readFile, stat } from 'node:fs/promises'; +import { join, resolve } from 'node:path'; + +import { Hono } from 'hono'; + +import { contextRoute } from './routes/context'; +import { sessionDetailRoute } from './routes/session-detail'; +import { sessionsRoute } from './routes/sessions'; +import { subagentsRoute } from './routes/subagents'; +import { toolResultsRoute } from './routes/tool-results'; +import { wireRoute } from './routes/wire'; + +/** Resolve the SPA bundle directory next to the compiled server.mjs, if it + * exists. Returns `null` in dev mode where the web bundle lives elsewhere. */ +async function resolvePublicDir(): Promise<string | null> { + try { + const here = import.meta.dirname; + const candidate = resolve(here, 'public'); + const s = await stat(candidate); + if (s.isDirectory()) return candidate; + } catch { + // not present + } + return null; +} + +const STATIC_EXT_MIME: Record<string, string> = { + '.html': 'text/html; charset=utf-8', + '.js': 'application/javascript; charset=utf-8', + '.mjs': 'application/javascript; charset=utf-8', + '.css': 'text/css; charset=utf-8', + '.json': 'application/json; charset=utf-8', + '.svg': 'image/svg+xml', + '.png': 'image/png', + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.gif': 'image/gif', + '.ico': 'image/x-icon', + '.woff': 'font/woff', + '.woff2': 'font/woff2', + '.ttf': 'font/ttf', + '.map': 'application/json; charset=utf-8', +}; + +function mimeFor(path: string): string { + const i = path.lastIndexOf('.'); + if (i < 0) return 'application/octet-stream'; + const ext = path.slice(i).toLowerCase(); + return STATIC_EXT_MIME[ext] ?? 'application/octet-stream'; +} + +export interface CreateAppOptions { + readonly authToken?: string; +} + +function bearerToken(value: string | undefined): string | null { + if (value === undefined) return null; + const match = /^Bearer\s+(.+)$/i.exec(value); + return match?.[1]?.trim() ?? null; +} + +function tokenMatches(actual: string, expected: string): boolean { + const actualBytes = Buffer.from(actual); + const expectedBytes = Buffer.from(expected); + return actualBytes.length === expectedBytes.length && timingSafeEqual(actualBytes, expectedBytes); +} + +/** Build a Hono app mounting /api/* routes, plus SPA static fallback. */ +export async function createApp(options: CreateAppOptions = {}): Promise<Hono> { + const app = new Hono(); + + // /api/* handlers. + const api = new Hono(); + const authToken = options.authToken; + if (authToken !== undefined && authToken.length > 0) { + api.use('*', async (c, next) => { + const token = bearerToken(c.req.header('authorization')); + if (token !== null && tokenMatches(token, authToken)) { + await next(); + return; + } + c.header('www-authenticate', 'Bearer realm="kimi-vis"'); + return c.json({ error: 'unauthorized', code: 'UNAUTHORIZED' }, 401); + }); + } + api.route('/sessions', sessionsRoute()); + api.route('/sessions', sessionDetailRoute()); + api.route('/sessions', wireRoute()); + api.route('/sessions', contextRoute()); + api.route('/sessions', subagentsRoute()); + api.route('/sessions', toolResultsRoute()); + + app.route('/api', api); + + // Static + SPA fallback (production only). + const publicDir = await resolvePublicDir(); + if (publicDir !== null) { + app.get('*', async (c) => { + const url = new URL(c.req.url); + let pathname = decodeURIComponent(url.pathname); + if (pathname.startsWith('/api')) { + // Should have been routed above; 404 here. + return c.json({ error: `api route not found: ${pathname}`, code: 'NOT_FOUND' }, 404); + } + if (pathname === '/' || pathname === '') pathname = '/index.html'; + const resolved = resolve(publicDir, `.${pathname}`); + if (!resolved.startsWith(publicDir)) { + return c.text('forbidden', 403); + } + try { + const s = await stat(resolved); + if (s.isFile()) { + const buf = await readFile(resolved); + const body = new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength); + return new Response(body, { + headers: { 'content-type': mimeFor(resolved) }, + }); + } + } catch { + // fall through to SPA fallback + } + // SPA fallback — index.html for any unknown GET so client-side + // React Router can resolve the route. + try { + const indexHtml = await readFile(join(publicDir, 'index.html')); + const body = new Uint8Array(indexHtml.buffer, indexHtml.byteOffset, indexHtml.byteLength); + return new Response(body, { + headers: { 'content-type': 'text/html; charset=utf-8' }, + }); + } catch { + return c.text('not found', 404); + } + }); + } + + return app; +} diff --git a/apps/vis/server/src/config.ts b/apps/vis/server/src/config.ts new file mode 100644 index 000000000..7c9421c14 --- /dev/null +++ b/apps/vis/server/src/config.ts @@ -0,0 +1,156 @@ +import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'; +import { homedir } from 'node:os'; +import { join } from 'node:path'; + +/** Resolve KIMI_CODE_HOME (env > ~/.kimi-code). */ +function resolveKimiCodeHome(): string { + const envHome = process.env['KIMI_CODE_HOME']; + if (envHome !== undefined && envHome.length > 0) { + return envHome; + } + return join(homedir(), '.kimi-code'); +} + +/** HTTP port for the vis API server. */ +export function resolvePort(): number { + const raw = process.env['PORT']; + if (raw !== undefined && raw.length > 0) { + const n = Number.parseInt(raw, 10); + if (Number.isFinite(n) && n > 0 && n < 65536) { + return n; + } + } + return 3001; +} + +/** HTTP host for the vis API server. Defaults to loopback. */ +export function resolveHost(): string { + const raw = process.env['VIS_HOST'] ?? process.env['HOST']; + const host = raw?.trim(); + return host !== undefined && host.length > 0 ? host : '127.0.0.1'; +} + +export function isLoopbackHost(host: string): boolean { + const normalized = host.trim().toLowerCase().replaceAll('[', '').replaceAll(']', ''); + return ( + normalized === 'localhost' || + normalized === '::1' || + normalized === '0:0:0:0:0:0:0:1' || + normalized.startsWith('127.') + ); +} + +export function resolveVisAuthToken(host: string = resolveHost()): string | undefined { + const raw = process.env['VIS_AUTH_TOKEN']; + const token = raw?.trim(); + if (token !== undefined && token.length > 0) return token; + if (!isLoopbackHost(host)) { + throw new Error( + `VIS_AUTH_TOKEN is required when binding vis-server outside loopback (host=${host})`, + ); + } + return undefined; +} + +export const KIMI_CODE_HOME: string = resolveKimiCodeHome(); + +export class VisPathConfig { + readonly sessionsDir: string; + + constructor(readonly home: string) { + this.sessionsDir = join(home, 'sessions'); + } + + sessionDir(sessionId: string): string { + return ( + this.findIndexedSessionDir(sessionId) ?? + this.findNestedSessionDir(sessionId) ?? + join(this.sessionsDir, sessionId) + ); + } + + statePath(sessionId: string): string { + return join(this.sessionDir(sessionId), 'state.json'); + } + + mainAgentDir(sessionId: string): string { + const sessionDir = this.sessionDir(sessionId); + const mainDir = join(sessionDir, 'agents', 'main'); + return isDirectory(mainDir) ? mainDir : sessionDir; + } + + wirePath(sessionId: string): string { + return join(this.mainAgentDir(sessionId), 'wire.jsonl'); + } + + subagentDir(sessionId: string, agentId: string): string { + const sessionDir = this.sessionDir(sessionId); + const agentDir = join(sessionDir, 'agents', agentId); + return isDirectory(agentDir) ? agentDir : join(sessionDir, 'subagents', agentId); + } + + toolResultArchivePath(sessionId: string, toolCallId: string): string { + const sessionDir = this.sessionDir(sessionId); + const mainPath = join(sessionDir, 'agents', 'main', 'tool-results', `${toolCallId}.txt`); + return existsSync(mainPath) ? mainPath : join(sessionDir, 'tool-results', `${toolCallId}.txt`); + } + + private findIndexedSessionDir(sessionId: string): string | null { + const indexPath = join(this.home, 'session_index.jsonl'); + let raw: string; + try { + raw = readFileSync(indexPath, 'utf8'); + } catch { + return null; + } + for (const line of raw.split(/\r?\n/)) { + if (line.trim().length === 0) continue; + try { + const parsed = JSON.parse(line) as { + sessionId?: unknown; + session_id?: unknown; + sessionDir?: unknown; + session_dir?: unknown; + }; + const id = stringValue(parsed.sessionId) ?? stringValue(parsed.session_id); + if (id !== sessionId) continue; + const dir = stringValue(parsed.sessionDir) ?? stringValue(parsed.session_dir); + if (dir !== null && isDirectory(dir)) return dir; + } catch { + // Ignore malformed index lines. + } + } + return null; + } + + private findNestedSessionDir(sessionId: string): string | null { + const direct = join(this.sessionsDir, sessionId); + if (isDirectory(direct)) return direct; + let entries: string[]; + try { + entries = readdirSync(this.sessionsDir); + } catch { + return null; + } + for (const entry of entries) { + const candidate = join(this.sessionsDir, entry, sessionId); + if (isDirectory(candidate)) return candidate; + } + return null; + } +} + +function stringValue(value: unknown): string | null { + return typeof value === 'string' && value.length > 0 ? value : null; +} + +function isDirectory(path: string): boolean { + try { + return statSync(path).isDirectory(); + } catch { + return false; + } +} + +/** Singleton path config pinned to the resolved KIMI_CODE_HOME. */ +export const pathConfig = new VisPathConfig(KIMI_CODE_HOME); diff --git a/apps/vis/server/src/index.ts b/apps/vis/server/src/index.ts new file mode 100644 index 000000000..e97d6603a --- /dev/null +++ b/apps/vis/server/src/index.ts @@ -0,0 +1,32 @@ +import { serve } from '@hono/node-server'; + +import { createApp } from './app'; +import { KIMI_CODE_HOME, resolveHost, resolvePort, resolveVisAuthToken } from './config'; +import { formatStartupBanner } from './startup-banner'; + +async function main(): Promise<void> { + const host = resolveHost(); + const authToken = resolveVisAuthToken(host); + const app = await createApp({ authToken }); + const port = resolvePort(); + serve({ fetch: app.fetch, hostname: host, port }, (info) => { + // Startup banner. + process.stdout.write( + formatStartupBanner({ + authToken, + host, + kimiCodeHome: KIMI_CODE_HOME, + port: info.port, + }), + ); + }); +} + +try { + await main(); +} catch (error: unknown) { + process.stderr.write( + `[vis-server] fatal: ${error instanceof Error ? (error.stack ?? error.message) : String(error)}\n`, + ); + process.exit(1); +} diff --git a/apps/vis/server/src/lib/context-builder.ts b/apps/vis/server/src/lib/context-builder.ts new file mode 100644 index 000000000..b31df2023 --- /dev/null +++ b/apps/vis/server/src/lib/context-builder.ts @@ -0,0 +1,500 @@ +import type { + AnnotatedMessage, + ContentPart, + NotificationRecord, + ProjectedStateSummary, + SessionInitializedRecord, + SessionState, + ToolCallEntry, + UserInputPart, + VisWireRecord, +} from './types'; + +const PERSISTED_OUTPUT_RE = /^<persisted-output path="([^"]+)">/; + +/** + * Extract the on-disk path from a `<persisted-output path="...">` marker + * at the start of a tool-result output. Returns null when no marker is + * present. + */ +export function extractPersistedOutputPath(text: string): string | null { + const m = PERSISTED_OUTPUT_RE.exec(text.trimStart()); + return m?.[1] ?? null; +} + +/** + * Render a notification's `data` payload as the XML string that the live + * ContextMemory would inject as a user-role message. + */ +export function renderNotificationXml(data: NotificationRecord['data']): string { + const id = escAttr(data.id); + const category = escAttr(data.category); + const type = escAttr(data.type); + const sourceKind = escAttr(data.source_kind); + const sourceId = escAttr(data.source_id); + const title = data.title; + const severity = data.severity; + const body = data.body; + + const lines: string[] = [ + `<notification id="${id}" category="${category}" type="${type}" source_kind="${sourceKind}" source_id="${sourceId}">`, + ]; + if (title.length > 0) lines.push(`Title: ${title}`); + if (severity.length > 0) lines.push(`Severity: ${severity}`); + if (body.length > 0) lines.push(body); + + // background_task notifications append a <task-notification> block + // with the last 20 lines / 3000 chars of tail_output, matching what + // the live projector would emit. + if (data.source_kind === 'background_task') { + const tail = (data as { tail_output?: string }).tail_output ?? ''; + if (tail.length > 0) { + const tailLines = tail.split('\n'); + const trimmed = (tailLines.length > 20 ? tailLines.slice(-20) : tailLines) + .join('\n') + .slice(-3000); + lines.push('<task-notification>', trimmed, '</task-notification>'); + } + } + + lines.push('</notification>'); + return lines.join('\n'); +} + +function escAttr(s: string): string { + if (s.length === 0) return 'unknown'; + return s.replaceAll('&', '&').replaceAll('"', '"'); +} + +export interface BuildAnnotatedOptions { + /** When true (default), rewind-orphaned records are kept with `out_of_context: true`. */ + preserveOutOfContext?: boolean; +} + +/** + * Walk `records` in order and emit an `AnnotatedMessage[]` that includes + * notification and system_reminder synthetic user messages. Tracks a + * numeric turn counter so `context_edit operation:'rewind' to_turn:N` + * can flag later messages as out-of-context. + */ +export function buildAnnotatedMessages( + records: readonly VisWireRecord[], + options?: BuildAnnotatedOptions, +): AnnotatedMessage[] { + const preserveOutOfContext = options?.preserveOutOfContext ?? true; + + // Track (messageIndex -> turnCounterAtEmission) so a rewind can mark + // later emissions as out_of_context. + let annotated: AnnotatedMessage[] = []; + const msgTurnIdx: number[] = []; + + let turnCounter = 0; + + // An assistant message is not a single record — it's coalesced from + // `step_begin → (content_part|tool_call)* → step_end` atoms anchored + // by `step_uuid`. We buffer the in-flight step here and emit a + // synthetic assistant message on step_end. + interface StepBuffer { + seq: number; + step_uuid: string; + text: string; + think: string; + think_encrypted?: string | undefined; + tool_calls: ToolCallEntry[]; + } + let currentStep: StepBuffer | null = null; + + const flushStep = (): void => { + if (currentStep === null) return; + const s = currentStep; + const content: ContentPart[] = []; + if (s.think.length > 0) { + const part: ContentPart = { type: 'think', think: s.think }; + if (s.think_encrypted !== undefined) part['encrypted'] = s.think_encrypted; + content.push(part); + } + if (s.text.length > 0) content.push({ type: 'text', text: s.text }); + if (content.length > 0 || s.tool_calls.length > 0) { + pushMsg({ + seq: s.seq, + message: { role: 'assistant', content, tool_calls: s.tool_calls }, + origin: { kind: 'assistant' }, + is_ephemeral: false, + out_of_context: false, + }); + } + currentStep = null; + }; + + const pushMsg = (m: AnnotatedMessage): void => { + annotated.push(m); + msgTurnIdx.push(turnCounter); + }; + + for (const r of records) { + switch (r.type) { + case 'turn_begin': { + // Close any dangling step so turn boundaries stay clean. + flushStep(); + turnCounter += 1; + break; + } + + case 'user_message': { + const text = userContentToText(r.content); + pushMsg({ + seq: r.seq, + message: { + role: 'user', + content: [{ type: 'text', text }], + tool_calls: [], + }, + origin: { kind: 'user' }, + is_ephemeral: false, + out_of_context: false, + }); + break; + } + + case 'step_begin': { + // Any prior unclosed step gets flushed; protocol guarantees order + // step_begin → … → step_end, but crashes can leave orphans. + flushStep(); + currentStep = { + seq: r.seq, + step_uuid: r.uuid, + text: '', + think: '', + tool_calls: [], + }; + break; + } + + case 'content_part': { + if (currentStep === null) break; + if (currentStep.step_uuid !== r.step_uuid) break; + if (r.part.kind === 'text') { + currentStep.text += r.part.text; + } else { + currentStep.think += r.part.think; + if (r.part.encrypted !== undefined) { + currentStep.think_encrypted = r.part.encrypted; + } + } + break; + } + + case 'tool_call': { + if (currentStep === null) break; + if (currentStep.step_uuid !== r.step_uuid) break; + currentStep.tool_calls.push({ + type: 'function', + id: r.data.tool_call_id, + function: { + name: r.data.tool_name, + arguments: r.data.args === undefined ? null : JSON.stringify(r.data.args), + }, + }); + break; + } + + case 'step_end': { + // step_end may carry usage; we drop it here since the annotated + // message already represents what the LLM emitted. Usage lives + // on the wire record itself for the Wire tab to show. + flushStep(); + break; + } + + case 'tool_result': { + const text = typeof r.output === 'string' ? r.output : JSON.stringify(r.output); + const msg: AnnotatedMessage = { + seq: r.seq, + message: { + role: 'tool', + content: [{ type: 'text', text }], + tool_calls: [], + tool_call_id: r.tool_call_id, + }, + origin: { kind: 'tool', tool_call_id: r.tool_call_id }, + is_ephemeral: false, + out_of_context: false, + }; + if (typeof r.output === 'string') { + const persistedPath = extractPersistedOutputPath(r.output); + if (persistedPath !== null) { + msg.persisted_output_path = persistedPath; + } + } + pushMsg(msg); + break; + } + + case 'system_reminder': { + const text = `<system-reminder>\n${r.content}\n</system-reminder>`; + pushMsg({ + seq: r.seq, + message: { role: 'user', content: [{ type: 'text', text }], tool_calls: [] }, + origin: { kind: 'system_reminder', seq: r.seq }, + is_ephemeral: true, + out_of_context: false, + }); + break; + } + + case 'notification': { + if (!isLlmVisibleNotification(r)) break; + const text = renderNotificationXml(r.data); + pushMsg({ + seq: r.seq, + message: { role: 'user', content: [{ type: 'text', text }], tool_calls: [] }, + origin: { + kind: 'notification', + seq: r.seq, + notification_id: r.data.id, + severity: r.data.severity, + }, + is_ephemeral: true, + out_of_context: false, + }); + break; + } + + case 'compaction': { + // Wholesale replacement. Drops any open step buffer — atoms + // before compaction are superseded by the summary. + currentStep = null; + annotated = []; + msgTurnIdx.length = 0; + pushMsg({ + seq: r.seq, + message: { + role: 'assistant', + content: [{ type: 'text', text: r.summary }], + tool_calls: [], + }, + origin: { kind: 'assistant' }, + is_ephemeral: false, + out_of_context: false, + }); + break; + } + + case 'context_cleared': { + currentStep = null; + annotated = []; + msgTurnIdx.length = 0; + break; + } + + case 'context_edit': { + if (r.operation === 'rewind' && typeof r.to_turn === 'number') { + const toTurn = r.to_turn; + if (preserveOutOfContext) { + for (let i = 0; i < annotated.length; i += 1) { + const emittedAtTurn = msgTurnIdx[i] ?? 0; + if (emittedAtTurn > toTurn) { + // Replace with a marked copy (immutability not required but + // makes the "after rewind" behaviour obvious to readers). + const m = annotated[i]; + if (m !== undefined) { + annotated[i] = { ...m, out_of_context: true }; + } + } + } + } else { + // Drop messages emitted in turns past `to_turn`. + const keep: AnnotatedMessage[] = []; + const keepTurns: number[] = []; + for (let i = 0; i < annotated.length; i += 1) { + const emittedAtTurn = msgTurnIdx[i] ?? 0; + if (emittedAtTurn <= toTurn) { + const m = annotated[i]; + if (m !== undefined) { + keep.push(m); + keepTurns.push(emittedAtTurn); + } + } + } + annotated = keep; + msgTurnIdx.length = 0; + msgTurnIdx.push(...keepTurns); + } + } + break; + } + + case 'approval_request': + case 'approval_response': + case 'metadata': + case 'ownership_changed': + case 'session_initialized': + case 'skill_completed': + case 'skill_invoked': + case 'subagent_completed': + case 'subagent_failed': + case 'subagent_spawned': + case 'system_prompt_changed': + case 'team_mail': + case 'tool_denied': + case 'tools_changed': + case 'turn_end': + break; + + default: + break; + } + } + + // Flush any trailing open step (session ended mid-step). + flushStep(); + + return annotated; +} + +function userContentToText(content: string | readonly UserInputPart[]): string { + if (typeof content === 'string') return content; + return content + .map((p) => { + if (p.type === 'text') return p.text; + if (p.type === 'image_url') return `<image url="${p.image_url.url}">`; + return `<video url="${p.video_url.url}">`; + }) + .join(''); +} + +function isLlmVisibleNotification(r: NotificationRecord): boolean { + const targets = r.data.targets; + if (!targets.includes('llm')) return false; + // Optimistic: an absent `delivered_at` means a legacy record from before + // delivery tracking was added (delivery actually happened); a value of + // `0` means the LLM sink was intentionally skipped. + const deliveredAt = r.data.delivered_at; + if (deliveredAt === undefined) return true; + return deliveredAt.llm !== 0; +} + +/** + * Build the projected-state summary by walking the wire records locally. + * Only the config-class state is materialized (model, system prompt, + * active tools, permission/plan mode, last seq, token totals). + */ +export function buildProjectedStateSummary( + records: readonly VisWireRecord[], + sessionInitialized: SessionInitializedRecord | null, + state?: SessionState | null, +): ProjectedStateSummary { + const baseline = + sessionInitialized ?? + ({ + type: 'session_initialized', + seq: 0, + time: 0, + agent_type: 'main', + session_id: state?.session_id ?? 'unknown', + system_prompt: '', + active_tools: [], + model: state?.model, + permission_mode: state?.permission_mode, + plan_mode: state?.plan_mode, + workspace_dir: state?.workspace_dir, + } satisfies SessionInitializedRecord); + const breakdown = sumTokenBreakdown(records); + const projected = projectStateLocally(records, baseline, state); + return { + model: projected.model, + system_prompt: projected.system_prompt, + active_tools: projected.active_tools, + last_seq: projected.last_seq, + token_count: breakdown.input_tokens + breakdown.output_tokens, + permission_mode: projected.permission_mode, + plan_mode: projected.plan_mode, + ...breakdown, + }; +} + +function projectStateLocally( + records: readonly VisWireRecord[], + sessionInitialized: SessionInitializedRecord, + state?: SessionState | null, +): Pick< + ProjectedStateSummary, + 'model' | 'system_prompt' | 'active_tools' | 'last_seq' | 'permission_mode' | 'plan_mode' +> { + let systemPrompt: string | null = sessionInitialized.system_prompt; + let activeTools = [...sessionInitialized.active_tools]; + let model: string | null = state?.model ?? sessionInitialized.model ?? null; + let permissionMode: string | null = sessionInitialized.permission_mode ?? null; + let planMode = sessionInitialized.plan_mode ?? false; + let lastSeq = sessionInitialized.seq; + for (const record of records) { + lastSeq = Math.max(lastSeq, record.seq ?? 0); + if (record.type === 'system_prompt_changed') { + systemPrompt = record.new_prompt; + } else if (record.type === 'tools_changed') { + activeTools = [...record.tools]; + } else if (record.type === 'notification') { + const payload = record.data.payload; + if (record.data.type === 'config.update' && isRecord(payload)) { + model = configModel(payload) ?? model; + } else if (record.data.type === 'permission.update' && isRecord(payload)) { + permissionMode = stringValue(payload['mode']) ?? permissionMode; + } + } + } + return { + model, + system_prompt: systemPrompt, + active_tools: activeTools, + last_seq: lastSeq, + permission_mode: state?.permission_mode ?? permissionMode, + plan_mode: state?.plan_mode ?? planMode, + }; +} + +function configModel(payload: Record<string, unknown>): string | null { + const alias = stringValue(payload['modelAlias']); + if (alias !== null) return alias; + const provider = payload['provider']; + if (isRecord(provider)) return stringValue(provider['model']); + return null; +} + +function isRecord(value: unknown): value is Record<string, unknown> { + return typeof value === 'object' && value !== null; +} + +function stringValue(value: unknown): string | null { + return typeof value === 'string' && value.length > 0 ? value : null; +} + +/** Sum `step_end.usage` across all records. Mirrors the core projector's + * tokenCount logic (step-level) so partial/running turns are still counted + * — turn_end.usage is the turn total but it arrives only after the turn + * closes, so step_end sums stay responsive on live sessions. */ +function sumTokenBreakdown(records: readonly VisWireRecord[]): { + input_tokens: number; + output_tokens: number; + cache_read_tokens: number; + cache_write_tokens: number; +} { + let input = 0; + let output = 0; + let cacheRead = 0; + let cacheWrite = 0; + for (const r of records) { + if (r.type !== 'step_end') continue; + const usage = (r as { usage?: Record<string, number | undefined> }).usage; + if (usage === undefined) continue; + input += usage['input_tokens'] ?? 0; + output += usage['output_tokens'] ?? 0; + cacheRead += usage['cache_read_tokens'] ?? 0; + cacheWrite += usage['cache_write_tokens'] ?? 0; + } + return { + input_tokens: input, + output_tokens: output, + cache_read_tokens: cacheRead, + cache_write_tokens: cacheWrite, + }; +} diff --git a/apps/vis/server/src/lib/session-delete.ts b/apps/vis/server/src/lib/session-delete.ts new file mode 100644 index 000000000..c6e6010c6 --- /dev/null +++ b/apps/vis/server/src/lib/session-delete.ts @@ -0,0 +1,50 @@ +import { rm, stat } from 'node:fs/promises'; +import { basename, isAbsolute, relative, resolve } from 'node:path'; + +import { pathConfig } from '../config'; +import { listSessions } from './session-lister'; +import type { ClearSessionsResponse, DeleteSessionResponse } from './types'; + +export const SESSION_ID_RE = /^session_[a-zA-Z0-9_-]+$/; + +function isInside(root: string, target: string): boolean { + const rel = relative(root, target); + return rel === '' || (!rel.startsWith('..') && !isAbsolute(rel)); +} + +function resolveSafeSessionDir(sessionId: string): string { + const sessionsRoot = resolve(pathConfig.sessionsDir); + const sessionDir = resolve(pathConfig.sessionDir(sessionId)); + if (!isInside(sessionsRoot, sessionDir) || basename(sessionDir) !== sessionId) { + throw new Error(`refusing to delete unsafe session path: ${sessionDir}`); + } + return sessionDir; +} + +export async function deleteSession(sessionId: string): Promise<DeleteSessionResponse | null> { + const sessionDir = resolveSafeSessionDir(sessionId); + const info = await stat(sessionDir).catch(() => null); + if (info?.isDirectory() !== true) return null; + await rm(sessionDir, { recursive: true, force: true }); + return { session_id: sessionId, deleted: true }; +} + +export async function clearSessions(): Promise<ClearSessionsResponse> { + const sessions = await listSessions(pathConfig.sessionsDir); + const failed: ClearSessionsResponse['failed'] = []; + let deleted_count = 0; + + for (const session of sessions) { + try { + const result = await deleteSession(session.session_id); + if (result?.deleted === true) deleted_count += 1; + } catch (error) { + failed.push({ + session_id: session.session_id, + error: error instanceof Error ? error.message : String(error), + }); + } + } + + return { deleted_count, failed }; +} diff --git a/apps/vis/server/src/lib/session-lister.ts b/apps/vis/server/src/lib/session-lister.ts new file mode 100644 index 000000000..c07ec2e96 --- /dev/null +++ b/apps/vis/server/src/lib/session-lister.ts @@ -0,0 +1,196 @@ +import { createReadStream } from 'node:fs'; +import { readFile, readdir, stat } from 'node:fs/promises'; +import { join } from 'node:path'; +import { createInterface } from 'node:readline'; + +import type { SessionState, SessionSummary, WireFileMetadata } from './types'; +import { readSessionLastPrompt, readSessionTitle } from './session-title'; + +const ARCHIVE_RE = /^wire\.\d+\.jsonl$/; +const SESSION_ID_RE = /^session_[a-zA-Z0-9_-]+$/; + +/** Count NDJSON body lines (excluding metadata header) without loading the + * full file into memory. Returns `null` if the file is unreadable. */ +async function countWireBodyLines(wirePath: string): Promise<{ + count: number; + metadata: WireFileMetadata | null; +} | null> { + try { + const stream = createReadStream(wirePath, { encoding: 'utf8' }); + const rl = createInterface({ input: stream, crlfDelay: Infinity }); + + let headerConsumed = false; + let count = 0; + let metadata: WireFileMetadata | null = null; + + for await (const line of rl) { + if (line.length === 0) continue; + if (!headerConsumed) { + headerConsumed = true; + try { + const parsed = JSON.parse(line) as unknown; + if ( + typeof parsed === 'object' && + parsed !== null && + (parsed as { type?: unknown }).type === 'metadata' + ) { + metadata = parsed as WireFileMetadata; + } + } catch { + // first line malformed — session is broken, not missing + } + continue; + } + count += 1; + } + return { count, metadata }; + } catch { + return null; + } +} + +async function readStateJson(statePath: string): Promise<SessionState | null> { + try { + const raw = await readFile(statePath, 'utf8'); + return JSON.parse(raw) as SessionState; + } catch { + return null; + } +} + +async function countSubagents(sessionDir: string): Promise<number> { + const agentCount = await countAgentSubagents(sessionDir); + if (agentCount > 0) return agentCount; + try { + const entries = await readdir(join(sessionDir, 'subagents'), { withFileTypes: true }); + return entries.filter((e) => e.isDirectory()).length; + } catch { + return 0; + } +} + +async function countAgentSubagents(sessionDir: string): Promise<number> { + try { + const entries = await readdir(join(sessionDir, 'agents'), { withFileTypes: true }); + return entries.filter((e) => e.isDirectory() && e.name !== 'main').length; + } catch { + return 0; + } +} + +async function resolveMainAgentDir(sessionDir: string): Promise<string> { + const mainDir = join(sessionDir, 'agents', 'main'); + const info = await stat(mainDir).catch(() => null); + return info?.isDirectory() === true ? mainDir : sessionDir; +} + +async function countArchives(sessionDir: string): Promise<number> { + try { + const entries = await readdir(sessionDir); + return entries.filter((e) => ARCHIVE_RE.test(e)).length; + } catch { + return 0; + } +} + +/** + * Build a `SessionSummary` from a single session directory. Malformed + * state.json / missing wire.jsonl yield health markers rather than errors. + */ +export async function loadSessionSummary(sessionDir: string): Promise<SessionSummary> { + const sessionId = sessionDir.split('/').pop() ?? sessionDir; + const statePath = join(sessionDir, 'state.json'); + const mainAgentDir = await resolveMainAgentDir(sessionDir); + const wirePath = join(mainAgentDir, 'wire.jsonl'); + + const state = await readStateJson(statePath); + const wireStat = await stat(wirePath).catch(() => null); + const wireInfo = wireStat === null ? null : await countWireBodyLines(wirePath); + const subagent_count = await countSubagents(sessionDir); + const archive_count = await countArchives(mainAgentDir); + + let health: SessionSummary['health']; + if (wireStat === null) { + health = 'missing_wire'; + } else if (wireInfo === null) { + health = 'broken'; + } else if (state === null) { + // state.json missing / unreadable — still usable, flag as broken. + health = 'broken'; + } else { + health = 'ok'; + } + + return { + session_id: state?.session_id ?? sessionId, + title: readSessionTitle(state), + last_prompt: readSessionLastPrompt(state), + created_at: state?.created_at ?? 0, + updated_at: state?.updated_at ?? 0, + last_turn_time: state?.last_turn_time ?? null, + model: state?.model ?? null, + permission_mode: state?.permission_mode ?? null, + last_exit_code: state?.last_exit_code ?? null, + custom_title: state?.custom_title ?? null, + tags: state?.tags ?? [], + archived: state?.archived ?? false, + workspace_dir: state?.workspace_dir ?? null, + wire_protocol_version: wireInfo?.metadata?.protocol_version ?? null, + wire_record_count: wireInfo?.count ?? 0, + archive_count, + subagent_count, + health, + }; +} + +/** + * Enumerate sessions under `sessionsDir`. Current dev stores sessions under a + * workdir bucket (`sessions/<workdir-key>/<sessionId>`), while older fixtures + * may still be direct `sessions/<sessionId>` directories. + */ +export async function listSessions(sessionsDir: string): Promise<SessionSummary[]> { + const candidates: string[] = []; + let entries: string[]; + try { + entries = await readdir(sessionsDir); + } catch { + return []; + } + for (const entry of entries) { + const sessionDir = join(sessionsDir, entry); + const s = await stat(sessionDir).catch(() => null); + if (s?.isDirectory() !== true) continue; + if (SESSION_ID_RE.test(entry)) { + candidates.push(sessionDir); + continue; + } + let children: string[]; + try { + children = await readdir(sessionDir); + } catch { + continue; + } + for (const child of children) { + if (!SESSION_ID_RE.test(child)) continue; + const childDir = join(sessionDir, child); + const childStat = await stat(childDir).catch(() => null); + if (childStat?.isDirectory() === true) candidates.push(childDir); + } + } + + // Cheap pre-filter on state.json producer before doing any wire I/O. + const keep = await Promise.all( + candidates.map(async (dir) => { + const state = await readStateJson(join(dir, 'state.json')); + if (state === null) return null; + return state.producer?.kind === 'python' ? null : dir; + }), + ); + + const summaries = await Promise.all( + keep.filter((d): d is string => d !== null).map((dir) => loadSessionSummary(dir)), + ); + + summaries.sort((a, b) => b.updated_at - a.updated_at); + return summaries; +} diff --git a/apps/vis/server/src/lib/session-title.ts b/apps/vis/server/src/lib/session-title.ts new file mode 100644 index 000000000..bd06a903b --- /dev/null +++ b/apps/vis/server/src/lib/session-title.ts @@ -0,0 +1,25 @@ +import type { SessionState } from './types'; + +function titleString(value: unknown): string | null { + if (typeof value !== 'string') return null; + const trimmed = value.trim(); + return trimmed.length > 0 ? trimmed : null; +} + +export function readSessionTitle(state: SessionState | null | undefined): string | null { + if (state === null || state === undefined) return null; + if (typeof state.isCustomTitle === 'boolean') { + const title = titleString(state.title); + if (title !== null) return title; + } + return ( + titleString(state.customTitle) ?? + titleString(state.custom_title) ?? + titleString(state.title) + ); +} + +export function readSessionLastPrompt(state: SessionState | null | undefined): string | null { + if (state === null || state === undefined) return null; + return titleString(state.lastPrompt) ?? titleString(state.last_prompt); +} diff --git a/apps/vis/server/src/lib/subagent-loader.ts b/apps/vis/server/src/lib/subagent-loader.ts new file mode 100644 index 000000000..84b316084 --- /dev/null +++ b/apps/vis/server/src/lib/subagent-loader.ts @@ -0,0 +1,213 @@ +import { readFile, readdir, stat } from 'node:fs/promises'; +import { join } from 'node:path'; + +import type { + SubagentCompletedRecord, + SubagentFailedRecord, + SubagentNode, + SubagentSpawnedRecord, + VisWireRecord, +} from './types'; +import { loadWireRecords } from './wire-loader'; + +interface LifecyclePair { + spawned: SubagentSpawnedRecord; + completed: SubagentCompletedRecord | null; + failed: SubagentFailedRecord | null; +} + +/** Directly list subagent ids for a session (only names, no tree build). */ +export async function listSubagents(sessionDir: string): Promise<string[]> { + try { + const entries = await readdir(join(sessionDir, 'agents')); + const out: string[] = []; + for (const e of entries) { + if (e === 'main') continue; + const s = await stat(join(sessionDir, 'agents', e)).catch(() => null); + if (s?.isDirectory()) out.push(e); + } + if (out.length > 0) return out; + } catch { + // Fall back to legacy subagents/ below. + } + try { + const entries = await readdir(join(sessionDir, 'subagents')); + const out: string[] = []; + for (const e of entries) { + const s = await stat(join(sessionDir, 'subagents', e)).catch(() => null); + if (s?.isDirectory()) out.push(e); + } + return out; + } catch { + return []; + } +} + +function extractLifecycleRecords(records: readonly VisWireRecord[]): Map<string, LifecyclePair> { + const map = new Map<string, LifecyclePair>(); + for (const r of records) { + if (r.type === 'subagent_spawned') { + const existing = map.get(r.data.agent_id); + if (existing === undefined) { + map.set(r.data.agent_id, { spawned: r, completed: null, failed: null }); + } else { + existing.spawned = r; + } + } else if (r.type === 'subagent_completed') { + const existing = map.get(r.data.agent_id); + if (existing === undefined) { + // Rare: completion without spawn in this wire — create skeleton. + const fakeSpawn: SubagentSpawnedRecord = { + type: 'subagent_spawned', + seq: 0, + time: 0, + data: { + agent_id: r.data.agent_id, + parent_tool_call_id: r.data.parent_tool_call_id, + run_in_background: false, + }, + }; + map.set(r.data.agent_id, { spawned: fakeSpawn, completed: r, failed: null }); + } else { + existing.completed = r; + } + } else if (r.type === 'subagent_failed') { + const existing = map.get(r.data.agent_id); + if (existing === undefined) { + const fakeSpawn: SubagentSpawnedRecord = { + type: 'subagent_spawned', + seq: 0, + time: 0, + data: { + agent_id: r.data.agent_id, + parent_tool_call_id: r.data.parent_tool_call_id, + run_in_background: false, + }, + }; + map.set(r.data.agent_id, { spawned: fakeSpawn, completed: null, failed: r }); + } else { + existing.failed = r; + } + } + } + return map; +} + +async function readSubagentMeta( + sessionDir: string, + agentId: string, +): Promise<{ subagent_type: string | null; status: string | null }> { + try { + const raw = await readFile(join(await resolveSubagentDir(sessionDir, agentId), 'meta.json'), 'utf8'); + const parsed = JSON.parse(raw) as { subagent_type?: unknown; status?: unknown }; + return { + subagent_type: typeof parsed.subagent_type === 'string' ? parsed.subagent_type : null, + status: typeof parsed.status === 'string' ? parsed.status : null, + }; + } catch { + return { subagent_type: null, status: null }; + } +} + +async function resolveSubagentDir(sessionDir: string, agentId: string): Promise<string> { + const agentDir = join(sessionDir, 'agents', agentId); + const agentStat = await stat(agentDir).catch(() => null); + return agentStat?.isDirectory() === true ? agentDir : join(sessionDir, 'subagents', agentId); +} + +function deriveStatus(pair: LifecyclePair, metaStatus: string | null): SubagentNode['status'] { + if (pair.failed !== null) return 'failed'; + if (pair.completed !== null) return 'completed'; + if (metaStatus !== null) { + const allowed: ReadonlyArray<SubagentNode['status']> = [ + 'running', + 'completed', + 'failed', + 'killed', + 'lost', + ]; + if ((allowed as string[]).includes(metaStatus)) { + return metaStatus as SubagentNode['status']; + } + } + return 'unknown'; +} + +/** + * Build the subagent tree for a session, recursing into each subagent's + * wire.jsonl to discover nested spawns. Depth-capped at `maxDepth` + * (default 5) to prevent runaway recursion on corrupt fixtures. + */ +export async function buildSubagentTree( + sessionDir: string, + mainWireRecords: readonly VisWireRecord[], + maxDepth = 5, +): Promise<SubagentNode[]> { + const visited = new Set<string>(); + + async function buildLevel( + records: readonly VisWireRecord[], + depth: number, + parentAgentId: string | null, + ): Promise<SubagentNode[]> { + if (depth > maxDepth) return []; + const pairs = extractLifecycleRecords(records); + + // Reserve all agentIds at this level synchronously so parallel recursion + // cannot re-visit a cousin branch's already-claimed agent. + const toProcess: Array<[string, LifecyclePair]> = []; + for (const [agentId, pair] of pairs) { + if (visited.has(agentId)) continue; + visited.add(agentId); + toProcess.push([agentId, pair]); + } + + const nodes = await Promise.all( + toProcess.map(async ([agentId, pair]): Promise<SubagentNode> => { + const meta = await readSubagentMeta(sessionDir, agentId); + let children: SubagentNode[] = []; + const subDir = await resolveSubagentDir(sessionDir, agentId); + const dirStat = await stat(subDir).catch(() => null); + if (dirStat?.isDirectory() && depth < maxDepth) { + try { + const sub = await loadWireRecords(subDir); + children = await buildLevel(sub.records, depth + 1, agentId); + } catch { + // ignore — leave children empty + } + } + + const status = deriveStatus(pair, meta.status); + let success: boolean | null; + if (pair.completed !== null) success = true; + else if (pair.failed !== null) success = false; + else success = null; + + const resolvedParent = + pair.spawned.data.parent_agent_id ?? parentAgentId; + + return { + agent_id: agentId, + agent_name: pair.spawned.data.agent_name ?? null, + subagent_type: meta.subagent_type, + run_in_background: pair.spawned.data.run_in_background, + parent_agent_id: resolvedParent, + depth, + status, + success, + result_summary: pair.completed?.data.result_summary ?? null, + error: pair.failed?.data.error ?? null, + spawn_seq: pair.spawned.seq, + spawn_time: pair.spawned.time, + children, + }; + }), + ); + + // Sort deterministically — earliest spawn first. + nodes.sort((a, b) => a.spawn_seq - b.spawn_seq); + return nodes; + } + + return buildLevel(mainWireRecords, 0, null); +} diff --git a/apps/vis/server/src/lib/types.ts b/apps/vis/server/src/lib/types.ts new file mode 100644 index 000000000..90a0149e2 --- /dev/null +++ b/apps/vis/server/src/lib/types.ts @@ -0,0 +1,725 @@ +// Vis-local mirror of the core WireRecord interfaces. Copied so the +// `VisWireRecord` union stays structurally equal to the core `WireRecord` +// union; the sole cast lives in `lib/wire-loader.ts` and is safe because +// these interfaces are field-for-field compatible. +// +// Plus the vis-specific response types consumed by `server/src/routes/*` +// and the `web/` SPA. + +// ── Supporting types (subset of core helpers) ──────────────────────────── + +export interface TextPart { + type: 'text'; + text: string; +} +export interface ImageURLPart { + type: 'image_url'; + image_url: { url: string }; +} +export interface VideoURLPart { + type: 'video_url'; + video_url: { url: string }; +} +export type UserInputPart = TextPart | ImageURLPart | VideoURLPart; + +export interface TokenUsage { + input: number; + output: number; + cache_read?: number | undefined; + cache_write?: number | undefined; +} + +// ── File metadata header ──────────────────────────────────────────────── + +export interface WireFileMetadata { + type: 'metadata'; + protocol_version: string; + created_at: number; + kimi_version?: string | undefined; + /** Implementation that wrote the wire file. Present on TS-native sessions. */ + producer?: WireProducer | undefined; +} + +/** Implementation that wrote the wire file (Python migration vs native TS). */ +export interface WireProducer { + kind: 'python' | 'typescript'; + name: string; + version: string; +} + +// ── Vis-synthetic timeline records ───────────────────────────────────── +// `replayWire()` strips the file header (line 1) and session_initialized +// (line 2) out of `records[]` and exposes them separately. To make the +// Wire tab show EVERYTHING that lives in wire.jsonl, we splice them back +// in as synthetic timeline records, with `seq`/`time` synthesised where +// the on-disk record lacks them. + +export interface MetadataRecord { + type: 'metadata'; + /** Synthetic: 0 (placed just before session_initialized). */ + seq: number; + /** Synthetic: mirrors `created_at`. */ + time: number; + protocol_version: string; + created_at: number; + kimi_version?: string | undefined; + producer?: WireProducer | undefined; + /** Vis-only: which wire file this header came from (useful across archives). */ + file_name?: string | undefined; +} + +// ── session_initialized ─────────────────────────────────────────────── +// Line 2 of every TS-native wire.jsonl. Carries startup baseline that +// the projector reads as authoritative — extracted by replayWire out of +// `records[]` onto `ReplayResult.sessionInitialized`. + +interface SessionInitializedCommonFields { + type: 'session_initialized'; + seq: number; + time: number; + system_prompt: string; + active_tools: string[]; + model?: string | undefined; + permission_mode?: 'default' | 'acceptEdits' | 'bypassPermissions' | undefined; + plan_mode?: boolean | undefined; + workspace_dir?: string | undefined; + thinking_level?: string | undefined; +} + +export interface SessionInitializedMainRecord extends SessionInitializedCommonFields { + agent_type: 'main'; + session_id: string; +} + +export interface SessionInitializedSubRecord extends SessionInitializedCommonFields { + agent_type: 'sub'; + agent_id: string; + agent_name?: string | undefined; + parent_session_id: string; + parent_agent_id?: string | undefined; + parent_tool_call_id: string; + run_in_background: boolean; +} + +export interface SessionInitializedIndependentRecord extends SessionInitializedCommonFields { + agent_type: 'independent'; + agent_id: string; + agent_name?: string | undefined; +} + +export type SessionInitializedRecord = + | SessionInitializedMainRecord + | SessionInitializedSubRecord + | SessionInitializedIndependentRecord; + +// ── Record branches ────────────────────────────────────────────────── + +export interface TurnBeginRecord { + type: 'turn_begin'; + seq: number; + time: number; + turn_id: string; + agent_type: 'main' | 'sub' | 'independent'; + user_input?: string | undefined; + input_kind: 'user' | 'system_trigger'; + trigger_source?: string | undefined; +} + +export interface TurnEndRecord { + type: 'turn_end'; + seq: number; + time: number; + turn_id: string; + agent_type: 'main' | 'sub' | 'independent'; + success: boolean; + reason: 'done' | 'cancelled' | 'error' | 'interrupted'; + usage?: + | { + input_tokens: number; + output_tokens: number; + cache_read_tokens?: number | undefined; + cache_write_tokens?: number | undefined; + cost_usd?: number | undefined; + } + | undefined; + synthetic?: boolean | undefined; +} + +export interface UserMessageRecord { + type: 'user_message'; + seq: number; + time: number; + turn_id: string; + content: string | readonly UserInputPart[]; + uuid?: string | undefined; +} + +// ── Atomic streaming records (replaces assistant_message) ───────────── +// Each LLM step is a sequence: step_begin → (content_part|tool_call)* → step_end. +// An "assistant message" is reconstructed by coalescing these atoms by step_uuid. + +export interface StepBeginRecord { + type: 'step_begin'; + seq: number; + time: number; + /** Unique id for this step (anchor for its content_part / tool_call atoms). */ + uuid: string; + turn_id: string; + step: number; +} + +export interface StepEndRecord { + type: 'step_end'; + seq: number; + time: number; + uuid: string; + turn_id: string; + step: number; + usage?: + | { + input_tokens: number; + output_tokens: number; + cache_read_tokens?: number | undefined; + cache_write_tokens?: number | undefined; + } + | undefined; + finish_reason?: string | undefined; +} + +export interface ContentPartRecord { + type: 'content_part'; + seq: number; + time: number; + uuid: string; + turn_id: string; + step: number; + /** Anchors to the step_begin.uuid that opened this step. */ + step_uuid: string; + role: 'assistant'; + /** Inner key is `kind` (not `type`) to avoid outer-union collision. */ + part: + | { kind: 'text'; text: string } + | { kind: 'think'; think: string; encrypted?: string | undefined }; +} + +export interface ToolResultRecord { + type: 'tool_result'; + seq: number; + time: number; + turn_id: string; + tool_call_id: string; + output: unknown; + is_error?: boolean | undefined; + synthetic?: boolean | undefined; + uuid?: string | undefined; + parent_uuid?: string | undefined; +} + +export interface CompactionRecord { + type: 'compaction'; + seq: number; + time: number; + summary: string; + pre_compact_tokens: number; + post_compact_tokens: number; + uuid?: string | undefined; +} + +export interface SystemPromptChangedRecord { + type: 'system_prompt_changed'; + seq: number; + time: number; + new_prompt: string; +} + +export interface ToolsChangedRecord { + type: 'tools_changed'; + seq: number; + time: number; + operation: 'register' | 'remove' | 'set_active'; + tools: string[]; +} + +export interface SystemReminderRecord { + type: 'system_reminder'; + seq: number; + time: number; + content: string; + consumed_at_turn?: number | undefined; +} + +export interface NotificationRecord { + type: 'notification'; + seq: number; + time: number; + data: { + id: string; + category: 'task' | 'agent' | 'system' | 'team'; + type: string; + source_kind: string; + source_id: string; + title: string; + body: string; + severity: 'info' | 'success' | 'warning' | 'error'; + payload?: Record<string, unknown> | undefined; + targets: Array<'llm' | 'wire' | 'shell'>; + dedupe_key?: string | undefined; + delivered_at?: + | { + llm?: number | undefined; + wire?: number | undefined; + shell?: number | undefined; + } + | undefined; + envelope_id?: string | undefined; + // background_task notifications attach streamed tail output that the + // live projector renders into a <task-notification> block. + tail_output?: string | undefined; + }; +} + +// First-class tool-call record (was once telemetry-only under a different name). +export interface ToolCallRecord { + type: 'tool_call'; + seq: number; + time: number; + uuid: string; + turn_id: string; + step: number; + /** Anchors to the step_begin.uuid that opened this step. */ + step_uuid: string; + data: { + tool_call_id: string; + tool_name: string; + args: unknown; + description?: string | undefined; + display?: ApprovalDisplay; + }; +} + +export interface ToolDeniedRecord { + type: 'tool_denied'; + seq: number; + time: number; + turn_id: string; + step: number; + data: { + tool_call_id: string; + tool_name: string; + rule_id: string; + reason: string; + }; +} + +export type McpApprovalReason = 'elicitation' | 'auth' | 'tool_call'; + +export type ApprovalSource = + | { kind: 'loop'; agent_id: string } + | { kind: 'subagent'; agent_id: string; subagent_type?: string | undefined } + | { kind: 'turn'; turn_id: string } + | { kind: 'session'; session_id: string } + | { kind: 'mcp'; server_id: string; reason: McpApprovalReason }; + +// `ApprovalDisplay` is `ToolInputDisplay` in core. We intentionally keep the +// shape opaque here — vis only passes it through to the UI. +export type ApprovalDisplay = unknown; + +export type SkillInvocationTrigger = 'user-slash' | 'model-tool' | 'nested-skill'; + +export interface SkillInvokedRecord { + type: 'skill_invoked'; + seq: number; + time: number; + turn_id: string; + agent_type?: 'main' | 'sub' | 'independent' | undefined; + data: { + skill_name: string; + execution_mode: 'inline' | 'fork'; + original_input: string; + sub_agent_id?: string | undefined; + invocation_trigger?: SkillInvocationTrigger | undefined; + query_depth?: number | undefined; + }; +} + +export interface SkillCompletedRecord { + type: 'skill_completed'; + seq: number; + time: number; + turn_id: string; + agent_type?: 'main' | 'sub' | 'independent' | undefined; + data: { + skill_name: string; + execution_mode: 'inline' | 'fork'; + success: boolean; + error?: string | undefined; + sub_agent_id?: string | undefined; + invocation_trigger?: SkillInvocationTrigger | undefined; + query_depth?: number | undefined; + }; +} + +export interface ApprovalRequestRecord { + type: 'approval_request'; + seq: number; + time: number; + turn_id: string; + step: number; + data: { + request_id: string; + tool_call_id: string; + tool_name: string; + action: string; + display: ApprovalDisplay; + source: ApprovalSource; + }; +} + +export interface ApprovalResponseRecord { + type: 'approval_response'; + seq: number; + time: number; + turn_id: string; + step: number; + data: { + request_id: string; + response: 'approved' | 'rejected' | 'cancelled'; + feedback?: string | undefined; + selected_label?: string | undefined; + synthetic?: boolean | undefined; + }; +} + +export interface TeamMailRecord { + type: 'team_mail'; + seq: number; + time: number; + data: { + mail_id: string; + reply_to?: string | undefined; + from_agent: string; + to_agent: string; + content: string; + summary?: string | undefined; + }; +} + +export interface SubagentSpawnedRecord { + type: 'subagent_spawned'; + seq: number; + time: number; + uuid?: string | undefined; + data: { + agent_id: string; + agent_name?: string | undefined; + parent_tool_call_id: string; + parent_agent_id?: string | undefined; + parent_tool_call_uuid?: string | undefined; + run_in_background: boolean; + }; +} + +export interface SubagentCompletedRecord { + type: 'subagent_completed'; + seq: number; + time: number; + uuid?: string | undefined; + parent_uuid?: string | undefined; + data: { + agent_id: string; + parent_tool_call_id: string; + result_summary: string; + usage?: TokenUsage | undefined; + }; +} + +export interface SubagentFailedRecord { + type: 'subagent_failed'; + seq: number; + time: number; + uuid?: string | undefined; + parent_uuid?: string | undefined; + data: { + agent_id: string; + parent_tool_call_id: string; + error: string; + }; +} + +export interface OwnershipChangedRecord { + type: 'ownership_changed'; + seq: number; + time: number; + old_owner: string | null; + new_owner: string; +} + +export interface ContextEditRecord { + type: 'context_edit'; + seq: number; + time: number; + operation: 'edit_message' | 'delete_message' | 'rewind' | 'insert_message' | 'replace_message'; + target_seq?: number | undefined; + to_turn?: number | undefined; + after_seq?: number | undefined; + new_content?: string | undefined; + new_role?: 'user' | 'assistant' | 'system' | undefined; + cascade?: boolean | undefined; +} + +export interface ContextClearedRecord { + type: 'context_cleared'; + seq: number; + time: number; +} + +// ── Union ───────────────────────────────────────────────────────────── + +export type VisWireRecord = + | MetadataRecord + | SessionInitializedRecord + | TurnBeginRecord + | TurnEndRecord + | UserMessageRecord + | ToolResultRecord + | CompactionRecord + | SystemPromptChangedRecord + | ToolsChangedRecord + | SystemReminderRecord + | NotificationRecord + | StepBeginRecord + | StepEndRecord + | ContentPartRecord + | ToolCallRecord + | ToolDeniedRecord + | SkillInvokedRecord + | SkillCompletedRecord + | ApprovalRequestRecord + | ApprovalResponseRecord + | TeamMailRecord + | SubagentSpawnedRecord + | SubagentCompletedRecord + | SubagentFailedRecord + | OwnershipChangedRecord + | ContextEditRecord + | ContextClearedRecord; + +// ── Vis-specific response types ───────────────────────────────────────── + +export interface ApiError { + error: string; + code: + | 'NOT_FOUND' + | 'BAD_REQUEST' + | 'UNAUTHORIZED' + | 'READ_ERROR' + | 'PARSE_ERROR' + | 'DELETE_ERROR'; +} + +export interface DeleteSessionResponse { + session_id: string; + deleted: true; +} + +export interface ClearSessionsResponse { + deleted_count: number; + failed: Array<{ session_id: string; error: string }>; +} + +export interface SessionState { + session_id: string; + title?: string; + isCustomTitle?: boolean; + customTitle?: string; + lastPrompt?: string; + last_prompt?: string; + model?: string; + last_turn_id?: string; + last_turn_time?: number; + turn_count?: number; + created_at: number; + updated_at: number; + workspace_dir?: string; + auto_approve_actions?: string[]; + permission_mode?: 'default' | 'acceptEdits' | 'bypassPermissions'; + thinking_level?: string; + custom_title?: string; + plan_mode?: boolean; + plan_slug?: string; + tags?: string[]; + description?: string; + archived?: boolean; + color?: string; + last_exit_code?: 'clean' | 'dirty'; + /** Implementation that wrote this session. Vis only surfaces sessions + * with `kind === 'typescript'`. */ + producer?: WireProducer; +} + +export interface SessionSummary { + session_id: string; + title: string | null; + last_prompt: string | null; + created_at: number; + updated_at: number; + last_turn_time: number | null; + model: string | null; + permission_mode: string | null; + last_exit_code: 'clean' | 'dirty' | null; + custom_title: string | null; + tags: string[]; + archived: boolean; + workspace_dir: string | null; + wire_protocol_version: string | null; + wire_record_count: number; + archive_count: number; + subagent_count: number; + health: 'ok' | 'broken' | 'missing_wire'; +} + +export interface SessionDetail { + session_id: string; + title: string | null; + last_prompt: string | null; + state: SessionState; + subagent_ids: string[]; + archive_files: string[]; + tool_result_ids: string[]; + wire_metadata: WireFileMetadata | null; +} + +export interface WireResponse { + session_id: string; + agent_id: string | null; + files_read: string[]; + health: 'ok' | 'broken'; + broken_reason?: string; + warnings: string[]; + records: VisWireRecord[]; +} + +export interface ContentPart { + type: 'text' | 'think' | 'image_url' | 'video_url'; + [key: string]: unknown; +} + +export interface ToolCallEntry { + type: 'function'; + id: string; + function: { name: string; arguments: string | null }; +} + +export type MessageOrigin = + | { kind: 'user' } + | { kind: 'assistant' } + | { kind: 'tool'; tool_call_id: string } + | { kind: 'system_reminder'; seq: number } + | { kind: 'notification'; seq: number; notification_id: string; severity: string }; + +export interface AnnotatedMessage { + seq: number; + message: { + role: 'user' | 'assistant' | 'tool'; + content: ContentPart[]; + tool_calls: ToolCallEntry[]; + tool_call_id?: string; + }; + origin: MessageOrigin; + is_ephemeral: boolean; + out_of_context: boolean; + persisted_output_path?: string; +} + +export interface ProjectedStateSummary { + model: string | null; + system_prompt: string | null; + active_tools: string[]; + last_seq: number; + token_count: number; + permission_mode: string | null; + plan_mode: boolean; + /** Token breakdown, summed from every `step_end.usage` in the session. + * Provides the 4-segment bar shown at the top of the Context tab. + * Zero when no step_ends have been observed (e.g. pre-protocol session). */ + input_tokens: number; + output_tokens: number; + cache_read_tokens: number; + cache_write_tokens: number; +} + +export interface ContextResponse { + session_id: string; + agent_id: string | null; + annotated_messages: AnnotatedMessage[]; + projected_state: ProjectedStateSummary; +} + +export interface SubagentNode { + agent_id: string; + agent_name: string | null; + subagent_type: string | null; + run_in_background: boolean; + parent_agent_id: string | null; + depth: number; + status: 'running' | 'completed' | 'failed' | 'killed' | 'lost' | 'unknown'; + success: boolean | null; + result_summary: string | null; + error: string | null; + spawn_seq: number; + spawn_time: number; + children: SubagentNode[]; +} + +export interface SubagentTreeResponse { + session_id: string; + tree: SubagentNode[]; +} + +export interface SubagentMetaResponse { + agent_id: string; + session_id: string; + meta_json: { + agent_id: string; + subagent_type: string; + status: string; + description: string; + parent_tool_call_id: string; + created_at: number; + updated_at: number; + } | null; + spawned_record: { + agent_name?: string; + parent_tool_call_id: string; + parent_agent_id?: string; + run_in_background: boolean; + seq: number; + time: number; + } | null; + completed_record: { + parent_tool_call_id: string; + result_summary: string; + usage?: { + input_tokens: number; + output_tokens: number; + cache_read_tokens?: number; + cache_write_tokens?: number; + }; + seq: number; + time: number; + } | null; + failed_record: { + parent_tool_call_id: string; + error: string; + seq: number; + time: number; + } | null; + depth: number; +} + +export interface ToolResultFileResponse { + tool_call_id: string; + session_id: string; + size_bytes: number; + content: string; +} diff --git a/apps/vis/server/src/lib/wire-loader.ts b/apps/vis/server/src/lib/wire-loader.ts new file mode 100644 index 000000000..3f29eaf54 --- /dev/null +++ b/apps/vis/server/src/lib/wire-loader.ts @@ -0,0 +1,179 @@ +import { readdir, readFile } from 'node:fs/promises'; +import { basename, join } from 'node:path'; + +import type { + MetadataRecord, + SessionInitializedRecord, + VisWireRecord, + WireFileMetadata, +} from './types'; +import { replayWire } from './wire-replay'; + +const ARCHIVE_RE = /^wire\.(\d+)\.jsonl$/; + +export interface WireLoadResult { + records: VisWireRecord[]; + /** session_initialized from the newest wire file's line 2. + * `null` when the session predates the line-2 header convention or + * the file lacks a `session_initialized` line. */ + session_initialized: SessionInitializedRecord | null; + health: 'ok' | 'broken'; + broken_reason?: string; + warnings: string[]; + files_read: string[]; +} + +/** + * Enumerate all wire files in `agentDir` (archives first, ascending by N; + * then current `wire.jsonl`). Re-implemented locally because the + * corresponding helper inside `@moonshot-ai/agent-core` is not exported. + */ +export async function listWireFilesManually(agentDir: string): Promise<string[]> { + const wireDir = await resolveWireDir(agentDir); + let entries: string[]; + try { + entries = await readdir(wireDir); + } catch { + return []; + } + + const archives: { path: string; n: number }[] = []; + let current: string | null = null; + for (const entry of entries) { + if (entry === 'wire.jsonl') { + current = join(wireDir, entry); + continue; + } + const m = ARCHIVE_RE.exec(entry); + if (m?.[1] !== undefined) { + archives.push({ path: join(wireDir, entry), n: Number.parseInt(m[1], 10) }); + } + } + archives.sort((a, b) => a.n - b.n); + const out = archives.map((a) => a.path); + if (current !== null) { + out.push(current); + } + return out; +} + +async function resolveWireDir(agentDir: string): Promise<string> { + const mainDir = join(agentDir, 'agents', 'main'); + try { + const entries = await readdir(mainDir); + if (entries.includes('wire.jsonl')) return mainDir; + } catch { + // `agentDir` is already an agent dir or has no main agent. + } + return agentDir; +} + +/** + * Load all wire records for an agent (main or subagent) in age order. + * + * Calls `replayWire()` per file and concatenates — core only exports the + * single-file replay primitive, so multi-file replay is composed here. + * + * The cast to `VisWireRecord[]` is the sole trust boundary in vis. Safe + * because the vis mirror types are field-for-field identical to the core + * `WireRecord` union and `replayWire()` only emits validated records. + */ +export async function loadWireRecords(agentDir: string): Promise<WireLoadResult> { + const files = await listWireFilesManually(agentDir); + + if (files.length === 0) { + return { + records: [], + session_initialized: null, + health: 'broken', + broken_reason: `no wire files found under ${agentDir}`, + warnings: [], + files_read: [], + }; + } + + const records: VisWireRecord[] = []; + const warnings: string[] = []; + let overallHealth: 'ok' | 'broken' = 'ok'; + let brokenReason: string | undefined; + const filesRead: string[] = []; + // session_initialized is the line-2 truth source. Keep the most-recent + // (current wire's) value, not archive values, so post-compaction the + // live model/prompt is reflected. + let sessionInitialized: SessionInitializedRecord | null = null; + + for (const filePath of files) { + try { + const result = await replayWire(filePath); + filesRead.push(filePath); + const fileMeta = await readMetadataLine(filePath); + const init = result.sessionInitialized; + // Splice the two wire.jsonl header lines back into the timeline so + // the Wire tab shows EVERYTHING in the file (including system_prompt). + if (fileMeta !== null) { + const metaRecord: MetadataRecord = { + type: 'metadata', + seq: 0, + time: fileMeta.created_at, + protocol_version: fileMeta.protocol_version, + created_at: fileMeta.created_at, + file_name: basename(filePath), + }; + if (fileMeta.kimi_version !== undefined) metaRecord.kimi_version = fileMeta.kimi_version; + if (fileMeta.producer !== undefined) metaRecord.producer = fileMeta.producer; + records.push(metaRecord); + } + if (init !== null) records.push(init); + records.push(...(result.records)); + sessionInitialized = init; + for (const w of result.warnings) warnings.push(`${filePath}: ${w}`); + if (result.health === 'broken') { + overallHealth = 'broken'; + brokenReason ??= result.brokenReason ?? `broken wire file: ${filePath}`; + } + } catch (error) { + // Filesystem / unexpected errors surface as a soft broken state — + // vis still lists the session and shows its raw records if we + // have any. + overallHealth = 'broken'; + const reason = error instanceof Error ? error.message : String(error); + brokenReason ??= `${filePath}: ${reason}`; + warnings.push(`${filePath}: ${reason}`); + } + } + + const out: WireLoadResult = { + records, + session_initialized: sessionInitialized, + health: overallHealth, + warnings, + files_read: filesRead, + }; + if (brokenReason !== undefined) out.broken_reason = brokenReason; + return out; +} + +/** Parse line 1 of a wire.jsonl as the file-header metadata record. + * Returns null on any read/parse failure — the caller already ran + * `replayWire()`, which validates the header independently, so this + * helper stays forgiving: missing metadata just means we can't splice + * it into the timeline, not that the session is broken. */ +async function readMetadataLine(filePath: string): Promise<WireFileMetadata | null> { + try { + const text = await readFile(filePath, 'utf8'); + const nl = text.indexOf('\n'); + const first = nl === -1 ? text : text.slice(0, nl); + if (first.length === 0) return null; + const parsed = JSON.parse(first) as unknown; + if ( + typeof parsed !== 'object' || + parsed === null || + (parsed as { type?: unknown }).type !== 'metadata' + ) { + return null; + } + return parsed as WireFileMetadata; + } catch { + return null; + } +} diff --git a/apps/vis/server/src/lib/wire-replay.ts b/apps/vis/server/src/lib/wire-replay.ts new file mode 100644 index 000000000..3060eac6e --- /dev/null +++ b/apps/vis/server/src/lib/wire-replay.ts @@ -0,0 +1,488 @@ +import { readFile } from 'node:fs/promises'; + +import type { + ContentPartRecord, + NotificationRecord, + SessionInitializedRecord, + StepBeginRecord, + StepEndRecord, + SystemReminderRecord, + SystemPromptChangedRecord, + ToolCallRecord, + ToolResultRecord, + ToolsChangedRecord, + TurnBeginRecord, + UserInputPart, + UserMessageRecord, + VisWireRecord, + WireFileMetadata, +} from './types'; + +export interface ReplayWireResult { + health: 'ok' | 'broken'; + brokenReason?: string; + warnings: string[]; + metadata: WireFileMetadata | null; + sessionInitialized: SessionInitializedRecord | null; + records: VisWireRecord[]; +} + +export async function replayWire(wirePath: string): Promise<ReplayWireResult> { + const raw = await readFile(wirePath, 'utf8'); + const lines = raw.split(/\r?\n/).filter((line) => line.length > 0); + const warnings: string[] = []; + const records: VisWireRecord[] = []; + const rawRecords: RawWireRecord[] = []; + let metadata: WireFileMetadata | null = null; + let sessionInitialized: SessionInitializedRecord | null = null; + let nextTurnIndex = 0; + let currentTurnId = '0'; + + for (const [index, line] of lines.entries()) { + const seq = index + 1; + let parsed: unknown; + try { + parsed = JSON.parse(line) as unknown; + } catch (error) { + warnings.push(`line ${index + 1}: ${error instanceof Error ? error.message : String(error)}`); + continue; + } + if (!isRecord(parsed) || typeof parsed['type'] !== 'string') { + warnings.push(`line ${index + 1}: missing record type`); + continue; + } + rawRecords.push({ seq, raw: parsed }); + } + + for (const [recordIndex, record] of rawRecords.entries()) { + const parsed = record.raw; + const seq = record.seq; + if (parsed['type'] === 'metadata') { + metadata ??= parsed as unknown as WireFileMetadata; + continue; + } + if (parsed['type'] === 'session_initialized') { + sessionInitialized = parsed as unknown as SessionInitializedRecord; + continue; + } + const normalized = normalizeRecord( + parsed, + seq, + currentTurnId, + nextTurnIndex, + rawRecords, + recordIndex, + ); + records.push(...normalized.records); + if (normalized.currentTurnId !== undefined) currentTurnId = normalized.currentTurnId; + if (normalized.nextTurnIndex !== undefined) nextTurnIndex = normalized.nextTurnIndex; + } + + const out: ReplayWireResult = { + health: warnings.length === 0 ? 'ok' : 'broken', + warnings, + metadata, + sessionInitialized, + records, + }; + if (warnings.length > 0) out.brokenReason = warnings[0]; + return out; +} + +function isRecord(value: unknown): value is Record<string, unknown> { + return typeof value === 'object' && value !== null; +} + +interface RawWireRecord { + seq: number; + raw: Record<string, unknown>; +} + +interface NormalizeResult { + records: VisWireRecord[]; + currentTurnId?: string; + nextTurnIndex?: number; +} + +function normalizeRecord( + raw: Record<string, unknown>, + fallbackSeq: number, + currentTurnId: string, + nextTurnIndex: number, + rawRecords: readonly RawWireRecord[], + recordIndex: number, +): NormalizeResult { + const seq = numberValue(raw['seq']) ?? fallbackSeq; + const time = numberValue(raw['time']) ?? 0; + const type = stringValue(raw['type']) ?? 'unknown'; + + switch (type) { + case 'turn.prompt': + return normalizeTurnStart(raw, seq, time, nextTurnIndex); + + case 'turn.steer': { + const turnId = inferSteerStartedTurnId(rawRecords, recordIndex, currentTurnId, nextTurnIndex); + if (turnId === undefined) return { records: [] }; + return normalizeTurnStart(raw, seq, time, nextTurnIndex, turnId); + } + + case 'context.user_message': + case 'context.append_message': { + // `context.append_message` (newer producer) nests `{role, content}` + // under a `message` field; `context.user_message` (older) puts + // `content` at the top level. Only surface user-authored messages — + // assistant messages are already represented by content.part events + // inside loop events. + if (!isUserAuthoredAppendMessage(raw)) { + return { records: [notificationFromRaw(raw, seq, time)] }; + } + const message = isRecord(raw['message']) ? raw['message'] : undefined; + const rawContent = message !== undefined ? message['content'] : raw['content']; + const content = Array.isArray(rawContent) + ? (rawContent as readonly UserInputPart[]) + : (stringValue(rawContent) ?? ''); + const record: UserMessageRecord = { + type: 'user_message', + seq, + time, + turn_id: currentTurnId, + content, + }; + return { records: [record] }; + } + + case 'context.delta': + case 'context.append_loop_event': { + const event = raw['event']; + if (!isRecord(event)) return { records: [notificationFromRaw(raw, seq, time)] }; + const eventTurnId = stringValue(event['turnId']); + const records = normalizeLoopEvent(event, seq, time, currentTurnId); + return { + records, + currentTurnId: eventTurnId, + }; + } + + case 'config.update': { + const prompt = stringValue(raw['systemPrompt']); + if (prompt !== undefined) { + const record: SystemPromptChangedRecord = { + type: 'system_prompt_changed', + seq, + time, + new_prompt: prompt, + }; + return { records: [record] }; + } + return { records: [notificationFromRaw(raw, seq, time)] }; + } + + case 'tool.set_active': + case 'tools.set_active_tools': { + const names = Array.isArray(raw['names']) + ? raw['names'].filter((name): name is string => typeof name === 'string') + : []; + const record: ToolsChangedRecord = { + type: 'tools_changed', + seq, + time, + operation: 'set_active', + tools: names, + }; + return { records: [record] }; + } + + case 'context.system_reminder': { + const record: SystemReminderRecord = { + type: 'system_reminder', + seq, + time, + content: stringValue(raw['content']) ?? '', + }; + return { records: [record] }; + } + + case 'context.clear': + return { records: [{ type: 'context_cleared', seq, time }] }; + + case 'context.mark_last_user_prompt_blocked': + return { records: [] }; + + default: + return { + records: [ + hasLegacyShape(raw) + ? (raw as unknown as VisWireRecord) + : notificationFromRaw(raw, seq, time), + ], + }; + } +} + +function normalizeTurnStart( + raw: Record<string, unknown>, + seq: number, + time: number, + nextTurnIndex: number, + turnId = String(nextTurnIndex), +): NormalizeResult { + const input = Array.isArray(raw['input']) ? (raw['input'] as readonly UserInputPart[]) : []; + const record: TurnBeginRecord = { + type: 'turn_begin', + seq, + time, + turn_id: turnId, + agent_type: 'main', + input_kind: 'user', + user_input: inputToText(input), + }; + return { + records: [record], + currentTurnId: turnId, + nextTurnIndex: nextTurnIndexAfter(turnId, nextTurnIndex), + }; +} + +function inferSteerStartedTurnId( + rawRecords: readonly RawWireRecord[], + recordIndex: number, + currentTurnId: string, + nextTurnIndex: number, +): string | undefined { + let sawUserMessage = false; + + // Wire does not persist turn.ended, so past records cannot prove whether + // core still had an active turn. The next explicit loop turnId is the durable + // signal for whether this steer stayed buffered or launched a fresh turn. + for (let index = recordIndex + 1; index < rawRecords.length; index += 1) { + const raw = rawRecords[index]?.raw; + if (raw === undefined) continue; + + const type = stringValue(raw['type']); + if (type === 'turn.prompt' || type === 'turn.steer') break; + if (type === 'context.user_message' || type === 'context.append_message') { + // Only an actual user message should count as evidence that the + // steer launched a fresh turn. The newer `context.append_message` + // producer carries assistant / system roles too, and + // `ContextMemory.appendSystemReminder()` persists injected + // reminders with role: 'user' but a non-user origin — both of + // those would fabricate a fresh turn ID below and mis-tag + // subsequent records. + if (isUserAuthoredAppendMessage(raw)) { + sawUserMessage = true; + } + continue; + } + if (type !== 'context.delta' && type !== 'context.append_loop_event') continue; + + const event = raw['event']; + if (!isRecord(event)) continue; + + const eventTurnId = stringValue(event['turnId']); + if (eventTurnId === undefined) continue; + return eventTurnId === currentTurnId ? undefined : eventTurnId; + } + + return sawUserMessage ? String(nextTurnIndex) : undefined; +} + +// Distinguishes real user input from non-user content that shares the +// `role: 'user'` slot for the LLM (system reminders, skill activation +// payloads, injection content, compaction summaries). The older +// `context.user_message` shape has no `origin`, so absence is treated +// as user authorship to preserve back-compat with older wire logs. +function isUserAuthoredAppendMessage(raw: Record<string, unknown>): boolean { + const message = isRecord(raw['message']) ? raw['message'] : undefined; + const role = stringValue(message?.['role']) ?? stringValue(raw['role']); + if (role !== undefined && role !== 'user') return false; + const origin = isRecord(message?.['origin']) ? message['origin'] : undefined; + const originKind = stringValue(origin?.['kind']); + if (originKind !== undefined && originKind !== 'user') return false; + return true; +} + +function nextTurnIndexAfter(turnId: string, nextTurnIndex: number): number { + const parsed = Number.parseInt(turnId, 10); + if (String(parsed) === turnId && parsed >= nextTurnIndex) return parsed + 1; + return nextTurnIndex + 1; +} + +function normalizeLoopEvent( + event: Record<string, unknown>, + seq: number, + time: number, + currentTurnId: string, +): VisWireRecord[] { + const eventType = stringValue(event['type']); + const turnId = stringValue(event['turnId']) ?? currentTurnId; + + switch (eventType) { + case undefined: + return [notificationFromRaw({ type: 'context.delta', event }, seq, time)]; + + case 'step.begin': { + const record: StepBeginRecord = { + type: 'step_begin', + seq, + time, + uuid: stringValue(event['uuid']) ?? `step_${seq}`, + turn_id: turnId, + step: numberValue(event['step']) ?? 0, + }; + return [record]; + } + + case 'content.part': { + const rawPart = isRecord(event['part']) ? event['part'] : {}; + const kind = rawPart['type'] === 'think' || rawPart['kind'] === 'think' ? 'think' : 'text'; + const record: ContentPartRecord = { + type: 'content_part', + seq, + time, + uuid: stringValue(event['uuid']) ?? `part_${seq}`, + turn_id: turnId, + step: numberValue(event['step']) ?? 0, + step_uuid: stringValue(event['stepUuid']) ?? '', + role: 'assistant', + part: + kind === 'think' + ? { kind, think: stringValue(rawPart['think']) ?? '' } + : { kind, text: stringValue(rawPart['text']) ?? '' }, + }; + return [record]; + } + + case 'tool.call': { + const record: ToolCallRecord = { + type: 'tool_call', + seq, + time, + uuid: stringValue(event['uuid']) ?? `tool_${seq}`, + turn_id: turnId, + step: numberValue(event['step']) ?? 0, + step_uuid: stringValue(event['stepUuid']) ?? '', + data: { + tool_call_id: stringValue(event['toolCallId']) ?? `tool_${seq}`, + tool_name: stringValue(event['name']) ?? 'unknown', + args: event['args'], + description: stringValue(event['description']), + display: event['display'], + }, + }; + return [record]; + } + + case 'tool.result': { + const result = isRecord(event['result']) ? event['result'] : {}; + const record: ToolResultRecord = { + type: 'tool_result', + seq, + time, + turn_id: turnId, + tool_call_id: stringValue(event['toolCallId']) ?? `tool_${seq}`, + output: result['output'], + is_error: result['isError'] === true, + parent_uuid: stringValue(event['parentUuid']), + }; + return [record]; + } + + case 'step.end': { + const record: StepEndRecord = { + type: 'step_end', + seq, + time, + uuid: stringValue(event['uuid']) ?? `step_${seq}`, + turn_id: turnId, + step: numberValue(event['step']) ?? 0, + usage: normalizeUsage(event['usage']), + finish_reason: stringValue(event['finishReason']), + }; + return [record]; + } + + default: + return [notificationFromRaw({ type: 'context.delta', event }, seq, time)]; + } +} + +function notificationFromRaw( + raw: Record<string, unknown>, + seq: number, + time: number, +): NotificationRecord { + const type = stringValue(raw['type']) ?? 'unknown'; + return { + type: 'notification', + seq, + time, + data: { + id: `${seq}:${type}`, + category: 'system', + type, + source_kind: 'wire', + source_id: 'vis', + title: type, + body: stringify(raw), + severity: 'info', + payload: raw, + targets: ['wire'], + }, + }; +} + +function hasLegacyShape(raw: Record<string, unknown>): boolean { + const type = stringValue(raw['type']); + return ( + type !== undefined && + !type.includes('.') && + typeof raw['seq'] === 'number' && + typeof raw['time'] === 'number' + ); +} + +function normalizeUsage(value: unknown): StepEndRecord['usage'] { + if (!isRecord(value)) return undefined; + return { + input_tokens: + numberValue(value['input_tokens']) ?? + numberValue(value['input']) ?? + numberValue(value['inputOther']) ?? + 0, + output_tokens: numberValue(value['output_tokens']) ?? numberValue(value['output']) ?? 0, + cache_read_tokens: + numberValue(value['cache_read_tokens']) ?? + numberValue(value['cache_read']) ?? + numberValue(value['inputCacheRead']), + cache_write_tokens: + numberValue(value['cache_write_tokens']) ?? + numberValue(value['cache_write']) ?? + numberValue(value['inputCacheCreation']), + }; +} + +function inputToText(input: readonly UserInputPart[]): string { + return input + .map((part) => { + if (part.type === 'text') return part.text; + if (part.type === 'image_url') return '<image>'; + return '<video>'; + }) + .join(''); +} + +function stringValue(value: unknown): string | undefined { + return typeof value === 'string' && value.length > 0 ? value : undefined; +} + +function numberValue(value: unknown): number | undefined { + return typeof value === 'number' && Number.isFinite(value) ? value : undefined; +} + +function stringify(value: unknown): string { + try { + return JSON.stringify(value); + } catch { + return '[unserializable]'; + } +} diff --git a/apps/vis/server/src/routes/context.ts b/apps/vis/server/src/routes/context.ts new file mode 100644 index 000000000..fdb1d5653 --- /dev/null +++ b/apps/vis/server/src/routes/context.ts @@ -0,0 +1,57 @@ +import { readFile, stat } from 'node:fs/promises'; + +import { Hono } from 'hono'; + +import { pathConfig } from '../config'; +import { buildAnnotatedMessages, buildProjectedStateSummary } from '../lib/context-builder'; +import type { ContextResponse, SessionState } from '../lib/types'; +import { loadWireRecords } from '../lib/wire-loader'; + +const SESSION_ID_RE = /^session_[a-zA-Z0-9_-]+$/; + +export function contextRoute(): Hono { + const app = new Hono(); + + app.get('/:id/context', async (c) => { + const id = c.req.param('id'); + if (!SESSION_ID_RE.test(id)) { + return c.json({ error: `invalid session id: ${id}`, code: 'BAD_REQUEST' }, 400); + } + const sessionDir = pathConfig.sessionDir(id); + try { + const dirStat = await stat(sessionDir); + if (!dirStat.isDirectory()) { + return c.json({ error: `session not found: ${id}`, code: 'NOT_FOUND' }, 404); + } + } catch { + return c.json({ error: `session not found: ${id}`, code: 'NOT_FOUND' }, 404); + } + + try { + const load = await loadWireRecords(sessionDir); + const state = await readSessionState(id); + const annotated = buildAnnotatedMessages(load.records); + const projected = buildProjectedStateSummary(load.records, load.session_initialized, state); + const body: ContextResponse = { + session_id: id, + agent_id: null, + annotated_messages: annotated, + projected_state: projected, + }; + return c.json(body); + } catch (error) { + const msg = error instanceof Error ? error.message : String(error); + return c.json({ error: `failed to build context: ${msg}`, code: 'READ_ERROR' }, 500); + } + }); + + return app; +} + +async function readSessionState(sessionId: string): Promise<SessionState | null> { + try { + return JSON.parse(await readFile(pathConfig.statePath(sessionId), 'utf8')) as SessionState; + } catch { + return null; + } +} diff --git a/apps/vis/server/src/routes/session-detail.ts b/apps/vis/server/src/routes/session-detail.ts new file mode 100644 index 000000000..3ab089740 --- /dev/null +++ b/apps/vis/server/src/routes/session-detail.ts @@ -0,0 +1,155 @@ +import { createReadStream } from 'node:fs'; +import { readFile, readdir, stat } from 'node:fs/promises'; +import { join } from 'node:path'; +import { createInterface } from 'node:readline'; + +import { Hono } from 'hono'; + +import { pathConfig } from '../config'; +import { deleteSession, SESSION_ID_RE } from '../lib/session-delete'; +import { readSessionLastPrompt, readSessionTitle } from '../lib/session-title'; +import type { SessionDetail, SessionState, WireFileMetadata } from '../lib/types'; + +const ARCHIVE_RE = /^wire\.\d+\.jsonl$/; + +async function readFirstLine(path: string): Promise<string | null> { + try { + const stream = createReadStream(path, { encoding: 'utf8' }); + const rl = createInterface({ input: stream, crlfDelay: Infinity }); + for await (const line of rl) { + rl.close(); + stream.close(); + return line; + } + } catch { + return null; + } + return null; +} + +export function sessionDetailRoute(): Hono { + const app = new Hono(); + + app.get('/:id', async (c) => { + const id = c.req.param('id'); + if (!SESSION_ID_RE.test(id)) { + return c.json({ error: `invalid session id: ${id}`, code: 'BAD_REQUEST' }, 400); + } + const sessionDir = pathConfig.sessionDir(id); + + let dirStat; + try { + dirStat = await stat(sessionDir); + } catch { + return c.json({ error: `session not found: ${id}`, code: 'NOT_FOUND' }, 404); + } + if (!dirStat.isDirectory()) { + return c.json({ error: `session not a directory: ${id}`, code: 'NOT_FOUND' }, 404); + } + + // state.json (best-effort). + let state: SessionState; + try { + const raw = await readFile(pathConfig.statePath(id), 'utf8'); + state = JSON.parse(raw) as SessionState; + } catch { + state = { + session_id: id, + created_at: 0, + updated_at: 0, + }; + } + + // Directory listings. + const mainAgentDir = pathConfig.mainAgentDir(id); + const mainEntries = await readdir(mainAgentDir).catch(() => [] as string[]); + const archive_files = mainEntries.filter((e) => ARCHIVE_RE.test(e)).toSorted(); + + const subagent_ids: string[] = []; + try { + const subs = await readdir(join(sessionDir, 'agents')); + for (const s of subs) { + if (s === 'main') continue; + const ss = await stat(join(sessionDir, 'agents', s)).catch(() => null); + if (ss?.isDirectory()) subagent_ids.push(s); + } + } catch { + try { + const subs = await readdir(join(sessionDir, 'subagents')); + for (const s of subs) { + const ss = await stat(join(sessionDir, 'subagents', s)).catch(() => null); + if (ss?.isDirectory()) subagent_ids.push(s); + } + } catch { + // subagents dir missing is fine + } + } + + const tool_result_ids: string[] = []; + try { + const files = await readdir(join(mainAgentDir, 'tool-results')); + for (const f of files) { + if (f.endsWith('.txt')) tool_result_ids.push(f.slice(0, -4)); + } + } catch { + try { + const files = await readdir(join(sessionDir, 'tool-results')); + for (const f of files) { + if (f.endsWith('.txt')) tool_result_ids.push(f.slice(0, -4)); + } + } catch { + // tool-results dir missing is fine + } + } + + // wire metadata (first line of wire.jsonl) + let wire_metadata: WireFileMetadata | null = null; + const firstLine = await readFirstLine(pathConfig.wirePath(id)); + if (firstLine !== null) { + try { + const parsed = JSON.parse(firstLine) as unknown; + if ( + typeof parsed === 'object' && + parsed !== null && + (parsed as { type?: unknown }).type === 'metadata' + ) { + wire_metadata = parsed as WireFileMetadata; + } + } catch { + // ignore malformed header + } + } + + const detail: SessionDetail = { + session_id: id, + title: readSessionTitle(state), + last_prompt: readSessionLastPrompt(state), + state, + subagent_ids, + archive_files, + tool_result_ids, + wire_metadata, + }; + return c.json(detail); + }); + + app.delete('/:id', async (c) => { + const id = c.req.param('id'); + if (!SESSION_ID_RE.test(id)) { + return c.json({ error: `invalid session id: ${id}`, code: 'BAD_REQUEST' }, 400); + } + + try { + const result = await deleteSession(id); + if (result === null) { + return c.json({ error: `session not found: ${id}`, code: 'NOT_FOUND' }, 404); + } + return c.json(result); + } catch (error) { + const msg = error instanceof Error ? error.message : String(error); + return c.json({ error: `failed to delete session: ${msg}`, code: 'DELETE_ERROR' }, 500); + } + }); + + return app; +} diff --git a/apps/vis/server/src/routes/sessions.ts b/apps/vis/server/src/routes/sessions.ts new file mode 100644 index 000000000..4b44d476d --- /dev/null +++ b/apps/vis/server/src/routes/sessions.ts @@ -0,0 +1,28 @@ +import { Hono } from 'hono'; + +import { pathConfig } from '../config'; +import { clearSessions } from '../lib/session-delete'; +import { listSessions } from '../lib/session-lister'; + +export function sessionsRoute(): Hono { + const app = new Hono(); + app.get('/', async (c) => { + try { + const summaries = await listSessions(pathConfig.sessionsDir); + return c.json(summaries); + } catch (error) { + const msg = error instanceof Error ? error.message : String(error); + return c.json({ error: `failed to list sessions: ${msg}`, code: 'READ_ERROR' }, 500); + } + }); + app.delete('/', async (c) => { + try { + const result = await clearSessions(); + return c.json(result); + } catch (error) { + const msg = error instanceof Error ? error.message : String(error); + return c.json({ error: `failed to clear sessions: ${msg}`, code: 'DELETE_ERROR' }, 500); + } + }); + return app; +} diff --git a/apps/vis/server/src/routes/subagents.ts b/apps/vis/server/src/routes/subagents.ts new file mode 100644 index 000000000..5a8ee476c --- /dev/null +++ b/apps/vis/server/src/routes/subagents.ts @@ -0,0 +1,226 @@ +import { readFile, stat } from 'node:fs/promises'; + +import { Hono } from 'hono'; + +import { pathConfig } from '../config'; +import { buildAnnotatedMessages, buildProjectedStateSummary } from '../lib/context-builder'; +import { buildSubagentTree } from '../lib/subagent-loader'; +import type { + ContextResponse, + SubagentMetaResponse, + SubagentNode, + SubagentTreeResponse, + WireResponse +} from '../lib/types'; +import { loadWireRecords } from '../lib/wire-loader'; + +const SESSION_ID_RE = /^session_[a-zA-Z0-9_-]+$/; +// Subagent ids in the wild look like "sub_<uuid-ish>" — allow word chars and dashes. +const AGENT_ID_RE = /^[a-zA-Z0-9_-]+$/; + +function findNode(tree: SubagentNode[], agentId: string): SubagentNode | null { + for (const node of tree) { + if (node.agent_id === agentId) return node; + const found = findNode(node.children, agentId); + if (found !== null) return found; + } + return null; +} + +export function subagentsRoute(): Hono { + const app = new Hono(); + + app.get('/:id/subagents', async (c) => { + const id = c.req.param('id'); + if (!SESSION_ID_RE.test(id)) { + return c.json({ error: `invalid session id: ${id}`, code: 'BAD_REQUEST' }, 400); + } + const sessionDir = pathConfig.sessionDir(id); + try { + const s = await stat(sessionDir); + if (!s.isDirectory()) + return c.json({ error: `session not found: ${id}`, code: 'NOT_FOUND' }, 404); + } catch { + return c.json({ error: `session not found: ${id}`, code: 'NOT_FOUND' }, 404); + } + + try { + const main = await loadWireRecords(sessionDir); + const tree = await buildSubagentTree(sessionDir, main.records); + const body: SubagentTreeResponse = { session_id: id, tree }; + return c.json(body); + } catch (error) { + const msg = error instanceof Error ? error.message : String(error); + return c.json({ error: `failed to build subagent tree: ${msg}`, code: 'READ_ERROR' }, 500); + } + }); + + app.get('/:id/subagents/:agentId/wire', async (c) => { + const id = c.req.param('id'); + const agentId = c.req.param('agentId'); + if (!SESSION_ID_RE.test(id) || !AGENT_ID_RE.test(agentId)) { + return c.json({ error: 'invalid session or agent id', code: 'BAD_REQUEST' }, 400); + } + const subDir = pathConfig.subagentDir(id, agentId); + try { + const s = await stat(subDir); + if (!s.isDirectory()) + return c.json({ error: `subagent not found: ${agentId}`, code: 'NOT_FOUND' }, 404); + } catch { + return c.json({ error: `subagent not found: ${agentId}`, code: 'NOT_FOUND' }, 404); + } + + try { + const result = await loadWireRecords(subDir); + const body: WireResponse = { + session_id: id, + agent_id: agentId, + files_read: result.files_read, + health: result.health, + warnings: result.warnings, + records: result.records, + }; + if (result.broken_reason !== undefined) body.broken_reason = result.broken_reason; + return c.json(body); + } catch (error) { + const msg = error instanceof Error ? error.message : String(error); + return c.json({ error: `failed to load subagent wire: ${msg}`, code: 'READ_ERROR' }, 500); + } + }); + + app.get('/:id/subagents/:agentId/context', async (c) => { + const id = c.req.param('id'); + const agentId = c.req.param('agentId'); + if (!SESSION_ID_RE.test(id) || !AGENT_ID_RE.test(agentId)) { + return c.json({ error: 'invalid session or agent id', code: 'BAD_REQUEST' }, 400); + } + const subDir = pathConfig.subagentDir(id, agentId); + try { + const s = await stat(subDir); + if (!s.isDirectory()) + return c.json({ error: `subagent not found: ${agentId}`, code: 'NOT_FOUND' }, 404); + } catch { + return c.json({ error: `subagent not found: ${agentId}`, code: 'NOT_FOUND' }, 404); + } + + try { + const load = await loadWireRecords(subDir); + const annotated = buildAnnotatedMessages(load.records); + const projected = buildProjectedStateSummary(load.records, load.session_initialized); + const body: ContextResponse = { + session_id: id, + agent_id: agentId, + annotated_messages: annotated, + projected_state: projected, + }; + return c.json(body); + } catch (error) { + const msg = error instanceof Error ? error.message : String(error); + return c.json({ error: `failed to build subagent context: ${msg}`, code: 'READ_ERROR' }, 500); + } + }); + + app.get('/:id/subagents/:agentId/meta', async (c) => { + const id = c.req.param('id'); + const agentId = c.req.param('agentId'); + if (!SESSION_ID_RE.test(id) || !AGENT_ID_RE.test(agentId)) { + return c.json({ error: 'invalid session or agent id', code: 'BAD_REQUEST' }, 400); + } + const sessionDir = pathConfig.sessionDir(id); + const subDir = pathConfig.subagentDir(id, agentId); + try { + const s = await stat(subDir); + if (!s.isDirectory()) + return c.json({ error: `subagent not found: ${agentId}`, code: 'NOT_FOUND' }, 404); + } catch { + return c.json({ error: `subagent not found: ${agentId}`, code: 'NOT_FOUND' }, 404); + } + + let meta_json: SubagentMetaResponse['meta_json'] = null; + try { + const raw = await readFile(`${subDir}/meta.json`, 'utf8'); + meta_json = JSON.parse(raw) as SubagentMetaResponse['meta_json']; + } catch { + meta_json = null; + } + + // Find the agent in the tree, then scan its direct parent's wire for + // lifecycle records. For nested subagents (depth >= 1), lifecycle records + // live on the intermediate parent's wire, not the main session wire. + let spawned_record: SubagentMetaResponse['spawned_record'] = null; + let completed_record: SubagentMetaResponse['completed_record'] = null; + let failed_record: SubagentMetaResponse['failed_record'] = null; + let depth = 0; + + try { + const main = await loadWireRecords(sessionDir); + const tree = await buildSubagentTree(sessionDir, main.records); + const node = findNode(tree, agentId); + if (node !== null) depth = node.depth; + + // Pick the correct wire to scan: main wire for depth 0, else the parent + // subagent's wire (which contains this child's spawn/complete records). + const parentAgentId = node?.parent_agent_id ?? null; + const scanRecords = + parentAgentId === null + ? main.records + : (await loadWireRecords(pathConfig.subagentDir(id, parentAgentId))).records; + + for (const r of scanRecords) { + if (r.type === 'subagent_spawned' && r.data.agent_id === agentId) { + const entry: NonNullable<SubagentMetaResponse['spawned_record']> = { + parent_tool_call_id: r.data.parent_tool_call_id, + run_in_background: r.data.run_in_background, + seq: r.seq, + time: r.time, + }; + if (r.data.agent_name !== undefined) entry.agent_name = r.data.agent_name; + if (r.data.parent_agent_id !== undefined) entry.parent_agent_id = r.data.parent_agent_id; + spawned_record = entry; + } else if (r.type === 'subagent_completed' && r.data.agent_id === agentId) { + const entry: NonNullable<SubagentMetaResponse['completed_record']> = { + parent_tool_call_id: r.data.parent_tool_call_id, + result_summary: r.data.result_summary, + seq: r.seq, + time: r.time, + }; + if (r.data.usage !== undefined) { + const u = r.data.usage; + const usage: { + input_tokens: number; + output_tokens: number; + cache_read_tokens?: number; + cache_write_tokens?: number; + } = { input_tokens: u.input, output_tokens: u.output }; + if (u.cache_read !== undefined) usage.cache_read_tokens = u.cache_read; + if (u.cache_write !== undefined) usage.cache_write_tokens = u.cache_write; + entry.usage = usage; + } + completed_record = entry; + } else if (r.type === 'subagent_failed' && r.data.agent_id === agentId) { + failed_record = { + parent_tool_call_id: r.data.parent_tool_call_id, + error: r.data.error, + seq: r.seq, + time: r.time, + }; + } + } + } catch { + // Fall through — partial response is still useful. + } + + const body: SubagentMetaResponse = { + agent_id: agentId, + session_id: id, + meta_json, + spawned_record, + completed_record, + failed_record, + depth, + }; + return c.json(body); + }); + + return app; +} diff --git a/apps/vis/server/src/routes/tool-results.ts b/apps/vis/server/src/routes/tool-results.ts new file mode 100644 index 000000000..30b67da00 --- /dev/null +++ b/apps/vis/server/src/routes/tool-results.ts @@ -0,0 +1,48 @@ +import { readFile, stat } from 'node:fs/promises'; + +import { Hono } from 'hono'; + +import { pathConfig } from '../config'; +import type { ToolResultFileResponse } from '../lib/types'; + +const SESSION_ID_RE = /^session_[a-zA-Z0-9_-]+$/; +// Tool call ids observed in the wild: simple identifiers, "call_..." or +// "Agent:7". Allow a conservative character set including colon. +const TOOL_CALL_ID_RE = /^[a-zA-Z0-9_:.-]+$/; + +export function toolResultsRoute(): Hono { + const app = new Hono(); + + app.get('/:id/tool-results/:toolCallId', async (c) => { + const id = c.req.param('id'); + const toolCallId = c.req.param('toolCallId'); + if (!SESSION_ID_RE.test(id)) { + return c.json({ error: `invalid session id: ${id}`, code: 'BAD_REQUEST' }, 400); + } + if (!TOOL_CALL_ID_RE.test(toolCallId)) { + return c.json({ error: `invalid tool call id: ${toolCallId}`, code: 'BAD_REQUEST' }, 400); + } + const filePath = pathConfig.toolResultArchivePath(id, toolCallId); + let s; + try { + s = await stat(filePath); + } catch { + return c.json({ error: `tool result not found: ${toolCallId}`, code: 'NOT_FOUND' }, 404); + } + try { + const content = await readFile(filePath, 'utf8'); + const body: ToolResultFileResponse = { + tool_call_id: toolCallId, + session_id: id, + size_bytes: s.size, + content, + }; + return c.json(body); + } catch (error) { + const msg = error instanceof Error ? error.message : String(error); + return c.json({ error: `failed to read tool result: ${msg}`, code: 'READ_ERROR' }, 500); + } + }); + + return app; +} diff --git a/apps/vis/server/src/routes/wire.ts b/apps/vis/server/src/routes/wire.ts new file mode 100644 index 000000000..4bd647900 --- /dev/null +++ b/apps/vis/server/src/routes/wire.ts @@ -0,0 +1,84 @@ +import { stat } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { Hono } from 'hono'; + +import { pathConfig } from '../config'; +import type { WireResponse } from '../lib/types'; +import { loadWireRecords } from '../lib/wire-loader'; +import { replayWire } from '../lib/wire-replay'; + +const SESSION_ID_RE = /^session_[a-zA-Z0-9_-]+$/; +const ARCHIVE_FILE_RE = /^wire\.\d+\.jsonl$/; + +export function wireRoute(): Hono { + const app = new Hono(); + + app.get('/:id/wire', async (c) => { + const id = c.req.param('id'); + if (!SESSION_ID_RE.test(id)) { + return c.json({ error: `invalid session id: ${id}`, code: 'BAD_REQUEST' }, 400); + } + const sessionDir = pathConfig.sessionDir(id); + try { + const dirStat = await stat(sessionDir); + if (!dirStat.isDirectory()) { + return c.json({ error: `session not found: ${id}`, code: 'NOT_FOUND' }, 404); + } + } catch { + return c.json({ error: `session not found: ${id}`, code: 'NOT_FOUND' }, 404); + } + + try { + const result = await loadWireRecords(sessionDir); + const body: WireResponse = { + session_id: id, + agent_id: null, + files_read: result.files_read, + health: result.health, + warnings: result.warnings, + records: result.records, + }; + if (result.broken_reason !== undefined) body.broken_reason = result.broken_reason; + return c.json(body); + } catch (error) { + const msg = error instanceof Error ? error.message : String(error); + return c.json({ error: `failed to load wire: ${msg}`, code: 'READ_ERROR' }, 500); + } + }); + + app.get('/:id/archives/:filename', async (c) => { + const id = c.req.param('id'); + const filename = c.req.param('filename'); + if (!SESSION_ID_RE.test(id)) { + return c.json({ error: `invalid session id: ${id}`, code: 'BAD_REQUEST' }, 400); + } + if (!ARCHIVE_FILE_RE.test(filename)) { + return c.json({ error: `invalid archive filename: ${filename}`, code: 'BAD_REQUEST' }, 400); + } + const archivePath = join(pathConfig.mainAgentDir(id), filename); + try { + await stat(archivePath); + } catch { + return c.json({ error: `archive not found: ${filename}`, code: 'NOT_FOUND' }, 404); + } + try { + const result = await replayWire(archivePath); + const body: WireResponse = { + session_id: id, + agent_id: null, + files_read: [archivePath], + health: result.health, + warnings: [...result.warnings], + records: result.records, + }; + if (result.brokenReason !== undefined) body.broken_reason = result.brokenReason; + return c.json(body); + } catch (error) { + const msg = error instanceof Error ? error.message : String(error); + return c.json({ error: `failed to replay archive: ${msg}`, code: 'READ_ERROR' }, 500); + } + }); + + return app; +} diff --git a/apps/vis/server/src/startup-banner.ts b/apps/vis/server/src/startup-banner.ts new file mode 100644 index 000000000..55f9dd41f --- /dev/null +++ b/apps/vis/server/src/startup-banner.ts @@ -0,0 +1,19 @@ +export interface StartupBannerOptions { + readonly authToken?: string; + readonly host: string; + readonly kimiCodeHome: string; + readonly port: number; +} + +export function formatStartupBanner(options: StartupBannerOptions): string { + const authStatus = options.authToken === undefined ? 'auth=disabled' : 'auth=required'; + return ( + `[vis-server] listening on http://${hostForUrl(options.host)}:${String(options.port)} ` + + `(${authStatus}, KIMI_CODE_HOME=${options.kimiCodeHome})\n` + ); +} + +function hostForUrl(host: string): string { + if (host.includes(':') && !host.startsWith('[')) return `[${host}]`; + return host; +} diff --git a/apps/vis/server/test/_fixture.ts b/apps/vis/server/test/_fixture.ts new file mode 100644 index 000000000..95cde6b18 --- /dev/null +++ b/apps/vis/server/test/_fixture.ts @@ -0,0 +1,251 @@ +import { randomBytes } from 'node:crypto'; +import { mkdirSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +/** Minimal synthetic session dir using the current atomic wire protocol. + * Real on-disk sessions under `~/.kimi-code/sessions/` use a legacy layout + * that can no longer be replayed, so tests use synthetic fixtures. */ +export interface SyntheticOptions { + withReminder?: boolean; + withTools?: boolean; + withSubagent?: boolean; +} + +export interface SyntheticResult { + dir: string; + sessionId: string; + cleanup(): void; +} + +export function createSyntheticSession(opts: SyntheticOptions = {}): SyntheticResult { + const sessionId = `session_${randomBytes(6).toString('hex')}`; + const dir = join(tmpdir(), `vis-test-${sessionId}`); + mkdirSync(dir, { recursive: true }); + + const lines: string[] = []; + let seq = 1; + const push = (rec: Record<string, unknown>): void => { + lines.push(JSON.stringify(rec)); + }; + + push({ + type: 'metadata', + protocol_version: '1.0', + created_at: Date.now(), + producer: { + kind: 'typescript', + name: '@moonshot-ai/agent-core', + version: '0.0.1', + }, + }); + + push({ + type: 'session_initialized', + seq: seq++, + time: Date.now(), + agent_type: 'main', + session_id: sessionId, + system_prompt: 'You are a test assistant.', + model: 'test-model', + active_tools: opts.withTools ? ['Bash'] : [], + permission_mode: 'default', + plan_mode: false, + workspace_dir: '/tmp', + }); + + push({ + type: 'turn_begin', + seq: seq++, + time: Date.now(), + turn_id: 'turn_1', + agent_type: 'main', + input_kind: 'user', + user_input: 'hi', + }); + + push({ + type: 'user_message', + seq: seq++, + time: Date.now(), + turn_id: 'turn_1', + content: 'hi', + }); + + if (opts.withReminder) { + push({ + type: 'system_reminder', + seq: seq++, + time: Date.now(), + content: 'test reminder', + consumed_at_turn: 1, + }); + } + + const stepUuid = 'step-1'; + push({ + type: 'step_begin', + seq: seq++, + time: Date.now(), + uuid: stepUuid, + turn_id: 'turn_1', + step: 0, + }); + push({ + type: 'content_part', + seq: seq++, + time: Date.now(), + uuid: 'cp-1', + turn_id: 'turn_1', + step: 0, + step_uuid: stepUuid, + role: 'assistant', + part: { kind: 'text', text: 'hello!' }, + }); + + if (opts.withTools) { + push({ + type: 'tool_call', + seq: seq++, + time: Date.now(), + uuid: 'tc-wire-1', + turn_id: 'turn_1', + step: 0, + step_uuid: stepUuid, + data: { + tool_call_id: 'tc_abc', + tool_name: 'Bash', + args: { cmd: 'echo hi' }, + }, + }); + } + + push({ + type: 'step_end', + seq: seq++, + time: Date.now(), + uuid: stepUuid, + turn_id: 'turn_1', + step: 0, + usage: { input_tokens: 10, output_tokens: 5 }, + }); + + if (opts.withTools) { + push({ + type: 'tool_result', + seq: seq++, + time: Date.now(), + turn_id: 'turn_1', + tool_call_id: 'tc_abc', + output: 'hi\n', + }); + } + + if (opts.withSubagent) { + push({ + type: 'subagent_spawned', + seq: seq++, + time: Date.now(), + data: { + agent_id: 'sub_test-1', + agent_name: 'coder', + parent_tool_call_id: 'tc_sub', + run_in_background: false, + }, + }); + // Write the subagent's own wire.jsonl + const subDir = join(dir, 'subagents', 'sub_test-1'); + mkdirSync(subDir, { recursive: true }); + const subLines: string[] = [ + JSON.stringify({ + type: 'metadata', + protocol_version: '1.0', + created_at: Date.now(), + producer: { + kind: 'typescript', + name: '@moonshot-ai/agent-core', + version: '0.0.1', + }, + }), + JSON.stringify({ + type: 'session_initialized', + seq: 1, + time: Date.now(), + agent_type: 'sub', + agent_id: 'sub_test-1', + agent_name: 'coder', + parent_session_id: sessionId, + parent_tool_call_id: 'tc_sub', + run_in_background: false, + system_prompt: 'You are a subagent.', + model: 'test-model', + active_tools: [], + permission_mode: 'default', + plan_mode: false, + workspace_dir: '/tmp', + }), + ]; + writeFileSync(join(subDir, 'wire.jsonl'), subLines.join('\n') + '\n'); + writeFileSync( + join(subDir, 'meta.json'), + JSON.stringify({ + agent_id: 'sub_test-1', + subagent_type: 'coder', + status: 'completed', + description: 'test subagent', + parent_tool_call_id: 'tc_sub', + created_at: Date.now() / 1000, + updated_at: Date.now() / 1000, + }), + ); + + push({ + type: 'subagent_completed', + seq: seq++, + time: Date.now(), + data: { + agent_id: 'sub_test-1', + parent_tool_call_id: 'tc_sub', + result_summary: 'all done', + usage: { input: 100, output: 50 }, + }, + }); + } + + push({ + type: 'turn_end', + seq: seq++, + time: Date.now(), + turn_id: 'turn_1', + agent_type: 'main', + success: true, + reason: 'done', + }); + + writeFileSync(join(dir, 'wire.jsonl'), lines.join('\n') + '\n'); + writeFileSync( + join(dir, 'state.json'), + JSON.stringify({ + session_id: sessionId, + model: 'test-model', + status: 'idle', + created_at: Date.now(), + updated_at: Date.now(), + workspace_dir: '/tmp', + // Required for listSessions() producer filter. + producer: { kind: 'typescript', name: '@moonshot-ai/agent-core', version: '0.0.1' }, + }), + ); + + return { + dir, + sessionId, + cleanup: () => { + try { + rmSync(dir, { recursive: true, force: true }); + } catch { + /* ignore */ + } + }, + }; +} diff --git a/apps/vis/server/test/app.test.ts b/apps/vis/server/test/app.test.ts new file mode 100644 index 000000000..23b4cbf33 --- /dev/null +++ b/apps/vis/server/test/app.test.ts @@ -0,0 +1,74 @@ +import { afterEach, describe, expect, it } from 'vitest'; + +import { createApp } from '../src/app'; +import { resolveHost, resolveVisAuthToken } from '../src/config'; +import { formatStartupBanner } from '../src/startup-banner'; + +describe('vis server access controls', () => { + const savedEnv = { ...process.env }; + + afterEach(() => { + process.env = { ...savedEnv }; + }); + + it('binds to loopback by default', () => { + delete process.env['VIS_HOST']; + delete process.env['HOST']; + + expect(resolveHost()).toBe('127.0.0.1'); + }); + + it('allows an explicit bind host', () => { + process.env['VIS_HOST'] = '0.0.0.0'; + + expect(resolveHost()).toBe('0.0.0.0'); + }); + + it('requires a token when binding outside loopback', () => { + delete process.env['VIS_AUTH_TOKEN']; + + expect(() => resolveVisAuthToken('0.0.0.0')).toThrow(/VIS_AUTH_TOKEN/); + + process.env['VIS_AUTH_TOKEN'] = 'secret-token'; + expect(resolveVisAuthToken('0.0.0.0')).toBe('secret-token'); + }); + + it('requires bearer auth for API routes when a token is configured', async () => { + const app = await createApp({ authToken: 'secret-token' }); + + const anonymous = await app.request('/api/sessions'); + expect(anonymous.status).toBe(401); + await expect(anonymous.json()).resolves.toMatchObject({ code: 'UNAUTHORIZED' }); + + const wrong = await app.request('/api/sessions', { + headers: { authorization: 'Bearer wrong-token' }, + }); + expect(wrong.status).toBe(401); + + const allowed = await app.request('/api/sessions', { + headers: { authorization: 'Bearer secret-token' }, + }); + expect(allowed.status).toBe(200); + }); + + it('keeps loopback-only local API access open when no token is configured', async () => { + const app = await createApp(); + + const response = await app.request('/api/sessions'); + + expect(response.status).toBe(200); + }); + + it('does not include the bearer token in the startup banner', () => { + const banner = formatStartupBanner({ + authToken: 'secret-token', + host: '127.0.0.1', + kimiCodeHome: '/tmp/kimi-code', + port: 3001, + }); + + expect(banner).toContain('auth=required'); + expect(banner).not.toContain('secret-token'); + expect(banner).not.toContain('token='); + }); +}); diff --git a/apps/vis/server/test/context-builder.test.ts b/apps/vis/server/test/context-builder.test.ts new file mode 100644 index 000000000..9823a2ccf --- /dev/null +++ b/apps/vis/server/test/context-builder.test.ts @@ -0,0 +1,242 @@ +import { afterEach, describe, expect, it } from 'vitest'; + +import { + buildAnnotatedMessages, + buildProjectedStateSummary, + extractPersistedOutputPath, + renderNotificationXml, +} from '../src/lib/context-builder'; +import type { NotificationRecord, SystemReminderRecord, VisWireRecord } from '../src/lib/types'; +import { loadWireRecords } from '../src/lib/wire-loader'; +import { createSyntheticSession, type SyntheticResult } from './_fixture'; + +describe('context-builder', () => { + let fixture: SyntheticResult | null = null; + afterEach(() => { + fixture?.cleanup(); + fixture = null; + }); + + it('extracts persisted-output path when marker is present', () => { + const text = + '<persisted-output path="/tmp/sessions/ses_foo/tool-results/call_1.txt">\nlorem ipsum\n</persisted-output>'; + expect(extractPersistedOutputPath(text)).toBe('/tmp/sessions/ses_foo/tool-results/call_1.txt'); + }); + + it('returns null when no persisted-output marker', () => { + expect(extractPersistedOutputPath('plain output')).toBeNull(); + }); + + it('renders notification XML with attributes and body', () => { + const data: NotificationRecord['data'] = { + id: 'n1', + category: 'task', + type: 'tick', + source_kind: 'background_task', + source_id: 'bg-1', + title: 'Test', + body: 'Hello', + severity: 'info', + targets: ['llm'], + }; + const xml = renderNotificationXml(data); + expect(xml).toContain('<notification id="n1"'); + expect(xml).toContain('category="task"'); + expect(xml).toContain('Title: Test'); + expect(xml).toContain('Severity: info'); + expect(xml).toContain('Hello'); + expect(xml).toContain('</notification>'); + }); + + it('includes synthetic system_reminder messages in the annotated stream', () => { + const reminder: SystemReminderRecord = { + type: 'system_reminder', + seq: 1, + time: 0, + content: 'please remember', + }; + const records: VisWireRecord[] = [reminder]; + const annotated = buildAnnotatedMessages(records); + expect(annotated).toHaveLength(1); + const m = annotated[0]; + expect(m?.is_ephemeral).toBe(true); + expect(m?.origin.kind).toBe('system_reminder'); + const content = m?.message.content[0]; + if (content !== undefined && 'text' in content) { + expect(String(content['text'])).toContain('<system-reminder>'); + } + }); + + it('includes llm-target notifications as ephemeral messages', () => { + const notif: NotificationRecord = { + type: 'notification', + seq: 2, + time: 0, + data: { + id: 'n2', + category: 'system', + type: 'info', + source_kind: 'system', + source_id: 's', + title: 't', + body: 'b', + severity: 'warning', + targets: ['llm'], + }, + }; + const annotated = buildAnnotatedMessages([notif]); + expect(annotated).toHaveLength(1); + const m = annotated[0]; + expect(m?.origin.kind).toBe('notification'); + if (m?.origin.kind === 'notification') { + expect(m.origin.severity).toBe('warning'); + } + expect(m?.is_ephemeral).toBe(true); + }); + + it('drops notifications without llm in targets', () => { + const notif: NotificationRecord = { + type: 'notification', + seq: 2, + time: 0, + data: { + id: 'n3', + category: 'system', + type: 'info', + source_kind: 's', + source_id: 's', + title: 't', + body: 'b', + severity: 'info', + targets: ['wire'], + }, + }; + const annotated = buildAnnotatedMessages([notif]); + expect(annotated).toHaveLength(0); + }); + + it('drops notifications whose delivered_at.llm === 0', () => { + const notif: NotificationRecord = { + type: 'notification', + seq: 2, + time: 0, + data: { + id: 'n4', + category: 'system', + type: 'info', + source_kind: 's', + source_id: 's', + title: 't', + body: 'b', + severity: 'info', + targets: ['llm'], + delivered_at: { llm: 0 }, + }, + }; + const annotated = buildAnnotatedMessages([notif]); + expect(annotated).toHaveLength(0); + }); + + it('marks rewind-orphaned messages as out_of_context', () => { + const records: VisWireRecord[] = [ + { + type: 'turn_begin', + seq: 1, + time: 0, + turn_id: 't1', + agent_type: 'main', + input_kind: 'user', + }, + { type: 'user_message', seq: 2, time: 0, turn_id: 't1', content: 'hi' }, + { + type: 'turn_begin', + seq: 3, + time: 0, + turn_id: 't2', + agent_type: 'main', + input_kind: 'user', + }, + { type: 'user_message', seq: 4, time: 0, turn_id: 't2', content: 'again' }, + { type: 'context_edit', seq: 5, time: 0, operation: 'rewind', to_turn: 1 }, + ]; + const annotated = buildAnnotatedMessages(records); + expect(annotated).toHaveLength(2); + expect(annotated[0]?.out_of_context).toBe(false); + expect(annotated[1]?.out_of_context).toBe(true); + }); + + it('resets messages on context_cleared', () => { + const records: VisWireRecord[] = [ + { + type: 'turn_begin', + seq: 1, + time: 0, + turn_id: 't1', + agent_type: 'main', + input_kind: 'user', + }, + { type: 'user_message', seq: 2, time: 0, turn_id: 't1', content: 'hi' }, + { type: 'context_cleared', seq: 3, time: 0 }, + { type: 'user_message', seq: 4, time: 0, turn_id: 't1', content: 'after' }, + ]; + const annotated = buildAnnotatedMessages(records); + expect(annotated).toHaveLength(1); + const content = annotated[0]?.message.content[0]; + if (content !== undefined && 'text' in content) { + expect(content['text']).toBe('after'); + } + }); + + it('sets persisted_output_path when tool output references a file', () => { + const records: VisWireRecord[] = [ + { + type: 'tool_result', + seq: 1, + time: 0, + turn_id: 't', + tool_call_id: 'Agent:1', + output: + '<persisted-output path="/tmp/ses_x/tool-results/Agent:1.txt">\npreview...\n</persisted-output>', + }, + ]; + const annotated = buildAnnotatedMessages(records); + expect(annotated[0]?.persisted_output_path).toBe('/tmp/ses_x/tool-results/Agent:1.txt'); + }); + + it('coalesces step_begin / content_part / tool_call / step_end into one assistant message', async () => { + fixture = createSyntheticSession({ withTools: true }); + const load = await loadWireRecords(fixture.dir); + const annotated = buildAnnotatedMessages(load.records); + const asst = annotated.filter((m) => m.origin.kind === 'assistant'); + expect(asst.length).toBe(1); + const m = asst[0]; + const text = m?.message.content.find((p) => p.type === 'text') as + | { type: 'text'; text: string } + | undefined; + expect(text?.text).toBe('hello!'); + expect(m?.message.tool_calls).toHaveLength(1); + expect(m?.message.tool_calls[0]?.function.name).toBe('Bash'); + }); + + it('builds correct origin tagging on a synthetic session with system_reminder', async () => { + fixture = createSyntheticSession({ withReminder: true }); + const load = await loadWireRecords(fixture.dir); + const annotated = buildAnnotatedMessages(load.records); + const hasReminder = annotated.some((m) => m.origin.kind === 'system_reminder'); + expect(hasReminder).toBe(true); + const hasUser = annotated.some((m) => m.origin.kind === 'user'); + const hasAssistant = annotated.some((m) => m.origin.kind === 'assistant'); + expect(hasUser).toBe(true); + expect(hasAssistant).toBe(true); + }); + + it('produces a projected state summary from a synthetic session', async () => { + fixture = createSyntheticSession({ withTools: true }); + const load = await loadWireRecords(fixture.dir); + const projected = buildProjectedStateSummary(load.records, load.session_initialized); + expect(typeof projected.last_seq).toBe('number'); + expect(typeof projected.token_count).toBe('number'); + expect(Array.isArray(projected.active_tools)).toBe(true); + expect(projected.model).toBe('test-model'); + }); +}); diff --git a/apps/vis/server/test/session-lister.test.ts b/apps/vis/server/test/session-lister.test.ts new file mode 100644 index 000000000..e2123e952 --- /dev/null +++ b/apps/vis/server/test/session-lister.test.ts @@ -0,0 +1,116 @@ +import { randomBytes } from 'node:crypto'; +import { mkdirSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +import { afterEach, describe, expect, it } from 'vitest'; + +import { listSessions, loadSessionSummary } from '../src/lib/session-lister'; +import { createSyntheticSession, type SyntheticResult } from './_fixture'; + +describe('session-lister', () => { + let fixture: SyntheticResult | null = null; + const fixtureRoots: string[] = []; + + afterEach(() => { + fixture?.cleanup(); + fixture = null; + // Cleanup enumeration roots from the second test + for (const root of fixtureRoots) { + try { + rmSync(root, { recursive: true, force: true }); + } catch { + /* ignore */ + } + } + fixtureRoots.length = 0; + }); + + it('loads a single SessionSummary from a synthetic session dir', async () => { + fixture = createSyntheticSession({ withSubagent: true }); + const summary = await loadSessionSummary(fixture.dir); + expect(summary.session_id).toBe(fixture.sessionId); + expect(summary.model).toBe('test-model'); + expect(summary.workspace_dir).toBe('/tmp'); + expect(summary.subagent_count).toBe(1); + expect(summary.wire_protocol_version).toBe('1.0'); + expect(summary.wire_record_count).toBeGreaterThan(0); + expect(summary.health).toBe('ok'); + }); + + it('enumerates session_* subdirs and sorts by updated_at desc', async () => { + const root = join(tmpdir(), `vis-list-${randomBytes(4).toString('hex')}`); + mkdirSync(root, { recursive: true }); + fixtureRoots.push(root); + + // Seed three synthetic sessions with staggered updated_at + for (let i = 0; i < 3; i += 1) { + const sessionId = `session_${randomBytes(6).toString('hex')}`; + const dir = join(root, sessionId); + mkdirSync(dir, { recursive: true }); + writeFileSync( + join(dir, 'wire.jsonl'), + JSON.stringify({ + type: 'metadata', + protocol_version: '1.0', + created_at: Date.now(), + producer: { kind: 'typescript', name: '@moonshot-ai/agent-core', version: '0.0.1' }, + }) + '\n', + ); + writeFileSync( + join(dir, 'state.json'), + JSON.stringify({ + session_id: sessionId, + model: 'test-model', + status: 'idle', + created_at: Date.now() - i * 1000, + updated_at: Date.now() - i * 1000, + workspace_dir: '/tmp', + producer: { kind: 'typescript', name: '@moonshot-ai/agent-core', version: '0.0.1' }, + }), + ); + } + + // Drop a non-session_* entry to verify it is ignored + writeFileSync(join(root, 'random-file.txt'), 'x'); + + // Drop a session_* dir with a non-TS producer — must be filtered out + const pySessionId = `session_${randomBytes(6).toString('hex')}`; + const pyDir = join(root, pySessionId); + mkdirSync(pyDir, { recursive: true }); + writeFileSync(join(pyDir, 'wire.jsonl'), '{}\n'); + writeFileSync( + join(pyDir, 'state.json'), + JSON.stringify({ + session_id: pySessionId, + created_at: Date.now(), + updated_at: Date.now(), + producer: { kind: 'python', name: 'kimi-cli', version: '0.9.0' }, + }), + ); + + // Drop a session_* dir with no state.json — must be filtered out + const oldSessionId = `session_${randomBytes(6).toString('hex')}`; + const oldDir = join(root, oldSessionId); + mkdirSync(oldDir, { recursive: true }); + writeFileSync(join(oldDir, 'wire.jsonl'), '{}\n'); + + const summaries = await listSessions(root); + expect(summaries.length).toBe(3); + // None of the filtered sessions should have leaked through. + expect(summaries.some((s) => s.session_id === pySessionId)).toBe(false); + expect(summaries.some((s) => s.session_id === oldSessionId)).toBe(false); + for (const s of summaries) { + expect(s.session_id.startsWith('session_')).toBe(true); + } + for (let i = 1; i < summaries.length; i += 1) { + const prev = summaries[i - 1]; + const cur = summaries[i]; + if (prev === undefined || cur === undefined) continue; + expect(prev.updated_at).toBeGreaterThanOrEqual(cur.updated_at); + } + for (const s of summaries) { + expect(['ok', 'broken', 'missing_wire']).toContain(s.health); + } + }); +}); diff --git a/apps/vis/server/test/subagent-loader.test.ts b/apps/vis/server/test/subagent-loader.test.ts new file mode 100644 index 000000000..978e621a8 --- /dev/null +++ b/apps/vis/server/test/subagent-loader.test.ts @@ -0,0 +1,47 @@ +import { afterEach, describe, expect, it } from 'vitest'; + +import { buildSubagentTree, listSubagents } from '../src/lib/subagent-loader'; +import { loadWireRecords } from '../src/lib/wire-loader'; +import { createSyntheticSession, type SyntheticResult } from './_fixture'; + +describe('subagent-loader', () => { + let fixture: SyntheticResult | null = null; + afterEach(() => { + fixture?.cleanup(); + fixture = null; + }); + + it('lists subagent ids for a session with subagents', async () => { + fixture = createSyntheticSession({ withSubagent: true }); + const ids = await listSubagents(fixture.dir); + expect(ids.length).toBe(1); + expect(ids[0]?.startsWith('sub_')).toBe(true); + }); + + it('builds a tree rooted at main', async () => { + fixture = createSyntheticSession({ withSubagent: true }); + const main = await loadWireRecords(fixture.dir); + const tree = await buildSubagentTree(fixture.dir, main.records); + expect(tree.length).toBe(1); + const node = tree[0]; + expect(node?.depth).toBe(0); + expect(node?.agent_id.startsWith('sub_')).toBe(true); + expect(node?.parent_agent_id).toBeNull(); + }); + + it('pairs spawn with completed lifecycle records', async () => { + fixture = createSyntheticSession({ withSubagent: true }); + const main = await loadWireRecords(fixture.dir); + const tree = await buildSubagentTree(fixture.dir, main.records); + const node = tree[0]; + expect(node?.status).toBe('completed'); + expect(node?.result_summary).toBe('all done'); + }); + + it('returns empty tree for sessions without subagents', async () => { + fixture = createSyntheticSession(); + const main = await loadWireRecords(fixture.dir); + const tree = await buildSubagentTree(fixture.dir, main.records); + expect(tree).toHaveLength(0); + }); +}); diff --git a/apps/vis/server/test/wire-loader.test.ts b/apps/vis/server/test/wire-loader.test.ts new file mode 100644 index 000000000..c1b3357d2 --- /dev/null +++ b/apps/vis/server/test/wire-loader.test.ts @@ -0,0 +1,558 @@ +import { writeFileSync } from 'node:fs'; +import { join } from 'node:path'; + +import { describe, expect, it, afterEach } from 'vitest'; + +import { listWireFilesManually, loadWireRecords } from '../src/lib/wire-loader'; +import { createSyntheticSession, type SyntheticResult } from './_fixture'; + +describe('wire-loader', () => { + let fixture: SyntheticResult | null = null; + afterEach(() => { + fixture?.cleanup(); + fixture = null; + }); + + it('lists wire.jsonl after any archives, current file last', async () => { + fixture = createSyntheticSession(); + const files = await listWireFilesManually(fixture.dir); + expect(files.length).toBeGreaterThan(0); + expect((files.at(-1) ?? '').endsWith('wire.jsonl')).toBe(true); + }); + + it('loads records from a synthetic session', async () => { + fixture = createSyntheticSession({ withTools: true }); + const result = await loadWireRecords(fixture.dir); + expect(result.health).toBe('ok'); + expect(result.records.length).toBeGreaterThan(0); + expect(result.files_read.length).toBe(1); + expect(result.session_initialized).not.toBeNull(); + expect(result.session_initialized?.agent_type).toBe('main'); + }); + + it('yields seq-ordered records', async () => { + fixture = createSyntheticSession({ withTools: true }); + const result = await loadWireRecords(fixture.dir); + for (let i = 1; i < result.records.length; i += 1) { + const prev = result.records[i - 1]; + const cur = result.records[i]; + if (prev === undefined || cur === undefined) continue; + expect(cur.seq).toBeGreaterThanOrEqual(prev.seq); + } + }); + + it('splices metadata + session_initialized into records so the Wire tab can show them', async () => { + fixture = createSyntheticSession(); + const result = await loadWireRecords(fixture.dir); + expect(result.health).toBe('ok'); + // metadata comes first, session_initialized second. + expect(result.records[0]?.type).toBe('metadata'); + expect(result.records[1]?.type).toBe('session_initialized'); + const meta = result.records[0] as { + protocol_version?: string; + producer?: { kind?: string }; + }; + expect(meta.protocol_version).toBe('1.0'); + expect(meta.producer?.kind).toBe('typescript'); + const init = result.records[1] as { system_prompt?: string }; + expect(init.system_prompt).toBe('You are a test assistant.'); + }); + + it('returns health=broken with a message for a non-existent dir', async () => { + const result = await loadWireRecords('/tmp/vis-test-does-not-exist-xxxxx'); + expect(result.health).toBe('broken'); + expect(result.records.length).toBe(0); + expect(result.broken_reason).toBeTruthy(); + expect(result.session_initialized).toBeNull(); + }); + + it('treats idle turn.steer as a turn start without splitting active steer', async () => { + fixture = createSyntheticSession(); + writeWire(fixture.dir, [ + { + type: 'metadata', + protocol_version: '1.0', + created_at: 1, + producer: { + kind: 'typescript', + name: '@moonshot-ai/agent-core', + version: '0.0.1', + }, + }, + { + type: 'session_initialized', + seq: 1, + time: 1, + agent_type: 'main', + session_id: fixture.sessionId, + system_prompt: 'You are a test assistant.', + model: 'test-model', + active_tools: [], + }, + { + type: 'turn.prompt', + seq: 2, + time: 2, + input: [{ type: 'text', text: 'first' }], + }, + { + type: 'context.user_message', + seq: 3, + time: 3, + content: [{ type: 'text', text: 'first' }], + }, + { + type: 'context.delta', + seq: 4, + time: 4, + event: { type: 'step.begin', uuid: 'step-1', turnId: '0', step: 1 }, + }, + { + type: 'turn.steer', + seq: 5, + time: 5, + input: [{ type: 'text', text: 'active steer' }], + }, + { + type: 'context.delta', + seq: 6, + time: 6, + event: { type: 'step.end', uuid: 'step-1', turnId: '0', step: 1, finishReason: 'tool_use' }, + }, + { + type: 'context.user_message', + seq: 7, + time: 7, + content: [{ type: 'text', text: 'active steer' }], + }, + { + type: 'context.delta', + seq: 8, + time: 8, + event: { type: 'step.begin', uuid: 'step-2', turnId: '0', step: 2 }, + }, + { + type: 'context.delta', + seq: 9, + time: 9, + event: { type: 'step.end', uuid: 'step-2', turnId: '0', step: 2, finishReason: 'end_turn' }, + }, + { + type: 'turn.steer', + seq: 10, + time: 10, + input: [{ type: 'text', text: 'terminal gap steer' }], + }, + { + type: 'context.user_message', + seq: 11, + time: 11, + content: [{ type: 'text', text: 'terminal gap steer' }], + }, + { + type: 'context.delta', + seq: 12, + time: 12, + event: { type: 'step.begin', uuid: 'step-3', turnId: '0', step: 3 }, + }, + { + type: 'context.delta', + seq: 13, + time: 13, + event: { type: 'step.end', uuid: 'step-3', turnId: '0', step: 3, finishReason: 'end_turn' }, + }, + { + type: 'turn.steer', + seq: 14, + time: 14, + input: [{ type: 'text', text: 'idle steer' }], + }, + { + type: 'context.user_message', + seq: 15, + time: 15, + content: [{ type: 'text', text: 'idle steer' }], + }, + { + type: 'context.delta', + seq: 16, + time: 16, + event: { type: 'step.begin', uuid: 'step-4', turnId: '1', step: 1 }, + }, + { + type: 'context.delta', + seq: 17, + time: 17, + event: { + type: 'tool.result', + parentUuid: 'tool-1', + toolCallId: 'tool-1', + result: { output: 'idle result' }, + }, + }, + ]); + + const result = await loadWireRecords(fixture.dir); + const turnBegins = result.records.filter((record) => record.type === 'turn_begin'); + const userMessages = result.records.filter((record) => record.type === 'user_message'); + const toolResult = result.records.find((record) => record.type === 'tool_result'); + + expect(turnBegins.map((record) => [record.turn_id, record.user_input])).toEqual([ + ['0', 'first'], + ['1', 'idle steer'], + ]); + expect(userMessages.map((record) => record.turn_id)).toEqual(['0', '0', '0', '1']); + expect(toolResult).toMatchObject({ turn_id: '1' }); + }); + + it('treats idle turn.steer after a failed pre-step turn as a new turn', async () => { + fixture = createSyntheticSession(); + writeWire(fixture.dir, [ + { + type: 'metadata', + protocol_version: '1.0', + created_at: 1, + producer: { + kind: 'typescript', + name: '@moonshot-ai/agent-core', + version: '0.0.1', + }, + }, + { + type: 'session_initialized', + seq: 1, + time: 1, + agent_type: 'main', + session_id: fixture.sessionId, + system_prompt: 'You are a test assistant.', + active_tools: [], + }, + { + type: 'turn.prompt', + seq: 2, + time: 2, + input: [{ type: 'text', text: 'missing model' }], + }, + { + type: 'context.user_message', + seq: 3, + time: 3, + content: [{ type: 'text', text: 'missing model' }], + }, + { + type: 'turn.steer', + seq: 4, + time: 4, + input: [{ type: 'text', text: 'after failure' }], + }, + { + type: 'context.user_message', + seq: 5, + time: 5, + content: [{ type: 'text', text: 'after failure' }], + }, + { + type: 'context.delta', + seq: 6, + time: 6, + event: { type: 'step.begin', uuid: 'step-1', turnId: '1', step: 1 }, + }, + ]); + + const result = await loadWireRecords(fixture.dir); + const turnBegins = result.records.filter((record) => record.type === 'turn_begin'); + const userMessages = result.records.filter((record) => record.type === 'user_message'); + + expect(turnBegins.map((record) => [record.turn_id, record.user_input])).toEqual([ + ['0', 'missing model'], + ['1', 'after failure'], + ]); + expect(userMessages.map((record) => record.turn_id)).toEqual(['0', '1']); + }); + it('unwraps the newer dot-namespaced record names (append_loop_event, append_message, set_active_tools)', async () => { + fixture = createSyntheticSession(); + writeWire(fixture.dir, [ + { + type: 'metadata', + protocol_version: '1.0', + created_at: 1, + producer: { kind: 'typescript', name: '@moonshot-ai/core', version: '0.0.1' }, + }, + { + type: 'session_initialized', + seq: 1, + time: 1, + agent_type: 'main', + session_id: fixture.sessionId, + system_prompt: 'You are a test assistant.', + active_tools: [], + }, + { + type: 'turn.prompt', + seq: 2, + time: 2, + input: [{ type: 'text', text: 'hi' }], + }, + { + type: 'context.append_message', + seq: 3, + time: 3, + message: { role: 'user', content: [{ type: 'text', text: 'hi' }] }, + }, + { + type: 'tools.set_active_tools', + seq: 4, + time: 4, + names: ['Read', 'Write', 'Bash'], + }, + { + type: 'context.append_loop_event', + seq: 5, + time: 5, + event: { type: 'step.begin', uuid: 'step-1', turnId: '0', step: 1 }, + }, + { + type: 'context.append_loop_event', + seq: 6, + time: 6, + event: { + type: 'tool.call', + uuid: 'tc-1', + turnId: '0', + step: 1, + stepUuid: 'step-1', + toolCallId: 'call-1', + name: 'Write', + args: { path: '/tmp/x', content: 'hello' }, + }, + }, + { + type: 'context.append_loop_event', + seq: 7, + time: 7, + event: { type: 'step.end', uuid: 'step-1', turnId: '0', step: 1, finishReason: 'tool_use' }, + }, + ]); + + const result = await loadWireRecords(fixture.dir); + expect(result.health).toBe('ok'); + const types = result.records.map((r) => r.type); + expect(types).toContain('turn_begin'); + expect(types).toContain('user_message'); + expect(types).toContain('tools_changed'); + expect(types).toContain('step_begin'); + expect(types).toContain('tool_call'); + expect(types).toContain('step_end'); + expect(types).not.toContain('notification'); + + const toolCall = result.records.find((r) => r.type === 'tool_call'); + expect(toolCall).toMatchObject({ turn_id: '0', data: { tool_name: 'Write' } }); + const toolsChanged = result.records.find((r) => r.type === 'tools_changed'); + expect(toolsChanged).toMatchObject({ operation: 'set_active', tools: ['Read', 'Write', 'Bash'] }); + }); + + // The newer `context.append_message` producer carries assistant / + // system roles too. Without a role filter, inferSteerStartedTurnId + // would see any append_message in the lookahead window and conclude + // that the steer launched a fresh turn, fabricating a turn id. + it('ignores non-user append_message when inferring whether a turn.steer launched a fresh turn', async () => { + fixture = createSyntheticSession(); + writeWire(fixture.dir, [ + { + type: 'metadata', + protocol_version: '1.0', + created_at: 1, + producer: { kind: 'typescript', name: '@moonshot-ai/core', version: '0.0.1' }, + }, + { + type: 'session_initialized', + seq: 1, + time: 1, + agent_type: 'main', + session_id: fixture.sessionId, + system_prompt: 'You are a test assistant.', + active_tools: [], + }, + { + type: 'turn.prompt', + seq: 2, + time: 2, + input: [{ type: 'text', text: 'first' }], + }, + { + type: 'context.append_message', + seq: 3, + time: 3, + message: { role: 'user', content: [{ type: 'text', text: 'first' }] }, + }, + { + type: 'context.append_loop_event', + seq: 4, + time: 4, + event: { type: 'step.begin', uuid: 'step-1', turnId: '0', step: 1 }, + }, + // Steer arrives during an active turn (no later loop event reveals + // a new turnId, no later user message follows). Only an assistant + // append_message lands in the lookahead window. + { + type: 'turn.steer', + seq: 5, + time: 5, + input: [{ type: 'text', text: 'buffered steer' }], + }, + { + type: 'context.append_message', + seq: 6, + time: 6, + message: { role: 'assistant', content: [{ type: 'text', text: 'thinking out loud' }] }, + }, + ]); + + const result = await loadWireRecords(fixture.dir); + const turnBegins = result.records.filter((record) => record.type === 'turn_begin'); + // Only the original turn.prompt should produce a turn_begin. The + // steer must stay buffered because the lookahead contains no user + // message — assistant append_messages don't count. + expect(turnBegins.map((record) => record.turn_id)).toEqual(['0']); + }); + + // ContextMemory.appendSystemReminder() persists injected reminders / + // skill activations / system triggers as role: 'user' messages for the + // LLM, but their origin.kind is not 'user'. Replay must not surface + // these as user_message records or count them when inferring whether + // a turn.steer launched a fresh turn. + it('does not surface non-user-origin append_message as user_message', async () => { + fixture = createSyntheticSession(); + writeWire(fixture.dir, [ + { + type: 'metadata', + protocol_version: '1.0', + created_at: 1, + producer: { kind: 'typescript', name: '@moonshot-ai/core', version: '0.0.1' }, + }, + { + type: 'session_initialized', + seq: 1, + time: 1, + agent_type: 'main', + session_id: fixture.sessionId, + system_prompt: 'You are a test assistant.', + active_tools: [], + }, + { + type: 'turn.prompt', + seq: 2, + time: 2, + input: [{ type: 'text', text: 'hi' }], + }, + { + type: 'context.append_message', + seq: 3, + time: 3, + message: { + role: 'user', + content: [{ type: 'text', text: 'hi' }], + origin: { kind: 'user' }, + }, + }, + // System reminder: role: 'user' for the LLM, but origin.kind says + // it's an injection, not a real user message. + { + type: 'context.append_message', + seq: 4, + time: 4, + message: { + role: 'user', + content: [{ type: 'text', text: '<system-reminder>be careful</system-reminder>' }], + origin: { kind: 'injection', variant: 'safety' }, + }, + }, + // Skill activation payload: also role: 'user' under the hood. + { + type: 'context.append_message', + seq: 5, + time: 5, + message: { + role: 'user', + content: [{ type: 'text', text: 'skill bootstrap' }], + origin: { kind: 'skill_activation', activationId: 'a1', skillName: 'plan', trigger: 'user-slash' }, + }, + }, + ]); + + const result = await loadWireRecords(fixture.dir); + const userMessages = result.records.filter((record) => record.type === 'user_message'); + // Only the real user message at seq 3 should surface — the injection + // and skill_activation entries must not show as user_message. + expect(userMessages.map((record) => record.seq)).toEqual([3]); + }); + + it('ignores non-user-origin append_message when inferring whether a turn.steer launched a fresh turn', async () => { + fixture = createSyntheticSession(); + writeWire(fixture.dir, [ + { + type: 'metadata', + protocol_version: '1.0', + created_at: 1, + producer: { kind: 'typescript', name: '@moonshot-ai/core', version: '0.0.1' }, + }, + { + type: 'session_initialized', + seq: 1, + time: 1, + agent_type: 'main', + session_id: fixture.sessionId, + system_prompt: 'You are a test assistant.', + active_tools: [], + }, + { + type: 'turn.prompt', + seq: 2, + time: 2, + input: [{ type: 'text', text: 'first' }], + }, + { + type: 'context.append_message', + seq: 3, + time: 3, + message: { role: 'user', content: [{ type: 'text', text: 'first' }], origin: { kind: 'user' } }, + }, + { + type: 'context.append_loop_event', + seq: 4, + time: 4, + event: { type: 'step.begin', uuid: 'step-1', turnId: '0', step: 1 }, + }, + { + type: 'turn.steer', + seq: 5, + time: 5, + input: [{ type: 'text', text: 'buffered steer' }], + }, + // Injected system reminder lands in the lookahead window. It has + // role: 'user' but origin.kind: 'system_trigger', so it must NOT + // count as evidence of a user-launched fresh turn. + { + type: 'context.append_message', + seq: 6, + time: 6, + message: { + role: 'user', + content: [{ type: 'text', text: '<system-reminder>focus</system-reminder>' }], + origin: { kind: 'system_trigger', name: 'focus-reminder' }, + }, + }, + ]); + + const result = await loadWireRecords(fixture.dir); + const turnBegins = result.records.filter((record) => record.type === 'turn_begin'); + expect(turnBegins.map((record) => record.turn_id)).toEqual(['0']); + }); +}); + +function writeWire(dir: string, records: readonly Record<string, unknown>[]): void { + writeFileSync( + join(dir, 'wire.jsonl'), + `${records.map((record) => JSON.stringify(record)).join('\n')}\n`, + ); +} diff --git a/apps/vis/server/tsconfig.json b/apps/vis/server/tsconfig.json new file mode 100644 index 000000000..1d7a4c2a9 --- /dev/null +++ b/apps/vis/server/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../../tsconfig.json", + "include": ["src", "test"] +} diff --git a/apps/vis/server/tsdown.config.ts b/apps/vis/server/tsdown.config.ts new file mode 100644 index 000000000..6e618ca51 --- /dev/null +++ b/apps/vis/server/tsdown.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'tsdown'; + +export default defineConfig({ + entry: { server: 'src/index.ts' }, + format: ['esm'], + outDir: 'dist', + clean: true, + external: ['@moonshot-ai/agent-core', '@moonshot-ai/kosong', '@moonshot-ai/kaos'], +}); diff --git a/apps/vis/server/vitest.config.ts b/apps/vis/server/vitest.config.ts new file mode 100644 index 000000000..0131797f1 --- /dev/null +++ b/apps/vis/server/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + name: 'vis-server', + include: ['test/**/*.test.ts'], + }, +}); diff --git a/apps/vis/web/index.html b/apps/vis/web/index.html new file mode 100644 index 000000000..070aafc9b --- /dev/null +++ b/apps/vis/web/index.html @@ -0,0 +1,39 @@ +<!doctype html> +<html lang="en"> + <head> + <meta charset="UTF-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" /> + <meta name="color-scheme" content="light dark" /> + <meta name="theme-color" content="#0b0d12" /> + <title>kimi vis + + + + + + + +
+ + + diff --git a/apps/vis/web/package.json b/apps/vis/web/package.json new file mode 100644 index 000000000..2146e568f --- /dev/null +++ b/apps/vis/web/package.json @@ -0,0 +1,39 @@ +{ + "name": "@moonshot-ai/vis-web", + "version": "0.1.1", + "private": true, + "license": "MIT", + "type": "module", + "imports": { + "#/*": { + "types": [ + "./src/*.ts", + "./src/*.tsx", + "./src/*/index.ts", + "./src/*/index.tsx" + ], + "default": "./src/*" + } + }, + "scripts": { + "dev": "vite", + "build": "vite build", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@tanstack/react-query": "^5.74.4", + "@tanstack/react-virtual": "^3.13.5", + "react": "^19.1.0", + "react-dom": "^19.1.0", + "react-router-dom": "^7.5.2" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.1.4", + "@types/react": "^19.1.2", + "@types/react-dom": "^19.1.2", + "@vitejs/plugin-react": "^4.4.1", + "tailwindcss": "^4.1.4", + "typescript": "6.0.2", + "vite": "^6.3.3" + } +} diff --git a/apps/vis/web/src/App.tsx b/apps/vis/web/src/App.tsx new file mode 100644 index 000000000..4843b677c --- /dev/null +++ b/apps/vis/web/src/App.tsx @@ -0,0 +1,20 @@ +import { Route, Routes } from 'react-router-dom'; +import { AppShell } from './components/layout/AppShell'; +import { SessionListPage } from './pages/SessionListPage'; +import { SessionDetailPage } from './pages/SessionDetailPage'; +import { SubagentDetailPage } from './pages/SubagentDetailPage'; + +export function App() { + return ( + + + } /> + } /> + } + /> + + + ); +} diff --git a/apps/vis/web/src/api.ts b/apps/vis/web/src/api.ts new file mode 100644 index 000000000..66346694a --- /dev/null +++ b/apps/vis/web/src/api.ts @@ -0,0 +1,119 @@ +import type { + SessionSummary, + SessionDetail, + WireResponse, + ContextResponse, + SubagentTreeResponse, + SubagentMetaResponse, + ToolResultFileResponse, + DeleteSessionResponse, + ClearSessionsResponse, + ApiError, +} from './types'; + +const TOKEN_STORAGE_KEY = 'kimi-vis-auth-token'; + +function readTokenParam(raw: string): string | null { + const trimmed = raw.replace(/^[#?]/, ''); + if (trimmed.length === 0) return null; + const params = new URLSearchParams(trimmed); + return params.get('token') ?? params.get('vis_token'); +} + +function deleteTokenParams(params: URLSearchParams): boolean { + const hadToken = params.has('token') || params.has('vis_token'); + params.delete('token'); + params.delete('vis_token'); + return hadToken; +} + +function scrubTokenFromUrl(): void { + const url = new URL(window.location.href); + const changedSearch = deleteTokenParams(url.searchParams); + const hash = url.hash.replace(/^#/, ''); + let changedHash = false; + if (hash.length > 0) { + const hashParams = new URLSearchParams(hash); + changedHash = deleteTokenParams(hashParams); + if (changedHash) { + const nextHash = hashParams.toString(); + url.hash = nextHash.length > 0 ? nextHash : ''; + } + } + if (changedSearch || changedHash) { + window.history.replaceState(null, '', url.toString()); + } +} + +function authToken(): string | null { + if (typeof window === 'undefined') return null; + const fromHash = readTokenParam(window.location.hash); + const fromSearch = readTokenParam(window.location.search); + const token = fromHash ?? fromSearch; + if (token !== null && token.length > 0) { + window.localStorage.setItem(TOKEN_STORAGE_KEY, token); + scrubTokenFromUrl(); + return token; + } + return window.localStorage.getItem(TOKEN_STORAGE_KEY); +} + +async function request(path: string, method: 'GET' | 'DELETE'): Promise { + const headers: Record = { accept: 'application/json' }; + const token = authToken(); + if (token !== null && token.length > 0) { + headers['authorization'] = `Bearer ${token}`; + } + const res = await fetch(path, { method, headers }); + if (!res.ok) { + let err: ApiError | null = null; + try { + err = (await res.json()) as ApiError; + } catch { + /* ignore */ + } + throw new Error(err?.error ?? `HTTP ${res.status} ${res.statusText}`); + } + return (await res.json()) as T; +} + +function get(path: string): Promise { + return request(path, 'GET'); +} + +function del(path: string): Promise { + return request(path, 'DELETE'); +} + +const enc = encodeURIComponent; + +export const api = { + listSessions: () => get('/api/sessions'), + + getSession: (id: string) => get(`/api/sessions/${enc(id)}`), + + deleteSession: (id: string) => del(`/api/sessions/${enc(id)}`), + + clearSessions: () => del('/api/sessions'), + + getWire: (id: string) => get(`/api/sessions/${enc(id)}/wire`), + + getContext: (id: string) => get(`/api/sessions/${enc(id)}/context`), + + getSubagents: (id: string) => get(`/api/sessions/${enc(id)}/subagents`), + + getSubagentWire: (id: string, agentId: string) => + get(`/api/sessions/${enc(id)}/subagents/${enc(agentId)}/wire`), + + getSubagentContext: (id: string, agentId: string) => + get(`/api/sessions/${enc(id)}/subagents/${enc(agentId)}/context`), + + getSubagentMeta: (id: string, agentId: string) => + get(`/api/sessions/${enc(id)}/subagents/${enc(agentId)}/meta`), + + getToolResult: (id: string, toolCallId: string) => + get(`/api/sessions/${enc(id)}/tool-results/${enc(toolCallId)}`), + + getArchive: (id: string, filename: string) => + get(`/api/sessions/${enc(id)}/archives/${enc(filename)}`), +}; diff --git a/apps/vis/web/src/components/context/CompactionRibbon.tsx b/apps/vis/web/src/components/context/CompactionRibbon.tsx new file mode 100644 index 000000000..ca0a0f8cb --- /dev/null +++ b/apps/vis/web/src/components/context/CompactionRibbon.tsx @@ -0,0 +1,28 @@ +interface CompactionRibbonProps { + summary: string; + seq: number; +} + +/** + * Horizontal ribbon marker where a compaction occurred in the message stream. + * Derivation is best-effort from the annotated message stream; if a compaction + * was applied, buildAnnotatedMessages emits a summary assistant message. We + * detect the prior "break" via seq discontinuity, but for now we render a + * stand-alone banner wherever the caller places it. + */ +export function CompactionRibbon({ summary, seq }: CompactionRibbonProps) { + return ( +
+ + + ⏪ compacted · seq {seq} + + + {summary ? ( +
+
{summary}
+
+ ) : null} +
+ ); +} diff --git a/apps/vis/web/src/components/context/ContextTab.tsx b/apps/vis/web/src/components/context/ContextTab.tsx new file mode 100644 index 000000000..cedb1fcfd --- /dev/null +++ b/apps/vis/web/src/components/context/ContextTab.tsx @@ -0,0 +1,281 @@ +import { useState } from 'react'; + +import type { AnnotatedMessage, ProjectedStateSummary } from '../../types'; +import { Pill } from '../shared/Pill'; +import { formatBytes } from '../shared/SizePreview'; +import { EphemeralBubble } from './EphemeralBubble'; +import { MessageBubble } from './MessageBubble'; + +interface ContextTabProps { + sessionId: string; + messages: AnnotatedMessage[]; + projectedState: ProjectedStateSummary; +} + +export function ContextTab({ sessionId, messages, projectedState }: ContextTabProps) { + const [hideOutOfContext, setHideOutOfContext] = useState(false); + + const visible = hideOutOfContext ? messages.filter((m) => !m.out_of_context) : messages; + + const stats = countKinds(messages); + + return ( +
+ {/* Header strip */} +
+ {messages.length} + messages + · + model + {projectedState.model ?? 'unknown'} + · + + + + + + + + + +
+ {/* Token stacked bar — 2px hairline under the header. Replaces the + * border-b so the bar itself is the separator. */} + + + {/* Message stream */} +
+
+ + {visible.length === 0 ? ( +
+ no messages — session has only lifecycle/config records so far. +
+ ) : ( + visible.map((m) => { + if (m.is_ephemeral) { + return ; + } + return ; + }) + )} +
+
+
+ ); +} + +function SystemPromptBubble({ text }: { text: string | null }) { + const [open, setOpen] = useState(false); + const hasText = text !== null && text.length > 0; + + if (!hasText) { + return ( +
+
+ + system + + (no system prompt) +
+
+ ); + } + + return ( +
+ +
+
+          {text}
+        
+ {!open ? ( + +
+ ); +} + +function LegendDot({ color, label, count }: { color: string; label: string; count: number }) { + if (count === 0) return null; + return ( + + + {label} + {count} + + ); +} + +// ─── token breakdown (input / output / cache_read / cache_write) ─── +// Colors are chosen from the existing semantic palette so the bar reads +// coherently with the rest of the app: +// cache_read = success (saved / cached — the "good" share) +// input = info (billed input) +// output = assistant (what the model produced) +// cache_write = warning (billed once, amortised next call) + +const TOK_COLORS = { + cache_read: 'var(--color-sev-success)', + input: 'var(--color-sev-info)', + output: 'var(--color-assistant)', + cache_write: 'var(--color-sev-warning)', +} as const; + +interface TokenBreakdown { + input_tokens: number; + output_tokens: number; + cache_read_tokens: number; + cache_write_tokens: number; +} + +function formatTokens(n: number): string { + if (n < 1000) return String(n); + if (n < 10_000) return (n / 1000).toFixed(1) + 'K'; + if (n < 1_000_000) return Math.round(n / 1000) + 'K'; + return (n / 1_000_000).toFixed(1) + 'M'; +} + +function TokenDots({ breakdown }: { breakdown: TokenBreakdown }) { + const { input_tokens, output_tokens, cache_read_tokens, cache_write_tokens } = breakdown; + const billedIn = input_tokens + cache_read_tokens; + const hitPct = billedIn > 0 ? Math.round((cache_read_tokens / billedIn) * 100) : 0; + const hasAny = + input_tokens > 0 || output_tokens > 0 || cache_read_tokens > 0 || cache_write_tokens > 0; + if (!hasAny) { + return (no tokens yet); + } + return ( + + + + + {cache_write_tokens > 0 ? ( + + ) : null} + {cache_read_tokens > 0 ? ({hitPct}% hit) : null} + + ); +} + +function TokenDot({ color, label, value }: { color: string; label: string; value: number }) { + return ( + + + {formatTokens(value)} + {label} + + ); +} + +/** 2px stacked bar that visually shows the 4-way token composition. + * Proportions use (input + output + cache_read + cache_write) as the + * total so cache_read's share is honest (it's not in the "billed" + * tokenCount but it's real work done on the request). */ +function TokenBar({ breakdown }: { breakdown: TokenBreakdown }) { + const { input_tokens, output_tokens, cache_read_tokens, cache_write_tokens } = breakdown; + const total = input_tokens + output_tokens + cache_read_tokens + cache_write_tokens; + if (total === 0) { + return
; + } + const seg = (n: number) => (n / total) * 100; + return ( +
+ {cache_read_tokens > 0 ? ( +
+ ) : null} + {input_tokens > 0 ? ( +
+ ) : null} + {output_tokens > 0 ? ( +
+ ) : null} + {cache_write_tokens > 0 ? ( +
+ ) : null} +
+ ); +} + +function countKinds(messages: AnnotatedMessage[]) { + let user = 0, + assistant = 0, + tool = 0, + reminder = 0, + notif = 0; + for (const m of messages) { + switch (m.origin.kind) { + case 'user': + user++; + break; + case 'assistant': + assistant++; + break; + case 'tool': + tool++; + break; + case 'system_reminder': + reminder++; + break; + case 'notification': + notif++; + break; + } + } + return { user, assistant, tool, reminder, notif }; +} diff --git a/apps/vis/web/src/components/context/EphemeralBubble.tsx b/apps/vis/web/src/components/context/EphemeralBubble.tsx new file mode 100644 index 000000000..17a555592 --- /dev/null +++ b/apps/vis/web/src/components/context/EphemeralBubble.tsx @@ -0,0 +1,101 @@ +import type { AnnotatedMessage } from '../../types'; +import { Pill } from '../shared/Pill'; + +interface EphemeralBubbleProps { + message: AnnotatedMessage; +} + +/** + * Ephemeral-injected message (system_reminder or notification). + * Rendered with dashed border + inset — unambiguous signal that this + * is NOT a real user turn. + */ +export function EphemeralBubble({ message }: EphemeralBubbleProps) { + const { origin } = message; + if (origin.kind === 'system_reminder') { + return ; + } + if (origin.kind === 'notification') { + return ; + } + return null; +} + +function SystemReminderBubble({ m }: { m: AnnotatedMessage }) { + const text = extractText(m); + // Strip XML wrapper for display (show just the inner content) + const inner = text + .replace(/^\s*\n?/, '') + .replace(/\n?<\/system-reminder>\s*$/, ''); + + return ( +
+
+ system_reminder + + injected @ seq {m.seq} + +
+
+        {inner}
+      
+
+ ); +} + +function NotificationBubble({ m, severity }: { m: AnnotatedMessage; severity: string }) { + const text = extractText(m); + // Strip wrapper + const inner = text + .replace(/^\s*]*>\n?/, '') + .replace(/\n?<\/notification>\s*$/, ''); + const tone = + severity === 'error' + ? 'error' + : severity === 'warning' + ? 'warning' + : severity === 'success' + ? 'success' + : 'info'; + const borderClass = + severity === 'error' + ? 'ephemeral-border-error' + : severity === 'warning' + ? 'ephemeral-border-warning' + : severity === 'success' + ? 'ephemeral-border-success' + : 'ephemeral-border-info'; + + return ( +
+
+ notification + {severity} + + injected @ seq {m.seq} + +
+
+        {inner}
+      
+
+ ); +} + +function extractText(m: AnnotatedMessage): string { + const p = m.message.content.find((x) => x.type === 'text'); + return p ? ((p['text'] as string | undefined) ?? '') : ''; +} diff --git a/apps/vis/web/src/components/context/MessageBubble.tsx b/apps/vis/web/src/components/context/MessageBubble.tsx new file mode 100644 index 000000000..953ad3058 --- /dev/null +++ b/apps/vis/web/src/components/context/MessageBubble.tsx @@ -0,0 +1,193 @@ +import { useState, type ReactNode } from 'react'; +import type { AnnotatedMessage, ContentPart, ToolCallEntry } from '../../types'; +import { Pill } from '../shared/Pill'; +import { PersistedOutputLink } from './PersistedOutputLink'; + +interface MessageBubbleProps { + message: AnnotatedMessage; + sessionId: string; +} + +export function MessageBubble({ message, sessionId }: MessageBubbleProps) { + const { role } = message.message; + if (role === 'user') return ; + if (role === 'assistant') return ; + return ; +} + +function baseClass(out: boolean): string { + return [ + 'relative flex max-w-full min-w-0 flex-col border-l-[3px] bg-surface-1 px-3 py-2', + out ? 'opacity-50 line-through decoration-[var(--color-sev-error)] decoration-dashed' : '', + ].join(' '); +} + +function UserBubble({ m }: { m: AnnotatedMessage }) { + return ( +
+
+ user + seq {m.seq} + {m.out_of_context ? out-of-context : null} +
+ +
+ ); +} + +function AssistantBubble({ m }: { m: AnnotatedMessage }) { + const thinkPart = m.message.content.find((p) => p.type === 'think'); + const think = thinkPart ? (thinkPart['think'] as string | undefined) : undefined; + const textParts = m.message.content.filter((p) => p.type !== 'think'); + return ( +
+
+ assistant + seq {m.seq} + {think ? think : null} + {m.message.tool_calls.length > 0 ? ( + + {m.message.tool_calls.length} tool call{m.message.tool_calls.length > 1 ? 's' : ''} + + ) : null} + {m.out_of_context ? out-of-context : null} +
+ {think ? : null} + + {m.message.tool_calls.length > 0 ? ( +
+ {m.message.tool_calls.map((tc) => )} +
+ ) : null} +
+ ); +} + +function ToolBubble({ m, sessionId }: { m: AnnotatedMessage; sessionId: string }) { + const firstTextPart = m.message.content.find((p) => p.type === 'text'); + const text = firstTextPart ? ((firstTextPart['text'] as string | undefined) ?? '') : ''; + const hasPersisted = !!m.persisted_output_path && m.message.tool_call_id; + return ( +
+
+ tool + {m.message.tool_call_id ? ( + + call {m.message.tool_call_id.slice(0, 12)} + + ) : null} + seq {m.seq} + {m.out_of_context ? out-of-context : null} +
+ {hasPersisted ? ( + + ) : ( +
+          {text}
+        
+ )} +
+ ); +} + +function ThinkBlock({ text }: { text: string }) { + const [open, setOpen] = useState(false); + return ( +
+ + {open ? ( +
+          {text}
+        
+ ) : null} +
+ ); +} + +function ToolCallCard({ call }: { call: ToolCallEntry }) { + const [open, setOpen] = useState(false); + const argsStr = call.function.arguments ?? ''; + return ( +
+ + {open ? ( +
+          {prettyJson(argsStr)}
+        
+ ) : null} +
+ ); +} + +function MessageContent({ parts }: { parts: readonly ContentPart[] }): ReactNode { + return ( +
+ {parts.map((p, i) => { + if (p.type === 'text') { + return ( +
+              {(p['text'] as string) ?? ''}
+            
+ ); + } + if (p.type === 'image_url') { + const url = (p['image_url'] as { url?: string } | undefined)?.url; + return
[image: {url ?? '—'}]
; + } + if (p.type === 'video_url') { + const url = (p['video_url'] as { url?: string } | undefined)?.url; + return
[video: {url ?? '—'}]
; + } + return ( +
+ [{p.type}] +
+ ); + })} +
+ ); +} + +function truncate(s: string, n: number): string { + return s.length <= n ? s : s.slice(0, n) + '…'; +} + +function prettyJson(s: string): string { + try { + return JSON.stringify(JSON.parse(s), null, 2); + } catch { + return s; + } +} diff --git a/apps/vis/web/src/components/context/PersistedOutputLink.tsx b/apps/vis/web/src/components/context/PersistedOutputLink.tsx new file mode 100644 index 000000000..598a11b17 --- /dev/null +++ b/apps/vis/web/src/components/context/PersistedOutputLink.tsx @@ -0,0 +1,53 @@ +import { useState } from 'react'; +import { useToolResult } from '../../hooks/useContext'; +import { formatBytes } from '../shared/SizePreview'; + +interface PersistedOutputLinkProps { + sessionId: string; + toolCallId: string; + path: string; +} + +export function PersistedOutputLink({ + sessionId, + toolCallId, + path, +}: PersistedOutputLinkProps) { + const [load, setLoad] = useState(false); + const { data, isLoading, error } = useToolResult(sessionId, toolCallId, load); + const basename = path.split('/').pop() ?? path; + + return ( +
+ + {load ? ( +
+ {isLoading ? ( +
loading…
+ ) : error ? ( +
+ {(error).message} +
+ ) : data ? ( +
+              {data.content}
+            
+ ) : null} +
+ ) : null} +
+ ); +} diff --git a/apps/vis/web/src/components/files/FilesTab.tsx b/apps/vis/web/src/components/files/FilesTab.tsx new file mode 100644 index 000000000..f3bae940c --- /dev/null +++ b/apps/vis/web/src/components/files/FilesTab.tsx @@ -0,0 +1,202 @@ +import { useState } from 'react'; +import { Link } from 'react-router-dom'; + +import { useToolResult } from '../../hooks/useContext'; +import { useArchive } from '../../hooks/useWire'; +import type { SessionDetail } from '../../types'; +import { Drawer } from '../shared/Drawer'; +import { Pill } from '../shared/Pill'; +import { formatBytes } from '../shared/SizePreview'; +import { WireTab } from '../wire/WireTab'; + +interface FilesTabProps { + sessionId: string; + detail: SessionDetail; +} + +export function FilesTab({ sessionId, detail }: FilesTabProps) { + const [openArchive, setOpenArchive] = useState(null); + const [openResult, setOpenResult] = useState(null); + + return ( +
+ {/* Archives */} +
+

+ wire archives · {detail.archive_files.length} +

+ {detail.archive_files.length === 0 ? ( +

no compaction archives

+ ) : ( +
    + {detail.archive_files.map((f) => ( +
  • + + archive + + {f} + +
  • + ))} +
+ )} +
+ + {/* External tool results */} +
+

+ external tool results · {detail.tool_result_ids.length} +

+ {detail.tool_result_ids.length === 0 ? ( +

none

+ ) : ( +
    + {detail.tool_result_ids.map((id) => ( +
  • + + file + + {id}.txt + +
  • + ))} +
+ )} +
+ + {/* Subagent directories — click a tile to drill into that subagent's + own wire + context + meta. Mirrors the Subagents tab tree but in + a flat, filesystem-ish view. */} +
+

+ subagent directories · {detail.subagent_ids.length} +

+ {detail.subagent_ids.length === 0 ? ( +

none

+ ) : ( +
    + {detail.subagent_ids.map((id) => ( +
  • + +
    + + subagent + + + {id.replace(/^sub_/, '').slice(0, 12)} + + + open → + +
    +
    {id}
    + +
  • + ))} +
+ )} +
+ + {openArchive ? ( + { setOpenArchive(null); }} + /> + ) : null} + {openResult ? ( + { setOpenResult(null); }} + /> + ) : null} +
+ ); +} + +function ArchiveDrawer({ + sessionId, + filename, + onClose, +}: { + sessionId: string; + filename: string; + onClose: () => void; +}) { + const { data, isLoading, error } = useArchive(sessionId, filename); + return ( + archive · {filename}} width={900}> + {isLoading ? ( +
loading…
+ ) : error ? ( +
+ {(error).message} +
+ ) : data ? ( + + ) : null} +
+ ); +} + +function ToolResultDrawer({ + sessionId, + toolCallId, + onClose, +}: { + sessionId: string; + toolCallId: string; + onClose: () => void; +}) { + const { data, isLoading, error } = useToolResult(sessionId, toolCallId, true); + return ( + + tool-result · {toolCallId}.txt + {data ? {formatBytes(data.size_bytes)} : null} + + } + width={780} + > + {isLoading ? ( +
loading…
+ ) : error ? ( +
+ {(error).message} +
+ ) : data ? ( +
+          {data.content}
+        
+ ) : null} +
+ ); +} diff --git a/apps/vis/web/src/components/layout/AppShell.tsx b/apps/vis/web/src/components/layout/AppShell.tsx new file mode 100644 index 000000000..1d7fbeea2 --- /dev/null +++ b/apps/vis/web/src/components/layout/AppShell.tsx @@ -0,0 +1,138 @@ +import type { ReactNode } from 'react'; +import { useQueryClient } from '@tanstack/react-query'; +import { Link } from 'react-router-dom'; +import { SessionRail } from '../sessions/SessionRail'; +import { useTheme, type ThemeChoice, type ResolvedTheme } from '../../hooks/useTheme'; + +interface AppShellProps { + children: ReactNode; +} + +export function AppShell({ children }: AppShellProps) { + const qc = useQueryClient(); + const { choice, resolved, cycle } = useTheme(); + + return ( +
+
+ + + + kimi vis + + + debug · local files + + +
+ + +
+
+
+ +
{children}
+
+
+ ); +} + +function ThemeToggle({ + choice, + resolved, + onCycle, +}: { + choice: ThemeChoice; + resolved: ResolvedTheme; + onCycle: () => void; +}) { + const label = choice === 'auto' ? `auto · ${resolved}` : choice; + const title = `Theme: ${label}. Click to cycle (auto → light → dark → auto).`; + return ( + + ); +} + +function LogoMark() { + return ( + + ); +} + +function RefreshIcon() { + return ( + + ); +} + +function SunIcon() { + return ( + + ); +} + +function MoonIcon() { + return ( + + ); +} + +function AutoIcon() { + return ( + + ); +} diff --git a/apps/vis/web/src/components/layout/TabBar.tsx b/apps/vis/web/src/components/layout/TabBar.tsx new file mode 100644 index 000000000..6f2d42564 --- /dev/null +++ b/apps/vis/web/src/components/layout/TabBar.tsx @@ -0,0 +1,56 @@ +import { useSearchParams } from 'react-router-dom'; + +export interface TabSpec { + id: string; + label: string; + count?: number | null; +} + +interface TabBarProps { + tabs: TabSpec[]; + defaultTab: string; +} + +export function TabBar({ tabs, defaultTab }: TabBarProps) { + const [search, setSearch] = useSearchParams(); + const active = search.get('tab') ?? defaultTab; + + return ( +
+ {tabs.map((t) => { + const on = t.id === active; + return ( + + ); + })} +
+ ); +} + +export function useActiveTab(defaultTab: string): string { + const [search] = useSearchParams(); + return search.get('tab') ?? defaultTab; +} diff --git a/apps/vis/web/src/components/sessions/SessionCard.tsx b/apps/vis/web/src/components/sessions/SessionCard.tsx new file mode 100644 index 000000000..590c158a3 --- /dev/null +++ b/apps/vis/web/src/components/sessions/SessionCard.tsx @@ -0,0 +1,124 @@ +import type { MouseEvent } from 'react'; +import { useParams, Link } from 'react-router-dom'; +import type { SessionSummary } from '../../types'; +import { formatRelativeTime } from '../../util/time'; +import { Pill } from '../shared/Pill'; + +interface SessionCardProps { + session: SessionSummary; + onDelete: (session: SessionSummary) => void; + deleting: boolean; +} + +export function SessionCard({ session, onDelete, deleting }: SessionCardProps) { + const { sessionId } = useParams<{ sessionId: string }>(); + const selected = sessionId === session.session_id; + const dirty = session.last_exit_code === 'dirty'; + const workspaceLabel = session.workspace_dir + ? session.workspace_dir.split('/').slice(-2).join('/') + : '(no workspace)'; + const shortId = session.session_id.replace(/^session_/, '').slice(0, 10); + const title = session.title ?? session.custom_title; + + function handleDeleteClick(e: MouseEvent) { + e.preventDefault(); + e.stopPropagation(); + onDelete(session); + } + + return ( +
+ {selected ? ( + + ) : null} + +
+
+ + {shortId} + {session.model ? ( + + {session.model} + + ) : null} + {session.archived ? ( + + archived + + ) : null} +
+ + {formatRelativeTime(session.updated_at)} + +
+
+ + {workspaceLabel} + + + {session.wire_record_count}ev + + {session.subagent_count > 0 ? ( + + {session.subagent_count}sub + + ) : null} + {session.archive_count > 0 ? ( + + {session.archive_count}arch + + ) : null} + {session.health !== 'ok' ? ( + + {session.health} + + ) : null} +
+ {title ? ( +
+ {title} +
+ ) : null} + {session.last_prompt ? ( +
+ prompt · {session.last_prompt} +
+ ) : null} + + +
+ ); +} + +function TrashIcon() { + return ( + + ); +} diff --git a/apps/vis/web/src/components/sessions/SessionFilter.tsx b/apps/vis/web/src/components/sessions/SessionFilter.tsx new file mode 100644 index 000000000..79581c5bb --- /dev/null +++ b/apps/vis/web/src/components/sessions/SessionFilter.tsx @@ -0,0 +1,127 @@ +import type { SessionSortKey, HealthFilter } from './SessionRail'; + +interface SessionFilterProps { + search: string; + onSearchChange: (v: string) => void; + showArchived: boolean; + onShowArchivedChange: (v: boolean) => void; + sortKey: SessionSortKey; + onSortChange: (v: SessionSortKey) => void; + healthFilter: HealthFilter; + onHealthChange: (v: HealthFilter) => void; + totalCount: number; + filteredCount: number; + onClearSessions: () => void; + clearDisabled: boolean; + clearBusy: boolean; +} + +const SORT_OPTIONS: { value: SessionSortKey; label: string }[] = [ + { value: 'recent', label: 'recent' }, + { value: 'oldest', label: 'oldest' }, + { value: 'most_records', label: 'most records' }, + { value: 'most_subagents', label: 'most subagents' }, +]; + +const HEALTH_OPTIONS: { value: HealthFilter; label: string }[] = [ + { value: 'all', label: 'any' }, + { value: 'ok', label: 'ok' }, + { value: 'broken', label: 'broken' }, + { value: 'missing_wire', label: 'no wire' }, +]; + +export function SessionFilter({ + search, + onSearchChange, + showArchived, + onShowArchivedChange, + sortKey, + onSortChange, + healthFilter, + onHealthChange, + totalCount, + filteredCount, + onClearSessions, + clearDisabled, + clearBusy, +}: SessionFilterProps) { + return ( +
+
+ { onSearchChange(e.target.value); }} + placeholder="search id / title / workspace" + className="w-full border border-border bg-surface-0 px-2 py-1 font-mono text-[12px] text-fg-0 placeholder:text-fg-3 focus:border-border-strong focus:outline-none" + /> +
+
+ + +
+
+ +
+ + {filteredCount} / {totalCount} + + +
+
+
+ ); +} + +function TrashIcon() { + return ( + + ); +} diff --git a/apps/vis/web/src/components/sessions/SessionRail.tsx b/apps/vis/web/src/components/sessions/SessionRail.tsx new file mode 100644 index 000000000..2a73817d6 --- /dev/null +++ b/apps/vis/web/src/components/sessions/SessionRail.tsx @@ -0,0 +1,188 @@ +import { useMemo, useState } from 'react'; +import { useNavigate, useParams } from 'react-router-dom'; + +import { useClearSessions, useDeleteSession, useSessions } from '../../hooks/useSession'; +import type { SessionSummary } from '../../types'; +import { SessionCard } from './SessionCard'; +import { SessionFilter } from './SessionFilter'; + +export type SessionSortKey = 'recent' | 'oldest' | 'most_records' | 'most_subagents'; +export type HealthFilter = 'all' | 'ok' | 'broken' | 'missing_wire'; + +function workspaceKey(s: SessionSummary): string { + if (!s.workspace_dir) return '(no workspace)'; + return s.workspace_dir.split('/').slice(-2).join('/'); +} + +function sortSessions(sessions: readonly SessionSummary[], key: SessionSortKey): SessionSummary[] { + switch (key) { + case 'recent': + return sessions.toSorted((a, b) => b.updated_at - a.updated_at); + case 'oldest': + return sessions.toSorted((a, b) => a.created_at - b.created_at); + case 'most_records': + return sessions.toSorted((a, b) => b.wire_record_count - a.wire_record_count); + case 'most_subagents': + return sessions.toSorted((a, b) => b.subagent_count - a.subagent_count); + } +} + +export function SessionRail() { + const { data, isLoading, error } = useSessions(); + const deleteSession = useDeleteSession(); + const clearSessions = useClearSessions(); + const navigate = useNavigate(); + const { sessionId } = useParams<{ sessionId: string }>(); + const [search, setSearch] = useState(''); + const [showArchived, setShowArchived] = useState(false); + const [sortKey, setSortKey] = useState('recent'); + const [healthFilter, setHealthFilter] = useState('all'); + + const filtered = useMemo(() => { + if (!data) return []; + const q = search.trim().toLowerCase(); + return data.filter((s) => { + if (!showArchived && s.archived) return false; + if (healthFilter !== 'all' && s.health !== healthFilter) return false; + if (!q) return true; + const hay = [ + s.session_id, + s.title ?? '', + s.last_prompt ?? '', + s.custom_title ?? '', + s.workspace_dir ?? '', + s.model ?? '', + ...s.tags, + ] + .join(' ') + .toLowerCase(); + return hay.includes(q); + }); + }, [data, search, showArchived, healthFilter]); + + // Grouping-by-workspace only makes sense when sorted by time — for any + // other sort the user asked for a particular total ordering across + // sessions, so we honour it and display a flat list. + const grouped = useMemo(() => { + if (sortKey !== 'recent') return null; + const map = new Map(); + for (const s of filtered) { + const k = workspaceKey(s); + const existing = map.get(k); + if (existing === undefined) { + map.set(k, [s]); + } else { + existing.push(s); + } + } + return [...map.entries()] + .map(([group, items]) => { + const sorted = items.toSorted((a, b) => b.updated_at - a.updated_at); + return [group, sorted] as const; + }) + .toSorted(([, a], [, b]) => { + const ua = a[0]?.updated_at ?? 0; + const ub = b[0]?.updated_at ?? 0; + return ub - ua; + }); + }, [filtered, sortKey]); + + const flat = useMemo( + () => (grouped === null ? sortSessions(filtered, sortKey) : null), + [filtered, sortKey, grouped], + ); + + async function handleDeleteSession(session: SessionSummary) { + const label = session.title ?? session.last_prompt ?? session.session_id; + if (!window.confirm(`Delete session "${label}"?\n\nThis removes its files from KIMI_CODE_HOME.`)) { + return; + } + try { + await deleteSession.mutateAsync(session.session_id); + if (sessionId === session.session_id) { + void navigate('/'); + } + } catch (deleteError) { + window.alert(deleteError instanceof Error ? deleteError.message : String(deleteError)); + } + } + + async function handleClearSessions() { + const total = data?.length ?? 0; + if (total === 0) return; + if (!window.confirm(`Clear all ${total} sessions shown by vis?\n\nThis removes their files from KIMI_CODE_HOME.`)) { + return; + } + try { + const result = await clearSessions.mutateAsync(); + void navigate('/'); + if (result.failed.length > 0) { + window.alert(`Deleted ${result.deleted_count} sessions; ${result.failed.length} failed.`); + } + } catch (clearError) { + window.alert(clearError instanceof Error ? clearError.message : String(clearError)); + } + } + + return ( + + ); +} diff --git a/apps/vis/web/src/components/shared/CopyButton.tsx b/apps/vis/web/src/components/shared/CopyButton.tsx new file mode 100644 index 000000000..d287dd044 --- /dev/null +++ b/apps/vis/web/src/components/shared/CopyButton.tsx @@ -0,0 +1,28 @@ +import { useState } from 'react'; + +interface CopyButtonProps { + value: string; + label?: string; + className?: string; +} + +export function CopyButton({ value, label = 'copy', className = '' }: CopyButtonProps) { + const [state, setState] = useState<'idle' | 'ok' | 'err'>('idle'); + + return ( + + ); +} diff --git a/apps/vis/web/src/components/shared/Drawer.tsx b/apps/vis/web/src/components/shared/Drawer.tsx new file mode 100644 index 000000000..55312b785 --- /dev/null +++ b/apps/vis/web/src/components/shared/Drawer.tsx @@ -0,0 +1,50 @@ +import { useEffect, type ReactNode } from 'react'; + +interface DrawerProps { + open: boolean; + onClose: () => void; + title?: ReactNode; + children: ReactNode; + width?: number; +} + +export function Drawer({ open, onClose, title, children, width = 560 }: DrawerProps) { + useEffect(() => { + if (!open) return; + const handler = (e: KeyboardEvent) => { + if (e.key === 'Escape') onClose(); + }; + window.addEventListener('keydown', handler); + return () =>{ window.removeEventListener('keydown', handler); }; + }, [open, onClose]); + + if (!open) return null; + + return ( +
+ + ); +} diff --git a/apps/vis/web/src/components/shared/JsonViewer.tsx b/apps/vis/web/src/components/shared/JsonViewer.tsx new file mode 100644 index 000000000..2e134740f --- /dev/null +++ b/apps/vis/web/src/components/shared/JsonViewer.tsx @@ -0,0 +1,155 @@ +import { useState, memo } from 'react'; + +interface JsonViewerProps { + value: unknown; + /** Default-open nesting depth */ + defaultOpenDepth?: number; +} + +export function JsonViewer({ value, defaultOpenDepth = 2 }: JsonViewerProps) { + return ( +
+ +
+ ); +} + +interface NodeProps { + value: unknown; + depth: number; + defaultOpenDepth: number; + keyPath: string; + keyLabel?: string | number; + isLast?: boolean; +} + +const Node = memo(function Node({ + value, + depth, + defaultOpenDepth, + keyPath, + keyLabel, + isLast, +}: NodeProps) { + const [open, setOpen] = useState(depth < defaultOpenDepth); + + if (value === null) + return ; + if (value === undefined) + return ; + if (typeof value === 'boolean') + return ; + if (typeof value === 'number') + return ; + if (typeof value === 'string') { + const short = value.length <= 200; + return ( + + ); + } + if (Array.isArray(value)) { + if (value.length === 0) + return ; + return ( +
+ + {open ? ( +
+ {value.map((v, i) => ( + + ))} +
+ ) : null} +
+ ); + } + if (typeof value === 'object') { + const entries = Object.entries(value as Record); + if (entries.length === 0) + return ; + return ( +
+ + {open ? ( +
+ {entries.map(([k, v], i) => ( + + ))} +
+ ) : null} +
+ ); + } + return ; +}); + +function Leaf({ + keyLabel, + repr, + color, +}: { + keyLabel?: string | number; + repr: string; + color: string; + isLast?: boolean; +}) { + return ( +
+ + {keyLabel !== undefined ? ( + <> + {keyLabel} + : + + ) : null} + {repr} +
+ ); +} diff --git a/apps/vis/web/src/components/shared/Pill.tsx b/apps/vis/web/src/components/shared/Pill.tsx new file mode 100644 index 000000000..58180f871 --- /dev/null +++ b/apps/vis/web/src/components/shared/Pill.tsx @@ -0,0 +1,78 @@ +import type { ReactNode } from 'react'; + +export type PillTone = + | 'conversation' + | 'config' + | 'lifecycle' + | 'subagent' + | 'approval' + | 'ephemeral' + | 'meta' + | 'tools' + | 'user' + | 'assistant' + | 'tool' + | 'compaction' + | 'turn' + | 'info' + | 'success' + | 'warning' + | 'error' + | 'neutral'; + +const TONE_VAR: Record = { + conversation: '--color-cat-conversation', + config: '--color-cat-config', + lifecycle: '--color-cat-lifecycle', + subagent: '--color-cat-subagent', + approval: '--color-cat-approval', + ephemeral: '--color-cat-ephemeral', + meta: '--color-cat-meta', + tools: '--color-cat-tools', + user: '--color-user', + assistant: '--color-assistant', + tool: '--color-tool', + compaction: '--color-compaction', + turn: '--color-turn', + info: '--color-sev-info', + success: '--color-sev-success', + warning: '--color-sev-warning', + error: '--color-sev-error', + neutral: '--color-fg-2', +}; + +interface PillProps { + tone?: PillTone; + variant?: 'solid' | 'soft' | 'outline'; + children: ReactNode; + title?: string; + className?: string; +} + +export function Pill({ + tone = 'neutral', + variant = 'soft', + children, + title, + className = '', +}: PillProps) { + const color = `var(${TONE_VAR[tone]})`; + const style = + variant === 'solid' + ? { backgroundColor: color, color: 'var(--color-on-accent)' } + : variant === 'outline' + ? { border: `1px solid ${color}`, color } + : { + backgroundColor: `color-mix(in oklab, ${color} 18%, transparent)`, + color, + }; + return ( + + {children} + + ); +} diff --git a/apps/vis/web/src/components/shared/SizePreview.tsx b/apps/vis/web/src/components/shared/SizePreview.tsx new file mode 100644 index 000000000..aa3e4ef88 --- /dev/null +++ b/apps/vis/web/src/components/shared/SizePreview.tsx @@ -0,0 +1,55 @@ +import { useState, type ReactNode } from 'react'; + +interface SizePreviewProps { + label?: string; + /** Byte/char count for the dim label */ + sizeBytes: number; + /** Preview text shown when collapsed (first ~200 chars) */ + preview?: string; + /** Full content renderer */ + children: ReactNode; + /** Start expanded if true */ + defaultOpen?: boolean; +} + +export function SizePreview({ + label = 'payload', + sizeBytes, + preview, + children, + defaultOpen = false, +}: SizePreviewProps) { + const [open, setOpen] = useState(defaultOpen); + const size = formatBytes(sizeBytes); + return ( +
+ + {open ? ( +
+ {children} +
+ ) : null} +
+ ); +} + +export function formatBytes(n: number): string { + if (n < 1024) return `${n}B`; + if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)}KB`; + return `${(n / 1024 / 1024).toFixed(2)}MB`; +} diff --git a/apps/vis/web/src/components/state/StateTab.tsx b/apps/vis/web/src/components/state/StateTab.tsx new file mode 100644 index 000000000..39a105d7e --- /dev/null +++ b/apps/vis/web/src/components/state/StateTab.tsx @@ -0,0 +1,217 @@ +import { useState, type ReactNode } from 'react'; + +import type { SessionState } from '../../types'; +import { formatAbsoluteTime, formatRelativeTime } from '../../util/time'; +import { CopyButton } from '../shared/CopyButton'; +import { JsonViewer } from '../shared/JsonViewer'; + +interface StateTabProps { + state: SessionState; + protocolVersion: string | null; +} + +/** Keys whose values should be interpreted as epoch milliseconds and + * decorated with both an absolute time and "2h ago" style relative label. + * Matches the shape of state.json written by `@moonshot-ai/agent-core`. */ +const EPOCH_MS_KEYS = new Set(['created_at', 'updated_at', 'last_turn_time']); + +/** Keys whose values tend to be long paths worth copying with one click. */ +const COPY_KEYS = new Set(['workspace_dir', 'plan_slug', 'session_id']); + +export function StateTab({ state, protocolVersion }: StateTabProps) { + const [showRaw, setShowRaw] = useState(false); + const entries = Object.entries(state); + + return ( +
+
+
+ state.json + {protocolVersion ? ` · wire protocol ${protocolVersion}` : ''} +
+
+ + +
+
+ + + +
+ + + {entries.map(([k, v]) => ( + + ))} + +
+
+ + {showRaw ? ( +
+ +
+ ) : null} +
+ ); +} + +/** Derive a human duration from created_at + updated_at when both are + * valid epoch-ms numbers. Rendered just under the header as a single dim + * line — omitted silently when data is missing. */ +function DurationRow({ state }: { state: SessionState }) { + const created = state.created_at; + const updated = state.updated_at; + if (typeof created !== 'number' || typeof updated !== 'number') return null; + if (updated < created) return null; + const ms = updated - created; + return ( +
+ duration {formatDuration(ms)} + {' · '} + from {formatAbsoluteTime(created)} + {' → '} + {formatAbsoluteTime(updated)} +
+ ); +} + +function StateRow({ keyName, value }: { keyName: string; value: unknown }) { + return ( + + + {keyName} + + + + + + ); +} + +function ValueCell({ keyName, value }: { keyName: string; value: unknown }) { + if (value === null || value === undefined) return null; + + if (typeof value === 'boolean') { + return ( + + {String(value)} + + ); + } + + if (typeof value === 'number') { + if (EPOCH_MS_KEYS.has(keyName) && value > 1_000_000_000_000) { + return ( + + {formatAbsoluteTime(value)} + · + {formatRelativeTime(value)} + · + {value} + + ); + } + return {value}; + } + + if (typeof value === 'string') { + const withCopy = COPY_KEYS.has(keyName); + return ( + + "{value}" + {withCopy ? : null} + + ); + } + + if (Array.isArray(value)) { + if (value.length === 0) return []; + // Arrays of primitives render inline; arrays of objects use JsonViewer. + const allPrim = value.every((v) => typeof v !== 'object' || v === null); + if (allPrim) { + return ( + + {(value as unknown[]).map((v, i) => ( + + {typeof v === 'string' ? v : String(v)} + + ))} + + ); + } + return ; + } + + // Nested object: render as a compact sub-table so fields like `producer` + // read as structured data instead of a JSON.stringify blob. + return } />; +} + +function NestedObject({ value }: { value: Record }) { + const entries = Object.entries(value); + if (entries.length === 0) return {'{}'}; + return ( +
+ + + {entries.map(([k, v]) => ( + + + + + ))} + +
{k} + +
+
+ ); +} + +function NestedValue({ value }: { value: unknown }): ReactNode { + if (value === null || value === undefined) return null; + if (typeof value === 'boolean') { + return ( + + {String(value)} + + ); + } + if (typeof value === 'number') + return {value}; + if (typeof value === 'string') + return "{value}"; + if (Array.isArray(value)) { + return ( + + [{value.length} item{value.length === 1 ? '' : 's'}] + + ); + } + return } />; +} + +/** Format a duration in ms as "3h 14m" / "12m 30s" / "45s". */ +function formatDuration(ms: number): string { + if (ms < 1000) return `${ms}ms`; + const s = Math.round(ms / 1000); + if (s < 60) return `${s}s`; + const m = Math.floor(s / 60); + const rs = s % 60; + if (m < 60) return rs > 0 ? `${m}m ${rs}s` : `${m}m`; + const h = Math.floor(m / 60); + const rm = m % 60; + if (h < 24) return rm > 0 ? `${h}h ${rm}m` : `${h}h`; + const d = Math.floor(h / 24); + const rh = h % 24; + return rh > 0 ? `${d}d ${rh}h` : `${d}d`; +} diff --git a/apps/vis/web/src/components/subagents/SubagentNode.tsx b/apps/vis/web/src/components/subagents/SubagentNode.tsx new file mode 100644 index 000000000..4fedaa485 --- /dev/null +++ b/apps/vis/web/src/components/subagents/SubagentNode.tsx @@ -0,0 +1,92 @@ +import { Link, useParams } from 'react-router-dom'; +import type { SubagentNode as SubagentNodeT } from '../../types'; +import { Pill, type PillTone } from '../shared/Pill'; +import { formatRelativeTime } from '../../util/time'; + +const STATUS_TONE: Record = { + running: 'info', + completed: 'success', + failed: 'error', + killed: 'neutral', + lost: 'warning', + unknown: 'neutral', +}; + +interface Props { + node: SubagentNodeT; + sessionId: string; +} + +export function SubagentNode({ node, sessionId }: Props) { + const { agentId: activeAgentId } = useParams<{ agentId?: string }>(); + const selected = activeAgentId === node.agent_id; + const shortId = node.agent_id.replace(/^sub_/, '').slice(0, 10); + + return ( +
+ + +
+
+ + {node.subagent_type ?? 'unknown'} + + {shortId} + + {node.status} + + {node.run_in_background ? ( + bg + ) : null} + + depth {node.depth} · {formatRelativeTime(node.spawn_time)} + +
+ {node.agent_name ? ( +
+ {node.agent_name} +
+ ) : null} + {node.result_summary ? ( +
+ {node.result_summary} +
+ ) : null} + {node.error ? ( +
+ {node.error} +
+ ) : null} +
+ + {node.children.length > 0 ? ( +
+ {node.children.map((c) => ( + + ))} +
+ ) : null} +
+ ); +} + +const STATUS_COLOR_VAR: Record = { + running: '--color-sev-info', + completed: '--color-sev-success', + failed: '--color-sev-error', + killed: '--color-fg-3', + lost: '--color-sev-warning', + unknown: '--color-fg-3', +}; diff --git a/apps/vis/web/src/components/subagents/SubagentTree.tsx b/apps/vis/web/src/components/subagents/SubagentTree.tsx new file mode 100644 index 000000000..40f8e7f20 --- /dev/null +++ b/apps/vis/web/src/components/subagents/SubagentTree.tsx @@ -0,0 +1,24 @@ +import type { SubagentNode as SubagentNodeT } from '../../types'; +import { SubagentNode } from './SubagentNode'; + +interface SubagentTreeProps { + tree: SubagentNodeT[]; + sessionId: string; +} + +export function SubagentTree({ tree, sessionId }: SubagentTreeProps) { + if (tree.length === 0) { + return ( +
+ no subagents spawned in this session +
+ ); + } + return ( +
+ {tree.map((node) => ( + + ))} +
+ ); +} diff --git a/apps/vis/web/src/components/subagents/SubagentsTab.tsx b/apps/vis/web/src/components/subagents/SubagentsTab.tsx new file mode 100644 index 000000000..1f9879dfa --- /dev/null +++ b/apps/vis/web/src/components/subagents/SubagentsTab.tsx @@ -0,0 +1,28 @@ +import { useSubagents } from '../../hooks/useSubagents'; +import { SubagentTree } from './SubagentTree'; + +interface SubagentsTabProps { + sessionId: string; +} + +export function SubagentsTab({ sessionId }: SubagentsTabProps) { + const { data, isLoading, error } = useSubagents(sessionId); + + if (isLoading) { + return
loading subagents…
; + } + if (error) { + return ( +
+ {(error).message} +
+ ); + } + if (!data) return null; + + return ( +
+ +
+ ); +} diff --git a/apps/vis/web/src/components/wire/IssuesDrawer.tsx b/apps/vis/web/src/components/wire/IssuesDrawer.tsx new file mode 100644 index 000000000..d7cbd1d78 --- /dev/null +++ b/apps/vis/web/src/components/wire/IssuesDrawer.tsx @@ -0,0 +1,144 @@ +import { useEffect } from 'react'; + +import type { Issue, IssueSeverity } from '../../lib/issues'; + +interface IssuesDrawerProps { + issues: Issue[]; + onClose: () => void; + onJumpTo?: (seq: number) => void; + /** Optional predicate: "is this seq currently visible under the active + * filter?". When provided, jump buttons for filtered-out seqs are + * disabled and flagged. */ + isSeqVisible?: (seq: number) => boolean; +} + +const SEV_COLOR: Record = { + error: 'var(--color-sev-error)', + warning: 'var(--color-sev-warning)', + info: 'var(--color-sev-info)', +}; + +const KIND_LABEL: Record = { + subagent_failed: 'subagent failed', + tool_error: 'tool error', + tool_denied: 'tool denied', + turn_failed: 'turn failed', + step_truncated: 'step truncated', + wire_warning: 'wire warning', +}; + +export function IssuesDrawer({ issues, onClose, onJumpTo, isSeqVisible }: IssuesDrawerProps) { + // ESC closes — standard drawer affordance. + useEffect(() => { + const onKey = (e: KeyboardEvent) => { + if (e.key === 'Escape') onClose(); + }; + window.addEventListener('keydown', onKey); + return () =>{ window.removeEventListener('keydown', onKey); }; + }, [onClose]); + + return ( + <> + {/* Backdrop — click outside drawer to dismiss. Subtle tint so the + underlying Wire timeline is still readable. */} + + +
+ {issues.length === 0 ? ( +
no issues detected
+ ) : ( +
    + {issues.map((iss, i) => ( + + ))} +
+ )} +
+ + + ); +} + +function IssueItem({ + issue, + onJumpTo, + onClose, + isSeqVisible, +}: { + issue: Issue; + onJumpTo?: (seq: number) => void; + onClose: () => void; + isSeqVisible?: (seq: number) => boolean; +}) { + const color = SEV_COLOR[issue.severity]; + const seq = issue.seq; + const hidden = seq !== null && isSeqVisible !== undefined && !isSeqVisible(seq); + const canJump = seq !== null && onJumpTo !== undefined && !hidden; + return ( +
  • +
    +
    +
    {issue.summary}
    + {issue.detail !== undefined ? ( +
    {issue.detail}
    + ) : null} +
  • + ); +} diff --git a/apps/vis/web/src/components/wire/TypeBadge.tsx b/apps/vis/web/src/components/wire/TypeBadge.tsx new file mode 100644 index 000000000..fbd1af1cb --- /dev/null +++ b/apps/vis/web/src/components/wire/TypeBadge.tsx @@ -0,0 +1,20 @@ +import type { WireRecordType } from '../../types'; +import { Pill } from '../shared/Pill'; +import { TYPE_ICON, TYPE_TONE } from './typeMeta'; + +interface TypeBadgeProps { + type: WireRecordType; +} + +export function TypeBadge({ type }: TypeBadgeProps) { + const icon = TYPE_ICON[type]; + const tone = TYPE_TONE[type]; + return ( + + + {type} + + ); +} diff --git a/apps/vis/web/src/components/wire/WireHeadline.tsx b/apps/vis/web/src/components/wire/WireHeadline.tsx new file mode 100644 index 000000000..2ff6126ea --- /dev/null +++ b/apps/vis/web/src/components/wire/WireHeadline.tsx @@ -0,0 +1,733 @@ +import type { ReactNode } from 'react'; + +import { useFocus, type FocusKind } from '../../lib/focus-context'; +import type { VisWireRecord } from '../../types'; +import { Pill } from '../shared/Pill'; +import { formatBytes } from '../shared/SizePreview'; + +export interface HeadlineRender { + /** Main headline content — rendered in the flex-grow slot of the row */ + main: ReactNode; + /** Right-side badges / pair refs */ + right?: ReactNode; +} + +function truncate(s: unknown, n: number): string { + let str: string; + if (s === null || s === undefined) str = ''; + else if (typeof s === 'string') str = s; + else if (typeof s === 'number' || typeof s === 'boolean' || typeof s === 'bigint') + str = String(s); + else { + try { + str = JSON.stringify(s); + } catch { + return '[unserializable]'; + } + } + if (str.length <= n) return str; + return str.slice(0, n) + '…'; +} + +/** Narrow an `unknown` to a string — used for wire-record field values that + * are typed `unknown` but practically always string/null in the field we read. */ +function asStr(v: unknown, fallback = '—'): string { + if (typeof v === 'string') return v; + if (typeof v === 'number' || typeof v === 'boolean') return String(v); + return fallback; +} + +function byteLength(v: unknown): number { + if (typeof v === 'string') return v.length; + if (v === undefined || v === null) return 0; + try { + return JSON.stringify(v).length; + } catch { + return 0; + } +} + +/** Render the collapsed-headline for a wire record. */ +export function renderHeadline(r: VisWireRecord): HeadlineRender { + switch (r.type) { + // ─── FILE HEADER / STARTUP BASELINE ─── + case 'metadata': { + const pv = r['protocol_version'] as string | undefined; + const producer = r['producer'] as + | { kind?: string; name?: string; version?: string } + | undefined; + const kimiVer = r['kimi_version'] as string | undefined; + const fileName = r['file_name'] as string | undefined; + const prodStr = producer ? `${producer.name ?? '?'}@${producer.version ?? '?'}` : 'legacy'; + return { + main: ( + + protocol v{pv ?? '?'} + · + {prodStr} + {kimiVer ? · kimi {kimiVer} : null} + + ), + right: fileName ? {fileName} : null, + }; + } + + case 'session_initialized': { + const agentType = r['agent_type'] as string | undefined; + const model = r['model'] as string | undefined; + const sysPrompt = r['system_prompt'] as string | undefined; + const tools = (r['active_tools'] as string[] | undefined) ?? []; + const permMode = r['permission_mode'] as string | undefined; + const planMode = r['plan_mode'] as boolean | undefined; + return { + main: ( + + {model ?? '—'} + · + {tools.length} tools + · + {permMode ?? '—'} + {planMode ? ( + + plan + + ) : null} + · + + sp: {truncate(sysPrompt ?? '', 70) || (empty)} + + + ), + right: ( + + {agentType ?? 'main'} + + ), + }; + } + + // ─── CONVERSATION ─── + case 'turn_begin': { + const agentType = r['agent_type'] as string | undefined; + const kind = r['input_kind'] as string | undefined; + const input = r['user_input'] as string | undefined; + const turnId = r['turn_id'] as string | undefined; + return { + main: ( + + turn # + + kind={kind ?? '—'} + {input ? · {truncate(input, 80)} : null} + + ), + right: ( + + {agentType ?? 'main'} + + ), + }; + } + + case 'turn_end': { + const reason = r['reason'] as string | undefined; + const success = r['success'] as boolean | undefined; + const usage = r['usage'] as Record | undefined; + const synthetic = r['synthetic'] as boolean | undefined; + const outputTokens = usage?.['output_tokens'] as number | undefined; + const cost = usage?.['cost_usd'] as number | undefined; + const turnId = r['turn_id'] as string | undefined; + return { + main: ( + + turn # + + + {reason} + + + ), + right: ( + + {synthetic ? ( + + synthetic + + ) : null} + {outputTokens !== undefined ? {outputTokens}tok : null} + {cost !== undefined ? ${cost.toFixed(4)} : null} + + ), + }; + } + + case 'user_message': { + const content = r['content']; + const text = + typeof content === 'string' + ? content + : Array.isArray(content) + ? (content as { type: string; text?: string }[]) + .map((p) => (p.type === 'text' ? (p.text ?? '') : `<${p.type}>`)) + .join('') + : ''; + const parts = Array.isArray(content) ? content.length : 1; + return { + main: ( + {truncate(text, 120) || (empty)} + ), + right: parts > 1 ? {parts} parts : undefined, + }; + } + + // An "assistant message" is now a sequence of atoms: step_begin → + // (content_part | tool_call)* → step_end. Render each atom distinctly so + // the wire timeline stays a 1:1 view of the underlying records. + case 'step_begin': { + const uuid = r['uuid'] as string | undefined; + const step = r['step'] as number | undefined; + return { + main: ( + + step #{step ?? '—'} + + + ), + }; + } + + case 'step_end': { + const step = r['step'] as number | undefined; + const finish = r['finish_reason'] as string | undefined; + const usage = r['usage'] as Record | undefined; + const outputTokens = usage?.['output_tokens'] as number | undefined; + return { + main: ( + + step #{step ?? '—'} + {finish ? {finish} : null} + + ), + right: outputTokens !== undefined ? {outputTokens}tok : undefined, + }; + } + + case 'content_part': { + const part = r['part'] as + | { kind: 'text' | 'think'; text?: string; think?: string } + | undefined; + if (part === undefined) { + return { main: (empty part) }; + } + const body = part.kind === 'text' ? (part.text ?? '') : (part.think ?? ''); + return { + main: ( + + + {part.kind} + + {truncate(body, 100) || (empty)} + + ), + right: {formatBytes(body.length)}, + }; + } + + case 'tool_call': { + const d = (r as { data?: Record }).data ?? {}; + const name = d['tool_name'] as string | undefined; + const args = d['args']; + const preview = + typeof args === 'string' ? args : args !== undefined ? JSON.stringify(args) : ''; + const activity = d['description'] as string | undefined; + const callId = d['tool_call_id'] as string | undefined; + const step = r['step'] as number | undefined; + return { + main: ( + + {name ?? '—'} + {activity ? ( + — {truncate(activity, 60)} + ) : ( + + ({truncate(preview, 60)}) + + )} + + ), + right: ( + + {callId ? ( + + call= + + + ) : null} + {step !== undefined ? step={step} : null} + + ), + }; + } + + case 'tool_result': { + const output = r['output']; + const isError = r['is_error'] as boolean | undefined; + const synthetic = r['synthetic'] as boolean | undefined; + const callId = r['tool_call_id'] as string | undefined; + const outputBytes = byteLength(output); + const isPersisted = + typeof output === 'string' && output.trimStart().startsWith(' + call= + + + {typeof output === 'string' + ? 'string' + : Array.isArray(output) + ? 'array' + : typeof output} + + {formatBytes(outputBytes)} + {isPersisted ? ( + + persisted + + ) : null} + + ), + right: ( + + {synthetic ? ( + + synthetic + + ) : null} + {isError ? ( + + error + + ) : null} + + ), + }; + } + + case 'compaction': { + const summary = r['summary'] as string | undefined; + const pre = r['pre_compact_tokens'] as number | undefined; + const post = r['post_compact_tokens'] as number | undefined; + return { + main: ( + + + summary + + {summary ? {truncate(summary, 80)} : null} + {pre !== undefined && post !== undefined ? ( + + · {pre}→{post} tok + + ) : null} + + ), + }; + } + + // ─── CONFIG ─── + case 'system_prompt_changed': { + const prompt = r['new_prompt'] as string | undefined; + return { + main: ( + + {truncate(prompt ?? '', 60) || (empty)} + + ), + right: {formatBytes(byteLength(prompt))}, + }; + } + + case 'tools_changed': { + const op = r['operation'] as string | undefined; + const tools = (r['tools'] as string[] | undefined) ?? []; + const head = tools.slice(0, 3).join(', '); + const rest = tools.length > 3 ? ` +${tools.length - 3} more` : ''; + return { + main: ( + + + {op ?? '—'} + + + {head} + {rest} + + + ), + }; + } + + // ─── EPHEMERAL ─── + case 'system_reminder': { + const content = r['content'] as string | undefined; + const consumed = r['consumed_at_turn'] as number | undefined; + return { + main: {truncate(content ?? '', 80)}, + right: + consumed !== undefined ? ( + consumed@turn{consumed} + ) : ( + + pending + + ), + }; + } + + // ─── META ─── + case 'notification': { + const d = (r as { data?: Record }).data ?? {}; + const severity = d['severity'] as string | undefined; + const title = d['title'] as string | undefined; + const category = d['category'] as string | undefined; + const targets = (d['targets'] as string[] | undefined) ?? []; + const sevTone = + severity === 'error' + ? 'error' + : severity === 'warning' + ? 'warning' + : severity === 'success' + ? 'success' + : 'info'; + return { + main: ( + + + {severity ?? '—'} + + {truncate(title ?? '', 80)} + {category ? ({category}) : null} + + ), + right: + targets.length > 0 ? ( + + {targets.map((t) => ( + + {t} + + ))} + + ) : undefined, + }; + } + + case 'team_mail': { + const d = (r as { data?: Record }).data ?? {}; + return { + main: ( + + + {asStr(d['from_agent'])} → {asStr(d['to_agent'])} + + {truncate(d['content'], 60)} + + ), + }; + } + + // ─── TOOLS ─── + // Note: the `tool_call` case is handled in the conversation section + // above (alongside step_begin / step_end / content_part). + + case 'tool_denied': { + const d = (r as { data?: Record }).data ?? {}; + const name = d['tool_name'] as string | undefined; + const reason = d['reason'] as string | undefined; + const rule = d['rule_id'] as string | undefined; + return { + main: ( + + {name ?? '—'} + — {truncate(reason ?? '', 80)} + + ), + right: rule ? ( + + rule={rule} + + ) : undefined, + }; + } + + case 'skill_invoked': { + const d = (r as { data?: Record }).data ?? {}; + const name = d['skill_name'] as string | undefined; + const mode = d['execution_mode'] as string | undefined; + const trigger = d['invocation_trigger'] as string | undefined; + const depth = d['query_depth'] as number | undefined; + return { + main: ( + + {name ?? '—'} + + {mode ?? 'inline'} + + {trigger ? {trigger} : null} + + ), + right: depth !== undefined && depth > 0 ? depth={depth} : undefined, + }; + } + + case 'skill_completed': { + const d = (r as { data?: Record }).data ?? {}; + const name = d['skill_name'] as string | undefined; + const success = d['success'] as boolean | undefined; + const error = d['error'] as string | undefined; + return { + main: ( + + {name ?? '—'} + + {success ? 'ok' : error ? truncate(error, 60) : 'failed'} + + + ), + }; + } + + // ─── APPROVAL ─── + case 'approval_request': { + const d = (r as { data?: Record }).data ?? {}; + const tool = d['tool_name'] as string | undefined; + const action = d['action'] as string | undefined; + const source = d['source'] as Record | undefined; + const display = d['display'] as Record | undefined; + return { + main: ( + + {tool ?? '—'} + · {action ?? '—'} + {source ? ( + + {asStr(source['kind'], '?')} + + ) : null} + + ), + right: display ? display={asStr(display['kind'], '?')} : undefined, + }; + } + + case 'approval_response': { + const d = (r as { data?: Record }).data ?? {}; + const response = d['response'] as string | undefined; + const feedback = d['feedback'] as string | undefined; + const selectedLabel = d['selected_label'] as string | undefined; + const synthetic = d['synthetic'] as boolean | undefined; + const tone = + response === 'approved' ? 'success' : response === 'rejected' ? 'error' : 'neutral'; + return { + main: ( + + + {response ?? '—'} + + {selectedLabel ? · {selectedLabel} : null} + {feedback ? · {truncate(feedback, 40)} : null} + + ), + right: synthetic ? ( + + synthetic + + ) : undefined, + }; + } + + // ─── SUBAGENT ─── + case 'subagent_spawned': { + const d = (r as { data?: Record }).data ?? {}; + const id = d['agent_id'] as string | undefined; + const name = d['agent_name'] as string | undefined; + const bg = d['run_in_background'] as boolean | undefined; + const parent = d['parent_agent_id'] as string | undefined; + return { + main: ( + + + {name ?? 'subagent'} + + + + ), + right: ( + + {bg ? ( + + bg + + ) : null} + {parent ? ( + + parent= + + + ) : null} + + ), + }; + } + + case 'subagent_completed': { + const d = (r as { data?: Record }).data ?? {}; + const id = d['agent_id'] as string | undefined; + const summary = d['result_summary'] as string | undefined; + const usage = d['usage'] as Record | undefined; + const out = (usage?.['output'] ?? usage?.['output_tokens']) as number | undefined; + return { + main: ( + + + {truncate(summary ?? '', 80)} + + ), + right: out !== undefined ? {out}tok : undefined, + }; + } + + case 'subagent_failed': { + const d = (r as { data?: Record }).data ?? {}; + const id = d['agent_id'] as string | undefined; + const error = d['error'] as string | undefined; + return { + main: ( + + + + {truncate(error ?? '', 80)} + + + ), + }; + } + + // ─── LIFECYCLE ─── + case 'ownership_changed': { + const oldO = r['old_owner'] as string | null | undefined; + const newO = r['new_owner'] as string | undefined; + return { + main: ( + + {oldO ?? '(none)'} → {newO ?? '—'} + + ), + }; + } + + case 'context_cleared': { + return { main: context history cleared }; + } + + case 'context_edit': { + const op = r['operation'] as string | undefined; + const target = r['target_seq'] as number | undefined; + const toTurn = r['to_turn'] as number | undefined; + const cascade = r['cascade'] as boolean | undefined; + const ref = + target !== undefined + ? `target=${target}` + : toTurn !== undefined + ? `to_turn=${toTurn}` + : '—'; + return { + main: ( + + + {op ?? '—'} + + {ref} + + ), + right: cascade ? ( + + cascade + + ) : undefined, + }; + } + + default: { + const t = (r as Record)['type']; + return { main: unknown type: {String(t)} }; + } + } +} + +// ─── tiny presentational helpers ─── +function Mono({ children, className = '' }: { children: ReactNode; className?: string }) { + return {children}; +} + +function Dim({ children, className = '' }: { children: ReactNode; className?: string }) { + return {children}; +} + +/** A click-to-focus id badge. Clicking toggles a global focus that dims + * non-related rows. Re-clicking the same id (or pressing ESC) clears. */ +function IdBadge({ + kind, + value, + shortenTo = 8, +}: { + kind: FocusKind; + value: string | undefined; + shortenTo?: number; +}) { + const { focus, toggle } = useFocus(); + if (!value) return ; + const isFocused = focus?.kind === kind && focus.value === value; + const display = value.length > shortenTo ? value.slice(0, shortenTo) : value; + return ( + + ); +} diff --git a/apps/vis/web/src/components/wire/WireRow.tsx b/apps/vis/web/src/components/wire/WireRow.tsx new file mode 100644 index 000000000..51118658c --- /dev/null +++ b/apps/vis/web/src/components/wire/WireRow.tsx @@ -0,0 +1,134 @@ +import { memo } from 'react'; + +import { recordMatchesFocus, useFocus } from '../../lib/focus-context'; +import type { VisWireRecord } from '../../types'; +import { TYPE_CATEGORY, TYPE_CTX_EFFECT } from '../../types'; +import { formatWallClock } from '../../util/time'; +import { TypeBadge } from './TypeBadge'; +import { renderHeadline } from './WireHeadline'; +import { WireRowDetail } from './WireRowDetail'; + +const CAT_COLOR_VAR: Record = { + conversation: '--color-cat-conversation', + config: '--color-cat-config', + lifecycle: '--color-cat-lifecycle', + subagent: '--color-cat-subagent', + approval: '--color-cat-approval', + ephemeral: '--color-cat-ephemeral', + meta: '--color-cat-meta', + tools: '--color-cat-tools', +}; + +interface WireRowProps { + record: VisWireRecord; + expanded: boolean; + onToggle: () => void; + /** For tool_call / tool_result — the counterpart record (if any). */ + paired?: VisWireRecord | undefined; + /** null when `paired` is undefined; true/false when the counterpart exists. */ + pairedInFiltered?: boolean | null; + /** Scroll to a seq and expand it — wired by the Wire tab via the virtualizer. */ + onJumpTo?: (seq: number) => void; +} + +export const WireRow = memo(function WireRow({ + record, + expanded, + onToggle, + paired, + pairedInFiltered, + onJumpTo, +}: WireRowProps) { + const cat = TYPE_CATEGORY[record.type]; + const accentVar = CAT_COLOR_VAR[cat] ?? '--color-fg-3'; + const ctx = TYPE_CTX_EFFECT[record.type]; + const h = renderHeadline(record); + const { focus } = useFocus(); + const related = focus === null ? true : recordMatchesFocus(record, focus); + const timeTitle = formatTimeTitle(record.time); + + return ( +
    +
    +
    + + {expanded ? ( +
    + +
    + ) : null} +
    +
    + ); +}); + +function formatTimeTitle(epochMs: number): string { + if (!epochMs || !Number.isFinite(epochMs)) return 'missing time'; + const date = new Date(epochMs); + if (!Number.isFinite(date.getTime())) return 'invalid time'; + return date.toISOString(); +} + +function Chevron({ open }: { open: boolean }) { + return ( + + ); +} + +function CtxMarker({ effect }: { effect: boolean | 'conditional' }) { + const title = + effect === true + ? 'affects LLM context' + : effect === 'conditional' + ? 'conditionally affects LLM context' + : 'telemetry only — no context effect'; + const symbol = effect === true ? '●' : effect === 'conditional' ? '◑' : '○'; + const color = effect ? 'text-[var(--color-cat-conversation)]' : 'text-fg-3'; + return ( + + {symbol} + + ); +} diff --git a/apps/vis/web/src/components/wire/WireRowDetail.tsx b/apps/vis/web/src/components/wire/WireRowDetail.tsx new file mode 100644 index 000000000..118c75138 --- /dev/null +++ b/apps/vis/web/src/components/wire/WireRowDetail.tsx @@ -0,0 +1,253 @@ +import { useState, type ReactNode } from 'react'; + +import type { VisWireRecord } from '../../types'; +import { CopyButton } from '../shared/CopyButton'; +import { JsonViewer } from '../shared/JsonViewer'; +import { SizePreview } from '../shared/SizePreview'; + +interface WireRowDetailProps { + record: VisWireRecord; + /** Counterpart in a tool_call ↔ tool_result pair (if applicable). */ + paired?: VisWireRecord | undefined; + /** Whether the counterpart is visible in the current filter. null when + * there is no counterpart at all. */ + pairedInFiltered?: boolean | null; + /** Scroll to + expand a given seq. */ + onJumpTo?: (seq: number) => void; +} + +/** Fields that are best rendered as collapsible large-payload blocks rather than + * as key-value pairs. Keyed by the path (dot-separated) from record root. */ +const LARGE_FIELDS: Record = { + new_prompt: true, + system_prompt: true, + summary: true, + think: true, + text: true, + content: true, + output: true, + user_input: true, + 'data.body': true, + 'data.args': true, + 'data.payload': true, + 'data.tail_output': true, + 'data.result_summary': true, + 'data.error': true, + 'data.reason': true, + 'data.content': true, + 'data.summary': true, + result_summary: true, + new_content: true, +}; + +const TOP_LEVEL_META = new Set(['type', 'seq', 'time']); + +export function WireRowDetail({ record, paired, pairedInFiltered, onJumpTo }: WireRowDetailProps) { + const [showRaw, setShowRaw] = useState(false); + const entries = Object.entries(record as Record).filter( + ([k]) => !TOP_LEVEL_META.has(k), + ); + + return ( +
    +
    + {entries.map(([key, value]) => renderField(key, value, key))} +
    + {paired !== undefined ? ( + + ) : null} +
    + + +
    + {showRaw ? ( +
    + +
    + ) : null} +
    + ); +} + +/** Renders the "↳ paired with tool_result (seq N)" section below a + * tool_call row (or the reverse). Shows a compact preview of the + * counterpart and a jump button that scrolls-to + expands that row. */ +function PairedSection({ + self, + paired, + pairedInFiltered, + onJumpTo, +}: { + self: VisWireRecord; + paired: VisWireRecord; + pairedInFiltered: boolean | null; + onJumpTo?: (seq: number) => void; +}) { + const selfIsCall = self.type === 'tool_call'; + const preview = selfIsCall ? previewToolResult(paired) : previewToolCallArgs(paired); + const isError = + paired.type === 'tool_result' && (paired as { is_error?: boolean }).is_error === true; + const hidden = pairedInFiltered === false; + return ( +
    +
    + ↳ paired + {paired.type} + seq + {paired.seq} + {isError ? ( + + error + + ) : null} + {hidden ? (hidden by filter) : null} + +
    + {preview.length > 0 ? ( +
    +          {preview.length > 400 ? preview.slice(0, 400) + '…' : preview}
    +        
    + ) : ( +
    (empty)
    + )} +
    + ); +} + +function previewToolCallArgs(call: VisWireRecord): string { + const args = (call as { data?: { args?: unknown } }).data?.args; + if (args === undefined) return ''; + if (typeof args === 'string') return args; + try { + return JSON.stringify(args, null, 2); + } catch { + // Circular refs / BigInt / etc. — don't fall through to String(unknown) + // which produces "[object Object]" and hides the actual problem. + return '(unserialisable args)'; + } +} + +function previewToolResult(result: VisWireRecord): string { + const out = (result as { output?: unknown }).output; + if (out === undefined || out === null) return ''; + if (typeof out === 'string') return out; + try { + return JSON.stringify(out, null, 2); + } catch { + return '(unserialisable output)'; + } +} + +function renderField(key: string, value: unknown, path: string) { + // Null / undefined + if (value === null || value === undefined) { + return ( + + null + + ); + } + + // Large-payload fields: render via SizePreview with JsonViewer inside + if (LARGE_FIELDS[path] || LARGE_FIELDS[key]) { + if (typeof value === 'string') { + return ( + + +
    {value}
    +
    +
    + ); + } + // non-string large fields: JSON tree + return ( + + + + + + ); + } + + // Primitive + if (typeof value !== 'object') { + const repr = + typeof value === 'string' + ? `"${truncate(value, 160)}"` + : typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint' + ? String(value) + : typeof value; + return ( + + {repr} + + ); + } + + // Nested object: inline JsonViewer + return ( + + + + ); +} + +function FieldRow({ + label, + children, + wide = false, +}: { + label: string; + children: ReactNode; + wide?: boolean; +}) { + if (wide) { + return ( + <> +
    + + {label} + +
    {children}
    +
    + + ); + } + return ( + <> + {label} +
    {children}
    + + ); +} + +function typeColor(v: unknown): string { + if (typeof v === 'boolean') return 'text-[var(--color-cat-config)]'; + if (typeof v === 'number') return 'text-[var(--color-sev-info)]'; + if (typeof v === 'string') return 'text-[var(--color-cat-ephemeral)]'; + return 'text-fg-0'; +} + +function truncate(s: string, n: number): string { + return s.length <= n ? s : s.slice(0, n) + '…'; +} diff --git a/apps/vis/web/src/components/wire/WireTab.tsx b/apps/vis/web/src/components/wire/WireTab.tsx new file mode 100644 index 000000000..45b515d20 --- /dev/null +++ b/apps/vis/web/src/components/wire/WireTab.tsx @@ -0,0 +1,524 @@ +import { useVirtualizer } from '@tanstack/react-virtual'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; + +import { FocusProvider, useFocus } from '../../lib/focus-context'; +import { computeIssues, topSeverity } from '../../lib/issues'; +import { + buildMatchContext, + matchesQuery, + parseQuery, + type ParsedQuery, + type QueryToken, +} from '../../lib/query-parser'; +import type { VisWireRecord, WireCategory } from '../../types'; +import { TYPE_CATEGORY } from '../../types'; +import { IssuesDrawer } from './IssuesDrawer'; +import { WireRow } from './WireRow'; + +interface WireTabProps { + records: VisWireRecord[]; + health?: 'ok' | 'broken'; + brokenReason?: string; + warnings?: string[]; +} + +const CATEGORIES: WireCategory[] = [ + 'conversation', + 'tools', + 'approval', + 'subagent', + 'ephemeral', + 'meta', + 'config', + 'lifecycle', +]; + +const CAT_COLOR_VAR: Record = { + conversation: '--color-cat-conversation', + config: '--color-cat-config', + lifecycle: '--color-cat-lifecycle', + subagent: '--color-cat-subagent', + approval: '--color-cat-approval', + ephemeral: '--color-cat-ephemeral', + meta: '--color-cat-meta', + tools: '--color-cat-tools', +}; + +export function WireTab(props: WireTabProps) { + // The FocusProvider wraps the entire tab so IdBadge clicks deep inside + // the virtualized rows can update (and be read by) the toolbar status + // indicator without prop-drilling. + return ( + + + + ); +} + +function WireTabInner({ records, health, brokenReason, warnings = [] }: WireTabProps) { + const [search, setSearch] = useState(''); + const [excluded, setExcluded] = useState(new Set()); + const [expanded, setExpanded] = useState(new Set()); + const [showHelp, setShowHelp] = useState(false); + const [drawerOpen, setDrawerOpen] = useState(false); + const parentRef = useRef(null); + + const query = useMemo(() => parseQuery(search), [search]); + + const matchCtx = useMemo(() => buildMatchContext(records, query), [records, query]); + + const filtered = useMemo(() => { + return records.filter((r) => { + const cat = TYPE_CATEGORY[r.type]; + if (excluded.has(cat)) return false; + return matchesQuery(r, query, matchCtx); + }); + }, [records, excluded, query, matchCtx]); + + // Pair map: tool_call_id → the two sides. Built from the full records[] + // (not `filtered`) so jumping from a filtered view still works when the + // counterpart has been filtered out — the detail panel will just mark it + // "(hidden by current filter)". + const pairMap = useMemo(() => { + const m = new Map(); + for (const r of records) { + if (r.type === 'tool_call') { + const id = (r as { data?: { tool_call_id?: string } }).data?.tool_call_id; + if (id !== undefined) { + const slot = m.get(id) ?? {}; + slot.call = r; + m.set(id, slot); + } + } else if (r.type === 'tool_result') { + const id = (r as { tool_call_id?: string }).tool_call_id; + if (id !== undefined) { + const slot = m.get(id) ?? {}; + slot.result = r; + m.set(id, slot); + } + } + } + return m; + }, [records]); + + /** Find the counterpart for a tool_call / tool_result row (undefined otherwise). */ + const pairedFor = useCallback( + (r: VisWireRecord): VisWireRecord | undefined => { + if (r.type === 'tool_call') { + const id = (r as { data?: { tool_call_id?: string } }).data?.tool_call_id; + return id === undefined ? undefined : pairMap.get(id)?.result; + } + if (r.type === 'tool_result') { + const id = (r as { tool_call_id?: string }).tool_call_id; + return id === undefined ? undefined : pairMap.get(id)?.call; + } + return undefined; + }, + [pairMap], + ); + + // Compute per-category counts over the unfiltered set + const catCounts = useMemo(() => { + const m = new Map(); + for (const r of records) { + const c = TYPE_CATEGORY[r.type]; + m.set(c, (m.get(c) ?? 0) + 1); + } + return m; + }, [records]); + + const turnCount = useMemo(() => records.filter((r) => r.type === 'turn_begin').length, [records]); + + // Issues are computed over the full records[] + file warnings (not the + // filter result) so the pill keeps showing real problems even when the + // user has narrowed their query. Jumping to an issue scrolls the Wire + // list — if the issue's seq is currently filtered out, `jumpToSeq` is + // a no-op and the drawer just closes. + const issues = useMemo(() => computeIssues(records, warnings), [records, warnings]); + const issuesSeverity = topSeverity(issues); + + // Stable estimate — ResizeObserver (wired via `virt.measureElement` ref + // on each item) refines to the actual rendered height. Do NOT vary the + // estimate based on `expanded`: that fights the library's measurement + // cache and produces layout gaps when the two race. + const virt = useVirtualizer({ + count: filtered.length, + getScrollElement: () => parentRef.current, + estimateSize: () => 28, + overscan: 10, + getItemKey: (i) => filtered[i]?.seq ?? i, + }); + + const toggle = useCallback((seq: number) => { + // Trust the library's ResizeObserver to catch the new content size — + // calling `virt.measure()` manually here races with the observer and + // can clear freshly-written cache entries, stranding items at their + // `estimateSize` value and producing visible gaps. + setExpanded((prev) => { + const next = new Set(prev); + if (next.has(seq)) next.delete(seq); + else next.add(seq); + return next; + }); + }, []); + + /** Seq → index lookup built from the filtered list. Doubles as the + * "is this seq currently visible?" test used by pairing + issues jumps. */ + const filteredSeqIdx = useMemo(() => { + const m = new Map(); + for (let i = 0; i < filtered.length; i += 1) { + const r = filtered[i]; + if (r !== undefined) m.set(r.seq, i); + } + return m; + }, [filtered]); + + /** Jump to a specific seq: scroll the row into view and expand it. + * No-op if the seq isn't in the current filtered set — callers can + * check `filteredSeqIdx.has(seq)` ahead of time and disable the link. */ + const jumpToSeq = useCallback( + (seq: number) => { + const idx = filteredSeqIdx.get(seq); + if (idx === undefined) return; + virt.scrollToIndex(idx, { align: 'center' }); + setExpanded((prev) => (prev.has(seq) ? prev : new Set(prev).add(seq))); + }, + [filteredSeqIdx, virt], + ); + + const toggleCat = (c: WireCategory) => { + setExcluded((prev) => { + const next = new Set(prev); + if (next.has(c)) next.delete(c); + else next.add(c); + return next; + }); + }; + + const expandAll = () =>{ setExpanded(new Set(filtered.map((r) => r.seq))); }; + const collapseAll = () =>{ setExpanded(new Set()); }; + + const hasStructuredTokens = query.tokens.some((t) => t.key !== null) || query.errors.length > 0; + + return ( +
    + {/* Toolbar */} +
    +
    + { setSearch(e.target.value); }} + className="w-80 border border-border bg-surface-0 px-2 py-1 pr-7 font-mono text-[12px] text-fg-0 placeholder:text-fg-3 focus:border-border-strong focus:outline-none" + /> + + {showHelp ? { setShowHelp(false); }} /> : null} +
    +
    + {CATEGORIES.map((c) => { + const count = catCounts.get(c) ?? 0; + if (count === 0) return null; + const on = !excluded.has(c); + const color = `var(${CAT_COLOR_VAR[c]})`; + return ( + + ); + })} +
    +
    + + {filtered.length} / {records.length} ev + + {turnCount} turns + {issues.length > 0 && issuesSeverity !== null ? ( + + ) : null} + + +
    +
    + + {/* Query chips — one-line summary of parsed filters, each removable */} + {hasStructuredTokens ? ( +
    + {query.tokens + .filter((t) => t.key !== null) + .map((t, i) => { + const invalid = query.errors.some((e) => e.token === t.raw); + return ( + { setSearch((s) => removeToken(s, t.raw)); }} + /> + ); + })} + {query.errors.length > 0 ? ( + `${e.token}: ${e.reason}`).join('\n')} + > + · {query.errors.length} unknown + + ) : null} +
    + ) : null} + + + + {/* Health warning */} + {health === 'broken' ? ( +
    + broken: {brokenReason ?? 'unknown reason'} +
    + ) : null} + {warnings.length > 0 ? ( +
    + {warnings.length} warning{warnings.length > 1 ? 's' : ''} · first: {warnings[0]} +
    + ) : null} + + {/* Virtualized list */} +
    + {filtered.length === 0 ? ( +
    + no records match the current filter +
    + ) : ( +
    + {virt.getVirtualItems().map((vi) => { + const r = filtered[vi.index]; + if (!r) return null; + // Compute the counterpart once per row — paired lookup runs + // inside a virtualized render loop, so duplicating the call + // meant ~40 redundant ops per scroll tick on deep sessions. + const paired = pairedFor(r); + const pairedInFiltered = paired === undefined ? null : filteredSeqIdx.has(paired.seq); + return ( +
    + { toggle(r.seq); }} + paired={paired} + pairedInFiltered={pairedInFiltered} + onJumpTo={jumpToSeq} + /> +
    + ); + })} +
    + )} +
    + {drawerOpen ? ( + { setDrawerOpen(false); }} + onJumpTo={jumpToSeq} + isSeqVisible={(seq) => filteredSeqIdx.has(seq)} + /> + ) : null} +
    + ); +} + +// ──────── helper subcomponents ──────── + +function QueryChip({ + token, + invalid, + onRemove, +}: { + token: QueryToken; + invalid: boolean; + onRemove: () => void; +}) { + const label = `${token.key}${token.op === '=' ? ':' : token.op}${token.values.join(',')}`; + return ( + + {label} + + + ); +} + +function QueryHelp({ onClose }: { onClose: () => void }) { + // Dismiss on: ESC, or outside click. `onMouseLeave` was too twitchy — + // the panel would flash shut on mouse tremor near its edge. + useEffect(() => { + const onKey = (e: KeyboardEvent) => { + if (e.key === 'Escape') onClose(); + }; + const onDocClick = (e: MouseEvent) => { + const t = e.target as HTMLElement | null; + if (t?.closest('[data-query-help-root]')) return; + onClose(); + }; + window.addEventListener('keydown', onKey); + // defer doc listener one tick so the opening click doesn't immediately close us + const id = window.setTimeout(() => { + document.addEventListener('mousedown', onDocClick); + }, 0); + return () => { + window.removeEventListener('keydown', onKey); + window.clearTimeout(id); + document.removeEventListener('mousedown', onDocClick); + }; + }, [onClose]); + return ( +
    +
    query syntax
    +
      +
    • + type:tool_call · type:user_message,tool_result · + type:!compaction +
    • +
    • + tool:Bash · tool:Read,Write +
    • +
    • + turn:3 · turn:turn_5 +
    • +
    • + seq: + {'>'}100 · seq:<50 · seq:>=200 +
    • +
    • + error:true · error:false +
    • +
    • + agent:sub_abc123 (substring) +
    • +
    • + id:abc123 (matches any id field) +
    • +
    • + bare words → JSON substring (AND) +
    • +
    +
    + tip: click an underlined id in any row to focus all related records · ESC to clear +
    +
    + ); +} + +function FocusStatus() { + const { focus, clear } = useFocus(); + if (focus === null) return null; + return ( +
    + ◎ focus + {focus.kind} + = + + {focus.value.length > 24 ? focus.value.slice(0, 24) + '…' : focus.value} + + +
    + ); +} + +/** Remove a single structured token from the raw search string. Used when + * the user clicks the × on a chip. Matches the token's `raw` text exactly, + * trimming adjacent whitespace. */ +function removeToken(input: string, raw: string): string { + const re = new RegExp(`(^|\\s)${escapeRegExp(raw)}(?=\\s|$)`); + return input.replace(re, '').replaceAll(/\s+/g, ' ').trim(); +} + +function escapeRegExp(s: string): string { + return s.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`); +} diff --git a/apps/vis/web/src/components/wire/typeMeta.ts b/apps/vis/web/src/components/wire/typeMeta.ts new file mode 100644 index 000000000..d965a2b73 --- /dev/null +++ b/apps/vis/web/src/components/wire/typeMeta.ts @@ -0,0 +1,67 @@ +import type { WireCategory, WireRecordType } from '../../types'; + +/** Unicode icon for each wire type. Chosen for distinguishability in a dense list. */ +export const TYPE_ICON: Record = { + metadata: '§', + session_initialized: '✧', + turn_begin: '▶', + turn_end: '◼', + user_message: '▷', + tool_result: '✓', + compaction: '⏪', + system_prompt_changed: '¶', + tools_changed: '⚙', + system_reminder: '!', + notification: '◉', + // Streaming atoms: step boundary, streamed part, tool call + step_begin: '┌', + step_end: '┘', + content_part: '◆', + tool_call: '→', + tool_denied: '⊘', + skill_invoked: '★', + skill_completed: '★', + approval_request: '?', + approval_response: '·', + team_mail: '✉', + subagent_spawned: '┬', + subagent_completed: '└', + subagent_failed: '×', + ownership_changed: '↔', + context_edit: '✂', + context_cleared: '∅', +}; + +/** Tone key used by . Most types fall back to their category color. */ +export const TYPE_TONE: Record< + WireRecordType, + WireCategory | 'user' | 'assistant' | 'tool' | 'compaction' | 'turn' +> = { + metadata: 'meta', + session_initialized: 'config', + turn_begin: 'turn', + turn_end: 'turn', + user_message: 'user', + tool_result: 'tool', + compaction: 'compaction', + system_prompt_changed: 'config', + tools_changed: 'config', + system_reminder: 'ephemeral', + notification: 'meta', + step_begin: 'turn', + step_end: 'turn', + content_part: 'assistant', + tool_call: 'tools', + tool_denied: 'approval', + skill_invoked: 'tools', + skill_completed: 'tools', + approval_request: 'approval', + approval_response: 'approval', + team_mail: 'meta', + subagent_spawned: 'subagent', + subagent_completed: 'subagent', + subagent_failed: 'subagent', + ownership_changed: 'lifecycle', + context_edit: 'lifecycle', + context_cleared: 'lifecycle', +}; diff --git a/apps/vis/web/src/hooks/useContext.ts b/apps/vis/web/src/hooks/useContext.ts new file mode 100644 index 000000000..3b98ee925 --- /dev/null +++ b/apps/vis/web/src/hooks/useContext.ts @@ -0,0 +1,34 @@ +import { useQuery } from '@tanstack/react-query'; +import { api } from '../api'; + +export function useSessionContext(sessionId: string | undefined, enabled = true) { + return useQuery({ + queryKey: ['session', sessionId, 'context'] as const, + queryFn: () => api.getContext(sessionId!), + enabled: !!sessionId && enabled, + }); +} + +export function useSubagentContext( + sessionId: string | undefined, + agentId: string | undefined, + enabled = true, +) { + return useQuery({ + queryKey: ['session', sessionId, 'subagent', agentId, 'context'] as const, + queryFn: () => api.getSubagentContext(sessionId!, agentId!), + enabled: !!sessionId && !!agentId && enabled, + }); +} + +export function useToolResult( + sessionId: string | undefined, + toolCallId: string | undefined, + enabled = true, +) { + return useQuery({ + queryKey: ['session', sessionId, 'tool-result', toolCallId] as const, + queryFn: () => api.getToolResult(sessionId!, toolCallId!), + enabled: !!sessionId && !!toolCallId && enabled, + }); +} diff --git a/apps/vis/web/src/hooks/useSession.ts b/apps/vis/web/src/hooks/useSession.ts new file mode 100644 index 000000000..339541e94 --- /dev/null +++ b/apps/vis/web/src/hooks/useSession.ts @@ -0,0 +1,46 @@ +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { api } from '../api'; +import type { SessionSummary } from '../types'; + +export function useSessions() { + return useQuery({ + queryKey: ['sessions'] as const, + queryFn: () => api.listSessions(), + }); +} + +export function useSession(sessionId: string | undefined) { + return useQuery({ + queryKey: ['session', sessionId] as const, + queryFn: () => api.getSession(sessionId!), + enabled: !!sessionId, + }); +} + +export function useDeleteSession() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (sessionId: string) => api.deleteSession(sessionId), + onSuccess: (_result, sessionId) => { + qc.setQueryData(['sessions'], (old) => + old?.filter((s) => s.session_id !== sessionId), + ); + qc.removeQueries({ queryKey: ['session', sessionId] }); + void qc.invalidateQueries({ queryKey: ['sessions'] }); + }, + }); +} + +export function useClearSessions() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: () => api.clearSessions(), + onSuccess: (result) => { + if (result.failed.length === 0) { + qc.setQueryData(['sessions'], []); + qc.removeQueries({ queryKey: ['session'] }); + } + void qc.invalidateQueries({ queryKey: ['sessions'] }); + }, + }); +} diff --git a/apps/vis/web/src/hooks/useSubagents.ts b/apps/vis/web/src/hooks/useSubagents.ts new file mode 100644 index 000000000..075a19a65 --- /dev/null +++ b/apps/vis/web/src/hooks/useSubagents.ts @@ -0,0 +1,21 @@ +import { useQuery } from '@tanstack/react-query'; +import { api } from '../api'; + +export function useSubagents(sessionId: string | undefined, enabled = true) { + return useQuery({ + queryKey: ['session', sessionId, 'subagents'] as const, + queryFn: () => api.getSubagents(sessionId!), + enabled: !!sessionId && enabled, + }); +} + +export function useSubagentMeta( + sessionId: string | undefined, + agentId: string | undefined, +) { + return useQuery({ + queryKey: ['session', sessionId, 'subagent', agentId, 'meta'] as const, + queryFn: () => api.getSubagentMeta(sessionId!, agentId!), + enabled: !!sessionId && !!agentId, + }); +} diff --git a/apps/vis/web/src/hooks/useTheme.ts b/apps/vis/web/src/hooks/useTheme.ts new file mode 100644 index 000000000..5255245d4 --- /dev/null +++ b/apps/vis/web/src/hooks/useTheme.ts @@ -0,0 +1,82 @@ +import { useCallback, useEffect, useState } from 'react'; + +export type ThemeChoice = 'light' | 'dark' | 'auto'; +export type ResolvedTheme = 'light' | 'dark'; + +const STORAGE_KEY = 'vis.theme'; + +function systemTheme(): ResolvedTheme { + if (typeof window === 'undefined') return 'dark'; + return window.matchMedia('(prefers-color-scheme: light)').matches + ? 'light' + : 'dark'; +} + +function readStored(): ThemeChoice { + try { + const v = localStorage.getItem(STORAGE_KEY); + if (v === 'light' || v === 'dark' || v === 'auto') return v; + } catch { + /* ignore quota/permission errors */ + } + return 'auto'; +} + +function apply(resolved: ResolvedTheme): void { + const root = document.documentElement; + root.dataset.theme = resolved; + // Sync the address-bar theme-color so native chrome (URL bar, title bar) + // transitions too. + const meta = document.querySelector('meta[name="theme-color"]'); + if (meta) { + meta.content = resolved === 'light' ? '#fafbfc' : '#0b0d12'; + } +} + +/** + * Three-state theme: auto (follow system), light, dark. The resolved concrete + * theme is reflected on ``. User choice is persisted + * in localStorage; absence ⇒ auto. + */ +export function useTheme(): { + choice: ThemeChoice; + resolved: ResolvedTheme; + cycle: () => void; + set: (c: ThemeChoice) => void; +} { + const [choice, setChoice] = useState(() => readStored()); + const [sys, setSys] = useState(() => systemTheme()); + + // Listen to system changes when in auto mode. + useEffect(() => { + const m = window.matchMedia('(prefers-color-scheme: light)'); + const handler = () =>{ setSys(m.matches ? 'light' : 'dark'); }; + m.addEventListener('change', handler); + return () =>{ m.removeEventListener('change', handler); }; + }, []); + + const resolved: ResolvedTheme = choice === 'auto' ? sys : choice; + + useEffect(() => { + apply(resolved); + }, [resolved]); + + const set = useCallback((c: ThemeChoice) => { + setChoice(c); + try { + if (c === 'auto') localStorage.removeItem(STORAGE_KEY); + else localStorage.setItem(STORAGE_KEY, c); + } catch { + /* ignore */ + } + }, []); + + // Cycle: auto → light → dark → auto + const cycle = useCallback(() => { + const next: ThemeChoice = + choice === 'auto' ? 'light' : choice === 'light' ? 'dark' : 'auto'; + set(next); + }, [choice, set]); + + return { choice, resolved, cycle, set }; +} diff --git a/apps/vis/web/src/hooks/useWire.ts b/apps/vis/web/src/hooks/useWire.ts new file mode 100644 index 000000000..44b3b9737 --- /dev/null +++ b/apps/vis/web/src/hooks/useWire.ts @@ -0,0 +1,33 @@ +import { useQuery } from '@tanstack/react-query'; +import { api } from '../api'; + +export function useWire(sessionId: string | undefined, enabled = true) { + return useQuery({ + queryKey: ['session', sessionId, 'wire'] as const, + queryFn: () => api.getWire(sessionId!), + enabled: !!sessionId && enabled, + }); +} + +export function useSubagentWire( + sessionId: string | undefined, + agentId: string | undefined, + enabled = true, +) { + return useQuery({ + queryKey: ['session', sessionId, 'subagent', agentId, 'wire'] as const, + queryFn: () => api.getSubagentWire(sessionId!, agentId!), + enabled: !!sessionId && !!agentId && enabled, + }); +} + +export function useArchive( + sessionId: string | undefined, + filename: string | undefined, +) { + return useQuery({ + queryKey: ['session', sessionId, 'archive', filename] as const, + queryFn: () => api.getArchive(sessionId!, filename!), + enabled: !!sessionId && !!filename, + }); +} diff --git a/apps/vis/web/src/lib/focus-context.tsx b/apps/vis/web/src/lib/focus-context.tsx new file mode 100644 index 000000000..4174ba107 --- /dev/null +++ b/apps/vis/web/src/lib/focus-context.tsx @@ -0,0 +1,96 @@ +import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'; +import type { ReactNode } from 'react'; + +import type { VisWireRecord } from '../types'; + +export type FocusKind = 'turn_id' | 'tool_call_id' | 'agent_id' | 'step_uuid'; + +export interface Focus { + kind: FocusKind; + value: string; +} + +interface FocusContextShape { + focus: Focus | null; + /** Toggle — clicking the same focused id again clears it. */ + toggle: (f: Focus) => void; + clear: () => void; +} + +const FocusCtx = createContext(null); + +export function FocusProvider({ children }: { children: ReactNode }) { + const [focus, setFocus] = useState(null); + const toggle = useCallback((f: Focus) => { + setFocus((prev) => + prev !== null && prev.kind === f.kind && prev.value === f.value ? null : f, + ); + }, []); + const clear = useCallback(() =>{ setFocus(null); }, []); + // ESC clears the focus — one-liner keyboard affordance. + useEffect(() => { + const onKey = (e: KeyboardEvent) => { + if (e.key === 'Escape') clear(); + }; + window.addEventListener('keydown', onKey); + return () =>{ window.removeEventListener('keydown', onKey); }; + }, [clear]); + const value = useMemo( + () => ({ focus, toggle, clear }), + [focus, toggle, clear], + ); + return {children}; +} + +export function useFocus(): FocusContextShape { + const ctx = useContext(FocusCtx); + if (ctx === null) { + // Safe default so components outside a provider don't crash. + return { focus: null, toggle: () => {}, clear: () => {} }; + } + return ctx; +} + +/** + * A record "belongs to" the focused id when any of its identifiers matches. + * Scope is intentionally broad per id kind so click-to-focus behaves like + * "show me everything related to this thing": + * + * turn_id → all records with r.turn_id === value + * tool_call_id → the tool_call, its tool_result, any approval linked by id + * agent_id → spawn/completed/failed + mail records mentioning this agent + * step_uuid → the step_begin/end + content_parts + tool_calls under it + */ +export function recordMatchesFocus(r: VisWireRecord, focus: Focus | null): boolean { + if (focus === null) return true; + const { kind, value } = focus; + const rec = r as Record; + const data = (rec['data'] as Record | undefined) ?? undefined; + + switch (kind) { + case 'turn_id': + return rec['turn_id'] === value; + + case 'tool_call_id': { + if (rec['tool_call_id'] === value) return true; + if (data?.['tool_call_id'] === value) return true; + if (data?.['parent_tool_call_id'] === value) return true; + return false; + } + + case 'agent_id': { + if (rec['agent_id'] === value) return true; + if (data?.['agent_id'] === value) return true; + if (data?.['parent_agent_id'] === value) return true; + if (data?.['from_agent'] === value) return true; + if (data?.['to_agent'] === value) return true; + return false; + } + + case 'step_uuid': + return rec['uuid'] === value || rec['step_uuid'] === value; + } + // Unreachable — `FocusKind` is exhausted above. Here to satisfy + // consistent-return (switch-exhaustiveness isn't narrowed by the rule). + return false; +} diff --git a/apps/vis/web/src/lib/issues.ts b/apps/vis/web/src/lib/issues.ts new file mode 100644 index 000000000..6bde9d7f9 --- /dev/null +++ b/apps/vis/web/src/lib/issues.ts @@ -0,0 +1,182 @@ +// Aggregate every "something went wrong" signal from a wire timeline +// into a flat list consumable by the Issues drawer. Pure — no React. + +import type { VisWireRecord } from '../types'; + +export type IssueSeverity = 'error' | 'warning' | 'info'; + +export interface Issue { + severity: IssueSeverity; + /** Human-readable kind — shown as the row's title. */ + kind: + | 'subagent_failed' + | 'tool_error' + | 'tool_denied' + | 'turn_failed' + | 'step_truncated' + | 'wire_warning'; + /** Seq of the offending record. `null` for file-level warnings. */ + seq: number | null; + /** Short summary shown on a single line. */ + summary: string; + /** Optional second line / tooltip detail. */ + detail?: string; +} + +const SEVERITY_ORDER: Record = { + error: 0, + warning: 1, + info: 2, +}; + +/** Scan `records` + `warnings` and produce an ordered issue list. + * Sorted by severity first, then seq ascending. Warnings (no seq) go last. */ +export function computeIssues( + records: readonly VisWireRecord[], + warnings: readonly string[], +): Issue[] { + const out: Issue[] = []; + + for (const r of records) { + switch (r.type) { + case 'subagent_failed': { + const d = (r as { data?: { error?: string; agent_id?: string } }).data ?? {}; + out.push({ + severity: 'error', + kind: 'subagent_failed', + seq: r.seq, + summary: firstLine(d.error ?? '(no error message)'), + detail: d.agent_id ? `agent ${d.agent_id}` : undefined, + }); + break; + } + case 'tool_result': { + const rec = r as { is_error?: boolean; output?: unknown; tool_call_id?: string }; + if (rec.is_error === true) { + const text = + typeof rec.output === 'string' + ? rec.output + : rec.output !== undefined + ? (() => { + try { + return JSON.stringify(rec.output); + } catch { + return '(unserializable output)'; + } + })() + : ''; + out.push({ + severity: 'error', + kind: 'tool_error', + seq: r.seq, + summary: firstLine(text) || '(no output)', + detail: rec.tool_call_id ? `call ${rec.tool_call_id}` : undefined, + }); + } + break; + } + case 'tool_denied': { + const d = + (r as { data?: { tool_name?: string; reason?: string; rule_id?: string } }).data ?? {}; + out.push({ + severity: 'warning', + kind: 'tool_denied', + seq: r.seq, + summary: `${d.tool_name ?? 'tool'} — ${firstLine(d.reason ?? '(no reason)')}`, + detail: d.rule_id ? `rule ${d.rule_id}` : undefined, + }); + break; + } + case 'turn_end': { + const rec = r as { + success?: boolean; + reason?: string; + turn_id?: string; + synthetic?: boolean; + }; + if (rec.success === false || rec.reason === 'error' || rec.reason === 'interrupted') { + out.push({ + severity: 'warning', + kind: 'turn_failed', + seq: r.seq, + summary: `turn ${rec.turn_id ?? ''} — ${rec.reason ?? 'unknown'}`, + detail: rec.synthetic === true ? 'synthetic' : undefined, + }); + } + break; + } + case 'step_end': { + const rec = r as { finish_reason?: string; turn_id?: string }; + if (rec.finish_reason === 'length' || rec.finish_reason === 'error') { + out.push({ + severity: 'info', + kind: 'step_truncated', + seq: r.seq, + summary: `step finished with "${rec.finish_reason}"`, + detail: rec.turn_id ? `turn ${rec.turn_id}` : undefined, + }); + } + break; + } + case 'approval_request': + case 'approval_response': + case 'compaction': + case 'content_part': + case 'context_cleared': + case 'context_edit': + case 'metadata': + case 'notification': + case 'ownership_changed': + case 'session_initialized': + case 'skill_completed': + case 'skill_invoked': + case 'step_begin': + case 'subagent_completed': + case 'subagent_spawned': + case 'system_prompt_changed': + case 'system_reminder': + case 'team_mail': + case 'tool_call': + case 'tools_changed': + case 'turn_begin': + case 'user_message': + break; + default: + break; + } + } + + for (const w of warnings) { + out.push({ + severity: 'warning', + kind: 'wire_warning', + seq: null, + summary: firstLine(w), + }); + } + + out.sort((a, b) => { + const d = SEVERITY_ORDER[a.severity] - SEVERITY_ORDER[b.severity]; + if (d !== 0) return d; + const sa = a.seq ?? Number.POSITIVE_INFINITY; + const sb = b.seq ?? Number.POSITIVE_INFINITY; + return sa - sb; + }); + + return out; +} + +/** Top-level summary tone used for the toolbar pill — "worst wins". */ +export function topSeverity(issues: readonly Issue[]): IssueSeverity | null { + if (issues.length === 0) return null; + for (const i of issues) if (i.severity === 'error') return 'error'; + for (const i of issues) if (i.severity === 'warning') return 'warning'; + return 'info'; +} + +function firstLine(s: string): string { + const trimmed = s.trim(); + const nl = trimmed.indexOf('\n'); + const one = nl === -1 ? trimmed : trimmed.slice(0, nl); + return one.length > 120 ? one.slice(0, 120) + '…' : one; +} diff --git a/apps/vis/web/src/lib/query-parser.ts b/apps/vis/web/src/lib/query-parser.ts new file mode 100644 index 000000000..67c669133 --- /dev/null +++ b/apps/vis/web/src/lib/query-parser.ts @@ -0,0 +1,295 @@ +// Structured query language for the Wire tab's search box. +// +// Grammar (whitespace-separated; AND between tokens): +// key:value exact/contains match on a specific field +// key:a,b,c OR across values +// key:!value NOT match +// key:>N / =N / <=N numeric comparison (seq only) +// bare word substring match on full record JSON (fallback) +// +// Supported keys: +// type — r.type exact (OR-able, NOT-able) +// tool — tool_call.data.tool_name substring (also matches paired tool_result) +// turn — r.turn_id substring +// seq — numeric compare on r.seq +// error — boolean; true → records that represent failure/error states +// agent — substring over any agent_id field +// id — substring over any *_id / uuid field +// +// Unknown keys become `errors[]` entries; chip UI shows them in red. + +import type { VisWireRecord, WireRecordType } from '../types'; + +export type TokenOp = '=' | '!=' | '>' | '<' | '>=' | '<=' | '~'; + +export interface QueryToken { + /** null = bare text token (substring across whole record). */ + key: string | null; + op: TokenOp; + /** Comma-split values; length >= 1 always. */ + values: string[]; + /** Source slice in the query string; useful to rewrite/remove a single chip. */ + raw: string; +} + +export interface ParsedQuery { + tokens: QueryToken[]; + errors: { token: string; reason: string }[]; +} + +const KNOWN_KEYS = new Set(['type', 'tool', 'turn', 'seq', 'error', 'agent', 'id']); + +export function parseQuery(input: string): ParsedQuery { + const tokens: QueryToken[] = []; + const errors: { token: string; reason: string }[] = []; + // Simple whitespace split — no quoted-string support yet (keep it minimal). + const parts = input.split(/\s+/).filter((s) => s.length > 0); + for (const raw of parts) { + const colon = raw.indexOf(':'); + if (colon <= 0) { + tokens.push({ key: null, op: '~', values: [raw], raw }); + continue; + } + const key = raw.slice(0, colon); + let rest = raw.slice(colon + 1); + let op: TokenOp = '='; + if (rest.startsWith('!')) { + op = '!='; + rest = rest.slice(1); + } else if (rest.startsWith('>=')) { + op = '>='; + rest = rest.slice(2); + } else if (rest.startsWith('<=')) { + op = '<='; + rest = rest.slice(2); + } else if (rest.startsWith('>')) { + op = '>'; + rest = rest.slice(1); + } else if (rest.startsWith('<')) { + op = '<'; + rest = rest.slice(1); + } + const values = rest.split(',').filter((s) => s.length > 0); + if (values.length === 0) { + errors.push({ token: raw, reason: 'missing value' }); + continue; + } + if (!KNOWN_KEYS.has(key)) { + errors.push({ + token: raw, + reason: `unknown key "${key}" — try: ${[...KNOWN_KEYS].join(', ')}`, + }); + // Still push so the chip can render (in red) and be removable. + tokens.push({ key, op, values, raw }); + continue; + } + tokens.push({ key, op, values, raw }); + } + return { tokens, errors }; +} + +/** Pre-compute helper sets used by some matchers. Call once per records[] + query. */ +export interface MatchContext { + /** tool_call_ids of tool_call records whose tool_name matches a `tool:` token. */ + toolMatchingCallIds: Set; +} + +export function buildMatchContext( + records: readonly VisWireRecord[], + query: ParsedQuery, +): MatchContext { + const ctx: MatchContext = { toolMatchingCallIds: new Set() }; + const toolTokens = query.tokens.filter((t) => t.key === 'tool'); + if (toolTokens.length === 0) return ctx; + for (const r of records) { + if (r.type !== 'tool_call') continue; + const name = getToolName(r); + if (name === null) continue; + const lower = name.toLowerCase(); + const anyMatch = toolTokens.every((t) => matchTextToken(lower, t)); + if (!anyMatch) continue; + const id = getToolCallId(r); + if (id !== null) ctx.toolMatchingCallIds.add(id); + } + return ctx; +} + +export function matchesQuery( + record: VisWireRecord, + query: ParsedQuery, + ctx: MatchContext, +): boolean { + if (query.tokens.length === 0) return true; + for (const token of query.tokens) { + if (!matchesOne(record, token, ctx)) return false; + } + return true; +} + +function matchesOne(r: VisWireRecord, t: QueryToken, ctx: MatchContext): boolean { + if (t.key === null) { + // Bare text — fallback to JSON substring, case-insensitive. + const needle = (t.values[0] ?? '').toLowerCase(); + if (needle.length === 0) return true; + try { + return JSON.stringify(r).toLowerCase().includes(needle); + } catch { + return false; + } + } + switch (t.key) { + case 'type': + return matchType(r.type, t); + case 'tool': + return matchTool(r, t, ctx); + case 'turn': + return matchText(getField(r, 'turn_id'), t); + case 'seq': + return matchNumber(r.seq, t); + case 'error': + return matchError(r, t); + case 'agent': + return matchAgent(r, t); + case 'id': + return matchAnyId(r, t); + default: + // Unknown key — treat as no-match so chip is clearly inert. + return false; + } +} + +function matchType(type: WireRecordType, t: QueryToken): boolean { + const hit = t.values.includes(type); + return t.op === '!=' ? !hit : hit; +} + +function matchTool(r: VisWireRecord, t: QueryToken, ctx: MatchContext): boolean { + if (r.type === 'tool_call') { + const name = getToolName(r); + return name !== null && matchTextToken(name.toLowerCase(), t); + } + if (r.type === 'tool_result') { + const id = getField(r, 'tool_call_id'); + return id !== null && ctx.toolMatchingCallIds.has(id); + } + if (r.type === 'tool_denied') { + const name = (getField(r, 'data.tool_name') ?? '').toLowerCase(); + return matchTextToken(name, t); + } + return false; +} + +function matchText(value: string | null, t: QueryToken): boolean { + if (value === null) return t.op === '!='; + return matchTextToken(value.toLowerCase(), t); +} + +function matchTextToken(haystackLower: string, t: QueryToken): boolean { + const hit = textHit(haystackLower, t); + return t.op === '!=' ? !hit : hit; +} + +/** Pure "does this haystack contain any of the token values?" — no op + * inversion. Callers that check multiple candidate fields must invert + * the combined result themselves, otherwise `agent:!abc` over a record + * that has one matching field and three null fields would incorrectly + * flip for the null fields and report success. */ +function textHit(haystackLower: string, t: QueryToken): boolean { + return t.values.some((v) => haystackLower.includes(v.toLowerCase())); +} + +function matchNumber(value: number, t: QueryToken): boolean { + const n = Number(t.values[0]); + if (!Number.isFinite(n)) return false; + switch (t.op) { + case '>': + return value > n; + case '<': + return value < n; + case '>=': + return value >= n; + case '<=': + return value <= n; + case '!=': + return value !== n; + case '=': + case '~': + return value === n; + default: + return false; + } +} + +function matchError(r: VisWireRecord, t: QueryToken): boolean { + const want = (t.values[0] ?? 'true').toLowerCase() !== 'false'; + const isError = + r.type === 'subagent_failed' || + r.type === 'tool_denied' || + (r.type === 'tool_result' && (r as { is_error?: boolean }).is_error === true) || + (r.type === 'turn_end' && (r as { success?: boolean }).success === false); + return want ? isError : !isError; +} + +function matchAgent(r: VisWireRecord, t: QueryToken): boolean { + const candidates: string[] = [ + getField(r, 'agent_id'), + getField(r, 'data.agent_id'), + getField(r, 'parent_agent_id'), + getField(r, 'data.parent_agent_id'), + ].filter((c): c is string => c !== null); + return matchAcrossCandidates(candidates, t); +} + +function matchAnyId(r: VisWireRecord, t: QueryToken): boolean { + const candidates: string[] = [ + getField(r, 'uuid'), + getField(r, 'step_uuid'), + getField(r, 'turn_id'), + getField(r, 'tool_call_id'), + getField(r, 'session_id'), + getField(r, 'agent_id'), + getField(r, 'data.agent_id'), + getField(r, 'data.tool_call_id'), + getField(r, 'data.mail_id'), + ].filter((c): c is string => c !== null); + return matchAcrossCandidates(candidates, t); +} + +/** op='=' : any candidate contains any token value + * op='!=': NO candidate contains any token value + * Handles the op inversion at the aggregate level so records with + * multiple candidate fields behave sanely (the per-field `matchTextToken` + * would flip on each null/non-match independently — wrong for multi-field + * records). */ +function matchAcrossCandidates(candidates: string[], t: QueryToken): boolean { + const anyHit = candidates.some((c) => textHit(c.toLowerCase(), t)); + return t.op === '!=' ? !anyHit : anyHit; +} + +// ── field access helpers ───────────────────────────────────────────── + +function getField(r: VisWireRecord, path: string): string | null { + const parts = path.split('.'); + let cur: unknown = r; + for (const p of parts) { + if (cur === null || cur === undefined || typeof cur !== 'object') return null; + cur = (cur as Record)[p]; + } + return typeof cur === 'string' ? cur : null; +} + +function getToolName(r: VisWireRecord): string | null { + // Prefer `tool_call.data.tool_name`; fall back to a legacy top-level field + // for older records. + const data = (r as { data?: Record }).data; + if (data && typeof data['tool_name'] === 'string') return data['tool_name']; + const top = (r as Record)['tool_name']; + return typeof top === 'string' ? top : null; +} + +function getToolCallId(r: VisWireRecord): string | null { + const data = (r as { data?: Record }).data; + if (data && typeof data['tool_call_id'] === 'string') return data['tool_call_id']; + const top = (r as Record)['tool_call_id']; + return typeof top === 'string' ? top : null; +} diff --git a/apps/vis/web/src/main.tsx b/apps/vis/web/src/main.tsx new file mode 100644 index 000000000..7d903ffe9 --- /dev/null +++ b/apps/vis/web/src/main.tsx @@ -0,0 +1,30 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { BrowserRouter } from 'react-router-dom'; +import { App } from './App'; +import './theme.css'; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: Infinity, + gcTime: 30 * 60 * 1000, + retry: false, + refetchOnWindowFocus: false, + }, + }, +}); + +const rootEl = document.querySelector('#root'); +if (!rootEl) throw new Error('#root not found'); + +createRoot(rootEl).render( + + + + + + + , +); diff --git a/apps/vis/web/src/pages/SessionDetailPage.tsx b/apps/vis/web/src/pages/SessionDetailPage.tsx new file mode 100644 index 000000000..649b3cb0b --- /dev/null +++ b/apps/vis/web/src/pages/SessionDetailPage.tsx @@ -0,0 +1,158 @@ +import { useParams } from 'react-router-dom'; +import { useSession } from '../hooks/useSession'; +import { useWire } from '../hooks/useWire'; +import { useSessionContext } from '../hooks/useContext'; +import { useSubagents } from '../hooks/useSubagents'; +import { TabBar, useActiveTab } from '../components/layout/TabBar'; +import { WireTab } from '../components/wire/WireTab'; +import { ContextTab } from '../components/context/ContextTab'; +import { SubagentsTab } from '../components/subagents/SubagentsTab'; +import { StateTab } from '../components/state/StateTab'; +import { FilesTab } from '../components/files/FilesTab'; +import { CopyButton } from '../components/shared/CopyButton'; +import { Pill } from '../components/shared/Pill'; +import { formatAbsoluteTime, formatRelativeTime } from '../util/time'; + +type TabId = 'wire' | 'context' | 'subagents' | 'state' | 'files'; + +export function SessionDetailPage() { + const { sessionId } = useParams<{ sessionId: string }>(); + const active = useActiveTab('wire') as TabId; + + const { data: session, isLoading: sessionLoading, error: sessionError } = useSession(sessionId); + + // Eagerly count subagents from the session detail; fetch wire/context/subagents + // only when their tab is open. + const wireQ = useWire(sessionId, active === 'wire'); + const contextQ = useSessionContext(sessionId, active === 'context'); + const subQ = useSubagents(sessionId, active === 'subagents'); + + if (!sessionId) return
    (no session id)
    ; + if (sessionLoading) return
    loading session…
    ; + if (sessionError) + return ( +
    + {(sessionError).message} +
    + ); + if (!session) return null; + + return ( +
    + {/* Detail header */} +
    +
    + {session.session_id} + + {session.state.model ? ( + {session.state.model} + ) : null} + {session.state.archived ? ( + archived + ) : null} + {session.state.permission_mode === 'bypassPermissions' ? ( + yolo + ) : null} + {session.title ? ( + + "{session.title}" + + ) : null} +
    +
    + {session.state.workspace_dir ? ( + + {session.state.workspace_dir} + + ) : null} + {session.state.updated_at ? ( + + updated {formatRelativeTime(session.state.updated_at)} ·{' '} + {formatAbsoluteTime(session.state.updated_at)} + + ) : null} +
    + {session.last_prompt ? ( +
    + prompt · {session.last_prompt} +
    + ) : null} +
    + + + +
    + {active === 'wire' ? ( + wireQ.isLoading ? ( + loading wire… + ) : wireQ.error ? ( + + ) : wireQ.data ? ( + + ) : null + ) : null} + + {active === 'context' ? ( + contextQ.isLoading ? ( + loading context… + ) : contextQ.error ? ( + + ) : contextQ.data ? ( + + ) : null + ) : null} + + {active === 'subagents' ? ( + subQ.isLoading ? ( + loading subagents… + ) : ( + + ) + ) : null} + + {active === 'state' ? ( + + ) : null} + + {active === 'files' ? ( + + ) : null} +
    +
    + ); +} + +function Centered({ children }: { children: import('react').ReactNode }) { + return ( +
    + {children} +
    + ); +} + +function ErrorView({ msg }: { msg: string }) { + return ( +
    {msg}
    + ); +} diff --git a/apps/vis/web/src/pages/SessionListPage.tsx b/apps/vis/web/src/pages/SessionListPage.tsx new file mode 100644 index 000000000..556ca63b3 --- /dev/null +++ b/apps/vis/web/src/pages/SessionListPage.tsx @@ -0,0 +1,35 @@ +import { useSessions } from '../hooks/useSession'; + +export function SessionListPage() { + const { data } = useSessions(); + const count = data?.length ?? 0; + + return ( +
    +
    +
    + kimi vis +
    +
    + select a session from the left rail to begin inspecting +
    +
    +
    +
    {count}
    +
    sessions
    +
    +
    +
    +
    + / + focus search +
    +
    + esc + close drawers +
    +
    +
    +
    + ); +} diff --git a/apps/vis/web/src/pages/SubagentDetailPage.tsx b/apps/vis/web/src/pages/SubagentDetailPage.tsx new file mode 100644 index 000000000..c5ef4868f --- /dev/null +++ b/apps/vis/web/src/pages/SubagentDetailPage.tsx @@ -0,0 +1,206 @@ +import { Link, useParams } from 'react-router-dom'; +import { useSubagentWire } from '../hooks/useWire'; +import { useSubagentContext } from '../hooks/useContext'; +import { useSubagentMeta } from '../hooks/useSubagents'; +import { TabBar, useActiveTab } from '../components/layout/TabBar'; +import { WireTab } from '../components/wire/WireTab'; +import { ContextTab } from '../components/context/ContextTab'; +import { JsonViewer } from '../components/shared/JsonViewer'; +import { Pill } from '../components/shared/Pill'; +import { formatAbsoluteTime, formatRelativeTime } from '../util/time'; + +type TabId = 'wire' | 'context' | 'meta'; + +export function SubagentDetailPage() { + const { sessionId, agentId } = useParams<{ sessionId: string; agentId: string }>(); + const active = useActiveTab('wire') as TabId; + + const wireQ = useSubagentWire(sessionId, agentId, active === 'wire'); + const contextQ = useSubagentContext(sessionId, agentId, active === 'context'); + const metaQ = useSubagentMeta(sessionId, agentId); + + if (!sessionId || !agentId) return null; + + return ( +
    + {/* Breadcrumb + header */} +
    +
    + + ‹ back to subagents + + · + main + + + {agentId.replace(/^sub_/, '').slice(0, 12)} + +
    +
    + {agentId} + {metaQ.data?.meta_json?.subagent_type ? ( + + {metaQ.data.meta_json.subagent_type} + + ) : null} + {metaQ.data?.meta_json?.status ? ( + + {metaQ.data.meta_json.status} + + ) : null} + {metaQ.data?.depth !== undefined ? ( + + depth {metaQ.data.depth} + + ) : null} +
    + {metaQ.data?.meta_json?.description ? ( +
    + {metaQ.data.meta_json.description} +
    + ) : null} +
    + + + +
    + {active === 'wire' ? ( + wireQ.isLoading ? ( + loading wire… + ) : wireQ.error ? ( + + ) : wireQ.data ? ( + + ) : null + ) : null} + + {active === 'context' ? ( + contextQ.isLoading ? ( + loading context… + ) : contextQ.error ? ( + + ) : contextQ.data ? ( + + ) : null + ) : null} + + {active === 'meta' ? : null} +
    +
    + ); +} + +function MetaView({ meta, error }: { meta: unknown; error: unknown }) { + if (error) { + return ; + } + if (!meta) return loading meta…; + const m = meta as { + meta_json: unknown; + spawned_record: unknown; + completed_record: unknown; + failed_record: unknown; + }; + return ( +
    + + + + + +
    + ); +} + +function MetaSection({ + title, + subtitle, + value, +}: { + title: string; + subtitle?: string; + value: unknown; +}) { + return ( +
    +

    + {title} + {subtitle ? {subtitle} : null} +

    +
    + {value === null || value === undefined ? ( + (not present) + ) : ( + + )} +
    +
    + ); +} + +function TimestampsSection({ spawned, completed, failed }: { spawned: unknown; completed: unknown; failed: unknown }) { + const rows = [ + spawned ? ['spawn', (spawned as { time?: number }).time] : null, + completed ? ['complete', (completed as { time?: number }).time] : null, + failed ? ['failure', (failed as { time?: number }).time] : null, + ].filter(Boolean) as [string, number | undefined][]; + if (rows.length === 0) return null; + return ( +
    +

    timeline

    +
    + + + {rows.map(([label, t]) => ( + + + + + ))} + +
    + {label} + + {t !== undefined ? ( + <> + {formatAbsoluteTime(t)}{' '} + ({formatRelativeTime(t)}) + + ) : ( + + )} +
    +
    +
    + ); +} + +function Centered({ children }: { children: import('react').ReactNode }) { + return ( +
    + {children} +
    + ); +} + +function ErrorView({ msg }: { msg: string }) { + return ( +
    {msg}
    + ); +} diff --git a/apps/vis/web/src/theme.css b/apps/vis/web/src/theme.css new file mode 100644 index 000000000..aa9f62309 --- /dev/null +++ b/apps/vis/web/src/theme.css @@ -0,0 +1,239 @@ +@import "tailwindcss"; + +/* ───────────────────────────────────────────────────────────────────────── + Design tokens. Tailwind v4 @theme registers the variables AND generates + utilities like `bg-surface-0`, `text-fg-0`. Dark theme is the baseline; + light theme overrides the same variable names below. + ───────────────────────────────────────────────────────────────────────── */ + +@theme { + --font-mono: "JetBrains Mono", "SF Mono", Menlo, Consolas, monospace; + --font-ui: "Geist", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + + /* Surface (dark baseline) */ + --color-surface-0: #0b0d12; + --color-surface-1: #12151c; + --color-surface-2: #1a1e27; + --color-surface-3: #242936; + --color-border: #1f2430; + --color-border-strong: #2d3342; + + /* Foreground (dark baseline) */ + --color-fg-0: #e4e7ef; + --color-fg-1: #a8afc0; + --color-fg-2: #6c7488; + --color-fg-3: #454c5e; + + /* Text color used on top of a solid accent pill (so contrast stays + legible regardless of which accent). Dark theme: near-black. */ + --color-on-accent: #0b0d12; + + /* Category accents (dark baseline) */ + --color-cat-conversation: #22d3ee; + --color-cat-config: #a78bfa; + --color-cat-lifecycle: #94a3b8; + --color-cat-subagent: #8b5cf6; + --color-cat-approval: #fb923c; + --color-cat-ephemeral: #facc15; + --color-cat-meta: #6366f1; + --color-cat-tools: #f59e0b; + + /* Conversation subfamily */ + --color-user: #38bdf8; + --color-assistant: #22d3ee; + --color-tool: #f59e0b; + --color-compaction: #f97316; + --color-turn: #14b8a6; + + /* Severity */ + --color-sev-info: #60a5fa; + --color-sev-success: #4ade80; + --color-sev-warning: #fbbf24; + --color-sev-error: #f87171; +} + +/* ───────────────────────────────────────────────────────────────────────── + Light-theme overrides. Applied when . + Accents get darker variants for AA contrast on light backgrounds. + ───────────────────────────────────────────────────────────────────────── */ + +:root[data-theme="light"] { + color-scheme: light; + + --color-surface-0: #fafbfc; + --color-surface-1: #eef1f6; + --color-surface-2: #e4e8f0; + --color-surface-3: #d4d9e4; + --color-border: #dde1ea; + --color-border-strong: #bfc6d4; + + --color-fg-0: #0f1219; + --color-fg-1: #3a4154; + --color-fg-2: #616878; + --color-fg-3: #9097a8; + + --color-on-accent: #fafbfc; + + --color-cat-conversation: #0891b2; + --color-cat-config: #7c3aed; + --color-cat-lifecycle: #475569; + --color-cat-subagent: #6d28d9; + --color-cat-approval: #c2410c; + --color-cat-ephemeral: #a16207; + --color-cat-meta: #4338ca; + --color-cat-tools: #b45309; + + --color-user: #0369a1; + --color-assistant: #0891b2; + --color-tool: #b45309; + --color-compaction: #c2410c; + --color-turn: #0d9488; + + --color-sev-info: #2563eb; + --color-sev-success: #15803d; + --color-sev-warning: #b45309; + --color-sev-error: #b91c1c; +} + +:root[data-theme="dark"] { + color-scheme: dark; +} + +/* ───────────────────────────────────────────────────────────────────────── + Base + ───────────────────────────────────────────────────────────────────────── */ + +html, +body, +#root { + height: 100%; + margin: 0; +} + +body { + font-family: var(--font-ui); + font-size: 13px; + line-height: 1.45; + background-color: var(--color-surface-0); + color: var(--color-fg-0); +} + +/* Subtle noise grain only on dark — adds industrial texture. On light it + just looks dirty, so omit. */ +:root:not([data-theme="light"]) body { + background-image: url("data:image/svg+xml;utf8,"); + background-repeat: repeat; +} + +/* Force-disable ligatures and contextual alternates in monospace — debug + tool must render characters verbatim. */ +.font-mono { + font-feature-settings: "liga" 0, "calt" 0, "ss01" 0; + font-variant-ligatures: none; + font-family: var(--font-mono); +} + +.tabular { + font-variant-numeric: tabular-nums; +} + +/* Scrollbar */ +* { + scrollbar-width: thin; + scrollbar-color: var(--color-border-strong) transparent; +} + +*::-webkit-scrollbar { + width: 10px; + height: 10px; +} + +*::-webkit-scrollbar-track { + background: transparent; +} + +*::-webkit-scrollbar-thumb { + background: var(--color-border-strong); + border-radius: 0; + border: 2px solid transparent; + background-clip: padding-box; +} + +*::-webkit-scrollbar-thumb:hover { + background: var(--color-fg-3); + background-clip: padding-box; + border: 2px solid transparent; +} + +::selection { + background-color: color-mix(in oklab, var(--color-cat-conversation) 30%, transparent); + color: var(--color-fg-0); +} + +*:focus-visible { + outline: 1px solid var(--color-cat-conversation); + outline-offset: 1px; +} + +/* Button + anchor element resets — kept inside @layer base so Tailwind + utilities (px-*, py-*, etc.) in the utilities layer win over them. + Unlayered rules would otherwise beat layered utilities in the cascade. */ +@layer base { + button { + font-family: inherit; + font-size: inherit; + background: none; + border: none; + padding: 0; + color: inherit; + cursor: pointer; + } + + a { + color: inherit; + text-decoration: none; + } +} + +.rule { + border-top: 1px solid var(--color-border); +} + +.accent-bar { + width: 3px; + flex-shrink: 0; + align-self: stretch; +} + +.ephemeral-border { + border: 1px dashed var(--color-cat-ephemeral); + border-radius: 2px; +} + +.ephemeral-border-info { + border-color: var(--color-sev-info); +} +.ephemeral-border-success { + border-color: var(--color-sev-success); +} +.ephemeral-border-warning { + border-color: var(--color-sev-warning); +} +.ephemeral-border-error { + border-color: var(--color-sev-error); +} + +.pill { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 1px 6px; + font-family: var(--font-mono); + font-size: 10.5px; + font-weight: 500; + letter-spacing: 0.02em; + text-transform: uppercase; + border-radius: 2px; + white-space: nowrap; + line-height: 1.4; +} diff --git a/apps/vis/web/src/types.ts b/apps/vis/web/src/types.ts new file mode 100644 index 000000000..1731b202a --- /dev/null +++ b/apps/vis/web/src/types.ts @@ -0,0 +1,359 @@ +// Client-side types mirroring the server API contract. +// Kept in sync manually — the server owns the source of truth. + +export type WireRecordType = + // File-header (line 1) and startup baseline (line 2) — vis splices + // these back into the timeline so the Wire tab shows every line of + // wire.jsonl, including the system_prompt. + | 'metadata' + | 'session_initialized' + | 'turn_begin' + | 'turn_end' + | 'user_message' + | 'tool_result' + | 'compaction' + | 'system_prompt_changed' + | 'tools_changed' + | 'system_reminder' + | 'notification' + // Atomic streaming records (replace assistant_message + tool_call_dispatched). + | 'step_begin' + | 'step_end' + | 'content_part' + | 'tool_call' + | 'tool_denied' + | 'skill_invoked' + | 'skill_completed' + | 'approval_request' + | 'approval_response' + | 'team_mail' + | 'subagent_spawned' + | 'subagent_completed' + | 'subagent_failed' + | 'ownership_changed' + | 'context_edit' + | 'context_cleared'; + +export type WireCategory = + | 'conversation' + | 'config' + | 'lifecycle' + | 'subagent' + | 'approval' + | 'ephemeral' + | 'meta' + | 'tools'; + +// Discriminated union — loosely typed so we can dispatch on .type without +// needing server-synchronized exact interfaces in the client. The server's +// own `types.ts` has the precise field types. +export interface VisWireRecordBase { + type: WireRecordType; + seq: number; + time: number; + [key: string]: unknown; +} + +export type VisWireRecord = VisWireRecordBase; + +export interface WireFileMetadata { + type: 'metadata'; + protocol_version: string; + created_at: number; + kimi_version?: string; + producer?: { + kind: 'python' | 'typescript'; + name: string; + version: string; + }; +} + +// ──────────── Session ──────────── + +export interface SessionSummary { + session_id: string; + title: string | null; + last_prompt: string | null; + created_at: number; + updated_at: number; + last_turn_time: number | null; + model: string | null; + permission_mode: string | null; + last_exit_code: 'clean' | 'dirty' | null; + custom_title: string | null; + tags: string[]; + archived: boolean; + workspace_dir: string | null; + wire_protocol_version: string | null; + wire_record_count: number; + archive_count: number; + subagent_count: number; + health: 'ok' | 'broken' | 'missing_wire'; +} + +export interface SessionState { + session_id: string; + title?: string; + isCustomTitle?: boolean; + customTitle?: string; + lastPrompt?: string; + last_prompt?: string; + model?: string; + last_turn_id?: string; + last_turn_time?: number; + turn_count?: number; + created_at: number; + updated_at: number; + workspace_dir?: string; + auto_approve_actions?: string[]; + permission_mode?: 'default' | 'acceptEdits' | 'bypassPermissions'; + thinking_level?: string; + custom_title?: string; + plan_mode?: boolean; + plan_slug?: string; + tags?: string[]; + description?: string; + archived?: boolean; + color?: string; + last_exit_code?: 'clean' | 'dirty'; + [key: string]: unknown; +} + +export interface SessionDetail { + session_id: string; + title: string | null; + last_prompt: string | null; + state: SessionState; + subagent_ids: string[]; + archive_files: string[]; + tool_result_ids: string[]; + wire_metadata: WireFileMetadata | null; +} + +// ──────────── Wire ──────────── + +export interface WireResponse { + session_id: string; + agent_id: string | null; + files_read: string[]; + health: 'ok' | 'broken'; + broken_reason?: string; + warnings: string[]; + records: VisWireRecord[]; +} + +// ──────────── Context ──────────── + +export type MessageOrigin = + | { kind: 'user' } + | { kind: 'assistant' } + | { kind: 'tool'; tool_call_id: string } + | { kind: 'system_reminder'; seq: number } + | { kind: 'notification'; seq: number; notification_id: string; severity: string }; + +export interface ContentPart { + type: 'text' | 'think' | 'image_url' | 'video_url'; + [key: string]: unknown; +} + +export interface ToolCallEntry { + type: 'function'; + id: string; + function: { name: string; arguments: string | null }; +} + +export interface AnnotatedMessage { + seq: number; + message: { + role: 'user' | 'assistant' | 'tool'; + content: ContentPart[]; + tool_calls: ToolCallEntry[]; + tool_call_id?: string; + }; + origin: MessageOrigin; + is_ephemeral: boolean; + out_of_context: boolean; + persisted_output_path?: string; +} + +export interface ProjectedStateSummary { + model: string | null; + system_prompt: string | null; + active_tools: string[]; + last_seq: number; + token_count: number; + permission_mode: string | null; + plan_mode: boolean; + input_tokens: number; + output_tokens: number; + cache_read_tokens: number; + cache_write_tokens: number; +} + +export interface ContextResponse { + session_id: string; + agent_id: string | null; + annotated_messages: AnnotatedMessage[]; + projected_state: ProjectedStateSummary; +} + +// ──────────── Subagents ──────────── + +export interface SubagentNode { + agent_id: string; + agent_name: string | null; + subagent_type: string | null; + run_in_background: boolean; + parent_agent_id: string | null; + depth: number; + status: 'running' | 'completed' | 'failed' | 'killed' | 'lost' | 'unknown'; + success: boolean | null; + result_summary: string | null; + error: string | null; + spawn_seq: number; + spawn_time: number; + children: SubagentNode[]; +} + +export interface SubagentTreeResponse { + session_id: string; + tree: SubagentNode[]; +} + +export interface SubagentMetaResponse { + agent_id: string; + session_id: string; + meta_json: { + agent_id: string; + subagent_type: string; + status: string; + description: string; + parent_tool_call_id: string; + created_at: number; + updated_at: number; + } | null; + spawned_record: { + agent_name?: string; + parent_tool_call_id: string; + parent_agent_id?: string; + run_in_background: boolean; + seq: number; + time: number; + } | null; + completed_record: { + parent_tool_call_id: string; + result_summary: string; + usage?: { + input_tokens: number; + output_tokens: number; + cache_read_tokens?: number; + cache_write_tokens?: number; + }; + seq: number; + time: number; + } | null; + failed_record: { + parent_tool_call_id: string; + error: string; + seq: number; + time: number; + } | null; + depth: number; +} + +export interface ToolResultFileResponse { + tool_call_id: string; + session_id: string; + size_bytes: number; + content: string; +} + +export interface ApiError { + error: string; + code: + | 'NOT_FOUND' + | 'BAD_REQUEST' + | 'UNAUTHORIZED' + | 'READ_ERROR' + | 'PARSE_ERROR' + | 'DELETE_ERROR'; +} + +export interface DeleteSessionResponse { + session_id: string; + deleted: true; +} + +export interface ClearSessionsResponse { + deleted_count: number; + failed: Array<{ session_id: string; error: string }>; +} + +// ──────────── Category mapping ──────────── + +export const TYPE_CATEGORY: Record = { + metadata: 'meta', + session_initialized: 'config', + turn_begin: 'conversation', + turn_end: 'conversation', + user_message: 'conversation', + tool_result: 'conversation', + compaction: 'conversation', + system_prompt_changed: 'config', + tools_changed: 'config', + system_reminder: 'ephemeral', + notification: 'meta', + step_begin: 'conversation', + step_end: 'conversation', + content_part: 'conversation', + tool_call: 'tools', + tool_denied: 'approval', + skill_invoked: 'tools', + skill_completed: 'tools', + approval_request: 'approval', + approval_response: 'approval', + team_mail: 'meta', + subagent_spawned: 'subagent', + subagent_completed: 'subagent', + subagent_failed: 'subagent', + ownership_changed: 'lifecycle', + context_edit: 'lifecycle', + context_cleared: 'lifecycle', +}; + +// Context-effect marker — `true` if the record mutates the live context, +// `false` if it is telemetry-only, `'conditional'` for records that depend +// on per-record fields (notification targets, system_reminder delivery, …). +export const TYPE_CTX_EFFECT: Record = { + // File header — purely informational, not a context event. + metadata: false, + // Startup baseline — seeds system_prompt / model / active_tools that + // the projector uses as the context floor. + session_initialized: true, + turn_begin: false, + turn_end: false, + user_message: true, + tool_result: true, + compaction: true, + system_prompt_changed: true, + tools_changed: true, + system_reminder: 'conditional', + notification: 'conditional', + // Streaming atoms together rebuild an assistant message → context-affecting. + step_begin: true, + step_end: false, + content_part: true, + tool_call: true, + tool_denied: false, + skill_invoked: false, + skill_completed: false, + approval_request: false, + approval_response: false, + team_mail: false, + subagent_spawned: false, + subagent_completed: false, + subagent_failed: false, + ownership_changed: false, + context_edit: true, + context_cleared: true, +}; diff --git a/apps/vis/web/src/util/time.ts b/apps/vis/web/src/util/time.ts new file mode 100644 index 000000000..27246cbd6 --- /dev/null +++ b/apps/vis/web/src/util/time.ts @@ -0,0 +1,33 @@ +/** Format an epoch-ms timestamp as a short relative string ("2m ago", "3h ago"). */ +export function formatRelativeTime(epochMs: number): string { + if (!epochMs || !Number.isFinite(epochMs)) return '—'; + const diff = Date.now() - epochMs; + if (diff < 0) return 'just now'; + const s = Math.floor(diff / 1000); + if (s < 60) return `${s}s ago`; + const m = Math.floor(s / 60); + if (m < 60) return `${m}m ago`; + const h = Math.floor(m / 60); + if (h < 24) return `${h}h ago`; + const d = Math.floor(h / 24); + if (d < 30) return `${d}d ago`; + const mo = Math.floor(d / 30); + if (mo < 12) return `${mo}mo ago`; + return `${Math.floor(mo / 12)}y ago`; +} + +/** Format an epoch-ms timestamp as ISO-ish local time (YYYY-MM-DD HH:MM:SS). */ +export function formatAbsoluteTime(epochMs: number): string { + if (!epochMs || !Number.isFinite(epochMs)) return '—'; + const d = new Date(epochMs); + const pad = (n: number) => String(n).padStart(2, '0'); + return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`; +} + +/** Format an epoch-ms timestamp as HH:MM:SS (wall clock). */ +export function formatWallClock(epochMs: number): string { + if (!epochMs || !Number.isFinite(epochMs)) return '--:--:--'; + const d = new Date(epochMs); + const pad = (n: number) => String(n).padStart(2, '0'); + return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`; +} diff --git a/apps/vis/web/test/api.test.ts b/apps/vis/web/test/api.test.ts new file mode 100644 index 000000000..ef71955f3 --- /dev/null +++ b/apps/vis/web/test/api.test.ts @@ -0,0 +1,39 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; + +import { api } from '../src/api'; + +describe('vis web api auth token handling', () => { + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it('scrubs token parameters from the browser URL after persisting the token', async () => { + const setItem = vi.fn(); + const getItem = vi.fn(); + const replaceState = vi.fn(); + const location = new URL('http://localhost:3001/?foo=bar&token=secret#token=secret&tab=wire'); + + vi.stubGlobal('window', { + history: { replaceState }, + localStorage: { getItem, setItem }, + location, + }); + const fetchMock = vi.fn( + async () => + new Response('[]', { + headers: { 'content-type': 'application/json' }, + status: 200, + }), + ); + vi.stubGlobal('fetch', fetchMock); + + await api.listSessions(); + + expect(setItem).toHaveBeenCalledWith('kimi-vis-auth-token', 'secret'); + expect(fetchMock).toHaveBeenCalledWith('/api/sessions', { + headers: { accept: 'application/json', authorization: 'Bearer secret' }, + method: 'GET', + }); + expect(replaceState).toHaveBeenCalledWith(null, '', 'http://localhost:3001/?foo=bar#tab=wire'); + }); +}); diff --git a/apps/vis/web/tsconfig.json b/apps/vis/web/tsconfig.json new file mode 100644 index 000000000..e1ecfcd8a --- /dev/null +++ b/apps/vis/web/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2023", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": false, + "types": ["vite/client"] + }, + "include": ["src"] +} diff --git a/apps/vis/web/vite.config.ts b/apps/vis/web/vite.config.ts new file mode 100644 index 000000000..646a173cb --- /dev/null +++ b/apps/vis/web/vite.config.ts @@ -0,0 +1,22 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import tailwindcss from '@tailwindcss/vite'; + +export default defineConfig({ + plugins: [react(), tailwindcss()], + server: { + port: 5174, + strictPort: true, + proxy: { + '/api': { + target: 'http://localhost:3001', + changeOrigin: true, + }, + }, + }, + build: { + outDir: 'dist', + emptyOutDir: true, + target: 'es2022', + }, +}); diff --git a/build/nix/update-pnpm-deps.sh b/build/nix/update-pnpm-deps.sh new file mode 100644 index 000000000..de2eb556a --- /dev/null +++ b/build/nix/update-pnpm-deps.sh @@ -0,0 +1,201 @@ +set -euo pipefail + +ROOT="$(git rev-parse --show-toplevel)" +cd "$ROOT" + +FLAKE="$ROOT/flake.nix" +FAKE_HASH="sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" +CACHE_VERSION="v1" +FETCHER_VERSION="3" +CACHE_FILE="$ROOT/.git/kimi-code/pnpm-deps-hashes-$CACHE_VERSION.json" +RESTORE_ORIG_HASH=0 + +ORIG_HASH="$(grep -E -o 'hash = "sha256-[A-Za-z0-9+/=]+"' "$FLAKE" \ + | head -n 1 \ + | sed -E 's/hash = "(.*)"/\1/')" +if [ -z "$ORIG_HASH" ]; then + echo "error: could not find pnpmDeps hash in flake.nix" >&2 + exit 1 +fi + +set_hash() { + sed -i.bak -E "s|hash = \"sha256-[A-Za-z0-9+/=]+\"|hash = \"$1\"|" "$FLAKE" + rm -f "$FLAKE.bak" +} + +cleanup() { + if [ "$RESTORE_ORIG_HASH" = "1" ]; then + set_hash "$ORIG_HASH" + fi +} +trap cleanup EXIT +trap 'exit 130' INT TERM + +hash_stream() { + if command -v sha256sum >/dev/null 2>&1; then + sha256sum | sed -E 's/[[:space:]].*$//' + else + shasum -a 256 | sed -E 's/[[:space:]].*$//' + fi +} + +hash_file() { + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$1" | sed -E 's/[[:space:]].*$//' + else + shasum -a 256 "$1" | sed -E 's/[[:space:]].*$//' + fi +} + +print_file_fingerprint() { + path="$1" + if [ -f "$path" ]; then + printf 'file:%s\n' "$path" + hash_file "$path" + printf '\n' + else + printf 'missing:%s\n' "$path" + fi +} + +input_fingerprint() { + { + printf 'cacheVersion=%s\n' "$CACHE_VERSION" + printf 'fetcherVersion=%s\n' "$FETCHER_VERSION" + + printf 'file:flake.nix(normalized-pnpmDeps-hash)\n' + sed -E 's|hash = "sha256-[A-Za-z0-9+/=]+"|hash = "sha256-"|' "$FLAKE" + printf '\n' + + for path in \ + .npmrc \ + flake.lock \ + package.json \ + pnpm-lock.yaml \ + pnpm-workspace.yaml + do + print_file_fingerprint "$path" + done + + git ls-files --cached --others --exclude-standard -- \ + '*/package.json' \ + '.pnpmfile.cjs' \ + 'patches/**' \ + | sort -u \ + | while IFS= read -r path; do + [ -n "$path" ] || continue + [ "$path" = "package.json" ] && continue + print_file_fingerprint "$path" + done + } | hash_stream +} + +read_cached_hash() { + [ -f "$CACHE_FILE" ] || return 0 + + # shellcheck disable=SC2016 + node -e ' +const fs = require("node:fs"); +const [file, key] = process.argv.slice(1); +const parsed = JSON.parse(fs.readFileSync(file, "utf8")); +const entry = parsed[key]; +if (entry && typeof entry.hash === "string") { + console.log(entry.hash); +} +' "$CACHE_FILE" "$INPUT_KEY" +} + +write_cached_hash() { + hash="$1" + mkdir -p "$(dirname "$CACHE_FILE")" + + # shellcheck disable=SC2016 + node -e ' +const fs = require("node:fs"); +const [file, key, hash, createdAt] = process.argv.slice(1); +let parsed = {}; +try { + if (fs.existsSync(file)) { + parsed = JSON.parse(fs.readFileSync(file, "utf8")); + } +} catch { + parsed = {}; +} +parsed[key] = { hash, createdAt }; +fs.writeFileSync(`${file}.tmp`, `${JSON.stringify(parsed, null, 2)}\n`); +fs.renameSync(`${file}.tmp`, file); +' "$CACHE_FILE" "$INPUT_KEY" "$hash" "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" +} + +echo "==> current pnpmDeps hash: $ORIG_HASH" +INPUT_KEY="$(input_fingerprint)" + +if nix build --no-link '.#kimi-code-pnpm-deps' >/dev/null 2>&1; then + write_cached_hash "$ORIG_HASH" + echo "==> pnpmDeps hash still valid; cached input fingerprint $INPUT_KEY" + exit 0 +fi + +echo "==> current hash did not build; checking local pnpmDeps hash cache" +CACHED_HASH="" +if ! CACHED_HASH="$(read_cached_hash)"; then + echo "warning: ignoring unreadable pnpmDeps hash cache at $CACHE_FILE" >&2 + CACHED_HASH="" +fi + +if [ -n "$CACHED_HASH" ] && [ "$CACHED_HASH" != "$ORIG_HASH" ]; then + echo "==> cache hit for pnpmDeps input: $CACHED_HASH" + RESTORE_ORIG_HASH=1 + set_hash "$CACHED_HASH" + if nix build --no-link '.#kimi-code-pnpm-deps' >/dev/null 2>&1; then + RESTORE_ORIG_HASH=0 + write_cached_hash "$CACHED_HASH" + echo "==> done. pnpmDeps hash: $ORIG_HASH -> $CACHED_HASH" + exit 0 + fi + + echo "==> cached hash failed verification; falling back to hash discovery" + set_hash "$ORIG_HASH" + RESTORE_ORIG_HASH=0 +fi + +echo "==> patching flake.nix with fakeHash to provoke a mismatch" +RESTORE_ORIG_HASH=1 +set_hash "$FAKE_HASH" + +echo "==> running nix build to discover the real hash" +BUILD_OUT="$(nix build --no-link --print-build-logs '.#kimi-code-pnpm-deps' 2>&1 || true)" + +NEW_HASH="$(printf '%s\n' "$BUILD_OUT" \ + | grep -E -o 'got:[[:space:]]+sha256-[A-Za-z0-9+/=]+' \ + | head -n 1 \ + | sed -E 's/^got:[[:space:]]+//')" + +if [ -z "$NEW_HASH" ]; then + echo "error: could not extract a new hash from nix build output." >&2 + echo "----- nix build output -----" >&2 + printf '%s\n' "$BUILD_OUT" >&2 + echo "----- end output -----" >&2 + set_hash "$ORIG_HASH" + RESTORE_ORIG_HASH=0 + exit 1 +fi + +set_hash "$NEW_HASH" +RESTORE_ORIG_HASH=0 + +echo "==> verifying build with new hash" +if ! nix build --no-link '.#kimi-code-pnpm-deps'; then + echo "error: verification build failed after hash update." >&2 + echo " flake.nix was left pointing at $NEW_HASH for inspection." >&2 + exit 1 +fi + +write_cached_hash "$NEW_HASH" + +if [ "$NEW_HASH" = "$ORIG_HASH" ]; then + echo "==> hash unchanged ($ORIG_HASH); flake.nix already up to date" + exit 0 +fi + +echo "==> done. pnpmDeps hash: $ORIG_HASH -> $NEW_HASH" diff --git a/build/raw-text-loader.mjs b/build/raw-text-loader.mjs new file mode 100644 index 000000000..6946c97b8 --- /dev/null +++ b/build/raw-text-loader.mjs @@ -0,0 +1,23 @@ +import { readFile } from 'node:fs/promises'; +import { fileURLToPath } from 'node:url'; + +/** + * Node ESM `load` hook: import `.md` / `.yaml` files as raw-string modules. + * + * This is the runtime counterpart of `build/raw-text-plugin.mjs` (the bundler + * plugin). The plugin covers build (tsdown) and test (vitest); this loader + * covers source execution — e.g. `tsx`-run dev flows that import `kimi-core` + * straight from `src`, where no bundler is involved. + */ +export async function load(url, context, nextLoad) { + const filePath = url.split('?', 1)[0] ?? url; + if (filePath.endsWith('.md') || filePath.endsWith('.yaml')) { + const text = await readFile(fileURLToPath(filePath), 'utf-8'); + return { + format: 'module', + shortCircuit: true, + source: `export default ${JSON.stringify(text)};`, + }; + } + return nextLoad(url, context); +} diff --git a/build/raw-text-plugin.mjs b/build/raw-text-plugin.mjs new file mode 100644 index 000000000..fba6eef62 --- /dev/null +++ b/build/raw-text-plugin.mjs @@ -0,0 +1,23 @@ +import { readFileSync } from 'node:fs'; + +/** + * Bundler plugin that lets `.md` / `.yaml` files be imported as raw strings: + * + * import description from './grep.md'; + * + * The file content is inlined into the bundle at build time, so prompt + * source files never ship separately in `dist`. Shared by tsdown (build) + * and vitest (test) so both resolve these imports identically. + */ +export function rawTextPlugin() { + return { + name: 'raw-text', + enforce: 'pre', + load(id) { + const path = id.split('?', 1)[0] ?? id; + if (!path.endsWith('.md') && !path.endsWith('.yaml')) return null; + const text = readFileSync(path, 'utf-8'); + return { code: `export default ${JSON.stringify(text)};`, map: null }; + }, + }; +} diff --git a/build/register-raw-text-loader.mjs b/build/register-raw-text-loader.mjs new file mode 100644 index 000000000..0c178fc34 --- /dev/null +++ b/build/register-raw-text-loader.mjs @@ -0,0 +1,7 @@ +import { register } from 'node:module'; + +/** + * Registers the `.md` / `.yaml` raw-text loader. Pass to Node via `--import` + * (alongside tsx) so source-executed code can import these prompt files. + */ +register('./raw-text-loader.mjs', import.meta.url); diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 000000000..57a09c39d --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,3 @@ +node_modules +.vitepress/dist +.vitepress/cache diff --git a/docs/.npmrc b/docs/.npmrc new file mode 100644 index 000000000..bf2e7648b --- /dev/null +++ b/docs/.npmrc @@ -0,0 +1 @@ +shamefully-hoist=true diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts new file mode 100644 index 000000000..ab968c7d0 --- /dev/null +++ b/docs/.vitepress/config.ts @@ -0,0 +1,184 @@ +import { defineConfig } from 'vitepress' +import { withMermaid } from 'vitepress-plugin-mermaid' +import llmstxt from 'vitepress-plugin-llms' + +const rawBase = process.env.VITEPRESS_BASE +const base = rawBase + ? rawBase.startsWith('/') + ? rawBase.endsWith('/') ? rawBase : `${rawBase}/` + : `/${rawBase}/` + : '/' + +export default withMermaid(defineConfig({ + base, + title: 'Kimi Code CLI Docs', + description: 'Kimi Code CLI Documentation', + + head: [ + ['link', { rel: 'icon', type: 'image/x-icon', href: `${base}favicon.ico` }], + ['meta', { name: 'theme-color', content: '#0a7aff' }], + ], + + srcExclude: ['AGENTS.md', 'superpowers/**'], + + locales: { + zh: { + label: '简体中文', + lang: 'zh-CN', + link: '/zh/', + title: 'Kimi Code CLI 文档', + description: 'Kimi Code CLI 用户文档', + themeConfig: { + nav: [ + { text: '指南', link: '/zh/guides/getting-started', activeMatch: '/zh/guides/' }, + { text: '定制化', link: '/zh/customization/mcp', activeMatch: '/zh/customization/' }, + { text: '配置', link: '/zh/configuration/config-files', activeMatch: '/zh/configuration/' }, + { text: '参考手册', link: '/zh/reference/kimi-command', activeMatch: '/zh/reference/' }, + { text: '常见问题', link: '/zh/faq' }, + { text: '发布说明', link: '/zh/release-notes/changelog', activeMatch: '/zh/release-notes/' }, + ], + sidebar: { + '/zh/guides/': [ + { + text: '指南', + items: [ + { text: '开始使用', link: '/zh/guides/getting-started' }, + { text: '从 kimi-cli 迁移', link: '/zh/guides/migration' }, + { text: '常见使用案例', link: '/zh/guides/use-cases' }, + { text: '交互与输入', link: '/zh/guides/interaction' }, + { text: '会话与上下文', link: '/zh/guides/sessions' }, + ], + }, + ], + '/zh/customization/': [ + { + text: '定制化', + items: [ + { text: 'Model Context Protocol', link: '/zh/customization/mcp' }, + { text: 'Agent Skills', link: '/zh/customization/skills' }, + { text: 'Agent 与子 Agent', link: '/zh/customization/agents' }, + { text: 'Hooks', link: '/zh/customization/hooks' }, + ], + }, + ], + '/zh/configuration/': [ + { + text: '配置', + items: [ + { text: '配置文件', link: '/zh/configuration/config-files' }, + { text: '平台与模型', link: '/zh/configuration/providers' }, + { text: '配置覆盖', link: '/zh/configuration/overrides' }, + { text: '环境变量', link: '/zh/configuration/env-vars' }, + { text: '数据路径', link: '/zh/configuration/data-locations' }, + ], + }, + ], + '/zh/reference/': [ + { + text: '参考手册', + items: [ + { text: 'kimi 命令', link: '/zh/reference/kimi-command' }, + { text: '内置工具', link: '/zh/reference/tools' }, + { text: '斜杠命令', link: '/zh/reference/slash-commands' }, + { text: '键盘快捷键', link: '/zh/reference/keyboard' }, + ], + }, + ], + '/zh/release-notes/': [ + { + text: '发布说明', + items: [ + { text: '变更记录', link: '/zh/release-notes/changelog' }, + ], + }, + ], + }, + }, + }, + en: { + label: 'English', + lang: 'en-US', + link: '/en/', + title: 'Kimi Code CLI Docs', + description: 'Kimi Code CLI User Documentation', + themeConfig: { + nav: [ + { text: 'Guides', link: '/en/guides/getting-started', activeMatch: '/en/guides/' }, + { text: 'Customization', link: '/en/customization/mcp', activeMatch: '/en/customization/' }, + { text: 'Configuration', link: '/en/configuration/config-files', activeMatch: '/en/configuration/' }, + { text: 'Reference', link: '/en/reference/kimi-command', activeMatch: '/en/reference/' }, + { text: 'FAQ', link: '/en/faq' }, + { text: 'Release Notes', link: '/en/release-notes/changelog', activeMatch: '/en/release-notes/' }, + ], + sidebar: { + '/en/guides/': [ + { + text: 'Guides', + items: [ + { text: 'Getting Started', link: '/en/guides/getting-started' }, + { text: 'Migrating from kimi-cli', link: '/en/guides/migration' }, + { text: 'Common Use Cases', link: '/en/guides/use-cases' }, + { text: 'Interaction and Input', link: '/en/guides/interaction' }, + { text: 'Sessions and Context', link: '/en/guides/sessions' }, + ], + }, + ], + '/en/customization/': [ + { + text: 'Customization', + items: [ + { text: 'Model Context Protocol', link: '/en/customization/mcp' }, + { text: 'Agent Skills', link: '/en/customization/skills' }, + { text: 'Agents and Subagents', link: '/en/customization/agents' }, + { text: 'Hooks', link: '/en/customization/hooks' }, + ], + }, + ], + '/en/configuration/': [ + { + text: 'Configuration', + items: [ + { text: 'Config Files', link: '/en/configuration/config-files' }, + { text: 'Providers and Models', link: '/en/configuration/providers' }, + { text: 'Config Overrides', link: '/en/configuration/overrides' }, + { text: 'Environment Variables', link: '/en/configuration/env-vars' }, + { text: 'Data Locations', link: '/en/configuration/data-locations' }, + ], + }, + ], + '/en/reference/': [ + { + text: 'Reference', + items: [ + { text: 'kimi Command', link: '/en/reference/kimi-command' }, + { text: 'Built-in Tools', link: '/en/reference/tools' }, + { text: 'Slash Commands', link: '/en/reference/slash-commands' }, + { text: 'Keyboard Shortcuts', link: '/en/reference/keyboard' }, + ], + }, + ], + '/en/release-notes/': [ + { + text: 'Release Notes', + items: [ + { text: 'Changelog', link: '/en/release-notes/changelog' }, + ], + }, + ], + }, + }, + }, + }, + + themeConfig: { + outline: [2, 3], + search: { provider: 'local' }, + socialLinks: [ + { icon: 'github', link: 'https://github.com/MoonshotAI/kimi-code' }, + ], + }, + + vite: { + plugins: [llmstxt()], + }, +})) diff --git a/docs/.vitepress/theme/Kimi.png b/docs/.vitepress/theme/Kimi.png new file mode 100644 index 0000000000000000000000000000000000000000..5b41bb6095dc2fcfc3128a77ed8025d216f12777 GIT binary patch literal 82192 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

    gKN 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 literal 0 HcmV?d00001 diff --git a/docs/.vitepress/theme/components/HomeFeatures.vue b/docs/.vitepress/theme/components/HomeFeatures.vue new file mode 100644 index 000000000..e98c01e5f --- /dev/null +++ b/docs/.vitepress/theme/components/HomeFeatures.vue @@ -0,0 +1,319 @@ + + + + + diff --git a/docs/.vitepress/theme/components/HomeHero.vue b/docs/.vitepress/theme/components/HomeHero.vue new file mode 100644 index 000000000..873d18b0f --- /dev/null +++ b/docs/.vitepress/theme/components/HomeHero.vue @@ -0,0 +1,147 @@ + + + + + diff --git a/docs/.vitepress/theme/components/HomeLayout.vue b/docs/.vitepress/theme/components/HomeLayout.vue new file mode 100644 index 000000000..1a1635c20 --- /dev/null +++ b/docs/.vitepress/theme/components/HomeLayout.vue @@ -0,0 +1,38 @@ + + + + + diff --git a/docs/.vitepress/theme/components/HomeQuickStart.vue b/docs/.vitepress/theme/components/HomeQuickStart.vue new file mode 100644 index 000000000..91c6c821a --- /dev/null +++ b/docs/.vitepress/theme/components/HomeQuickStart.vue @@ -0,0 +1,210 @@ + + + + + diff --git a/docs/.vitepress/theme/components/KimiLogo.vue b/docs/.vitepress/theme/components/KimiLogo.vue new file mode 100644 index 000000000..2afef84d6 --- /dev/null +++ b/docs/.vitepress/theme/components/KimiLogo.vue @@ -0,0 +1,22 @@ + + + + + diff --git a/docs/.vitepress/theme/index.ts b/docs/.vitepress/theme/index.ts new file mode 100644 index 000000000..ceb339acd --- /dev/null +++ b/docs/.vitepress/theme/index.ts @@ -0,0 +1,12 @@ +import type { Theme } from 'vitepress' +import DefaultTheme from 'vitepress/theme' +import HomeLayout from './components/HomeLayout.vue' + +import './styles/vars.css' +import './styles/base.css' +import './styles/home.css' + +export default { + extends: DefaultTheme, + Layout: HomeLayout, +} satisfies Theme diff --git a/docs/.vitepress/theme/styles/base.css b/docs/.vitepress/theme/styles/base.css new file mode 100644 index 000000000..7a860ca7f --- /dev/null +++ b/docs/.vitepress/theme/styles/base.css @@ -0,0 +1,231 @@ +/** + * Base overrides applied to all pages. + * Touches links, inline code, code blocks, custom blocks, blockquotes, navbar, sidebar. + */ + +html { + font-feature-settings: 'cv11', 'ss01', 'ss03'; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +body { + font-family: var(--vp-font-family-base); +} + +/* --- Top navbar: blur + remove the hard bottom line --- */ +.VPNav, +.VPNavBar { + background: rgba(255, 255, 255, 0.72) !important; + backdrop-filter: saturate(180%) blur(14px); + -webkit-backdrop-filter: saturate(180%) blur(14px); +} +.dark .VPNav, +.dark .VPNavBar { + background: rgba(13, 17, 23, 0.72) !important; +} +.VPNavBar.has-sidebar .content, +.VPNavBar:not(.home) { + border-bottom: 1px solid var(--vp-c-divider); +} + +/* --- Sidebar: brand-tinted active item, slim left accent bar --- */ +.VPSidebarItem.is-active > .item .link .text, +.VPSidebarItem.is-active > .item > .text { + color: var(--vp-c-brand-1); + font-weight: 600; +} +.VPSidebarItem.is-link.is-active > .item { + position: relative; +} +.VPSidebarItem.is-link.is-active > .item::before { + content: ''; + position: absolute; + left: -14px; + top: 50%; + transform: translateY(-50%); + width: 3px; + height: 16px; + border-radius: 2px; + background: var(--kimi-brand-gradient); +} + +/* --- Headings: tighter tracking, no underline on h2 --- */ +.vp-doc h1, +.vp-doc h2, +.vp-doc h3 { + letter-spacing: -0.02em; +} +.vp-doc h2 { + border-top: none; + padding-top: 24px; + margin-top: 48px; +} + +/* --- Links --- */ +.vp-doc a:not(.header-anchor) { + color: var(--vp-c-brand-1); + text-decoration: underline; + text-decoration-color: transparent; + text-underline-offset: 4px; + text-decoration-thickness: 2px; + transition: text-decoration-color var(--kimi-transition), color var(--kimi-transition); + font-weight: 500; +} +.vp-doc a:not(.header-anchor):hover { + color: var(--vp-c-brand-2); + text-decoration-color: currentColor; +} + +/* --- Inline code --- */ +.vp-doc :not(pre) > code { + background: var(--kimi-brand-soft); + color: var(--vp-c-brand-1); + padding: 2px 6px; + border-radius: 6px; + font-weight: 500; + font-size: 0.875em; + border: none; +} + +/* Inline code inside headings: drop the chip, keep just the brand-colored monospace word */ +.vp-doc :is(h1, h2, h3, h4, h5, h6) code { + background: transparent; + padding: 0; + border-radius: 0; + font-size: 0.9em; + font-weight: inherit; + color: var(--vp-c-brand-1); +} + +/* --- Code blocks --- */ +.vp-doc div[class*='language-'] { + border-radius: var(--kimi-radius-code); + background: var(--vp-c-bg-soft); + margin: 20px 0; + box-shadow: var(--vp-shadow-1); +} +.vp-doc div[class*='language-'] pre { + padding: 20px 24px; +} +.vp-doc div[class*='language-'] code { + font-family: var(--vp-font-family-mono); + font-size: 13.5px; + line-height: 1.7; +} +.vp-doc div[class*='language-'] .lang { + color: var(--vp-c-text-3); + font-size: 12px; +} +.vp-doc div[class*='language-'] button.copy { + border-radius: 8px; +} + +/* --- Blockquote --- */ +.vp-doc blockquote { + border-left: 3px solid var(--vp-c-brand-1); + background: var(--kimi-brand-soft); + padding: 14px 18px; + border-radius: 0 var(--kimi-radius-code) var(--kimi-radius-code) 0; + margin: 20px 0; +} +.vp-doc blockquote > p { + color: var(--vp-c-text-2); + margin: 0; +} + +/* --- Custom blocks (tip / warning / danger / info) --- */ +.vp-doc .custom-block { + border-radius: var(--kimi-radius-code); + border: none; + padding: 16px 20px; + margin: 20px 0; +} +.vp-doc .custom-block .custom-block-title { + font-weight: 600; + letter-spacing: -0.005em; +} +.vp-doc .custom-block.tip { + background: rgba(10, 122, 255, 0.08); + color: var(--vp-c-text-1); +} +.dark .vp-doc .custom-block.tip { + background: rgba(61, 149, 255, 0.12); +} +.vp-doc .custom-block.warning { + background: rgba(234, 179, 8, 0.10); + color: var(--vp-c-text-1); +} +.vp-doc .custom-block.danger { + background: rgba(239, 68, 68, 0.10); + color: var(--vp-c-text-1); +} +.vp-doc .custom-block.info { + background: rgba(148, 163, 184, 0.12); + color: var(--vp-c-text-1); +} +.vp-doc .custom-block.tip .custom-block-title { color: var(--vp-c-brand-1); } +.vp-doc .custom-block.warning .custom-block-title { color: #ca8a04; } +.vp-doc .custom-block.danger .custom-block-title { color: #dc2626; } +.dark .vp-doc .custom-block.warning .custom-block-title { color: #eab308; } +.dark .vp-doc .custom-block.danger .custom-block-title { color: #ef4444; } + +/* --- Tables --- */ +.vp-doc table { + border-radius: var(--kimi-radius-code); + overflow: hidden; + border-collapse: separate; + border-spacing: 0; + display: table; + width: 100%; +} +.vp-doc tr { + background: transparent !important; + border-top: 1px solid var(--vp-c-divider); +} +.vp-doc tr:first-child { border-top: none; } +.vp-doc th { + background: var(--vp-c-bg-soft); + font-weight: 600; + color: var(--vp-c-text-1); +} + +/* --- Outline / TOC --- */ +.VPDocAsideOutline .outline-link.active, +.VPDocAsideOutline .outline-link:hover { + color: var(--vp-c-brand-1); +} + +/* --- Buttons globally (e.g. hero CTAs) --- */ +.VPButton.brand { + background: var(--kimi-brand-gradient) !important; + border: none !important; + box-shadow: var(--vp-shadow-3); + transition: transform var(--kimi-transition), box-shadow var(--kimi-transition); +} +.VPButton.brand:hover { + transform: translateY(-2px); + box-shadow: var(--vp-shadow-4); +} +.VPButton.alt { + background: transparent !important; + border: 1px solid var(--vp-c-divider) !important; + color: var(--vp-c-text-1) !important; + transition: border-color var(--kimi-transition), transform var(--kimi-transition); +} +.VPButton.alt:hover { + border-color: var(--vp-c-brand-1) !important; + transform: translateY(-2px); +} + +/* --- Hero default frontmatter (used as fallback when custom Home not rendered) --- */ +.VPHero .name, +.VPHero .text { + letter-spacing: -0.03em; +} + +/* --- Footer --- */ +.VPFooter { + border-top: 1px solid var(--vp-c-divider); + background: transparent; +} diff --git a/docs/.vitepress/theme/styles/home.css b/docs/.vitepress/theme/styles/home.css new file mode 100644 index 000000000..5ee0125b4 --- /dev/null +++ b/docs/.vitepress/theme/styles/home.css @@ -0,0 +1,85 @@ +/** + * Home-only styles. Scoped CSS in components handles most rules; + * shared utilities and layout container live here. + */ + +.KimiHome { + --section-px: clamp(20px, 5vw, 64px); + --section-py: clamp(28px, 4vw, 56px); + position: relative; + padding: 0 var(--section-px); +} + +.KimiHome__section { + max-width: 1152px; + margin: 0 auto; + padding: var(--section-py) 0; + position: relative; +} + +.KimiHome__sectionTitle { + font-size: clamp(28px, 4vw, 40px); + font-weight: 700; + letter-spacing: -0.03em; + margin: 0 0 12px; + color: var(--vp-c-text-1); +} + +.KimiHome__sectionLede { + font-size: 17px; + color: var(--vp-c-text-2); + margin: 0 0 40px; + max-width: 640px; + line-height: 1.6; +} + +.KimiBtn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + height: 48px; + padding: 0 22px; + border-radius: var(--kimi-radius-button); + font-size: 15px; + font-weight: 600; + letter-spacing: -0.005em; + text-decoration: none; + transition: transform var(--kimi-transition), box-shadow var(--kimi-transition), + border-color var(--kimi-transition), background var(--kimi-transition); + white-space: nowrap; + cursor: pointer; + border: 1px solid transparent; +} +.KimiBtn--primary { + color: #ffffff; + background: var(--kimi-brand-gradient); + box-shadow: var(--vp-shadow-3); + border: 0; +} +.KimiBtn--primary:hover { + transform: translateY(-2px); + box-shadow: var(--vp-shadow-4); + color: #ffffff; +} +.KimiBtn--ghost { + color: var(--vp-c-text-1); + background: transparent; + border-color: var(--vp-c-divider); +} +.KimiBtn--ghost:hover { + border-color: var(--vp-c-brand-1); + transform: translateY(-2px); + color: var(--vp-c-text-1); +} +.KimiBtn--link { + color: var(--vp-c-brand-1); + height: auto; + padding: 0; + background: transparent; + border: none; +} +.KimiBtn--link:hover { + color: var(--vp-c-brand-2); + transform: translateX(2px); +} diff --git a/docs/.vitepress/theme/styles/vars.css b/docs/.vitepress/theme/styles/vars.css new file mode 100644 index 000000000..4a4114a9c --- /dev/null +++ b/docs/.vitepress/theme/styles/vars.css @@ -0,0 +1,120 @@ +/** + * Design tokens for the Kimi Code docs theme. + * Light + dark live side by side; VitePress toggles the .dark class on . + */ + +:root { + /* Brand palette — cool blue family from design board */ + --kimi-brand-1: #0a7aff; /* primary */ + --kimi-brand-2: #5baeff; /* mid */ + --kimi-brand-3: #81c4ff; /* soft sky */ + --kimi-brand-deep: #043153; /* deep navy (anchor for dark surfaces) */ + --kimi-brand-whisper: #eff8ff; /* near-white blue (light tints) */ + --kimi-brand-gradient: linear-gradient(135deg, #0a7aff 0%, #5baeff 60%, #81c4ff 100%); + --kimi-brand-gradient-soft: linear-gradient(135deg, rgba(10, 122, 255, 0.14) 0%, rgba(91, 174, 255, 0.12) 60%, rgba(129, 196, 255, 0.10) 100%); + --kimi-brand-soft: rgba(10, 122, 255, 0.10); + --kimi-brand-soft-strong: rgba(10, 122, 255, 0.16); + + /* Shape */ + --kimi-radius-card: 16px; + --kimi-radius-button: 10px; + --kimi-radius-chip: 999px; + --kimi-radius-code: 12px; + --kimi-transition: 200ms cubic-bezier(0.4, 0, 0.2, 1); + + /* Typography */ + --vp-font-family-base: + 'Inter', -apple-system, BlinkMacSystemFont, + 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', + 'Helvetica Neue', 'Segoe UI', Arial, sans-serif; + --vp-font-family-mono: + ui-monospace, SFMono-Regular, 'SF Mono', + Menlo, Consolas, 'Liberation Mono', 'Courier New', monospace; + + /* Surfaces (light) — clean white + subtle blue-tinted off-white */ + --vp-c-bg: #ffffff; + --vp-c-bg-alt: #f6f9fd; + --vp-c-bg-elv: #ffffff; + --vp-c-bg-soft: #eff5fc; + + /* Text (light) */ + --vp-c-text-1: #0b1a30; + --vp-c-text-2: #475569; + --vp-c-text-3: #94a3b8; + + /* Borders (light) — picked up from palette E1E3E6 */ + --vp-c-divider: #e1e3e6; + --vp-c-gutter: #e1e3e6; + --vp-c-border: #e1e3e6; + + /* Brand applied to VitePress vars (light) */ + --vp-c-brand-1: var(--kimi-brand-1); + --vp-c-brand-2: var(--kimi-brand-2); + --vp-c-brand-3: #006ae3; /* hover, slightly deeper than primary */ + --vp-c-brand-soft: var(--kimi-brand-soft); + + /* Shadows (light) — no negative spread, avoids "kink" at rounded corners */ + --vp-shadow-1: 0 1px 2px rgba(11, 26, 48, 0.04); + --vp-shadow-2: 0 6px 20px rgba(10, 122, 255, 0.15); + --vp-shadow-3: 0 10px 28px rgba(10, 122, 255, 0.22); + --vp-shadow-4: 0 18px 44px rgba(10, 122, 255, 0.30); + + /* Buttons (light) */ + --vp-button-brand-bg: var(--kimi-brand-1); + --vp-button-brand-hover-bg: var(--vp-c-brand-3); + --vp-button-brand-active-bg: var(--vp-c-brand-3); + --vp-button-brand-border: transparent; + --vp-button-brand-hover-border: transparent; + --vp-button-brand-text: #ffffff; + --vp-button-brand-hover-text: #ffffff; + --vp-button-brand-active-text: #ffffff; + + /* Custom blocks tinting (light) */ + --vp-custom-block-tip-border: transparent; + --vp-custom-block-tip-text: var(--vp-c-text-1); + --vp-custom-block-tip-bg: rgba(10, 122, 255, 0.07); + --vp-custom-block-tip-code-bg: rgba(10, 122, 255, 0.10); +} + +.dark { + /* Surfaces (dark) — neutral dark with subtle navy undertone */ + --vp-c-bg: #0a1422; + --vp-c-bg-alt: #0f1b2e; + --vp-c-bg-elv: #0f1b2e; + --vp-c-bg-soft: #15263f; + + /* Text (dark) — avoid pure white */ + --vp-c-text-1: #e2e8f0; + --vp-c-text-2: #94a3b8; + --vp-c-text-3: #64748b; + + /* Borders (dark) — navy-leaning to stay in family */ + --vp-c-divider: #1b2e47; + --vp-c-gutter: #1b2e47; + --vp-c-border: #1b2e47; + + /* Brand applied (dark) — keep the pure blue family, brightened */ + --vp-c-brand-1: #3d95ff; + --vp-c-brand-2: #81c4ff; + --vp-c-brand-3: #5baeff; + --vp-c-brand-soft: rgba(61, 149, 255, 0.16); + + /* Softs (dark) */ + --kimi-brand-soft: rgba(61, 149, 255, 0.16); + --kimi-brand-soft-strong: rgba(61, 149, 255, 0.22); + + /* Shadows (dark) — brand-tinted glow, no negative spread */ + --vp-shadow-1: 0 1px 2px rgba(0, 0, 0, 0.4); + --vp-shadow-2: 0 6px 24px rgba(61, 149, 255, 0.22); + --vp-shadow-3: 0 10px 32px rgba(61, 149, 255, 0.32); + --vp-shadow-4: 0 18px 48px rgba(61, 149, 255, 0.42); + + /* Buttons (dark) */ + --vp-button-brand-bg: var(--kimi-brand-1); + --vp-button-brand-hover-bg: #1f8cff; + --vp-button-brand-active-bg: #1f8cff; + + /* Custom blocks tinting (dark) */ + --vp-custom-block-tip-bg: rgba(61, 149, 255, 0.12); + --vp-custom-block-tip-code-bg: rgba(61, 149, 255, 0.16); +} diff --git a/docs/AGENTS.md b/docs/AGENTS.md new file mode 100644 index 000000000..14d574ea6 --- /dev/null +++ b/docs/AGENTS.md @@ -0,0 +1,179 @@ +# Documentation Agent Guide + +This repository uses VitePress for the documentation site. Most user-facing pages under `docs/en/` and `docs/zh/` are fully written; New or updated content should keep both locales in sync. + +## Structure + +- Locales live under `docs/en/` and `docs/zh/` with mirrored paths and filenames. +- Main sections (nav + sidebar) are: + - Guides: getting-started, migration, use-cases, interaction, sessions + - Customization: mcp, skills, agents, hooks + - Configuration: config-files, providers, overrides, env-vars, data-locations + - Reference: kimi-command, tools, slash-commands, keyboard + - FAQ + - Release notes: changelog +- Navigation and sidebar are defined in `docs/.vitepress/config.ts`. Any new or renamed page must be wired there for both locales. + +## Source of truth + +- **Changelog page**: The English version (`docs/en/release-notes/changelog.md`) is the source of truth; the Chinese changelog should be translated from it. The changelog is currently generated manually by a skill that syncs from the CLI package's `CHANGELOG.md` after each release. +- **All other pages**: `docs/en/` and `docs/zh/` are mirrored pairs with the same paths, headings, and section structure. Edit whichever locale you are working in, and update the other locale in the same change. + +Keep both locales in sync before release. Machine-assisted translation is fine; review the locale you changed and its mirror for accuracy, terminology, and broken links. + +## Authoring workflow + +- Each page should keep the section ordering established by surrounding pages. Changelog and FAQ are exceptions until their placeholder content is replaced. +- For other pages: edit either locale, then update its mirror in the same change. + +## Naming conventions + +- Filenames are kebab-case and mirror across locales (same slug in `docs/en/` and `docs/zh/`). +- Use consistent section labels that match the sidebar titles. +- Use backticks for flags, commands, subcommands, command arguments, file paths, code identifiers, type names, field names, field values, and keyboard shortcuts. + +## Wording conventions + +- Do not change H1 titles or nav/sidebar labels. +- English H2+ headings use sentence case (only the first word capitalized unless it is a proper noun). Treat "Wire", "Plan mode", "YOLO mode", and "Thinking mode" as proper nouns; do not treat "agent" as a proper noun. +- Chinese H2+ headings keep English words in sentence case; preserve proper nouns listed in the term table below. +- Use `API key` in English and `API 密钥` in Chinese; keep `JSON`, `JSONL`, `OAuth`, `macOS`, `Node.js`, `npm`, `pnpm`, and `TypeScript` as-is. +- Use straight double quotes with spaces for quoted content: `"被引内容"` (not curly quotes). Add a space before and after the quoted text when adjacent to CJK characters. Use corner brackets `「」` for special terms (e.g., `「工具」`, `「会话」`). +- Prefer "终端" over "命令行" in Chinese when both are applicable (e.g., "运行在终端中", "终端界面", "终端操作"). +- Use "工具调用" / "tool call", not "工具使用" / "tool use". +- Use inline code for tool names (e.g., `Read`, `Grep`, `Bash`). + +Term mapping (Chinese <-> English, and proper noun handling): + +| Chinese | English | Proper noun (zh) | Proper noun (en) | +| --- | --- | --- | --- | +| Agent | agent | yes | no | +| 主 Agent | main agent | yes (Agent) | no | +| 子 Agent | subagent | yes (Agent) | no | +| Shell | shell | yes | no | +| Plan 模式 | Plan mode | yes | yes (Plan mode) | +| YOLO 模式 | YOLO mode | yes | yes (YOLO mode) | +| Thinking 模式 | Thinking mode | yes | yes (Thinking mode) | +| MCP | MCP | yes | yes | +| Kimi Code CLI | Kimi Code CLI | yes | yes | +| Agent Skills | Agent Skills | yes | yes | +| Skill | skill | yes | no | +| 系统提示词 | system prompt | no | no | +| 提示词 | prompt | no | no | +| 会话 | session | no | no | +| 上下文 | context | no | no | +| API 密钥 | API key | yes | no | +| JSON | JSON | yes | yes | +| JSONL | JSONL | yes | yes | +| OAuth | OAuth | yes | yes | +| macOS | macOS | yes | yes | +| TypeScript | TypeScript | yes | yes | +| Node.js | Node.js | yes | yes | +| npm | npm | yes | yes | +| pnpm | pnpm | yes | yes | +| kimi | kimi | yes | yes | +| 审批请求 | approval request | no | no | +| 斜杠命令 | slash command | no | no | +| 工具调用 | tool call | no | no | +| Frontmatter | frontmatter | yes | no | +| User 消息 | user message | yes (User) | no | +| Assistant 消息 | assistant message | yes (Assistant) | no | +| Tool 消息 | tool message | yes (Tool) | no | +| 轮次 | turn | no | no | +| 供应商 | provider | no | no | +| Prompt Flow | Prompt Flow | yes | yes | +| Diff | diff | yes | no | + +## Typography + +- **Spacing around mixed content**: Add a space between Chinese characters and English words, numbers, inline code, or links. Exception: no space before full-width punctuation. + - ✓ 在 TypeScript 中使用 `class` 关键字 + - ✗ 在TypeScript中使用`class`关键字 + - ✓ 详见 [配置文件](./config.md)。 + - ✗ 详见[配置文件](./config.md)。 +- **Full-width punctuation**: Use full-width punctuation in Chinese text: `,。;:?!()` not `, . ; : ? ! ( )`. +- **Keyboard shortcuts**: Use hyphen between modifier and key (`Ctrl-C`, `Ctrl-D`, `Shift-Tab`, `Alt-V`), not plus sign. Exception: literal application output (e.g., the `Press Ctrl+C again to exit` hint produced by the product itself) keeps its exact rendering. +- **Code block language**: Always specify language for fenced code blocks (e.g., ` ```sh `, ` ```toml `, ` ```json `, ` ```ts `). Exception: natural language examples (user prompts) may omit the language. +- **Callout titles**: Use short category titles for callout blocks (`::: tip`, `::: warning`, `::: info`, `::: danger`). Put the detailed description in the block content, not the title. + - Chinese: use `提示` for tip, `注意` for warning, `说明` for info, `警告` for danger. + - English: use no title or short words like `Note` for warning. + - ✓ `::: tip 提示` + content starting with the key point + - ✓ `::: warning 注意` + content `\`KIMI_CODE_HOME\` 不影响 Skills 的搜索路径。...` + - ✗ `::: warning 不影响 Skills` (title too long, should be in content) + - ✗ `::: tip Skills 路径独立于 KIMI_CODE_HOME` (title too long) +- **Version info blocks**: For version change callouts, use `::: info` with a category title (Added/Changed/Removed in English; 新增/变更/移除 in Chinese). The content should be a complete sentence. + - ✓ `::: info 新增` + content `新增于 0.2.0。` + - ✗ `::: info 新增于 0.2.0` (title too long) + - ✓ `::: info Changed` + content `Renamed in 0.2.0. ...` + - ✗ `::: info Renamed in 0.2.0` (title too long) + +## Writing style + +- **Natural narrative**: Organize content like writing an article, guiding readers smoothly through the material. +- **Avoid fragmentation**: Don't turn every point into a subheading; use paragraph transitions instead. +- **Global perspective**: "Getting Started" introduces core concepts only; detailed usage belongs in later pages. +- **Progressive depth**: Guides → Customization → Configuration → Reference, information deepens gradually. +- **No "next steps"**: VitePress already provides prev/next navigation; don't add manual `::: tip 接下来` blocks at page end. + +### Example: good vs bad + +Outline prompt: + +``` +* Install and upgrade + * System requirements: Node.js 24.15.0+, recommend pnpm + * Install, upgrade, uninstall steps +``` + +**Bad** (mechanical conversion to headings): + +```markdown +## Install and upgrade + +### System requirements + +- Node.js 24.15.0+ +- Recommend pnpm + +### Install + +... + +### Upgrade + +... +``` + +**Good** (natural narrative): + +```markdown +## Install and upgrade + +Kimi Code CLI requires Node.js 24.15.0 or later. We recommend using pnpm for installation and management. + +If you haven't installed pnpm yet, please refer to the pnpm installation docs first. Install Kimi Code CLI: + +(code block) + +Verify the installation: + +(code block) + +Upgrade to the latest version: + +(code block) +``` + +## Build and preview + +- Docs are built with VitePress from `docs/`. +- Common commands (run inside `docs/`): + - `npm install` + - `npm run dev` + - `npm run build` + - `npm run preview` +- The build output is `docs/.vitepress/dist`. + +## Changelog syncing + +See `sync-changelog` skill for the changelog generation workflow. diff --git a/docs/en/configuration/config-files.md b/docs/en/configuration/config-files.md new file mode 100644 index 000000000..9100e9ab0 --- /dev/null +++ b/docs/en/configuration/config-files.md @@ -0,0 +1,230 @@ +# Config files + +Kimi Code CLI stores its global configuration in a single TOML file that covers API providers, model aliases, agent loop parameters, background tasks, external services, and more. This page describes the location of the config file, its top-level fields, each nested structure, and a complete example. + +## Config file location + +The default config file is located at `~/.kimi-code/config.toml`. The directory and file are created automatically on first run with restrictive permissions. + +If you want to place the data directory elsewhere, set the `KIMI_CODE_HOME` environment variable: + +```sh +export KIMI_CODE_HOME=/path/to/kimi-home +``` + +The config file path then becomes `$KIMI_CODE_HOME/config.toml`. Regardless of where the directory lives, the file name is always `config.toml`. + +::: tip +TOML field names always use snake_case (for example, `default_model`, `max_context_size`). If a key contains `.`, you must use a quoted TOML key; otherwise TOML will treat `.` as a nested table separator. +::: + +## Top-level fields + +| Field | Type | Default | Description | +| --- | --- | --- | --- | +| `default_model` | `string` | — | Default model alias; must be defined in `models` | +| `default_thinking` | `boolean` | `false` | Initial value of the Thinking toggle for new sessions; can be flipped from the model menu inside a session. Even when this is `true`, setting `[thinking].mode = "off"` will still force Thinking off. See [`thinking`](#thinking) below | +| `default_permission_mode` | `string` | `manual` | Default permission mode for new sessions; one of `manual`, `auto`, `yolo` | +| `default_plan_mode` | `boolean` | `false` | Whether new sessions start in Plan mode by default; omitting it is equivalent to `false` | +| `merge_all_available_skills` | `boolean` | `true` | Whether to merge Agent Skills from all available directories | +| `extra_skill_dirs` | `array` | — | Extra skill search directories, layered on top of the default directories | +| `telemetry` | `boolean` | `true` | Whether anonymous telemetry is enabled; only disabled when explicitly set to `false` | +| `providers` | `table` | `{}` | API provider table; see below | +| `models` | `table` | — | Model alias table; see below | +| `thinking` | `table` | — | Default parameters for Thinking mode | +| `loop_control` | `table` | — | Agent loop control parameters | +| `background` | `table` | — | Background task runtime parameters | +| `services` | `table` | — | Built-in external service configuration | +| `permission` | `table` | — | Permission rule configuration; see below | +| `hooks` | `array` | — | Lifecycle hook configuration. See [Hooks](../customization/hooks.md) | + +## Complete example + +```toml +default_model = "kimi-code/kimi-for-coding" +default_thinking = true +default_permission_mode = "manual" +default_plan_mode = false +merge_all_available_skills = true +telemetry = true + +[providers."managed:kimi-code"] +type = "kimi" +base_url = "https://api.kimi.com/coding/v1" +api_key = "" + +[models."kimi-code/kimi-for-coding"] +provider = "managed:kimi-code" +model = "kimi-for-coding" +max_context_size = 262144 + +[thinking] +mode = "auto" + +[loop_control] +max_steps_per_turn = 1000 +max_retries_per_step = 3 +reserved_context_size = 50000 + +[background] +max_running_tasks = 4 +keep_alive_on_exit = false +agent_task_timeout_s = 900 + +[[permission.rules]] +decision = "allow" +pattern = "Read" + +[[permission.rules]] +decision = "deny" +pattern = "Bash(rm -rf*)" + +[[hooks]] +event = "PreToolUse" +matcher = "Bash" +command = "node ~/.kimi-code/hooks/check-bash.mjs" +timeout = 5 +``` + +## `providers` + +Each entry in the `providers` table defines the connection info for one API provider, keyed by a unique name. + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `type` | `string` | Yes | Provider type; one of `anthropic`, `openai`, `kimi`, `google-genai`, `openai_responses`, `vertexai` | +| `api_key` | `string` | No | API key | +| `base_url` | `string` | No | API base URL | +| `oauth` | `table` | No | OAuth credential reference; see below | +| `env` | `table` | No | A configuration sub-table keyed by provider-specific names (such as `KIMI_API_KEY`, `ANTHROPIC_API_KEY`, `GOOGLE_CLOUD_PROJECT`), used as fallback values for `api_key` / `base_url` and related fields. This is just a sub-table inside the config file — **it is not written into your shell environment** — and is consulted only when the corresponding field on `[providers.]` is unset | +| `custom_headers` | `table` | No | Custom HTTP headers attached to each request | + +OAuth credential reference structure: + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `storage` | `string` | Yes | Credential storage location; currently only `file` is supported | +| `key` | `string` | Yes | Unique identifier of the credential entry | + +```toml +[providers.openai] +type = "openai" +base_url = "https://api.openai.com/v1" +api_key = "sk-xxx" +custom_headers = { "X-Custom-Header" = "value" } +``` + +## `models` + +Each entry in the `models` table defines a model alias, keyed by a unique name. + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `provider` | `string` | Yes | Name of the provider to use; must be defined in `providers` | +| `model` | `string` | Yes | Model identifier used when calling the API | +| `max_context_size` | `integer` | Yes | Maximum context length in tokens; must be at least 1 | +| `max_output_size` | `integer` | No | Per-request output budget cap (`max_tokens` on the wire). Only the `anthropic` provider currently honors it. When the alias resolves to a known Claude family, the value is clamped to that model's documented ceiling to avoid exceeding the server-side limit. Omit to use the per-model default — see [`providers.md`](./providers.md#anthropic). | +| `capabilities` | `array` | No | Capability tags to add explicitly, for example `thinking`, `image_in`, `video_in`, `audio_in`, `tool_use` | +| `display_name` | `string` | No | Name shown in the UI; falls back to `model` when unset | + +`capabilities` is unioned with the capabilities that the provider capability registry matches by model-name prefix — entries can only be added, never removed. You usually do not need to set this by hand; reach for it only when the model is not covered by the registry, or when you want to force-enable a particular capability. + +If a model alias contains `.`, use a quoted key: + +```toml +[models."gpt-4.1"] +provider = "openai" +model = "gpt-4.1" +max_context_size = 1047576 +``` + +## `thinking` + +`thinking` controls the default behavior of Thinking mode. Even when the top-level `default_thinking = true`, setting `mode` to `"off"` will still force Thinking off. + +| Field | Type | Default | Description | +| --- | --- | --- | --- | +| `mode` | `string` | — | Trigger policy; one of `auto`, `on`, `off`. `"off"` forces Thinking off; any other value or omission does not disable it, and the effective behavior is decided together by the per-session Thinking toggle and `effort` | +| `effort` | `string` | `high` | Default effort level used when Thinking is on; one of `low`, `medium`, `high`, `xhigh`, `max`. The levels actually available depend on the provider | + +## `loop_control` + +`loop_control` governs the step count, retries, and context compaction threshold of the agent execution loop. + +| Field | Type | Default | Description | +| --- | --- | --- | --- | +| `max_steps_per_turn` | `integer` | `1000` | Maximum number of steps per turn | +| `max_retries_per_step` | `integer` | `3` | Maximum retries per step | +| `reserved_context_size` | `integer` | — | Number of tokens reserved for response generation; compaction is triggered when the context approaches this threshold | + + + +## `background` + +`background` controls the runtime limits for background tasks. Background tasks are launched through the `Bash` tool or the `Agent` tool's `run_in_background=true` parameter. + +| Field | Type | Default | Description | +| --- | --- | --- | --- | +| `max_running_tasks` | `integer` | — | Maximum number of background tasks running concurrently | +| `keep_alive_on_exit` | `boolean` | `true` | Whether to keep still-running background tasks when the session closes. Set to `false` to request stopping background tasks when `kimi -p` finishes and exits, when an SDK session closes, or when a harness closes | +| `agent_task_timeout_s` | `integer` | — | Maximum runtime in seconds for background agent tasks | + +`keep_alive_on_exit` can be overridden by the `KIMI_CODE_BACKGROUND_KEEP_ALIVE_ON_EXIT` environment variable; the environment variable has higher priority than `config.toml`. The schema also reserves `kill_grace_period_ms` and `print_wait_ceiling_s`; these fields currently pass schema validation only and are not read by the CLI runtime. + +## `services` + +`services` configures the built-in external services Kimi Code CLI calls. Only the two fixed keys `moonshot_search` (web search) and `moonshot_fetch` (web fetch) are recognized; other keys are ignored. Both entries share the same fields: + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `base_url` | `string` | No | Service API URL | +| `api_key` | `string` | No | API key | +| `oauth` | `table` | No | OAuth credential reference, same structure as `providers.*.oauth` | +| `custom_headers` | `table` | No | Custom HTTP headers attached to each request | + +```toml +[services.moonshot_search] +base_url = "https://api.moonshot.cn/v1/search" +api_key = "sk-xxx" + +[services.moonshot_fetch] +base_url = "https://api.moonshot.cn/v1/fetch" +api_key = "sk-xxx" +``` + +## `permission` + +`permission` configures the initial permission rules loaded when a session starts, controlling the default approval behavior for tool calls. The default permission mode for new sessions is controlled by the top-level `default_permission_mode` field; an explicit startup permission mode, such as the CLI's `--yolo` flag, overrides that default. + +Rules are written as a `[[permission.rules]]` array of tables, where each rule contains the following fields: + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `decision` | `string` | Yes | Decision result; one of `allow`, `deny`, `ask` | +| `scope` | `string` | No | Rule scope; one of `turn-override`, `session-runtime`, `project`, `user`; defaults to `user` | +| `pattern` | `string` | Yes | Match pattern in the form `ToolName` or `ToolName(arg-pattern)`. `ToolName` must match the runtime tool name exactly — built-in tools are `Read`, `Write`, `Edit`, `Bash`, `Grep`, and so on (see [Built-in tools](../reference/tools.md)) | +| `reason` | `string` | No | Rule description for debugging or auditing | + +Example: + +```toml +[[permission.rules]] +decision = "allow" +pattern = "Read" + +[[permission.rules]] +decision = "allow" +pattern = "Grep" + +[[permission.rules]] +decision = "deny" +pattern = "Bash(rm -rf*)" + +[[permission.rules]] +decision = "ask" +pattern = "Bash" +``` + +::: tip +MCP server declarations are configured in `~/.kimi-code/mcp.json` or the project-local `.kimi-code/mcp.json`, not in `config.toml`. The interactive configuration entry point is `/mcp-config`; see [Model Context Protocol](../customization/mcp.md). +::: diff --git a/docs/en/configuration/data-locations.md b/docs/en/configuration/data-locations.md new file mode 100644 index 000000000..2d081d6ac --- /dev/null +++ b/docs/en/configuration/data-locations.md @@ -0,0 +1,131 @@ +# Data Locations + +Kimi Code CLI stores its runtime data centrally under the `~/.kimi-code/` directory in the user's home folder. This page describes where each type of data lives, what it is for, and how to customize or clean it up. + +## Data root + +The default data root is `~/.kimi-code/`. The `~` is resolved by Node.js's `os.homedir()`, so the actual path differs slightly across platforms: on macOS it is `/Users//.kimi-code`, on Linux `/home//.kimi-code`, and on Windows `C:\Users\\.kimi-code`. + +You can override it to any path with the `KIMI_CODE_HOME` environment variable: + +```sh +export KIMI_CODE_HOME="$HOME/.config/kimi-code" +``` + +Once set, runtime data such as the config, sessions, logs, input history, update cache, and OAuth credentials lands under that path. For the full reference on `KIMI_CODE_HOME` and other environment variables, see [Environment variables](./env-vars.md). + +::: tip Exceptions +The **built-in tool cache** (such as the auto-downloaded ripgrep binary) does not follow `KIMI_CODE_HOME`. It uses `KIMI_CODE_CACHE_DIR`, falling back to a platform cache directory — `~/Library/Caches/kimi-code` on macOS, `$XDG_CACHE_HOME/kimi-code` (default `~/.cache/kimi-code`) on Linux, and `%LOCALAPPDATA%\kimi-code` on Windows. + +User-level Agent Skills search directories live at `~/.kimi-code/skills` and `~/.agents/skills`; project-level Skills live under the working directory at `.kimi-code/skills` and `.agents/skills`. See [Agent Skills](../customization/skills.md) for details. +::: + +## Directory layout + +A typical layout under the data root looks like: + +``` +$KIMI_CODE_HOME (default ~/.kimi-code) +├── config.toml # User config +├── mcp.json # User-level MCP server declarations (optional) +├── session_index.jsonl # Session index +├── credentials/ # OAuth credential root (directory 0o700, files 0o600) +│ ├── .json # Hosted Kimi / Open Platform provider OAuth credentials +│ └── mcp/ # MCP server OAuth credentials +│ └── -.json +├── sessions/ # Session data +│ └── / +│ └── / +│ ├── state.json +│ ├── logs/ +│ │ └── kimi-code.log +│ ├── tasks/ # Background task persistence +│ │ ├── .json +│ │ └── / +│ │ └── output.log +│ └── agents/ +│ ├── main/ +│ │ ├── wire.jsonl +│ │ └── plans/ # Plan mode plan files +│ └── agent-0/ +│ └── wire.jsonl +├── bin/ +│ └── rg # ripgrep cache (rg.exe on Windows) +├── logs/ # Global diagnostic logs +│ └── kimi-code.log +├── updates/ +│ └── latest.json # Update check status +└── user-history/ + └── .jsonl +``` + +::: tip +The tree above shows a typical layout under the default data root (`~/.kimi-code/`). The paths for Agent Skills and the built-in tool cache have some special cases — see the "Exceptions" note above. +::: + +## Config files + +`config.toml` is Kimi Code CLI's main config file, holding user-level settings such as providers, models, and loop control. See [Config files](./config-files.md) for details. + +`mcp.json` holds user-level MCP server declarations and is merged with the project-local `.kimi-code/mcp.json` at load time. The fields are the same as the project-level file; see [MCP](../customization/mcp.md) for details. + +OAuth credentials are stored as files under the `credentials/` subdirectory of the data root. The parent directory uses mode `0o700` and each credential file uses mode `0o600`, readable and writable only by the current user. There are two sub-locations: + +- **Hosted Kimi / Open Platform provider OAuth credentials** live at `credentials/.json`, for example `~/.kimi-code/credentials/managed:kimi-code.json`. +- **MCP server OAuth credentials** live under the `credentials/mcp/` subdirectory, with file names generated from the server key, for example `credentials/mcp/-.json`. + +Writes follow a `tmp → fsync → rename` atomic flow: strictly atomic on POSIX, best-effort on Windows. + +## Session data + +Session-related data is grouped under `sessions/`, with a top-level `session_index.jsonl` maintaining a JSONL index: one record per line containing the three fields `sessionId`, `sessionDir`, and `workDir`. Entries are appended when a session is created. When the index is loaded, each entry is validated to ensure `sessionDir` still lives under `sessions/` and that its last path component equals `sessionId`, preventing external tampering from pointing entries to illegal paths. + +Each session directory has a path like `sessions///`, where `workDirKey` is a bucket name encoded from the working directory in the format `wd__` (for example, `wd_myproject_a3f8c1d20e9b`), and `sessionId` is the session's unique identifier. The full path under `sessions/`, including each `/` bucket, is created with mode `0o700` and accessible only by the current user. + +The internal structure of a session directory includes: + +- `state.json`: session title, `lastPrompt`, `createdAt`, `updatedAt`, `isCustomTitle`, `forkedFrom`, and metadata for each agent. +- `agents/main/wire.jsonl`: the Wire event stream of the main agent, used for replay and resumption. `main` is the fixed id of the main agent. +- `agents/main/plans/`: plan files written by the main agent in Plan mode, named `.md` by plan id. +- `agents/agent-0/`, `agents/agent-1/`, etc.: subagent instance directories, each with its own `wire.jsonl`. Subagent ids are generated by a per-session incrementing counter (`agent-` followed by an integer starting from 0). +- `logs/kimi-code.log`: the diagnostic log for this session. It only appears after a recorded diagnostic event; an ordinary conversation may not create this file. +- `tasks/`: background task persistence directory. Each task stores its metadata (status, pid, exit code, etc.) in `tasks/.json`, with stdout and stderr written to `tasks//output.log`. Task ids use a `bash-` or `agent-` prefix followed by 8 random alphanumeric characters (for example, `bash-a1b2c3d4`). + +`sessionId` is restricted to `[A-Za-z0-9._-]+` and cannot be `.` or `..`, preventing path injection. The session list is sorted by `updatedAt` in descending order, where `updatedAt` is the maximum mtime of the directory and its key files. See [Sessions](../guides/sessions.md) for details. + +## Built-in tool cache + +The first time Kimi Code CLI needs ripgrep, it downloads and caches it automatically. During the download, the archive is written to the system temporary directory and verified by SHA-256 before extraction; the binary is then installed directly to `bin/rg` under the data root (or `bin/rg.exe` on Windows) and marked `0o755` so it can be executed. Subsequent runs under the same data root reuse it with no further download. If `rg` is already on the system `PATH`, the system version takes precedence; deleting `bin/` triggers a redownload the next time it is needed. + +## Logs and update state + +The top-level `logs/kimi-code.log` is the global diagnostic log. It mainly records issues that do not belong to a single session, such as startup, login, and export failures. A single session's diagnostic log lives at `/logs/kimi-code.log`. + +When filing a bug report, prefer `kimi export` for the relevant session (see [The kimi command](../reference/kimi-command.md) for details). If a session log exists, it is included in the export by default. The global diagnostic log is also bundled by default; because it may contain events from other sessions or projects, use `--no-include-global-log` when you do not want to share it. + +`updates/latest.json` records the version update status detected via npm and is maintained automatically by the CLI — there is usually no need to edit it by hand. + +## Input history + +Command input history from the terminal is saved per working directory. Each working directory corresponds to one file, at `user-history/.jsonl`, where the file name is the MD5 hash of the working directory string (UTF-8 encoded). The file format is JSONL, with one history record per line. + +Input history is used to browse and search previously entered prompts in the terminal interface. + +## Cleaning up data + +Deleting the data root directory (default `~/.kimi-code/`, or the path specified by `KIMI_CODE_HOME`) wipes all of Kimi Code CLI's runtime data, including config, sessions, logs, input history, and the built-in tool cache. + +To clean up only part of the data: + +| Goal | Action | +| --- | --- | +| Reset config | Delete `~/.kimi-code/config.toml` | +| Clear all sessions | Delete `~/.kimi-code/sessions/` and `~/.kimi-code/session_index.jsonl` | +| Clear diagnostic logs | Delete the `~/.kimi-code/logs/` directory | +| Clear input history | Delete the `~/.kimi-code/user-history/` directory | +| Reset update check state | Delete `~/.kimi-code/updates/latest.json` | +| Force a ripgrep redownload | Delete the `~/.kimi-code/bin/` directory | +| Clear hosted Kimi / Open Platform OAuth login state | Run `/logout` (clears only the current provider's OAuth), or delete the corresponding `~/.kimi-code/credentials/.json` | +| Clear MCP server OAuth login state | Delete the `~/.kimi-code/credentials/mcp/` directory; `/logout` **does not** clear MCP OAuth credentials | +| Remove user-level MCP declarations | Delete `~/.kimi-code/mcp.json` | +| Clear user-level Skills | Delete the `~/.kimi-code/skills/` directory | diff --git a/docs/en/configuration/env-vars.md b/docs/en/configuration/env-vars.md new file mode 100644 index 000000000..78a59591e --- /dev/null +++ b/docs/en/configuration/env-vars.md @@ -0,0 +1,124 @@ +# Environment variables + +Kimi Code CLI uses environment variables to override default paths, switch OAuth endpoints, and adjust runtime behavior. Most variables are read when the `kimi` process starts up; a few (such as the telemetry switch, the OAuth lock, and diagnostic logging) are read when the relevant subsystem initializes. Kimi's own variables use the `KIMI_*` prefix; in addition, the CLI also reads a number of standard system variables. + +::: warning Note +**Provider credentials are not in this list**: key variables such as `KIMI_API_KEY`, `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, and `GOOGLE_API_KEY` are **not** read automatically from `process.env`. They must be written into the `[providers.]` section of `config.toml` (as `api_key` / `base_url`) or into the `[providers..env]` subtable; merely `export`ing them in your shell will not give a provider credentials automatically. See [Configuration overrides](./overrides.md#provider-credentials) and [Providers](./providers.md) for details. +::: + +## Core paths + +`KIMI_CODE_HOME` overrides Kimi Code CLI's data root directory, defaulting to `~/.kimi-code`. The CLI's own application data, `kimi-core`'s config, the ripgrep cache, and OAuth credentials all land under this directory. + +```sh +export KIMI_CODE_HOME="/path/to/custom/kimi-code" +``` + +For details on the data layout, see [Data locations](./data-locations.md). + +::: warning Note +Make sure the directory is writable once you set it. Multiple `kimi` instances that share the same `KIMI_CODE_HOME` will share both the config and credential files. +::: + +## Provider credential key names + +The following key names appear in the `[providers..env]` subtable of `config.toml`, where they serve as fallback sources for the provider's `api_key` / `base_url`. **The main `kimi` process does not read them directly from `process.env`**; only the values keyed under a `[providers..env]` subtable are recognized by the provider clients. See [Configuration overrides: provider credentials](./overrides.md#provider-credentials) for the full resolution order. + +| Key name | Applicable provider | Purpose | Default | +| --- | --- | --- | --- | +| `KIMI_API_KEY` | Kimi / Moonshot | API key | None | +| `KIMI_BASE_URL` | Kimi / Moonshot | API base URL | `https://api.moonshot.ai/v1` | +| `ANTHROPIC_API_KEY` | Anthropic | API key | None | +| `ANTHROPIC_BASE_URL` | Anthropic | API base URL | Follows the Anthropic SDK default | +| `OPENAI_API_KEY` | OpenAI (used by both `openai` and `openai_responses`) | API key | None | +| `OPENAI_BASE_URL` | OpenAI (used by both `openai` and `openai_responses`) | API base URL | `https://api.openai.com/v1` | +| `GOOGLE_API_KEY` | Google GenAI, Vertex AI (as a fallback for `VERTEXAI_API_KEY`) | API key | None | +| `VERTEXAI_API_KEY` | Vertex AI | API key (when not using ADC) | None | +| `GOOGLE_CLOUD_PROJECT` | Vertex AI | GCP project ID | None | +| `GOOGLE_CLOUD_LOCATION` | Vertex AI | GCP region | None | + +For example, to pre-populate Kimi credentials in `config.toml`: + +```toml +[providers.kimi.env] +KIMI_API_KEY = "sk-xxx" +KIMI_BASE_URL = "https://api.moonshot.ai/v1" +``` + +::: warning Note +`GOOGLE_APPLICATION_CREDENTIALS` (the path to a service-account JSON file) is read by the Google SDK itself from the shell environment, making it the **only** key in this group that goes through a system environment variable. It follows Google Cloud's standard ADC flow, and the CLI is not involved in resolving it. Every other key only takes effect when written into a `[providers..env]` subtable. +::: + +For the full description of provider types and fields, see [Providers](./providers.md). + +## OAuth and the hosted service + +The OAuth flow connects to Kimi's official authentication and hosted endpoints by default. The variables below can point them at a self-hosted or test environment. + +| Environment variable | Purpose | Default | +| --- | --- | --- | +| `KIMI_CODE_OAUTH_HOST` | OAuth authentication host; takes the highest precedence | — (falls back to `KIMI_OAUTH_HOST`, then to the hardcoded default below) | +| `KIMI_OAUTH_HOST` | OAuth authentication host; used as a fallback for `KIMI_CODE_OAUTH_HOST` | — (falls back to the hardcoded default below) | +| `KIMI_CODE_BASE_URL` | Base URL of the hosted Kimi API, used for API calls after OAuth login | `https://api.kimi.com/coding/v1` | + +When neither `KIMI_CODE_OAUTH_HOST` nor `KIMI_OAUTH_HOST` is set, the OAuth authentication host uses the hardcoded constant `https://auth.kimi.com`. + +::: warning Note +`KIMI_CODE_BASE_URL` and the `KIMI_BASE_URL` from the previous section are two different variables: the former targets the OAuth-logged-in hosted service and defaults to `kimi.com`; the latter targets providers that use a Kimi API key directly and defaults to `moonshot.ai`. Distinguish them by use case. +::: + +## Runtime switches + +| Environment variable | Purpose | Valid values / Default | +| --- | --- | --- | +| `KIMI_DISABLE_TELEMETRY` | Disable telemetry reporting | `1`, `true`, `t`, `yes`, `y` (case-insensitive) | +| `KIMI_CODE_BACKGROUND_KEEP_ALIVE_ON_EXIT` | Override `[background].keep_alive_on_exit`, controlling whether still-running background tasks are kept when the session closes | True values: `1`, `true`, `yes`, `on`; false values: `0`, `false`, `no`, `off`; when unset, reads `config.toml`, then falls back to `true` | +| `KIMI_SHELL_PATH` | Override the absolute path to Git Bash (`bash.exe`) on Windows; only needed when auto-detection fails on Windows | None | +| `KIMI_MODEL_MAX_COMPLETION_TOKENS` | Desired budget for `max_completion_tokens` in a single-step LLM request (the actual value is further clamped by the context window and input size); set to `0` or a negative value to disable clamping entirely. **Currently effective only for providers of type `kimi`**; for Anthropic and other providers, use `[models.].max_output_size` instead (see [Config files](./config-files.md#models)) | Defaults to 32000, influenced by `loop_control.reserved_context_size` | + +For example, to disable telemetry on a shared host: + +```sh +export KIMI_DISABLE_TELEMETRY="1" +``` + +`KIMI_CODE_BACKGROUND_KEEP_ALIVE_ON_EXIT` has higher priority than `config.toml`. For example, running `KIMI_CODE_BACKGROUND_KEEP_ALIVE_ON_EXIT=0 kimi -p "..."` temporarily requests stopping background tasks before this process exits, even if the config file sets `keep_alive_on_exit = true`. +## Diagnostic logging + +The variables below control `kimi`'s diagnostic logs. Logs are written to two locations: the global diagnostic log at `$KIMI_CODE_HOME/logs/kimi-code.log`, and each session's own diagnostic log at `/logs/kimi-code.log` (see [Data locations](./data-locations.md#logs-and-update-state) for path details). All of these variables are read only once at process startup. + +| Environment variable | Purpose | Default | +| --- | --- | --- | +| `KIMI_LOG_LEVEL` | Log level; one of `off`, `error`, `warn`, `info`, `debug` | `info` | +| `KIMI_LOG_GLOBAL_MAX_BYTES` | Maximum bytes per global log file | `6291456` (6 MB) | +| `KIMI_LOG_GLOBAL_FILES` | Number of global log files to retain | `5` | +| `KIMI_LOG_SESSION_MAX_BYTES` | Maximum bytes per session log file | `5242880` (5 MB) | +| `KIMI_LOG_SESSION_FILES` | Number of session log files to retain | `3` | + +When an integer variable fails to parse (non-positive integer or non-numeric), it silently falls back to the default value. + +## Clipboard bridge + +`KIMI_WSL_CLIPBOARD_IMAGE_PATH` is injected automatically by the CLI when it spawns the WSL clipboard helper subprocess, used to pass a temporary image path. The variable is written into the PowerShell subprocess's environment and read by the subprocess script internally; the main `kimi` process does not read this variable itself. Setting it in an external shell has **no effect** on the main `kimi` process — users do not need to manage this variable manually. + +## System environment variables + +Kimi Code CLI also reads a handful of standard system environment variables to detect the runtime environment and pick default behavior: + +- `HOME`: the user's home directory, used to resolve the default data path. +- `VISUAL`, `EDITOR`: the executable invoked as the external editor, with `VISUAL` taking precedence. +- `PATH`: used to locate external dependencies such as `rg` and `git`. +- `NO_COLOR`: when set and non-empty, forces color and theme detection off, falling back to the dark theme. Follows the [no-color.org](https://no-color.org) convention. +- `FORCE_COLOR`: when set to `"0"`, also disables color and theme detection, falling back to the dark theme. +- `CI`: when non-empty and not `"0"`, disables theme detection and falls back to the dark theme; the telemetry module also reads this variable to mark the CI environment. +- `LANG`: used to tag the locale in the telemetry context (purely as a tag; it does not change CLI behavior). +- `TERM_PROGRAM`: used to detect terminal support for OSC 9 notifications (iTerm2, WezTerm, ghostty, WarpTerminal, etc.); also written into the telemetry context. +- `TERM`: used to detect terminal support for OSC 9 notifications (xterm-kitty, xterm-ghostty, etc.). +- `TMUX`: detects whether the CLI is running inside tmux, used for the terminal notification path. +- `COLORFGBG`: detects the terminal color scheme (dark / light). +- `DISPLAY`, `WAYLAND_DISPLAY`, `XDG_SESSION_TYPE`: detect a Linux graphical session, used by clipboard and image-related features. A `XDG_SESSION_TYPE` value of `wayland` is also treated as a Wayland session. +- `WSL_DISTRO_NAME`, `WSLENV`: detect whether the CLI is running inside WSL, used for the PowerShell-bridged clipboard fallback. +- `TERMUX_VERSION`: detects whether the CLI is running inside Termux. +- `LOCALAPPDATA`: used on Windows when probing for the Git Bash installation path. + +These variables follow the usual conventions of each operating system; `kimi` only reads them and never modifies them. diff --git a/docs/en/configuration/overrides.md b/docs/en/configuration/overrides.md new file mode 100644 index 000000000..1567000dd --- /dev/null +++ b/docs/en/configuration/overrides.md @@ -0,0 +1,110 @@ +# Configuration overrides + +Kimi Code CLI's runtime parameters come from the user config file, command-line flags, and a handful of runtime paths, endpoints, and switches read from process-level environment variables. Each layer serves a different purpose — the config file captures long-term preferences, CLI flags are well suited for temporary tweaks at this launch, and environment variables mainly locate the data directory, switch OAuth endpoints, and toggle a small number of runtime switches. + +Environment variables are **not a universal fallback for configuration fields** in Kimi Code CLI. They fall into three categories with different scopes, and cannot be flattened into a single linear priority list. + +## Three roles of environment variables + +1. **Config file location**: `KIMI_CODE_HOME` determines the data root holding the config file, sessions, logs, and so on, making the config file path `$KIMI_CODE_HOME/config.toml` (otherwise `~/.kimi-code/`). This is a "where to find the config" step that runs before everything else; it is not a fallback source for ordinary parameters. There is also no `KIMI_CONFIG_PATH`-style variable for pointing at an arbitrary config file. +2. **Runtime switches**: a small number of switches such as `KIMI_DISABLE_TELEMETRY` directly turn off the corresponding subsystem. Even if `config.toml` has `telemetry = true`, telemetry is still disabled whenever this variable is set to a truthy value — its semantics are "additionally disable", not "ordinary override". +3. **Runtime endpoints and diagnostics**: `KIMI_CODE_OAUTH_HOST`, `KIMI_OAUTH_HOST`, `KIMI_CODE_BASE_URL`, `KIMI_LOG_LEVEL`, and friends are read during OAuth and diagnostic subsystem initialization. See [Environment variables](./env-vars.md) for the full list. + +## Priority of ordinary runtime parameters + +For other runtime parameters (model alias, Plan / yolo mode, Skills directories, and so on), resolution is: + +1. **Command-line flags**: parameters supplied at this launch; override every other source and apply only to the current launch. +2. **User config file**: `$KIMI_CODE_HOME/config.toml` (defaulting to `~/.kimi-code/config.toml`), used to capture long-term preferences. + +A few environment variables explicitly override related config fields. For example, `KIMI_CODE_BACKGROUND_KEEP_ALIVE_ON_EXIT` has higher priority than `[background].keep_alive_on_exit`. These exceptions are called out in [Environment variables](./env-vars.md) and in the corresponding [Config files](./config-files.md) field reference. + +::: warning Note +Ordinary runtime parameters **do not** fall back to shell environment variables. For example, provider `api_key` / `base_url` are read only from fields in `config.toml` (including the `[providers..env]` subtable); they do not fall back to shell exports like `export KIMI_API_KEY`. See [Provider credentials](#provider-credentials) below. +::: + +Kimi Code CLI currently reads only one user-level config file. There is no project-level (in-repo) config file mechanism. To isolate configuration between projects, point `KIMI_CODE_HOME` at a different data directory (see [Typical scenarios](#typical-scenarios) below) or temporarily override specific fields with CLI flags at launch. + +## Config file + +The config file location is controlled by the `KIMI_CODE_HOME` environment variable, falling back to `~/.kimi-code/` when unset. The file name is fixed as `config.toml`, and the directory is created with `0o700` permissions. The file can declare long-term preferences such as `default_model`, `providers`, `models`, `thinking`, and `loop_control`. See [Config files](./config-files.md) for the field reference. + +## Provider credentials + +Provider credentials (`api_key`, `base_url`) have their own resolution rules: Kimi Code CLI reads provider fields only from `config.toml` and **does not** fall back to shell environment variables. Running `export KIMI_API_KEY` in your terminal alone will not give a `[providers.]` entry credentials — you have to write them into the config file explicitly. + +For a single provider, credentials are resolved in this order: + +1. `[providers.].api_key` — the key written explicitly into the config file; highest priority. +2. The corresponding key in the `[providers..env]` subtable (such as `KIMI_API_KEY`, `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GOOGLE_API_KEY`) — moving the environment-variable names you would normally write in your shell into a TOML subtable. Used only when `api_key` is left empty. This is just the form of a config sub-table; it does not actually modify your process environment. +3. If both are missing, startup fails with a message that the provider has no credentials configured. + +`base_url` resolves similarly to `api_key`: `[providers.].base_url` is checked first, then `*_BASE_URL` keys (such as `ANTHROPIC_BASE_URL`, `OPENAI_BASE_URL`, `KIMI_BASE_URL`) in `[providers..env]`. See [Providers](./providers.md) for the full reference of provider types and fields. + +## Process-level environment variables + +Variables in `process.env` are read at Kimi Code CLI startup and fall into the three roles described above in [Three roles of environment variables](#three-roles-of-environment-variables): + +- **Data root and log paths**: `KIMI_CODE_HOME` switches `~/.kimi-code/`; `KIMI_LOG_LEVEL` and friends control diagnostic logs. +- **Runtime switches**: `KIMI_DISABLE_TELEMETRY` disables telemetry (overriding `telemetry = true` in `config.toml`). +- **OAuth endpoints and diagnostics**: `KIMI_CODE_OAUTH_HOST`, `KIMI_OAUTH_HOST`, and `KIMI_CODE_BASE_URL` control the hosted Kimi login endpoints; `KIMI_LOG_LEVEL` and friends control diagnostic logs. +- **Background-task exit policy**: `KIMI_CODE_BACKGROUND_KEEP_ALIVE_ON_EXIT` overrides `[background].keep_alive_on_exit`, letting you decide for this process whether background tasks are kept on exit. + +See [Environment variables](./env-vars.md) for the full list of variables and their scopes. + +## Command-line flags + +Parameters supplied via CLI flags at launch have the highest priority and only apply to the current launch. Common flags: + +| Flag | Effect | +| --- | --- | +| `-S, --session [id]` | Resume the specified session; without an id, enters interactive selection | +| `-C, --continue` | Continue the most recent session for the current working directory | +| `-y, --yolo` | Auto-approve ordinary tool calls (aliases: `--yes`, `--auto-approve`) | +| `--plan` | Launch in Plan mode | +| `-m, --model ` | Specify the model alias to use for this launch | +| `-p, --prompt ` | Execute a single prompt in non-interactive mode and exit | +| `--output-format ` | Specify the output format for `-p` mode: `text` or `stream-json` | +| `--skills-dir ` | Replace the auto-discovered Skills directory (can be specified multiple times; applies to this launch only) | + +Mutually exclusive flag rules: + +- `--output-format` can only be used in prompt mode (`-p / --prompt`). +- `--prompt` cannot be combined with `--yolo`, nor with `--plan`. +- In prompt mode, `-S / --session` must be given an id; the interactive selector (bare `--session`) is not accepted. +- `--continue` and `--session` cannot be used together. +- Outside prompt mode, `--yolo` cannot be combined with `--continue` or `--session`; `--plan` cannot be combined with `--continue` or `--session`. +- `--yolo` and `--plan` can be used together. + +::: tip Tip +`--skills-dir` replaces the auto-discovered Skills directory for this launch and is suitable for one-off use. To persistently append search directories, set `extra_skill_dirs` at the top level of `config.toml` (see [Agent Skills](../customization/skills.md)). The two options have different semantics and can be chosen based on your needs. +::: + +## Typical scenarios + +**Switch the data directory for isolated testing.** `KIMI_CODE_HOME` simultaneously affects the config file, session archives, ripgrep cache, and every other data location: + +```sh +KIMI_CODE_HOME="$PWD/.kimi-sandbox" kimi +``` + +**Stage temporary credentials in the config file.** Since provider credentials are read only from `config.toml`, to use a different API key for a single launch, write it into the `[providers..env]` subtable in advance: + +```toml +[providers.kimi.env] +KIMI_API_KEY = "sk-test" +``` + +You can also set `api_key` directly on the provider; see [Provider credentials](#provider-credentials) above for the priority between the two. + +**Skip approvals for this launch.** Suitable for batch tasks you already know are safe: + +```sh +kimi --yolo +``` + +**Enter Plan mode for this launch.** If you want the same default behavior, set `default_plan_mode = true` in the config file: + +```sh +kimi --plan +``` diff --git a/docs/en/configuration/providers.md b/docs/en/configuration/providers.md new file mode 100644 index 000000000..a7319dd17 --- /dev/null +++ b/docs/en/configuration/providers.md @@ -0,0 +1,136 @@ +# Providers and models + +Kimi Code CLI integrates with multiple LLM platforms through a unified provider abstraction. Each provider handles one API protocol, and models are declared on top of a provider with their own name, context length, and capabilities. This page describes every provider type currently supported and how to configure them in `~/.kimi-code/config.toml`. + +## Overview + +The `type` field of each entry in the `providers` table determines which implementation is used. The currently supported types are: + +| Type | Protocol | Typical platforms | +| --- | --- | --- | +| `kimi` | OpenAI-compatible (chat completions style) | Kimi Code, Moonshot AI Open Platform | +| `anthropic` | Anthropic Messages | Claude API | +| `openai` | OpenAI Chat Completions | OpenAI and compatible services | +| `openai_responses` | OpenAI Responses API | OpenAI's newer Responses endpoint | +| `google-genai` | Google GenAI | Gemini API | +| `vertexai` | Google GenAI on Vertex | Google Cloud Vertex AI | + +All providers stream model interactions by default. Thinking, vision, and tool-call capabilities are matched automatically by model name prefix, so you do not need to spell them out in the config. + +API keys may be written into the `api_key` field, or supplied under the `[providers..env]` sub-table. The lookup order is `api_key` > sub-table key > missing error. **Kimi Code CLI does not automatically fall back to shell environment variables** — `export KIMI_API_KEY` in your terminal alone will not give a provider credentials; you must write them into `config.toml` (see [Configuration overrides: provider credentials](./overrides.md#provider-credentials) for details). `api_key` and `oauth` are mutually exclusive on the same provider; setting both causes an error when the model is resolved. OAuth credentials are handled by the built-in login flow and do not need to be configured manually. + +The `[providers..env]` sub-table lets you supply credentials or endpoint overrides directly inside `config.toml`. These values are scoped to the provider and do not leak into the global shell environment: + +```toml +[providers.my-anthropic.env] +ANTHROPIC_API_KEY = "sk-ant-xxxxx" +ANTHROPIC_BASE_URL = "https://my-proxy.example.com" +``` + +The most common ways to switch providers are: use the `/model` slash command inside the TUI to pick from already-configured models, or edit `config.toml` directly to adjust the `[providers.*]` and `[models.*]` tables. See [Config files](./config-files.md) for the full field reference. + +## `kimi` + +`kimi` connects to the Moonshot AI API using the OpenAI-compatible protocol. + +- Default `base_url`: `https://api.moonshot.ai/v1` +- Environment variables: `KIMI_API_KEY`, `KIMI_BASE_URL` +- Extra capability: video upload + +```toml +[providers.kimi] +type = "kimi" +base_url = "https://api.moonshot.ai/v1" +api_key = "sk-xxxxx" +``` + +The Kimi Code hosted service configures its `base_url` and credentials automatically after OAuth login; see [OAuth and credential injection](#oauth-and-credential-injection) and [Environment variables](./env-vars.md) for details. + +## `anthropic` + +`anthropic` integrates with the Claude API. Standard Claude models automatically enable vision, tool calls, and thinking where supported. For custom or unreleased models, declare `capabilities` explicitly under `[models.]`. + +Thinking can be controlled via `/model`, `/settings`, or configuration. + +- Default `base_url`: follows the Anthropic SDK default +- Environment variables: `ANTHROPIC_API_KEY`, `ANTHROPIC_BASE_URL` +- Default `max_tokens`: set automatically per model. To override it (for example for testing or for an unrecognized alias), set `max_output_size` on the model alias (see [`config-files.md`](./config-files.md#models)). Recognized aliases are capped at the documented server-side ceiling. + +```toml +[providers.anthropic] +type = "anthropic" +api_key = "sk-ant-xxxxx" + +[models."claude-opus-4-7"] +provider = "anthropic" +model = "claude-opus-4-7" +max_context_size = 200000 +# Optional: lower the output budget for testing, or set one for a model +# this CLI doesn't know about yet. Omit to use the per-model default +# above. +# max_output_size = 32000 +``` + +## `openai` + +`openai` corresponds to the OpenAI Chat Completions protocol and can also be used to connect to any third-party service that speaks the same protocol (simply override `base_url`). Thinking, vision, and tool-call capabilities are inferred automatically from the model name. + +- Default `base_url`: `https://api.openai.com/v1` +- Environment variables: `OPENAI_API_KEY`, `OPENAI_BASE_URL` + +```toml +[providers.openai] +type = "openai" +base_url = "https://api.openai.com/v1" +api_key = "sk-xxxxx" +``` + +## `openai_responses` + +`openai_responses` corresponds to OpenAI's newer Responses API. It always operates in streaming mode; capabilities are inferred automatically from the model name. + +- Default `base_url`: `https://api.openai.com/v1` +- Environment variables: `OPENAI_API_KEY`, `OPENAI_BASE_URL` + +```toml +[providers.openai-responses] +type = "openai_responses" +base_url = "https://api.openai.com/v1" +api_key = "sk-xxxxx" +``` + +## `google-genai` + +`google-genai` connects directly to the Google Gemini API. Thinking, vision, and multimodal capabilities are inferred automatically from the model name. + +- Environment variable: `GOOGLE_API_KEY` + +```toml +[providers.gemini] +type = "google-genai" +api_key = "xxxxx" +``` + +## `vertexai` + +`vertexai` shares the same implementation as `google-genai`; setting `type = "vertexai"` switches it to the Vertex AI access path. + +Authentication follows the standard Google Cloud flow: authenticate via `gcloud auth application-default login`, or set `GOOGLE_APPLICATION_CREDENTIALS` to a service-account JSON (this step is a generic Google SDK mechanism unrelated to Kimi Code configuration). **The project and region must be written into the `[providers.vertexai.env]` sub-table** — `export GOOGLE_CLOUD_PROJECT` or `export GOOGLE_CLOUD_LOCATION` in the shell will not be read by the CLI. When `GOOGLE_CLOUD_LOCATION` is missing, the CLI tries to infer it from `base_url`. API keys (`VERTEXAI_API_KEY` or `GOOGLE_API_KEY`) likewise go into the sub-table. + +```toml +[providers.vertexai] +type = "vertexai" + +[providers.vertexai.env] +GOOGLE_CLOUD_PROJECT = "my-gcp-project" +GOOGLE_CLOUD_LOCATION = "us-central1" +``` + +```sh +gcloud auth application-default login # one-time +kimi +``` + +## OAuth and credential injection + +Some platforms (such as the Kimi Code hosted service) use OAuth instead of static API keys. Credentials are injected at runtime by the built-in kimi-oauth toolchain, and the login flow takes care of writing and refreshing them automatically — there is nothing to configure by hand for this in your config file. diff --git a/docs/en/customization/agents.md b/docs/en/customization/agents.md new file mode 100644 index 000000000..411d15f2c --- /dev/null +++ b/docs/en/customization/agents.md @@ -0,0 +1,45 @@ +# Agents and subagents + +The agent in Kimi Code CLI is the core that drives a session. A session is always hosted by a single main agent, which may dispatch one or more subagents to handle more focused subtasks as the work progresses. + +The main agent is responsible for understanding the user's overall intent, planning the steps, conversing with the user, calling tools, and finally aggregating the results. Its context spans the entire session, and it is the entity the user interacts with directly in the terminal. + +A subagent is a temporary "assistant" spawned by the main agent. It takes a clearly defined task description, completes the work independently in its own context, and then returns the conclusion to the main agent. Subagents do not converse with the user directly, nor do they pollute the main agent's context history. This division of labor is particularly suited for work with clear boundaries that requires a lot of reading but produces a short output — such as "exploring a codebase", "reviewing a large implementation", or "planning a complex change". + +## Built-in subagents + +Kimi Code CLI ships with three built-in subagents, each targeting a different kind of task, ready to use out of the box: + +- **`coder`**: The default subagent, a general software engineering assistant that can read and write files, run shell commands, search code, and land concrete changes. +- **`explore`**: Dedicated to codebase exploration. It operates read-only and will not modify any files. Suitable for quickly searching, reading, and summarizing a repository without making changes. +- **`plan`**: For implementation planning and architectural design. Its toolset is narrowed further — not even shell commands are provided — focusing on "thinking through how to do it" rather than "doing it". + +## Invocation + +Subagents are dispatched automatically by the main agent. The main agent decides when to dispatch one based on task complexity, context usage, and the independence of the subtask — you don't need to manually specify the timing. + +Each dispatch surfaces as an approval request in the terminal (unless it matches an existing allow rule or you are in YOLO mode), so you can inspect the task description before it runs. You can also ask the main agent to prefer a particular type of subagent for a task — for example, "use `explore` first to map out the relevant files before making changes." + +Subagents can also run in the background; their results are synthesized back to the main agent automatically on a later turn, with no manual polling needed. You can also resume an existing subagent instance to continue the same task. + +## Context isolation and resource cost + +Each subagent has a completely independent context window. It cannot see the main agent's conversation history; it can only see the task description that the main agent explicitly passed. The subagent's own intermediate thoughts and tool call records do not flow back to the main agent — only the final result appears in the main agent's context. + +This isolation brings two direct benefits. First, the main agent's context stays concise and isn't flooded with exploratory logs over a long session. Second, multiple subagents can run in parallel without interfering with each other. + +The cost is that each subagent independently consumes model tokens, so it is unnecessary to dispatch a subagent for simple tasks — letting the main agent handle them directly is more economical. Subagents also cannot dispatch further subagents. + +## Permission inheritance + +A subagent's permission rules are inherited from the main agent: "always allow" rules that the main agent has accepted via `/permission` or during an approval automatically cover every subagent it dispatches, so the subagent does not need to re-approve the same kind of tool call. The `Agent` tool itself is allowed by default, so the main agent can complete multiple delegations without interrupting the user. + +If you want a particular kind of tool to be permanently unavailable inside subagents, you should tighten the main agent's permission rules. + +## Storage location in the session directory + +A subagent's runtime state is persisted to the session directory for later inspection and debugging. Each subagent instance corresponds to a separate subdirectory under `agents/`, which contains a `wire.jsonl` file recording prompts, message history, and final status in chronological order. Background subagents additionally expose their lifecycle through the `tasks/` subdirectory. + +::: warning Note +Session directories, wire files, and task records are local debugging material and may contain user prompts, command output, repository paths, tool results, or traces of credentials. Do not commit these files to public repositories, issues, or chat transcripts directly; redact them first if you need to share them. +::: diff --git a/docs/en/customization/hooks.md b/docs/en/customization/hooks.md new file mode 100644 index 000000000..404261b3a --- /dev/null +++ b/docs/en/customization/hooks.md @@ -0,0 +1,150 @@ +# Hooks + +Hooks let you run local commands at key lifecycle points in Kimi Code CLI. They are useful for lightweight policy checks, audit logging, desktop notifications, or wiring into local automation scripts — for example, intercepting a risky tool call before it runs, or firing a notification when a background subagent finishes. + +Hook commands run in the local shell, and Kimi Code CLI writes the event payload as JSON to the command's stdin. The command's stdout, stderr, and exit code determine the hook result. Except for explicit blocking cases, hook failures fail open and do not interrupt the main flow because of a misbehaving script. + +::: warning Note +Hooks are useful for local notifications and lightweight interception, but they should not be treated as the only security boundary. Script errors, timeouts, and ordinary non-zero exit codes fail open and continue to allow the operation; high-risk tool calls should still rely on permission approval and human review. +::: + +## Configuration + +Declare hooks in `~/.kimi-code/config.toml` with `[[hooks]]` array tables: + +```toml +[[hooks]] +event = "PreToolUse" +matcher = "Bash" +command = "node ~/.kimi-code/hooks/check-bash.mjs" +timeout = 5 + +[[hooks]] +event = "Notification" +matcher = "task\\.completed" +command = "terminal-notifier -title Kimi -message 'Background task finished'" +``` + +The fields are: + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `event` | `string` | Yes | Event name. The value must be one of the entries in the "Events" table below; any other value causes the entire config to fail to load | +| `matcher` | `string` | No | Regular expression matched against the event target. Missing or empty means match everything | +| `command` | `string` | Yes | Shell command to run. Must be non-empty | +| `timeout` | `integer` | No | Timeout in seconds, range 1–600. Defaults to 30 seconds when unset | + +Each `[[hooks]]` table accepts only these four fields. Misspelled or extra fields cause the configuration file to fail to parse. + +When one event fires, all matching hooks run in parallel. If multiple entries share the exact same `command`, that command runs only once. `matcher` uses JavaScript regular expression semantics; invalid regular expressions are silently skipped and treated as no match. + +Hook commands are launched through the shell (equivalent to `sh -c `), and the child process's working directory is the current session's `cwd`. On non-Windows platforms, the child is placed in its own process group; on timeout or session interruption, Kimi Code CLI first sends `SIGTERM` and then `SIGKILL` 100 milliseconds later, so any grandchild processes forked inside the hook are cleaned up as well. + +JSON fields passed to hooks use snake_case. Every payload includes: + +```json +{ + "hook_event_name": "PreToolUse", + "session_id": "session_abc", + "cwd": "/path/to/project" +} +``` + +Additional fields depend on the event type, as shown in the event table below. + +## Return values + +Hook command exit codes and stdout are interpreted as follows: + +| Result | Behavior | +| --- | --- | +| Exit code `0` | Allow. If stdout is JSON, text may be read from `message` or `hookSpecificOutput.message` | +| Exit code `2` | Block. stderr is used as the blocking reason | +| Any other non-zero exit code | Fail open and allow | +| Timeout or process error | Fail open and allow | + +If stdout is JSON and `hookSpecificOutput.permissionDecision` is `deny`, the result is also treated as a block: + +```json +{ + "hookSpecificOutput": { + "permissionDecision": "deny", + "permissionDecisionReason": "Use rg instead" + } +} +``` + +Blocking only applies to events that participate in control flow. For example, `PreToolUse` can block a tool call, and `Stop` can append one continuation message to the current turn. Observer events (such as `PostToolUse`, `PostToolUseFailure`, `PostCompact`, `SubagentStop`, `StopFailure`, and `Notification`) are dispatched asynchronously in a fire-and-forget fashion; their return values are ignored and do not change the main flow. `PreCompact` is invoked with `trigger` (not `triggerBlock`); its return value is likewise completely ignored, and it is not a blockable event. + +When a block takes effect, if the script does not provide a reason through stderr or JSON output, the CLI falls back to `Blocked by hook` as a placeholder reason. A `PreToolUse` block is written back into context as a failed tool result, so the model can choose an alternative based on the reason. + +## Events + +The following events are triggered automatically today: + +| Event | Matcher | Main payload | Behavior | +| --- | --- | --- | --- | +| `UserPromptSubmit` | Text content submitted by the user | `prompt` (`ContentPart[]` array) | Fires only for real user messages. Text returned by the hook is wrapped as a hook result, written into session history for transcript/replay, shown to the user, and the current LLM turn continues without sending the hook result to the model; if the hook blocks, the block reason is returned to the user as an assistant message and no model call is made; if all hooks produce no output, the normal LLM turn continues | +| `PreToolUse` | Tool name | `tool_name`, `tool_input`, `tool_call_id` | Fires before permission checks. If blocked, the tool does not run | +| `PostToolUse` | Tool name | `tool_name`, `tool_input`, `tool_call_id`, `tool_output` | Fires after a successful tool call. `tool_output` is truncated to the first 2000 characters | +| `PostToolUseFailure` | Tool name | `tool_name`, `tool_input`, `tool_call_id`, `error` | Fires after a tool call fails or is blocked by a hook | +| `Stop` | Empty string | `stop_hook_active` | Fires when the model is about to stop. If blocked, the reason is appended directly to context as a system-triggered user message, and the turn may continue once | +| `StopFailure` | Error type | `error_type`, `error_message` | Fires after the current turn fails with a non-cancellation error | +| `SessionStart` | `startup` or `resume` | `source` | Fires after the main agent is created for a new session, or after a historical session is resumed | +| `SessionEnd` | `exit` | `reason` | Fires after the session is closed and its metadata is flushed | +| `SubagentStart` | Subagent name | `agent_name`, `prompt` | Fires after a subagent is configured and before it actually starts running. `prompt` is truncated to the first 500 characters | +| `SubagentStop` | Subagent name | `agent_name`, `response` | Fires asynchronously after a subagent completes successfully; does not fire on failure. `response` is truncated to the first 500 characters | +| `PreCompact` | `manual` or `auto` | `trigger`, `token_count` | Fires before context compaction actually starts. This event is invoked with `trigger` (not `triggerBlock`); its return value is completely ignored and blocking decisions are not read | +| `PostCompact` | `manual` or `auto` | `trigger`, `estimated_token_count` | Fires asynchronously after context compaction is successfully written. Blocking results do not change the main flow | +| `Notification` | Notification type | `sink`, `notification_type`, `title`, `body`, `severity`, `source_kind`, `source_id` | Currently fires when a background subagent result is written into context. `notification_type` is one of `task.completed`, `task.failed`, `task.killed`, or `task.lost`; the sink is `context` | + +`UserPromptSubmit` return text is wrapped as a hook result: + +```xml + +hook response + +``` + +If multiple `UserPromptSubmit` hooks return text, each result gets its own `` tag. This message keeps its hook-result origin for transcript/replay, but is not sent to the model. The model sees the original user prompt and the current turn continues. + +If a `UserPromptSubmit` hook blocks the request, the block reason uses the same format and is returned to the user, but the turn does not continue to a model call. + +`Stop` block reasons are appended directly as system-triggered user messages so the current turn can continue: + +```text +continue from hook +``` + +## Example: block risky shell commands + +The following hook reads `tool_input.command` from stdin before a `Bash` tool call. If the command contains `rm -rf`, the script exits with code `2` and writes the reason to stderr: + +::: warning Note +This example only demonstrates how a hook blocks a tool call; it is not a complete shell safety parser. Real policies are better implemented with an allowlist, or with dedicated shell parsing that handles quoting, variable expansion, aliases, and multi-part commands. +::: + +```toml +[[hooks]] +event = "PreToolUse" +matcher = "Bash" +command = "node ~/.kimi-code/hooks/block-dangerous-bash.mjs" +timeout = 5 +``` + +```js +let input = ''; +process.stdin.on('data', (chunk) => { + input += chunk; +}); +process.stdin.on('end', () => { + const payload = JSON.parse(input); + const command = payload.tool_input?.command ?? ''; + if (command.includes('rm -rf')) { + console.error('Blocked dangerous shell command'); + process.exit(2); + } +}); +``` + +When the hook blocks the tool call, Kimi Code CLI writes the blocking reason back into context as a failed tool result, so the model can choose a safer alternative. diff --git a/docs/en/customization/mcp.md b/docs/en/customization/mcp.md new file mode 100644 index 000000000..bbde8cc47 --- /dev/null +++ b/docs/en/customization/mcp.md @@ -0,0 +1,83 @@ +# Model Context Protocol + +[Model Context Protocol (MCP)](https://modelcontextprotocol.io/) is an open protocol that lets models safely call tools exposed by external processes or services. Kimi Code CLI acts as an MCP client to integrate these external tools, exposing them to the agent alongside built-in tools (`Read`, `Bash`, `Grep`, etc.). + +## Integration scope + +Kimi Code CLI connects to MCP servers via stdio (local subprocess) or HTTP. Once connected, MCP tools behave the same as built-in tools: they are available to the agent, subject to permission rules, and go through the approval flow. + +## Configuration and login + +MCP server configurations live in `mcp.json` in two layers: + +- User-level: `~/.kimi-code/mcp.json` (or `$KIMI_CODE_HOME/mcp.json`), shared across projects +- Project-level: `.kimi-code/mcp.json` in the current workspace + +Project entries override user-level entries with the same name. + +The easiest entry point is running `/mcp-config` in the TUI, which guides you through adding, editing, or removing servers. To check connection status, run `/mcp`. + +The top-level shape of `mcp.json` is: + +```json +{ + "mcpServers": { + "filesystem": { + "command": "npx", + "args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"] + }, + "linear": { + "url": "https://mcp.linear.app/mcp" + } + } +} +``` + +Entries with `command` are stdio servers; entries with `url` are HTTP servers, so you usually do not need to write a `transport` field. HTTP servers can provide static credentials through `headers` or `bearerTokenEnvVar`. When OAuth is required, run `/mcp-config login ` to complete browser authorization. + +Optional fields: + +| Field | Type | Applies to | Description | +| --- | --- | --- | --- | +| `env` | `Record` | stdio | Environment variables injected into the subprocess | +| `cwd` | `string` | stdio | Working directory for the subprocess | +| `headers` | `Record` | HTTP | Static headers appended to every request | +| `enabled` | `boolean` | both | Set to `false` to disable the server without removing the entry | +| `startupTimeoutMs` | `number` | both | Connection timeout in milliseconds, default `30000` | +| `toolTimeoutMs` | `number` | both | Per-tool-call timeout in milliseconds | +| `enabledTools` | `string[]` | both | Allowlist: only expose the tools in this list | +| `disabledTools` | `string[]` | both | Blocklist: exclude the tools in this list | + +::: warning Note +Stdio entries in a project-level `.kimi-code/mcp.json` execute local commands when the session starts. Only enable project-level MCP servers in repositories you trust. +::: + +## Tool naming and permissions + +MCP tools are exposed using the naming convention `mcp____`. Permission matching supports `*` and `**` wildcards, so `mcp__github__*` covers every tool from the `github` server. + +Calls that do not match any rule trigger an approval request. Choosing "Approve for this session" in the approval prompt auto-approves subsequent matching calls. + +You can also pre-load permanent rules in the `[[permission.rules]]` array of tables in `config.toml`: + +```toml +[[permission.rules]] +decision = "allow" +pattern = "mcp__github__*" + +[[permission.rules]] +decision = "deny" +pattern = "mcp__filesystem__write_file" +``` + +The full syntax of the `pattern` field and the accepted values of other fields such as `decision`, `scope`, and `reason` are documented in [Config files](../configuration/config-files.md#permission). + +## Security + +- Only integrate MCP servers from trusted sources +- Check that the tool name and arguments are reasonable in approval requests +- Keep manual approval for high-risk tools, and avoid broad `mcp__*` wildcard allowlisting + +::: warning Note +In YOLO mode, MCP tool calls are auto-approved like any other tool. Only use this mode when you fully trust the integrated MCP servers. +::: diff --git a/docs/en/customization/skills.md b/docs/en/customization/skills.md new file mode 100644 index 000000000..f0a5fef31 --- /dev/null +++ b/docs/en/customization/skills.md @@ -0,0 +1,127 @@ +# Agent Skills + +Agent Skills are the lightweight mechanism Kimi Code CLI uses to extend a model's capabilities. A skill is a Markdown document with YAML frontmatter that describes a piece of expertise or a workflow. Kimi Code CLI scans known directories on startup and injects the discovered skills into the system prompt, so the agent knows which skills are available in the current session. + +Compared to pasting the same guidance into the prompt every time, skills offer these advantages: the content is captured in a file, reusable across projects and teams, can be loaded with a one-shot slash command, and can also be invoked automatically by the model when needed. + +## Creating a skill + +Skill files must be placed in a [known scan directory](#skill-locations). A skill can use one of two file structures: + +- **Directory form (recommended)**: create a subdirectory with the main file named `SKILL.md`. You can also place scripts, references, and other auxiliary files in the same directory. If both `/SKILL.md` and a flat `.md` exist in the same directory, the subdirectory wins. +- **Flat form**: use a single `.md` file directly; the skill name defaults to the filename with `.md` removed. + +### File format + +`SKILL.md` consists of two parts: YAML frontmatter and a Markdown body. + +```markdown +--- +name: code-style +description: Project code style conventions, covering naming, indentation, comments, and file organization +type: prompt +whenToUse: When the user asks me to write, modify, or review the project's source code +disableModelInvocation: false +arguments: + - target + - mode +--- + +Please handle the code according to the following conventions: + +- Use 2-space indentation +- Use `camelCase` for variable names and `PascalCase` for type names +- Public functions must have TSDoc comments +- Lines must not exceed 100 characters +``` + +### Frontmatter fields + +| Field | Description | +| --- | --- | +| `name` | Skill name. Required in a directory-style `SKILL.md`; in a flat `.md` file the filename is used when omitted. Names are case-insensitive. | +| `description` | A one-line summary. The model uses it to decide when to use this skill. Required in a directory-style `SKILL.md`; in a flat `.md` file falls back to the first non-empty line of the body (truncated to 240 characters) when omitted. | +| `type` | Skill type. Supported values: `prompt` (default), `inline` (same semantics as `prompt`), or `flow` (manual invocation only, not auto-invoked by the model). Other values are skipped. | +| `whenToUse` | Description of the trigger scenario. Aliases `when-to-use` and `when_to_use` are also accepted. | +| `disableModelInvocation` | Set to `true` to forbid the model from invoking this skill automatically. Aliases `disable-model-invocation` and `disable_model_invocation` are also accepted. | +| `arguments` | Named arguments the skill accepts. Can be written as an array of strings, or as a single whitespace-separated string (e.g. `arguments: target mode`). Once declared, the body can read them with `$`. Purely numeric or empty entries are ignored. | + +::: warning Note +In a directory-style `SKILL.md`, both `name` and `description` **must** be filled in explicitly. Omitting either one causes the skill to fail parsing. +::: + +### Body placeholders + +The skill body expands a small set of placeholders before being sent to the model: + +- `$ARGUMENTS`: the complete raw argument string passed when the skill was invoked +- `$ARGUMENTS[0]`, `$ARGUMENTS[1]`, and shorthand `$0`, `$1`: positional arguments after whitespace splitting, starting at 0 +- `$`: a named argument declared in `arguments` +- `${KIMI_SKILL_DIR}`: the directory containing the current skill file + +Positional arguments respect single- and double-quoted text: in `/skill:commit "fix login" patch`, `$0` expands to `fix login`. If the body does not contain any argument placeholders, the appended text is added to the end of the body as `\n\nARGUMENTS: `. + +## Skill locations + +Kimi Code CLI scans skills in four scope tiers, with more specific scopes taking higher priority: + +**Project > User > Extra > Built-in** + +User-level: + +- `~/.kimi-code/skills/` +- `~/.agents/skills/` + +Project-level (project root = the nearest ancestor directory containing `.git`): + +- `.kimi-code/skills/` +- `.agents/skills/` + +Extra directories are declared via the top-level `extra_skill_dirs` field in `config.toml`: + +```toml +extra_skill_dirs = ["~/team-skills", ".agents/team-skills"] +``` + +Built-in skills ship with CLI and have the lowest priority. + +## Invoking a skill + +Users can invoke a skill explicitly with a slash command: + +``` +/skill:code-style +/skill:git-commits Fix the concurrency issue in the login API +``` + +The model can also decide to invoke a skill automatically based on `description` and `whenToUse`, unless `disableModelInvocation` is set to `true` or `type` is `flow`. When the model invokes a skill, body placeholders are expanded first, then the content is injected into the system prompt. Skill calls are allowed to nest up to 3 levels deep; deeper invocations are terminated. + +## Full example + +```markdown +--- +name: review-pr +description: Review a pull request against the team's standards and produce a structured review report +type: prompt +whenToUse: When the user asks me to review a PR, inspect code changes, or assess commit quality +arguments: + - pr_ref +--- + +Please review the PR specified by the user using the following process: $pr_ref + +1. Fetch and read the full diff of `$pr_ref`. +2. Check against the following items one by one: + - Whether corresponding test cases are included + - Whether public APIs have documentation updates + - Whether new dependencies are introduced; if so, explain the rationale + - Whether error handling covers edge cases +3. Refer to the checklist in the same directory: `references/checklist.md` +4. Produce a review report containing: + - Overall conclusion (approve / request changes / comment) + - Required changes (blocking) + - Suggested improvements (non-blocking) + - Things worth acknowledging +``` + +Save the above as `~/.kimi-code/skills/review-pr/SKILL.md`, and put the checklist at `references/checklist.md` in the same directory. Then restart the session, and you can invoke it via `/skill:review-pr #1234`; the appended `#1234` will expand into `$pr_ref` in the body. diff --git a/docs/en/faq.md b/docs/en/faq.md new file mode 100644 index 000000000..a421d16db --- /dev/null +++ b/docs/en/faq.md @@ -0,0 +1,3 @@ +# FAQ + +This page collects the questions new users hit most often, along with short answers. When a topic needs a fuller explanation, the answer links to the relevant doc at the end. diff --git a/docs/en/guides/getting-started.md b/docs/en/guides/getting-started.md new file mode 100644 index 000000000..120b74e6c --- /dev/null +++ b/docs/en/guides/getting-started.md @@ -0,0 +1,135 @@ +# Getting started + +## What is Kimi Code CLI + +Kimi Code CLI is an AI agent that runs in the terminal, helping you carry out software development tasks and day-to-day terminal operations. It can read and edit code, run shell commands, search files, and fetch web pages, autonomously planning and adjusting the next step based on feedback as it works. + +It fits scenarios such as: + +- **Writing and modifying code**: implementing new features, fixing bugs, completing refactors +- **Understanding a project**: exploring an unfamiliar codebase and answering questions about architecture and implementation +- **Automating tasks**: batch-processing files, running builds and tests, chaining multiple scripts together + +The entire CLI is written in TypeScript, distributed through npm, and runs on Node.js. + +## Installation + +### Install script (recommended) + +The quickest way to install Kimi Code CLI is with the install script; no pre-installed Node.js is required: + +- **macOS / Linux**: + +```sh +curl -fsSL https://code.kimi.com/kimi-code/install.sh | bash +``` + +- **Windows (PowerShell)**: + +```powershell +irm https://code.kimi.com/kimi-code/install.ps1 | iex +``` + +This downloads the latest release, verifies the checksum, and places the `kimi` executable on your `PATH`. + +### npm installation + +If you prefer to install via npm, you need Node.js 24.15.0 or later: + +```sh +node --version +``` + +The package name is `@moonshot-ai/kimi-code`: + +```sh +npm install -g @moonshot-ai/kimi-code +``` + +Or with pnpm: + +```sh +pnpm add -g @moonshot-ai/kimi-code +``` + +## Upgrade and uninstall + +After installation, verify that the executable is ready: + +```sh +kimi --version +``` + +**Upgrade**: if you installed via the script, re-run it. If you installed via npm: + +```sh +npm install -g @moonshot-ai/kimi-code@latest +``` + +**Uninstall**: if you installed via the script, remove the `kimi` executable. If you installed via npm: + +```sh +npm uninstall -g @moonshot-ai/kimi-code +``` + +## First launch + +Move into the project directory you want to work in and simply run `kimi` to start the interactive UI: + +```sh +cd your-project +kimi +``` + +If you only want to run a single instruction without entering the interactive UI, use the `-p` flag: + +```sh +kimi -p "Take a look at this project's directory structure" +``` + +To resume the previous session, add the `-C` flag: + +```sh +kimi -C +``` + +On the first launch, Kimi Code CLI has no credentials yet, and you need to configure an API source before you can start a conversation. In the interactive UI, enter the slash command `/login` to begin the login flow: + +``` +/login +``` + +`/login` opens a platform selector supporting: + +- **Kimi Code** — OAuth device code flow; open the URL on any device, sign in, and enter the code to authorize +- **Moonshot AI Open Platform** — log in directly with an API key + +To sign out, enter `/logout` to clear the current credentials. + +::: tip +If you want to use Anthropic, OpenAI, Google, or other providers, edit `config.toml` directly to configure the API key. See [Providers and models](../configuration/providers.md) for details. Runtime configuration such as the model and provider is also written to `config.toml`. See [Config files](../configuration/config-files.md), [Environment variables](../configuration/env-vars.md), and [Configuration overrides](../configuration/overrides.md) for details. +::: + +## Your first conversation + +Once login is complete, you can describe a task to Kimi Code CLI directly in natural language. For example, you can have it familiarize itself with the current project first: + +``` +Take a look at this project's directory structure and briefly describe what each directory is for. +``` + +Kimi Code CLI will automatically call file-reading, search, and web-fetching tools, browse the relevant content, and then give you an answer. Read-only operations such as reading files and searching the web are executed automatically by default without requiring confirmation. For operations that modify files or run shell commands, it asks for your confirmation before executing by default, and you can approve or reject as you see fit. + +You can also have it do something more concrete, such as: + +``` +Add a function in src/utils that converts any string to kebab-case, and add a unit test for it. +``` + +Kimi Code CLI plans the steps, modifies the code, runs the tests, and tells you what it did at each step. + +In the interactive UI, entering `/help` shows all available [slash commands](../reference/slash-commands.md) along with common keyboard shortcut hints. To exit Kimi Code CLI, enter `/exit`. You can also press `Ctrl-C` — the UI will first clear the current input and prompt you to press it again, and the second press exits. Or press `Ctrl-D` twice with the input box empty to exit. + +## Where data is stored + +Kimi Code CLI stores its local data under `~/.kimi-code/` by default, including config files, session records, logs, and the update cache. If you want to move it elsewhere, you can point to a new root directory via the `KIMI_CODE_HOME` environment variable. For the full directory layout and environment variable reference, see [Data locations](../configuration/data-locations.md) and [Environment variables](../configuration/env-vars.md). diff --git a/docs/en/guides/interaction.md b/docs/en/guides/interaction.md new file mode 100644 index 000000000..56279d570 --- /dev/null +++ b/docs/en/guides/interaction.md @@ -0,0 +1,67 @@ +# Interaction and Input + +Kimi Code CLI runs as an interactive TUI with an input box, conversation view, and status bar. This page covers basic operations, mode switches, the approval flow, and shortcuts. The full list is available via `/help`. + +## Input box basics + +The input box accepts free-form text; pressing `Enter` sends, and `Shift-Enter` inserts a line break. When the input box is empty, press `↑` / `↓` to browse history for the current directory. + +Press `Ctrl-D` twice to exit the CLI. `Ctrl-C` interrupts the current turn while streaming; press it twice in the idle state to exit. `Esc` closes dialogs and also interrupts a turn while streaming. + +## Slash commands and `@` mentions + +Anything starting with `/` is recognized as a slash command, covering session management, mode switching, configuration, and more — such as `/help`, `/new`, `/sessions`, and `/model`. See the [slash command reference](../reference/slash-commands.md) for the full list. + +Type `/` to open the command completion menu, which also includes commands from [Agent Skills](../customization/skills.md). If a skill name collides with a built-in command, use the full `/skill:` form. Press `Esc` to dismiss. + +Some commands are only available while the agent is idle; interrupt the current turn first if the agent is streaming. Mode-switch and query commands such as `/yolo`, `/plan`, and `/help` are always available. + +Type `@` to trigger file-path completion. Selecting an entry inserts the relative path, and the agent can read the file directly. Dot-prefixed directories are hidden by default; write `@.github/` to include them explicitly. + +## Plan mode + +In Plan mode, the agent spells out its plan before taking action. Press `Shift-Tab` or `/plan` to toggle; `/plan clear` clears the current plan file (available while idle only). + +The agent outputs a plan and waits for your confirmation instead of making changes directly. Once the plan is ready, you can reject it (staying in Plan mode) or request changes. Leaving Plan mode still requires your confirmation, even when YOLO mode is on. + +## YOLO mode + +YOLO mode automatically approves most tool calls, skipping the approval step. Enter `/yolo` (or `/yes`) to toggle; available both idle and streaming. + +::: warning Note +YOLO mode skips ordinary approvals, but not the approval required to leave Plan mode. +::: + +## Approval flow + +When the agent invokes a tool with side effects (such as modifying a file or running a command), an approval panel pops up for your confirmation. This is skipped in YOLO mode and for plan-file writes in Plan mode. + +Use arrow keys to select an option and `Enter` to confirm; press `1` / `2` / `3` to select by position directly. `Esc`, `Ctrl-C`, and `Ctrl-D` all reject the request. The panel usually also offers an "Approve for this session" option to auto-allow similar calls going forward. + +## Working while output is streaming + +While the agent is thinking or making tool calls ("streaming output"), the input box is still usable: + +- `Esc` — interrupt the current turn. +- `Ctrl-C` — also interrupts; press twice in the idle state to exit. +- `Ctrl-S` — insert the current input-box content as an additional message into the in-progress turn. +- `Ctrl-O` — globally toggle the collapsed state of all tool output. + +## External editor + +Press `Ctrl-G` to open the current input in an external editor; saved content is loaded back when the editor closes. + +Editor priority: `/editor` config > `$VISUAL` > `$EDITOR`. Run `/editor` first if none is configured. + +## Pasting images and videos + +Paste images or videos from the clipboard directly into the input box for multimodal models: + +- Unix (macOS / Linux): `Ctrl-V` +- Windows: `Alt-V` + +Placeholders appear in the input box and behave like ordinary text; they are replaced with the actual content when sent. Plain text falls back to a normal paste. Media attachments are kept in the current session only. + +## Viewing all shortcuts + +Enter `/help` to open a panel listing all shortcuts and slash commands. Scroll with `↑` / `↓`, page with `PageUp` / `PageDown`, and close with `Esc`, `Enter`, or `q`. diff --git a/docs/en/guides/migration.md b/docs/en/guides/migration.md new file mode 100644 index 000000000..d0938ffb4 --- /dev/null +++ b/docs/en/guides/migration.md @@ -0,0 +1,37 @@ +# Migrating from kimi-cli + +Kimi Code CLI is the next-generation terminal agent — and a fresh start. If you have been using the previous generation, kimi-cli, you don't need to start over: a single command brings your configuration, MCP servers, and session history across to the new version. + +## Why migrate + +kimi-code is rebuilt on Node.js and no longer depends on Python or the `uv` toolchain, making installation and upgrades simpler. It also ships native binaries out of the box. + +The terminal UI has been redesigned for a faster and lighter experience. + +kimi-cli is transitioning to kimi-code, and migrating lets you keep your existing configuration and session history going forward. + +## How to migrate + +There are two ways to migrate. + +The **first time you run `kimi`** after installing kimi-code, it automatically checks whether kimi-cli data exists under `~/.kimi/`. If it finds any, a migration prompt appears, and you can choose to migrate now, do it later, or never be asked again. + +You can also **run it manually at any time**: + +```sh +kimi migrate +``` + +You can choose whether to migrate chat sessions as well. If you don't need the history yet, pick **Config only**; otherwise pick **Config + N sessions** to bring everything across in one go. A summary is printed at the end. + +## What happens during migration + +**What gets migrated**: configuration (`config.toml`), MCP server configuration, input history, and whichever chat sessions you chose to migrate. + +**What does not get migrated**: OAuth login credentials and MCP service authorizations are not copied, so you will need to run `/login` again and re-authorize MCP servers after migrating. kimi-cli plugins are also out of scope. + +::: tip +Migration **never modifies or deletes** any of the old data under `~/.kimi/`. kimi-cli keeps working as before, and the two do not interfere with each other. Migration can also be run repeatedly — sessions that have already been migrated are not imported again. +::: + +After migration, sessions imported from kimi-cli are tagged with `[imported]` in the session picker so you can tell them apart from new ones. diff --git a/docs/en/guides/sessions.md b/docs/en/guides/sessions.md new file mode 100644 index 000000000..5619e8a2f --- /dev/null +++ b/docs/en/guides/sessions.md @@ -0,0 +1,110 @@ +# Sessions and context + +Kimi Code CLI persists every conversation as a "session", preserving message history and metadata so you can close the terminal and resume later. This page covers resuming sessions, context compaction, and managing sessions from inside the TUI. + +## Session storage + +All sessions are stored under `$KIMI_CODE_HOME/sessions/` (default `~/.kimi-code/sessions/`), bucketed by working directory: + +```text +~/.kimi-code/ +├── config.toml +├── session_index.jsonl +└── sessions/ + └── / + └── / + ├── state.json + └── agents/ + ├── main/ + │ └── wire.jsonl + └── / + └── wire.jsonl +``` + +- `state.json` — session title and metadata. +- `agents/*/wire.jsonl` — agent event stream. + +::: warning Note +Manually editing files under `sessions/` can leave a session unrecoverable due to ordering constraints in `state.json` and `wire.jsonl`. +::: + +## Starting and resuming sessions + +By default, `kimi` creates a new session each time. To continue where you left off: + +**Resume the most recent session in the current directory:** + +```sh +kimi --continue +``` + +**Resume a specific session:** + +```sh +kimi --session abc123 +``` + +`-r` / `--resume` are equivalent aliases. + +**Pick interactively:** + +```sh +kimi --session +``` + +::: warning Note +`--continue` and `--session` are mutually exclusive; `--yolo` and `--plan` also cannot be combined with them. +::: + +## Switching sessions inside the TUI + +- `/new` (`/clear`): switch to a new session. +- `/sessions` (`/resume`): browse and resume past sessions. +- `/fork`: fork the current session (see below). +- `/title ` (`/rename`): set a session title for easier recognition. Without an argument, shows the current title. + +`/sessions` works even while streaming, but switching requires interrupting the turn first with `Esc` or `Ctrl-C`. `/new`, `/fork`, and `/compact` are only available while idle. + +## Context compaction + +Kimi Code CLI automatically compresses message history when context approaches the window limit. You can also trigger it manually: + +```text +/compact +``` + +Pass a custom hint to tell the model what to preserve: + +```text +/compact Keep the discussion related to database migrations +``` + +## Forking sessions + +To try a new line of thinking without disrupting the current conversation, use `/fork`: + +```text +/fork +``` + +The forked session is fully independent; you can switch back to the original at any time. + +## Exporting sessions + +Package a session into a ZIP: + +```sh +kimi export +``` + +Without a `sessionId`, it exports the most recent session (interactively asks for confirmation; pass `-y` to skip). Use `-o` to set the output path: + +```sh +kimi export -o ~/Desktop/my-session.zip +``` + +The ZIP is written to the current working directory by default. The session's own diagnostic log is always bundled along with the session directory. The global diagnostic log at `$KIMI_CODE_HOME/logs/kimi-code.log` — which captures events that do not belong to a session, such as TUI startup and login — is also bundled by default; pass `--no-include-global-log` to skip it. + +::: tip Tip +Exported files may contain sensitive information. Review the contents before sharing. +::: diff --git a/docs/en/guides/use-cases.md b/docs/en/guides/use-cases.md new file mode 100644 index 000000000..d90cce51a --- /dev/null +++ b/docs/en/guides/use-cases.md @@ -0,0 +1,118 @@ +# Common use cases + +Below are typical scenarios and prompt samples for Kimi Code CLI. + +## Understanding an unfamiliar project + +Use `kimi --plan` or press `Shift-Tab` to enter Plan mode for large-scale research. Ask it to produce a plan first and approve before execution: + +``` +Walk me through the overall architecture of this repository. Focus on: +1. Where the entry point is and what happens at startup +2. The dependency relationships between major modules +3. How configuration and data are loaded +At the end, draw a simple module relationship diagram. +``` + +For focused questions, ask directly: + +``` +How does the event loop under src/runtime work? Where do events originate, and who consumes them? +``` + +``` +How is "permission approval" implemented in this project? Which files are involved, and what are the key types? +``` + +For large investigations, the main agent can spawn **subagents** to handle subtasks in parallel. See [Agents](../customization/agents.md). + +## Implementing a new feature + +State the requirement and acceptance criteria clearly. Use Plan mode for complex or risky changes. + +``` +Add a retry utility under src/utils: +- Function signature retry(fn: () => Promise, options): Promise +- Supports three options: maxAttempts, initialDelayMs, backoffFactor +- Throws the last error on failure +- Add a set of unit tests covering success, success after retry, and full failure +``` + +Tell it how to change the result — no need to edit by hand: + +``` +The backoff uses a fixed value. I want some jitter added to avoid thundering herd. Update the code and the tests. +``` + +## Fixing bugs + +State the symptom, reproduction conditions, and expected behavior: + +``` +Running npm test occasionally throws this error: + + TypeError: Cannot read properties of undefined (reading 'id') + at SessionStore.update (src/session/store.ts:142:18) + +It only appears in cases that trigger multiple updates concurrently. Locate the cause, fix it, and run the full test suite once at the end to confirm. +``` + +Not sure where the cause is? Have it investigate first: + +``` +User report: after a successful login, the first page refresh sends them back to the login page; refreshing again works fine. Investigate the possible causes first, list the most suspicious spots, and wait for me to confirm a direction before making any changes. +``` + +For mechanical tasks, just hand it off: + +``` +Run the tests, fix any failing cases, and run them again to confirm everything is green. +``` + +## Writing tests and refactoring + +Tasks with clear boundaries and acceptance criteria are a great fit: + +``` +src/parser/markdown.ts currently has almost no tests. Add a set of unit tests covering plain paragraphs, nested lists, code blocks, tables, blockquotes, and mixed scenarios. Follow the existing test style in this project. +``` + +``` +Extract the repeated "read body → validate → write log → return" logic under src/handlers into a middleware. Run the tests after the changes and make sure existing behavior is unchanged. +``` + +For multi-file refactors, use Plan mode to confirm the plan first. Use `/fork` to explore alternative approaches. + +## One-off scripts and automation tasks + +Bulk edits, statistics, and research can be done with a single prompt: + +``` +Change all var declarations in .js files under src/ to const or let, preferring const when possible. Run lint after the changes to confirm. +``` + +``` +Analyze the access logs under logs/ for the past 7 days. Group by endpoint path and report the number of calls plus p50 and p99 response times. Output the result as a markdown table. +``` + +``` +Research the mainstream dependency injection options in TypeScript (tsyringe, inversify, awilix). Compare them along three dimensions: API style, decorator dependencies, and runtime overhead. Give me a recommendation within one page. +``` + +Use `--yolo` or `/yolo` to skip approvals, or set an allow list under [permission configuration](../configuration/config-files.md#permission) for a safer middle ground. + +## Generating and maintaining documentation + +``` +I just changed the interface signature of src/auth/login.ts. Update the corresponding JSDoc, the example code in the README, and any paragraph that mentions this interface under docs/en/guides. +``` + +``` +For every public function under src/api that does not have a docstring, add one. Match the style of the existing comments. +``` + +``` +Based on the command implementations under src/cli, generate a draft command reference listing each subcommand, its arguments, and default values. Put it under docs/en/reference for me to review later. +``` + +Archive or share a session with `kimi export `. diff --git a/docs/en/index.md b/docs/en/index.md new file mode 100644 index 000000000..e82ce8cc9 --- /dev/null +++ b/docs/en/index.md @@ -0,0 +1,13 @@ +--- +layout: home +hero: + name: Kimi Code CLI + text: The Starting Point for Next-Gen Agents + actions: + - theme: brand + text: Get started + link: guides/getting-started + - theme: alt + text: GitHub + link: https://github.com/MoonshotAI/kimi-code +--- diff --git a/docs/en/reference/keyboard.md b/docs/en/reference/keyboard.md new file mode 100644 index 000000000..39939cd04 --- /dev/null +++ b/docs/en/reference/keyboard.md @@ -0,0 +1,97 @@ +# Keyboard shortcuts + +The TUI interactive mode of Kimi Code CLI supports the keyboard shortcuts listed below. These keys primarily take effect inside the input box; some have different behavior inside popups (such as the `/help` panel or the approval panel) or during streaming output. + +Type `/help` at any time inside the TUI to open the built-in shortcut list. + +## General shortcuts + +The following keys are always available in the input box: + +| Shortcut | Action | +| --- | --- | +| `Enter` | Submit the current input | +| `Shift-Enter` | Insert a newline in the input | +| `↑` / `↓` | Browse input history | +| `Esc` | Close popup / cancel completion / interrupt streaming output or an in-progress context compaction | +| `Ctrl-C` | Interrupt the current streaming output, or clear the input box | +| `Ctrl-D` | Exit Kimi Code CLI when the input box is empty | + +Pressing `Ctrl-C` **during streaming output** cancels immediately with no second confirmation needed. + +**Exiting the program** (pressing `Ctrl-C` when the input box is empty, or pressing `Ctrl-D`) uses a "double-press to confirm" mechanism: the first press shows a hint in the status bar (for example, `Press Ctrl+C again to exit`), and only a second press of the same key actually exits. Pressing any other key in between clears the confirmation state. + +## Mode switching + +| Shortcut | Action | +| --- | --- | +| `Shift-Tab` | Toggle Plan mode | + +Press `Shift-Tab` to turn Plan mode on or off. While Plan mode is on, the agent favors read-only tools for research and planning and can write the current plan file; when needed, it can also call `Bash`, which follows the current permission mode and ordinary rules without triggering an extra independent approval just because Plan mode is active. Toggling the mode alone does not create an empty plan file. Press `Shift-Tab` again to leave Plan mode. + +## Input and editing + +| Shortcut | Action | +| --- | --- | +| `Ctrl-G` | Edit the current input in an external editor | +| `Ctrl-V` | Paste an image or video from the clipboard (Unix / macOS) | +| `Alt-V` | Paste an image or video from the clipboard (Windows) | +| `Ctrl-E` | Expand or collapse the Plan card (when no Plan card is present, follows the system default behavior of moving the cursor to the end of the line) | +| `Ctrl--` | Undo | + +Pressing `Ctrl-G` opens an external editor to edit the current input. The editor is chosen in the following order of priority: + +1. The editor configured by the `/editor` command +2. The `$VISUAL` environment variable +3. The `$EDITOR` environment variable + +After saving and exiting the editor, the edited content replaces the input box contents; exiting without saving leaves the input unchanged. + +When pasting an image or video, it appears in the input box as a placeholder, and the actual media data is sent to the model together with the message on submit. The preferred source is an image or file path on the system clipboard; on Linux, both Wayland and X11 are tried, and on WSL the Windows clipboard is also read via PowerShell as a fallback. + +## During streaming output + +While output is streaming, the input box still accepts input and supports the following extra actions: + +| Shortcut | Action | +| --- | --- | +| `Ctrl-S` | Steer: inject the current input into the running turn immediately | +| `Esc` | Interrupt the current streaming output | +| `Ctrl-C` | Interrupt the current streaming output | + +When you press `Ctrl-S`, the model sees your message at the next interruptible point, without waiting for the current turn to finish. + +## Tool output + +| Shortcut | Action | +| --- | --- | +| `Ctrl-O` | Expand or collapse tool output | + +When collapsed tool call results are present in the history, press `Ctrl-O` to toggle between collapsed and expanded views to inspect the full tool output. + +## Approval panel + +When the agent issues a tool call that requires confirmation, the TUI shows an approval panel. For the full approval flow, see [Interaction and Input](../guides/interaction.md#approval-flow); the table below lists the keys available inside the panel: + +| Shortcut | Action | +| --- | --- | +| `↑` / `↓` | Move the cursor between options | +| `Enter` | Confirm the currently selected option | +| `1` ~ `9` | Directly select the option with the matching number | +| `Esc` / `Ctrl-C` / `Ctrl-D` | Reject the current request | +| `Ctrl-E` | Expand or collapse the full content when the panel includes a diff or file preview | +| `Ctrl-O` | Toggle the collapsed state of other tool output | + +Options that require additional feedback (such as "Reject" or "Revise") switch to a feedback input state after confirmation: type the feedback text directly and press `Enter` to submit, or press `Esc` to exit the feedback input and return to the option list. + +## Popup mode + +After typing `/help` to open the help panel, the following keys are available to navigate and close the panel: + +| Shortcut | Action | +| --- | --- | +| `↑` / `↓` | Scroll one line | +| `PageUp` / `PageDown` | Scroll 10 lines | +| `Esc` | Close the panel | +| `Enter` | Close the panel | +| `q` / `Q` | Close the panel | diff --git a/docs/en/reference/kimi-command.md b/docs/en/reference/kimi-command.md new file mode 100644 index 000000000..088e6c34e --- /dev/null +++ b/docs/en/reference/kimi-command.md @@ -0,0 +1,154 @@ +# kimi Command + +`kimi` is the main command of Kimi Code CLI, used to start an interactive session in the terminal. When run without any arguments, it opens a new session in the current working directory; with different flags, you can resume a previous session, skip approvals, start in Plan mode, or point at custom Skills directories. + +```sh +kimi [options] +kimi [options] +``` + +## Main command options + +The table below lists all options supported by the `kimi` main command. All flags are optional — running `kimi` on its own is enough to enter an interactive session. + +| Option | Short | Description | +| --- | --- | --- | +| `--version` | `-V` | Print the version number and exit. | +| `--help` | `-h` | Show help information and exit. | +| `--session [id]` | `-S` | Resume a session. With an ID, open the specified session directly; without an ID, enter the interactive picker to choose from historical sessions. | +| `--continue` | `-C` | Continue the most recent session in the current working directory, without manually specifying an ID. | +| `--model ` | `-m` | Use a model alias for this invocation. When omitted, new sessions use `default_model` from the config file, and resumed sessions use the session's current model. | +| `--prompt ` | `-p` | Run one prompt non-interactively and stream assistant output to stdout. This mode uses `auto` permission for tool calls and does not open the TUI. | +| `--output-format ` | | Set the non-interactive output format. Supported values are `text` and `stream-json`. Only valid with `--prompt`; defaults to `text`. | +| `--yolo` | `-y` | Auto-approve ordinary tool calls, skipping approval requests; Plan mode `Bash` approval and Plan mode exit approval are not skipped. | +| `--plan` | | Start a new session in Plan mode, where the AI favors read-only tools for exploration and planning and can write the current plan file; Plan mode `Bash` is handled separately according to the permission mode. | +| `--skills-dir ` | | Load Skills from the specified directory, replacing the auto-discovered user and project directories. Can be passed multiple times to stack several directories. See [Custom Skills directories](#custom-skills-directories) below. | + +`-r` / `--resume` is a hidden alias for `--session`; `--yes` and `--auto-approve` are hidden aliases for `--yolo`. They do not appear in the help output and behave identically to their official counterparts. + +::: warning Note +`--yolo` skips human confirmation for ordinary tool calls, including file writes and shell command execution. Use it only inside trusted working directories. Plan mode exit approval is not skipped by `--yolo`; in Plan mode, `Bash` also follows the same ordinary allow rules as `--yolo`. +::: + +### Flag conflict rules + +The following combinations are rejected at startup: + +- `--continue` and `--session` are mutually exclusive: both mean "resume a previous session" and overlap in meaning. +- `--yolo` cannot be combined with `--continue` or `--session`: when resuming a session, the original session's approval settings are preserved. This rule only applies to interactive mode; in `--prompt` mode, `--yolo` is rejected earlier because it is mutually exclusive with `--prompt`. +- `--plan` cannot be combined with `--continue` or `--session`: Plan mode only applies to new sessions. +- `--prompt` cannot be combined with `--yolo` or `--plan`: non-interactive mode always uses `auto` permission and does not enter Plan mode. +- `--prompt` can be combined with `--continue` or `--session ` with an ID; bare `--session` without an ID would open the interactive picker and therefore cannot be used in non-interactive mode. +- `--output-format` can only be used with `--prompt`; the interactive TUI does not support writing the full event stream as stdout JSONL. + +If you need to force YOLO or Plan mode while resuming a session, switch into them from inside the interactive session via slash commands instead. + +## Typical usage + +The most common entry point is to run `kimi` directly to start a fresh session in the current directory: + +```sh +kimi +``` + +If the previous session was interrupted (terminal closed, network disconnected, etc.) and you want to pick up where you left off, use `--continue`: + +```sh +kimi --continue +``` + +This automatically finds and resumes the most recent session under the current working directory. To pick a different historical session, run `kimi --session` to enter the interactive picker, or pass a known session ID directly: + +```sh +kimi --session 01HZ...XYZ +``` + +When the task is trivial and you don't want to be interrupted by frequent approval requests, add `--yolo`: + +```sh +kimi --yolo +``` + +If you want the AI to read the code and produce an implementation plan first, rather than immediately editing files, use `--plan` to enter Plan mode: + +```sh +kimi --plan +``` + +### Custom Skills directories + +To load custom Skills directories, you have two options: + +- **CLI flag `--skills-dir `**: Can be passed multiple times and **replaces** the auto-discovered user and project directories. Useful for temporary overrides or use in scripts. For example, to mount two directories at once: + + ```sh + kimi --skills-dir /path/to/team-skills --skills-dir ./local-skills + ``` + +- **`extra_skill_dirs` in `config.toml`**: Appends extra directories in the config file and **stacks** them with the auto-discovered ones. Suitable for persistent configuration of team-shared Skills (see [Agent Skills](../customization/skills.md)). + +## Non-interactive execution + +Use `-p` when a script or CI job needs to run one prompt: + +```sh +kimi -p "Summarize the current repository status" +``` + +Output uses transcript-style blocks: thinking and assistant text start with `• `, with continuation lines indented by two spaces. Assistant text is written to stdout; thinking, tool progress, and the `To resume this session: kimi -r ` hint are written to stderr. Prompt mode does not wait for manual approvals: ordinary tool calls, Plan approvals, and agent questions follow the `auto` permission policy. Static deny rules still block matching tool calls. + +To switch models for a single invocation, add `-m`: + +```sh +kimi -m kimi-code/kimi-for-coding -p "Explain the latest diff" +``` + +If a script needs structured output, use JSONL: + +```sh +kimi -p "List changed files" --output-format stream-json +``` + +In `stream-json` mode, each stdout line is one JSON object. Ordinary replies are emitted as assistant messages. If the model calls tools, the output first includes an assistant message with `tool_calls`, then the corresponding tool message, followed by later assistant messages. Thinking content is not written to JSONL; tool progress and the resume-session hint still go to stderr. + +## Subcommands + +### `kimi export` + +Bundle a session into a ZIP file for sharing, archival, or bug reports. The exported archive contains all files under the session directory, such as context records, state files, and the session diagnostic log if that session has already produced `logs/kimi-code.log`. + +```sh +kimi export [sessionId] [options] +``` + +| Argument / Option | Short | Description | +| --- | --- | --- | +| `sessionId` | | The ID of the session to export. When omitted, the most recent session under the current working directory is selected automatically and a confirmation is requested. | +| `--output ` | `-o` | Output path for the ZIP file. When omitted, writes to a default filename in the current directory. | +| `--yes` | `-y` | Skip the confirmation prompt for the default session and export directly. | +| `--no-include-global-log` | | Skip bundling the active global diagnostic log, `~/.kimi-code/logs/kimi-code.log`. It is included by default. | + +By default, export includes files inside the target session directory. If that directory contains `logs/kimi-code.log`, it appears in the ZIP as `logs/kimi-code.log`. The global diagnostic log at `~/.kimi-code/logs/kimi-code.log` is also bundled by default, because it may contain events from other sessions or projects. Add `--no-include-global-log` when you do not want to share it. When included, its ZIP path is `logs/global/kimi-code.log`; rotated files such as `kimi-code.log.1` are not bundled. + +When `sessionId` is omitted, the command first prints the session to be exported and asks for confirmation; `-y` skips this prompt, which is handy for scripts: + +```sh +# Export the most recent session under the current working directory, skipping confirmation +kimi export -y + +# Export a specific session to a custom path +kimi export 01HZ...XYZ -o ./bug-report.zip + +# Exclude the global diagnostic log to avoid sharing events from other sessions +kimi export 01HZ...XYZ -o ./bug-report.zip --no-include-global-log +``` + +### `kimi migrate` + +Migrate local data from an older version of kimi-cli to kimi-code. This command has no flags and runs fully interactively, guiding you through the entire migration process. + +```sh +kimi migrate +``` + +If you previously used an older version of kimi-cli, run this command to migrate historical sessions, configuration, and other data to kimi-code to avoid data loss. For the full migration flow, what gets migrated, and things to watch out for, see [Migrating from kimi-cli](../guides/migration.md). diff --git a/docs/en/reference/slash-commands.md b/docs/en/reference/slash-commands.md new file mode 100644 index 000000000..b8aed9047 --- /dev/null +++ b/docs/en/reference/slash-commands.md @@ -0,0 +1,86 @@ +# Slash Commands + +Slash commands are built-in control commands provided by Kimi Code CLI in the interactive TUI, used to switch modes, manage sessions, view status, and more. Type `/` in the input box to trigger command completion; the candidate list filters in real time as you continue typing, and command aliases participate in matching as well. + +After typing a full command name (such as `/help`), press `Enter` to execute it. If the `/`-prefixed input does not match any built-in or skill command, it is sent to the agent as an ordinary message. + +::: tip Tip +Some commands are only available in the idle state. Running them while the session is streaming a response or compacting the context will be blocked, with a hint to press `Esc` or `Ctrl-C` first to interrupt the current operation. The "Always available" column in the tables below marks commands that remain available during streaming or compacting. +::: + +## Account and configuration + +| Command | Alias | Description | Always available | +| --- | --- | --- | --- | +| `/login` | — | Pick an account or platform and sign in: Kimi Code uses the OAuth device code flow, while the Moonshot AI Open Platform signs in with an API key. | No | +| `/logout` | — | Clear the credentials of the currently selected account (Kimi Code OAuth credentials, or the corresponding open platform provider config). | No | +| `/model` | — | Switch the LLM model used by the current session. | Yes | +| `/settings` | `/config` | Open the settings panel inside the TUI. | Yes | +| `/permission` | — | Choose a permission mode. | Yes | +| `/editor` | — | Configure the external editor launched by `Ctrl-G`. | Yes | +| `/theme` | — | Switch the terminal UI color theme. | Yes | + +## Session management + +| Command | Alias | Description | Always available | +| --- | --- | --- | --- | +| `/new` | `/clear` | Start a brand-new session, discarding the current context. | No | +| `/sessions` | `/resume` | Browse historical sessions and switch to or resume one. | Yes | +| `/tasks` | `/task` | Browse the background task list. | Yes | +| `/fork` | — | Fork a new session from the current one, preserving the full conversation history. | No | +| `/title []` | `/rename` | Without arguments, show the current session title; with an argument, set it as the new title (up to 200 characters). | Yes | +| `/compact []` | — | Compact the current conversation context to free up token usage; optionally pass a custom instruction telling the model what to preserve during compaction. | No | +| `/init` | — | Analyze the current codebase and generate `AGENTS.md`. | No | + +## Mode and runtime control + +| Command | Alias | Description | Always available | +| --- | --- | --- | --- | +| `/yolo [on\|off]` | `/yes` | Toggle auto-approve mode. Without arguments, flip the current state; pass `on`/`off` explicitly to force the corresponding state. When enabled, ordinary tool call approvals are skipped; the Plan mode exit approval is not skipped. | Yes | +| `/plan [on\|off]` | — | Toggle Plan mode. Without arguments, flip the current state; pass `on`/`off` explicitly to force the corresponding state. Toggling alone does not create an empty plan file. | Yes | +| `/plan clear` | — | Clear the current plan. | No | + +::: warning Note +`/yolo` skips approval confirmation for ordinary tool calls. Make sure you understand the potential risks before enabling it. It does not skip the approval required to leave Plan mode; in Plan mode, `Bash` follows the same ordinary allow rules as `/yolo`. +::: + +## Information and status + +| Command | Alias | Description | Always available | +| --- | --- | --- | --- | +| `/help` | `/h`, `/?` | Show keyboard shortcuts and all available commands. | Yes | +| `/usage` | — | Show token usage, context consumption, and quota information. | Yes | +| `/status` | — | Show the current session runtime status, including version, model, working directory, and permission mode. | Yes | +| `/mcp` | — | List the MCP servers in the current session and their connection status. | Yes | +| `/version` | — | Show the Kimi Code CLI version number. | Yes | +| `/feedback` | — | Submit feedback to help improve Kimi Code CLI. | Yes | + +## Exit + +| Command | Alias | Description | Always available | +| --- | --- | --- | --- | +| `/exit` | `/quit`, `/q` | Exit Kimi Code CLI. | No | + +## Dynamic skill commands + +In addition to the built-in commands, user-activatable skills are automatically registered as slash commands under the `skill:` namespace: + +``` +/skill: [extra text] +``` + +For example, `/skill:code-style` loads the content of the `code-style` skill and sends it to the agent; any text after the command is appended to the skill prompt, as in `/skill:git-commits fix the login failure issue`. + +For convenience, skill commands also support a short form `/` that omits the `skill:` prefix, provided the name is not already taken by a built-in command. In other words, `/code-style` falls back to matching `/skill:code-style`. + +Kimi Code CLI ships with a built-in `mcp-config` skill for configuring MCP servers and handling MCP OAuth login. It still belongs to the skill namespace in completion and help (`/skill:mcp-config`), and it can also be invoked directly as `/mcp-config`. + +Skill types that can be exposed as slash commands include `prompt`, `inline`, `flow`, and skills without an explicitly declared type. For skill installation and authoring, see [Agent Skills](../customization/skills.md). + +::: info Note +All skill commands are only available while the agent is idle; during streaming or compacting, press `Esc` or `Ctrl-C` first to interrupt the current operation. +::: + +::: info Note +Flow-type skills are also exposed via `/skill:`; there is no separate `/flow:` namespace. +::: diff --git a/docs/en/reference/tools.md b/docs/en/reference/tools.md new file mode 100644 index 000000000..f113f2072 --- /dev/null +++ b/docs/en/reference/tools.md @@ -0,0 +1,104 @@ +# Built-in tools + +Built-in tools are the toolset that Kimi Code CLI ships with its core engine — no MCP server installation required. During each conversation, the agent automatically selects and invokes these tools based on the task at hand; users can also inspect every tool call in detail through the approval request interface. + +Compared to MCP tools, built-in tools are managed directly by the runtime, their lifecycle is bound to the session, and no external process is needed. Both follow a unified approval mechanism: **read-only tools** (such as `Read`, `Grep`, `Glob`, and `WebSearch`) are auto-approved by default, while **write and execute tools** (such as `Write`, `Edit`, `Bash`, and `TaskStop`) require user approval by default. In YOLO mode, approval for ordinary tool calls is skipped, but exit approval in Plan mode is not affected. + +## File tools + +File tools handle reading, writing, and searching the local filesystem, and are the foundational tools for code analysis and modification tasks. + +| Tool | Default approval | Description | +| --- | --- | --- | +| `Read` | Auto-approved | Read the contents of a text file | +| `Write` | Requires approval | Create or overwrite a file | +| `Edit` | Requires approval | Exact string replacement | +| `Grep` | Auto-approved | Full-text search powered by ripgrep | +| `Glob` | Auto-approved | Find files by glob pattern | +| `ReadMediaFile` | Auto-approved | Read an image or video file | + +**`Read`** accepts a file path (`path`) along with the optional `line_offset` (starting line number; negative values count from the end) and `n_lines` (maximum number of lines to read). At most 1000 lines or 100 KB are returned per call, with a truncation notice appended for anything beyond that limit. If the file is an image or video, the tool suggests using `ReadMediaFile` instead. + +**`Write`** accepts `path`, `content`, and an optional `mode` (`overwrite` or `append`; defaults to overwrite). The parent directory must already exist; `append` mode appends content to the end of the file without automatically inserting a newline. + +**`Edit`** accepts `path`, `old_string` (the exact text to replace), and `new_string` (the replacement text). By default it replaces only a single unique match; if the same content appears more than once in the file, the tool returns an error and suggests using `replace_all: true`. `old_string` and `new_string` must differ. + +**`Grep`** invokes ripgrep to search file contents. It supports regular expressions (`pattern`), a search path (`path`), file-type filtering (`type`, e.g. `ts`, `py`), glob filtering (`glob`), and an output mode (`output_mode`: `files_with_matches` / `content` / `count_matches`, defaulting to `files_with_matches`). `content` mode supports `-A`, `-B`, and `-C` context-line arguments along with `-i` (case-insensitive), `-n` (line numbers, default true), and `multiline` (cross-line matching). All modes support `offset` + `head_limit` pagination; `head_limit` defaults to 250, and passing 0 removes the limit. In `files_with_matches` mode, results are sorted by the most recent file modification time in descending order; other modes preserve ripgrep's original output order. Sensitive files such as `.env`, private keys, `.aws/credentials`, and `.gcp/credentials` are automatically filtered out; `include_ignored=true` also searches files ignored by `.gitignore` and the like, but sensitive files remain filtered. + +**`Glob`** matches files against a glob pattern (`pattern`) within a specified directory (`path`, defaulting to the working directory). Results are sorted by modification time in descending order, with a maximum of 1000 entries. The optional `include_dirs` (default true) controls whether directory entries are returned. Pure wildcard patterns (such as `**` or `**/*`) are rejected with a prompt to add a literal anchor; patterns containing brace expansion (`{a,b,c}`) are likewise rejected — the underlying glob engine treats `{` and `}` as literals, so such patterns silently match zero files. + +**`ReadMediaFile`** sends an image or video file to the model as multimodal content, accepting only `path`. The file size limit is 100 MB. Tool availability depends on the current model's vision capabilities (`image_in` / `video_in`). + +## Shell + +| Tool | Default approval | Description | +| --- | --- | --- | +| `Bash` | Requires approval | Execute a shell command | + +**`Bash`** is the most versatile and the most permission-sensitive tool. It accepts `command` (required) along with the optional `cwd` (working directory), `timeout` (milliseconds), `description` (background task description, required when `run_in_background=true`), `run_in_background` (whether to run as a background task), and `disable_timeout` (whether to disable the timeout for a background task). The foreground `timeout` defaults to 60 seconds and is capped at 5 minutes; the background `timeout` defaults to 10 minutes and is also capped at 10 minutes. + +In foreground mode `Bash` blocks the current turn until the command finishes or times out; in background mode it returns a task ID immediately. Background tasks time out after 10 minutes by default; if a task really needs to run without a timeout, set `disable_timeout=true`. When the task completes, fails, or is stopped, the agent is automatically notified to continue processing; during execution, the result can also be inspected explicitly via `TaskOutput`. stdin is always closed, so interactive commands receive EOF immediately. A two-phase termination strategy (SIGTERM → 5-second grace period → SIGKILL) ensures processes terminate reliably after a timeout. On Windows, Git Bash is used as the shell by default. + +## Network tools + +| Tool | Default approval | Description | +| --- | --- | --- | +| `WebSearch` | Auto-approved | Search the web | +| `FetchURL` | Auto-approved | Fetch the content of a given URL | + +**`WebSearch`** accepts `query` (search terms) and the optional `limit` (number of results to return, 1–20, default 5) and `include_content` (whether to return the page body; default false — enabling this consumes significantly more tokens). This tool requires the host to provide a search implementation; if no implementation is injected, it does not appear in the tool list. + +**`FetchURL`** accepts a single `url` parameter and returns the page content. For HTML pages, the host extracts the main article body (`extracted`) rather than returning the full HTML; plain-text or Markdown pages are passed through directly (`passthrough`). Likewise requires a host-injected implementation. + +## Plan mode + +| Tool | Default approval | Description | +| --- | --- | --- | +| `EnterPlanMode` | Auto-approved | Enter Plan mode | +| `ExitPlanMode` | Auto-approved (requires user plan confirmation) | Exit Plan mode and submit the plan | + +Plan mode is a constrained working state: once entered, `Write` and `Edit` are tightened — they may only write to the current plan file, and other paths are blocked; `TaskStop` is also blocked entirely. The remaining tools (including `Bash`) are still governed by the current permission rules, so a `Bash` command can in principle still modify files — whether it is allowed depends on the active approval policy. + +**`EnterPlanMode`** takes no parameters. On success it returns workflow instructions, including the plan file path if one was provided by the host. + +**`ExitPlanMode`** reads the current plan file contents, presents the plan to the user for approval, and then exits Plan mode. The optional `options` parameter lets the agent provide 1–3 alternative proposals (each with a `label` and `description`; the `label` is capped at 80 characters) for the user to choose from during approval. Labels must be unique and cannot use the reserved words `Approve`, `Reject`, `Reject and Exit`, or `Revise` (the system uses these to mark approval results). Once the user approves, all tools become available again; if the user requests changes, the agent remains in Plan mode. + +## State management + +| Tool | Default approval | Description | +| --- | --- | --- | +| `TodoList` | Auto-approved | Manage the task to-do list | + +**`TodoList`** maintains a visible subtask list across multi-step operations; state is stored within the agent session. The `todos` parameter accepts an array where each item has a `title` and a `status` (`pending` / `in_progress` / `done`). Omitting `todos` queries the current list; passing an empty array clears it. + +## Collaboration tools + +Collaboration tools handle inter-agent coordination, user interaction, and skill invocation. + +| Tool | Default approval | Description | +| --- | --- | --- | +| `Agent` | Auto-approved | Spawn a subagent to execute a subtask | +| `AskUserQuestion` | Auto-approved | Ask the user a question to obtain structured input | +| `Skill` | Auto-approved | Invoke a registered inline skill | + +**`Agent`** delegates a subtask to a subagent. Required parameters are `prompt` (the full task description) and `description` (a short 3–5 word summary for UI display). Optional parameters include `subagent_type` (agent type, default `coder`), `resume` (the ID of an existing agent to resume), `run_in_background` (whether to run in the background, default false), and `timeout` (timeout in seconds, 30–3600). `subagent_type` and `resume` are mutually exclusive: when resuming an existing agent, addressing is done solely by ID. When the foreground `timeout` is omitted, the subagent runs to completion with no time limit; when the background `timeout` is omitted, it falls back to `[background] agent_task_timeout_s` in `config.toml`, and if that field is also unset, there is no time limit. In foreground mode the parent agent waits for the subagent to complete before continuing; in background mode it returns a task ID immediately, and upon completion a synthetic user message automatically routes control back to the main agent, with no polling required. For details on the subagent system, see [Subagents](../customization/agents.md). + +**`AskUserQuestion`** presents the user with a structured multiple-choice question, suitable for disambiguation or option-selection scenarios. The `questions` parameter accepts 1–4 questions; each question requires a `question` (question text ending with `?`), `options` (2–4 choices, each with a `label` and `description`), and the optional `header` (a short category label of up to 12 characters, such as `Auth` or `Style`) and `multi_select` (whether multiple choices are allowed, default false). An "Other" option is automatically appended by the system, so there is no need to provide it manually in `options`. If the host does not implement interactive questioning, this tool returns a failure notice and the agent should instead ask the user directly in its text reply. + +**`Skill`** allows the agent to explicitly invoke a registered inline-type skill. It accepts `skill` (the skill name) and an optional `args` (additional argument text). Only skills with `type = "inline"` can be invoked through this tool; other types (such as `prompt` or `flow`) and skills that set `disableModelInvocation: true` in their frontmatter are rejected. To prevent recursive infinite loops, skill nesting depth is limited to 3 levels. For details on the skill system, see [Skills](../customization/skills.md). + +## Background tasks + +Background task tools manage background tasks started via `Bash` or `Agent`. When a background task reaches a terminal state such as completed, failed, stopped, or lost, its status and tail output are automatically sent back to the agent; use `TaskOutput` only when you want to inspect progress before that automatic notification arrives. + +| Tool | Default approval | Description | +| --- | --- | --- | +| `TaskList` | Auto-approved | List background tasks | +| `TaskOutput` | Auto-approved | View the output of a background task | +| `TaskStop` | Requires approval | Stop a running background task | + +**`TaskList`** returns a list of background tasks; each record includes the task ID, status, command, description, and PID. Optional parameters: `active_only` (default true, lists only running tasks) and `limit` (maximum number of entries to return, default 20, range 1–100). Tasks that have reached a terminal state also include `exit_code`; tasks explicitly terminated by `TaskStop` additionally include `reason`. + +**`TaskOutput`** returns the status and output of a specified task by `task_id`. The inline preview includes at most the most recent 32 KB of content; the full log is saved on disk, and the tool also returns `output_path` with a prompt to paginate it via `Read` (around 300 lines per page is recommended). Optional `block` (default false) and `timeout` (seconds to wait, default 30, range 0–3600) parameters can be used to wait for the task to finish before returning. In the response, `retrieval_status` is one of `success` / `timeout` / `not_ready`; tasks aborted by an external deadline timeout additionally include `timed_out: true` and `terminal_reason: timed_out`, and tasks explicitly terminated by `TaskStop` additionally include `stop_reason` and `terminal_reason: stopped`. + +**`TaskStop`** accepts `task_id` and an optional `reason` (reason for stopping, default `Stopped by TaskStop`). It is safe to call on a task that is already in a terminal state — it returns the current status without error. diff --git a/docs/en/release-notes/changelog.md b/docs/en/release-notes/changelog.md new file mode 100644 index 000000000..f1b68dbe0 --- /dev/null +++ b/docs/en/release-notes/changelog.md @@ -0,0 +1,5 @@ +# Changelog + +::: info Note +Detailed release notes will be published alongside the first official release. +::: diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 000000000..f992683cd --- /dev/null +++ b/docs/index.md @@ -0,0 +1,29 @@ +--- +layout: home +hero: + name: Kimi Code CLI + text: ' ' + actions: + - theme: brand + text: 简体中文 + link: zh/ + - theme: alt + text: English + link: en/ +--- + + diff --git a/docs/media/intro.gif b/docs/media/intro.gif new file mode 100644 index 0000000000000000000000000000000000000000..1acb1d82b1b9070a1f580a2126645b8ea1645405 GIT binary patch literal 3517259 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«Rl*`ksT=`p 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%0A`D8+SjxkaNRV?Bd>51GC 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 z z{XLg4EO7v12Iddja+`S-RV;8QJLR)_oAoqaEc7@6n(}?Q&Ax3A0fUj74#2<5fm19I z!x^0p;k*0q;A#*ASPt&QXJBe{CdTJ3pA@-NMp{lSdT5(HQodN02`%9Nt+(o4gp~>g z8zMKKhksupqgbYvRHmkNG8hB2u}koiT`1GLFVp#-Idvn|vZ5ex6Xixzu1lRS_tk#L6(+Fq%e4))HDQYOX3{l*L%S-qvC;NM>?%SddJlCr zg=ZpF0V|U}*l8K>6*kiPqto#Z4JC!Sw(6g~)iTG%s_07lcj&bh(CR~z_iV4mO~~5j zi>E@?%DK~LPaPlov1J%5-Z^1x{U9c+e$1oFy*NGhNbRw0xlzfver)4H?y$DwzRK&* z815+@1+vFD1YAvijKdH1U7Wsn-PHjBmrda>Lg34P%;-&O_-u_2K?kPW{_c zKPk#5)~>LNk^VG5qf{HAJ-&5`j3#U&3BScyq8>+Am zBuHbG;;D;IdBeVCdmfQfaxKkK_)C{CGpaM`3g0+x$;jV2X2Nxo(yw536Vp0wb8wM1 zH-2zj)e4$$ma4+pBs}B;c}{vx*1K`$9}11Fz&GC4=a`QjiW0muc?mV-!7Cn0^7GY& z`!*B^$|p*Fwm*MXaujhRALr`mQSiWt{inCe#aXOfNv&A_vf%H*Sz%qN-T7Tl+!-iS z)mnebjl3U#>tA!u0%wXj4BLmzD*DUz+zOwgNh%OurWTz%fI5)^Mjeh#rUJRe*qEXd zyY`_b?c8AFq)7_R1fs5yBi7Rl3gS=_4oc^DthYAQCSvd7FD#66?g{-Zh-1GXNTy2{ zM5yg^sj!w2MnktPA8}SMF_>RhMbs?*@M=n>eLso(E#+5sagD>8#MxX;8H3icK-*lS zf~+O+*82qzGfnVX#E#MI0<9>IagEwFEH;CSdT|aRNfdG+)p5hniJjF7{_1Zj zi+D+sZr|jQ2*N|)45QP0prz15tvHSHiHS6R`MpKhdVqqlBv>2ZuD*U0r3Mu9%8JD~ z@*$NH7K7L~dPG{DgoOdM0U8q_i|GX^-eaC2FlruB`HfDknVaiN(QK+ye=vAA^KX#^ zfMc@cLWPpF)j)t^0W78}$}D}Gw~Vot

    GWq54Ps0S<^zqp(@V;1C#WuBp;pJ6hgj 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≺nCIt_k^d7%z`e>EO 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


    fKr%D#ZB@ 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μbLo~f|#KJh77B+wKY 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

    X1N` ziHVHP$k)Sy{Ob+j+=QUD1q2`^+6c_M%fjfSsgxeZJhP~BioryPe$Z?0g5a)*Udoy& z%qW^J00m<{EE8-@7cBZi7U^UI@=9Ly$Nbt$AUspL{9f3w2gq6ttzm!@7XqMDN{fy< zZEUv7pqMy;daSGpZf=)^bLy&z6K_JiOmoW$ zA-$IiPOmjnq~7qResf$Z-cvMxgs8^R{;~RAuV>rd#%$Ti59Dn02_^Z5h zYZOrBNj)bBW|+uF6M{WD0TIaozMSOH2~i(;fry3Dro76jpBh*v5*Hq89`#{J*TgpT zgC+_ZX`NvZIlB0X(!@A|)-%y6n1QQyt=Z0b!hjCBhsYwez<#@K$4a!tOC*D7m;@5) z=O`Fn60}CeK=XCv{`D@$z%Kda=3C;5fR*Sv7|?uW_RUF4KXDBRlI_iDkccN2+!WnI zs1K8xklVPBUT4^2F4SgePi-CT+isnCYaQ?-C4u6X3WYjdibNCejLOP{y!y#fv zI}EKyz56cIIxzc?pE3ih=7H(g3o6{A z4#c>@FHs#C~B78ee+!+Hbj^6VmttqF1v$5BZYeXoh*#v-%w5X$EH8$fJU`U`O39QYc(*8FFc8Ov;3NX3^3 zi-1=1`r_|%w?YfAZ2~B(^|-jQuA|!D+thbV0tgI4zK50x@5fagBV6d2tto|%uy&L$ z&;EY^fIxr0k!R2TJEiw0gWV{%f<4&BQrK6m)OYaKW?jjD|9jgwqNk8O*;t|oeu^W& zIg5V;4;~AlFj*#jaFNa6o1Yqjri&uI01x`QsV4c9Ct{}6s(_vVg3&OQgCO6odj@C; z4f?Hu#L(ai7^(yqy5t?RX$cO8N|Pq3t76Ix%#Gj5FbIS^k-adeC3%t7+MxulVS_;6 z__LL1IlGZOsxCQ}+bPEX?c$J%mdj|}GlbpRz1@h-(<#!1Kf7pif{T{Zgqw^5Q>d%YB8; zc&tjuS5&a}yM)oeR>i5bI^gG|bGmb|4u#m_L*TosTf7R&2N2H8qI84cKvL-|0r#os zJx;soYw8kUsY9?1_wD1u1LRx5-43nTD-sd84&VA~u2k?&u<4}Gu3^tE+XJlH0R4`( zd6PE+yj3}`EgpjY%>*P~3~dDq1srtD$6A-$s>!1}l;34Uf?w$wqtqir`$%Y=GlPa>Q;KDFa3^fZdQ_Rs#pbUZB%h3>$ zBM9Rl53*-^@b%7>YWYy0dc9Ei3x}-3CvxZH{}7R3$%|q+>$cUV67Gj>HI{+3vm2nt z6JOs9U#r|FqqI9g7a4+6(*$jri|ZhtK8ygaAPzz;?8JlJB2m`IPTnHZwkbIUq7Vmd zjir)Jnf-_9t7VR>9JuQdz@D7~sz$qf?^y(NT+p19^5|D%d4OW%z?-{&XRzsZrI>k7`gyU!4%AH4in zKkNz3t!_Q!$X+rhfxt-0neG^kqX`g&-u&=DqKGF*d7Pvm!O%k?M+|dt%Cq5NL@lC* zY!Kr~N5daZ2sR{H(&WjKL$C!?q{QMMcEMzdBgKG^I9Pvf6ubdV8Ki|JPYJWQp;^rd zrJ7C9;%#G`YadZI?A1yFLoLk+UUS4i0x51owdwMD0}-ich-`9$L<~nXPX&|~Ef@%y zL2hn9q!@{4ot7KHnhhTOrl>4sG8(qhCbm}DvuWwp`ekuSF=m-F{}@C}0uyms7>J2E z)0G5jkSr3jDh8a(9C^=Z9vn=u{~}$#t`Tj>hm*3?1t$dJCUtSHs~ZpzuFbQVYKbdX zF$+ynkv1x;sqM^F>;NZtde$=e;jQhDTfNrqH0c{ztlmebqv_p!FpM*7i@|tS>HxG- zgmHtD?WQvDKm-$1a6twebnrn4Bb0DL3M;hm!UyAtq5+2tYyyWjLbT%u@(#>m2D%;$ zWRo29@I=HhnCJ~6PBt=VMN#5t!6AqwqUa?`@Y9GRk1$Lr6==W_4-szu(1VXSWb`18 zTCjn}B@dV|r5SM)p=+q3`m+cvrks)hD&tV9DmlhtxrH2N0(of_K5hxFq3^Cq!8L3Q z(?+f7QWQ*eC6(kPIG~khN@~)g7NKlZ2F%=y zi3&Hmd@{-uxLU0>Gkek_)JDYuwmig0|+3hXrk|uDwC>&Sen!LPJ zP7XK(j9O(}Hwu{IwT4kMMSuciqHA1Vn-B+CJcoT-xQ3N2N##H}x<)Ri1Ki;5WX~aBM+b+8sv->o8ZWi5S=YZ4C*N|?BqXklwgb@C+^J@rzsSp7fQfV z%Y#~&@g%_0af8Ga>^5LRmrZQq8c1;wGQtuhJ%HmBsi2u_2$H}worGtPD+=p2zz0_- zr7|E$g)SCI|C2Fc!xPOQ7JraIidJj_QDOTGYdE!^spJ73)QZI_f}orXQiE8nuoM3{ zQNcSM=rezD+jNljizS(-74X>G7-rZO+lXrwY+;~x{@}vhG)_-#a}x!pA{mmT@J$$d z*RZ-)CRhmYhxD@_GGI7}un2}du^3<~_GYOXMgxY7*~BTjl&4ZaYKu@41v54XHdw%d z8(s@uBOUojNJjFJ!!sC&MAU-hO@(0&n!+EX#~#jnGE0Mi1R?-a7g6rXNvq77D3!p6 zoiG8GM*D-WD5Qf@P-YM%*`F+e#1S5z@i?{U*ahuqid1ZjO0mG$4_K6?UsCXK$7&cQ z#$Yi*|E+OK!Q7>FY9R_72t^jIu;pT;M2hr0b1E5GMLSE81CHp@A#N}uA0$;Whv5?u zZYtRze2EB*QA8G|fTw{(LCiQnZwR^krotvAo{Bvwa3XTn`CpL&lKEYf4h5JLPFleaaw89>{nDVL>KQ zvdN$F&Wm_jYE!)!ET&F01)NwQGLgwhskY6jT1^gD4FuJ%f_1A~eb87d)>N-Pbyry} zt6Ig%R;ikGtX+L8U6XoOyyjJ}{|c&yEJ;+Oo&>3TCG3Mvksg+^^mK(~Y-1h!Sja|J z|FV+{-F=vx4=l zf?<*oAIT9Sy#$(ibOuRYi4BKic+bPZb|Xh{$Zh+^nZYz;&-bie>I zp`;j$^AHRXM8X=uw_k#L9Ecx`U>xUI#{yGuO0?VC8_%dl9f`3)sF2Aa9J!((|2cAp zg7Bl1JS<>fEarDQ;q>c978<#I1 z4Bc3GI&Lf{1RFBK&&0_hztIM=9N`l~qAeDiIS{AT!RragAQ#XEul=ae>U9zZ7Ipb` zxu{SZ5xT7{XoCT@h%sxBxKS{o2=b%teeVND+NP7%WOoxpJx_kDMq0`P|A;h#14=+| zKuZ$E7;fMq@RFLy`~?`5)3e!&x_Xh+@--;Sm*BLZR38}i$5B{Y1@KRQeEkEbC8CicN<7|kpz+$L-aXD>h{Hw>EQ$c9b^w!@ zIVohpMiVwsLe6_YgNq-u#-}hXtDtyv?gH?t6;CZt^ZX?|A2Hf=|4kAw%D`2!ikGIm z1$N{o-@0NSYT38zT7Rc~$f#<2k>P%KO3bR12m{F`@(w((ZCjhi5j^3aarkahq0^X# z-duQjuPou2_aHMQA~fbG@;hV`T)W2WJ%zc0;(-1;y`o4x7g;~Uh(G@W0sB$6Edm>g z8G`s(ze(95k(xi!L6!ytzHTDE*w7Kt3x?cLJ`ya!;bK01A<+`R@V?J5 zpB}*%XJWA?0I2)o7YIxsTS9;u%A2ehJW?pUk(dBzkdvsICfys3plO-%cmo6&g2h3A z+_0PEKsEhS2EcvXr4k~pHdrxges`1 z5}y`~rYXZZ{(+~2$_#-R!b?fR2fB`&2nrPw1{@i)Yp8`QB12t>lUrCh*Qk{H$-gax zMfd=5M|h_cz>LI$KU2Vl zK#-d|WE7k*rB?Z z>c@}auOS!}yt77qStuLHiP1wWN=UDSNG};Mqx6uL|EbV{RuBdxvW)H^sbA?MUpa&B z=t$a05f`zkSojDxL98>##8mi!One{2l7&M%5-IBpmT$B0vLH zOiago%z!b>tyDqCtW3+iOw1$+$<)gY;LFVXOwbHX6WmOD+)I2EP1Hrkr0`NmN&B~(TutBn%|L9;{~#F7*yPIDygL|_u?8U}-2@mx5(b|< z5^fkB4}pen`zYYdPVH1a;Uvz<6P%n7T z0%Z^#porwm183ob6;VQsIg&u2g7{?4|6_xw71@*YQ70#0l?rj8N&%eliqhg~62sd- zEd5hJwJk06PT5?}>p9Rdbsk?SNC?YDPROZN@{vy~A0Z)#j)=|*xrLQTfe$^9tML$U zkc`x5%RntvQ+=%N+&j+nOhxpx@!X3qX)iaEM2_JJDC-gQu~a#wrV8+piNkS1}bTWzk61I4Opy9Q4d*Bz|v1v z_0(aU)c{&pP8Ay_Ajm@Ki&E-G{|Up9SyDesRRs1(sPA(hH`=c-TCoCTJvGY`!{JFo zqD_HqS(n|Z*7Sl9nO5+`kQ~6&1;bg2yV>Ua*MNb`o_$%N9opT=%7Nh0LM2+IUE1w5 zOhPqIrj1&uRZXW|)zh3>tj*fNtXhPM-K zUEAfC*+t#k-Cf?bklfwd|K1H=;cbxLt=Zu{UgQ;?&h@c*91+D$Ug(A1;&svJtzP7f zSxj_Z%(Y(b?Og%YUf1nj@m<~TO@ikgU-VVo^5sk-Kmu4zU-%W>9YBK8U8fa20`N5u z6(|E2fWsU6UjPnZ0UlriE?@&bU<6KJ1zunVZeRy~UC)z5}_02jCbDV|~~u3{^`Vl2*LE#6`-?qVC`<|21A?Hg01#eq%U}V>zB0FWJ#W6O0Hx}zGO_!WKABs0qtWy{$%LYo=$FJP%hGp7 zWN3$u=--^?|8$mUiv~?+#^;OPXvoZHj{fM-^k|SC>B|&pl0Iq2G-;Gx>AqBHmVRlg zbZMBL=|Gfen!afgv}v5)>95mip8n}a^J$)&%YNnUoxGs|GHr*66F=>e>=&9=H%5xPq^)EwP4>CkTTp;OcRP=#nr5NkN_+aZ5o+2S1efoi}7Qf=#WD2HJPTD7)iwr+?~n1;LtiA#8avND8rID`aHOC@lJQ2;#! zdCLlDtG}L#zV2+h{)TBVgvDOMo3cw7zW8E0aO@7Ipe;03n{Zt5Oulo*9$NQY^_1@oo@UPy*nfbHwf?)Q#w z;Z_A);Dxk)0ZW+f?N;s8MrFO^0YM;%1h9m5C~yO@g>?P6y=v2J4<~UYLf+ zHtu9d?qR?MpCy4Ss0P8VhQaoOZkXg20}JZ)k_#j&B{O057oY4zLAx7=~^r zhu^mF%u;WAoP{ATa=)g7z(#HqpNiv-a(Y|w7@zTQv2hdshSDyH63=lKpM?%kg&Cjk z|L|7u;PvIDu5mhGg>iU?X#fQ}fbX_B1a+tbOF#u1SB1GIa|vIA4S0gHhHSrP3Ux?x zDtCwC&g-zgg638SC9v^h=m0uUhsH*UFu-&MKyn-31rHW%1VHi)P;yOA2V zAay4H@{{-mTSyL8=y5X_bQ|x6v?g?v(C|DrhS#QVI-mm_w{tbWT~W?ns4j`-eusA$ z2Uei)%Sv<+$8$rNYsvNlcd!Eqh=d|{a#u_A_eF_g=!VTY0wVu*DCqMExPo$6?x~pX zCvb5PSoH+2hH41%3omtZFCKSbg$dwm_MUIYw)TnQ>sfDzT}TJmJ_AOl02@Da{{wmM zUpIzTFNbgc2L3+w-A(qHt!fZpg?9jjuJUZrE{9^rb_vk-HK6n`sDl^}cSR@ne#a9g zw}ePv_jIsrcZYYBz;h~HzHN@TUF{iz7-{nY4})pg_zItKCQpV6Ptw4xscFfO)?b@|ORGqfZG`_W-&V^4~TFNjH$FN9+-2a$MheyPkO8 zu6V=u;64j`v2Ovg4`(N41;zFNADD95wrs9H0LYF61;Fz*cmWX?@gWBl|H^l9dz|;= z{sy-nh2U?8zvuM8U;FR&08~&15vOYkxOg$Q$L7cJcVKTXC~^@`c&O)mkQjV-Fa{fF zfQaw>smS_cn1%=-{*z~K(nod>Q37gy=0o6xaKeE@;O+y7he!a3AUq;n*>HqlgCGP! zJh5fWQlvE`CJs~;X(Gmq9-fRcW-E+`7M6CY@^eunNEiq;VmvUaU7e6AUL5k6(t{n6 zhYqpK80lf8m@Te2wDdw|Pm~#tsCvj_!^EITG5WD{gWwuF4N}x8T5u}ZuwuuOEo=5H z+O%rdvTf`3E!?VS>iTizSD> zf~wrHqcb-%UfX%H*I`Zes>vS-uYJM?eh!L?x@Dwpgg>C(b? z6EAN3IP&Dmmosm^ya|XSxRVHWZv8s;?Ao_;@9zD(ap+2<=LT>7Jo@zN*RyZ$-YnlS zQ>K)EZ~s32{QCFv@82`NeD(P^Ab|xMcp!oaz6IbI0xI|*gb_+OA%)s8=wO8zYPcbX z9eQ}yh1BWxA&DiLcp{4Yg;-sRExPz3j4>X!;)pWZcq5KE>Ihrqq?A=!c_o&wQJE!|U3&Q?T3doS|0bDbaw(>nX{x#AkO128 zBb#x`IVXy4!bvBddFlyad~e>lC!m22C#ZUS<>9A@getlyqv;unD2r`rF)0<3<{(2H zH!SByq;qBZpBVT3F-WMiReIN_tZ^zArCR*Z+7NCG8Wt6N=%MLcI;3h>tCqsB>r%j) zb!%(Ho~6ei#vUgukE?B=1+O^x`Da;81o8(bj*R*P6N=2b8?$hA3j?S~wf6%erLHze zjT4DMg}L#rW$m|O_2I;4WI1x_SnSHX6t>!SikcgIgqiLT^^zrq9!Q-L$iESTm~6J& z4&iONj>YPlvSsBk2gI|T5l6ni;+j?;vetzzxf-1T|J};cLfYF!Jf3KQ51Qkk8<#TCF8@;m!I-KmZ!${}A*UuT#dRW$*X}UGPAwhjK zzuXS22-ZoBcXQcRW5h(yLyP_J*lrnH zdC@mly?fW0ckVgXubK{n<^#9RxmbWtWcb`|OFnlYnaYj);?u*f^#%-!t^MYKwbQzCl4#2!6woNiYX0rR7-*R5V4tVA&V2$5d$I^cs<%N5N16ILISII zK#HA#b;(L$;5aD1JX~uI-HRdIF!r$8oL4|@RQ$!nUXu&{KtW?_K;ulru0SXdwcGNp01*w-a+gVYOZeyepI610t zX;6zmGo<_|QL2kE5`#Z{BGS^>#RkrC|AAT@*#c)+z%l6ZS_>Q`AD~5oAGmUsAyc9W z>6o?@LeXr91f@X$xy3Xx;fa5Uq9Xj|0V+H)nK)r)1;^q(I0blc52hQPXDz3IzfrNykv8ii zX9T!O5^xh=^%_^`d}>vOwqT+-|1hW-b81PJevzW}yreVLiBBBl6`+fZ*atNtOPJO( zp#@y1%DyNzwode49nAwJ$OXEms`QvSWozLyDp(fPuB8p7EK)G5h{_V}3`}KW2VW&X zTQNqK2$i5m-}zZz!bfKB!<=c+Nk2~}1+(0oiT4uMLp?ecW5508sx0gub zHm3QypaSbVtH%EKxH1^){{wf5)rrWGwPLXj+fVBPYT$yHvX53OL_wlZyB62t|E7H88ywq_^yoA znTj!*;4t$6n2h9EjrX@o&2E>oFiYc^k({|{!Ow!{)UAqNc-jU3mxq5j8%s+1-xymo znv$EERPR@sF$}_+iyTi8Q-HumBe#z7TPDA}WyDZV0m5S7mZdd9=-?_&bU=>i?Z`&e zFh9&tUtY0zB`8#C)^4;uBPq5*3S+!(Q?tFkuc=WD-BqvgyyS{+JO0hVz~ zmHp}Fp;*veQ{7Ep{~g}ck$MA!PH=D0ylwC58MH(drUAjdx_^rBy@s*mpe5*H`F2JL zkiB)T`#navrtZ{<(<`M@E#P{~YQkLRcTHC{1a;c)q#C7kv886TBzro{VqWx0%UNz9 zM;s~($J*u2hOA%fM!>|Tu&D3K?%MJhw|_?Gl8Zd!w|$t5W5|D$$}%Y5%lj_EboU~{{v@S1rvyCwm_KQ~`mB{J5{H#_?k^7NwD@wFFg zw9g}1=4u7}8?ufR35a~%b$)wer~c*5celVFfATu>h+dDDx&j3d| zY%9!HcWObEwLax&u~qe8u6&DF2l|VvOR8n-fzscnW3@TK*Mc!Dr!Y^V{H;nnHJ>#3 zS^vRSq6rxUxfJ`UUAYMt!`L33o!B+0pC;v84lzulRa6|HkiMmhqSe|#DOVhvPyogb zCydvtD4%%gksG97bhsJbje#nS;2>F<+~olVK95Ch9aQ+9YPlc`x{JW{ks}Ne`(#rD z(%8s^|CsUET^7AT1RYR;ag`lS(FNg3(m`13tY3^#;mG^}w|GlL)tCP1R9cM@wZlJ(m z9s7~kJPp||QQ|HoOUb#L9dbY)9@g_c)kIMZH@(66)l}(d+6gWiW3>(<9bM7X96t@v zCMFF+J&y}DK?j~*uDM#QbYReo%&A3B{JqPZ`C|L{BFq#4|Bc9l2~(vI;|lW2k-1AX z@zO4`4_Qr$X6cGCLe4NX<1*UfULa%TybMHf%3yIyq(tCXDPuPRkkULOF}lk;R)?#l z|Jd{(4le2I>Kqtb*bGlej9R)PTk2e+yro}VUtFriEVdRPYDYZo)I9GVwk!%dyxbX;JDkf%Tre=*nD$#ZfMcUR|WBtWaAh5FZqMXIrkI1T1* z&W)j^=ekViE=}icYUlIx0c6JKX!6z;qDHr=r>E=+Zk}ZZIZA*+Us_~k>)nNJt_He5 z0x;PnUHBV&$^Z$?Wv)o&c1}xQbmy?7*B7pj)?^>?xR!J|SxVj;&qU1`=1_ey&UcEB z*bIu^yp}l8rZCpbi*DzQ7EiaV4`fc<++0{j?$FQlX!Z!_3#E^d7Th7V|C5c5=Koxe zi3JUkN{@zyk0cVQj%Lmj4Udm%X_W>km>wzloG5P+TyRwmN3v*+M$UdFsf*Sqn3iIg zQ0eRiTKmAwSB0rSs;P-e=!=qQIPvM8e&d?n-JJS~7ofm++EF8!VlfqgA2HH1Rc9R0 z=irf-812tbGSi*Kk&IR%R3#vafs-ORkoS-Pfvu2hO_uUuYU+5=mL<}Yf#?Nh;64pg zA229wDG`+!QV8jg2e#tiHR>etRwhkirRC`&Z6m1y78x}Vyp_-@HPMKnBq1zR0|h9g zh7hzwloLEA5w5DNB5NV=POuKvt*&JCAVOyqAObqng=qPD=Ia^7O~)Q#E? z{C&$oIpPXx)Yp}oJMmRr#a748&eAeLqrDfe9j#J8Ny+A{oFUHmYup~S%IR^q26Y-jA3yUQ<+v(LfNoA z9ue9MYyFX6&~16?&!;Btras~NsjcxbZpNkUy`dGxrP$Rv|I;chroR-ChIQ`KcG2LG z3nb>Pp-QeK>ZRnNs)nf{&}t&q4((Y8ZQPcv=xQiiS*~2RpYFD9=ZIfd?WmN2D~hpR z;I%E2h1Af#6rLp(x{+9*zT(U*2*JG`kO`$HzOA>JmjGd$7`Rzbr66>P8LbiHK@lh=lL2j;jBB8mM2N|c%6<Y>5wVAK7l3k{r7@VzJm@O#5ecS-X(*S$eqQRZJkzgAQ#_$%~rzTjpJsZW19L&OP zJaz8j>aC9rT?N+b>1tv!dRod#>&~e!#7M2JK$ao}|8T~Z;0Vdp3P-L4!xz%t8O2TC zsiKh>HH^biZoY!vqezT}cH+u@-T-Eu2v_Ewy$KX#=v&yEDh6M-5Tg*?u*)o8%mJ*# z(hYMW4Xu)AG`|OHI+qZ7%BI^^@fXp%npP@xFGKWbq z4dBv7ak`lcG?Q);Jyb;=Vdqfdxcq^utkHpW|6DdV$o>881gWyzs-c~A#?E|j8sEVY zDC(CAa!paO)4_|x=8rW7T!{f8Ah#~GfdyES^1~f5%vcrkt|JdQV<8lsLtEj)-44#S zFJoAuKZPGeqiSWsvjN95Kev|ZITaZT?kAGlwv97R2bmgvE=a$swHzK~jPD1ZvAGD5 zdfo8^@NSWrBzDtC`jF`T)D%=*9E*eX(G`^?xN%nCI+PerDRO}A463xD`STJ3&`DJAa zg_QmTH(F0ps@CQU#F87+ zZxhOqR zHN|7GJ0n!)Z>wh~tPNyoBVyNltFVE%?lfH%5Bo-~ryNf1TDs3b3wfYan4n_^g>DE_ zV~3JE$r#b-eq6Cy9-M>j|Ma^%ugxL@_hOKCujQf80p}mx}Tcr7j z|E0ZyyKC+Vz&CtmdOOeJJH%JK#mkArGyKJGJjb7jZGAh%cf81tJXwA`3uwIC1f5sj zC3O#{N~R|xAB%`Lsn?iFs^`VZ)BLDcr|yjTwv#;2b0(ske8?MgM9+(S1~ptjR>iP8 zJ{KqtLvb@7rhKY~DUaHDPT&I(!mO|Jg9SaEFy+6Ln&v0Dl%(p5c|?q~RhJ>%oN$)h~Qes3`u zImz;>Ch^@OHWN=9{~dvjMc5rx0>#+CEfciT;}D};&C+|!3ayfrDgJU?Tq{w>nXD9D zeloVczm|0V8B2Z*=<}wl{T1RZ?$7cSkOHbMLOXNgFMrGgIoTVk5h)8Ir<~OyGYVNQ zA1A))Jur{8HTb=0H@B;F53;(c>*q{0joVt&p8IxLxJ$vYS{)mMRo%%{neL{g@dkd_ z9=Ux11Pmsgw0IzK5aGc@d~OW9u|dqAhkEoN{Nd=K!a_N6P-?Ov1&lv#V3fRp5hTix zBMl~W`4VPKnKNnDw0RR}PMte>_VoD^Xi%X;i54|_6lN0~cHZO}BZP^Jpo*fVNK_CZ zqN)n1fYj(F|IVL{9e=b$7}0FVGk?so1e?~T4oGEZLefK*Ysw)W0-XUlW)O*s4~Hl+ zNn#LI#Bcucfs2q&oW>9-fz2~FCfbCW$u8Cl5mCsOEDw(1G$kWqN0u>X28+lq6O>cN zs#N(>ZjKFw1#=qdrclMwfy~jn75XD}C zjxWCyaYGCD_@gha#F7iGv-IY33Butjd+v=2+q*C;J=!}BG>|B25x(`33arKEOneWy z7_(YX|G?o2WbB|GyBm_oB8@x}$t0ER?mMQAc!9j05`@vmvm8^f-LDgF011-+)qn4=|s`M-15|OGqxmv z6HKkxBq_q^BK0h+L$eIAQYzyU)Db1DyflynmmHPUQcXP-)l_k6GLI*tKJ=E$Af+Usm%wVN0wxmKj1&qKP7yI_GXSJY92~loS73GvwUYX^Vbz;?3 zselSg40yYZjG`h66m_^jaJ90p{HTbG=XGUvC{ZWI^!Y=1gD$OONSX6lF@VwFFCsC9 zqtHQ@YR0!}KBz`fzOe-pOX@S|as#ihqV1WYu5SG~NVK~KG1?P5g}mK`y}VG7Ipjx%N5`r5$&f^2#m09CIRJhEnFtPr4h?%q10lbnMV9o%GaIU!C>V zUtY>d7GA9!ICfpXeN?AV$DQ}yegFN`&1a`vhj@WM9{J>zU!HlJ+j79R+{Ks;3N(gCP{*_Xfd? zL@Vb)D* znJ`ECFezlqW#iuX$(+PO8{9~yzpMibT`-^~gQ$f$3IWU_m7-5SQQYuVd!W1Oe@Pt46r;UD2XAszc0Z$8B)3YFh6Ck)o|7ey=u%%#q7{<*t4jU3AqSH4-c}6Vyr_LR$NpHiJ^?E z^@py8UFRuwnF``Sg05BTY$^^a)>POu1+FNBE^r8u(3-*|x1dC41$RVbtm1+$>g+9w zCzDhZwobOa1~+u!Shrp_vkjAlSpgfQ>HbzN@GQk^ZHru$Lgt&$jK)q$;9AjWK^BEz zMl)!%LVNnq6@}GBTTNSwXXIk2Wi1AAXL4N87Kpk!m_QW`|NB-|qV~VWJ!?u%!QjeL zw;)rv1zk;HLI%X47M9h;E4lI0gP0eP#yiesxW4Fq=!#7%6F;ujWX6AC-! zlwd%^9L(Tq>2h%kXs)ue!l(~a&B|R9@0J#UGrKBcyNNI zEPlx|D4Wg$#DUIdhz57*yjjpuA*H~bzzxoz3u0KZIdWzjMAi`tF+jP6TG&K|YU|cs zXfn-cX62&MyN<3%6wQ#FG7|eDoa6zsve@S)4gl*o$eMv} zU`H>+RuRb#EzL3U1clxz8f9o>-;033v6Fr3m?)yd&pH!0M*U_^jCmcVXf6T}5o=|v z0?x(If*w-6R7eYF6YpNdGg3g)KwJZfS1kmawXk2W+@QmjbT_Q`^sr-1 z$vJ4FbG=X}bHy<7{J!0kcJqhA{b%XmD&h1>{CA zHr57@OJ5m0@EYF9x%^Ncf;lE|Pr-o(fpAhB|KQ{gv8sX@aDd4cz!PIe#3uOZ6})<- z!W(EeHDm&7=4hiUu@D9WD_)W@o7t#$okJQu@OKg%K;oK&w17*4Q;vHzC4-=AbV^79 z-Hu-Ke+ln9y8;b*OY<5tS@Z$yqsyo|`og)-fj9I3&CA~cq=+E)@Srhk3H10-r^sf1 z!dtde0R8#VFy}jaagE}VAR1nVZNfUeU2(A6_I1xi?m4Z>31Gbxb+@5AC6TKHL=V^ma?BOyik!O#Qe4VJt%F2!RlqAs6B;8hFi0NTHk*Z3BqmG+L?< zX3rb&BIXVv{cuk(yrG?FEf{ZN5FrAu$4n6K!i%MZ zsoy3*=#r4RZUGGyg9jfhPfqUzpJm$^AO+)10f#RzY|yPffwdIP{iILyzUeH&Yo5L@ zVW=Sm=PxOih#G1^5x`Cag(Ij41D?2HztGUi;x4b)1m$D_0+T4 zG7Obo!U>Nn`hvllj!>J{OWPoCH`Y(5l7k&4a0_M%8LpujXyx3h@773w64mV%tPIh@ zrPEH45u@V=s7n|QN*V4BAxaSq|6L&!Ujh~;kh@M%_AY{jmdFvpk=!gXmb4(<&`?JH zE$9TQ`<$g2I&T9-g8ild9QZ^Fi_si4Q5%qfp?D}I*lz& z5n-VU^JElh4IzRsCZ}J0;KE%BLzU)K)@a)0~Co* z0+gr_5TFdz5+?hODd)})|7ijiJpmd%kK<73E(z}=zAfAgX#F6g6^LV6gmI@PGEaUi z8L#pt*Rl{{pa8xNBrvifV?hA;QYN5j5X>PD5P%gi?2$v8fM>9|KOg--s*%&AX27#E0=#!iP$G8j{rXV`-Z57nU zxUS(TBB$1H3li92)*^8vNlwdHunfIPR^}uZm|+X{LKN0v5QgB*CX?|>Zw~Hkusi^V zz|GY{2OT{P#cJiR|B#^wfIu|3VJ>uI3`~#g8gy^Iu7zBI9f}SffYDZ9RA+_{w_;&O z7nD~vZ*<@;s&15pJ`MzqZ7-mV3P^z+1_3EUA$VzR3 ziV5D6+@52qjIOfaK?Wz2E_4d?1ay`%vl@TFR&<5@cqJO|j~WYP%=QMNASVcDDh?3f zoia4D7=RO8Q5;=y6P%4ueUKQc#VtB43(+zV#KDz%!PSCO18+zM)~u@<)!+0~z?$t$ zi6b{mZ2Cy-0&j683e%?afaMU>&b+}o;c3z4GvW?qtg_K1dPT&DRX*fwZ^S?ji-AeW zX~;59P-}(~|8uIZ#0-_r!0f(jgp6s2lnEK;AV-(W7a0l$Mh*hW$`?(M5-yBI2NJ*Z ztrjYayvj-zyD7#>VL_oxPiZ3jEQJ&%@D~<@4}4Mj+G^0OR2We56V@xBLUbV4%R1k+ z(g@Ku*XtJ~EhYk?B$)w~Zp_w{!7+9S0yPdK!fm~-FeZKhS+<}V?yD6bi$ZOyEKIEV zER!H*adhl!(ei^6U_qr2L1OQ6{eEj;c`{_Lu;ps@7hNx)X7^Wm1`_v0^!It6CdPhKM`Xk?HP^GSp&2ZOT%Hs#G9N;iYzJz!h&al0-UJICw^de z%>o2M&vzLRY->eNf_F%YmnUKhS#N@QMZ@^qF9n`Amqf{W;X-G)H7>>vc~J-TiuL~r z3VW9gOvjgeOUGhKck7~e(ulQ1&9|Vqw|VIol(f`-4+(ekS5*QttOz%OgjPB!s*}7| zCMvIh`DcIShly4R3jaqg_6>g%xG4OCg7asAFIa;&n1gL1gF6_6Lzsguu_ zml%DLn2Dblin+&$qnL`T*oJc0inCaYy=RKI*o(h7b-Eag$C!-I35&}Zjni0*&RC7x z*o`YkjNe#}=h$@EI6Ekkj`LWL97T@{8IQWSVA)|E_H%R~4IVI* z^2E4Uh|F2IR z*3k9Tt(v={ti-{yLgzM%p#ZRs{oLTP#0e>-8<~j9%CbJ~`0r;;bMc!ku$Lp*8sw9$6L^xoXWWnfq}h zTYBCc)uu-|2YwBfn{31e8mYIZi&J489=R8Erg0x)o}&WvTrL0NY%dn2*d7(@WIr!WLh%i?IlH-Iel>h|A;Lw3C|6X zPYmeV74Gq&ICiZOA(*MOKJ*f&_&2E|8+gRn38WcLWuRIaKt~U(NJV3_y+Y~c+7sv+ z29SXryulm7xxA`=k680I~dFXo>5y|RN7Qm zIzLb-Du5S%zd0>&>;`mUt=-!sFsoXJ&-)ZoRC-%389KOuj@X!WfP z5qcD;RS8s;x4I{sc?$Z1fOYbOfuA6)`i9#+YT6_8E}8vI0sSmg|3DQbC(vivOa{bd zc5YLgbd)p?6tW-zD}v#XX@tH*!!$aE3e0X~FdOo^E*Mx79^e)pBJ0360U}G=Qz;Ft z8hkY?+8}D7*j|{zvz&KA*&H4@ndwruA=$f$#gWM&DA5fT08&7$ztHj2%Dk!DwbpF) zt{%Cmsh0u0e8YlG&K-OL6d{>;pX$ z;@ZD6+ZB(fwta!TeT`Nj$!jxoz&#~xdD_$6j_jC!8Zmp-9p0^*-Q%6!)i{pp9p8oc z-t(Q`cX;3Jh-D=Fegpn?LA~E!Xx@)vn^G~)iZ@)nWEYJhW^>fdl|*QDI2g57(-UTb zts{fbJ?tL%mT`w;ha$$V>fo2>->J{T+_2mU`SVsNEwvrndqHxw9jQ91CN@4My4|0? zFnqbS)V>{2G%5Jd_cVEa z6(YW})j~{2;Xijto-!!#9Vap^;uLUwwP&R0PGNf$0q{w04w_A^K1=ggp$|sNf-pn# z$pP1~0^DSaJ}4m;2w^5JpIQ1KX1Ai~2)~bvn-q#SUFL)ko~0F5Sw4U(ulfQKS^@Mg zW=<03mj2T#)}{6t?D6%A5O)1+oFEl~|3FHgE&_q@8_Od#zxQPZ5<<>O++YeahDx$F%CqC@rhzcfQ1F>ko zTIKq&fsd)r?xHPd7H8!8TA^pot#ya9b7294v#km~AQs)B`baMzv9qpom)z z39XQMP;0gsIhG(@)jEr;YASNET9Ipd<5^K`)T(Cm#I0zrg0h-<5=IP0Qp3GYxsl+6 z*wkyACRo*$i_|ZP0>yeZ*i5uGQxl^x>nuw%#z0?7-Guw~S2UzrzAn{e(ABe_${g=D z%M#6x(6kwWAw=`52R~-dY3j!Ow$cH!XQ6Wwq z5}*|nF|i;Jmte$5;D(O{6e13lAvNJmL8(N8pD?W);7E%Dil`AM>o{?ML4}?n0}~FW z8BJ4nRm9_6znq!nUB&4J++2fbgPu)kY6J~!tx6?lt_8>oba`uf(aFGT z4(++QTZ<$x^JfY&7W!j$ty&aJuncGm-~StOBGf1X$LiTkDhyc5pH=K^mFx{gXqA`~ zzQOQJ6NSYngDmY*`V58M{P63r*dChbsnL!|krSgnTBe_rT56+a%%~gVAjy))K%F{F zj8v|g>5|ryCZCLQ$||qS@(vz2>uPH@keEKkOhu4LJ+&gW z@sVZ@wLCL6e$*fW?HRP53YDuq!X~%N&sJ0{S+^8WjJjKQ%+A=P-CR+rbHfOEQO*=4_R(YWPwy5WfSZiAcZ)iJctR4fzeW zMInwTc2u(u7%T}zF@Ml`LSei&jJ@FPO|X^tPP`5^kDUvCEssxr`R1Qb{>yeuDKkkw z6ap%U&=k`|G0>PQ+P2b>IRPwZQgkDX*(>%t2yAe^2T*1>EoPwUzO@w59f?(TH@B{Qr zAz^pXL&CN*6Q0K8sZA2Z2w6E%V7koH zwK9&%ZxZPRP>R#8IFQi;UxQpA>b1r;zL9qdoB;47Ai`Om2%Qd5WG)BM6X+q(EAG(+ zrX)2ZrW^nnV+Z7}M-( zce~up$V^$9B>yUqb_xwc3LeiOhV+^@oarrK9N^~|?}ZgaTNnaP$~XuqNL=fe6HeC+xLBXhB|h)UZXg5I=`_xV#^6 z@Pq@AMEvA{51^tF6DsTk@nIyrG$xK?$lFvDb6B3zM9%?ysXuO+LJOHKBaSOVg%OKy z!-3p!hX*(pmi#~_F}80C-cp32qIV4Fl_!p;EZ{ff)MX(~Fq9F(U?T%h#8Z~0hZXhW z66!(8-L12o@9fC$F7nQw+UAlJp@=y6X_o6^Yk@&xL7^AN(RBzX$?O(L60Zt>h#R z`N%24@{UVjIWJGvt#24})YkkDIFh;2at?Fhw%q1Gzu^hv?emH&ed+1ucaymDOcHEa z6#sbDgD5-^;)9GTddbXeAshjAGKt~p>-8=tN|E9-kOC6xeaatDJ`tZ3ZUf}8`oDcb zb%o`@;dp&t-YIknO59g~ftU(YW$<86lhy-1IYX0o!*^GSuD#*p1SF`S!@{fgE;?|; z-+yNc!6RhV4pKzfEi!~pNBF()^F$_pkcY9Okn4j$yQ?;_iA+kp>N1g~Cpd9mHv~fP zo5=jyXEOCMc#0Bi`E-LL&-Ca^-`h?vb}@1zbS_?_IHL0?u%1N)YAI*#f=ntYOv4EU zf&~`8{T)`An|-zIS{O`l=vP`f!(C_3AUwjFFh&+1eWiR%AbzgJ3C7VKUSkvN$NyK< zp*k7i2I*2Ch!KE(We}AD3&OAtF7z0)aUpl1YkiV{u~8pkG66Q?Kq5te*}*!MfpSiS zHR}-z|AuR_pbcY@f8201K_oSzNK|dBR&9wbQaWO=5sB=rLBH)c;G1BP#B3 z7GA^&8`2$-wTt;uK@tIS{$v_!Xe}u=0o;KmVCam`SZZPj65+9R21EiXq*K`l6;o3{ zGNMoDMM^Q!i|*1|IEZVc1Xo4EY6yUa3_wR1B{`lzh(Upb{3kgLK|;YG3g$%(I>?PD zQVZm#Iovoecj6W~L^#(bRj!vpjin2d;y?H#b#jr8%jl0mg%mzGAIP&skjRPwqYI?K zS~rkKxyXyR!fExWQ#^wwK@}BiLRD0h0?Z%=6%vo%;y4Uckpcs3(CCvt$z9U;5o*Lg zxMmLAa1Gp$FwZkR3AABI*p15QOLW9du`m&;1YlNn9q6c8bhkL|X#X}^lx14Eju6!h z3{VpQ)`?|=gwI48@)Awr69-aQaLB+C4m3kpc91+YClBadTewyciIWH*n3Xb-AHofv zAu?L&5QA}PI_Ht5p#@GR7-ciIlU4mq_UII2Qu232Va}9`P%#fF_ffS0WiO{_z=zvj3d3r-*|xk8ov-cfp72 zGC-s`n&7B>FwzjanVLs55&QX`vMHh?nm!lj5;SEp;FLmx0xn|I2NgJ;2Z#)F*CV>o z1d-D%SENO|_6$9Dj($Q6R~Q*rrGUg>TJ}gDY!pltxJ2LpCq%k4ck`Gl=z`(2Tzxf; zyHSCIVv^h^e;UZ1ll5FynSTs2DKKDEWThGT_zNhtSXFuhXmK|kiV%3S8aE^%L{piR zSqhd}fd>Qw-l$KI^>4QT4|Deieswo|1B{vR44)yMzC=}N1K@d<{bhh~Qs}hgUU8 z31~n;Np^WxxvKHw1fJ)kE2e1}ga(w5s~gdHrOI_)hJ3-As!at6p1`1IfUPm+5H%Tf zPM}|;RC|OFe9)?lqjylaG^;w+Vln|LbutFQibtnc1d3No4~Ash%C9XZ5h3QS_9}I# zswihLVpQO-ulcDB>#!&hsw)F@Pl1kdSahm*b4jPMlyk8io3R}Mbc=vqG)Dq+hC5Lg zbQ?PoM5leV37{UkvM~FwGfT4{W}=i-XOut;om8`RAq)=}2yK-MG$^rR@R&7=v`MRM z5v#OK>$Fb`wM@daQA@Q|YqeL4Yd4FcSj)9t>;JV@JGIm%vtLWLWox$27PcoL5JT2% zd&Wq2IuaOkBuWu~ZOainLX%f%G}!efhqoj}pbMwB62DPyfm^pUVWe^!2iS))+j_Bm zV2)HObo!D^;gwcrJAG+e5**SkhuaJ~6iKdtVWD{z8ThET%-rRrm#intvpMvUl9kt zx~my?UM?nH&c{VNVP25tApCk|V5fU-2>)|HcD#t-Fe%0djzA^#<-aP12!)VL{R_Hz z06*rsz1BLx)kLtnN3`Awx(cfZ;2IKE*S;RCjPEs!IG1|s%f8}cnt>K~j}m*xH>wr! zK5?Lx5eEa}Wn}Rym&Iy^tFgW^umy@$bvq1myVzk7*t`f9tW{A2eRp^jJb87}3inH4 zJqKd_#lXheuG6b-)r&_xsimM>rjaE@ZK_p#+i|b(KOg8fTJ?i62bkeeB#crP7jpo0 zay^-G7rsGC=~otOl0sGJqSP3Y?iZyZp#zmc$c_~mZE7l!nm=}9B|;dmHsMh|8oLN& zo0sgS_7lo8v|L!>DSlNQKjK{B0sk-Trwdk6f8j@{ry+!8_@;S#jm1Ebi`-X8rOOct z68*U|fdL=8tc8qBP08^e%S>3#a#(poB8)7LX54gI ztG^q84B%ji=^+j~LRu2@pn_z&30y@WRVaS|C}WmdNAgBe1a5dj9ZW}LKM$t*|Bt>B|Q&Dw2cSyAaGq2yhvt(X@{D+N{AfA9n5Fh2zi7bYk| zT2mY{;g6-=*V%0`fQn?nYzn%i3@P$98+o`j_*CcjLEXO)CYGJYps|Ov3b%xCQ2X<@WW-WOfeET1}~R z-2^VaXKC}F-K{o@2icGb#RQfqU@Do|5C=Uv5mO+Zq!d`H^uCJz;gzj?Fi;4(upYci z;>l*>Ab}J}r2m=73F8-$3Tikhea1xDXA0s`IyqiB34K&OUcPt@O_oeV(Vz&;ND%^X z2DKzl03y5xaVX5NE~E$ornKy1ZaUSzDmy)hK)s?thC`uvpU8o9coGQPxx3YabXpUZ z;_>4gP@Zu&I8fQ0L?uocP2!qxDWQuTA2nA5)eBE+S=3yZIf=)6UcRlt-IDLm5#3eYnWJv+Nr{ zFW^2Z#;HaiUg`+IpCz>F;MVG0p{VmC%@@Km!t9q6FM~Ff+9dTIcxoZg;S}~wLJkBC zL2yB&g#Uv089%~7AQLepw=j;n1{K3nMv-&8__-cP1>EqtDs)*!h{bEb;XiVY^upOV z$cD~XlToad9k1Jxtw0p5TnkvEA)V-s%Ix($l1UMm3s^(&YuYt_4<5$i7wm8$Qq3LY zQGei3_BIO9G#wr>XbK;VA8kL(%&;l(7Z`ZCT;PDHUi_W`@Kk4$&vV!}iV58F!3vPD&$1TQOp!n z{uqwhhMCQz|0|!~0ORANpssy#Li#747RZpj;YVp9t z?9qah)aF+J78ghWL(RaIAIwbeXDbTw94XQj2)R7vH}RR3Fd<+WE| ze+4#JVTUEwS6uHrwOD1BWwu#opM^GBY41B$*J-cCcF6*><+fXIzXi9Z{Yu4iPCdXf zsL!Wdfy^a!<8;E6cVoq@%}su3#U({H$?`xkWcdZr4oL|F#y~37!$4R#-8Ni@ABH&M zTB|J=UT~&!!Vx3p<)ZL9&1>= zz8hUaAt&~p+$yjaE32!@YvN;O*S=O48KD#M`f_mvx2o;EG4~trWKkcja2Vo6Tycd6 zZ#-(LA~1i0B6GBhP<|djY?sIftbr6_l!fq1ga1c9E6|*2meCEruEK=PlV#5erKN+ zf}>FDVhB4Rv6_|yBOtI# z0T1t}EoS+H3?655jUzTl7&0Wr2GUpz*LY(P2ao}*l&7TakfR8YoY#TK*dkYy5h9IA zWO}MJ1=7t%0{Bu4F<1h_5mM-jja)?~9fAx%jbR-xl+Y>;ImSdTvXPIBWP~Q^lmuNe zlTk!wGL`wJDb}Zoa>=DGU=a%tzEBwFbC(!DM7d9r^<3zw>(VesI|}oAGGmEbS{xKRh>ZGe(H>a ziew8BNor9~no@#XqZLer=?ukTQX?E8o*lhH5lA6BT7eVdS7exB;B0q&HoQ7&mhn}GZx5N+(;OrC1Ej8E7(}D zwo3;nz~O}Xpb|m18Lk+RB7$MT#R3V02S{xy)ToSvEQ&60W2!QOSa*Y(MR0zyTg^}J%I@p-w7O@#h z4mW|B1Pw-bILg8MDA9{`5aIx(5b!NLNxt;OGeeXRr%QfO(qBmUwi{4JDo&~g4UDqz}7>(Xdc@YKlL8h>u_5<%G%;&`&&)f3ArJa_9ES{SmSi1N4h0v?01k z!7LV6dd(>w+>jiIBVk{K3?jS%!pQ1|$Nz)zF_5Y08X_B+L1IYqc6w1^qAJ+Xht?f~ zD*8(`;= zwcG$bbAR0AC%>%6QEq6{t=#1=@Bf|3VLo%3*WBjClKIVbzH^@U98@`PQ^uVobZHUY z=e08W$dZ0^%mN)hY^$4ZM-&J}EYx03ZI2E}af?FF$JED^RlIJ2by?X3>`rt7DTudr zHx&V3m5|dQ(EJonOowG-e~H$mz7)kWJ&H~T9z6W%L#xxQ@Bz}Mdv4JTPH;EifL`6M z`p4*ovx*7Gi2UL0O^iPEqO@)QgY)DhZ1}#U7ljr>A*@KCAzumge`rkIN3o z!nf)%0wT(Bx|Eia_S@=`_uL_fQ$G-cx{_}tpC!Lvetb5%oKL#X1%D|D`Wl3S54vOx z!3{)P&ukLMdaPq+{8Q@6>i^D9{jvE(ZDgs77<}J;cb%c8rL$SyjBT7agwNMbg%#;D zCdoNFzF`>i^S%6VgUGW#@++Y33qTTJg6UhEy@9^90V8)Iuiuk4SUItDVn6>&2<{6( zqyU_~S*O`4j;$$z(3v~ez(9Ixg}kYY7A%kn>=WbzKicaSz!N1?ctKjj5J1z49|)&k z*gd1#0%)+P>oF@cdIspJ5Z6;0wxbAWcpe?w02mvT(6A?DFsGJqkS@%U=~;(=a)g06 zoafnsiBPR53E4MGHJD29@21#jY) zR$zuREQa6Eg_Ww8CjYoDY6zM!hz63=20W~i@3ANyxI`C%5l8?;LEOSkjE0115GnjE zg0h|{REAsd2}Jy0(6%nTo5!+L`>W-)04&P_`^VaG*URlf*FKn zNJ6V9#wUaUS!jlLYAZ;*6k^<&X~L0Ub3=g;25UmL86ur$;2%SBM7uSv+PO z3HLdm!a#&N>^c%)K3>ABgAs@Kc?I_?pIsUsGTeYbI7Kbv!5*|d+@qp^+(&)67z|n- z6&nfF!mSu9p}O)C5V9>f|*Nlr{c4rG|Y$u zkqB&r2xgd^9sfKGJT%3D%9oowvHy`RT7&^=z{B@@5AE3rI|51Op~dZ338$PL_HhbP z(5sh>j0da0lDV&ZtiC&94IKc7#KAG^MbItNvddr00Y|aS z?jxk)V7@kE#iyW4x`{Eb#0C>Klr3@yL7N+#GC=NqmrlcQH)kPv&cvI;^t z^N}2*yV`0in$*VoYK@v|NQbl&YjGxV5QnOJ%xeG#y=pUBnnw;W%97hA2Jt3#|Az*4JMv|q07)?=vOw+`%qHIkDFir-* zNaleCP)oY5XbC|a&&8Xk-MqeLJEc~*k@P__jCrSG05xr#gLTTrMiK}(V}OoiC~r_P zU04Ult5B6f2D4zJQUHg#D@BfM8ANz66*GrQz^%1%(F0-5MQRBOZ8$)Kg)-aGTPQIE z$c23REzFooHlwfiwk#nk?MQNdaz_uL`3nS%~})OY$rf>}Hg1td8w5o{y`RL#6$ zl|V;qve#%KA@WOvz(`Jr8B}dH?b06W6Ve|-u_ARbJ-P*LU_v@Y)j<#n4bl+Jl+sXg zBgfM!i0HZ^U?24yD?Yh~-=I}71;NOu%rgywwF1`Pz?!#G$9<7Xc0wQIv{RSBQY6#U z#WW~C6$W563%gK;8r3{P6&6FK58b1qIFc_#_^*UCpE*^N@2bsxn*fD8J@3gR9{<}L zSvmk$bx^dlvR;T;-B{Jv{814}Jl7I~{E`TbM64^hFqIC2UmfW7o+GD0#?{35WD ziB%YR(i?Gw<%uX{RaRmg&;kfBXtgJ;&`^J?D(GT37l{rJgf4glFuH6*m{FwLk}4f2 zJ$-9}+1jfa>=2D4((TyHfs-#&K!Dux00h8UN0buEMS!!lORNjpdGc5H(o5~+o1k&4 zUd=E4gfX94SfTLRu#GW_P!Iu7NohS%{#>Ff0W8Z6qX9(S=ZZ#FwObB>E^&o6l#LdZ z4HU@4u`)8nO$da+n22b2GK$E@1x*;s^n~a&DF+kD2!b+ZgpGpuCS6<_{{M;ICyS_7 z2!zJ4LZm3w-^`)hIA4w&+}9SSqJT6D&rCAoVaJEZrG#YX?oZ}tA`V0o^EQoSa zJHshuWz-5;!gL6hjPoXg&q!{ zztE*0f&uZ;OyLw7CNV1sG{bP_!^QfuF@^>X`}u(twtjm zid~`xxV2VmvVJ&-gKME?>z(H5hm=mbmRRorlz|EBk2=|8asyqJmIu0Dyf$oRvTGqe zY#n4|_fYJ^4i)@_?8#=6o~G=}#_Y`g7R%P`&i3rj#<|4SXV50?(l+g2;q22^?bT*& zDH840hV9sv?N3SV*|zQ5#_hRz?cC<=-uCT9+3nvJ?&0q3+9vMfMsB<|?&N0f=BB;n zcJAnwZkvPd>9+3cHo5A??(ODomJ9Cg2Ji5OIqeqj^8Yq(wEOP!R`2yjF!E;a_l9q7 zaqsx1@A~ez^iJx2!#Dk&>c8$0`v&mbuI%F_*~bAts33qd9l74zr!kPK*& z6eg%;{|@jBr?=ELg>~@obvR8V5wmNR4>0!d9n4I4P;nLKj7AEjRP^HaNDvh#-Jn~< zaq8mWY-|nZ@r~nbD#+i&aka?&J^pi)AIJrGP#w~tKpga%a)t+VE(8)!@kSk4CO~jk zGDzX;muCR+O^EX5{7m2d%;Q@d`I8&~GD34`K+EX}Ccm39-##?&@jBNm&gO%aaIPW9 zp&~_GF;nrD$(R*i2Q8dqc&JVUB?m+YhcIp!KL5uiKT&ci>2Ps~^fWc257&~3iOolqHADrOH}bhw@gB}CUR)94IqUNf1$w>yiiPXL>Fv3=k*1{ZwWVt zC|@=EbO;h_!81%f>|z`H3<=06g(w%Q`|P0nb;WIQqe(A5Fo#zj3Jh|<-GQ)Slv;^5 zctoAfS$No>VhG|6XNDA#FmotaQE>D}R0g7G8?GC<`}_f2oUK4;wuPwPILKc?frsT= z33`y3E2zXkr`qe&El z5~~HMcB$UEJoaC9)1W+fK@9R&%&nh1893}bH4M(vs0fv0ka_xh+Y?zZq^5yN&sM+}e;VloYN??IsQ zNqI+D`GyHfAlD$~wGF2yu<%d2m4=7YKfXrM2C4Xc*OQed;jDHCJ9_7RQbonUs4!) zM2FJ52mF@Mdd0~7rOX;AIovt*CRs0hC5HrT=a7I(7GjSsyoY+?75Fp+8K-vx%QueQ zH;8~7ZO_;K%?ADUKpxy!g-)M>MgJe2zFAhK_XJ^gc4)_CBaDW(kFZBabx7bbp0|Ah z0f~;EnVl~MfEctP6+BsPI0YL*Vj;tY#o&n|xT=6Dc*XKSGxuYNIc|aU+~TysLq-`4 zZ^3g^h#WjLBU=?jHaB=Av|2Qq!9wr{9Pc=x z)q)Q%emwc|=Fg)~uYNsy%WdDro%>ab8!Zi+fCJi4jGH3gMaw5#eM_w-8i7%r0U8Yc zJ;m33Oh6dmS~{#Xpi~JmR8f5r-jLuA9<0^WfFC{x!$L(w7+@0?8hB!XKcIoeYwgik zqm4J>n4^w6^2j4wZowrNU1QbK8Xpt%D4qlaQK49pPeK``lv7e!rIlB@2V{Ihils;_ zdi;mwe5({FrkQ7=nWma+vbp1r@Cj)a17Ws#m6LVinWvt6^4aH^ZwASspMw%wsG)}< znrM`OvJt1EkA8(Dr2msrTB)UMGU_OlZj@Qp7J{ay#a~%bVc%M8Z52l#6rMI@ABfCh z;-$0FTC1%!W(ulVdT4XaGr2UW6-TC;6$~kodLRa2==IVHvQ{wwi!Pz*lGs|FR0Uiw z#9qd#46P(LQvf!ny}qMI&MxpIjXBI3B=hBD8zOQ%!DorNkxid^%oy=5U#t-n+q zAZ=(6Hl!PfQc1?doyQ%>z!utwyRf>UL3V(}|6bg&$A+>CRve>t#?1`}h_Ozvo)A;a zvOnBX3^A>|2;3?7Qq_tW7;hoLAf#-c^&E94!vnw;U0!c2+ zn47{W#khiuv;R>06`q({qB6Rw~urYhnIDMAk(hqhznCiPN?)Z%jK3TYm* zNE)dbE*8-^2)r1e2#BDDfKS1m1WG3XCwPMt@^YXy{D!Zaw1N-kTbyum$U!ji zN)fyg1p-_F4N9zo6Uq=tFw9X168^!3&!dD=bl^E;#LGmU;e-5`$hw^<(R;&NBOBfH zoF^#@{IBubCkW5lSh{ zANe3ejbJ5SDm2mb5by)zG{cO{h#*rwnZHH;;sB6jz}X6;N*R)X0e_%|_qz40;e1eU zfB}anEcvSgOcL5MuCor_p7~eqqy>P(vG#YmhhG@RD^d zQvxiD$~BHM$*wKM4T4O8`fm8cp-3f}jFV|Zx#0nF-oTvB;wkhtD$%X&E*8^b2se~r zv}&zJtDVv(U;B!yHy+^y|GdgznWx3q7-vp%l0_;7z(o%PGk%3Ej2;x!AnYJA82^yS zs44Wr6k>$0pgWaC0W^kywe*3cO<4s{9D5B;iUdC{ts@Paq6Bbgp|qx@q0x2<%d!$Q zs3cm8P>aZ_h9N+)OJP6`n6tWjAx#*uP$o7`#@eW)!f4@xD+ri#&CN0a9NKinH=|-y z<-&BX)I_UVS(zQ3&e8+ojO$z%@>1mna<6{fFMrF!Pr=5qEbk&wS0Mu-RYj{A8}Zdr zv>^)C1gi<`a3@rVf!kbQQlrn>h%#ulL3r^cfj4kM)=rU@Zv$Tq<+ zTE}lcn-DKLwS!D3Dw1@d1Fm^1Wg`o)aqq(%Dh!t+o?s9$a`756PJjv)EdRpvy7`M@ zc(H|1z3#fiF+LEP@CLvE5i)9VgH7~A!%_(Zh%xl8F$|$P(VAL&doe!AK!=bP)J%r1 z(FDGv7|94sTXFkq=tHOFzk?OaB1wch6Wt;j&4>nWkR$0<^dhU0jv4vRSl_3V!kLej zFDLfLxJgSITLx@~f`T^b2*x7Km%Vf_Oe732iCICtfCg|L+@M`g8rOf!9a4Q2O0sRn zsq4b>X*7ckhXzQJ)Bug8lT9>gwAjqc#$)&bQEL!bEw~PG=I&gL49EEjT%Z8~DW*FW zPGCEE*>fMSgTQW5i22Ox>xImsq3smF0vpn`pJ-4@qE<*c8mqGH2aIq+w$m0=I zy%(DSjQJ?&~=`^3wBcDBPk?sA_yRkTj`yW>6YdAGZnC<>K03>me`^HJ5e zqI0xRK~{QSJmdTBc8N0@8YiteR~WAHUxXB#U*3@(=#`eBS>5n{W?0r5Z~D`>hwtvP z;vnknsX{{q0h`PD=Cx8%j@^DFvV5)Wo#L-)1}7@Hr<1ao^8YMJJ}l{|Uq18e@_4Wy zIV$gh0m{0UMiNlMJ6a$`QI-l7()}V8dG*LG8n-XFWytBoT{O$s1{h3JZV2sm1?d!M zx1yY4bTJz#m?=StUf`Q>)J`d+)IxlMQ7y|;{NLSp6e5tqFKm|UR0~#p-`a@Z`Sk)N zlw11y&ZPdRj`x{^=-mdotF$K z98tu?H58p?^o9@(QUmo)rVYe11VRq>i~*`&k(rkgEf_E)qsT4*_r>fQ2*X|0wm-|a3v8DA{8Vc0-1ma zwylCAbQ>vT7)>EVQ6Pf^dW1{_g(>8L4k_OYQlbkMPgl&*b0ptsOqgc;&IYUvk{p{v zf!^}q76TMO5lsO*rGS$02X;tGsL|d((Uc8dp*PG#cwJ8l0AOJJ7_3CY1Q0{k@P~2m z%>=j?@WqlR@E)fT(}fI}Fxt-(6^t_#Oz<%U9y&}aibhjCqkWOqhHYLg9srfO1W`Z( zeyzhWRbo53qsqmg5X>Oh)SwiOnvdGVqrPLP8P0P3p*?1Nd zE)VYw6@keFz2rhEu-Grk3khhJMzxGERU?!jPQXWUR~oJc zl4!y4=!!()N0Tt%6a`E;a@V><V#T_Ck_!*U1`F5wluoF!SR zWsbqak+9Zck(O5Vj4-$uYDU>)aivOj&;Lqp0AJ$af<0j=g{HaKRbI^{Lj6a>m;@Ej zSXq(CSIP>E*c>TA=6EijWXg#|Ii+O$3STagM?Gi4Xv4f9p)E;HcFv0@QcHY#i)U0B zY`mJLX$E*LVl6BgmgT|;4Jd9fhFvKkZK&7Rq{6ca17}1iGHAmE&X5)|1Nji>RYIVL zVq2NnqkO)Ig+dgW`He|EgE>f=sL+lw%#Wlz2Da_Y7#Q1zh8}t9XxuT1KS3VELEUhz zrnq?Bsn{ZsmWXX#&<(_#Q-}(Y!dYRw28p}~leP#4{8=j=h?DAzfLPqR^jww}>4^|0 zbRwylipniUh`;1OlA)&J?P#1Xp8x8}Db!`02Tfv~>M7mP>7HiXCXA_{3aZ}q>7W|w zp(3i1!bM#8Xremmqb6#hLh7Va>SIbOSqvXM_6X(GMf8P7%$;eaifScZYFNNb_~d{= zC?C;fs*&YrSB&L6R)x~226+76-Z&6gtcrQ?!IzpGu8nH2I^LoNmaGa(YwV1K`OQ@h z-xzp@@eK^J0N-iZ-X?~LS9PiHsgb|n60}}xt2V@CGK9oDtG7-CvQQbPx)QjC>$oBm zwVLa;a?v;9UcIubu;MGMKHpaq!I*}ZoQ0QEW|z)*)Zh&f{z&4!$buibL<)T1!kUQu zC5I_cWdA|zx~xJiq=Knvf&UW4LIBbP(M2QJ5U5G$0HD64eW=R+r6}ucrI&1MVT@25 zXe?j+-zhZde!v1LxFy1tD`fdm2%c=ou7%xrU@VXW2tMo3x*_&)&_p^d*>nzLaY9~I zs3{b}!q88<;Rb?56tK<#c~#m2UMSA8j=8wOC!QGp6k94@lR zf;Ir4`Pr4}8fGq74gZ+@VbUfnGKg-j$|un+hSPcHaRLqHL?SqZgxaQCDmV+c_$D$c zZYpfTEeyfXI@ppqfkDY= zoE^)=%*017L^DasGnB$;PiE5WU&*dNEJ04m~nR43VCEo zDKJCEC<*i+^pL|gx~z=gP}V{#ENm1j1;+`$Q*|+`HqO}2#0<02MY<~1S9UO&Eh4j4 zW$jq&HsL^NSFn8k6n#dY193pG5!+k z{xXi2Sr$%O(Et0GAs@|A_~4ZG{4AzP$}z!|wG3N}`Xfj&FXt>zNrEx*Ii({MAa>61 zPu!<{5Dh$7%Rqn?XF{PaBFTBB?=mLEeHP|PK_gU~cjTaB&rjk0FVJq>(7VJsZfT@iH48-u@`L_#JT_ zN%?uh?F+9- z0bONJy6&)KGFeU4zBuH+uoaiR)2GRT6$diZiPb7=Z2^9gf=p2sOVLlk0!I?&cT`nq zQ1N+Ta{o4ZrawQ`FdI`o+9?%u&?GbSN#Bn%g9j$3%!ySq(QGH{2;#GJ^IHPN8IVH* z^UXrY&h7m3DR|z9G1ZaiK@_dC34@+w=;mFHqe-N$607fI?O;%(C@co-O9tN*9Mcsp z>FcfrpQYcN3^cL?6X>J>Bqt?bsP#)15%TKNL-TI}Iw?fP(>W8!MSHXTt1M#n?h^)Gaf6FNsSS=t)ZUzht$Fuezz0FIfnX*&O!Lwn5L6@RG_nR*Z z^8aR#GmHv_AG=$hA>@oinrN5n%{0SJ22=`TWYfOWWjCa7{z94X%TF#0G}Lck@ER=2 zv_lbtw}|$+UB_sP=tSK|Nm4^>x%af?sbbr&d76cU=nH@N7?cKSseq=6q`)211laj1 zXeu6kY=OjiNMSRGVSA~s+E!&vDSvAejGQToc=zDBNGWyQg*3QY3wW9WNQHBXkqst? zqZWrCDM%tnAIvgfc3^Sw1z}K)4_T63Fm3nhcZN8A6M2@b@iXsJ5V;S9zv_*^ z6QwvoD?oOUQ+c20caq>w?L_mHEa83EN&6s8m5X_vT=|%r`I$dTnWOodv$>VO(f=A~ zN}J31oa>32V=53QN~lfw1tFoujm!o9e(;Y~f`b zv;Cc(bDZpankSSpN&dE%8y^5hl6LJ*(MXqN*$vJ3ObUWXA;@)AJYX<vo=eC$Z%on>1TY_x@mA%>h8ngJY!?(UEnx;sT01cad* z=}zeo0qO4UPHAahT0#&I5J8;dxvujs&i)P0hi9+7*18eYmjsTo8OWtO>YDM~YkN*# zxWV4_mYd*bS|LiwRINkm$2N(2;S=G(^x6uFry@Fuf&CdwBk~vegkg@K=MCp!`=PkU zDH(?0s3*lOv7@kQg`hg|A!bL(*oS!-`xX?*l}3nuDVf$)P|M)gsVNkhEfc=q_j$}F zTQcPwUWuK^Ge$8H#`4S~5{e9`+IcMkxtnsti}-2tq#LsWbHeTA>Z`ajAH<#Dh)tmV^UU0PD-@CLoeLFhByjIA|4rGT;twEFdH`a&Oou-qC?057AF{ZPW+<+Om<%DcG7X(`PtHMZ5omzx3Cqf{R4vPnS;wLN)>iOnpfHb|hM0 zdz4rH7#ogjU&F|_k^_RW5VOUvI^|%(Bhn~{Z6lj>=u6T#t;RFN((N_!Mcj{9y3!rA zKE4dYCKJzKo&T5EXACBZj3fBiHTo*yCuA=rYRKd0O{c6eM<&GM)F~EwJB4(`oNgsr z1Kws0aC@_?5VspGyB%NVaie3yW{-7XIkTs2+~RO7tZ`K&56#PtPnC*b)+9g~L{^2f zN&CgGs{wO_KAJjo<_b#_VC=nBi25;ZA4j5F((b{@)EvCVwrU688p^o33xflH{n(qxe)%yZ zI8P~)`|a7!m`_!lUCZoU^_4XWoH4z8_*`d}Cyr&^XcA*kBYo>(us!x>Y{tO%rD&Lw z^3-0mgS1X!lR$fbGBIT(tJcUY_qj_8CJXNo4=qA{2&NprSl1RAa;-WMAsmKXjQI%r z7Cq?Wtey5>V}c?D5&kfk*|_qLPBAnjtdNxZ$Z1 zUuJ9YlAbd>7yG4te_x(|iB71mU3{e%&sOpE077rASxf|RbrpqW?lY&BQC55XwyU8Z zB@OM&1~^``CfzD)>nhiI9H4Q51k&wu4_2W%Lpa%`G=gh3kiIm zjSrH7DsW1LjyxhZgr9@KLsu71NG_9f949CPJ@DzazIaZQl^hWsA(J;Bg?rPaLK`izaoYxZqyJc7l=qOe{>zt3@3x4(eK*y%bNr<}& zb&%$hB=)r_dVO=hn#f$096@`#izA!ET1r{%PcJzS#EFt?II?w}I#cP=Ungsw{IEX(wyYF4nL3PBZe2JS-z$hbE+*f&(F199l~h6*rNUOqh}%vz4oGwcg^-wS za{mEJ^b&aEO2})_Uej2j;~QYjf>mhGpt01r{VpYywHpXbEBE+~F~w4|Rb@WrJaQ2v z9MlZLDoHg>09B1CV{IU=CpYU$h7Cx|a!@N=}=`xyO7?vNtc2q7zPGfVj zFTR@jyB#XoNHpHO8@dy(x+p^~dtXhuge6g;9N%a?!AEW7LZ>U7V;bw*u0|~f8s6H% z9y0{@N}=gro)fC_Wi<7*aprH!m}Bwyv4m<&zR>3&zih;WIv4-g`W{cc?KDDj@;o&E zr=9D2;+NO^q1qd}#q2s>tAP-S*;y1T7Nc35A~9}G17y%Txke|VIg{fbf)EOLIt`^E z!gLn6ZC?C)Z>aG}?A(Hi=7?3IbIbvY15HT}SIK2VthqDV852F_wWCP@#g$4hEU(qkctDm_ChmjZo3|V} z)2$4*T)O75Q(sHOY#Kj)6d*+8`RDV#_i$Bp%Rd;sX!mexT49dd=9W_3 zBy@>PGUIGgX8$-H!?PAInkE_elB5#q9qigc*27Y`HdKdq_LHA**QxHI7|#_&Zp#Geb55jKC7QCzKi~FK}0L@f(l0M zA}Dp??e?_e_UI!G=Q;EHB{6aL*LR56A}NgJ&mMSQVPvcH`&*ZweR{3Av3Ue>25fZA zq!aJwDxg8~1s<8M)h|kvj`anN+1N$=P0u@aI7MCzzxF=$Kk4`;DF-LMGeov!Z7UM~ zMI!m{JyY^XCLK!|q))b;{N+nRhf=XB+JH8CSt*%1L zTtEZ~V4ax!;r)&%J<+Y|OOwh9H;K3d|Idp_0NE3Sgxi zVkzZEiJBN|tjP(FKV4(wdyHi?$6vi}Ll{~FhV+lhV>J4y>~MK_Nj*?l$zt#uRV*g8 zUwK7pxyY9z@&(j!WQH2t+!`;s98X?@%5wZybtzSzre{tCZz2-o290J`dw{N1iM!%e z$%46LC}pFqCp=ZsID#Mr+itKC1|N2dm8A$T!slb8w4MlTEvAGh8Qj3|5@f$^rxF=q z0Q;^e9v6taKn@x7zmCiB(ZOd6ym)F>9HIw1C02Od-26oc0tg+YSO!TN^z+U=OJ1;i zVWV~B0`w4xGsJxryVsN*#GE_oIe4MhK%=)OE-znUBbC+GV)GR+3&}eN6ZaLw@0i=;NnxxDuA97J{Js z+S*Fo5%tN;3xJsBhC<|kRGFAqEM22k zL1vN!UDsx%I!(}q|D~oU?FIvbzP+q97N?Ue8Xg)ygZ`9{p1|%rtI0CbS^+<-1>R8i zd;C2m`{k=%cf|?02hc7d@VYVcpb0LmAY}B*Wf=4{SoQe*^;wmy%U!;c77D77 zyqCiXSKuSP8b84`MiY8{0`F&}R-6O3hLIKm%!G#?Rf~sRJlA@WI{qzkDe|J`_FaB0 zI%Ecwnz1YnHS(J1kXx0Jf3=t65lk7yv?Ivb0whPtkg~z5+RFEZNNx@$C;gPqI__;2 zn>Q4@oa9S3WXPMv#$wW?a6L9MB)cp>C$YOnI&eONXEf?Aub6vsS?2eV#{BTw=UL^n zFr6$XKkkZg+X#qd7~gMwQi3`yE<#&01MR?Yl4+fqb~)n5>)XR>(m|A`l4>j|qJlqX zcd9b<32juQRSt(yU`4o?ds@}2Icd$Z%qK|Tb=sKRy+@tP(mV1Ca^uyy+ZI(^0bXYRl z$!)x2CT9=cd+;kCM=?uFvmtZkRfmdp`g>h+*D_0B$?KgqL6n#L(WmRtCC8bp@?rhk ztVNgQb{B#8w|iL+Ukz~xG2ZsH-fok}+@HC;a(eq)_+sJ9E46({E@kIMGa5%KmO_A==3 ze&y>e`^($T*XISR&zoO9?!NC_M11|%yjdE2!zgZyMf~E7yka{1(tdfQi1_EOy~^(J zFV3d;C=yU*bYI>P(Ddu7UL>$4oevzOl3Kv2qZLN-=Ro`hJ-{75JueK<( zK|?ivkVK$g!PgY;$2+8}fATXkzeD%SxTl~m81xV$W@A_tn1QbigUWcI-g|oo$4lN_B8EbN|0kcc|QxL#k0WD*vIG-}3A_O8= zM>qL~B)ph-y+T6aoQ8G~Hm1hfx|IP#`XMYI8}0)otA>D3nby;$*?l zGKiy&PLrlrtvjaZ2p1n8m6plh68}OS91A&BkMP#PlA!}Nkk;IR##Y_RQ)Bz=2rtjGwON0W__yaykZKjGpep2X8kqao*5D&-6 zE4uz&YVw$-+FfQPo@60j{)RfSzbfvEr@}j+!oRyB_^~2PyfP}FGOoKa>9I0Ryec!G zD!04p!(&ykcy(DobyatD-D7o=cuiYCO;=u(iC(I1H%>oyw+8@c_OW(Rylyq1Zlk+y z=do^2y#6Sl{`r_02#q|v`zT?|EkF-N=7A{?xPvYv-F|S>WsY*wuIa z6crNK1&r;#cJPAjm#%g5~dxtr~RQb zagv?BTN7&AGjC{S=_MzgpC;ylMi#e67z$W8Yno09XiIzMoO?$OBzmF(Lvkk4uK!Hm znfHSoXEC1_4E_vdTF%cw7imBI!u^9w`8@KXZ;6do#`Xc1@xww5Vw!7bsi~k-Ov(~| zzs&WDn$~=oh1P>+YX$Qe%LoS`7`!|{^iD3;nR3*M~i+pK%uY?9h)3*O4z2{S-`G3xXA?YceHw>|f~y(qP_ z8oaa7x3lxSvnTcSDERAX-`9)huh%H4|L%hS``!27^Yee`(z{q8yTJb4i|5dx;FVXw zdpMGN_<4Ku(t)`FIx#*T@L&|tU4l-@_-@G~y3pp70u>V5(P~#PrTks;s*F%MV ztnoqoe%Q82$g#xVqt_uvZ|L@Qq>t^SKR)}rDk)wQ?Bv`QO;0Hhqe*>pF8l_h75;&B zd&Nk;{ri_bx&4UsS%Z?eEq5_dB`md`r~7w60R8F4Z<|vF9G1!x*}pa`EK_6iY=3!# zPdYRj3tz5ZdB=Zaj$K~OM4S|Gtu&?<=) zyTX?=dnb59zQ+{zkaw)TU#=6jhfu2ES<&o*trI;w+?S~yNbi8{h>{oAKSsiJ z(a_((iA{sONxno@qe3}8E<{;{i2O%PU;OI>&PyS3$%Ms8bel(((2q}W^1)ViV>b?Z z`m+Tx*g^pwqQsI!IX=&iwNh!eNY9a6Sf^Zj21ZF2&#Bs(3ZNDVQXM;8N02h{`Qufa zPc@g#ymTYSj#sf>P>ygPwak#Ub0A`2*SR;J(m4W&IEJL!z88qnDHY42lVXTC0Y$tc zARnq4V<8h!Z`D3JYcA9N_#zX9EoB3~$cNF+;bsgP{uZxJdmkD>}Hm&P?E$-@lI!HmuuAJy{dqjm~0y5Cc*?xJxxpi81jYY_wKO2$lS35d_AOfQ-KRx3PJ$X&R(N(`CA3+O~&T516 z46(W2@Ww%L~460h^Yn$IjMa?v8(iV7mi9A9iD$lBIs#cS3Ix4JBpHm`TUAr9EER#QW*rQ~= z5uRd`omuyBe<^5hSvKi;Jt0=pjk7h3+-WHxjxK}t)p37yQQbRQG787S4^pow)9+J~ zRCN;3XhMw;ZT@(-$R^S#x-e5%^Q*gLLLjSz07=tF@Lh3;O7?rxDCA>_XMcTS0~UYK zZgwE$D4L#&i_EQ_@gLy@yv(Xna+aSm+>%Yr7NB9fg-~pMi{%FW6?H`-L8>`J?3M}h zNT9mfjAiLc+9zNfo(nqwE+@pg_d$g?gEEq6UO%;Zj#t?`Z&R+6G?LN+$YIa6eIz;r z%)MLSX7gWeG!(6qGHGJbaH6@=%pn7bMUn7lSY20-uneGU5?hg)?;P6;9ilV4_coi8 z^Q`tyZCoe&A!yY?D!LYwo!IAw6RMIKSTA0k`|M&;(C3C{ytFI#i4S9JRA!OL6>KoK zu|nZCe;4kN6)sP6GAX=%A5-`l#$&Oa_v%H8T=;nxf8bW2{Cm+iQ!7|_LFeo{y(^ym zv0^2*7-n%;*CYcLphO{%8KWYp4govqr?=it8>-TGGI-RiIqs@3CL&h;E?Fev%bzhA z^B{m&D3yt>)dVGcHK!-KV3X@HtSCBJ-Dz1}s2^G`mAR@2luAp+=hFkt3AEARrlsmY zp&Qtlu+}n}_>sMQ?i7Iuy26A6<=(J$ItCr-?@?MZn6$)e>+|p7pHrPZ&1qj`Ph@p_ z=@kcB9CJG~=o!4vx)v;jxUx(mzEt_~WmYw~OvviOJ<2Oh?P#eO3fHcq*}2JBcJarCX#=q&-N+iQ<58kO=c)8>PO$d3C%EMK^>hoeVx9P<%I>rare63?CB zkwiAd1%{0%teH=SkAD-j21Vr)6{O%|)eO*#CKljCq^XLw@?%f`%l7S$40$vSYI4bm z$jH$X<|-O1ZeZmt=N@prpeCt1^2}A!a8#UX)n>WCD{*14h3qjdgwP!ZzQxQpm@-aJ zDZjLijz1{LI2=!Z)Jo=fb*5IKwzDIeSgUf!@TmNz_te}qkt>eYd3&Aw`^~NQ%WiHT z1~#4xdy2FMd}(0zVO{<8_Z3i!w9gDKb>%+hQa5 zuV9Pcez}I3y)pEQ3sY>ai;33T*38gl1-j43gv8qz>b^<^UsJiF@Kia=!d+}WM(l*% zlW^!O#0F0QII(*;dZJ?;FG@!Aqb_oNBvyN2Dq1Wp{?HqPKrfo#mA=HQIh{n|9?9i} z-u2Z2FY)D{`m_v3ZGMtC;$fl5!sz#kwi|39Kl$R~Va~06Y2UcW87o-c;{x;_?gxo| zFt+bi#`{A%&aps1@j=+ZAg?@eJlNAi3gQTp9V^|ItNKBTZeOC((OpE;cCBatLWsHb zE&oxg!l9|ZH-9upo=xw%g=+Ulx_?I1rEW^^Sv^uTE z!sxLcGM&d;OX<8Y()80S=AL$Sry#kQ|$RgHTYu2+HA@Rjg4;cEM2&p;ZE zz*lz8JsJ7p4jn*tp)?D@l((U%H@#p@T`k`U4G7wh{LdN(S5y!_dE1 zv09Rqzu;$!tY5Fl7JI(D(E2@X_V*@dzV}!p#q)Du;bp+Lr*BGHk4yP~Z!5L>&iG#V zKR){1HKz1^chGv;+zPo}%nAy3oqziJ^!L{wst=7!;7{{>6w}y*@lC>OlLLv5-&=pm zo}@QE?V6z;*XR51-+lRgGSNa>XGznDT%kkfGoX~>2U|j8hK(a%y9OZ@IHeh*^D~D} zu&*QQ_Cffs{$lhRvjl93gaV91v6Ta6zP*GZjDe!k_>@#byLhw^tL0IG(nh?KZ|Kl3 z_^UBN8Fw<|x9WJTwjeLK%^n|fli{TjY;3`Wx{_dh!Pvb_4aG$syu22MlM^A{86nB) zBswbp?x#l|y2IBS;Cv`<#I9536+MBEfu}Z5DJRm_8X02%`ZE-Y5XRn2jl>Ga<&UMA zYh_)~1~C2Pr;PTkr-W&)QXNp(JkW;@&icug32BOV2s!=#tDKe z37mED3LE+KcQu*!fufZrCnG>Y8}(UBwyIQP|7#rl2b={NI|)T)#V^4K`5K#XV+B1zWCn|x@a6DDuTqec6Z(Vrt51X) zuadP)!VsSbv+rJ(xu?$4hs*0G;Oh{mE~TSHyrcOmP4YT& z;{5PUvY=jQJi;$tS7A!|C;e%bHuvz#XT?`xOO|63sVIus_8`>YmD3F4Y1fS}0vf9$ zjR@crDR!WhK^&|>-Ysz6*l7;rIvur4cFur3qJ*_(869tu=bQ7w43x~(DN@3hi;zYOfQjk~-Y5(|$pil@Z@jo7{6dzXB=$pr zw+Sr*jt=~1b-6XQ7@=bz@RuSk zRj$(+#7_LvW?YGRw{N;gxtD2$Pe27vX5b>c%pyL*@3A73x-vYT_qN&hPB%9quQI;7 zGPjC0iaOwQh%R=zGQ+eg>oF$zvGSFwRY*Wp;bT>iQ*g}?cV2f@8he#KWp#CTb!~h= zJw2e*w7NW?I#AVvlEu3-uco`ZrgyWZ|FLF}x^`H+cGR?XJfLe;iPMl2?D$UH^Tv{_?T@in`%O zyy4cg;Xa_@A+O=FyW!7f!{5gSG-xA+MB`mIrvOg<_GVo$9u{&FlPJ)G3)BQjz#uhi zf+3qI^P8x9nrOC~=$@Kj&}O(qGox8Eb6_)TelvSdGv`(__fs=3w1r=yMbNB8IIsmb zwMD$AMRKc!_Pj+Vp+yeA6|sfR%+W+V(-@rBxEgJ^SKW%FZc{=^>zlP12DTaJx0&{| znQygOJhfRu+pQ(qZOz*41KS<*+nsvaUAEd?pW2bo4v&O3k{Jx2zz)Cs4xgvSpmTdr zO^5P%N3cX^7Cye#?h$)ttq1hq0#>aVBw%OpmC!88yIF1MhvE2 z3m_DQ4Dq2QrDk^Fb6R3na4Lvgr2OBK|J1r9*t)d65 zPB@&I6VAfN$i~HJmBeUU!ovmlCCb6V#=*hKq3XuTC(Ok! z&Z8p3CnUko$;U4!Bq$~==$R=jE-NO$Bql5@rl=<_Ehnz$EFmr-DJ&`_s~|0>B5md) zD=jT2kC0bXRaDVXRMkl&Ks=^5y^ z9T=FH8<|>}7#f-w8JidznZB|%?Y%HFHGO4e`^x5xrJjV928)%2h1F|IE6dkbHg-1l zPWEqH?48};n903yadmd}c#HfOnD%n@^mq64@w8R+^a=3x^Y!r$@(m304-5`;(FzO+ z3krP~5*8T}5f|nn9TpN2{w^#cIzBQyB04THIzBBfAt~NVDIxiNa$06udRBT?URLgh z+;`%+1%>&AC56SMg(X$RpUOX#SC?1SRoB+n#46U*H`h0{Hnq05r0cY_ceQu*boci4 z^bhq9jSf}#4v&tH6ljl>x{MZEj*d@_Pt8ovex99QoL^jCT3KEBvi@a#bG_AbV{2#o z>+a6(e_wa^_YaQ_`u&f-oqp@K`*wPEa`Nr$`^CkN>mOsjKdx_n&W8THy}N(-{WRzC zXW<}1X*2)Xv!jTVPKY`bB@x~&f6!Sn~Uj{2P*ts7H+kB)}_ z20}6M>C`(L_o|s7FaA%UVNfgN)a5*y$q{h=I@{IqZN5l8fli~l^>n#X?_ZZjciZ_| zqs>gAM$fov+uuYh4(P z^<`KbNkF~O`g=K-%q*&@^#&bYZA8yqALikg^ylW`=Xtg>cK!7QWHB@?IGWz$zYtvJ z%70-+T!r^U5ZTVfcc8MXjIgN1j@@V)A7!d2I+IB-9bTGaS{!9@PAcuIs#qMjwYudF zb!a1NY7*UX9VCS)`44tK2vqWzHq-Vk*%8 z?gXa-fm3NwJDG3nNgxJnY4JB;_yjIMHf~u?P0rRj(}~}9S;5=-^xMZWbUqywU1f#i zT>F5?Pf0FBo#nd0t}7=T&XPrYNuVPt2Us4)*=a@o8bR=Z-2cipN6TOu)!La_ zohYDgIwR9*bs@1g-dTOGDs$yKn0v)pBL;J;?T7Z{1vYp`v+0IPnqg#dL7$;>T?3!p zLRuA7ADT`OPbRq3GrJ-6aHzujfq)p+2#4p~`8T04Wg^AX_Y{@l$HTJ()c zQ5FHlawlg}!T_R#zh8~$V-Z}Bk{WStVv8ErUXRnrN4rnJVmLisLx?k0CRrpw3gWLpQoTl@RsKNG>wexk@_=K(r``}bjnk?ZfH9EbYf#|6HZzrPj5a6cZQNxB9! z2|KRJ63s{<-MT}=^ZmB(kLO>zF+6`C4zu0*e+%HQ3@PX#$)2CCUVi^;oI6xiXe2%1 z%@IJSz(Ml*8s9Rdm4Bdv@`5Y=L>{9tG#<*x_^u&!@sY7inu^{w9Ku;}&z|!E2L!+a zYR3ZTD^t~iI=;eS7$xpLmcZn0grjIUz*D<3On=b?4S`FcS~N-u8HM zgbSyQ0uRGTqOK6Mpf|B9E^^rXst6_wKKXD;Ymzw1L(p>!NDvPIiXX*=EB-s7fY39` z1k0LOi*do0s<2GZusld7G`DM zM6IJ4;Hj}B+On{Zym%;;5>h4OeZXqA9G|5=?Bk#u1|%&8ASnaEYyzlE-JwzV!7ILI zSm`o_D-x(%5&^>wIq-J%!U=?`J~=L6iiKf&awFa+KLB1>bjGykGLbkr4vG}NSxO%! z#+2~F=-Xb3=yw{;-ZPydus+j(DSOvAlX4(97ga?ky+7!c?Xz%qM-^l1Lp=S8@mCg9 zIRrCFv?x|mgvs-cPg>1d$}u{sPG=67%R($}^y1YTcnVcgQ^VzJ5@LuFPb;660~ zKL-re!O<>ijw-iGSK#u=+&4PyNbHkQS zJP1qS2OOhmE`~s~Gj8VGg6Wg_dvm$AN5M*%(*r0~(WmNfA_=?v6ix%XVd6(7BMkvy zB zQQKFnclphb-uxeQ{a594^kK4>WLg}R`SFMGQk2f?zj*prjmJW-{q0RGOlW@0I=loo z1_xipXJTrcbp7!kiPCH>F|xt@r%x=Vaud2UXq>8=nKE7OP-yYYo#yKQ5chZ20GtjLWtwMM54BUxDpPWVhO_wui11e0yS?%o+zruFWuWF1LMQXS+r9RUJ>CNeb;%-qyVK)!M(H>_lfrW#8b zs)-8@rTrQKc7ZO>THG<;catu&bQbk~MJb*rhNc3cVtxQ@(b_Kg!qjl`e_Lv=sOZNh zSMM%x5{BcE`_Dcow@w)ow~UW$Vz7CsRuL{%E+#qba?hl@X=2H?NEtK%_3?_$6RRnj=#q8pz}e_>aRQ67terF0QC#+Rm=o!0m&aA@eA{ z%-focJ|K=(?|p4DrQjC6`P-5nW}w=e=`1&yahzdlA`8Fn6`SygyYSw@Q#$kpF-?O2 z*GQ9m3tTU)hYQ~6g{FCCTms~@8B1UrKs=3v5uU&t}3unS!$2BU{nnLz2-6xDY5M^_5MMTZv`^X) zPNBEL6@0+5Lin{~rBQpmhNmS3g@-rmrovPxXyPCn^8cL1C zF;JxoP9+NYr;DQAK>TvjB{$N)@2AU9Wso(c(+YUW`DY}_X3&fPI{!6ZsZdH3XH<<- z`P@gb%Obr)T{(1shW-$>)l4m_Hxk8mEbgTC2GDdggjH9T8($Wh9);>1xKL2}enxtwAYScaDExxr2j@5fYprtWfbs-jh*Q^~+qBBYAixayg!1r;Z+t2XkN zT65i-oE1@uWZ?jr=@as%l@FdpDFTb$QKuhZz(VyjmhRygL;8XiTu5Y<^Y5yUPgD6n z#K6$Tmnh)J1%}K&ijGciiU`;}5}4l-QWwKJiXf)NS)p`*SqW9mDFuN)=BQy?TXjaV3Z#Q)bGUCpE)t@ch_Hv)BSV7+&t3! zGs4{Cx%|p&c+hEh+-ztZBKdRHfYC!O0dB+$Y%JGl#O-Npm&FCq87n2cNEk`k?KLud_1QA|K9xvVH+tJNZ}MHAX4OW!J9-KrbdHsI7|+|y== z-e&&PrnA>(DbcP@-)f;^Qp%>Q?0L=~D>Ok;y zg!FXqqIHJVbVQnU`UZB!s&&Nobh>SICik?bLc1I!x-ul%vjV%!&fD{9ItsSBe4o0C z@H_m4x?}UZ%AswQ`P~{l-L(>}^-tXr(4J=JmR7SK{=lBjt>*5Y9=5HXezWF5XfI5n zcQm2t#=qA*p?O-ZeRiwY@~L;Br&mX;Z_%vpOG4KO5W)chl)k5uKo!x+gZek}m3X1_ zOvP9fe1PaCjJzwR1|(jRcK;G%H-;>Zh$k(2Alfw)8+U=BE_8q?jYeaypG1|HN@d`O zXFuc`SG;K8Clrh!39^ELYI#9AFc8%?Zj$y820F0zs{cfEz_by4d1&ZNfL3N{h(Z>J z1UXFWB6Y?AqEn}B01Z(FjbN99yg)#p4(m@2diL6pRX)LogptD;Y#IV)Jas+-0`Pwc zXfA_zxHjNa-T?{%AOIZ*pT(ow#-&dj!E3_IL*g|Q0h4&qP0{d-X7N%jhp(N7sCq&A zE<+gZxOIbLwID1kEId%!L=kcb7lu_aI8H$THmx0LK!E@ymXmP=^j4gB-}6D1oOtvu zE3kq;BZtq7fIK;rt@jpK!aLL|pWBqw9cl|Gw;0~#mjSMY)e=!+ULdCaF@d>?5j z!b;g4q&8>GE1E0Z=27D7kU-*M7ULMGBdS6NDv;CU-v>-Xr%905b~KaBKR<)Pz^UG` zDDz2)Ud)_c(BE3jJP?o)gjK)^%7YC9uIGW^L1G#9?d`dh*=pS4&#P$w(!S}~#Ko_F za0zG^k_eWLzVld+Ph;>+Q(BD|p-tVokXE2%mUOP>B?sq0*SHX=`KI1!03rBhdyaT; z!R1QMF$hWY45}@{A^R{(5j-tW3o4sk1nn*=B`x904p;t}yOYE{kmzUA;N#rt^DbQl zhE1e^2HtEBDD2>=Sgv66u815&;w>&V`>JM&J=$wMywAbz!Y^uL{UG;W6NOIyje;shWg;P1-RJSLg{jO_I8@S++q98yOW!C%QzKgSGWJ7|lN6?`Qw#*`xX@~h^v z>EPErjghjsk)N2LKr z#;%p7{YS{nBa6>vmUPDg?0vTVjpqEv4*eze{l_>7eXhyJfjW%E?w{7IzZHgm8>%7| zEBVHP|BX?gZs9Q^+(L@Rs*VGd_EPbTG@-vQA%Hqoni7%vZfczW#WjGHj^xvLN1TOV zSeb0};>l;+lY%#Ew0jt+WB-`H+pDVXDiXC4c@XHmpYVvk#*n;}^%;=|lDi0FBkZj8 zQQlzdBf|Ea`qmH46SDvZS?DKw;z(w%iX>Zb!l7yB)hzEzMt1xqWx~^(sb4AFY*(3( zo0cvNj1~MZ#J<#mVx0UPmis4r-YV+rQX^OlP#(RyySzHf9Fcc=e_A1aTY4Rfb&BCs zRSl`JR|{*xs>PPq!SfF1v;XaETgb#`Zh1jPZ#aDX4ZY!fy4H@`15l$_MxKU+1VAi( zlR#E37$1OV7(07(GC%qx`St4WtR(LK{Hz4*Z24v&Fv7Rli2VmX-{7 z4GsI);8f9jsf#vx&6-ldBrfM=d-W24O`RZk&s|g3_HH=Sf*V zU)2tf=bmZ24SUKe_9q)SQ6)V+fy>DQ{xB}45)7OZRyb`E#d05B*RU&241n)P&a#sq z@aDN?SU>pGa3fJ@w@QoWygaufDWu$kcOegVFVvW9TTKLp$aV)#zj%LgCpP#< zW21g*Fu}=fh7%c*c^ifXRv0kaDi$C-<+r?t&LzJOYcxtk>Xj3_aD?=U2!_i^oY}!r z0FLTx+wUNyaKBOIXhO6I?KbK_8g3k-bDPGxJN^<@CVHitv%Xa9KS`V~;%TlYk6Hyi z>N1LImu8IOVeo0@hFg5RrAPjj*ghB4Wkr_a>86=e3(PtMd#1CeBL6bpZG|lUDnv{E zSsk?^G(!IT9icRZXjNAB%x@L>3a2H|){wZnU7kM%cc>TyoYI4wB4J!w56qM&`oI|)Luowpy=$Uel1>uVYQ zv#nL*UCa!8&-5Q3lyl3JiM$2E`%^!%)X`c{J1&z!w~&s@ZQf+3vZ-8r#58 z*e?3KwQhhX;w=Q1S6y$G@MeYVAo{&Kd&)m-QGq4_ft4$d_NRB&Dyozp>9o$ayX8@p zl|m=BYKhN2x9{!Cz@$A!mLp%@3JOa_b?)tuo^dIkVuxeK5Y_%X1uV@@7Hc)vJK{>m zyj&3|f1gD6toR%?+j z(Hv{Dlo>f<;^>j{;%!1RmYVOBF9|Ee{M6(-<){8?r>}dtYjTE#IH1ua<`=7d?ia-^ zyfE2kSB_0o`Kdz;fl$(uTf#4D^9Mw1P$hU!>cwf7T)j!jzlf&D(;~Y?2nWBY99c2) zLkw~J{}6W9Pf@<%qsNH_mS*W@S!(I-?v(CsNu@zTmt}z^r9--;5or*RZjkP7kQNaU z;rRK^nK|c&Gjsljd!D)Hxv%T>Zlc0N0lw1Q)t9uH4zneRHW{TyGxYUhs0|dMNDlV(Y%KoIx7JeM84SW}(@C z$!Q>-x85Yut>S+jhN&OF_e7D!)8=hx7^>IbiO_Q9G=X9L=dmA zH5&0fY@ELBjdfD)6H+v!u2D;ua+*C`*0jYm$m(Z4Y>qP3a%45g9(Q!EOkB}&Q8D;1 zSL(d=*6fAHv$H|Yn&F|KiLmCYD1+QR=JGg~$Z;$0hFk^9my#Rokk{P?aqIhKQs-}U z-dh?N{Tz5H`}g-Z7o%~_r|KOJ_W_-_o>yku02f6%H;$>IDqqqZGTc2s4rXdnmKcqG zMd^o`V=89MWPv=+f&~)9Ccgxp8OPu zvs5#1-90tgz?uEn?XQcM?rwD+JYBzO^eN<^x^Z{aqzBWuzWL$1Eq;jUK#I_(7$*-0 zy11ob731fI)Y?^h_7JlPXXAhm2Ock_Elj^f88?MtoV&aTS^b1V+FZTCN^eT@8QE>z z(&%}zx~gHZowC<&u4@*g+eyfl<)mnL%i!yG z{$)1zHx!NHKQBpNVr<{xWEsQwl+aY*IA-mOV1`4%Id*T(@*kQvoQcOW6m9y}sDRx4ZFX?Axc7MFj@sb12QPJ0R8WXChw- z<^CXbm9ki6(^3&MAP;lVEdVjeT;P_4-5ehYD9vG7f5;yXjW%K{X3R(VR(uX~V~b2< zj5G#-ZtAgB+gInA&3@{qH$&rQmr-JS&{6SjB84mLrR}-Dye^pNCCfs zb!WPas%vn4F*prW$OKoAlnoq|YE3xh+(td^c4@6d21_NX&;gP^{c4D&c`t-7wnVR-qO=e;tt~>(kMjsdmK`q8Z=O5& zS(d0zqT`f2m74bVC~369uwL2aM2TDx+pj9!mYK>l1z$y- z<;8DhRn%dQcomMOY`5DA4=Ghu93FoU)j-3Xfe_UI&#LD?j!xf0`k@sCJ=K1bQEO~MXWy_fWT|kwiM*Sm$D_;Defudoj z*k5?3r(jIl7oedR;&B{79MqRt-FWh)HNH(CuOb4C6tH`Y%_B5ePpfrEsR{awFrpM0 z7aS*Ij?tzO0WhPRK*vsgd`2r&k;WA_s%Q;M>pQvBgcVK9~a|Y1=C^WuztV@9rN0^YOB3P~cIaY#F zw>U(P4xjmYU5^0+n*6B8SX)a;r_V$;OfRj^!cq>l(r0TfXB{N3{5Og%F-E20hk~%QTH+Ll`W@-8& z{LHuxX}7XvUBf8BIUeC`zjcE>T2j#oIlj*vT96Ko5L2mH@d2)tA-^@Ph;Dl zBWTX&zuOYn2>%-;h4Bgr8P}G`kcmO0km5lSlDbpPa*?upa|(qBF)zUBp5fqCH|BI# z5z*IL*rL?-k~9jB9oi3`4q|PdgY1H^m&%DlVT)4Z1E2L!we|sl zOgPNYPD}2hMdd18FCq0dbiqmVQ<8}JGKka!z_S`4R&T{$Z=Pf;vhQO2)g_aSe?yvG zD!k-{0CRu+m$rynyn@e)&twg9{sX@jwgi}zdk@Te=k>Pe@JUIu;L1y*_}WT*oAf1= zubON!jI7tZHp3IFyDYU?B#gn~=&jO*eoWLP308ke1XM>N9;9uw+l1neW!uBIG+BiB zLt)-VU$ldVSZ0-gGgZP=;k2|!6qOj?$uT3+g+I9SeNIS$4VYF1l98qj#eM;sKB2rM z-*Q1{la3(m@7lABB0-1iK#`ZI+3i%<6_0$GJk^yMDM2Ex_8N1)Fi+&hB}RJ(N=lko zACdDmzVzpOlF?JB?u8Kc zM~wFq2`0qZOT{iC_6~rrZ^Yf_`s8R=a=1UdfHp9%?W&46S-zd>bTkE&Z1!&mt9r>U zLe0a=OqdTFT(bkr#|r@DIYu^2T)p;VK!Ll`PgiW$X!Y4xN-% zGZy;cR1zC4I5<6Q_zqF(POCR3n5d_z^kr%EZY8f+^JU!1SU(ne9hAL2MXL?%vG!N)R74-kABrG?EYYC15~?PfQ&1aDQ^+Pt(e_=6*5J+IE`4S&kJC zfyHxpiCfOXz!qGiImcoC$UH4pvsHmdGKY z>(`y3m(81pq>&nOxq!>fFMBeFniaUtnvNo+1D;QFzdbLq-dxmPO`IDrjnr{85Upt) z_G~#%(Bhx1N>0kkAc!-~Y zUCy^M*@KI&%{)!Q+p-zTH(4mH9D+B*`25ouKCYu)4e$)M`WHceNrVd#MfW@0Z zamq#y33s9KjG*>e`yIO7PV_~{e!SHv_nEkne^Jh(`Ru1RoUY5@Z}6I4Th`x5HeH{s zoEy2?PkYHWY{;+u^DsFb+vL>vlP^Uz;+qo-H^_?`mrSc*VB<Wb=_YVlhDP&|0>YL5#S)P$2w%$0h!(T8AXYlldY`5hEd}AZx zEdaBlktmdYYYV?8J&`;?ksQ_aj$RnH^=FH@lY1Wd_=Gq9Hqw3ZW#_!DjPBmEMC*m3 z@7Jw80565udopyT!fSOQNnyDaEJ?lltCv7V-y{o!Mw`POW9warrQ48ekuPA$cX5&um0mpTTT3eKaGJeOMy?v7mxP+ELT%e?Hkff`dL?2AA=A_0b->n z=NB4*GQfS`o;a(uU&QR_Km-Zv8;d^F2_w`A=|xp{DO`pCuFW1Nl6eK|g9LIJu;}^> z%T{h`S3Du-5^vGtp5^Xc{X3v_j+VMsu zrWwm{#J{U%?a$9m?K?}HUq%k8um~M7BciW}`$27$h)#MwD?Zzcn1P4q*NK-!1!ZG# zK{3gI{lZ?QugKeTJBvQR1s>uDYNHC*4ylZDIB&ae20*j0ot6>^sQWH-I9W6mO?*Qa!p6$~z>TK>Iawr4|Y=pDC7fLYGTRCxdD8E1nc=VYOg_gQwl_(rV)v=uc{HSbab=ML>QuWT zsX6G;0=5a5zZ-nIMp9U9_LvG>&h*AzT8jtYwCfu>C(53@;ftozERY!v^)Dq=ug~zf zXBLo0WGa~a`f+*rEDJ-$X^aof zLzJs7Ui{%ys5atCqx2ey)2r_G6GIL$_32kTuAe|&&*&U-peP_j6}vu(B8MoDKFy+V zc0PF=TWQ)G+sTn$BVxzY&;#Oz*hwy1FSJa2SLNrZj$KTZ=@BX;)XTO9a#m0AV67l8 z$XI482``G3;HWJ7P^D{Bju{-u2H$6#v($k^j+v=j>D#JwUSCJ6gUI~JS%~?6=6N+> zw3I85wZYWYEwq0QXk|igj1?&*=ADZvw0&4LPj|d5mHe4|+%jH?lma8EJZ*uRa6aM; z+t;4ey6h@tRQ6VgUdB~#-4_kqO1zjUK>fj~fJ=Ee)!4E@Zx|MRVW=3*u0pzxQYKRn zxIvwej5#-%jV95HW0WN)RA3B{VP2z%V5xbI!qfVEX00D@JEO0sGhsP98RW(9qj<~m zQ?5hHdoW`hx0l(ZlK-m$bydhpG8VOl$#y>(2YKe1mcMPqSwJ?oktscQ9wuT5pKM2? zS4#>tP0PeL{npS&(a3)tI-rNO2t6iWm_kQ8-_)e$&O00At#0a_WS4vr+fP=Ftgb}7 z=&&_Zu4bmI(KH+Rc@Rzb7j~@Or4%?`YLVDcQJre-7+aWH_i$2YQO9%M!r2_W`AHJ+ zfnzI?E-=3Jhe?ONoTCEfd5R#5?hh8__kZ+8B;QHjt(0iB->rxvRT%yT5T^fp&kLEo ze{-4sq>mI!`FwsoE-U-@=i0k>k5KT$v&{3~KbKO!o|Z_7u+S|C+`IRp-1-O`_xC!KZ1dZZF2ar^$1oeMjV5RKC&tVf-95}&jH#`^{?;9 zla^aZnbl8B&qz|VYKI>I9bmk=TBHq;`yOb;;5V9EAG zlC;tml+IU5gw}{1&|8EmU~9ro8lFc?IUAV>f{Dksay-8Dnj<^}BS4Ib*$!1uktLN5{Gi$aA-R>VCH`fnDfmk;T;}XD@VAp zgQ@FSMsf#*Vl8{!YRi`WHC|88{Bg8LQTA5Xdsj=KYnrkm|4%WBx8=?P{xE;r`>|ZW ztw)=wdgEN*t3jVpj`7ft^x8V!=u1~3HPKy$BuGo-978)f*CMHYSA^hg2=S*haa0OQ zdxAf^5n8c5c~5V7%76}*v>t!>=2fCeytezf2qDq@9T;4s;ulZ=m*Afk*48(23GY}) zwozXvd|{RO{0!)#sj@%!a8|m~gLPfC{y@18QtyqRdt4M|!%@Kh^m+XiWo}wAzxIeJ zS1)a_M`VE~N~nPYU!pf20Mx_fD#&A+xKWi%|7lc?z#K&sspl!Q~kEfu+=T*vb7U*l-=2^Nvm9xDqz1V^d(l`wIi zs2fnrCDjRi6s$dY@grp}|4FDqv6f0NwPXIH^=(<^?hu$f>z$bR&-=xsF5`Z4Ph9de?Hf~n{O9e#0M^i4(*b7znt8xYz)ow6{*A#^;lZ)SQs%jsXSgyE_7#;QGd8Sv7znqYZ#ks=DIOu@WoQq_8?D!70uUX zkLI3`Z8z~gd}9+d?uP`!bn(f~1Oc>AcVn*s=aPzCXHqH`sU+JKx;DV_LVD>H|JFFUK>ghH_1H>*ELFwy-*L? zOjT@!*qa*`DW$tRR3fLf($}2Ii#?-{H);%p(d{&A)V+8bXuMERWBw+b;n9e1Dyd&~(l&*{9w{0_VH|7zmiz!8n-Z0EAPIK4e?@Eym7&NBh) z&$7he;u-T|j}^fw{7i}>07=)M*u7%JM%^xEs>8y(hv5greAtI2qx7{TJu_V&tDjp! z)DRd4quW}8)>u)ys0sEhpRx4;V+v`LG~CKM^`P~HtwVlg;#lB#JPL!y3t|zBaZ?(~ zK?>Z>4cbtJ-yj0%{4LJyB7;sE7hMJLgdw@*@IPtD?OyA1kPXQK>6Hg6S}^K;=jsIa zz86%p%W|t-3r-zWIx%mEZi%T&{X+MnqaqPpgFG@V4e_9U@M)S^o;686V?yPSRzeR* z4?R#IpfksWa2%K#;>;1fayn8Pw4RAF_bF8=rNgXR^JG-Eho&+wWQP`wQD+}K6cKAw z!fZ=fxVbln=?3YpRCj94$uC%xS;yaqM<%Jy2IxAQ2uzP8Sd0Rm9p3?8sQu1f#RuPk zzugv2yHNxG=HX~}IKR{|2Ju+kV_QE?e0-Jo?d8SC*H5foAGBU$vU!u2dtVHj(3ku0 zu=!%D`G~Ojsg(N>toUk|`&+XG5ZeX3Vhj9tBm>xjqRPv|%7Ze>->6W($uAGCJIX3! z3+ZMHmB|PhVhdX=r=r^oT`hlmQI5C6_U?%d=40{}6Bx9$-{S%`4v>*&6MDXip0CQmrwS)Jqv^N1RS}ud!}}FeI}b!Cy6}Iu0ZzicN!Rp7fzzxWqiMF zA1vWP%9UdDk%vvt)VGz`;wMELhjJog_^WhO0JuBQ!gTSVUTE;W?IChK4%vBVnGVi6 zJi%n?O?Cc8a+_zoiYrMq)#`d8-onCRHEVjpUUK1D@bux|^Z66IYs`p~jyh?6hm%m` zJ`n^bJ#jhqCdEdGqtbplUcbsWJNB6cm;d8wAw(%-7LqN({35-oHY3|`8eE%IILGSLZ;(os+)*inzXB%FJ3kWCuLf5wxSurmR(z; zz6U#3w`EiZrBt_ntoEnhEP3@Zj=Hd8==*+!+g;N$d!<$-|8*YAc6I07_s%Ler7>&N z!9z~R(;rBjHUhoAhw6&3!s8($a!-rU8yhrVj1HXzggEg{YwE!}m zU3i9TVH-_z!ZpURI&!{|O@DAAg-X8c096JK!p{hI$I0vHx++#?)h67z-lO_OzW8nL zXo#(<1|hfYWoA}D(q&ZDPOc$|P*3!7MhEw_^Z8GbLrg>K~jaQ9HDF*>Z z?qX3XQg!Q7BNq?j<*be|%xGXk-n{*Bii=D(qHN~EL93`#t=vzp8F*hL1%sD_aaq9008vp@bRlHmhk=nlj?v<2Hbm5}{=HHhRH6J>OR0P8;KmGTBhO zo<25mtlaU}vKKX)!ulH^W2Q-3$ zxB^=JWT0mC@+F?^oGxy%vunTshDT7Ijm;d6rT)j2{KSK431* zYtwmo^({N05sNLU{8W0QREiJC$LH!Yg7Z+dbupV-RJ<58TOQ)H3FXCu6G;1A>h53@0^IaTs8bQ-_En9u$cGo5io zaE!oqv~2KYiS9LO;OM6wNXJ>P5#VY(VjU9TUTWap65u%!h=bJg{A!TR^RCXYMSX@G*Xof3CQPHW){(Jyviii#={s0 z=k!xO65|mx+D19oOFDZNhF65M5w!%mtN~2$uKsleC<5ALCc1B$w^;Wf@_6n!-VHzn z%&)Y+em2eCQYv7M$}hKQ8uzKL^@i4NPErwj)zIC@=lcq$kiSOrm|M_kUSqD<9!uyu zLNlm-0+gn;xV;lCx}Z8v%>TJT|Eg(i0Kj zKZ>yBGz{##4_(uF!n+%H*f#uP%zepX>W9E))qbvz3fyKf1*V_9=&<(F&}Frb9-Vm?d48w zh|#hq<2;R5)(tIS+D(bhaD>f~eu4dbu&{@5G=t`o=CD9ygzH*U#fBA|7BgL9Q|?EO z>~Q7@3i81>@^%ED|Cv$}tY77RE<8G%zHKj;_T$G)@|hV>$wb-~?j{1W-ST1}erxZk zOF*JMscmv;UTb7oXdz8*+fQ%o%f}b3<2PjDQ9~bwR<87b8s8W03GgK~1X^tax6$YA zEb%e3uWBM0@&pq~3Z@ zl}YNuL~^B`C4KOsk{p{Ap){`0e$sF$k93 zH*qv!c%jYne)&e`z%qziE>LT7w&&y4@Kvpfq^zfM=jkcLgO&Ke$*B$HFbE8*;)5%*WR|2lzxLz0!eUWU#x(L~M^-q@q|uYidfX%2jB7301Aab` zJ$smbcHo9To}%r{MgUlg`xJV5qTn7B8HLqGZi*F{;b{`~wwD_Si5Ox-emlk4N=GVD zt!YXh$I(=Fla^4knw11oGGqN5ZxWUq0H}zH5yXEK{x8n7t}eE76#hp2oAKc4;a+qJ zOGgD*;h+@YM&^Lv_Tp7p88Q^@b%16F1C4c|(``52$2DwiS?(r>yvu?KBGSJ>av~YmSaa41x*NY-F zp4oHo6mgI(Pk8L|C>W0dBG>$TIz(+?YTsc*CR~B>m?S};^cWkIic2(1>%X4-JUoQS zJf1AIulTKVq4dT>{Lxx5yh(eP@{CwX+tMXLS%jTM9dX7<{0RWKu9V8kpXE>?lE#VCz0U2>4WTy$ypK}9UQ1MlRo~UX1g1%K4*`n#6ABKzOo?T>1mi`<5 z5c4;G$(C&+=`xnThjNpzIHp@=tTa+;E)nq}U8P$TOJ>Jn5p?3jLWkwHfyJFU2;j@zi7+jxR&`ydzCwowXB1 z!@9hKWXW%{K(cpJ?!_lJo9-n_KZzM9%F}1>r-YEM?57!7XWL|0y`?%dbI8v={J^HT za+vGC`P(u-?1}0a=|xaKSaTddGFzB#Eg@Lv%&f+P5Gmz_7``$!oZ+MTaQ)(A*AsP3 z5w}J#1cbXEduFl=$T@GJBKgz1Ap$ul=O6WR@g&3d%em|(^Fj0a?7(z>65-+H0;g)z z$L%K|wFV8C%--ePjQz%;W#Y1|v3QLqeGN!AB47g=>6z^17WU!q-;gzzHe8X>P!pB< z)jA9lx2HxmgHeYc+(0gsA(2ICxhRbwx+A%07TM-*43Gj zV&!Sk30Osw%)~QlO28gG|7u$Dv# z4JbYwlY4?1mJQ5PFe4I^(gwW)f1_*iE2;={2IN#kJVDVS{w<$^-&aj?>xhm28MNh6 z=NOoAD^^e32a_(~j!Mv|gEl&Mh^u#<3g=k|;9vvUk)8f~;4p{TkoG%unguO=W|j~U z7-*gpX@m)%z_HxLhmIvu5zP^SLK{)7S8>^P%Q1c5e$M*jHBKpCK^A1q4I5uKFQONS z#xRn@eA!p3NHn5j*y6`|X)ww`@gZIE_XkXnpZjn!IYbZd%`$C+2eR6yWb$i-9QsjYfPoiY|Y zCX|L|YrGtK4|J)hTa&Tn{QaUFjgh5pkLwxD`cr*X8`Vh<90cFiGg30jG9rUx!dL#) zo4-DK8sknNn`vDB9I@z7E+(D zy|&{eyze(+{D6gSyFA~%Nrn#SLwU9~@lC~3w>Gz?TE`C3TN-Ui|L|oI&^5w}D$)H) z-pSKra%kaQfWckMC3CpRbPq)4o|w<-K30xdp^bsLxxRhl)tJx+CN}%B@Ei>yDa0Z; zcl5wxed97@W^D_VV#yoB+AD&Ab_d(!zK?3tD_WLz7nFHFz;5jwXY;syJa9ksZxlW8 z9j)07;P;3PhD%C;l#T!&@0i&u<;W4a1J>8SCrZ${vp(e>ihugu2xhcTFBL%g?ZyN>ef(Z}Nu_h`sZtMwzirI)DE4jPc<@7M!{fY>YPZaLx&<_?KI)W)Qn`75%KLf|nTE>$sU;{j>( zG_YOM2coQ>57Q0q`9$7_6Bq*wC5muN=HNRA#};DMz<{s4&u%6u9i~g{iS&Z?7(WhZ zu6v=emS>Y*Nq=T&X8;Z@Kpjly;6ZrJW)`U~rO6$UWt3rD#QAiM&Kp?kWg zTAW+6*w5r1ke{<9a&nk>^~|CI{F2+)q`r7LGx$!$tgmleY5@ zU8@p3g|ko=`x*pY`xjkOXevQB0x8VjF8h>b+7}&q+X%b|8o1aLxqW>zfj_J%xlShd zVD_IyKrhh#g&spo_{H}4T=$7(cg5>p@oNfT%GTz5xTdyvDWEJE%;$9>0&kO=TUTgA zGcQJ!0E;urgPD7UoFhK7+7jyjU|)#JZGhn8L3jvv?+8jTjOs-LQbk91qflzOe%F9k zXKlD5A*wgh1jnLT;6uzGK)M@+I&E0jY&Bz#Eu<5yXgZJ0Kih?t#&4a( zMdW8rnwB&Vr9{UO$JG~xDCr_VUh%15D?xY6*Sxh!TzO1eSGat`H)^A|2Y1Aby`+-6 z+J}&2{*kGDRtzQe8@H?P3ex*-6AJpFdII^rFL><_Q2eDxl>q>JzI7xXCA6U zj{d_2%6UkYU-!cqS1^*ch{?@EkV%Zyuxj3bX+jRPV5sBI?Y~dLuZQA4*K2QN{4(9x zeZknBOy8zCaG%rxsB6MIW%90h7j9(AmS`F$yek7T`y6k$_R47OHt@pF?A?&jGp`fG z1tN&$BJ!%VO{qeR$3Roej%|ySv)t$<{eon)&speq$odJ{n1%%i3&_cK6f zE9W?88UB%N=!ES&U07Jy+E){Dj7UQGkv0$Y)+JzXf94DT=RWwm68F&4?Gv^h0u>Tu zn)~ACy|XQ05m0#`hp_B<6=#Fda9X;(oilAR%)zOz2&Y)C$6s)00qop|XK5?sVM`1H zbLjujC=b{H!qlGi2??+SiGO3(kE1%;fXE=Fe%-W*4 zoZ|~hGP?GVSG+=^Y}(cfp&PQHHiY(AeXH)v0y|R_-^Om6b_LiF7ZKiWlRM3ba7no2 zjl>a&jo2=zP&hcG8%|d8^%5JZ+3abbl%~Hfif0m@5l{J;SUO(@Ksxa*J!KI1DQ6xx zpufLc^m{Rhisj^gq;qF&axKN-=R~pV_UpoMDc~y?rql)F+t-273w-aA1Ktq^Mq~!Q zLeWH3dXv6HLeb1}EfVndoBkW(H%uz14JIM2nIY|>j2#rAjhXU8nW5uLq2DON=1jtt zT0{Gn!pI7_3Y~C*BmC4egD)xG{V;j=EA!o-rFYL1FmzKGRu&9*8AeDMPGTBPkrhs} z9L_)qXEKGeWx+Xv-klG?!&`H1mwfO`0;wQil>ZGu#{M4&66GI+M}|$SmSme00VE{1 z9H}qrk0!$M5hs}`Dv14mLU{jD1!bfccybXqbQ(<+GbK!A zVCB;zBJI>@904`?^l)_$B*SfEthsvWlWl)2*$4lc6{HvW2eI*%{~N*!`f<83{tv=y z=X=8W9|*4>N%9V1SMpz};2(si)!y*`gYXDyl3RBF2f~~D^2=hVI1HCTyR+?jCq82J z0!Vv*eKcL7S@7iFF87`Fv%%I>*Oy#+&hQ-- ziAPqAlv5=?RF(pgtH?D4s?1NS>rbEkE6p>1X0TM*O^sRBbl!E)03{nrt3M5pyPx@G zvj(0uOo)6xYn)c$Ja3vec0X@kcCJ2eSr7Pr{tp1;ylC6caDS!LSXO<}aV9dPOA$p2 zzU;hRbieGnKdip&e!TmB`4t6=>#7Gp;c?XqWUIOA!xQ}n;Ss4$@DY)kcw7%szWfiu z3p~6255l_{Va}}KNBiNUFSlqYMN)(>A zQ*vShwuL;RbnYnsAiQQq3d8>wgvTvH;^^gxobz~XVb?zRjm$1ji&TA2(k)O+Gjk=b zqBbSAxYZ$1K5~*Wto>$3&hDsVmeK!-Xg9nDdu9*VAwhg*`Y@X8FWo`Jyx%>1A*Iy# zHO%lh=Z{h-!9<)R-QBADz+buez!-V*G56oP_mPKFS-8g}w_T8f#@{mcxXsT}>j=%) zq&EWBTl%K})a{F>`hq*hUNy8%f(T?e@;643c6fy%Dh{OQcP~#k9x4hZCYl@`v9ABI zgJH<#4`~I%B(#>^?Y~20(qkUB*nb4`H|c*S&9CrAWEC4IX%EZwV=G}&oiKiU*5*T8 zbE`)l{@IX|Lnz&|p9YYb?bT`oV`&JiD82ZVoMNK( zfm|lH0Yw`}ro8+S5W6-CE*d)nIeNMB3w#z$s{u=}&2TT*aa~ZH+X!ZNp;9TP-<7xNzZD^qoQWPgQLI*ZzqkSEvF31g6XS*%V5oB-}4p` zy3|h7B5M=CqV+f3^&$WE{sU zEbDrty-8)pj)XqXx^@?0U+do!Zze=B7T_U{L6-L4dX@z2tJHJ5RMMwfW zg?YooB_Vj`3rr%RSH6&3;%#>Kh6<+9UrBtuQ7(m$R{}KwQ|Em|>s#6eZUA!aOi3}r(GHci zf&Stvzf0!MjYIrG!NkMMr_|im{JVh&bMGQjFoyqgq&fn;?;bzU!NkkZuA*rziBF0` zfPPF6KoF{x)CPvXS!tQLRP`-L+Rot6MX>?Aa;cD6K+sL~`p~$bEf$v^CH{JQ1FuXoRUNmpp``m`((a;og^IGx9Z1VK!faB^QAy?QSaiiLNh%|yBG^%l7J z?NwdVQljdjW}~P5TkDho!n5{QojV-;rxqI>Cy3WMJ3BsJAg<=^+b`zZdSUALpNsp6gpKnJ#m_@-9cWfIoNriqfL|>Dch+ z_tB@UTN5_#+izd3&Yq3CG8qia%HE*?DIQ;HMh_WblZT?W!94U}nR9QkfTWtln0_zO zKc%B94FAK>i3xyo>Zl@|XPJa9$P)#rZ$D_5Er8Y1 z_*UAPM(++T1105mv3MAwmNi8jIm4SB5gKaT@@)w13bq&b2>l>dJ+VmRASR=rNOKp6 zSzDwvC8O1Sq&*aDCl=)_#^4kb<+g0BMj7IM9|cW`@)C=l1%>$qMc>**2em~XGe(Er zM}OIghKV76E=8fLA(6{;@GNBfJw@E}GBT-xf~*|1h9V{-i!#+TCbKODA@7=d9}~S3 z^HmU0Y8v}7D7GRiwyG_*W;wR*KDL@)JgNd#qF`4a6xWs&*U=W&xg6Jh|BvmLj8ODw z2F4D#uHqU`nfOq5<6xMI_6eAMjJHxF*W9Q3eD?k%#(P` z2X?F>yAqg2ER=XGR9fth{usEmSS;vRB%HX=kytd1ID(K^%(OVOz&PBsxZI&QLY#OK z=y(#1cu&eG3XS;Z)Ob1zS*o>og0^@j&IA_D1bU4GHb7cDM_K|6Y#dKn9QR8CAx@$o zY$BgVBAP^^xJI00TO#UOqU>6%JWdiUXOgl;tovQO+DS~K0~CBol2%!a&P$SkMPk8T zlD~@ADBW~crKKze0uR1y(*_5ejKD{S~A3LMxsRe4|2+iz|4*{ z^v;vao-lf)7miHoENs85*~9eQlgvz@$A8IM09eqcjiIi;WTm#Ctiz(UQ)dJ&XY-(D zwQ*)IdL*{*r5ZJ7?>g(K#AXua<=o@YAGCeC=0w?R6GBwTrCQ6DVp4!CM@7a(wL~Gu z5Wqvl&7D%rrdCfu))wj zr0yA_*XvPTQ@~Qzq%W=cth=SOdzZ8Ei@s5yG3Tegj!^odxhE}B{ zV3WrrU@vP`)!bnd+$;)c7!A<)q)XQP zLtLJE#G!xP2(H(Rg-VF1-L)~Db~~7y+1E2nQW15vOmoPJ3{H1_0h@{lq76==3OvR8 zRK;t-1x7HU=qt$Bb1EJzYRMlvg>5pUz>jkw+9aLyKCWs$_RsHeRO%CX?4soP; zG(G#(6X~eVy=ipM{y8ZD$sTnQ#|{n;)kN?$DEj%CaJ6AZ;Akjv z$Zw^M0_?WM{$(f1zQ%PZ<`tU&8XFU=+7&L7&Twj^Tu^@Yv}=qysJBwijt2A&PUjYA zCwB1^&l=M{3feQDx3cfWT=D1YT))^GvOF+xr5d=OD}UhPIA04YZHSvmmWiL$eX^>8 zr`0E2Z+;c9*VD2>9wjx~Xh0n`reDujrB(gz)+D-7WqqcG@7Z8zRpUq7Sf0?FsKTDq zT*ad01w32ra8}tu-jY7i`t__be8PvqvK0C&4*4}m=0rmYciY2Ii(N;XRY&`&8TS!x zM>mEpGPfY_Y}3!f*6NBg$@)4(<)Nj*nk8 z;qqW;6CG?|r4L-Ox@2B>0Pc?NijEwuZggs_n^$W&EqnVRyD#>wQBtUuUZ2mr(jGdZ zglKzwEWbZ#$qHxW56~vHfmSF;Ro`{=I3E|obQajR^-hf@YoeCq;da^M;?5~l!C-T9 zu6UwJ_r_;X5FqpgM6?81)Mrm9_T|;r+&FxN>~V(&rAw-c9%m=k?r)-PMHFMtSgtyK z#b=J_uLbpLa!k(OXr{MrkPjqA5e#rJju@it0$&=e&@AAMtbyY;G-+hn0Wyr<2GG3% z$eNp5U(0zn+bG=S1!;!}-v=cdhJJ3echTlcAoOgW4a29kzBsj3z7Dn&sG3w((cpWT zl{b)iH}+_jTc3+00*GN4wR$x9{X8@Jnmiq%P@&X1bAGhPdC=A!MPQ$JjghdA8qg|y zNeyha(!$*6;_VnSC>aM`t|t=nxzGxTGU!TfQ+3jA%~UGrf9ng{ocMx2w#bzRdp6X;Do#HW)E57w)gg1f5MQSEoatNWH6ZUjjg$xGkX!S5?x2g`_A?msva-cWdfK%DHJfF2 z!MalfVS@R+g2JV8+PfF^0xc45{1ld_{U-E^?Nm zViFp5ayN6-7a%=DRz8TRJ+q0aee2!g*4LE|KTUZs+eg80puv%8F--*P2@ur@l;JOWWjaG>O`D8+37BJ)w$WRTO4!p>U)0^^@LR zSfAM}#BtO9s9F-F+n$`OCvKm5zUb+;+5RFt1uDI8T{U4sFbU6Bj@Z2lhB)sMFh#XG zhk7~x?6&Juzr__b@KeYS^GJQSt%(LJ+ICQKIa`ibQ&{-50kGxL!PllQn&#nmYvWJsA~QaODHviwNmfrS zMMZl>(T9Bo@Ai2^BTWM3tJGCdfG5j;yPP;;v_$4pt#tDzFq{c7o3AlY4GV5_MW>2C zx@A9^a)WLbK%=qmgNf!6ZBUd>yrj=eqhMohT`{0Dlwggi zlmDrmj*OZK0fXyopA#sKjRNM1;gkq&zm?sms zAQ8_=^`pPPUr}%_3nTE3NQ=r-7ugy^#5pLA*gO81N00u?+0VaQ%0OL#AUq~2MdN@S zfk0?ja^qEbRFDu@1Mx_4Cb6LaEN0ck*0~|kSOiLc{52GiydYTa;7uy~!U%o_o5@1z z*aMkN66EB`wFR@mq;Fo;@8YqP!v&ZUr>vF@hKhL#GCox0K%`U|WP|O&`jXX3oEc2` zZ_m3@=@OIAkJ24q%B5Q%t)`2#lxVaAZSq8&sg21rdo^yC*WMPXciP1f@uoVyq8QZ# zHQ^qvsWRy2F+t_JgmOBH55_~Y7nL;|NThHJHIVD9JXo#PnamXHY&-&NHtR=3IfO79 zEq1gyk*9SwpKo@HMH;jTH(%~rO4R@eGI(4L`%7lJZr)sPZr8g*F>YNQ>~8lbOEqRc zw|{#*-yB4c>HYlp{{HeXz;@;J@dE@L!=4`up7oj^0$q8wt=99-P5_29(OwXaw((jJ z;ft{hD)i^YYatXN3fcT?mZim0+&XN`BV*azC^a(r+1cXHqZ5YF;~@Lv6 zBIrf;iqc<978L&i;Zbu!{sG|~C@fC$`|}SRO1nHPeNk|!QQ1W^vJ^VE4{%Y_wM+b3 zu}{@dSyn<6;8;KKq4s@ww{7^At#O@XBE>KwLS=Z#tY@b}nr31?7#bQ~PdwLDL9Y(9 z)LeG}N9krFw(ZHDn}A{JPWiUsv38^}Hc50G!Ths9vX(95Ue$+An)<0P{2FEIUil~_ zPCx&kNp65BifI|0skv#JVv%ZT*c1)INpQH}AQE~Y2f$M=uVFb|An-5|JK)DjM>uyJ( zu*)o*xH{~|X}>+%WBknME$OCzdcV6wy2W*#moiH0b%{f8c9)78R)53w z7NmujDe%r>2LZaZ2nUoNi6VAu|`C_q-=} z;;WU~g|QcQ(UxFHp{N!0hx!f#;eD+N%V`Hti$fC<6_0@Mg9xM>C( zQ65Z;24ccEcM?nOo|}n~ltY3K3mwW57>+WyM30I#TqhzNjenqqiFGO;BKXA&W1?4@ z5_^`QD8`RiKoA4T5Ta@Ji=8Fx__%2NOsb7^ndsu~n|*x14{{R-Rq$}a*mRsk8U$j7 zT7OPdk`N8mxFQZ}wblCY1x&<7Ueq1EDI(|I$zGK-kdA?M*-}vhaIt>>D_$NJ`o^}C{`RA446>z4d2T+y|pTntx@Ub_9|F}#8VbeQapXobu3 zq+TvWYSI=TFuqsswQG@26~+7ysAh^eVd#n6fury z%FRh>R?cIDRM%k80O7D`=@Z46Lt!=+4(|pNhX|c_L+p*>W{KYtu2RMGvkoks_XjXh zO{eUSnxp3aUbxbJg8FSZ>P|+0htN+D&2)iOAdnQ-;e(7A;Qpy8d}()o)mCs<9(<01>JJ&8hG-B!_=khQtoNqX}1-zS&0EZu8qx-=BM` z)Z>Vj4A;+2Z%kGuUg+2xe>r51uHG7j0~e}Ty_|DB+@B>3%J%=o@+^Ai(d~BrWj@s6 zIT#7|nmn0j%j<&!Y0YirK^bTLpkeoT2=u%Wc&!Op>Zib%s!qITfqgY)^E=P^}I8L3A=ojj%UQ;Fk?$`_5dm!x zRcjH;+xDnbQ104LhH=;_e}M&TQtif2if}kV4jPpH-s5LM&2!v}^`Q_DaAWJQGLyKn z%9QeZuuhT@d|XZDnZ6wpq<#sanRQ&%`ji%Nm}BPPnuES6QYF^!WZmh5rrt?r&H_zy zjMfk!AMiv`$M_5ZJV!=_PljTl=oK3gBmosLX;SRT*J2LNB0<{X!S7kmt3w{zl}~#LOz=?jOvA<93vT}V$JR&uALYO`xV}}^^o%o0l=-IK&r@2Sfy96 zC%o3gSK2uuKGMQQ^axy}R?$ohYQ7zi%sL4{wBBZ_!Gz7E#Kdqy{bY{Sd@ynU;&2JA z{oJ$e#F?Rn!>ro5LqP%1b94sSrYukjZIsxVwpH#Ba#!?WK=P< z%+i~EZG|XN2Al)~6K)CU{XCO0`&FWm&B4~vBi_>Y0@7nVl}(w_6P?N9os(^C6aC&e z0gID3VMUy*(nIw*E3E=_7^oD5QegptTX)egYw(>g&6p6Jo|}w|kwWu4wZ*eU2Iqot z*YE%f{{W_5T7PMZid4{w^Y+WMdSdTs?pTn6%+c1|q>up6-OZ8xcbVVJ-&`d8S6IVw ze8J7bFn5{gF82j?3OU;Pv#B#+D+^;D7pLJcD({6^_!4{gQT!FPhBH>VDqkq8GfLUa zL4skxD0#}LL!|~*L7CKf7S9-B#fje0O++K_E7T8@!P1pI=3n25k3l8=a>9+UQxd?BLs2mUQx5_~(0Qm1OCXY*?*|Hi zTLo825IQ_MlbEbA@3;H;#JaBX%kX#uM5HzHps ze#vmsP~?c2Z~9%TwB)SEi+aBm`Z}C?6U~R|AMXOY#C6`U63NTASxH}{OEB4&n zUi>N6)G@cdqK+Uq*dX3T*aEj;c;o8dEmp55$S$2G{6DFcNQAQStHjTObJa>91p0kL z&wOrdl`Bup4k~i&;!TH0A<_qh`qZ5r8<41;+oi9WVzY#g5?Z{~@d_rcMla(}rtBLS z5U1FiN-BlvPE2<_>z@kB919~3rW92eTSFJdO9UZ|y#dHn8AG4Y4vH>EY{`>}Nwilb zp2AaZ6bm6hh-(xv6rQfO_pOmmF$r95j*9q^HiR`_LkpLSSPN#c#q5tFf#?kSi!LIj zaTzrznqVj3a8&Dxw3e&rbKjjRibv}V+=?s$TPkTK=O>j{V^;e+DR5X#z`dmo12yN% zeu5O5?yPqK`{clW)Yjqm?PGtH9D<-2LCWH%$rG1bTN- zS$8sep455|Ha{5+^q!*h=>7Cws`TdS_1;A)-?#K+^7KA_>kWhHgSuh>2=u`plfY&5 z1*!ERZFfEy=ck%nh| z4Z8&20HL30&mctTHl&+2RLz_y+u8(q>sblfiZ+%}@}xm!+jCkN0La&C)=O=IH5Oj{ z$~^2HM}=2%7x|U#CHk|Ggef^wStgdmlx29)P>{h`2vZ1cQ)i2q{0M}sFV2u>SZ*b2 zAJ?{eZ(VRmW>Z{uQs#1h_rX}cIo_;rOP;~R|BH#Ts_rWFuJZEFl#?w9*a%fjO~gm( z8l!PC#4M)0Jb~e6jqH9RO=>olT++POJ~ot?jNsvk9}mu4U#_WJQ*}bpZDI)q|*b;za`1ona#i zI|8SVG+<+?u2&&Fv}o5sS2o0Bwm=P76u%c4Fg2S6Dd3%RHx8Kd$NK17F>s6Bmasq8 z&}0lAz~to)k`g52|BCPn*C`U3T2@6|E>WdQRW0jp08bq?iSP_#*!YGvVPW%v?9qjj zEpLX*I>kXDj$sc1^=wWheTIVq*KFE-ynvl*o-TNMie+$s|JzW(I|UQYd~;CbtTP&V zil=BQCDnJ<6`oZ27Y;_0StpW=7Sz~2Q|qvy={Uno@!hZCwlH~{y*Z{HUABycMmRGh z2K7fI10Qp3JS1v5iYe@5T|VRW>WeoHw2RR7(Og0|S_!~%G6jWjHeMwXsdm&FQg*CV zdR?_xC-%jttL`#Mjyl^|X`{_q_vMqwO_j|1ktgk&cLL6+~Hl&oxA= zJ?-2!Pmm(b2aB%dBTv1O^O+y7W&-V#VfD7rk73#E_mwu#-t8)XcSI4)`SO|<40Z(9 zV>UHV$?n_6>Zb0M!u$fIC4<$ z^eSIRadaL3f~}oTGqQ2Y`EJiYVlON*>*J%qgFI5xD$gZ0Mt)Z`=8@cM6ZKSgzB)WD zMVV2&AaTdKBxQz)53^)z>&&GyASZ02b@OYZN=GpU@74C@mHff{CegZS0gzCP=$M(#pFq)a-`)3#Rwm%4w~h(F#v@~9M}@Lt!F;)NZui@Y>im*Ub^sj5 zu`dqFYe8U-!=)8pat^t!J6^IoJgQ?9TLz+9D6L{G$u(F5w^0!1{9?37VT&t*yk~SO ztL+Q|RBAzhl~G?q-p}M$SX4)sxJp--$uL5c4lTaxglt!6{J+H4M)y_a#OO z8B*SqVq(aq+)&XSMBzYv1FTa8MRg!Nk|aQdk&!5|P*~~hK}tag^c99b$8+fQ_W+t? zYfuoMQN$xX2ZCMad4Ka$+k=NK!=D@j2prrta`en;AAj1h?SA&*Vet9IZn!V!^GVm| z$@wF3S?Uma0Dah6ip1{lMrUEi#J)q@?(n%>e*;8H!&NOew8<4a`@@orXl8Zc$EHMX z%#USNx2G5wB!cVAm{5a~PkshHzdQ6xa^MS z$2X*-u_R)^dj6Jd+z&yH0i)HPe2|U_5e=6Gc;eZg%%#}+`I=-V>IXhp{hb-ueE2uc zacvL50~rwkFGKB7Uz+7g%@)Ut(+AqsT0?5lPa14S(IlKmhp9Amq{IYf)Uv{*P7LdH zKlt8Z66Qq4+l}bKB#&L0_6EbT0AFm+pB;odV|fXKIF$`VJ5y9>KDBDhnFaWxe}FO_ z+)CkwpRcz#U0r@>`;{{P4KoJ6lc%o`L+Do|Ol!+IBFx;68an{r$2-x9o*Qf(C=}?r z%PeXf=QTL~`4H>8F6aKCk_56Nd7e6b7zJKXu7ojx^Q*@*aVYV}aL)HDKVcD(<*%&U z?%0qb`Jy=K5Tt6XFolMK9E6K`p}eopq_L&~P|&g_5~_qzbmS89LS&*#h9U}Gji05z zAfk{ZYgo3K08;_y0c%}2GeBPat_L+mibZ)FMTTJW3q_{GI45P6UA_iow#R83WzP5E zL&{t~I4-KZAVX8C{4kn!s)FaOHLAimNiOOlOkPdu;xx1IBvRvUNkSd`msZc%K9m;H|yj{ zL2mk{rDe*hrZvls3!_!XH~Q9H+k>jsgLJQIvHkoGhK>u|!s?Ejad?JKoh5F@?iVl* zyL7J8H^$zCGTH*uZ#N}G!sfWzNoEl1qZ+-aQj_tj6z%49%7V;e*O8-h5I2Lw5z_J1 zkZhX@<~(rs17kX6n}D@Z<_T#Q6-HVx-^dWdeGxUOxzOtllxZH;xk*!ZrD;Uec;^We zgGmJgEZ(y1j(4>04~?py8{>PZ3ikcS1l;J@mQ7;3uOu~$=Q7usVB%awuIffuB@N!7 zS(F8NU}?{H~!XIks+&Tgsz!+|bBUO%1}j>8tTW^8OoyM`K5KoW`FJ+mE}W z3`-TEfjfi%*7)W-^WzD9bI#TX9Agq1$+iRUB+)r}bqIyd_AHm5Y?g4DnBj|JMhw4n zA{p+*OyK=|3=l)~iAvyvG~tX`H9a++l4g!qgq5WGapddJR~c1NI0+qK2H8Um5yIAq zibCyALdzf=E+46Q;ti9uY%FcAzOSv!FHby2H0T?)8~6Fb9Svm|(M-c578?iy19e3; zfnXv#y) zWO2k>ug{ZilEsMb83xI;LkG*;5(x~K)mzFYsGYN#iKUAUTVpi)cK@(WmWp`)Y4lR3u*KBAz!gdjn2L-r}kAXQ!=rKOV= z6*GmAsia1?08Ou+__r0=551B4$;p+aagKXmIwb%Qj>+IXXGItq^8zA|g`eE#BB$#LKHK~fm(m4Y$UYEW&pVKt}uv;m|rB zYye{GmqC&X)V!4V`4|~Dxr}w%K=JgLx+I<}T`|YyGH$ESyzygb=_zCo*f7`_zJ2Sy zcdM>VN8Aa8LAHHdT6(Mz#Vx87R`U94GZoBnUb3|-L zmvro%kq)!IaU#>BT40$xH2a+Htc2wnppD{{3LppuJHGxETqnp^Eh@_P8hI0V+d&s? z({;L{MxUvV%NF|PPgp?vFeq8yqsJz{ro>FajBvrS5I<|epUxF4N3De+> zFC+fC#!z~XD9xKaRcTNb4biU6oV|@RCg}1U^`)MDdzo|HG!cZz7ZAgnjXZVfmU{Wx zx_&T!ne^nzA5s0#nFG5oyvf~INhaMfPN-$tXnIT)(h(6r_;AN&_7nW`ZKoO5eZprS z3veV|vk){yhPh5^9jDVhWWCGBMD`O~I1OE}JNOwRl#(Fm12=BX;6^l1yz@rD2SFugLEr;Fi<#&gFNHr`WNHY5 z>}tsTq#wJO%$c^&IjBobbPrE|(aK!f1RYz5Uxj!yfr3NP-qxPQzk&8)CK~>}VgF9B zp!0J%OuLJY@Fq=rDO2Z-li)Zmcacc`@;(>UmcaTZd+cU9cOS4Iu()*s5{(eOg0ZX> zs%+4xBZc`YR~MHGVPk-4NQ~{7j2*ozSCxTdh}yY28DSpS%9~-7Xn>2fg=8D#+)ALu zL$J8BfHJ;|ZyNrL2!UaUX{{VBu^$ZgD!jY4j#vEi^K>MEZ?4pSpK#}j( zIx!jOfs3%`Ts&6c)-dlLnTa3hzOP9l2*UOxIIH1QZVF1|u%vwg)Vv6OV`$HF9`&h& zws#;lBpRZMA_3d5S*sr7v>ZJHqHC!#?N+YbF+XK_R>GXN5QYHDE%Lni(45ph>1J8)+RsPWGGfODR*-LcXz=(1S;-qpXmYd@^>FXgjNtbK~h4nAOvqiWcLNIoS89Op1z_M1e8MHhW!JzbrXV1tArN&Wp`at7WG2;* zCZndOq+_9C6{4p9Ld_#bOG!;jOGhhd$6!~@{FR%9^(!kU9~Yk>m$D77mJ6StgrJbH zkf@}HPl2eUf<)wuq?C-LjH0BekF;f|Lj9his-}{Xibm(Dfsu)+m4k(~t(C2#t%I|J zldF@fr-#?~5GUz~==dZT_TsXtit75h#^&~puEF84<<<4g?VY{-1CamvKmmHQ?8WJD zV@3-d+s;uwPnJ4MR%zC(R=Q4Hc=qhnx=S-;%p5m*PBGBR+PBWFRc~B9clyj+y#ajv z`~!sH0z*Q>!XqN1qGMv?T!W=>;!{%7(lau%vU76t@(T)!ic3n%$}1|Xs%vWN>KhuH znp;}izB_a}wDkb4w|Dmsk5A7puW#?aKL8-$aQNc2xqSgJ=rsRYg(H>B6tBw{`G&-!y*a87 z7Z9z89g07u4;>5+p;!)Fg)5p!VKN%elxQfP%HVK1{c9Bt@IQ9p#G?p7dK=3YO5_V9 zvm~3!mnzg7{@R68M%d*KC-6G*T?2;$E7kd*cj4gO0->oSSma5hTCbC#LO+YEPqzN| zT{vxxKIn;Hp$Nh+z+D=06vY_bsrII%1qu97sNMGFiw$ml(zvX;v?!AvX2=nl%!JFm z(S#a4U3U)JBk5cz)ZETj(!=1QlzH2(H`?><2YL2>-p&`TotYY%n(p7Z_l6^1;$wpN zzLP)wwx7BB;PLi8k3!2O1aC&w_xru{sOOKshCSntBuZt#i6ZL`^DL&t zX7-w8EGa%Tr=@y*ss@(jmCc@m<<-OMG?n$sk~CG#hcr~x?T_iCHQi7fB(+QK zlA(2~M6{KTgA8ZqAya6{W%bs`E?nqze$cbg8pPNV;-}zaGT0d%qc_ta-nkWWRd9n-ydHeZQz?_xoYhtmgOQrrXu;r=1YSkLUdq zyN{RSlA4d#^OmcRx9br`!2A6Q5Z3#AR0H^Uf4TwyKoD|4pv8N^aIbw~q;tWrMth;S zuKiFVb0M5S!FiPJ{k1xF$XV5)ksSL1u+Q;fgi13}<7X`>G6mtVhy&1dt%4XZwBY@i z2e60BgL{4R5H|{hXm)1dc?$(mR9Xi}aBo7zrSs8JMh7XlZo=dv^D#=qhiEi!!qq49 zFoAA2+5bA#{Zv3`Bu3?|EQ&=7pr6)(GLAi6Igz_faS$V4xd`5|;9M;juYk zFs#uMm;1<2l|f)stNGbVF_z)=+rS|P6*+OS17U5)CUjqKQ+g4KnL;Ec4RP;MN2QBd zQpP4tx$e?tBa2x}B&IAi@6uN%i`iPnrffa$GIkJ3I7TF<9n_mifVVXCq) zJt%poNoG7G3$q^sVfgM&V!z)e5P!Uv2!heb2d;uZLNtBiu1l4-;T7cmT`rhGa!Mh> zx`>NaMTP6{Iu)rqP%r~#fmTXBPeXT4hyoc@BD>xauL_R^VBi&#Zt0!ZCV<0qdzFzH z03nxfFeuhpEt7Z}gp#{ZDitKbmnScz3bTyil{yesmSJ8x#<(w6pDI^v8DFaQe5lYz ztWY13T&_=ls5FHG=$)eW1kc{TJNUBlh1y=+;3rj^x zuEpzu;0A=R3h&?7MyWs>kxH$NYd+SePF0$+PpnOgLe!6^R+_00uXFpeN;u%kTMYTq zNhXC9NU2wGO_VTYX@WM2_#)Xh^v-|2u_%%Mt)v)Hs|C6x+ic3uAmfAwdw{#ZIiv%j zn>Dd;-NuF?SWpc-2C8%6bJ?yWf03Pz8ytq#)LFd>ShN_cC?VHK^4^9#Hb0X3`MUnp z{XA9Uvy@VFe5s}cpihopv?p55c56P?PW%z&6|^(>l>D^25Rf4i1ba2t{|w5cS~a-& zq)ID=9UYj(Z?SkmqR!Dax#|PLI)@p>|?(n>3BunVGIRr6;6xLkos=MSn{$n~z zta^x|>4fUpUyR80bAG*z`Rr3CvRocg`Vwulrtx6^HhNpvI#{C;@&ti(gz zh`D$O1guRheL_gC5hb54_v4}A!=hzdCbH;QJ>CYrLfKl+YA+Qx-nO5oTe_d7u6*9! zeu5yk_Cm{E`{TXuz{s`sV@+QNbHDGR#JKa<>rC57bYjr)HjpJwTF)r*%rcs^y1qA- za=a{0k?1f-xe(Mw>%AW^#k5VD$=;=(#jSjp=SYX3dl8B40vUeQhPndf2k823g&UP-)b%r(7o1f90U5GjE zk_CRZk5JTy#6K=+oIdVyk-Lt?%L8yc|V*n*Bt%qa?j3fO**@sx$<;J4|cGBvatMO zMRadmaQfRY`Sb9E>B9G(MAy0wUX2sTp7$d;!0QI!<9Vj%<7o!)@dgxOY+o=AUkG(y zD0g3&RA0DOUqm;b%&$HOp(cVvi~uBTKLD92Iu*iNi%*Zd-$;oMCbmBbhd-IRKLv+B zp}YT7h|~NhUBIndwV2(ts2KICD}}1d^Cu;^1!rjzYf&igZ%uyaWPu%+jGaS_69+z0 zrr)!UAZw2pyfJjX!u*I>woq!dP%HJolVHct(ZWrX zDv%O{Oc~~+9+vDLmYN!t-Wry<8kYSWmWv&p&k_Egt8fP5VU^Fm+Q;EF91)G`5zYUx z3a9aB7Y;kJpCfWmJ#yGRax^t^ycM_$=lzQ)qIW-h7RGGhSZW^IY$?={el>D)HER1g zY6m-d&pcv{BRY^Q;=nw5b6@eKG+N)8@j_k0;p@uE0q^R6+)5`sM3)6p}|YI!_E+ z0O8JJ|JA+!_cusDE_t-EMYef)b)|8eHQkJ}%}@90EAJ~nZ|=Wc{unDRg*;WI#DBVc z4ioIZUA}F{&udGMrO^N4@&ki{|HI|SCnP2%r=)5tWGJNl?eh6EGYkK6`LXh-#aUH< zxqKtJ+K$dzm)5^rJ}1J^=wB{>dS-TReqr%1m%qNTxwZXsXZJ6ce{_8EA1+@9=HdCp z66PHo@|70mzQb%6yEg!)0YY58ppP*ag_H!9C{8SZ^V1j0O>r>Nkr*N&FzRc$!m$J@ zdceI`Y<-bb7_Huav-$tw@)Kxu#{bFX|3$nYkZP`6YcO9Z{|A@sWqC&mgxYx{8p#4?WvBItDn6Ae-kf$jV93Q zO?S54{YAWx?P`BGTWzrVn|QG^5b;^AyYuA_@Ir5W7>j_(+c@Q#}jK6WO+j~lyiCjjLS@d88Dd^ZT&v~)KZ-}QJmgeVy3^2z=XFQ|%3 z_kb?{crSu!nCe#~E0A~*#c^2rE1Ku=_*V=+6!m_rFs8+RoH#|$$o;oDD^?2 zvZ}>FlDcWxL9)Bb4mb##`&f~V;kp9FC$rnw!_?HOF;foP?iba}FSBLAS&o;1X4yLa z^^`ehz7m5uaVT)doWaAfr3exAv&V2Tgfu6GiK><-MX9FcC&ihrrza)3!9bT^m~07j z`Nid@K$m}dT2VVpb5_~7YQSsk_7l3~tUR<^v z7ylt%G@o5|To2R!ZxS!)ZU+CEcwv1zMpaUIJ5JYfememqUffNxuKY9cVosd0>V96D z{o;NBNW6GhR95>Z;)TuQnt4gpsLMAL7N9`%%@?w)fM;(@$S$hUc9? zEZgVZP|E7(y-4=U=U=g63@`hMYPK&2sb={z{K(SFSK>ma7c-Nkp%4i+MQ5GeWHGb#{J5OH5HB&x78s96vgIwDut+&ly$ zQxLetX#uIoy~<1uZdj&JC*pr=EYZI;*8ks$41x*?FpeQ0XZ|00YYq3>U10JelRTo(xTn!|rekDb(tl-RZ|5-i0L)f^!ODNv z-2dC}Pyqhee`&QMlQvZxmi*#P!X@+T(?5yK-Imxc_WSSOw~LoRI8m*Bm$xR$ zv({=&9KJ8qpG0Pku%8Y+(U>(Kbf+;!kVx|eMwARc_LCAYkzpAdO8z^M2@6dBr$pwT zw7N^h07xe)#$ZL^Za!D9)tF>ssz(LViQ?cuQl(m|w>th!C;D5fv1|Omz|m+`dHRSq zM`FQ2Vfg=vPINc`saPQ^(+8GBhJhlSE%O!`0P{~;edLO;=LghkE2*6A;7XVJ#jsM9 zj6ZauzqFb#W$gBNA>doh|3a%FFwJ-TU?@s=fQiiUKS;GX0K<#$Z>@fg4x_>a|C7l4 zTdN_f#s3erTHE#H;47OC^(ymW#{uB)E&g6Acug5`aQ;Dq2T?SfX z@gH!azoZ)Qe^;x&9e{qOEzvff$nVVznKYO^cl3u{jsJqm_S0HrM)&;ODU{zxf%UVX z)=l3mB4L-!K1{4cHkVeRpkR{y6|r@VOprTU|_pI|H+03=Ps_h&fsd3;yS6(~ zoO_q>9O2Zu&);P~PnPgKjm`KN&Sob-lzxMjob|`O&z(T062uyx4d%MfLy0OCrj(ou z*Sycio+=e(AD@f%ye}XsiWU@-oR3exFQk$wlT;g@Pj0_2+GL6W;NJYrTL<#E*)g@H)LP*Wdf(Q!b$!5svO?jhXS zcT@F_!w;M(0c479f~8GKy1ChfUhiLt%=$yM`&5PQ)A&mF>qCt%Vx>N`)M`KOV{NER zr6JbD>M+-1U2IgPG39?vWIP`mauKV{#VWe$;vXANSm-Uui`SRhADe2Us;tc_qL=3% zn_H*oMXW0}wqGAxdJ(JbL!`8E1rZTOhK3!`d$kW;iDCXlwVVGRRJ)MBD-?JXOn4On zcr7xNPk1Osbf^yW=r{xzk~E+2h%t$2v53g9Id!nL!f*&kacFsgpLRSPTzn$JfAuZo zEClp_CM)8OY@(I?g|zd1?yaa7D?)LPdg2MN1F--cxbQQBzO=$1NHLjz4XT zq#YeI8$AsTUdTMR~~n_7D<691^0Uzz#1z3jH-sVnx=ues;Z`rp_ZnmmcE|06sC@W zv96JYo-&VK=edEdF0cYJGPN`sx;C}226jPan!*-l=6@O?WfEX1WNj!9?1gL{J?u3Z z>?iLXoPhBlhxYOSlc_hI&-Z% zyZgHP2KonwfF;$?=*aNc#7L#<=*ZYuiP?C*%J}5;)Xdz>+`|0g(&F+eu*F(!uwCES z+S>WKwX^%P+jp-^5m;*d>a+velB1K;;O_43 z(nxR%?hpcm03kp~AV44Xo|!XqKFm7*;a%^ldaCZL-YTK*I%*e#iXPl<_MvM-&`GcHW|nrH5hIGh`nS zYx18Jhy2&$rWmCXV8Ch9BcRCSXQ5PRjAgMnWc6|=QUCS0u`;%_>Ip0>ao#XIk7X30 zkm?jF2JF8k1jW_1AWQ!g*wL%U6(RLrhNSD@u@EhV$p{znhkhJTw)vcC>eo}j&qz16 zEiF?iH=QDCrLU8UGN*}4RxAu#m2t;kiAa0ZO%88Du44!&SypMyWM1!#te}iuH4~+Y zUUBl-z_4Xx^i|D@ctb$IRQuydp7l^{{nijXpf2N#BDYgjUXWTk#O9)rSWocA*(acYQk-c6IjBQ5d z4k|p>bXUDs(d=4xH)iSpdB%U1vGu{It&nmeKkaaIc6QEb#eStau1VNw7jcv4X}6;A zh<$297pyOCN4CfDgc}jM`4BrxyXrC~6_YhqZM(Dmo_K%f?B1=uesNco zF*fp+NkSLEOwCl+e$`!q&VRpxW;W-9L;Zu}&rVywuf-@W_3zpx=oxVTyM2XEo-Hz9 z8c%^a0>wnZ&lxKzBWrG-lu=T)OA~mvNSP(y4uN_&TclT)IUmoe2F|L~LkGu%pfb$I zfP>Iaq_wF8vT9e=wi9ZLaTE^IO&xz5rhK1hplkn%;dB!3B>puRTT<5f~;2tJoq(+$% z*%cCCb&BXnQwY*dA&`WW>~*v%Q0zha90Wuq?jHkC3@>{)j*VdBJLnIQDK(+2fUwrOaCu)n?yTyF$8y-IY6+%YcKCxXjC!zUfj=do1Q#B|Qi3O*f)@@9`x6iL&mI6X|&V6J;s16l*2A{U^%mhKO!0 zB~VV5q5P><{NG8|B(!&t$&oM?iySxXf9WP>`6aPauour$|s4Pm7aNHJnv4^3bC@;L<`d_*Uz7Xj>HpA8u#qr) zFa(HOeT)f>9Z4$J(n>!eL}7;1YG+NaRPquPYGSDpItZ+kK6--wh#`;+hU#+69b#}M zA?lPKtk6aRK`@>%Fb6rMdgyOP#56*aun5%8b0zqrsqC4U>v}XLVTAPnc9p&fnXdWO z8uFxC>pc0CxW|vxF+}sElp}RC3@}`&MKE{`vQFbMf@>Okp_B(se2a{V`vKPtg$)5^ zwYL)pK9i@E`-f@@Zx01V7ms^DtdO&FJDL!;kQr;G5z{3@iIYOzsEz&^=hNLsBqX_` zM@jD%>#ueU=5WyZT}RDa4U3pCHoh$ljb11XQ#d3##0KGQ?R$jPGch(eZNG`&_BiYt zpyf6=4YjB@M(7@mgHj)9?uaMT;#|*hZ~Fa0A+JCfq3)M=W>49Ef~|w@FKZnlpnK)v z#pw6HJwk6Gn5`noi2S(hA-5P20xPSEvcscP`liTIi$aJBUcw_#go@&ylTM+EY2Pz<$$+`5yd(~EBYO+<9{y8)p_pq&A(g(YSQ%H$k3 zYiO7OC1M1e=#Qx7FBZw$HTCI$ zxlGEFpC!iIB!~tzyLviHf5b35Su)N4NI*@vjh* zA&D&F#f09KVXZmA#|~2e5Lch~e2Xwa^roBJmP&G-PJk_1SFz(y^P^yLFTXsTqY230 zk$7d5E$aiZihI9XbcI6@{fhOt(x2a4UNZjDI;1Cejm;?0(q8m{VB~Abt2`4KjP$C* zD++D}K2>bW6dhs0mfxahw+UWvFbA*WJbLzXiWMbCiE;bD15PtZ314sqk+V?U%iqph zPHKoEb{u$4z^Po1{v0hA8p!9>s4LZpePCp4Wuu8uHRbVs*T=Hgxv56sjtdzBsW}4S zpw-$M$K@y;n!5ENJnHe$e(dWL{gc34%a`hccI^A z21NRq4;==?0p9n4-!s4SCbj?YCqq&3IOxwP5zSDm=C`XqH52!61SKFL)!vVj~M-Zw<*o$bJ9DPgud7?pVk2$S+IKy!x9v z9iv70xFeA!AUhNdSsLN;Pe>B(hrjX&T>#f%PcL|tDWei1?jNUA5@h@;Ku#z+PB8$d z*~>K4i((xeCk52s95wt0*?lAQwE+=<>nrCF{`POw`}X(mkI`xuqhqsOn>dDQJ@qA2*6AGom2T*Tvw+n-Dms?6%pNVAk+OjaZNm## z5BAX^LF;02#v=aksDNIG>z&i=dPd?sfr2<+{r0N@dT_N{g=JJG1U-)#%^Q_cRVYwP z)A0jw+)ahdmZ605H#ArVhE@zJOd73x5fhe`yyy=MM=^1ze(j^g^H;|cE9=!)%aC?@ zNkdS~zF9k6C)nO1D?mti@98!(P>%FIFt6Gnqgc5#`&Dlcqa#e-+Zc=>-LUfg+`$1L_R1sf0GKajgRSZO?r@FAx;BjMF#gM`p4O7wD=Z?!L@@r&Y3vabQgW*l$!j;t3yrjB%E=4F%T?3Y>V3!?ejs-o%RBg*HxiIP z8Aqt;%fx85(hHou46kHndJdTX)X>a)34Mm{@xg{TbZ$|dDc!D>Y43b(k{>pHq>;j`9LqM~*uXWE- z$fGG{EzQ_Nv1yDhqC)r{t%qxu*}()=jW%}WR;?)C3%2zOAjiaUV&Od*!qh*DZ4y!M1}l+qj< z@Oi8$Q+I~uxjC9p6nRX=D{^(Urs9uQ+4r)7Z8CTBKOauuCk$Z1l0hq@;P8^crGzvz zeCK#V!t10yVNB>rdEl1ghehgwFuP`T>AR@J)U)q(t|@2)Lv^%Km{t;Y~+ zzS~vxYa30gC|!9zO+zI|KPJg6pCu0mX-%~H=70rfq@~5u1sSX1;txM3 zo9OzQUfxsv-poU&)7wVJ1M{OeY*FmoG@y^*lIK$0!=035hgpZ-0rR|&sQw|QNu!yI zSlOcT#+{-i11#zL+QW=nS@DbJqFIbeYbcQmsMn&aay#^A+D+?<)f4D6w>xJ4blCRM zt-WtxW$&cb>vYYda}aZz*zEKY>yr4@$xhhilh^fLjKD7;)v2yCR7^Witos#6vmD+K zme;K&-<>Fi8%@y}PtoJI*=_669rLd%Ew9H&rKf(2Z!@ktm!g+(sw~a7X`|Q%?%Uz*?Zz@=WJy;J{8_?PlFTGkj~{NUUN1P3N}Q;2AsRH;h65zQHT@ zuG576pJGGpo0fl24=3L8F4r$P+6~tYHP5E1D|J$f`SI>Q7^ul;hgsqoyrWF%Fdt4?Vrl0 zoDw*uVE<(?7}!R#qf;3)U7bH&+do~uGu`+!-Ap;tD*pNz%yb6Lbm!0X_RsY1%nUxw z3{%d&G8o28XZuW{LfX@>(1qEZ*@S>GY!0S*@wpY#ISkA>aof4A{<)o?0ikMN)|I&< z@%iKW9un>Ov;6t<*(MHzdF{UWYs!V2r_#%y&$mGffAbeAjTWZr7hawg0GGO_l#57a zizpJ+LY|B01B;lK18BR8xKvB{5{vj|OT<)*guzSX153a1mnff?z`JwQRLcxz%d)~nvoC_r{rtgSL{vwUlElIT^XSTxdMpmI< zMJ0HJd8s$JZ$*=8H9QwzyT?lsWE)v)FryzXCMsF^w!u)7}eT;O}T`axnNGFa&?=SEb)MgSH;X5CP7 zaDLMBMp{8$s>EhyaB4>IX0BO8&cJ5jrE9_SW+|4277|HW@K$~UUWpr6B5A8}jzq^Z zLMBB^z8O_Jt=6p=IUET!P!mP`-_|e{NlP%kd?779nWE|4f%8c0Z&ePD3zs zAluLxON&OoR6m z7Y}MekvKchdap63Ll2cZQ1K*>=CzKPxsCwXlphxlD;EzL_KrU_)BY>?W<`i3bG;&Z zZ1w2?GyF)qs2EeaP`4F~;o-6S334pWgVW z(tHhy3B~MP1j@sUF&CQA+5lK?(lo;(pk#A|kPr~v;+YmUR`4~zPI4P6=`kGky%~V@ zrC=u@d6#|%^|h$kR{Z+-eo_^3MxJuEkaBvj7F)J=Rx^B_wYRtCwr5OzVYjs)qKR5{ zfH@zES)eHsw1+aIiSp_C>%uU|MG_6zjQXh%(YP5kifbR*jF}l?n0I~QUX6&bhY|=m zg{GWUf)R9@F;6%tWkPoe7Qg>|L3>MzSp!BWC`2gYqBL$kL%+WI;)Xt{iG*&sdwdx? z8TzwX@5Hzm5S(%;&xL5TP0^f!?y`r{X1V`+09DBy(~#@p-|hx|3Sv~@Md#e9paW*n zHO2upmIu7~7vaIhN6D)Z$nhTb74-o<=3cqo;sKsJrgF;JRT9z!?oXK2wGJ-krTO<} z#v{7LpYWHc`9h9gZV008l3pp7Fq!?odlV?#U~}oyIIew)^j{(KzuFEi zZRw8$<3YR>rQP27A{c|{;134iFM`zHwSn78k9`XMUlC2nfn1L|5M;>E74YMo%Euog z7JuU4To~hOr)#^Y2tyARO+WXgb`LF(;-tV;`xI#*pz|S2^~J~OgCoh(`&KE8y9Nwn z0ItYZf2#iCE7cK=h|6|k@lZAvk5RqcaOp@skxan-cw_0SVhXK%GJ{d;Az3Ul6Egc~ z*aQMijxnePF^jyGMh`b_j9$D1vH*C zPZuGsNs*{{-WI8$Zmy;`-tn)>%GsBe;7aY^=rf#U5X%PrYPM~ zpG+RcnFvY?`R}>qrkgi*vpE89Pj)tM9Tv&8ppo8w@{6PVCdn*iqxQ+`lt{eG+9tkm z3eFlMzOAph5yX2k`dyjIPuJQEwGlWkRN{ZE4(wd}c4YHJ?!W>+42zT*1DzSBOk%Rg zBJteFO8z(1ap9ftqg8T8?{AA3Hb3|W_|4JV;hbr}EFlhnK~n}r19wpdL{VP@d}RAC z)p5l`7F)J;p#+aE-9rvv9%*q{Er|V<>InaF{YrIA_f#NvA^bi{uBN-FKplAZ>pxUS z-gZ1pSgHbQ%>SS|-YByW+`!P8k`r%KINJV8b*y((fz;o5{g>*f%Fmv!P0sVh=T=Sl zYr6M*%y9Xwy7=vi_X7L&@vVk5B9YJH;uFK2rX0a5)p1Fb%=b=9i5_c3Q<=TvmFk#N z&aa~`_WMo;L+HO$M}Z7pEe$`*KPBB?~N!Dh?GAQ*LB<*ySAh1&RQM6)xi7m=H;7BTEg#GUU?GBZ7l0Obb1a`EED;ZL_i@BCf zB-TqwRDMbB=5gs~wZ~n78^heaPbKe-N2qGA;W=XkeRZY0z7mabmJm=Jy((#Tw`^0y zRZ_UMThgR#6TSY#h6~zhY`DAIJ~iaZEl=1a0`C{Z+G5vnF>pxS863m}hINX$c;L9F zB-AKA(P1HQTbQ2Cj5BS!4b%!gxW8-2)b#F{+E_>LK4+3J0beh+piQFf#4gv?RR`Bg z`AxoYm*Zv!~&dsz|T z0aRQnojdT4&A!v4kT{BSlRqC1=`R#RcqbN}Mx??45M0|V*xixrP?eMYqJA)l+X)EA zUr7Q8DGnWvFNSbMnp|Ju@?`nHeK+U5^;cS0>Nr3%P@aEpL+P&!EjCJY5604}<8t4= z%xsk28_QdNM}G2J?&!wUKU4=*lXVg~{ij_fJ+xQDM>!B_)62hXX4+>SA;1yc3EqE*rv#AO|`^f*yqN z+`npqX0p-gF#lA9AUBc7d{_X%ZzXCy!QP5idcql!~$NjV4mcL^MaNZ+-zY{`nL+5D+|ZS{;$>gQ2#U9OsS zYf{n9Xr*>Z&MXW6EvOH(LMwy3BYyLyga||fLO+>yZ}frOuhc@&5cueL7v6Hag&UJ7 zquTk+``F2kZY&3Ad>7$|+h2YC{>N}*zTKH1gOc_6$;#>oIMZQo9CXw+am}@0THB_*R+3%rm5O}gAF%z4) z%pFT+Mo``60_3|;>Lf@SH|*x!eRT{z*)PbYd%yL~+8P*=;VZh%V`6FYwg6Z94iL{L zY_nvVCJChak*nE@SzZz}*VS1>n9=h?BBkbL|H6Him z=*Je?Xuj7K;H=xa7I;B(M1Y6w%^J6-wr#@^ztJ-7=z`j$r_>q>9j%6gWVZ-L+3?HAg7_D&;YW4=wVspf@%Dv88}Mr2|+yU~1Z z(RX$nA@BejgQOo1o&Bd;=9@AhUx#r|`gxRWuwg#>*E|F}-wx@rs)|Fr?0#j7K^-OgZ1fMNAdF4Fb%1Rt=tXwH1Z7_o)GmBCD|8rK56Y<)#?OyR7vUL7#VrY&(M$z+I2QMuDTMd4ILHL~#K_ zoX)G9y3I`j8{q7|JMe_7f8F(hz9jFPu!1|Se;0Rl+u3Jf;=4HEo}25hYXVmL$3~}; zXDuf|V^{E5OZA`TVOpyL<61aJ&NN&DeGM*~19QKRt_F`ZG+scZjo+xc^HoA__~~r1 zFLSxpCsTd_IgYI0*y zSP(Cg)sLR?QpO#6m#xgt9v4~fm=FPv zQLO>+$<-kUSi%@BRkHn#>uG^iA+AhD;&nk@iov%WAH}K?$J$&Une??;9zUVUCMazM z3R&C5;RsS?BaDe#Mas3=8DVwwAo|CE!a4-t z-LGlXWffj>^7@tO8U~uCJDP4l`FNSgpw&?<# zsp&}B!n6|lVUCZrgtVkj6GQxT?e%O!>C*#|BR8bfGx=}sx5Jo@bDAh;TWs?u>s6Ol z3hR*OXt#>G+r_(A3em{b+6n83rr)fr;B->XiSeo)il_q5XLkr^7EIOpB30p|K_!#% zb7MTf>T-Ha-tdh#b9+#AQUF8motlcVdNZc__Rh$C4T5An)o##SH;3w8qT1084dZOA z8d-0T(Sit@biHTsLtY-&8RLDu#=m}zr&*2OHo6x=*$@P&D>>MgC*mRT>;#rHpE%I7 zsU{(VEJ_K&NA08(4cDT0V4MzzZ;fAx+$BasGD|;6Iqb!iek9z!Si zp%)yk%~P>l;=Sx7lHrpO{ar z+*_O3mquTWzBZ9z$kH`FT3Gc6v-z4H2otpR8`` zspKD8FVF5g`>KJTe2JKBsdtC!8UGvt#T&VbH3qshK*r#&95T3Qwn1C}n&<9ZV$}M$ zoMAW?RYJk+jI!ay@kY2_5y-m8UJpzLEqR^IwTUv!EJ>RfTfw-Xjm!W_WXuIN7^Dm+ zze_S`$>((~n7h2&NST}Q-Q^xWT+0xN%lZncJL1rfXWEXJ+jq(jdg99_6e>Sz%BPde zC~cls$;CXL*n^6j*9*nFB6yyQT5fT1%?^-XaG?5*HLNPU#GCMCa3F(g6 zDpaLVZf_-9&Yte`Ib8%yp0}7*GEUSi*lR>Dv7#|J%jl@9wbbl$8O6*q=jU<7Ij^d4 zR&o)_$aFE&b8^K*%3^s5X{`p)bG7|_z4glF{^K-b!3WT(3oin{3fxrAgG2ruUn}XI zV2`-PmhM^)d0Q`^d@lbW#a+SA_=B0idYm5>n0rt&Q<2?>rh{+&<#_qWqCM60x^SOS zzIHuHObYUgiDz4EK1`Zl_zG6HO&LAxT+y%K=_9{ zb6e%zBoykc>qjNZO7nVo%>ju`pT0^UB}=coIfAFi?4a2e&VS{Fq#vvuQQz=wY40}E zoj}+K1*K=nKJjkyrhep&@9BK5WGefte;-e`zCcvljTQv!m!MNy!iivm5nN?dT)pk|W;5 zGCzro16Fx$ z*;c-%4D6JBkyTP+;J24FpLt_~@7*4n&je>g6wwpxJBhdF=qgPFS*XR9a@c<^ zWw|Tmd?Vj`kXI>QHp;8lo;q*DqL8n2#QWhD0joc}+$Q*du5{4OJen^ZKHc(dNlzlv z;ZfIisDhW3gX_T8IkjE!@NyJrQr{+HA8F6)KPKl%?W|=@RTNFaAW3TQ!rY@O`$$OJ_!jTrfpf{3ZAS&1%poZ6Cd+N+7zO`KvCO-MbhK& zcMAO4WGX}WbeMV@8$$(ll)IA52`RoKcD7mGT+I^WGVX{ARGvmepPihKEJRa075Td^9Pk4yrso{`2~)R^+QE=+xub1vniL|k8EjB4um z-3Kz1WN7m92OVzJg3gevf#J-5n|JPE zJ_AxJ?^=L=Jc_zJeW~HOVq2fS>5tpagW3FBS8Sh<^F*L@$18s?^50nRmZ3ZAiYMWbtSzI| zA(gYRf!xpQ6Nn#A7-+N2jEn{7+{!?%O>TzIj|^B#vN(aQQq)5uy3Hgc;Q^GAA&bN25&Zz zP+J8jr@Z%ewI_DsfXj2>p_k5kGwakb();r~zTaFljxtmE_B@is#uzib`7z}C+ka^( z#JEnl8-h>In$$0$nmQ6ZvbZ+;;Vn{SwdsZDUrFA39-hPRwMlHbaI zwtqf_J#@WC1>piw8%cq-ET0ME%J6TSqh^`(r+e|*6}v@Re`@W4BfSA$P9fvs*o?m2h5y-t@KT>fzF^}qjfbNS;>(_}+9 zs*8lpUreh8uXXrrTtv*gG=IoiXoDBCnoUgR?%xMCkJ)eHYNCU*40vV6<;+)~&p_&xW-cK^*$NyPQwr)_Qu+-y_6!q6H^{HVXH`UqvA zh-2GiTc+t`B=!h^J}y}~v_Dfu2L9kD|5D_}bGJow@=W@RJSR|}?^_84)H|^grY7*) zp2iYO^zZA+h>9?66p^v;q1Oym^oEyb7|L=W5R3UpH+7(s3hJmtLhPwqA(#?IHXMo{ zArYrfpQt?<%N#G&CK3spYOEb|%eN4w^DXBuYU8d7fE+pEX z@H|gETmLfK@`22;q&8rMP9s;P_m%3{`;V*t7ndBXRkZRbt(E7ud%B@u7*mE0qtS$A z+e=O?XDyn6-VdLzOYxdrUnf|Y(vG0M+F0|yyO!!2ZWQt4(ML&;+p1YHfL(9Gr|O#} zpC94Kd1|3MUQ<$b0nQ#(g?)JAPP&$xv#}>^bwOVTMN(9|*L>Rh)x@6pSG3;D;SKzz zi~ec(IG;LF(|EZdfNDp}sncSS<>H5-&QxGh&!RC=s)S#nW+yLUVluMiwcziv@ql3E z7$b|Ttvy!TpmMqiN(rOu;I#)JcJF zMH%t+nzDO}w=4glI$m~c>;1O8w=Vc?vJuXMLhIS6DNlmL)Tr;_IDFB0v%rfp(f*Nh z|Mc#=Oe@{Oka;F);{e(|N9zdVhK<8esU7(gU*+NTw=;t{%3m(3eYn@Ame_DRm5 z*G(-#ZsQO2(EeWn6X--=$-ozXI@3EWHSXERCS1b_#echDN)s5o=<5uMOoA0j+h@CD zGozEpM(`e5sL*w)VMOX!@4}a;Dh^+?oe5>6h~$)CjxrZcGho_;iJ_QzY47!zogwi~Gp#aM@F;K5+px|UE#t!>sp-e7 z?lm)JH?0u2Iz|Yi?T7|STlR!F6E7cZ2J3fg&U|9YWE$(7lXhF~TK~z0&(y4gS6klR z6O-^L>%3=LTmFeSv*?$adEeHyf~z2A@r9l_|K+yA`+jE0+nR;oo3^5tCuV5`=cZ5? zK^}?(3zVQ%Q~tMC@uBJeq&o6v_W7I%vDdz~;M>cnlU6u+rsqDUwU;x!QXPM#(FSUb znj)h67yn17pd()uVD%7`ti8vgMauL z6$=(G&mVbMo{6Hu50ih70QCBEBeyYfhtvPf)}kAXrmWe;xD?Dq+V`WFJ5sZ=1?!TF zy9L7@@RfMbO_NFG7ohX5lGNY-vKR9(P8+*~P=Z6j3t=?=JV%c7XD~Tgv<#Yr@H@sA z2{*maFf%L;1sHGdIQW`E=YTO{I9LP-;l%^Z2#gPVmtDi5(o4u#d@cjy8_1)k+aTlF ziT*(A;&I_KfR#aV<(%LnnMP(7t{Fb1rwe` z^Rfc1o^Q;l;!PEl0FP1L8pY_@;uIrLo#?6=8p>X~O?D?9W8Q@iMYutA@^%)O zkh4EDyutzjM9JNqK_kQ0rI;mK4F@g9wSWt)MW7~MNK~&zVym|4Hh#AZG9-QkT^;hc z+m0*D4#JMmW9j=wC<&Z(!b9IN6ee7rm!=#n0OhzaV0{?8E;cFtg(?+CM8hpO~p_RcUMz2kH+^(9^c@sS9`=)dP_lK$)8cWOK_x@?smM}9{)$P)9&Wvb;CNg7Tx zt0c$}ONOpVYC_LFB%h{>eT({M_;?tMwA$5p&m7#RuLa^k_1QHH+U4gFvy`9r=K7gQ zXu`0zhlVA_n@dF~hP{-H&Z;?LJ&aSP$I=Ab-Q2~nN-_Z_J5I>?ad|fdHWJ4dMTEtu z2VQMEEwHyha5Mo-7bH+h%93E2mmWDnh^)6VjFTTzsT#_ZPr9WiZQw&*SV4p8VUei| z_uYpfc-^7;yQ+sw5QIa^@gNcHBETBY;q$|hKb(79q2MI9vEN}EG#5&Y z)j<7rq>L%mEVq_AKwK=xctJJpnY_RRhXIvNeJzC8HLi3$wyBMMkQ>_Nvp}l3ChgK&m*A_ ze^{br&OP3dF$f`X4ktB=TJeBp7LbP10!GFh;n~P%@k)%ulHqO!MltR=W6BcCM+C9b z*m%Yg`6y9?WbeN1;AmS27~5RVg)!O!A;LiH!cZUphCP3TC@#&~4rU-674!l=nc;J# zgOH_R*n1N>0E?sf3K7yVTG{phkV5^*F8X@;jg0_8IDH96vGoYl>;yC-Vo@;$*q)A)LZJxIhCBk1>x z104Qvjc7Ucr#xxR;Wrams#{|`ge1dwWvJkT#8?aL$FLxTZJpMmP6k9)*RNc1R*J{r zDAREowUnsE&}(UHZb*TqWii?y)HGa$ zap6!+@-PcvC6OwVL^6^r{?T3bS;#c{(WRZjB1}nvo*9zkg*Rl&*STtFRG%~WtO_Foh=RLEg$886E zQ|)+{>s_{dmD(nJn=rvJ`?TezOnL&?d5{wrR~Jf+S!+#nZYjPGk=wuPP^4e9`-t$B zhQr{)4vZxcfe~_04n)ioKbWzuHMjyYSt>AsW}=uh#d#0N^Y``!s6Q4L(e}#}c+Cw* z!3^EskXqNB7@^E1fp*j}({fo)DiCPT@@JKy{QP0Ga-%npXwktD85TGdc{P+T5rz7R z`r4LM(3zrp$|_W{F=~}l%spuzi1RM&@|fJk7DmoC;r8Lc!fykZCRnxEXyJN=G!rGy zaDYIj$`0Zv;obV7Qfm4=`Z6AR(GIARQwrbgFx+^_|Gu&BGIvF@!X@WLsz53uq)mZ_ z4R!W2{x}i#ogBI*f93TMAN7kPBS(Qf$Wp}_z>dTe9(hPwUwGcB+&2C;+K$m**W}Xk z$QCpz&{{hgB*RY%C5Sw9zNaB84c~+nJ9A#e(`s^AZY;`W-@h~7X+ z0Evt=EPL=ILIGI6RO|qq!TXUKc!a>HwB-rIWc=$I7;RC>JJzHf-NbsZ(>d0pa=G$r z7D*xOU`bjHYPETUMfgJH(ansCx=q0fogWFoETXGX(0|`7m&d2?3!e5y7u!tVqTf zUTtA$4Jl9f(IhG8<~42(Tx&cIA13`Evo?;vqf@{b0R$0y6M^#fv35vr%b7JIiNDy) z8P8)6liA))^1W7Tr;3!0Wgxdmd_NimJ~t%Z>5J3A9iK=w2>0DRqV8FA~4hFw6hSjcj&Zd_Op8n?{xSe=or)K7$IQuvD2wY(7CG9IZwx- zsncah(Dk#Q^XvV`dZ*jAAk(g(?ivmJ(m+NI z8!x^xPembLgLgKkeuC`LzS|pC;$Qtf2>E{&U=@buMRx_Lbp=#)z5hVWHi-nRf9Fsl z6to}|%&xz_+GRf_6!KH(!=Ep~e}o*r;fLY~hi!lPK+^5DhY`*#98vWpOi0*?T{u!h zIO?M=dHcJl{Z(;0;h1;+m}cEE1nD6OnVKK$;$-LIXgcGCuwwT!*kpv=9F$VV5XiP$ z82C3M6HB(|Sw0p+ zrFgPaJrDhVQ9f6`DKQW1SSY9%ZYvyrQ)PA31!cGAJknHiJC%=Q{liZxQ?*vKyjTxN z;z)2A5EFZ-^OlG4swdPXH`_HK=j88S*J6`(m+0ULp2%~lwYk{_y(UUw`giu17w>W{ z&`tbl+UD1{IRiM0ZFyKk(#wi6*dOAF8S_$%S?fiK0uMWQL}q8D5;c2(9JSns+l8rQ}jpJU!H-&EecR&b7psKkQ;X(0_63(Z__q@U_6*2$5 zvE=A4(pZ0a=R^=t%M!qJ9oJ$2O&f(XWsddcl8@25TRk{7Y9MWKl6qO52Ns*rp4G(i zn)@GKiB=|aA!FF8E^G2-pYX)eFn}Pjl}oJ=p~TqXsK()y=uhu+_%PYNc67KR?<0JB z9BDsA;G!JW*y#&D>hAs=w899Cz2eE1RSSOi$krkMJxx?0AB-ye*5&WvS$r?h{{T1r zEAq!C`}-p8St!%9|dx0%&am>4YL+gT&2w9hV z4pV(uH-#=~z%%*`_0I#zeI#9_Eqs$DJ2dF7ZnNfV%1*_lH0VXU%ptSkXWMMH&C6wr- zQmgj_zAOz=OYc1)k#}%rZFFWY9=mSe^bFyWQbHdP!)BvG!`NWlM6C+BrVTMQ6-$Tq zDocR$rJ?(FKV+`6{Homns3FPiwZ=uZzkl#rvNh>b@@G$ z`?+qmIjwhciaEIN_67X8H@n4DOO*AJ=X+s0$1$GZe$xO^+Q97s`_;4gB`zb*Cdb;; zY8C{vh@)GyI8Wme@SUht^6h9n88#{CB=ErLeAQYE*wOX%_h&OB^LrZ!(R}^DtU=mfqD~VD}dth-;ct?dAt1>sGQ+WGwI zB6sV@Q>2LaWDi%&4tkH6cq^P@mB^3jt{Yy})e7+1xQ)bRE{hGi+3oMOe zpdbgnK`2m-qwVNE_&LMIw_!ESQUs;P!gtct6eSH|@lNWDzViZQ-0ONzNF!6FFG8^- z?#d{M%+i+>47TE`a)5exOR+4)$K0n|+v&?H63>Jyn(|acjEZU!M0$JROP*EzKw~X# z9_50JHDfob)m2m9-Hde#gUgO};?KPq8`ep7cpJ7EW|^DzdQmHzjui!&Th0vw#M`bN zyO}1GA1d^}*o;$UnUeNNknH-b3j8+F6{%oB=QIFTRZ4$YhcKz z0*~eh0Z%*uS%ZaINkoAe4cw&#k#PCrfvF+-I&71$W*GsQ&9?W*ecv{A?xjakU<|M; zTKnCQhW;Dyp^N5ig(|#+=$1Qbc=?Z7q%vfVNM7NjNjyN|IdyLdAl-Hr$ z^I!!P_IAL;|8+eHJ0^BE)@&V>+ud5Dl-LcE#JW*JQJM=RaoLkj_?EZ$OeMctPT^0YserpGrO!+3=(mG9v8mD zJvl+*bd6X81keP z*kUiN|6m5iEYgTS3{r2B$jlwYLxJwZfpg58ATvP0#>J#X61Z@qh$In*%pIW;Qwk)0 zh5 zxlRH>KA`MM25d31T+prxvCQLla*>HOykeb$z(pKDNjon76sXtoVorus3t)8eY?QzR z|27lh3GayxYrD7~2q07xVV*C27s5y@vO|GZsBR94{6cjz%f>1Ujis^zX!VXxGtf4WK7D1?=oBE0x?^cYwEbT1tp% zJIr#~nO9O+LIHeDFC(=w7F%?)c3n{4B3p1>$@ZcWT*zt$g4$pIGHa;0u@Eif{{+}o zC~*`7Ors=*)exy_%7zUFXZTVl-nS*LuW3yth3!`X+>+0}k8`UZv}#+AK7)(#3*uf8J%taH*;+En&^~)_RZZbqt34`E%%m@@( z0S(q*7*=GHIAjxOyg1!UV}jxsNs7WiUmIyN#+bKb2jrOJ$L_;u|Fj1F`ZxdV^y8*D z0_=u6vXPoCPfXbTrEd!bEkvEAG?t=p#dyPde?Xi9bvY3&$opKsl+LtyxjB4HGb#|m zQax%q$IO`g!V`+-(@asfeDqjGpQWhS7PEm31YAdo%x8M=V>_zJ4K^~TM~om76$ z*`GaaJh;*AcXfE&B@gQ+%~vVi`HZzi?I^}@0lh*Ckrq_!PNIyVu4YIq!*#3*0L7^wx&SgJq7uR=Ldal2 zNGs4DXTsVkel8E;%6oWn{?f4(kOOaN){h7lH|QG!S?C57#QA34N_%7;lsU zsTI0zVm?C^V9A(xMiLq!L;^63AP>8qBNU?079x=ICTN)2A_BEgk5Sioe-2LPH(@;riGB=IdUaR=_@+mHkmN%3AfF(LN;5F&uY93=t}#&Dc&!y$K!HF5_M?n4^E%Ml(3Ax{z| z74jt6F(O;?HZ;pNU{WMx(oz%xC2#T|RT3w65~xmqCw~$sgOW#}#YSAxC_P0elTs;{ zGFfy|QetCCC^1IvBqBDkQnZOycpx7`g(4__|Ei)baRy|1Ap|Fa-yDJ^6_ahy2x=}U z$_jum&t`Azl5N(;3K4TL8Iv;~b7OL$vnC=6te`VXb22ycG&M^hLXR=|(luXGsDdsx zzGN#ILN0)8_Rc62CTfME%n$ZO-UKK~fFSc$U>_@i63z{OtVR?SQ!m~RO%@0fNE3IQ zN`_Eye=^{5?r|>f(GvhcNSewMgsclNVLaUfbuytj(bG4lVhqevPBeqW8Ym)U<~%_M zzUJV5s(?9vb4XZd4gf*z%u~r&$9zOmW`g&OEN z?}w*Q(0Z8janMmO{FBfqL7VPCHv{qx|14peC?Ozw^E_cwMrU+c7*RK}>(x*$o4o8G zibtNBU=;pft?2Qa4)fTCC!6Qo0z0AVwv;|mnj5srbHKp=`N zr;s>-8OBiZz9UG76a(+T5O63g5urcsPJYd(~H6#YT6d%Z}$j=|6vadE{nKuu+t19upr4xW{xf`b!ZBnx5nB;eLB zuhfgTT1q>-c4{|{LkJ8lzE=ARZEI8{-~ur%a#nR$*Ee={Hld9ohb%rk(C~Z%xXH-v!=-1=EiEfECfrz6AFN9EzRVnBW%|#cSDRJlIOnYvs30EWeC%JP5|2` zx7}LLZj(&2N+^?-RkvKO+=6%1?jtj+SH+w|aVeH@Prz{@H|4UfUMlxt9qDYzkxsq~ z1WgA{s{r=Gr4&dP{&X&}#zK5ofOQ+#ffZ5cDs#IYi441^Y_)tMlH_IbQ z3W~Gi)@U|_OBi%N7Yu};Tu(so3b@Qn_w=kp8y{GY_n0MI_a-s|w;HZ*Urdi8i_^|2 zdWR`yMb1>q;uZ|>Cw4G_za(zUP{q#oQP*` zC_|ua7)*#?|H-%&kv2yM#3Q310AIoLT46APZ;=>EjG!}(?wG`G;i;rhkNB9K_1KU9 z_#(#XNTLLY*rZOJrq`xO7?*?=hs^Qdr3-#Xzl=CJ{P}YtaY>FTpfSW2$3TLPAllF* zU;cT?vPAQ&Wb54QX_^ssCFo2X5r ztDre&I=-lKqjwrlbdeb+ah_3{zuK9oS@)gcnInCy7LQU!DuED$&CK$kl@3f;keF18 z8mm?JsH=qoA}4m4`b7xCDoDB#IKh}Ma$bxntJj*Xn^HEn1vSB1SmK&B+S;z~x>dFs zulJg-|N9y?_1dq01FhwnuxBK&58EXGyGA_IR(v{VYM`ZEaX5N$B$oOkYV$TM+axwo zQ6{@10H!KOQX~-DHQ#zsnt};D0ea|D8co_zS`alUS|m_tu(wfZCL#rcbBz<5C&nY z1_KRPd@7`iAYi~siy;vbAu3V=Ip3f?;2Xv$A|p#iX`BnjsY5Z6Q%kN%67-~B-jfg{ zIuTll?l6=RbbAQ!z#+9{#=$d(=5z(Dbj{CVspc}C8^Os}=MK!A%@_5nb12Mb&l1Q4 z1o9#cn4AW7NLB?@B?w_9FzBE{P{xt;bK+zzNN3V5;SWSx(jq60@*r%0fH#jkfkWCo zN-|<`nw+HT=_cwEckG&Owb_E)#1*|>0@R7(+?xzx&^z4xKAa{F?wT2d&N{=GFMa>LVHBuwy zVr=n;l->j|){obFQ_642Vnnqdso9n@`4}VQEbn7Noa(chATiW9*#=VD^Bt+?=q#}m zzbr&!qUrs3+f=_eZF>*xwz!HqZRColgEL}>j6Iu7>xP7(&U`E-nor<-iG>ovLS%;B zf?SijX>mv|5_;UL`w`m`bwN*&qDPuXQc7+&S<|2v0rpUUAf zPLT?Te3NzT@|El9V+CY*b#Tv)+tqSyQ-!R+t~9S)MWjhOMax3?tm2x)+yaqcAcHfG zYb3KNLd+)pB<38f$i>aztM&`>l^GTm720<9;=u69t~hneL%IvZ47>nDL^`z9eQdY@ zFxSDOG2L*7ZVk_^&|4#XO(}#x@gwU93LrlcJ&w;Kjz3FXk=1=A{JI!fwL$ChmOttD zO>FNZ=aN}_uXF0(Y$3uwTwwj-0Krd`q@z6cTeDO_0D=&(6$l=j0FfbsEj$H5>;RT$ z1q@n|THrF`sMLyEO+X0qQ&5`7kt9o+Jc%-;%9Sizx_k*Urp%c%|7+T;dFISble@ko zL(ZieM3|7&yW;F%mb9*?3VA$JFch};!6-(Z?sBb> zOfQ0XeBT1}S19TVkZ{{ggSE4#Dy0tt{nD!Lu|^L9FaD$0eCp|ZrJR*sc$Fjs8O2%( z^|Lu&{Rm${+6!3)Z*?_9FHx+;NpzqT^NLcraAz1Lk&TrZ|8bU~u}cj=CFT`yi~Uy1 zVqCDt*C@THGKfUHDJEw_-eMo`cFRfTm8R%8oCOx0oqb(GbxQ;|ju+ z)Ei_C+?J|A5nlauueeEEeiviDFwJYO>O> zVXpD!=i5kARuM@t|A7`5c9(gQj86#Nh!Cq;*3e@kKSJ2VAiJE~WW*9rOmW2))09(^ z-3cY-rapC>XR8DMp#ZvO<_gSJUohhr9s{A|h_ecKf+q(}kT{q@uUeW=fp3U)6a{1* zxB?JB5nM0`1%M$lXf;py0D5^IdS}lW6!3?d6cWj8Rh5E(OC+{}xWdN#A}|LtK2ahs zT1_xB2m)xlm$QIFteEPWUB!HNDQ&@U_q%vM<{J&bR$vyeSse%?phhdOEV^~U>s^%y z5I60yikUSA0)Q~XEMH**Ezw;PaIQeL*JgW}|8LZ>=#)k$^pXeGpd)Cj%RlUemb%02 z@Bo|?2q7~i>`4t(V7q6)^nsE};~nq?2#=5?g6RrF$+Z7PkwN^D&O^cr!%8iS7oU%Q z`s%MQ(~}f4-&s+a3@4+nDKd7PBWB_hVQ0G5`u&lwuc^NJ}&1b(lOTkUL}7-^ zXay=p1&N0>2zZqUVw^yuHQzmuipF{r|A(G9K~5RwGz~%pyaEHnIH~SM9$;OsZqY?h zNCb#jl%Og|F)nXOMU1LA6<@4Zz+EtIL2JC@2wA}iU15=oaXeg9nt+Qa3JgZg_|%Fv z*owpXA%7xTpbKjxzKtBHj@x6OEpLg-Trw$R7aPJF@BpF9%psV=;li`{A`&Cm<_Ei5 z+#85?7)X5a4a-Z~enj&%VMsv(X^F!#4>n90aEu9VNdqvo6^%Ubi7!AfrWK6okY_Qo zV84Xscep^gWRfH;@Z_gG*-6dbi3phRB2E;(St)ZetatnD!WwE2Iy6KnRLnZ&3BVA} zc)Amx^n7SJ3HqIA=xdh>QQIC$Zn{go8sCFu~UZ$QiuVu5-mjy7o$`ubGUmfJLUg3JVnr ztrM|?Wo%=2C@9BDHY$sKtYH+@2y=$UvXkvBXS+ibK6*AwgZ&T((u&&Ds#bk#U2RKj z*1@;N!4RKi9-OwxSj7G`x4iAGZw>Q<*#367#6@XwkBeLwM1mBa*b`}!3tit@qB5?n zu63_l-PU3^CAE#NcfSkX|L}@;yyX3Ecgu_13T$`1>}_xQwCmpZ%6Go+qpyAMi{Jd} zcO~v)D|*Mf-vA4EzyvO^fe*}*{!Xm2|JCht8w_CwlUJ@0ZZLu`jNuGxn7J~uU|GBM;f&?C5kW6DHA`RUH1ZF4)Tt_E@5E>dP42IfNNIMyzK)k~p zPE}{dL;?`Iqrn^&gojUOde;;l6cFxEKyNmM4qd&BSiO^mL=3|aba3fx-K;1|st?-n0VN=wSg_{Nqf53#67gV?~U(^DcB}{Qb`3^L5R71 z!HI+ng^KS0?4D%;7rnp)B?`L0h2J$A%fi zD3U?0ZKff{|1fq_7dSC(J{4=mw{x1&9Y_keRo3OVXt>PR^rg*mAQZwX1<7`h^PSTM z6+S=jVe*alyz6}@`?iTNis6R=QbO8*n6Ae|ZYK`pqDDZu&z=#23PHZkuHn*n_P*4{ z8qkMCh-t>9Ol2Tzg{T6x+VXNAFOqXjo+Jtw$)9Nv=B+&*$6l@kD6EVsb1_%Y1)2?I zLm_#{kJL{75vwXR&{M?3E)umezak-0!~kPttdqkm9gS z_ZuXp0-X%VXKbMH7nN<*83(=QvSU~y%f9@r)S-#|7yAAcoe64=yE;ACP^lXX>`;m0e!W}4QEpcLTT(&L)lOiVZ1;&F3e_=vB76%EJ7PKc7qV;~?0SuuxC9yG8 zqBkluqk;QEd>!b2?e~AKB4-q&g(e6#De`8JrgQW{hM{nOL{@`~$cTh?gGvHFkzqjG z+fg z|A^6X0tJ8oB$x@Oa-dGSMh+K_`j_HVDkEk)KaTS$Yn=ljk^;g`;>ZYmwv<8KOOM}LIgxk zmI>)3Xc#1n84w62qL3Y^i5##B!3SJ&GzdC}N{C{Tqo7X1K^a9?3ic?1dq_u!6gQf3 z9HAhQy5S$G(LWZcC`d#KqcR2@>5Ipuj!`L<0>+L=QZPp2OW$Ep;O0$Z^&!4g|7mzI z2(}SDe((oFPz4gXP#=Ys+E-5d;Ro#mY6-QK07Xp*VG(4DPAd-Y+mu4-RYfD8J(zwo#RQK<%ydt^quLco--Jp z>*=2F37_$KVeKiO^=Y5?iJ#U*pF6>R=INjR37`Qgpy-vK1)5>|SrhUG{|G-F_xFdH3+6eaqh<`*SP~A zTB4@G2?pAT2uc&Y5e+>wfQh$PVrh52r9{DLE@=szZpA7@dZgxMBa&dGg0-YkDy35* z5*Zp;3HV=1I;BworBKQzM}P@`MGNkCo8$&ULCPndFp^*Kd7RT@oZv~Q@RLEo1&VZ} zHhOP3dJ=p^C(O|_A`x#!dX-x-24H|^UbzFr1OtltNq#T~hcj@*WSB;PGz!IOX#xq3 zhA#DFVv%}T?r~6;5(U6w1;EmmM{$H+N>W3yCkYjA@${FLQ3ZZb|0SB58guZfEx;Xd z5SNRJRK9eXe}D%vm6pFGS6uK1S_J~0dM}0APyrWNfLa(uHK+xXtU_Uy|1_c6rYOal zt8i(Wvnm={!mNpETvutSQB_fxTBxvkuI!XjmRYS2AXwfiSGXEk=E|)@ajlEtB1YA% z9AzU6aZgqNRlbA^+YuM^w3X>fZjXwmho+}qXcgSmr(tqCR^Unz6o4awm25|%F|v{m zazujSf}J-C^JodZz;@l12P5a3EwDjmXc!`N475O#sgXxJvk`2ENuNqWy)b~ZfEQrH z5F$IX62u9W_*SZ+LCXjuy73?haT-1wHyL4%Q7c2p7#kTx{}G$A3c?^i6mUdv@)Qqo zNGyvkE1N;x<_SUcVUJW4_JRqykO=^n5Ec0%RD(jjm_hRsi3Zn+Zzx7R(n?l4c0|cI zVf43aYqvxjHBD4A5An9vxE8Mva(@tZ{X<(xs(xT{G7=aUkZZI*;Y41ikW{;|B7;Za z;TI|Uwqa;3V%w2&k&}4f6}UjKgyJlKvAN{(889|_{Sjl^Ly`@bg%Mk56N`Y-Pz*6U zM}UVOSm-Umb9;ibg?SNy)Po5aw^?NHL}&C#9S18OlX3CkITAONa*j91|^)+>-J^OKZg|Ap1#7%6!K%peIr5m!Vzb%B;B z$QLH=5-q!Pv2$aDTzXj3_lzWm0ItWkkzr7fvl@~RBC%IkDJvFrW5Bp5h1s)IaDl>I zah#?)IHKq@r6ncxOJY~X8sDT~_fxmvim-=$PD%+50L$HqY0FhI-mnT{ak-NJq zCl-abgOx4Cn2WSR9($p@%UdL;g{3Fnu`Wq!5uFLNy|x-j`~VlErgKgtN={80vw#IhgMi|X&OMUK)!g%|6L%kL6o4Fv}MkXQ`FpRAI@cZ&bk8W;#hDKiHvP{xjkB$VI>7ONG0vnF^@S32df zX-LOz76qdkzLl^F=s{K{@kXb}6@W}C3V=yQu+Xdd&c>|BPuY^dvRRNU$xJa43nIQv zTP8vb3<{lQHc4S{_!AeDHOX8}Xi&Nc8 zDJA@8l4R6onjy$6Iy$zEaRqe1(Q(Dt{{&G@cVjb-JZvh|ELV#M6$2u~Tb39J?9Jpy zJO?t$fY;S8%`Vb$XCtD{o488vd^C696$zY-QE^4v5!ENK1}lih`&?wqd&x75I2~sT zvp6XmxqEgJj9~^c*SA1Zr_DgiIDoP^a`PS#Q2=J)*XtsLhAe4=Q`1U_r1xhq-BQ{h zOoti(wYew&{|r4*+oLV{DWsj*(Sg;UOo#f*JM4ke>ZjA|H#3_P&XG$4-a|Iv=$3nG z*#TJ+x1%2zZNjp_!evtcI|+Fzwq=@#(FtIT!rXo~2iOcC z+Wg(%*r$qZWQ_e1yb+I=8;Ow7ZD11(my!o{n=;T*wvaJ^$zTZ{5rGlHL7<>WKm>-8 zkus_PlAGBr%;lEyjgiJvw0!v5{*kcfv2lo~PF)MKSIRcB%RD>EWlrnRogL$@INY9y z+zJUW_eiWsN37vN&9wi>W- zlyJ8P;&fnf3x|^nT?e!)=T*W$Z*w#c&_@cM=wT|H8qH;(H++S~BDTGc%%|31#94yKapxDz;JH>TvQb0uc%$ zl?Hb!>X1-6@=@7vqQVIR;)~r&`J8E>5-AP!s?i1r;>LJGUafcg?7~h5uq)a`fV`s z2ThHdrxFE?APGpH|CO4t%pRpu1}`NmAeLrH2$DJoqTylPUh+3h250rKT3NOkXj>X+tf!oA1;FPeXce!qtG!q*M1elx0WqkheV?3LR#$v)Sj}50 zdmp7(z;`IX$ z)Wy&iI)_1GHGlg9+60Oj{JsSWI*6v?Rr|}oUd505f93qqU#8Me{ddaz*N^>2(iGXx z{oU{VuDSi+FaG1dh}CcY=RYXqum0<=TIlco@Bg3dFaPWh|MhSG06PEqAF=n({{R6( z;6Q=}4IV_8|4`vVh7BD)ga|^~M2ZzHUc{JD<3^4hJ$?ikQshXIB~6}0nNsCSmMvYr zgxN9TOqw-q-o%+x=T4qIamEB1ROnEmMU5UsnpEjhnLeF9g&I}rRH{&!Ud5VK>sGE^ zy?$+4RqR-@WzC*t2v+S{wr$158GJyX>jTJ&hrr8Ry=omw?b)2&^NdM_@W>$k{C98=2oy)I%OH|lYKeqEh}Phw5-Sw+X$ScV ziei;9AT%#S4L77KJrA`yVU%ck0VbL_zC#E_5D#{GDan*6bJ!MdTAlNFPOoD$|#lOFibJW{OLn8 zTgrfoXgFD+hCJkZV~By)kQ0P70Ko$Y0MS4t8bacn6A&mlXkraNzCgnd81##Vk~9}Z zh(|w+Xo89PhA?!{`C0%*6&2?YG|Cl7#1u&iFs&lgfl|#A#wuRLGs`IQ_#?<2(x8P) z|2p@KgODF!JRwtxzK9~xDhQIx||7Dp;dY4Yn)B(+cu1u>wwfq=kYp zlJT(_M{8)s#{@+zw~{4k$W~N=c#K2B8s=qY&FePp^t&H&?a`1WtI)TCT7X%hk#?^$ zB;ROi`4xm*kLmFaE6DB9(K#H>SKt)@&LcZed}Q+kV2a827%no{!4{Ikdp2g7$CS3_ zl6L52L{0`hQa)COfx(DoP&t8#klXYG$T~6cbBB?eC$Xv zY~~hMKA7T&OF*~L$osdal5xFLzA3d&97aTn(A z)*x3J6a<8o*;iJ86c-^x!}$s%seW|l&F;6_T`19XbNlb--1F&tQ|Y@jOqJLzBGJAK z;EH0a$dBu~b}~s+qJdDe9hW?jI|?-o7`M0*qzb|#D8bBl3aY^eIS@i%M1m3z)X^BC zmkbzCFGp|i(*Q%!KuO?-0|(0zzFH9pRS?j8FPfA3dRBoZSgT*~A{qz=|AC7iNdj~D zgJKlxqCe^I4<$5^4|XnyFC<9`20c6$1r+!IAt9^)WPE@sBIt(+Vr^B8D!`W97Q8g* z$ZW-91sZQ*Ml$N>7}Hwb_b)!SIe6rdm+4rCR@I6(oJM!p};D1CldWFp!J z!^Iu(j;A_V4y8CsQcjVI(6ORPvWALAu)=LPQ(qNwQHepEEMilmg(cYV09+2DjcQcW zA4WmJg1C|s3Ul5;G+9T2ID%;NQr6EuZZGI-4RR%$ZQqh4+0z@D@r*~|AL<9l+Q_}NoX)Z zW|S~L6!=-hoR+#;FjF9qV4lYS5j#(uz?nl}#TX0`3WQ8lb)Zm%2H8f791R3r2V!0( zTv|H>a=;MH0|q8kx=nIA>H*Ztl`Y6uA#&1!nV0|t)=cUNR&=cr%nR8VV7j|5%^`No zBt?Q+kk!#m3>DfeXjsMClY^GEA}|7;KxzO(k`^Sb2BAm=+O*S$eD8@jd8t$wYy#3LT?w^TZ3t11O#EU^eLh6m_X1_GP9&n zZ!>uSCx}!fPYh^P9c{=hSXo_&?FbXZ#BBx$(TECg*T4t92!Oz(USI%Wp;&UMp2jQT z2|HH2e+?X3cc4;2Sk11hstFK?8!?wk(_MlEqFOm%iBb4#!vx6#mIh0ZaT%eoDemfr z0pwr^{Xn11oiK)kz}JQl*NyoxGKF?Z;kl|f!kA;KK_qlr2_e|Zqy3vj4{`@H5^O@C zphYg>1JW?7!o~$WP$(?y5K?Fv6)OWodvIYpaf$C}|0Z3tA;U5(Quuo)?yUj8^kO0TeTb&T8%QL zwR&4zg+~rU6~_h;tg%h$M&x1_e<<*k#eK>Zm=73QmJ*kl;OIqVBBeAy5E0YR?j-iY z5HJ`co1M6*CNy}EP5Xinc1#OMWG8XC@Nt&ju!{#t;uX@}6(){1q+6ZusTF6mK}wNS zAmjqviO6OZROhdF`s<$}7NwNq(2FRLxZ-ae|MiDgp;II5Vh-+uQ;N5_ii+0}56#$Y z$q^EY7DF@>qbtT5779q4%e5EG{WstOj*yiromdozaTWfhH&AbhPXuOmzrk(Qc?q%w zP9s{xFuGw?ymw1|lftpaJ$KKcOBlt#B-L-ka+Y<<5j?DgS`7hs4x}M>{RVc5Mhb&l z7)qdn#Kl2YQSdp54btL_#JA2I=t0;g@}s)L0~ik_TrAG$&jSJK>gx#oDqx3;97WLs zl<}1pIh*JMaLm=pj7*qtNJvja;$3=f?6jS7R#QMLQ#39t@*Shs`GPS1lZ%^&Fi4aL zyu3Ev$839oj2kUbC18NC6S|j-5jg|m zK$AI<(YvA0JH19b21+2nI=ZIrkS`RWCKRbX?$Es$5jmJ6wP0F70NFp|F*2M1gX=)4 z^pik^s0BPAgohKOB#MGzkdvl^x9cl~1rvzZixHu#JqH>;8fYN%Tc13tJzfeI_QMtd z(ZD;PKPIeE5Zj@xbMM*>PtFeo5F=4I_~q9qU*Ns+rsig1yDFY zNsL1dU=gz`#xr_9LJ$B2phh%<0XF2uvf37)0T5&RDVzI(0!+Sh-AOZ@>INJ)c9OSV=}g$uzqK=i2!VTDHM8}*tas&f|x z$&Wc0$csXSzeI(_vB1CFvoTPGN;{h{N(h=nuM@+BE?}8V__S8&JwH??@gapUG6c$u zgstH-!1PT0D}}cFk|TgNfuJ_P+&0jeHNYDM!a2+qR0w)vg%KnJOV|^wN)c9QEA^n9 zV3V>WPn#($-TIOp|9KsUK+pD+D-aSP_G}1FoKFZrJ)}$#??6vCdQbeEj|&S=;^WWs z{7;{lkr=Gc`-G74WKh`}P@4)(x^Nmu3^1FxDfS`^t`dj_<>HE-0Z^sDcUl_{DDzRQd=B8K2@|zIT~CbPV5Vsq|-0(i>Gli zybA%#gOd~XK`vQr!Z*PoF|vgrC>A%|AS;L)%QK%bK!abby6CIcU{hI?lqQy%6}$2_ zu$+T2|692;E27Ml1a&ML){6p_&6;}Im33uWn`u{fg}0LdH@*8VzB94nd&)3ezb#9; z46gqI-MzwO}27irqhjTSF> z+LicBfqTO$z!gIHM63Bw?-I( zl0^mN5~Sn3*_*8nOxW% z|MNG{>yBC+Qs!wYH3i-H{jJeWi4yBTON9Ym+QUvofDFh)t$kgCSR^pOJEY6sFm01P zWW*g&!t-4|?In=JILbTKsX+l=S z*QXrDaqPeWF5okr+|YBY%Y6{*``gG9viS)&`32(W5M9!pQgDqr!O=Y0S}78|JC@>UM?bB?QgGOHj94rIz^#IWBe;bfPz4U|o;<<3Avluo zDm3gE#*l?u2&>A!k-p}NLprG&FU(hAaM?S0;wTo-ScMgxHQyE}mZZy(pdEst|AiC+ zjsvaJtl`SUAl77rg4+2^gCvSJH4Z>e2t7`fwmHR>Q-hiV-ZoXORR!71uUZvgiV`58 zsaX}DSAY^DNUBIE(vGsHUS`NwaD-5RlHvT`L)<^gR7;pzWQgRQk+LxrbV1_jwzk7f zs_QdXL$z8sO;8xNS$2g%t2tPc+_L*rBQRAAT9i$8=jEW^{)vphI=d^iQ2}mSl!cae z=4ay=Vl=sDnOR}@?dO8tpLlkOfNqv~Gw6n{qM=Y|W^w3==45F=Xo|+@jQ%aZDAR~e zER6RFr!ZL?Y(S45h>(V9q6pXdLKmo$&*+T^k+Ns$|FT-2;GsR9 z3NmG2|2$MjNK~rW*PW&iR-KBNcIuOm7oq7E)VaBR>F9)uh@T2ztM2H#QroVEhzF>- zA4o8zM&Sk~B0lA6!E%wa%7B!9YqJi=s?G^9Bt`<2>xOvhz2*o)F&=(0z&ul42z~2R zl@rq>1rDSqOOS*Sbhmi=4_=O4?&QHdI~S@GY}-UY-h7eNjD%OEkVVsz36-mz!8&La zI@AQox3rKVfrRjRUt3nr1^WZuM5$6sZ14dDj1pDWe9a;`%--f9BpHROmP=4L97wnW z#`Gu~#KKd=Hz zwVox`;B%QDFo>w;syX6~vI2QHP|K2b@<;1cZ4&dfa)jW9aS!8qg}&)F5=*zHw6-1V zZ&3CY)iD$w6Q?HNv@|pXm)zwT!vhNLJIOS)|9piKG8B)>KKwOEFtDK`h{H%21M}*I zTkuD4n_~)i;h}v4ks4K+bXjB*8(SP(;jGFp9ORHeogT;HGI?({wE~u?g^loQk@~7% zl(&T=up!{PQaG^{!D)}vMBPLbNrp!4Y{Wc^97-_aM(gr#|0BXy5|_inT(2QVKv+*) zE!k`fm*mU7e1=@CN)QjIrTpy@koG zL&Uodw&Xt4Ss4W7+V&QbHwgCm>EENA#_>zoDhZ!Ong~Pv!otS$xBkF`R3jov21Oww z`JRb4SJx3(i7kDMv7I-C6owP&u#Nl4y&>M7Zgx%yFY^NeWo-pg1K$85x*0lZ4tMu& z0^D=m7R&SXnFNM@_XMDYBX14i-pv}|`Rc1B_1AUCen&S)@~M4iu|&kLi%R&|LvDjf zuR1*(O6)sPKTu2PL|FZvxS>S@GEKpSd`|Dg8hop{LI&e#+1TdN6iXId7J zX>@1YVR#SXbG=coZ6_AtYMjML7~ul=T}0RTR%|q;FFgeq_zED?8T?v;u88O*#ymqf zPREX|Z9<9S`V-Sm@IgK_8anTTVfvb{R&CYnAo;r<>nt8KLH_lUf3=w{L^r|pxekw} zt9ir)C6jjIXpgTD?)l8sdeI}*kN;?*k5r?FXr$-!$7zHXs2uG07hx#*R0Y?eJ%S|$ zCzdN-3WD%kf_>C2sdvY8fmcVc=O;jb0d6ufk*gQaqBqFP-*&3+0Q47o=G*nAmXk-%R)c zY-M~4Y%r07HZTjET`3@d7*^%SiVSIr4eW&~1BW39Ie>_m>D7=OU(#d%5{Cv2R-7nA z3@LKtNNFZdqD-lBCCipBU&4$jb0*E2HgDq0sdFdKo<4s94Js67!&_q*N&<$4!ku2V zK#dwyLE^3<5Hm!om1}DysuLYO=(wvDB~>sW!oYNkmJy`5XypP+GU6X#PfPt#5NBb9 zKcFJc;Y#r;$)<2Uo#4WDDk?(?DBVI8nFis&xsDoPWouWW6M#oC3<>6y)>T&Lf~Yih z6k@D!xk8L2X==zgwho`=kZ7x|Dxvde|IKTV_fd($MLHUm8#SJ>yC|8A_@x!Dt`S!i z52@!{|Nj2}0~nxy0}@yu zfu5m8P<{($aEp9sEw^Arge6EJf+X!=VS<$5X9;Kuq81&95@sl2gax6P;)NnjIMQj+ zfw$smE0UxhZTT6&A%hswn2``Yws3-oD&nXj5cg@gqk&5@*`$+CLK&r$Q}VQ67pg#l zN-HPs*rk_WCW#zhV3wp3nP;N8!lQx1lz5*--7$!b@i>WsZYt`A}C3XqB#U^Avr=yXi}8YOKXI-;>V4StaUFS zYteECLqpWdiWL<6%LPM&WU}dw1h3WEa1k>FnsjD$SpzB<8B|IYtT^UchM553@SK0t zfM&oMgxj*qFS~3qhQ&aV1}4mWAu~yF>Wp7XURKaWFX}oBjBgJAJeLB4+=AiJ5fvbb zygr}Qvq)p)5}_1pc(O}D|AEcHsBo;9rq&6WM8g*+xLA!LE}>MhhGR|Cods7ET-Sz& zZkQPu7)oh|?vjv!p>ya`hmh{>hnDUJk(7=hBt^O#q(MNCZjdl|{eH%G&R;lZt-bfU z_H_f&lquYVQ{j+d{_VxA)P@91-b4f_frsm)K~2c%tHnm1LhB)0a8yH|;6#>XfnVzs z=PdxI)#&csx)JVZB)jh`@u9=?^pRUws3HXp`;jy~AIE=N_!*3^X+1mGCBnB11xrza zc!*$6LzO0IuA#~%iN4Ng_=O4GEa_Wg_YW!w73})K&o=SBUg5y?0m@h}5%L0}=Z$eL zph_GSVv+ugHZG+PE(WOLfAmA<`P~5^qHUim(BxS~xUOH9tILY*JV^5<&&OXCT=9Q)R7LW%grbc%e-8b{n zmr_Yo%ke>n2;_hb3Hw;O20PkWC57fd;r{;FD~5RZ0NgKCw{%a`c1*W;j?Lg+g-149 zT(6>x6F3WY;!MsUJcx|8Ldx>=LvdDc>Y&3ZBb5LwImWXVy;AA>1-P>A)D)j=6UQ9v(c;77Xce}2C)^0?i^Uy7;+ z$5!JEFQdNvsBwdPo8wb)w^4E}r%>Xuq&*T}jvJ^a;CXkAZ8gT!B%d&mm8r8TNgg5X zYUaYd?FjZ2F$m;Bl#^PlNFD{#XNwi0;_~t5oSnC0=3#79H6uG_jRO>z zD8T{lmyBt&{2_~y569q-zY8%{uBj*z7`4N;5UHe}DYq!rV74`1 zS##g2f&dg`T3>Q-tIGONO%>R2Ac$8n2+YrwKL-=v(bJ*ZtB}u^Dk0`Eev9g;lT= zkHUVXnx5z->0#a|8_n`JFs83=bBX!d@#`$cPwccJg?Md73h_pKBL_ zn+qp^9kNPSmp%+%|IC{G%xuyIVBsF6Y0>4T=ucs*G!GXegh^Gv%Za;j)S5QF^d~t2 z+H5asyvSv1-!(#%GZ-C|!y_>EbK0=^+8ivS*@>xW^tLAKl~5K$!66{7tx#qSw*{~Z zvN(FZugk;uNx?)ymNLRUy3_n$obiD%(LX^K!z4n@PU$2Dqh7X`tN|f>hLhd@S!~K3 z*C6nZ$Lo~rg*eT298@Q6mVN9#gtf-ruBSm0-g()tnZo%tm3+uh>Ut95gu!v*ZcR2` zKjs-4SGIy5Z(&_1;cBiBl^SL{+6$mk<52yzJ`8+$@(}-$zBq8efUkSpb}=Ik#s;l}3TVRQl^nTfD5Dd&nAD6xIjh!J%hHlMo(Jo&VMq@(we#$* zn$&=+IEJc0SC2o8@V6CI-LT_iXts_P{F><$C81Fv_SI-0C9w0LnmWWMPrEM?q(Mr4 zC4=mG`a{o5X*?Yo=H#2QPr|~BV6KL^-&3r;!m{4OC7a#i_#A_K;YFnS>Lv`M-Sq4# zOAYJm!uvRNs{)?B9+;)7YVIy2_wt8vvdQ;CE!4{_a~b=kbDNgzn$*DVGWCPhUhUrfcwFa{M5WZeSg`D=}j_4e56$KHpRf3Cm({nbzS_cH5C#lj?mCwXiNy;&|o^UvP1zl^9K2MN{@ zDJ`djjM&uSMwpUlyu@Uj9AP=ZlZ;7-qEK=^#?WibSkyQ)V!Pp=(KNq*AcL^3P=x+j zpXajArO9f@@-Cu(XPu0U(hqjrTx3<^2 zfwVb6&dPS-V+KHU`qe}#g!(KasYnrGRGh33?su_FG8e{bVj_@j_{$6@ z86$gSFOsvAgbN5F)-xv*jo>E^x2li>F%@s|SO|@&60An@JVwzD!O4^5-YL+&u~KfP z#%WN2l81cfJjlfh4Bak`*KaWPWL13Q%9MARhgdJP-0FqYsl7u(LT-&bE9G3@_kf7u zeB{h3M_(*geuwm{7Y%lAtT0XP$_eH?vi~8ONH21ET71D6vG85!|@7co)rDn z<)e1^mv=}J@N_Shmzm7LL8Zi9NMbfjT2eI{t921d3$yoIbn6rJ3_a5RAzB6|s&mY6 zrf6M+sf>uWpf)6I7enBxhbM#uq{(t_Y^DEkjJbGHkxrf`qy(~V`Yb|D2!^L&5e5<_ zZxWCfN$eA^v*Nq_uAg0mFA}ESZKcp`g43a_TFd~W0BEzxUs#!u1HxqG$UXdU^;ULW zd=0Ss3bC#L4C!GKns4`v-@Yw=506q12wWj}#D%o)B9%il&O)pU6v%gp;WDhI?o3^- zOpx9}v$;JQZ-q#KA8K-8kPr;`<6HXy6c!1qh~KtAg+=bD?1K2-b8*(Ei;3c9b?Y7D zA0E6a%&XYMwQTS7OI(XfT?N8l8Cj^i+X%WFDOOpxsDt!Kkv#cK>O&vBp_+VMMH2^1 zI8p$X(Ydea?UnU!b++IAcQpwT7-QEZWC^zN>WH+Qo0nV zOhS?~Ks%901F>RDmSg5Oepif4mOc24Ok!0I1%sdr;`COz70;ns=IYaAg?f-J(%7G* z>~ty3bjXM(Yr%==qjXFb7{07%7$b$9>43Q&_Cgq2arleF(&j=F6%Kjz!)bkva5Ija zd@Z8j-pSvC4Q6&gO^!6o&9q#*wu|RmZyAP?4=>&y=2<-3aYa4l$ynV z!yhepJpAyN5IA>Octlu8V_!KiRV{WO1dO(y%7_Jirlqn3j4iNR!ngk|7;FC8l`BOt6J zDD;E49-IdA1b-5w#}yoifh{+S<`13S#gOI%wNWZYU7}(WkF^A1gG7u_zo&WuLk!St zTWAg=H|N+X7l1x=UFw+PGX_;;Mce~MUkV~|jE4B+;K>de>I9pqdOPsktmdmWl4fFr zFSIwpJ_S++_wK63TFpbzB-x3M(N%mWE1iwQoB@aGh!7Mu&p2k%(@~fo&pL0uwo2+; z{L!Fa)Fm^xO3cC`yP)J6ViJghLqTx~q_%fW!t#^zL?`=8t+{%>OS4lZF&pY4o>WN< z^SvK}!pdx3*$+KZL$8+Ch*#TW^-892fXkqbW|gvpFe`rc(ofU=+PKU}C*Ceg#DqB) zT|ZMO>AL;tM(zwe|8$#)_qF%rA@&)fG@0OZYSos;N?D)FsKlc%Y_?{F3x&CoAIVr# z=AUf<36a~FiTYuW8%infLkyWCJXdevR5>Dg3f|>CSg&G10@SGI16K`HHm2beg_~RI z7(FSzIZMg25KzoEK|6JUO5`O#2}e68uwV+Su=mzqKez^ye9Xv{V;<3qQ-)dBrKodr zHuohczbAA$$vo|WGe6??T*7+_UXg_-6>=@=!ptBWAeF>vR4dX8(U%>#o3a7OBkUOe zt+AQtP!y_=*QHsyLzs;=}8Z{ z$#@G4a_@)!7P^4WGvE0qu!}c9^tQPd58hO6Jx`$=E_I=IGWoE0!CoB!T8RHG0NAR7 zv)LR)Fn~xx1UPz$m<^YVT^oP$qV;VJgUSQ}isnq%hnfkpc6(Q@>1-li=!TZ7*@Zs- zKTgzQ=W-i5oOy2(fpaX|K;F^IKtz*q-x__~%n1U6tn}Q=tO^hB`tD|t!IHo!fq>~- z%79+q^SmRVl>6pD5tDF?1uC7MCdBfjR1zCG ze+%8A5E^`|6+neoNXf#M_7cm}h+KiJ4@?=C>2JkIEI(*ch))`T({@|PeNB3$h>K0& zJOjx1T%n+vHsI7IeC~Mqy88U}+px2a`XOTw8&uTrTEu?$?4)fGhTk8|xQ0fq>>Q!D zf2y-A$L)^o@w-+Sm(Nq@)uM!yuEONrqSMvB8>&CICe#=bexAYnVbW)PTjrxMtSg}> zB&3?t%3espx`fA@apH+5`qqtau~QV2-jj=g+viwHY@*w=7qc%&N(~Op`BRJ(dGFrO zIqOkwIs>BuiuJYcDlnIV)RIH9$Qw4r($`}(*7{AXx^3DBmAKxh+e;yx8E`huk zFN(^37Gatqz85rkP-eo8=o^(s4|;WA~W1iJ!CX7*5USQBu?@! z>E(nvd@MMD^JzolE^ELq4Bu#jNmWF3m??$(*>77&#KVVU$|nWmBg~r789B_mbll;# zN1D9i28WVk`_lhLi-;c5FS&!ec>V9hbD%$SsLLVr)jZ-;TNp1g!&MBwr$*~>1Ly~$ z-Xy$x&PE@VkP+Q>t2a=?JOb@Wl>1po7ij_^MUX0h}h z#AJ3z@>}@&(F-_+*ZG7 zX&X0#7y!(yfdiMH6w=_l0<^hkw}53sU7?#bG5m@G^6(3{0pnpSeN>JFJ{@)BlrWfg zQgfkV^~=bh!O{?jdFrd>AFt)&iJx9S)$z$ZQjCR!73t3{L;qA5a;~uS>nu?|h7CF2 zA3x~`ppZ}+s-=*HLSS%N=w{aWZ>WH6ot;km-$NJ3d4*Oxc4gqFu-!E(jVs9H4U4U? z&|jG49#u&PJ(n8&BfTDJ*|frZd3_~;JSgw)@b^glir|cTh%Dd*Mi>en-byFECF?rO z|I(mMPoU4$Fqas|XS))`KMx~>DC@7cT5UhIrKK`R$^l5H0)=^6}_Ir^mtch(Z(X=yjxn;^E8Dg zSyNYron6FiDkG#!E}iRXH(!n{H~5_E<*-z5KAb6?=k2uC>bAf5InT%C+n1m_NMiDN zjA;ZRZ0k2AZ&lXw_nT$>s+E(lL2rEiKJLA-OulnK8WLcW&|S?An000JrPU}vfS)M=p7sXN&T9Ti>!l^8O>BPFw+y4Rv94VYF2 z2kLP-qEgkp?atYmxv@uwkte&y87AV?7Db%J7&yGL;Lj1{R4OV}C4ajcSya_Uu^4rf z-I3|AGFv;Aah}czRj{g&K>WlZKQ#xJW*wMR8jKdg2bya`#xwb@qV1N39b3p}I4Nex z>R`{8>nZjT3CuiN5rnm)c9pe@s}AT%gEgn{fYefnDyW(;j+RKo9OaqqZUQ>qbs)96 z+f{(1|B+@#lXo+_BGyYHy0uB!_Z7TNYO^Kmo0K;LKH0Ucazv_MFEox*mgZr*xNoa5 zEEz##x62;M<(J*l{R3aZEDYbza4Y!bl;Mu&5|k@Z5@Y~Ohun#?Iu~7 zA>RRp#dyV0I+LWiCFU>$_6nAMfA3>E09NVSGdQkoKU+Pn@BO=K>ViR=#W1zCGainn zk_$MkUyqV>ZV#2tW3*HnnwjW0wC+4>yZbBY`h@_`&AA0*pW$SX+GYK6m{Dtvh@Gf@ z{c2o*osMEy@l5J^8cw!xJ^Lqd{bnAi_3HMkv7yGz(u(&{@uFS*#?Ljk=~s6fXq?cy zZ=qzb@3&+4Ht)ZuXuW>e%Nh2*Vlhv8{dibezxjAv?(+KAY5U!yA2UHd7SJEf_wD!f ztk#>L?GBf3f9{fgcPrz#_>oh6zCeOYx2NtcDp?HDrx}r}PLgl0Z}Ok}mf?UH zC&>C(K~ci4un5L*$O{{E?FcFEtDdbVS*RT7Z&C=W@HT+NmsYjy4DJd_067X&XydCx zTJCj-|J9WS3`E?1{Zq?fJJ z3M2-_ufIB$|+a7#@PTm5yQj;$Pi(!h|Ucq)x9Grp&)*HJ42*w5m&c|I6Wrzj1#RIVjbTUxZ8JSrDC?gqJzy9#XjXn{`?OHg{H&4Jfr_-U(NL5*WqmU z|Ga5RJ+i{S|0bF;Q4MUqAhHW|tXj6KfQoM)*?`#ykQm*_5k&E?YC6zxd+dlkOE2$l zTH69H6ZNwsI@*pOB2!E{3V1UrXO7>y4&0+lH8ZM;9gg(TM|yFX=&GafBg0-AgUtqj zV=Vcx@ocU^L8^YOlh?87HjQEXR7Raw%hU0z%Ur|HXBqWDx5t`T4~D&@nGF*Gn+imE zMuWUReO?2?Ay8W5QB4D{(6W=a0(mQr#s*FAZ%=IA4H(ZPW;T~)Oqy%tnS8A?5UF^3 z>R|3*vNV+Wsb%oQ(J9Y#gOakf^Y+x`dewAUnW$}0gcs%T9dlM(wtd*^%p;3qb>6B6 z$2xfk^AqY7%Oq*X#;x;PyOJ=s2L@$dWfJP{yw|s)Q|RWD<<*dR_IX5K{&CBBP|M=x z3KSb($*9-0I+|w3-KXt!?|CSQj^z*2ClMB881M#+*NBEz^CAWrPy1&miiJxap-37D9CY4VHrCnx$}%WFkfD_>=tn;9J>txfaR zItN8h@z zG)rohI$huI@+>)wlC|w+bmsqXjcj+a=1uspkiYN#+0V(n`?`SabL~r?`iYnV&t{s> z!qm!Gi+z(<^Z2h0^`|-K%ev!OnSqA}cmBF)-a`SRw&OQqk|^CeS|tk0u}NN%uhY+b ze4{=KSK#8TloxokpKm02{wI`wJ>}DzH+Nn~(6Nd4!I$FWE9G~FzrVjXy2s6;zsevh z-2J8QH}29Mko_iM6Ey8N`}BPJxw(Iy*a1Z1Ddr6QKxOW%yFqzbKU8E(uq4eP~q~`CfWKz(nn9JkZoy>9A zQ_v-z3pQ}};ePf@=hbo7uQjrulzErh+q@^w^DlLeYFzrj!4Ia_?1JsKJU=wUyU!9M zIT=}^h{!n2J2*qXdOLNnaR>3Aiph`!>Xl$VoS#}-V)j=3W(aG_pBVjH=WOK2xH>^5 z=L8T+8kPIkziKR*@QjMe%@v=C{s!2dZIQmP7t?lC#&tA3sTT{o=!%*-%^nTatPFV_ z{?`l`OEK_SM!T7kEsy3Y3HNY3cD-!aL>{{ggQ6p3+BF`_9E&&(xZkZv9B@HkDNpPd z&gH2+_%D6wi{sIc#9srdr0?PhkXR5fhPu0^$vyW>#l%IttB^tX9IBtp5rbHtnbW!E z?1*>i>KwIhUdYMeqX9P;Y~?^&3k zVhOqA*B%6m?_j@?+)p^@2zEcSch9m78eljmIqbghCPY2T$4Fdy6xjrV$C)Xhg zOmK3vCZYCRqjsM%GfV{z!MkE(0fF2Srj2&Ntu+vjnQZ^$WNQIDU1kcWN=UssCs~<@ zY6@`1frb9+nPgSiM+9{%Si~L3MSeNw9YXo?>R<@RzFUFW$vHTazZu0$Q>MW3-tFpw zhh_yh&nC#3hgL2I(sHHHY9+vI!y&nVRKbs#W_V+AE0yrM>8S`?TC)iOb%%-Ojy>l;oP80EYq6oE- zygV=-(B&tMMJ5tvhlOVvC3vxYFg^^2&nQXT)SHRr!bmB>Fb_OeGtl3-#FtEzB(0-+yz9_<8Ep05+nE!RjCM(8wlYU_ZlhBNH^wkOVP~Hi? z%;X|(&6!pD7!;(g%x!t+sJ85dEUD42+a1&F7c3#hjrWwdNny1GO09rY2D%Ca`!h|=_X;Pp_k*a9W6LSSH1}shUPY3z;hA*#jePTM|kMdNQ+0sMA%6hML8`) ziCUsueBX)j6QUFB{KTWHo3vN>d?W9p?=qs}mZMYJVvZNzee6J5#!JGemWPtnS->3=N#)3*%X-n9+Pgr70+=C__`X-*?|TR(a2O;~S^ZKOjg2_kQ$6-m*js!rPh-*I z3;+P*12zsGU3o7vn z`YM5%o`putn2wQ+o|%)LRghlMok4(;k&T;)U4VsyhlN{$m6@5HiJBe8#?CFs&Mye# z;(-avbMOdpvcoue#knLkxKv%Z1w?r`S$PCs2yhDu@bCzTND2yz2#U!HOUMX&q>H|Q zi|RQ^h>E@tW_=+p_CiKZ@`a@2OJzw@Zz)l6X}E%noXSf!-j}j3HwB;35 z6~YP?RWuY;kxG&*DvF9K@zbgZaaA=9RSg~W1hfWHPgB=e8^M9p)Ie%!Bek`33`}%& z_4L&x^xO9hjLi&9%#95Uj17&9jSNj*S(uubyfU|XZE5#LPul$TYx6fKbJQDi%eR)M zYL?daZ~M-z?HsKg+-yx`?cTn%b98plV|1|4c5rrgcJp-h@N+fcadq)@ow#-P^!D)f z^Y#n$wUYMdtB4GsLxWv@Nl=PIe%+!pW%$)q}P{Z8(g1myF{7{90qSAum%A%5wB_FFk z#+!YttgWi4tEp?OLpRsQD59G`HRd=reQN*I-q|WB+R^p7tGlgw9Y`X}d&Z`&JN-~Qd5FS&dx8+FD@_p?5=NaZ^nau-akBi)A&8@`)9)O>FMeJp4R`LgZ~6b zAfQuDMls6bk^hUxYAEV|2gDo7QAS(jM?$1zkVtEcoh^CcL?b|1QaN9)O)d9c_3KDRiACo`)RsJA!njc04fq#SFT_cG_m$34^NXg-{;(5-$w+0k;m)L=QTX>9T7bgj+f zcyqF|_552;IMMS&n&A|_fi$i+Q(f)X#fst|eqVHT+#WACy*UeS2pE-S$pSu6oL>+3 zJZEgKely+u$B~ZBBhbQ#9d8MndH7(-g*fIvmOt(6TOrt#wz+|%K^B-!?Az~VF)d|1 zl}T_7Y_}t!))@nuWRrJ5-#cl1JcO%u_I3>HV}*tCTxEJVF8@$PC=fdPZxX=(yZ87gV0 ztd4?-LJT-@jI;3tZA!OIgV^Vg|`o zXSI^1Zq-7flw z#ZE5zfl4)-I$%S`%Ry<96wDzypA!L9n*X@3Mp){P`|+QZsvnJUx1L;$^AB@hPY5qL zUQdee*IZ9Y-JM)d%i{6e%pjztS(V;}_bmjE_kJsa%K3gf zn!WCRCtmFA{(A%;uy;4z(D`96+otYe&yOs9Q~v!qkG}_FH0trNqP*_$sHXMo@wj1_ z_t#1DlJl?Ans+n3XI*z^zs^7Z=hb^K@N)R(a)iD9_tk{h`S0r)CB8p53x+O#ZpG$3 z!!$SzjY3uUsQCW&t<7U&!&?NDGjXwkrl}xIwHG}!-#THAG8|NhyfRRiU9Sao&KX@bt0( z&=ok?U=(&BMJX&uPPqY^%IJGy3E7w%i7Itx$pNZRUE&WU^IPy2nWf2V`i>(X-9-<{ zqgnL{XvxgDm{hZSC>#YQ&|OyvRx@Kl|EXx;97Rg#B@c>Jk4`!tMa6Csmx~XpXzryq z<{zk+zgQZb^6t2P|J@8jYF|amS_3!vO(iqJo%%^5RK2MH@Oeh!ztlW4+?zrKjjh&& zL=K`jtrw?=e5qnz>3FfQwL8)ZZL1`&R}2&f+i zMH{#eMTs3L3Q<-v9-HUU-y#SFApXu!Ap(Z5>#lx@s_yf18DQH>Yyg(}od`a6z63%e9Vu^ZzjPu%H_GoW@qn*zRb+bXST z&ZhosJ`m=yH>B14+W50&!IQ_)rh0()sX6IW8>G}}KX+|23i+vN%L|m7tcU=jBeEv%Tr8-qv zpD!T54hofp`&BUoIsmu2U%B1Vd<&>ZyOOvrV7`q_9bOGyF}1BR0_ZDUi( z^~a?CBlP>1aX4fqHRTqV=CK&DRn-%Y{;h2+WydH~tL+e39>gu4bNAIS&qap;!^l6f z;nST1i9&_fptXrD4UBz}diYjAa$sMBZs@U|ZSiWduQDrsUyN#mZ6s``?0tny>1qQ<7y~z5B$sQiC z+AbW33j??N+vG8XN!_9WScgJlUYsO{Jsqy^)Km}YJ=O*F&G56J+jMCl>g#$FwPVeir|iSVTef9%S>{B>hP1OK(Tg+ zB>v|s5F<|WrL&*qjQN&R(sBTv`#ZCd_oRF%Zyw;wc;S$IQqr~ns7%AX(S5+OqHJJ+ zB@&amb``qV8u-NgOgrKg;Mc7hzW6LbYU~VEz|F=RV`vmc@AUNgCMGg`>p5mXSl1Uq zj#iI*QSYjufbMdqZ5z8@B^maVKumKt#%-eZEf+Kc_B2&ck2cO;IRQs<@bWuf7i!b{ApXU$Cdo@ zsUm(Dy4>Jl3GEP7$};zFIX8IYmEnQVz;`e)53h=l-@~C;NKcpzwtbuPtzrb0c@PqS zWu6?N`eX}Ec6HF80zHc1EBX4&i2Y~C07QQx>Xdas8hL3Bdm#5?I)Sci%E2Z3p5O79 zKMi|8lrerO{+EtVEH&)Dt3>!7W={$tIuiv}&cq}qzY(^GCMya+wA$-c;KGz*6G!;f zlzsY*DEG)g-&&)?#ekC0_Q;up@1b^!2LYtncI)E4qJ2057lO$RZr>RbvCuBg+SqkV zz?>*P1t9ak#t$bY>8E?r0W3-QK7Pg*EXTmi;4LNOYZ~Eh%rfibRFTXH$0&7A#%+J| z`brSQl1#j8Twp*>KMLG1AUO0DrXWByBU5Nr_-CQiuu5x=7)!b9RA~i!!G9iOgEXBd z?=*4hx5DjdG7o98)ah~(=?LR=MZa|AoOIRpboG^V&4+Yt>I|I+6V7>IUZIS+e-_Cc zVWS7Ljd_SkyHL!h)TI<1p20U#p}6eV+8LKAtX|sKioSArFg)5+9ZV!BdGEv`C9_ zJAbGL*MA^+VpLXSJ1>&)W%Aq1AIvYgWc7iHJQfYmPZIW8A(*K*K#K+(3=`Rbfqc;H zdyhqMfich_1c%(Mu;vQ;IVBZVF54d~SU8K3PmET5&IddbwZqB}44jaR8=d=yMBii? z4~C+WA=9 zg_3n>GZo<&Z58kjQl~^@K@jln(M%`n5-)*#F8L29l^>w{?`Z(oCbgRx&%hcUxiy5NS%WrbaY4TkO3n*_Y$Qa=t z1awS`3P&n&V1-ozZ}aZ0RImnfd~yVR3!yN3V9HGi@ojcEhdd8|)lZ5_?j5j|AGk>s z1e|61CI2!+wc0@M{d6V7`9*e7M-{0mE4vCD^a!S0sHCrYsU1=cR}~PnM=Ew?7syv7 z5umIJOMHy;TRbs#UX&SY7dn*+>e19!N7U|c)((RyWH9RVcCcbcF&t#|#Ttb3;iyy2 z{3F70LW0~t{er?u44|q`*w^xf;##*$v@08VqFBW|2D9yxYYF(MY$P4T0!I^6RZC%@ znP@=-Sj{fYm{V0+*j9CdjCemgYOkLPfW;GRAX)Si=q)f<6e&DKkZMxLb>f)$M8zS$WKw&0RBVF_^{-adTg!r>i`mycXrrpzxajF}*-DE)A=cVI z!qJ@pol)b!cQg%cYG4c0CzmyH#_j6A}SbhNI?3fPUqAuxU@N5;>m*Az_ z74*WS^lA<0iUZ2YYyU+1S?IFVQ|)tCS+z`DE@ND+9{5YEUr$GGiX5tQ4g^_ zb!4Xw>3R>DIo+}R$0iP_7=LdnE&u27_fM4{VHVAvZ~JI#@&bFIUPVo>VoVvOD0wHk z_yXT3ns$oh_rHjT3s=iNH{myL&yXV)u58I2Tp%A6rrhA7kgdV3l>S`T%^)#{?qUrbH2-yYe&oM?N!{SRvp%*ieEqkXS1S8uaGCuyJEZaNg5 z3pHFHVgYrc?0R(6fkvT2Z*N*uLwd~O4Ue8^Q>>~fl5>i))_QCm+G|FopAlCsVtzI& zDx)X105$zmO5L8HBYBVHVW%Y|`v!j0uFAQ*WN-D8r!W7 zOnY**j!q$Ba(k06_u+UsMB|){6aK&YTTUv8{3rZ=LCLjShi{B(8gV7O6q}E#`H@r2 z7&F1!6S2J=BNO1{6MGpH9!E)mp63v$YLRH%Xo6aWLBwogd%Kui_7-#VrbBOl{g{3n z=yF}rCwDaTcgI(?e&6-dwHIFqr04ISR9OXgD>_EC!KdRrbd#Ah6an0G3NJcv9}_uG zJKvt*;l7&A?V|N>X?0apP?Ii8q;2Bem{*uwfHZ4+Q$y+VsF-EDt665GG@!tV2~mI6 z(!e~M;}m1~%py(yqDd3g5j4-Po3SEWc!LdeQuyY0hMF;t&>n8{C(HFug|KSZ+ta!T zy3#Skv{2Cm+-Zngkcuaum4GwV&?=+?^OroVlj9F#6u{k^O7Z1+Em zkrevogMS%|pNq z#)sEqPFK^(yoZf$9mXkRAOC7{G|a5q35X|tuP&fIuh}LM%55U0gK_t?ZytC5J??pS z(kF8=@cLvZ_++HuWUTvSV)JC`@5#)w(>a;bh1aKx!Kcg5UaxeU^|zjue?29`z#*DS z5v}W#IsDFreYV{VNr%X+VsUHKF-axTle`#iB;h{_)|H)V2mak5omGW)Q$Ygv+i8BJ z66}R+9|;8xI&i#iG8=s&Vrh)H@Cm#CZDR=|R2yZIv&;!oJf~kMw3loV*chB;94bMJ z2;&Kv(KkQbkO=;wE9s|Zc^C#^^$cMeKd=Zz)Rqh`>lwyI69mYBr978pp6k~15Q4V> z%s>Lwb9*t0vnp4@xhU`~p=SM2UF0e}v40+tj;}~fh+RJ`Ic&;=0p${VkX*oTvD%oeC;~FKJ2HmS2G5@mO%&)B zE?P!J@`1v0Oi-BU~{d zm`3I79McA(dAPy?OF?lw6n?TAA-APP>V+)q5f;(ch4JCsTAT_Gu#W=CnBjdl=5ZFv zWU^xBgnn0*eJZuG^buE(AMhqhs^aX`#Z%KZx4o&zYW%l`tquQv2t`Z}{ADo9m_(ax zRY{gDyKT{$QBlq3L@qsP!M?>ZsgSF}_3AQCbz_$5dSZex(kUZik@l;LmwVIYZ@vj+VvybpiB$Y7Win#(b)fhc@p>XQ#j>i_)23>S z0ATELUa(OU(?vtz3=>+doAIHp#T-`cMe+x9u}1^nf#@0GoJ9VyB78`Ay?h+t232yz zzrg=7kF?LNG?tLUzVJ6;Hi(BgdLbG^7}eyYROnn&+mDA`xWiz8+g}Obc`dB?f+@iY z(Dj#FPJol9sE=8#cH);#(eLH>bLp;lq*Vx%N>vU~)ZMoiT`qUkDMY|lvg|KrYxU<73 z_pS3)2{BTH2ER9yZN&m|M0)f!kp&R`wN~7DbmYt2)O$d*X==7CWIr&_xtn>TPr%4< zo!HAfiZ+wOJn%SRD@MsR-QeSBA9E@)K2+m;D1N?d+$emlqZ9}C)v9WoK>}!EJyk0f z(MR~ho@!Zr$CX7AwUmpBQmd$Vs0iJ=!meo(N;acPK48lO{ zQhnJnE697*q4kfq@T;XDpQV)xh|g9yS%J?^oLKkw?`ek5ZtJpq{`$3t0E7LHY~KC# zKl%2TA>gdj#mV-(|NOP*jtj+8;7y6o!sp{)kMVm-0o#Z)1?$*n&zmN%YVUo3l z6Y1>bfs$*?@>$rfXbjbRCgY;buqhVYBb5~r6+~M6#{s+#*&R>R7!tOAIRSDd%B(@w zB%fipf|Gule^OGUjfL2Xp%-$teRh0m{R?Q%UP4@s$mE0FeYw@eX%2%kA*w=>Y*&$# zdQ`E1(khL(+S_thnKU84KEqys>hK6G46maLc9R}5(>_Da8dItU+Q5AN5^`Kb&-z2& zDEISZ=x8w)j$NP?Mpf*VUg7)=P}IQ$ncKaPR7!4tXH8S;Z+#y5;?JSn2!B*Qn+CM);n52w18tFRL`a0Y*!|&h}VO z8yb;+_OhzVd;Y*Jxis>L^|k5kGZZp4S(Mrm7vC+9;C%B0@PEG}?SKS*~hH?dmb{Ulsn3y1QzKgIl*mjTCN0;a<4AYmmZ^TYz98K!n4$*4n+h_v!Nk&M%mk@5LO?7%Pq)YEsQ z*`5;_xV1)bZxpqa4H_8`Q9-`!p4wVtqAFt?Woi%ElY3nn#Ma7a&h{MiKtNSza5>B9 z;5LZ-H!4tk1sKxsvo{!a_(+bqPlm>>xr@Ea+-*??IB`)Q&8hX`ItLMegH$;C&g=IC z_GIvHd4=;`mTR1jF0IxG7#`hp$9kkBRGLM+`p$SGg=a})mD>q=#9q%GpGdc}ljOwr zW_^|D#!6G+xw3qb=tmRU%zoySHvsN8x!zjNQxg=^x#dIwNtXbwwG1OqxME&-h9Twl zAE^O?*N_X9ax+SZYs)8Q>aZ0{|9z@w?@e0c>y&c;L(UIA)9*}fbB=p<#GidkGeCF6 z!na>)((dBzBtKMrz5YoVK{T0U;>6$Dy$3h?b=w&5&0Sx(NBP;}c)J+S|728BIk^?R z<%|}8I4xikydPDQLzTjpANVF5w!KCFTje#2-Zy{DvT>Ng&##lJ7|t60+ZeJ;ZM804 z$4%E1EeLZnuToJ-gR!E2oJ=m4k6UL!-fgESG2Gc;qX_N7r)xJ&5~~9Sx1*h45j9Y7Lk^VCJc&2{@vTIhf9ZuT*ebW z&X20A7jB}SB+HsY;_#_Ewh1jRS@k3}!G#61S$Y=2f>%QF6pS-G(QM?J^f4RrB&zgh zB1TFSrKC+9MJ5GJXOiRsI;r@_l_G`$w@;v`($)$-oS^#WXwpwVzqL9jOUwNdX&{$4 zRgvLSubs$B)kr}fHXQseBaY>Og-k03??;^2Z?q2)7@Xpz%=%<$FkHY!ij+9uksW(N zpM8r7!o2H=N+y(DLgCHC5Y-ExMFa`{mbu_(st&Hkt6zLH?mZKg)w#W&~)@ZZ*A(;eOyP&OnN}>^USH@$7TwWySCT2N|xApDognj zF-j*I#7UH5DMk|_NJ{Dc%9(M>S-HyD?rkwO%DE8wA6PN!W?X`7%Iz#exx+m|)kC*- zos&q)E%1=6rSbL?&d*6GRt#eb?y-=oFa*lUUg%KZG6F|`p~WK^+(zVtJkJs_MkHw5 z9g*n92J6IS{8$gUP(V~B3OUpry8>IO|B8fiLUv7QO4eSbwx?RtJxkp?%35N|#%MZ; zE?QYGt+-1`By}|XB(@38Upg$HD{tn8S-Zi}a`c7W`2(`|VF=FkD z)Zj_Z1Wf_T2{pZkN4<#p&o|pYE9VRjHqx=L^s>&CiD4QhGN}MlZKqj3oc2p*BS7G~ z3r3h7ac`bNagAgIV{OD7&p$tVXv6C0-+=W)d0B_2VVKhJ3iws*HOCa}PFtI34*u{u z6@7t)G1ZAe8fgU`t+4_;|5;Icte!8nr5dUqHi5yJDuSburM{FrztFZ(sgj0es!j2` zr!m6Tpg^)EJSM`g%I)nO09XS|@QNMiv~zCLdV z*9dv@D>b1?tcDY`AtuMtryrcp!_{?Y*o!k!#=cR#&=F;|PGkNl?gHYQJ-+Hv9}U!h z4^h8iTT?%Wz%f&Z-p?v?5UZ{&CMZ8O|U zGrYfMKwNWtYI6{mIf0Zpp_)07i8-;8IZ2>7sTv(#Ci23^oXcXKqeXP~>PYVn~#3#&Vk92znqd$|WPX zFXMYriTWP$ib*^r;8moL7CdV9)Moo08F+QpnrdYA)_xxj-ouml`iy0AxNK$h&G7Mh zNsj3)ep(pFGQkRqPqQ4( zB0plV1Z@$$b6ll*{f+Ru;l^e*S2=kKD$JznZj)iU$tdaAt8JL4a4V1Q#D^)rbbza# zt?fKu{XH+wv%3nCiJ8$%Z9*meNZkF}wW`WiR_!XGsU6W8-~!vliA=~v#z`RwZ z``?*oH&Rr6yYmtDZK>Kq*%zkJH7%v-!DxpMmU;?Ojs}9ndrKGY9mt_j#z9sc6|=fV z>MxfU&$IWAJGf4})K2&Y*88rn_u53%oHCoOtM61V?cduO>)0fRNxX@;ELuFfjLSOl z=rs+vNGQP?9(b3{=X9=HOwXEBNcmtB zq?TZW^5Q~WhU)a+8G9foF0K5MbVkfBmJM#=)!>z@Gr_ge(|nJzKe8rll~v>ukJK}R zB9Laen*``A6wE|KI(ANd(5{YmL~{nCN5&oo%DHQ#V;2q*krJR5GWw<-EEF-k=*1QA zh!1Q}w=5_f&oT1t5 zGx+#w0THhZ{ma_0A%(1A+8K|D|#}ip^>&*f@ zF=IS4L#~&I-}*wfSbS*eQh#EK{g_*I1ZO*b_yi6jkqYL1m@D=QSKMW`JBTpdede7* zic0W~b)Ja*RII6qa`J=Pjo&#xRsfmqd)#rJ#hY(iN0h-;E(f|Ae#eFgeV13nwLg)= zl(>CRd~H)RS147#iBTzLjeC|!#I@Pq|9rMN+0NODF|!Bj=!+(B<_cFu z%ovsGA)cs`Nwc8JM~O*q@Mn>vYdl;(;1bnkkVZ@Cha(cL3|VRQ`rgT$MIQ~KMq?$E$icjzC<^ zmP5ow$xr<7yafG(dHe4vd5UV2kp<~mC`xon`A)WKr_Eu1ED~n@HH7#OtjeE}Sk;F< zd%`#aI(g@_z6@?g=9U`&xk?H(MR*HckS~@zELT`RAcEwI?Y}3TpwK2I#%U$jl)AUp^ls^XLhr3yusdg!pvZ2! zndK*LF*sdp({~JHvM~Q0@fF@RE_JG#iBpYf`n?&qSy+~2gaV-!N zg$EgyVnsgkq9jhR*ds%#KCLp1jF$B<0`d+m1c6%Dt;I~nUQgti4$28>w}6_2SWzjA z)4H3dx=xB7LxrYGEIg1=Sd21JD)o0+Y}OQk`5*r37o^%dv4Zm5#hzG7f|K;;>H4Tz zilqVS0vKtS6H%wB=aQ}(#fE<)Sd}Zp()Nq|bxTNc>lDP;Yg>tRPNChRC3$L*)>Z(e zboV1>h2pCxEtGR}3-;z2v2XbP!xQdd%+|^q9G- zZm#aAzXGAy9=X-Aac89<@%=NZIc@8KE35&z9IjDO6~ zFid3V&pVoK-NQRZ<@cL+oHka1Z-Oz$pKp@2y60w$uIcwP-!!kEPIw;QhClzT=(isJ zImutY`R8S@Bn1}aDFOr*>E;pzmQ;WCv@B_;{Ba}FG7M;)(6Q_l{7h-TCAen(>UV3A zMNU9lu|B+4XalTRO<3+YCF!M)S2DGswyvfm{MEWx13QsUK6w%F9=0L87oC9$BG1~p zt2HK+!A;(dG2He7N+0+v9*}<{eLO6j`%??5W4OC`Hzf+#mz`X%uq`K&S@StB+K>{s zK#{@GVeRqHS1(Tc%!U8*s<-$Jq!@Fgw2R0JKuq4RY1T9J#;;4Z23mqnsO6o{X6Qpm zfMz(6^SxCmP-G{_X|s@Q+bsmhFe!6TB~rnLd*t+t-HMV~UCqlupJ)nn2G{JH+E@W1 zZh5b)1JRMpaYImoYX*eP;KPlmMd-Zh05JvhO<7GazAg?_7qc-T47UlLQeB!`g&ao7 zvk)yYUlA;1q~FBkR<mVwUB85_+8qE=+99Q=tfmUJhl6iSJh zGswe`FfLa@M$-_DU38YfjK72zeImjnX7x@&Y;a8D1VCcmm1O|po0W-+zU>9>>fVvI}V+NZr7l1H_Mf%u*HUjZL6M?|2 zLN>dr-G8rC2UFRa0&Sn^FONsM_;c%KITIGRDSR1Z9VeF2b zTeO@GIDM`}6Q#(-ktFNFt-X8u51024`}TEmd(Y7Y7d>{y){mz4-pc@P|9$qak5att zW{uy?uh^4!7KOVnG0ZjI5|G<-t!#GoXDqfBKtddy7?xn|mfb|7Uz9laNB27a0i z+i98PN4oauer6n(>Yx0yWSU$bM&$DlzQ#0;Gt8_3nJ(r|Xoy6`^J!S9_1NZ@0Pf7GUf zYMvL!x&nu|4j{(&ON?b4xv?NCKviZXmth8M7u&25ntpAQxOs4!HlL(d<(5CwvlzaK zAY$+qfzD-MPy;6WK_jrjkR8DQ|IHL4M_WE`wY>z>o-BBQji0hxo=!(Ggx`%xCtcf8 z7rCtnP%C|r{cKoTfosswVP?bX_R>u9YEC=jh+F#xe)P*ofkapmJs2Ks#FjvpS$09_ zcjaTO2(1eAD&1#sd-l8F++QQs!F8<9JF$*mD=bjnj>-Va?7KdWqNNSR6GL59Easl1 zY=BjAZ&_C_1T?t}B0M`Dkz=<4#kY)7sq47HjZB0$Cr5z@vJ4tKV(buv(Hf9U4;%f| z7cT^Ha>VnyjfJ1+%HW%3zH)B2Fy8Pm{MYi<_^8q+&tufvwpK$J5#r0~-=ju`^Ir98r~`eMoBMv$eULo2#H*Z}>bqwzmfT%s zu-mw$hIG_pbm?QR=l$$^hG=C}4E2$80hr zN`X<~7|Rz0NFh;a5@|9AbIhGY&kez2@}N(gVAMYRR5Hj8WiZ;ZR&Cm#02SHeTR4Vq zl!;!*o$c2TK05%OtmMbl=R7bDQh}OFHiv-H;R+ZJbsFU!3`8viZcnGMhM(flf|v?H z+1Mi-&xHiC$^=q{ghe}qx`iom1w_`n4rYbKp1bB&h$Oy+FZoMS&{xv@LR}kG!XjD< z>u~ZmTFMxC(otF}sS(OmT57ftsv}w&;t}e5TJX~_4JsY&(J|?AaQgNX&Guxpzi>i_F-OO^_bgD;Ey(f#feq1d(tsk@qI2LrkbEDQ*POJ3@|xVT{!?2;+kLFYT7mIwOpEBjDN+H3WESvFcZp>Pg`5};a%yfuS@ zS&4$@0mP+5F@hl;u|!FMK`EU9+V&b+R{|iSRcSgBNE6qHMK^jXnCJH-MErT*5i!$q@x9_+ttOK8|i6Y8u z*l2Sy>EhF@iSwB_s*VVn%IsHZ9Y(DwXPF$^3LUqZ$dAgL;%S}lnaGowT#yP~aGCLl z%UylxH5iJLNA}*p3~h0XVIl`L;A73Wb*5xhQKd{scEk;N`$OVE2k8V@qFm!$Fhz;VvFA(Y27XtX>$yjXF0Ua=3daIM0_ zg{@0HiX%`VZ$=xG9;wv7Q|7Rx-hfCZJ>Z#s4BZ@8z`;UN8=&0TD*6Kn-T553in+Bi zd6owGxD^&8G(wBJxo?yalsGBl9ug=uf&vewd@G;0s=q&>Ya>!FTk~x55n(#P1WnmR z$$#`j!EQM5rT7ABt2_-qkp3cXCVNPg9kosHx}scedet*s%Qn?C6Mp)-VJj08Kf8^k zWqo{$v%+In&uEw>uW8C_kp7vLR9UGUUw8N+c#v|3Mbl^yckfjD?lkdbc3JvNp~QBJ zH$D%#mw!ZD>IPG2#Rw#)gGr-l*^EjLY(eV{TZ7YYNm5)ZcICpJFg-oJT{~&ZM#dxng$7)_9R_#L3AgS!M4U zxTst%&U_hz;p>q7Ae3gd(RKZ>MyK%CqH(_R)m?Gyacnf+2G8ZWl(ue6IPZ?CNp+b% znH=c<{br3?)2~+eiP(W4jZJg7e}L{B=MS5XK+O;xcA-?sU=w8INL5+seE4r%mO3$l z$-06xdruQ!%#&l-Tb9ZHot84oYfLkm5k_z!$Ed%Jx}=R`79ayNQpyePaLzuH4in-{ z-Z2V^Sx=I8NbdN#9jKDR4OWpqJt?b}yk z><@ozMgzx3xNP^JRtJkaJuL?ZAZAj!%fqr9908f1{_$MvHkU^?Mkn!Hzt{V*rSVQY z6HcKAXZd#DG-*hL27YEiVOw06M|GE%T;K2OzW?RALao2T<-R7azozECVXD93;=UEC zzm?*?Q>edF_aCkdrv8^$=B47`d_l|9yzKu*%k&(gFK+n-Ei?7Q{w0?A z|09-}nEo3r|4S@$SfuY0-mb3YnRkpMI-=OmJovQ(-tGlP0lf9dZ^S`aK zmtXJ;SN8F6|A#9F1_!tq2eNVgTP%k-tA_lWE5BIf=(rc3{DPC?TtxqOpZsr}ocBL) z@{3Fk2L1;pM=R7dHaBKDy!hmQ)ZM;*R>wN4Nb#r&S9!!t@ z%x<+mnWxfTx7F=^d&cO4a=D%36n?MQu~)x46h**pGTqU*KgPb4Ps!kdaWIw1Z@)d= z*?c@lJ`(g_M7g-^JTdI-AEKPE+Wp@|Sz1z5*#BXtyW@I)Jf(m-By83kmyMmJK&|(M zD6=3lN@Sujr<$j-;5`2dT2}aZ09S%{OKHdohD(HO)U|&HnlcSJ;fK|w)5hBCD5iEOA?u>;i;aE63GT0n# zYkNGTi3huKG_z%h(IQ6`mL|!0BOrDeY~}+OS)0^YAQ>@XaRlQv!dQyB!lM-dU{@Mq zj@&7fn(m3CketCJb#NGOm2PVtAO$+o{D3z?Yb=9dLnxP`o^5`dr+#C0l<$AfTBwic zrngAzjEkO<;>~2ooQ7i_vdF-YH?T$@sB3pxYKUvVB16%2zz_ltnPh>baL6JNO9~<({h_D|NIC!m}q=um~Q%^Zlqa64)2m>XLV%Osg&N;4$ za%B=F(&|Bos6I@kB%G-8Y5x%AxfNpUN=MwyhTAE z{6C0tq9WJhzlic42?YLll zG&VB<1s&Hx1oniPvQ=XwGS6@fbFhWGz}|51Lm~s^+5ktglEPQ*xS)>HJ*>zg1r%^1 z*z~tW&|t;rs$L;cy%~DCo#q(d6F$ER3~X5g4wr>$fDI1Jiy1^H>FydCBK=FSUN;RX z|GiHBh*6>(#Z35WdH18t`hZS@Qi`4gB%bY*j@9yLJfbFIh!+&1XmOM7y>I|y zI0TXy@bMPTE47uOkE%kDnB&wj*jSYZA{huW<^|%o3@y+gxW}|auo<*X1OiqoY0PG& zL3}c~2zfJ>>=W3G09K}2y30WNMclE@D(hTjTyy@zG^XV3#PeLBn{~lo>*pU9WH720gACGT81@QViR>+-OqMqwOyKE7QKC~Lq1BB4jJOR zE^8SR@Gw%tk9t41Z#;vLMBZ~mj0;N~C~kBze?@C5%Jm{Q+b)_QPAD*F#vu#P4J;P% zAT2Ofvgtglg*(YBrLWZ3Jr3(<#0jV*=xF{5p-1d^-g9s5aI|;w>Rw&Fxo^L|<8+C{ zQr!jp?D!zS<(3f(Ipq7<`Q{rEG#6{@46542Tv+GP&Mx*3QP#hGA<8O({}AQZ{4YdV zyosJLuJ=l?(05R>WM7H(|nX#OA*P;iGujkLSlSNiTM73 zvLQKRj+p8O*Vb&QwmiBw%cAX~dU$$J@X|?*yG)o%R7k}aT32ahCN-QaSj8~@Ku0)> zdhNXC5W{L!qX*R895eIU7oNv5UZ1U1oV`%QN6%Mm)UsP zcV`6bMMVmTZ*bUqwnn<5nrDUtJ4q5)M2o$@yt| zMx|=?@2@C4N+T2osR$U+^O4B&@C757;q4z`R|++X{(T>X2SFF+7Ls$1zky-*t{>t) zJSnM%iew^@_Rpil5GO~N1P$P4u`IR}OT`H^VB;}-_T*mu1FWZqgKhieMm_}HS;_70 z|2*oP3Amza+W2zB!1{v`-Nmc-o9wd5SR$so%)MJA1&~Y8Hwa>GU!PO7*5fiF-N5;y z>_S9L04&M_b&GYY&-)h5-6g#ocpYg&eOdKM9-qP49b4zOcwS=A7{&@mzkSKKrCCoD zwL&Z+?I0Wm^WV~9t`C@I1A9c$UA<`#pM>(=8{^kq72@1oB-mDW(?=uEL)X23ntsZf zW_S;H={akMjBozwLKm)k_u>7lv*EteE03#*fSY)F{+BN-26 z;uzKd4|Vur1fE8uBa4IByU4H*(1$fF!Usgsao~`!efw2d85nW=`E!JwUbt=rqu!M? z)#q@b`EVB$RBmqiE67KX8?!6NpG83LU5`M2G z{C!A3AWP(Fku{@F{3D;pW|{aHj>HfYF2A3MX)7mDEZ_+KXy}G234eptsu<^r9FLf^ zPO6}Zf=?c8d#_DYbO!`KCf)FJQ#JwI5))C{RpIYcmz798pTWrFyi6#Bh6Cm(L=)`6 zyHf94l5?z*d}UJE$oQ4kQ&6I#!Vk3>PeH~b+_QR&lNHqbg-QLvq~m%7KM#O_pK*0> zQ`Kbv0CNL8b2Jrcy_OL~OCD4uu)LHd3iTsUQ(d3u+Q7FnRdJmhe9BYeVWF#mW)Ecg zv6nssPvIugCqZaEBB(f^;>vQ|| zCnE2n8c7T2Oz3P2yfHo0V47h2cqr@8n306|#tDqJHo`6k%+XlTuU$uZccpB*p4zgW zJ=!VX^q5nl&uH$R&E9~dd6hHJNf0ohvS@?W->I_k+^PKmpUE{^&lU;ap%LwyOzv?@ zrmRI~vQOIlY5qqH?U>KGq{%6l{JBhsxo$k@;yh|Drzq;m1#lfk`bX`hbrmKV)Hd6K zTC$Yq38ixpAQCx0jSI5h4pFvfraOx7E=G}7AzGoWetc`8_yjwR8Jva<$OX;JS_E3I z11BtsKOSZ%9H27C5a2v!@~{<7H{l!bY0}9fN!}ni1S82#>NKY@zEW3htK=7(%uOas z(ONIDfAoPJrV#lS#M%~pS0+d(EzO@0{K`NUDS%GWm|nZC6tj}3V2*F#TC7_|V9ZG7 zPy0(TdkA^aOWhHOqR& z>RDzj>3uC9X%A(EPb-Sf;N4c{ujZxd9t}Qrsc0NZjXx^%lckgFBE7q=;$}+^x+cUf z_BOqR0pUmi9fV7Vg^Fo0-20&QG)>=wLHUQYMf~l(%nymv)v?&>9>}zzvmi zqJ7JIvf-!t8fDNT5t(d&SWV}d!5jQHrUoR|2BqlPY2%YbT&b0s*{XtB`2qU1^N*Do zwndcc^~}KyXLfagqIHDBrQTVf^n(W7%7)`FDwI%xnywnYq+}fYVjyppcV~72dCm=H zS+_xsF0`s43j|NCj1j2kF4X$V2F!ZaC-5XEjP)hPCM(c2)7Cl7dzvUG*&w~g$dK=@ z#rT!m8(qk4uDA8Xm_JNf$3{T@4Twll%RE7L87)oxi^Teij{b&+U$;gPMx7HduA5(LmDGQ$~<{qu)XzTNORew_>w(RgUZSmwG(E1!RfSGVxYhKeu92 zzxaIeq>p75$@Vsn2A!EXoroTVZ&BNIA`(sYNs@BpMa{_!3#&oVP3I|^LcfyodApK3 zn*&u;;{*kW@H=y{d_UU}y!WmbzV3)UL@mWk|9;zT+)!SR*xi9a1k5Jbyd#^;sbRgX zt+ywrJ*(rwQc#KM6*|NJ^o#KKb7x^$4jIZ*iEv~0g=L4}b(Z-ZD8`4B9!k{j*B6IL z{tB~iI=Ea~x`QdHIhwcDO{JGPxQA;46$WNFqaZ&$=}CO**{<@Y%x+SpNY(q6taXiq zY6u5S0n7DUjM8#>vvTn$+df&P&%_q+ZM3}q2C`Bglv8OQBdZdbtl+=u^TzK*-cVuF z#y2s`bjj(!7B8WxX%KtNG& z+qb@x7-eT0)v&FQS)liQc0uM?Zc@%6Qlx18LDBAFK&T%*vd&v*d_Bf;o{1D!$BWEaIY)kj5cYAMfeBgP&a&LPDix9xfywYRf;dVcH-y?n|GG!=>DtP6 z=G{*yl;dL^(=q3rYGgIveJjeK10n;ncbGb>)3y5&Witny&Z&BFxwXrT`29-e_+$^5&K0AK*HUl_hBWMtV3%KAWQr4Mr<;+)@-R; zMCO<&GS1LT2k(I`|-s^?y->EH(B-DsWQj!xY0oM6; zv!JEr5eHIhpV!d!(rl~({f}1Iy%w6B(}q;57Lu#!%ByV}-QH?{AkB^Sk@$p`*1E5N%Y5d*X}Q0&&Fl*dbK+zjOdWfQu?VS z*8S2_%5lgmIhWja$9)EPJm70WIIG=T)nLv~;-G2u!$HQ{>9JbU{+#lFq%ZF!mtxu{ zBdL}sb3tLxL37ItB@zx_7L2K!RaXx4w$B_%dV9Ao|7>4TeZ7(VdT0FgKH%#^-q)wz zufMmx{{8b6fqDl?Y6oDllNgbUN^Q!Mw}ZXCgZp=<0(H)NbsLL58z^PO0K~L?y9;vT zl%_}T`DMuamcwv*$;*7>g9cU};_z#kPLUR^WAQ!N&6Tlc%wrWqDlzQJd31mbt7Hpi zN#Z8U)ZXKIc`00${2D_<6}C3`g|7=#YL*nogQ-0{!yb)!kPR9LOPwODQ|SZ3=RwAG z`^tSl!Bj>!7aeW$@=OG*s2zk+oHA!=kX_&yRw0HZBQOnsWf(XCpg4>YVB&KUWRpq( zLF7B0sUUc?*vy-O3J`5Qt8zjfIErS!`Q)a{h|rXM{S;ebr~U>1_ci2D*IKreq* ztL06(K`um}Uq?p^I(5ilQXl??(uj^jqwk8S*)|K3l0O#v@x4~%2)*azhsNC!rw{{&JL7`Z{h^&j^-1+!U9N#>EqVJ-aKpN zPoHD_`4WP>N6d|2h;@!af&m93>K*VmrZ$m;69gXKy^Wth+0_*01O}O&d7LYl6=TXD zB8)=DxRM?~K~t!ya?-D^OsV^kX&+69E|s$O>w)*L@AkH&4p$jUQCv{0O>@w8P+H&? zZGdAve}rgL(AXWs?KLp%6UY)*MY-lDUg49)BkWtsKCA>ZlAytAj?xDH?DV{i=&}s* zfqXL##xS|U7(_Uuse!Q~UV>bTk1wh}03mUdjgnm0Q}ySUR%-g+?fejcu@N{7dM-Qg zU@~m-RK~Z3B(h<|Ot~U1+b_$fS``|lY5~!%JDH`J#%Ezqv_n}TG!^lT z;=G}&=DC@=H~QmRdBw85xY+WIEdgQ_(JDhA$ZhD0#O) z!UqBF8Wgc9)>bEaS9?6sfa{2vQ%%t8MsqDJ6!K^+6p~k=MFQHM;AhiC>gMd$t&N|~ zI|F`xM4Rs~VVDMZEQc;V4K61O8ZTdy-FptF(ap|^f4z3~F|>0+z5Yse`dofq>Smz+ z`^#yt^YcGM`ES7Q#~Qw`vmf5cWB|i-X(O(!$uwa<9+?@8*BpL!MIyWl22lG?W}21T>-S zt;;hi{44HD)mW!;N4oS!EC@322I%iT#Av1&iDkLcy1l5=uK@Xh~VG}NSJ(e=Ac z^EQtb)L#CaL27G5k}t&x#a9gy|HvGJ!zP^&87}4x7GnRai=V*^K`d1$GUz1A(8XtZ z$-)S&o0ZdPa35yIQa@P~lZ1(|g^<`~371*vca>K<3Oz%6N}5;6UlkNrWlN?es z_c>uU*DmRHOEnkR89MmB^YyFh{M`2I62~%DEyC8Bog?&vN*syvAXX%Im!z|xzw0Q4 z*KgNx`V4=!N%r>NZc}5i>tEjR{_KG!upj+?zgP_OIz*&m3Gm=9xI9DttOfb<3I(*T zc8#z6HsGiESy_xjAt~Y-vnnBmavg;~a=s|;;8BA9#@eE@Pd=$~Am=uxd@oqkPHWOD zqwVW!|EB(OyRYGkTeJQrEHr|>#!w(6N zX2Y;63h_9$-;rM`Gk+i`r1tWECqy>=O-h$#{Ik`Xpki7 z+^dREy_HR{Lgx=_TdQKGtrKQ;Sn6KrGRLu~AK8o|gfRyeZ7qWt^9ivi41r7kWF2_Y zrbO7{gls&?z~>Tl`#|s>$^Qs)7R{TyUgosPU~mV&(ZE(M6eVwEgpS<^#$3AC3@O1? z@UrPMnzYNEOS0W_<>f)OO$CJREM#6?V!n093D9WPO}EVaPR{d?A8bmUzGym493IT2 zr^@4XzS<3Yb{Ix3B`&DPV6B_sDh)*0leYK$4WmDJUe4sB0qdI{lM`g;HG`d-rXbvZ;5D zN6Gp;BQ5=%a@Ky$1~Y0WpMc2>O}Px^{rT(zJ^T!n7vjBDV_yjY2Nt@RSW!sZs>1N)AxFxK^m$6o0*djaOcGn*KSw zCOIw9bY0=gkF?ipxa_R`*pI1<54_#XoUE>$$Nfi0$Bw8b-~Du|hXx#)d;tq8b{Tw& zSq^iO6Ps*N@xp`&+ppPtZ#gTPVhpx$J2%X#!^bx)GI&I7h`j!+3m4`LkLHR^?4_C zrl6~7uWsRgphWAqN{Yim38+EDHM1FC&)`_##)C~GzVhR-rl`Rw@}0CJiYE7`InP-O z$fP`PNOVe}G&L$Def)CNUGQYRHw1iN`oWJv;iMUSi{diSGq!B-?rr*5i%TkvuIF!) zfI(;h<@3Qs_vBNTmr9UDwm2tM>>I*2%>u15proIL0=X9hspM>b#c41R1{`>XNIB-~ zIE|f**_OrCtoA!elu7?yUrAhQQrDeg7lnPgqxPpTSFKfz_a|k*ox(XAlh*e$8OLl9 zSj>@qOov?&lz$Z{^my*zf<4fkmcB~UC4F^dDJuQ=(_`Yg@WwhEi8^Ab(?wZ^BaPJ~&HTuSy(!IzQ436JH!%oF zrbXno$1g&DbBu>@~RG**z0D>YI46EZ6|HeW>&!V)M>`zh9vx?-VJAY2fN|Y15zL z08l9=e074U&-XutjL51(ctP$oItYt{XWXg%b$lD>qWY!-s)Oohf7P`()%9G}FEy$gJ*u12s#}|?+vloZf2;0b zt3B^hs_k*8?MtW~z|;$x;v9P+VgPzA`TAdFEA7`byTHbA}ZnqECO6x?U4nEArzRl%~<~rb7vhDb>FRhI))lL zMY;r}8>C~TM5IAVBoygZhHe;;mKZ`(8YGpJ?(RmAP(V>q-XBXNkjKSRejy>2$#x%El;JlAh5V>k(n)C$E0i*Z z@{sSs$z2dK0SsF%lDZ?zD>VB4;Oel&Sa+^$GnM?wJb)AUuPHk#P5ngLq6Eve1 zw?<5iM?=7svO?`{k3kteqk;FD(q2U?xnKFHKauYoI+)L2G8}q!1BpGmEva*w>Gn{{ zyKxg80iL?F9DS_$t!dgLJ-=AJD2EVgQH)#CzX#8gM z5(~1sd^fP^U_<2Da>38JXY>tT&Ec~z&U{M2D10LjQHo*bEKpJ-P1>Za#U2KCYo**! z6}_VG3-45EApW{KHOLWjKD36LVWyCqC=|(y1+RjPl?Lm-zz|kM(v`!tP8B1A%*#I) zR4jjOWHzx-6uqc|w`J@4sHziWrH2RLv0z zfk7GJ6)4C^qQNCFX^!iQ@ONByhlH@$cIIhl4HtJc>Lh)R))ej$i({CEaxP}2m2%ff zmhtOS=f7?DeE-L!S0T= zP(((57`xiE*ha$?LrX%7A+d~R%ML6)<2^B*q?$8{HoX@ARHtxIf*O^cGPFTND3uJ?7>NhW9tRz)6ekjCok$?}c07N4OA&M}*#e zsg5L@uRe)9Hq%|Ml5L2XV()0AGPxSd^pt&>*f6u%>OBo!>g2O3-55G+QH#}CodsIt z<@;w`T5S<#U#*ChaPP;O`$*?5z{xtg(_2!8%{AUgh*r>R&WSpTv6dP|#uE2YN#faJ zU`nDM8-y9+7)TuWOMb?FVd3+@$mdbEaB@xLRipJ#GCw6r)N~S@ayxSr1NFjZSyD7+ zwUw$bB)ZfBo4x1!=8}V1q4JNNyB2ly3*4*-38#C}aBqcgd6CQ%%38QY>gjVWmMV`T zcjb5>b=1$}od+B3%1D2(e+g zfOH-^LKVTle$#ujXf|yRrH0eI>Z4?9N3lrbFVS`8FT3q0_Nt({iof^H6X5X5>I{CzcDk_$!kxuaS z+j|fHU5Hl9nJKWZRTE z`y?b#FR3k=z;Z7GtWeDFY`?P3t8mbf)3*D5{i^61D{?l$$#~k{KmqMY0L))9lxp^x!o}(#cWR*ira~ z5v!oF{SzC^I*2)*!re)pXI`Mj0N%8lkZOCe4o4oN6pWf-X5(|LI~Wm%uMPCCI-P}n zy#F}(c{KNVK)a!RD5l@8M!$-|YR1;0>aa;9-kpcIuiWB7&JVW~1Edw-=gDUUbmWb( zsHdyDEOU%G~Co-na{%tK0_ka~rfhQb1G7<-_ME zJN*Hbhklt;rOHa?QQ8biCCVWz=(~zH9`bsR2RMCd+;fsk0BL^^&bmhWv?8Jx$B`cE zBtY#X_Fh_@LUsc>yU@I6onCgzEbHQ}%7UgN2@ROJ4RVqOM>wydl> z2Ju_&GkVFo0kI4xX@$C7)t`;M4-8%B1%wnoi+g6?zJF_=!!KGrz^ItkZC=%Xk~P>f z{)UIdXmRqW|{580l&f&VP!2issMeRVDVH52@#d+RAk0%JA`sp=?gM=duZ@= z6GAW^RUq~?q3u8^KNlVbr|4bUw%}h8WkHASzeJSzt#w226{YkGDD+mj9Eeas)Z!|! zchu3^#ffTb*!6DH4VsAH#Fx`C$Erj=$CiISNso+~rC05+^E0A+ZK!)7PJ-6dw?=EZ zkXLDuWL4AdeOo~W9c1^=r+6wnk?Wr2aV47^C=ThI*LM4md$lu$`S#afA&$02{F!|m zB^}jvMI>CiCoI>xf^^XqAYT5$Wyn6H6tka%{0vtWP1x)4 z>@mW$W4zku+_1cdk3~DniAk=fY^#qM3mY3x$9VSQ$050 zvdp?N(NXi-7>04v3+mVk(*mc7LCLs&f%?i-9@mO%pL9!W*I+lR%bl?jT_Y#!0%a6; zvDLE|mV!aEa_D(1TU?KDWLYYvtm_}9!D5(ivD)S(QY-k`d2-ghi?IsEW`fW~UvbmT z;e2&4U3f1D>Jz;F3Wz9QZ$iWZBFaa^7ZK&C(U_KR5#^g=+;1);%H%dbBFZeUD1eBv zwpe)=Yhemk_qF=d^YTWHRx#eb*=_?MqP*f!(Jy||z&daPOWY|n^B|!5#^3| ze51EKYb!_Z=&LY{x$b-NPZ*LWI7_jzp!!T2J$vl(&^T8;kN{i#7TUCZ&kot7?5^?P z99RDv>=NslREO|sQ{_eFQTR_1T5$j91`>EM!~ z6y0wY9pG7X6XiyR*<`NP%frPQ&)#I{zcoELwJvRA=6i@{%QB7k5uL7Fa6tSN!Slly z2sr4>Aluu~c;49dk(5;89LbXV9J3VKObeJ!%!sJxc^ z*w&?fsw;4G88<#~XsN#heFo-j9bO=;8^CFnfn}z{L)Kjyi2F1_j$Hwna)0-p$Tltb z_ITX}u+Img%*21X!Oh^Xh7LrOS7TY^?r__OYGvyXm*l+(x@R9|B&$Q($Rh6(U>|O= ztxZ0VC-3~+KEifZi*kWQ;ockjNSB5?S3cx*sIA#YJu%n3dd|`*f$9JY^uJAum*2_D z=nx%AbesMvYZr$c0fS`CDg$qR2E@Aj=`9c;zBp?)gO5XO&Z!2AT0Z2;9(!n^7vs1w zYY%IH)j6tg3L~dIQLVv#Ox&x}wP+2@UK5Z)Vw>1o^YD<~d&3b){r*ur>8yRFvX05) zs}TN@e3b-q$7l0mA-&l5d)}bp>M)SsUqU!k;mlYxuh7Lrot32Z%P!^?pY*>*81r;@ zuOj{H>N*6BLegrOjk~qfMBJ1a7{!SI$DwDHI(9wGC?f7h@M6o2Nv7A zPqceBl5R#$aHU>;FkJR}x~OinWILGM$ut{GPS*|b7##d~ipwBontw|~uQHRthX(Vg z`6g@^Ol2*bI|=F;M{({c;o@h!Iq4&LW4JFu=ToZM6(von77QuqJrFrQTg<94izs}I z(fXWt5Hj5@PxC^PkrozyGnefS8)9+ct0gu?j9bxl#cd^ve1xSUgFd_|8OU<@Y)`0B zCMLk+bPo5P3immtn7)z~isKlR3_HSar5NWFsQvur?V`;{JdDLQ@pSG3hUpio<>X~P5R7pacPy(Q`*lhkq91++?`%C0ka zE5Z}1;`w(iP6rd75((w)1`MDxU`gyq2)sUi$f-13$hK-KB!$$%>hx4e3=37Lo=1P< z-17Rc@%*r3y%wf=B_9`6Z5nC$DXn_oD-rx_v6D3{LNZsTchPc1grwidakpDS?o>>E zp*1|j7a)Ck$M3K@pNsRBH13#5LntGk8t}V;UaH74W^{K!tWTLuZyxxCp)h! zHRMG&pnLQwB%TrSwRooneG=l)X8Nm3SAX;#y@LICwxq%%i!C8v*6Qn-dFEby+uZ| zI1ln~zuMGl6dA85J}7d0wPkcJGTF-cx$Nny?K>Y1h=$WXLrKrp9oU>^-1CtcWTkej zqfRPc@g)zCJqF>fMge)Q5X>D0-{{su#*67QqUpJyyVclb!wyi`lBOBbA?3lodD z5yc!G6dMhUcbi)Pp%KVG?O#NnzSN^1 zpPZh3Jx4%Zrc^A2KSe?yV7tf<0f<9-NQz<^kRKxBwf541LND?|0OF7*yZI@L$~hTE z(=W#1Wq@dNwyPpn83EN7bOlnGHV9HxO_r(_2xCjujEcp|CV;tr!1yZ?eECpQ#h#2B6mqSQiA`pkN zzuZ_3<7#JF3FjNPT!{bzL@NN}aAPG(5{-2g1~3j+qZOI*S7Vd~-mL-wBG$E7O)aan zI30`pwRi)UcWVj8fvj&6fdJ9lB+H!qx5*D)zI*%3uKj{>IBvC`>h?B&9S9Hsj6*0I z+Xmd9#5&Ijk4n!llZC6!FUyPz#F`!2BF~DO5DBxQdZtxJjpFOH`OXX+oZOOw2etMq z5HC%ZE69no#>*>CW7~q3MQUsn*3K797Yl!yE+}aO@!1m`U>qhqD95Hsf3g2h z7>9og5EU7AYyDsxzR`&S3lN!kKm7b(1&DTjk2w69AG$;we#;NRgE)`RH?k9Kf5$i! z?XF%gKKaHtY(l?&y4yzj==8UY!^!*JYE#p$AI`q~2oPQW@;0{wxCvkVF9Jje*l+}N z#U><*T_}b!9Er5E2~A|z2OJJZVODGg>FxTG^}^8vJDYJmcKv9uGcXhsTL_YO{aKVV zKqY<>MwNB}fYXnCU$K>9dN)w0Hv`wTvz7XIH%JmY6C9-2Mn|z1tf-txkkHx2^mZ{o z)rpEIOYsex-d?CqZzf4K5D@m*3p2*fBI{6WKjBH<3%69xqL}Dx7pU8dunW(kT3754 zncj&>G6(%B(#ycY$<&Za?E>XfF~hXpHV(~)*{%8Tqr!@{!}n3cL9diybnz1d8H zU0tdk`%mH6&sh|dx;2vbWAl`svuSsAYt`+?@$)KU;G02-O~BmvmQZwicMwiDL44W> zBUg};G6N>B|10(!o`kMm3z3gWgW)-Rxk`Q3dLNS~dvgS;0mh-n$7hR|jKhA1{Tmh;n5QSyXPMEHCqM&MtSf&5VSV4%pS3~)rAEHh;2 zE=_DC=_@IWABbdP1a>4qZzzym0KL(;KqLctb0IbW0a@Mwh!J0akBuWQB-|G#}J3T4$nCCzArf0{aaU^FG z^FnVbJ^C*6Mu?NGWah(R!;^szC+E;_dc)*o7ZMa65g7%Gj(Pe`Z<5vJ!k%5~O{!a3 z_NCtNo3ffia*HnXriL5OjG3S!q=~MAt5LYMoW7&>Khm4SqvK(8%nQ8%QB9V>oBhxS zxOEaAO}q|rML|gXp*OPtQun*wXk&fi3g*WF{K~~IbQ0`K)R&gPrdod z-c)(P)XjqYF|f+2LIgINqAGkN?82UGX|AH4yXy37AgZ~8~b@NkLqvuRA<4X(zh&+kd1C~exA z$LhS@yYt`En~jWMrh<*kaDmN@ESNmoyX@GDz-ppJ!MhxQsrxP$9>}(tmmP1tnV*+a zuvzfpB@kFGZD-qhQTbLOHyYs<3wHb%#M^mZu)r+!dH?`L_$ z%KG<^hv%S)Zdeh~h%M!gRkaZyI&_g+!L<Z%o}y?1+oNs$tHs>K_BEe`V_aGrdU@91Xlc z>h97+`mQ)~aUKc7)V?Ei|AF3|?9`0tx&K~ovR-=rfvG#GaN_lO_78gV=&>eO$iLScvNS}E-|G$7lte58q>Yh6RKEMq^+sebGaiKS}s-&%o?dLpnfTLiJOfm9L^k&*TBJekQGYkrD0Q81j?qBOoY4~&Y`$|1} z)BEu?z0WyayLya{_Y<11a{#^RHKq8N*s1&*y>Uuvuy|LV{}1)1H&^0I*T9owM_UZH zn5)c^og7zuo}DXiv}1H*21Guh{R%dRpjaWGC`%%tWo^wUyE8mBVBu{_J5$elVb?qKWJN}d_iBq5+1R2ewILK2}DbPsh z9?cOs$XAaj(9DI5Nz=s&XcrY|DaDNyalR-t!YS10fQ*+Xk6{~&7wS!Pk3+JFy&gT3 z|EYA~A4UeAx6}5HWI;-ZE*0wk89sjtq zVg+GbbXI`Uog9szou49ss5-BwHT&_SAn5$9vzqPXUPeH=?5r*>Gcc{3FG+Vg$U(o; z<9~HlfYLn#u}J>vtp1>MdDm_U(Toh!_@Q3V<1ag_KPX)?BMkoSG{anv_M?eLs2l>7 z@^_{CcADX`vnnlLYx;Sb;s5i_>UT=#-RpDFSxw&2lK8*WS^f8wZrpmSsOD|KR&m4U z&8-pu9=~1s-{`EiFX-|ArnCA-O1Iy|lKVTQ`#U}UZ=Kce>GA(e>3)Bi;h%O^fYSZr z&g$Yc!@p9x-<@XoA9PmQoh>AF|C!RMhiCp#>HgeV$^HAz>WL5aSz*xE-Le}}xcgV1 zoIc|q06MGjE>wCQ^RL;Z7#{(p%;nObE&&;Q%Yy~y;vM`5q6{H-VEpd@Xa zrk3-^9>t*jI`Xd`g@gOFAn8YsQe^IZ>4^me-*Bc3qjQd+hcU;5Glc;|xu@}Q3`x%z zlhQLnE?@2f(M%Q}af+-gzOoY53YLhX!b?xgNWE1ZUL)O+7RuGteCdhFw6(FPCn|@% z6iT1v?i;!A#PY_*Ncj^I*vhgi=O^A?zTAu$lw^WkACfg2kTyE6)acj09ny#|x$wji zl-0S1WCI^j8d(wf;G?xia!WEzk(B{2_pcs>%d9&^sp3yB_pcu1!V?2tZa*qXphvN8 zMMo#Hoa_GXiA5YahiMhwWy_2ZMu1*;Vn2G6AD);qI#`G6=gaN3LsAuKf|jUUTfX+| zsLtiftykeQl6Sk%=(0!2rGMm}AszW32%lzSzOTOeFP<1~gU`nwo|yA`b%^3dnfS>V z%5Oa}(wmJEk3U$}tpJ|b4azgxBWA!810fn;cw)xqV06w4PpoBdQF%$$aw$Mr`A|7P z$Ub-}XxMTiJcuUV(v+MIO=dZS{iT~+sCd!FayVIwWn{R*q8sc6^`_!VRPZDQ;E8QA zm;;^|=14R|;9a(ovIO9XeU?=MJTZB*dq9s;+Z}Hd$eKfI90_=09SC-!v$@WkF{ zSX`B8w_1lf&I6v9(rOpriG5wX4|rmFt?)pm0!vUZ=Vo43gan&aMzpFmYkHhffmKS9 z?dH4W)F*5Oi5ZdB*748N3JPNLi#M%fie9r_gO&AL+k{ok7hDUjeZOhr|LQB-3*Tm3 z8}_sgCeeFk-2$GJ75(yIB$dNjSj6X5;}-QqFQ;8>h^prUDT!*9<9i5e-{yD{)@{DL zMp*y8eTJZ6f828z<ai3xXK|8RQIqgZ?#)_!z$V0%02f9K`)GEOBKrrWW%^2KO1 zM!AyH9O_(Zb5%?RqYpS+ozvtmENRm3!3W1LZVJ${M8o#ulHqIuQ+meS!f`b5yMkuv zC#jr*7tPhi&|4ZTn)#F!)>(?nQ%0pGi;CsTB+L~5Q?Nqj zZdnvmZ0ssBd9FX0fWuD=@WiZM$?|;j#PlsXx`Z`g!S$;QshLU-gLlEHv#Y7nQ#=}< zTVe~LC~R_F5Y^B3*G^Wu z9ae1SuH#gKZ1nEJyLYq3g&jE0Dogak6b zudoa`T?Z$2D)>0~6VvhLiGL}D;IhVI9U8F2QA7Ac(YiBLTQ?$D7=|J}&Oc?*;N&ZQ z?@^w{2H^2+;$4{?Ou*%1y!kZubrPgCho#Rd-cIjE+GmYF^(f*DYEQdI3jiKfo&j4M zT{l|3z#Wj*j+xdSe}w|0F;RVI8Bx$)M*L?upmO2PIIP7slu}C$WmEN`qBim_|A4T;~(W(RJH4MWxkM^l&auRDs;%?V0NL z_*?8E^tl>tS!ZPc)WriSAqmY^I&BcxhNLJqLHs-1^H zH)oeAzSMN86gwpJ)H#3>YP}v8b72mnZc2ZtJD_?!CvC`bC@);HZvKV?!Pt+U_bTMNT zys>8fE6u*-;e!NHhu)+)rGbLIOSqngtu(l0!3xUD1kVoJSX9eGFOKTeAHLy=EDLYV zohF|-Y~M<>3AFDun|@!@A&FZK3sSy3s?&S67-d2MW@JBltQId7J0-GKqLq~B*T92N zAuQ!%P2OdU`}3%d;z@bfm{$|Cnoj1@AkVWI!4|=Zcwn5>kwQt{M?zB;cx#q7`L8#1 zruBsgwm)SttH{ex_zx%cSLO)zy;Jo(9)aUlMC$eAJGAs zO%kzq_YeWQsR`BiF-a5|ejR2a6qTuIi~0+NSYbb!t78%F6HTdM7&}>EuF8(J{>k)Y z|I3%veLJ?ECo_w<)ioU|?;W0<%x^A1*kAqnOW%hl zCnpOCcr{Jvs=L1W7}ID83@v`OyZ$EwVMAcW@dHeSwJx^Dn+D!s( z&hBE^dckc|mvx_cMa8U-u1U?Z^^)8AC!z1JGRdX&Jpv$7EsHFf0SkgQcxYP(Zz=ma~X_@(1IR$?yr~dxuA538Osee^q@ksu) zz-qmY^rr#~^4kK7IE;=koGFSeh9QxLBNXrZXT4P}7D4G%p)L29% z0Ohd!8DDHwR#9UU&>HKix2rQJYP_zKsOoCF96H|V=wRGpaK<^rv;Kp$c$&>HVR9D$*u&Pw+i zz0b4lb>1L^znj2Dy+XSPNWX@Tj4MShu8vm`5T(O@TrK=wU;$SPB_6_|J2N$oTbBhE zQrf^Pbz0_{WHjuU-wYkSRV97?<RR}24IVAZ60kj@+`M_uoStj1_xn84?53>_aw z5Mb_E1ZJ4kp1oMOn{IvB6!WO;$Qqs=csy4)_!{>^t7mrLrsyYyumjTQ8)PS~NI1!g zn+u>&21$xGH7^WJ9mIgqAS6zh;uo1W$o9`)V{W$kHYVOh`u))HcdhYK8g5=IH362N z?Y>DMl% z1|lo9ql(x9AbL8>aQfic4FVC3-bgK9v#DK-X7+t()R{W{SBi2T$?m;B4INioL$R$o zp_3Ug*{!Q&`S7lQrlO~#i*5ZHT#z$iyjY0TJqE0d?mNKHvGh~iRWso{t>IrLu%3B5 z&ZiKLorKr-z!BlCd`S59iM<59B{N4ukHegJa4^5QOO&t$vE=Q3Et3v=d^LKC+5a-3J&g!nBt`b>cWOI!|B z<|a^JeXh(?DYDk?(Nn03RZe@XXIzbNJ6N)`s-~~VHiG;q<=XX^0?6-eM`&l4eiT?U zsQM&d0tc$BnD6(@cF2>ao==<^(C6vTS=6x9zeIlI+HrG!Klw|;hDwRYM9=)^y2}E~ zYyIZJ@iagl=qq{hr7)&u|4ZX4@%4`=*o#QHu}x^IrM{%Si)f;U&0rUkGq7?egv+Pp zd21;)pO`u>)@Z9L5q)s$7TUh9V;lY-3arl-;pjf53nq#6(Xm6pHYrN9i-$O3)P_cT z_~y;xG9B1_jF0a3t}=-pb*V@GyjnPqg&to`b&K(}U(X*3tY23Pza1^~(h!*;4?%-T zR#@+@(ABGBkJULsuWgTup-Us(+2zGW=J+u>|MW{}tBdcEA zSJ~2=IUcX+uPS!!+cG-wU=RK34DySRHN|@7bc6F&c~swnTL*D1J+`0MGC95#Sf96| z&vgvV=%c1(I;mY;r8bh@%FJ%beNnV2u#PydiSpiAQ7%VY+d#2SQFg{bG_WUx|o7{&uL-;w?@FcuW;#tW_)#C56D&Rdb7O5qE-GUA9D5Wb38Fr2eM3KILXu zjZpeb`^e?i6%)~Tct>qaZ~Pql&f3M+)qv_OM@8DTvXkW6*E^f2c!s>57=&yFH7LHI zASQOV5pZE$*}y+6faV{R!oSZ3jy3avtt(`5E?5=XNXfU^0Q*$2 z4sGU~)bAZ8CroUzpR<7mf&A%qVFZ-lIZPtF2uMF2rczqw#~DcB+eEn3zjc^q5&eER zOb1)l6*kv>;J@9v(ge1y7M$wOPFTT)D_9NKx^f%wPcNG+NqmmX->nygc;$C2JQE}DiLw{lE#*Qyt0WQl@brM< z^APT5DZ6yk&*^U0O+%LbmQRRBNK2m7uh9=ZA((AkprhW`zIC3{v2Z=|Yp!M^u&MG- zx2|GXA5&v56&xy{5QOeBG67TYxY!or&%rdD*bpqUi><2%fdb#Qu7rGG_k=WWgQxi0 zsF$;YA(ELqXc@BfuS@T$7|0RbMhsyU*$ZVYqm!v5yUsbE7Yg}kD)poW`J6ivIcTOe zu(db%&7ZcehH!cl=_pM~6`o+1Mk$3O(UeG4Ng>G)T)inp7iQd9!mq&`)v$7fj`ecu zDvgewDWK*LTURtH!vjpQe0Ry;CqN!nmB#fLBVn>C$?L>)#b>Wd6S4Z=WJ}~ppx(S! z=a0!~8rhRLBKh_U^e914p|&aS(Am!y7QwbjT*=;L|9a9E*z%5 zVwdWl4pUMo+x))3mzsQ;68DKPwS)3M6hH~SUAM0@nd>TjDQ;>UMcL&;^=cs^sB!Sz zo}(r9IO?eb#)8SSl*b23w4zf8>Dvd{OGr>x?BDX?0%cyC& zaG35ZuTTIElTd$Iq-(>Q;r+voot+2vKHnTBMb+|{gg+KQ_dGp<>M>{}i#}z|A+anX z5i2&iR8aX~+Ua@(y>f_KJ6>}fg|=R|fESN@&7GB#_tMosiIyKoRcZS5|EsIbuK!5^ zbTD|xzU40wKVGFe8%$ccWpGlbLHXuki8n`$3E|0Pr|Qdc{laA)0@8_G-j`tb72$IZ ze3}EeOUUgy)rUcYp7Vk5N-6ScI{+5<-RxQV6LF**Fnh4TBv|?k0B!_q#(setkzpl* z0Jsr4Vn%jST6!{Jeex^xqU`7NA=Am zVqh0$yh6)J&%ngW$RsVqWO`9BF>`TW?hnech^Vl%s8aSB~yW(7(pc5Z=h z^bsyu18xq^3;GBLHxC~_pJOaPKpznj0Nkbv00}Rnh>)O>W*#sOSwvF#%yQ5m9j| zx$gjy8%AW809S=rdyXwuo(I@ns9 z*xEbUI=b6Cx;VOcIO|_?F_Um{_qsrmxB@{tM-R^nB*_yFz6&0SZ-~z$H6H-?2le(1 z4D~a`^z(-L17H$1j=+$Jpx~g8fB=9{64LiB>;YDooiKnZiFj;yfhu_{9~~PX6PFyH zlyXTc$xO@0PWP3|$j-@pUXT@{n-%AflW3ijUzC5zEGaB5FHW{AF01?rEUD%Ztd6@? zon%~B|EfOIzTtKA>*ltWwl^2pk`!=zx^{b4Z&#s1cW;01z|g?Z$Z+A^;nA@RY>Ds< z09!IOGdErLc(&Shc7AboabbROd2w-Jxlt3Kn5?$?t*yUXf48}L0Wxvj>D1Y|V3}Zk z*xTQ2u>y+9gFbHnWOBHmb~@m6*7xB5Hp>Jcj<5p65pYbiA1Z5r{snQw50{c8SrHH| zX&^DM-3n}Qiaw|brcx?r=o0ncOf;x1S8w~>TFZ~&s0=BqA(11UV!XPXmcM^x!Pqm* z3hP%JMCPf2#zVoEYc1TC9b|8Y!4Op%y-I{B%m8IQATVI`D=+|*uDuPxA2K@RrjYML}EoZ4{8DxgCCFtB5iA3e{hoT5EHmegbX~H)$)&97^X%#M|81j^~T$ZQ{ zbY&CX&5vW46^ctO?<6gRJ~`S-ZrFA{Ek|~HR76fFxF!GlUJIZ2erFPy1L!F`uU#HU zxgK7yBwhI0sVRuI(?5byh%pn*H^dlq`!X=#t<1p}?VsB2q^O+GS;?-9EL&I*<yS`!$YGd?5F@h%DsIz_@GBaGEGh4IYa**? zS62CBk*q9FF79 z)gRi9N2C-v%h-CKbJ$^8e0o?sUW}267!U$hwVQBBXaBSUQ<%SZ1$Rs8jUDOuCz-&= zL4TzYqQDVuHH{u3o&_&5ZoBcwAmp3$MBC9__w(I+k*^-TChDMae6-_1N9V6nE$dZ5 z9l2Y4?kOl^auTgD+YuE}KZWa#G}~mMoGwjo)JiKC5ONc#V{?eoyJ7WWf%6HEVsl_C zfpvfFIvic-SoLaa!THzw{*E47$RqK+4jonTM|mK}Yx-RdB$2!M2=1e;%E))Y0(Ga5W`Y((t0Hv!PzX8{T>?8uA z=#ZQhcQbO+xgO)wlUEGhr!(p25T+1yb7?QVk2AAko&4|J=}^hTgVAco1r`HZ9=-OP z`Dmbs$luh0r)#b_k6;mu2wJzkL!Ut&r|^nj!aS6-L9f~NGBCi#1_Q}?__E{ZvNQD! zCkDV7pGpmxdzXizG(ra}h<6(hzj zYtRelDzSMi2HKa^wH!>=lSYDa8zLOmTn}uJjW7qvC0|8fx3KX`lLZI8thT=`*;_%u zw3T!oLSV3&j)0RVi4IZs@o?Z?k__`7KZsGY-yrBR=;SekKfPm!Ap5o6S6;AgNYvg! zAG_HH!wz9MBbp<_G0)e^MPoS7zruqKI2`?>cV|O`^JN@n<%`S~M$0%$!!EbMmuHApThT&1 z(W&ZU;?%d*yF|ij1;!-K+Us7 zANz%`Z}nw(V%4)*F1qqIBgC(4SO8a?Ck7XqPzwa4X9W^p-aU49xZ(W+2oPB>&+^|~(%YKko2hWn_? zp(WwhOlfeo(%UKQqaH7zAd@zcVhLnnSHV0JU7=pt?gK5na)Z!RK}I_mPt2eb%DN5A z*cES2{1FXie-&au5E`N@KHXc=UIg^IT}*5RTAU^UtPE@+bu74@i#fVW53MtA9KtlS zMmLR@9UP}?9dmI)vfa*^Xdlaj@3B0xLkC>GdKYOx(ynmuHZ?I}HynkP4Ajg;i{NG( z^~GROQ+ikum4g(72}e05eS*E`NmPo0K7mlS2*TlV#}EqI%7T)c2H@zRSlbg(lA$ik z*u6DHrX`iPuoEKS_Q3k&#HUEst$v;8yvcWo6k7~zO=^$bZWq#_jzz@v7>^r!NC9sn^x++p<{DgYVchL+7D;j~c-2WjxQU@F*_Y*;Ua7Y{&kyn{}_abO^fFrEPRlln~k6+u*i0_7}{a-N%b|=!uwqH0W6SZ7x{$ST&{e z&uLOBHo`+tv1Y*m_|@S~=Y%*eN+G;Un7vR{QE7)(grKsI*w&*l+I~2AArZ{klmo`l z_v=ZgR$TCWR~B$&wk~mC7uXLS-62tYvdX?e%Jsk(&0)}qdPCejh}C~xBR2?R*$kPC zg@$6<<9-V|d=PV?;wINRdb&D|*BAypg{~MO{#dTZ(}av7<{+Hd4BqAp0S~cpLOZHs zW`|u&R_iqCJCDb5Wpb;*xYImp=&5H%*^B$>`lN6XVYqCDHNx#@%I@}6tYm!*aS=hc zO*XA;ocS!_E`rT=W*{z`W99&SZ5o9;!)WR1dby2s&vj&!V}l$g-GNWJ{{=_EHcS23sjl#=rx;b<|=R+qFjlpV# z>FMaAR&n7ppsR9Vq|}HMjfNI@+H+z$JIbrWXyT(?nPCOB_L{~4;nPJ4D_R<0q7Uvo z_)QNpX%ZX>u+wP}BH%dvaz%&M1+eY{xiZAH8bqH(bfR(}iqII&TRbmvGXiStE0UPV z>*6HcJ@E6gpV?(gM1yLK5okP^a2C-SldNfpv5!Q`pMwhM%kt$jVo8G0(58{zL9Pc3 zM)KK}U=lm9pRc&ySVj-vqS{kLm9r`%I3lbn1xbVbMUQo^dZs;ADpxMdgLjvB)u3GO zAbz55r$U|_0d=D{qop$8rKv`@MvwiHpd-UwdF1O21e4^~sR!&z>1HbJwkqo_9u>qr zGF_&3;KG#htjg%(#}cJAoGD}u#o*M(lrp}lum{3lENcp_?2M-6Agi?(t#v-G^zK3CKG zL7O;j8}frTNJAT+PaAu0n>y7S9G^A~gEv~?Z6Vfg^sw7RPtoe@-WZ>@YEreEZ?~Em zv;)tyI8hv$K8kr+tf5{mWwgs|NjVz4|v&`ZpW;w`cp`pZ4!k z4eW~zd@>mL>@{$hGH~25a5_8i^>hGXr@tDBE-3j8_mT-l>LBRVAokoK?%5#t$`FD0 z5Ru^!$>Sli)FFIvU2oHX6!wA=ieWnOVFtrtrpLo9sl#lqhS}$aInRc3G^nJzDq=n|cI$sz0xLQ!&_Y2e_i zEt+AOnQ4jOY7|Fl7N70J$qP!J?Jb%ee=ys3Wv)?Uwu57Ce2!`K@!a$d)6}cE`Ewkm zxwE-t4(lHA`L|~@OONL_Q)%7-#F3q=J7@FzsaJuu*)j2j!-terXQafig|8~QtRKnD zJzOdSQJ^qx9TB49WO}DvR9r7YHv|kPu5KcuC6XsgGK|q*cEF0e!l6eGY0uE-5it1j zXaxpAHj9LzP7IxcpavGYtVNtrW2|=UVX`MHLTM`^^CXnG4k?S+xcySeTug3U%Rw^E z$n1zZr8p_DoCg7MO>A_x9ukVeusLxH`7)5Fny|E>i&HPdX?IqiHoQD9YQ8qLf^C8t zB1@~ah{b0Dsz7i^wgb`iV|7BYLlM|Q?bbWtn4B=AA}IDe3|otn&}k8C@Ep2M1VyQY zZM+1pHh^)v5OA`UnFAaAc+2uAc7Nn%5E*uZas>VHV1~i z0K?IepoMb1dtHjwmyKKr#&zk(>UW?`hHg|sH*kxQv+UM$cDF=cZ}-k`lTm97?pSFV z)20mKI7t#tl^W8+kzX5Q$)({WbG=gqBj!nBg>qrdz}7wNKr@n9$&wiE)o3#?>||pQ z=aY?OIA%E~vLXy;0byM}jW*GE)d}{Vr3q^Wj){@Jz69GLGX@Px^5mrLzFx%PG+IA? z|CTEQBd2ujLouS5xmT_<9o)YGLW3i7*dpRIg28x%cXx4?UhRunZ#i2_)Wizu!)7JW9E0xF)WXsE#A) zz3A?WGRg(&m&D14LXRmyR8vEvM%eSW-Rna^eJ4TK0>fskK=a~!fA;?K{Tr(~Q(s)K ze%${mOl-H#!u4fI{mb=RyXnYZywb6~i_ycb66*D9SE}Q9pR1vvseL{$1AhBc?A_gR z)s2?LwbaJ1+Y4Xe2S>xN!HI)7(*7q?>NusUXGQ17H*vvB4mwKX)~#qPGhA}up}{2? zqfsYCAr@b)m;9Xa4?TSup5=qI@~lAG!`WT@^Io^IMrhVra5}y2VI<%vqTC)-O;Z+T z^HK0?NHK)NEh$q|nVroECkICb8HhF>+?H*o?+nttc4!?JgL~ab{N|>j-uSj|rPWyR zy_xYH{TlnV{^$A=?+qK=58kiMOnkW8u|{=-;Y(e~Q#Njq#SGF2-`WZ9 zeGzHiC{qn7y@ek$CrBHnLs4wHv5sFh+b;6hW+KZtn)xKtFOvGhyRnk{3-kX6mOyF0 z{d@TF<4O)2dfDLG<+0dW?yY%u)FvApcOft(f^Gr0;e5k;~ z*usMo)xyDWtu;v7;h+X-s8b+7F!|!ohrXcWi?r6j1dx(2`2XV2Bg#u*ji???Yn0e{ zIuwaStzh(v^cG#?Pm}s26s^XrNVFkQE0Q!VCyrvOVPH8Hd1R7HHu+?fQ+}wVF<1uqWtd}@d1jhxw)tk91tcfy7Sh1Z@&A^`fjrQ7JP8R3pc!7zzGx@|Tj_k48HOE*1cr9(e`b=F&V9blHn7X5YFYq$OO zq+?edciwyVo%hsd|9yDki=RArzf@L!dFGpU{(0z~w|eZrC@8?F$FUz<9b4G9`GUqX$6LT?lXx1ld?eV!OMYgk%J%Zu>Zea;UIbc1EH$+qrSVC%M4bNffV|n zE^dWyI_HvA40lpPwAf%}Z;_!xq7aDlX(e6qYYLc7;*cS3fLk~0ArAGGACvfjiAxAV zdJ>hy^|;W4-no_t`e1~r-H-=B)RuaH$SSL~rHM04%NsBdL-l}R4?1L6x*Ax<{sh4c zD`Z&<{QwzhZIO*P%$6W?BgielQIK1_V;9#5A1oqKY&4Y57bU60Kt7UMM)>0)`GdmT z@PG?k-~lGnQiv;t0}e*8-$|&@N+)`f2Cw8oDqA@&f#{J7=TqY-4|qgZCh84vm{ulR zC`vnmfd@utRuALX8%T^f>G$7bmCM9`lZJPTaW?|Qb`(DUej5l zq{8%2Swt}6GL21Pp%rb|$8=WFi7ik9FJnhXAp*04w`|)w&0q+9y>l(r zL7A4^z&I1BmO#9roIZr;U|N_zdG4eS@Y{(JI{Hdas_~uE;-fSbX;POaWt`gN;WLZq z%-ob^3l@}9ft)o&i}J>sK~iB#At_B|RzrWPQ?vL|o-!aJ zL0uRKiLP^_vAn57p;_co&z;NJCl4Suc|L!11 zH)^(lZJkvfGdBGfv8z-wruSi#nkhK#R83ph>rN6?m5m3utZYK?o!wT!g96toU;0n61M@)jX) zRj64yNM6;FcD5mnTq@!B)Z_*hvVeW6ZQG*+*2*fdeK=!D)~!$gxgO3cKCv5#=sjIs~+}gD=ASb#6YOyiXd2rYHqDj zzgX)&2G=c^)^35^{6a#erOX11z!T-XKk+UU4SjYmv~cUJV6&PEY&;)f6^!e?qB+d* zE9o8tN$8`(8d`n0asR-J2y6(C+1Q$;<2#itXIhsL%o~t%g%3jJ_Y|nnXVG%U`4}TR zHkNvtRj;Er3GUE-GYa3B(O}`8)^ByE z!-X|_w-6p=Flcj&Y?2#Y1p5VY&Sg%~qYoR*)paj}r@eEN#GI~#(ZCoXyH*SecGs01 zwS?QMMed&VAg5xB!^g-^){1=z9jtJ$#U0~psdx_{KH?9X`0(oz{eu2u_BSER!&p*1 zv@ieQ7Yz>|pt~^fc3!#}!kM1MemBPYJUZXcx&@od{KQv%_t~u#XG!;&QResI3d0z7PU{z54NDdkhW$byz(;;ISUv9p>wtJxs$3bsP%r}q;j5oxFpa3poK9{N}P^^eHVmwJILlML-f1)%(E4kx? zI#p^v0W!d>sJK5nKuPOAlcOk0WEK)DEJ=)jG1{da)4}@lMEb*}#_B+cJ2+CKziuHe zc4H|FA|tbt7F+x^#%slD{KPcKtqo8!jFN*f@;CImIPzl%U6ZTyLc%00BUt*z;v2CF z>$lB2L2+{kBMh|^GYj{_B;!-TT^cwYqo~bFkRdWUb0OzLRUBY)nIf)Ww2a zCjU&cHw{p`uxmqs3%AU2I=j*$JX$hiJhnFE0wG~Wr29i014l;e$mR1gRlEZsaW4V#7d}y4x>m$lc$*^zdg7<#d@kaaK8qEgV9qssl3UPv@gCRNBIdu z0+FuX%d0)eMS5zA)yk+Pm`0W!ux8sr{)w$5`^Ksqk-UOJ0?JD5$^*#!KMOpzCQD3CqRWGbzJ1KFxtqLoLyLYg z$x^}rL6}AM5v{x;#`-Znpo0v#6g*6$xohOSk)kdYdn8|r#;`oDx*I)cyQL~H%>O(< zD)8E=;A(_ynSY!9AEd0_+IMcr9 zT+a5`CgB+jMKTKrnmD1FtD}oJW|1hS9E@^;FJD}Xq}rwntf&nYD#8?~k_tC?Be%em zffC)%9b~=hBhd+!$>iE3Bb%@=BFUrLPYN+Awy*o#@#zd%|0*p>c46C6<49D>YsyDACd$_1SszQDBP3#f;D zzrZY(`ID(WR26+GSf8CIa6MK1fTeS(tIsn^N`fI@6$7Nr*&hYiH!2r<>aM6&7pjH4 zt))Ar-IlQ7S(`N`@ez!ll3)i;)oh6~0w{rdgM0E&pjrYdNUSkz(JvFJL>;O}?9w7~#41?4P8+qbsRCdX z7smxNNzI|>!htG$GyWDh30~41H3a88+ zGNCI|FZD5^lL0kYNX2Bj@={N!+$otAY(UEIn4P3(hl?MJ;KpVzxV_&%OUhkytpzo&F#$K^QWh9+-@>7A-t*RKVe5MGBNOt|X&-o0ZG@rd>Tl&Vsf#{<}j3Oi4t+ zI+MCEyU0_SO#~~?P{uZaXfZ;4vhA#hTUtk@a@+Y5plZBivgNHLCBzPVND~9eF;plw z>dCA8We@_{jZCm_rsrmYwamQcl$$u>HC(zXO8g!B~j9yyai$D8Ko{a+XlrOYe$fn?fwo||7BMi+v;BYj@;VZqiD9RMRt5}Z2 zMI+%+{MJg#W@jR>W~;c`W8#_SP=|hHMx2(*^SkA9uM3Ui-bKoATr$@?UTyBQ61Fh* z8^v>cpoS>Ic@G|S!eq8l?hD!(L*vs>x| z<|?A8T`QGnzls?}R zZy$g|%-#IHDh$l@V@HQOU=Gi3`L@q>`y$Bd>3eKzC5L=rqj|$udfA#IU942>&GUq9 zp(wx9d={(D5*J7fr~m1>WQ+3ZCy&2lPGWufT|;N88{OY^TErc7NKYlDa|Qxc%;g!x zP+|}I-={uC51cVG!Yqh$%T5tRD5gki!{)&tTUhdR zDzGP3o>IVY0a9@5Oc5^?z8nhXDngS`!y-JHH7eI4A4@J=*!F2#k63{K-5?Ooo3c)| zA{`irXgI$h7613S`L-%SHi9RQeEgQOUZ0CKZ_G;BGHA!4D~>L0nl#EShfkyCY?L+V z$rxs5hK#oK?VF$Hc7}~LH*b=y9S*NJQ%aw*r1fqTJ}~y{;Une9@ol}hL(F0mU)L=9 zGVtXpTRSJ%xV!A`#KnhK?;S(sooM{XFkP8=`?pjIK@AO2GG+k^I3R%q8h9Xr2d1Eg z96=@cAcPT0I3a}^A@^K`8Q$j}haGzOp>-OHIAVTS0hSVkVr(Z^fBuz(1{(m9I3tZU z+87;(IqJA0k3HIV=nrW)JCYusA^<|iDuE-{KO4@lRo_XrIC!c-#`6r-(3OW#&6^d|C zPh5fcUmkF@ASD$C=J8S*Z?1$T7aSqY&yR8Wq2v>Ca^y-*gf zOX;TT!A!M~kZ6N(q;5|quF;1>I7IDYKG(;J1{k!T-)1!9;ShNyT@⩔L8YGQE6tGAZmj>8NIN982R6^&0 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

    m!as%xi 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 L; z9k==h4vkJu_BRy;+4*sMz>|9pG!euxTT0h^SF+QF*F_iXO_WmgO!WUy1XWReEr&M@ufZZn_3{P&62PA?`8HI(N|xP1!CXAoAMHW z&h~fk)F1H%%82?r`~)?lRW8y~R`LjHzWKLr$#(Br9`Wr4-+ph;tSr0|n(zm_vaFgR zjuX6 zDg2hUmK9%H&J^rdLlOU|b^^6GNZ{6BuG9ViG2+G_UqgnZ@R0vdp~4qw3cfgz@CA<> zBNQfp!pp_N6Hk~>Soow%MUEw@9JH8X;}eo4C8iVzv&ssPF<;)Sg3_f#FFYnV|8JKI=*ODN%rvuDp;^e(Hvr{Ff1w{`>sM&kgm&AZfdSzl_de8Q=SxK zaZrX8uRjqy49=Ada$^sVZmqIaW$slTvldqYxvEntUYI1hS`5jwOCqcacS=c^wY?6x-A)o2&EE#C=4)q;JLcyRxVZ)WZ01$yT!G>%zWbX^!X z<}#)kgKT7!QEs_Ky^H|Dupk`1n;{MR@`6( zT=poSj9_{=4WgZN8$nFV>PUtRD3QbNl-z_P1Ihmp1RWulHzIH$J^*7qQ`M600H!La zB7De|P71J+iDR$`Wsw3(!Ssev6L3Q#zGhKK6AkfWGSDZxfCiGL0Ohe=ci)9KUU}!G zw<{p;o$V)AhzaJ7E-!imn$+S1DHv8f@eEuFzOV$o3()YEh(G_m>JO%Paf6O>8?mLP ziy&pw&2SqDUzg5Z!i zO$dwdD3BKCMjZf*k}nXtD$5NzBLG?Zej@InF&*j93t)U|S=*^MfFw_@SGB}tuCaDt zk6g5*L{t|b5$+U99B@FAR7#zj0zqI2Hx&Q3JxH(YPR*{_okR=t_JXGK?-Hpz3 z-^*0eEt$L5B86?a=e@for1aBuf3as#eh%7oqK)qK=t`9qMEd<0<^`9Jx`vqo#Rrgk z>#JPhV{ia%y+F($lm}K=v`ZoW<2C)?nk4F`5xiZiP`Be&;N3SmwTU$^0U!~O00iI~3KGCi=eq$VUW66H)L}YBLf~Eu zIDnee;T8`x7Y_>nL^NnZKDAPzBEtVuwp@I{0V^Uv%p&CyUwlD@LWCQuEV9FzIK*+q z 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 z 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

    hiV{t!hEhJBh&tRSAj> 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

    wrghae9nYb^I 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%0DJ1Cnl~ZCSfxs=_MwGBsNVbHiJ24T}P?@ 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%0A8a9VP)=!7(vuq3SZ|gsctQ$3>7u&3VcNP8S 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%0D(S-PdZBy#W0N!Klan5JWn_ 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

    XYFEEwSLTdf#6>c{l>e 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 z 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

    sf4;&|BJ&5(eA(!&Au+bc;-hHenJcN4DRHJn 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⋘P?r6cTw9bLGnh~Qqg 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 cXC8$FN0@{|2gqK6QdKH^?J0RYhd1Vuk^$N&HU literal 0 HcmV?d00001 diff --git a/docs/package.json b/docs/package.json new file mode 100644 index 000000000..e8f933887 --- /dev/null +++ b/docs/package.json @@ -0,0 +1,19 @@ +{ + "name": "kimi-code-docs", + "private": true, + "license": "MIT", + "type": "module", + "scripts": { + "dev": "vitepress dev", + "build": "vitepress build", + "preview": "vitepress preview" + }, + "devDependencies": { + "vitepress": "^1.5.0" + }, + "dependencies": { + "mermaid": "^11.12.2", + "vitepress-plugin-llms": "^1.10.0", + "vitepress-plugin-mermaid": "^2.0.17" + } +} diff --git a/docs/pnpm-lock.yaml b/docs/pnpm-lock.yaml new file mode 100644 index 000000000..a6200346d --- /dev/null +++ b/docs/pnpm-lock.yaml @@ -0,0 +1,3179 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + mermaid: + specifier: ^11.12.2 + version: 11.15.0 + vitepress-plugin-llms: + specifier: ^1.10.0 + version: 1.12.2 + vitepress-plugin-mermaid: + specifier: ^2.0.17 + version: 2.0.17(mermaid@11.15.0)(vitepress@1.6.4(@algolia/client-search@5.52.1)(postcss@8.5.15)(search-insights@2.17.3)) + devDependencies: + vitepress: + specifier: ^1.5.0 + version: 1.6.4(@algolia/client-search@5.52.1)(postcss@8.5.15)(search-insights@2.17.3) + +packages: + + '@algolia/abtesting@1.18.1': + resolution: {integrity: sha512-aehCadlWOGvrT91KUIZpC0MbB8KBW9yUuvTJFd2xesR7le/IsT4nJUnjCCZ4ZqZCeTcPHPV5mo//fZ5oxcSVYw==} + engines: {node: '>= 14.0.0'} + + '@algolia/autocomplete-core@1.17.7': + resolution: {integrity: sha512-BjiPOW6ks90UKl7TwMv7oNQMnzU+t/wk9mgIDi6b1tXpUek7MW0lbNOUHpvam9pe3lVCf4xPFT+lK7s+e+fs7Q==} + + '@algolia/autocomplete-plugin-algolia-insights@1.17.7': + resolution: {integrity: sha512-Jca5Ude6yUOuyzjnz57og7Et3aXjbwCSDf/8onLHSQgw1qW3ALl9mrMWaXb5FmPVkV3EtkD2F/+NkT6VHyPu9A==} + peerDependencies: + search-insights: '>= 1 < 3' + + '@algolia/autocomplete-preset-algolia@1.17.7': + resolution: {integrity: sha512-ggOQ950+nwbWROq2MOCIL71RE0DdQZsceqrg32UqnhDz8FlO9rL8ONHNsI2R1MH0tkgVIDKI/D0sMiUchsFdWA==} + peerDependencies: + '@algolia/client-search': '>= 4.9.1 < 6' + algoliasearch: '>= 4.9.1 < 6' + + '@algolia/autocomplete-shared@1.17.7': + resolution: {integrity: sha512-o/1Vurr42U/qskRSuhBH+VKxMvkkUVTLU6WZQr+L5lGZZLYWyhdzWjW0iGXY7EkwRTjBqvN2EsR81yCTGV/kmg==} + peerDependencies: + '@algolia/client-search': '>= 4.9.1 < 6' + algoliasearch: '>= 4.9.1 < 6' + + '@algolia/client-abtesting@5.52.1': + resolution: {integrity: sha512-HmXOGBOAOJPounpBzBpuY0zDYeiCpxgHnQmuA7JO6ScukcBdGp3/XM9zJk5pJx/xNGD68mbPGXWpDxGtl6BwDQ==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-analytics@5.52.1': + resolution: {integrity: sha512-5oo4+I8iixie9vXhCyNFCzeIr8pqA3FQ//VsLHTDvZAV4ttYOPGvYHGQq5NSalrLx5Jc3dRro/5uDOlnUMcBJg==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-common@5.52.1': + resolution: {integrity: sha512-qCDoZfx5MpX7XQzvQ3bC4tSEMkQWQMaF/ABtLuoze03Y/flR563CCSws02qIJ23oX7lxl92LsilZjINVyTdtLw==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-insights@5.52.1': + resolution: {integrity: sha512-hnGs0/lsFJ2PWDxNBz7pxreXo/Xz7gxYRcfePBUjsH26ad0kU/sgnVZd9LwWBpsQv65z2jlb5dkyaB9WE9M9FQ==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-personalization@5.52.1': + resolution: {integrity: sha512-2VxxNc/uBysyKvGeBdSM5n9eIDKH8kWD7wd9/yqbJAiVwU4Yv6tU1LSJusHKrXV/aCu1KW7t9Gug9QyeEmtn/Q==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-query-suggestions@5.52.1': + resolution: {integrity: sha512-O6mPtsw3xEfNOe6gWFpYLeAZAIljNa4Hgna3bq15PwyN7nbjTY0wXJFRbzs/0YVf75Br+SbOQUmjKxXYjDiSiQ==} + engines: {node: '>= 14.0.0'} + + '@algolia/client-search@5.52.1': + resolution: {integrity: sha512-gA8oJOV1LnQQkDf91iebNnFInHuW0gRPEgLSOQ7EfipCEjYTHm5swm1DlH9H5RaRw4RrHuzHBegnlzc0MAstcg==} + engines: {node: '>= 14.0.0'} + + '@algolia/ingestion@1.52.1': + resolution: {integrity: sha512-U9zZfc5xIu9wRxZkt+HceJUAD4VKHKbAyLSloJdEyMRmphXeibfrY9cxqIXBcmPeZzGhn3Imb35Dq8l19PkJhw==} + engines: {node: '>= 14.0.0'} + + '@algolia/monitoring@1.52.1': + resolution: {integrity: sha512-a3SGNceHmkQfq77iG8Ka+w1pvwfZa/0lzEIgse30fL0kD+yKnd/dg0dQvSfFPAEt2f21DMcGkDSSeJlO3KdQjQ==} + engines: {node: '>= 14.0.0'} + + '@algolia/recommend@5.52.1': + resolution: {integrity: sha512-z98QEguCFDpxb4S/PyrUK1igqF8tPsdbqOUUO6ON91vJ58w+Gwa6ncrI0oNXSFcrkxA5EqPKPQ2A1PBCn08TYQ==} + engines: {node: '>= 14.0.0'} + + '@algolia/requester-browser-xhr@5.52.1': + resolution: {integrity: sha512-CI7+/0I11QeZM59Uc8whd2or0kqzFVjpaPn9Qpwll/krHcBAxk24WkAQ6WX+IwDVMfpont4YGbKwAmCre3vE8Q==} + engines: {node: '>= 14.0.0'} + + '@algolia/requester-fetch@5.52.1': + resolution: {integrity: sha512-S6bDuw9byfOvm3T71cgdoZgrgnZq6hpdMLkx52Louh57nUAmvGQESz2aojOynQHjbTiV55smvAFbgn0qT4tJrg==} + engines: {node: '>= 14.0.0'} + + '@algolia/requester-node-http@5.52.1': + resolution: {integrity: sha512-tqZXM+54rWo4mk5jL5Z/flE11nPmNEdXwFBM5py9DkOmbjeCNemfVd45FyM97XdzfZ0dl9uOJC6PYn1FpkeyQg==} + engines: {node: '>= 14.0.0'} + + '@antfu/install-pkg@1.1.0': + resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.3': + resolution: {integrity: sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + + '@braintree/sanitize-url@6.0.4': + resolution: {integrity: sha512-s3jaWicZd0pkP0jf5ysyHUI/RE7MHos6qlToFcGWXVp+ykHOy77OUMrfbgJ9it2C5bow7OIQwYYaHjk9XlBQ2A==} + + '@braintree/sanitize-url@7.1.2': + resolution: {integrity: sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==} + + '@chevrotain/types@11.1.2': + resolution: {integrity: sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==} + + '@docsearch/css@3.8.2': + resolution: {integrity: sha512-y05ayQFyUmCXze79+56v/4HpycYF3uFqB78pLPrSV5ZKAlDuIAAJNhaRi8tTdRNXh05yxX/TyNnzD6LwSM89vQ==} + + '@docsearch/js@3.8.2': + resolution: {integrity: sha512-Q5wY66qHn0SwA7Taa0aDbHiJvaFJLOJyHmooQ7y8hlwwQLQ/5WwCcoX0g7ii04Qi2DJlHsd0XXzJ8Ypw9+9YmQ==} + + '@docsearch/react@3.8.2': + resolution: {integrity: sha512-xCRrJQlTt8N9GU0DG4ptwHRkfnSnD/YpdeaXe02iKfqs97TkZJv60yE+1eq/tjPcVnTW8dP5qLP7itifFVV5eg==} + peerDependencies: + '@types/react': '>= 16.8.0 < 19.0.0' + react: '>= 16.8.0 < 19.0.0' + react-dom: '>= 16.8.0 < 19.0.0' + search-insights: '>= 1 < 3' + peerDependenciesMeta: + '@types/react': + optional: true + react: + optional: true + react-dom: + optional: true + search-insights: + optional: true + + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@iconify-json/simple-icons@1.2.83': + resolution: {integrity: sha512-6Pp9V++XisT9RKH7FB4RLPqUDzcmLtSma0ovOEIoEWGrXtHwBFsH7oN1z8vvCVCb95fb87QgR46/zRLyN9Y3kg==} + + '@iconify/types@2.0.0': + resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} + + '@iconify/utils@3.1.3': + resolution: {integrity: sha512-LPKOXPn/zV+zis1oOfGWogaXVpqUybF3ZS6SCZIsz8vg0ivVp9+fVqyYB7xq0aiST/VhUQYGO1qo6uoYSiEJqw==} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@mermaid-js/mermaid-mindmap@9.3.0': + resolution: {integrity: sha512-IhtYSVBBRYviH1Ehu8gk69pMDF8DSRqXBRDMWrEfHoaMruHeaP2DXA3PBnuwsMaCdPQhlUUcy/7DBLAEIXvCAw==} + + '@mermaid-js/parser@1.1.1': + resolution: {integrity: sha512-VuHdsYMK1bT6X2JbcAaWAhugTRvRBRyuZgd+c22swUeI9g/ntaxF7CY7dYarhZovofCbUNO0G7JesfmNtjYOCw==} + + '@rollup/rollup-android-arm-eabi@4.60.4': + resolution: {integrity: sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.60.4': + resolution: {integrity: sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.60.4': + resolution: {integrity: sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.60.4': + resolution: {integrity: sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.60.4': + resolution: {integrity: sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.60.4': + resolution: {integrity: sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.60.4': + resolution: {integrity: sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.60.4': + resolution: {integrity: sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.60.4': + resolution: {integrity: sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.60.4': + resolution: {integrity: sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.60.4': + resolution: {integrity: sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-loong64-musl@4.60.4': + resolution: {integrity: sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.60.4': + resolution: {integrity: sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-ppc64-musl@4.60.4': + resolution: {integrity: sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.60.4': + resolution: {integrity: sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.60.4': + resolution: {integrity: sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.60.4': + resolution: {integrity: sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.60.4': + resolution: {integrity: sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.60.4': + resolution: {integrity: sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openbsd-x64@4.60.4': + resolution: {integrity: sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.60.4': + resolution: {integrity: sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.60.4': + resolution: {integrity: sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.60.4': + resolution: {integrity: sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.60.4': + resolution: {integrity: sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.60.4': + resolution: {integrity: sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==} + cpu: [x64] + os: [win32] + + '@shikijs/core@2.5.0': + resolution: {integrity: sha512-uu/8RExTKtavlpH7XqnVYBrfBkUc20ngXiX9NSrBhOVZYv/7XQRKUyhtkeflY5QsxC0GbJThCerruZfsUaSldg==} + + '@shikijs/engine-javascript@2.5.0': + resolution: {integrity: sha512-VjnOpnQf8WuCEZtNUdjjwGUbtAVKuZkVQ/5cHy/tojVVRIRtlWMYVjyWhxOmIq05AlSOv72z7hRNRGVBgQOl0w==} + + '@shikijs/engine-oniguruma@2.5.0': + resolution: {integrity: sha512-pGd1wRATzbo/uatrCIILlAdFVKdxImWJGQ5rFiB5VZi2ve5xj3Ax9jny8QvkaV93btQEwR/rSz5ERFpC5mKNIw==} + + '@shikijs/langs@2.5.0': + resolution: {integrity: sha512-Qfrrt5OsNH5R+5tJ/3uYBBZv3SuGmnRPejV9IlIbFH3HTGLDlkqgHymAlzklVmKBjAaVmkPkyikAV/sQ1wSL+w==} + + '@shikijs/themes@2.5.0': + resolution: {integrity: sha512-wGrk+R8tJnO0VMzmUExHR+QdSaPUl/NKs+a4cQQRWyoc3YFbUzuLEi/KWK1hj+8BfHRKm2jNhhJck1dfstJpiw==} + + '@shikijs/transformers@2.5.0': + resolution: {integrity: sha512-SI494W5X60CaUwgi8u4q4m4s3YAFSxln3tzNjOSYqq54wlVgz0/NbbXEb3mdLbqMBztcmS7bVTaEd2w0qMmfeg==} + + '@shikijs/types@2.5.0': + resolution: {integrity: sha512-ygl5yhxki9ZLNuNpPitBWvcy9fsSKKaRuO4BAlMyagszQidxcpLAr0qiW/q43DtSIDxO6hEbtYLiFZNXO/hdGw==} + + '@shikijs/vscode-textmate@10.0.2': + resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} + + '@types/d3-array@3.2.2': + resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} + + '@types/d3-axis@3.0.6': + resolution: {integrity: sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==} + + '@types/d3-brush@3.0.6': + resolution: {integrity: sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==} + + '@types/d3-chord@3.0.6': + resolution: {integrity: sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==} + + '@types/d3-color@3.1.3': + resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} + + '@types/d3-contour@3.0.6': + resolution: {integrity: sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==} + + '@types/d3-delaunay@6.0.4': + resolution: {integrity: sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==} + + '@types/d3-dispatch@3.0.7': + resolution: {integrity: sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==} + + '@types/d3-drag@3.0.7': + resolution: {integrity: sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==} + + '@types/d3-dsv@3.0.7': + resolution: {integrity: sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==} + + '@types/d3-ease@3.0.2': + resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} + + '@types/d3-fetch@3.0.7': + resolution: {integrity: sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==} + + '@types/d3-force@3.0.10': + resolution: {integrity: sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==} + + '@types/d3-format@3.0.4': + resolution: {integrity: sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==} + + '@types/d3-geo@3.1.0': + resolution: {integrity: sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==} + + '@types/d3-hierarchy@3.1.7': + resolution: {integrity: sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==} + + '@types/d3-interpolate@3.0.4': + resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} + + '@types/d3-path@3.1.1': + resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==} + + '@types/d3-polygon@3.0.2': + resolution: {integrity: sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==} + + '@types/d3-quadtree@3.0.6': + resolution: {integrity: sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==} + + '@types/d3-random@3.0.3': + resolution: {integrity: sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==} + + '@types/d3-scale-chromatic@3.1.0': + resolution: {integrity: sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==} + + '@types/d3-scale@4.0.9': + resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==} + + '@types/d3-selection@3.0.11': + resolution: {integrity: sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==} + + '@types/d3-shape@3.1.8': + resolution: {integrity: sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==} + + '@types/d3-time-format@4.0.3': + resolution: {integrity: sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==} + + '@types/d3-time@3.0.4': + resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==} + + '@types/d3-timer@3.0.2': + resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} + + '@types/d3-transition@3.0.9': + resolution: {integrity: sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==} + + '@types/d3-zoom@3.0.8': + resolution: {integrity: sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==} + + '@types/d3@7.4.3': + resolution: {integrity: sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==} + + '@types/debug@4.1.13': + resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/geojson@7946.0.16': + resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} + + '@types/hast@3.0.4': + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + + '@types/linkify-it@5.0.0': + resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} + + '@types/markdown-it@14.1.2': + resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==} + + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/mdurl@2.0.0': + resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + + '@types/web-bluetooth@0.0.21': + resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==} + + '@ungap/structured-clone@1.3.1': + resolution: {integrity: sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==} + + '@upsetjs/venn.js@2.0.0': + resolution: {integrity: sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==} + + '@vitejs/plugin-vue@5.2.4': + resolution: {integrity: sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==} + engines: {node: ^18.0.0 || >=20.0.0} + peerDependencies: + vite: ^5.0.0 || ^6.0.0 + vue: ^3.2.25 + + '@vue/compiler-core@3.5.34': + resolution: {integrity: sha512-s9cLyK5mLcvZ4Agva5QgRsQyLKvts9WbU9DB6NqiZkkGEdwmcEiylj5Jbwkp680drF/NNCV8OlAJSe+yMLxaJw==} + + '@vue/compiler-dom@3.5.34': + resolution: {integrity: sha512-EbF/T++k0e2MMZlJsBhzK8Sgwt0HcIPOhzn1CTB/lv6sQcyk+OWf8YeiLxZp3ro7MbbLcAfAJ6sEvjFWuNgUCw==} + + '@vue/compiler-sfc@3.5.34': + resolution: {integrity: sha512-D/ihr6uZeIt6r+pVZf46RWT1fAsLFMbUP7k8G1VkiiWexriED9GrX3echHd4Abbt17zjlfiFJ8z7a3BxZOPNjg==} + + '@vue/compiler-ssr@3.5.34': + resolution: {integrity: sha512-cDtTHKibkThKGHH1SP+WdccquNRYQDFH6rRjQCqT9G2ltFAfoR5pUftpab/z+aM5mW9HLLVQW7hfKKQe/1GBeQ==} + + '@vue/devtools-api@7.7.9': + resolution: {integrity: sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==} + + '@vue/devtools-kit@7.7.9': + resolution: {integrity: sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==} + + '@vue/devtools-shared@7.7.9': + resolution: {integrity: sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==} + + '@vue/reactivity@3.5.34': + resolution: {integrity: sha512-y9XDjCEuBp+98k+UL5dbYkh57AHU4o6cxZedOPXw3bmrZZYLQsVHguGurq7hVrPCSrQtrnz1f9dssyFr+dMXfQ==} + + '@vue/runtime-core@3.5.34': + resolution: {integrity: sha512-mKeBYvu8tcMSLhypAHBmriUFfWXKTCF/23Z4jiCoYK3UtWepkliViNLuR90V9XOyD62mUxs9p1jsrpK3CCGIzw==} + + '@vue/runtime-dom@3.5.34': + resolution: {integrity: sha512-e8kZzERmCwUnBRVsgSQlAfrfU2rGoy0FFKPBXSlfEjc/O3KfA7QP0t1/2ZylrbchjmIKB4dPTd07A6WPr0eOrg==} + + '@vue/server-renderer@3.5.34': + resolution: {integrity: sha512-nHxmJoTrKsmrkbILRhkC9gY1G3moZbJTqCzDd7DOOzG5KH9oeJ0Unqrff5f9v0pW//jES05ZkJcNtfE8JjOIew==} + peerDependencies: + vue: 3.5.34 + + '@vue/shared@3.5.34': + resolution: {integrity: sha512-24uqU4OIiX29ryC3MeWid/Xf2fa2EFRUVLb77nRhk+UrTVrh/XiGtFAFmJBAtBRbjwNdsPRP+jj/OL27Eg1NDA==} + + '@vueuse/core@12.8.2': + resolution: {integrity: sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ==} + + '@vueuse/integrations@12.8.2': + resolution: {integrity: sha512-fbGYivgK5uBTRt7p5F3zy6VrETlV9RtZjBqd1/HxGdjdckBgBM4ugP8LHpjolqTj14TXTxSK1ZfgPbHYyGuH7g==} + peerDependencies: + async-validator: ^4 + axios: ^1 + change-case: ^5 + drauu: ^0.4 + focus-trap: ^7 + fuse.js: ^7 + idb-keyval: ^6 + jwt-decode: ^4 + nprogress: ^0.2 + qrcode: ^1.5 + sortablejs: ^1 + universal-cookie: ^7 + peerDependenciesMeta: + async-validator: + optional: true + axios: + optional: true + change-case: + optional: true + drauu: + optional: true + focus-trap: + optional: true + fuse.js: + optional: true + idb-keyval: + optional: true + jwt-decode: + optional: true + nprogress: + optional: true + qrcode: + optional: true + sortablejs: + optional: true + universal-cookie: + optional: true + + '@vueuse/metadata@12.8.2': + resolution: {integrity: sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A==} + + '@vueuse/shared@12.8.2': + resolution: {integrity: sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w==} + + algoliasearch@5.52.1: + resolution: {integrity: sha512-fHA8+kXTbjagw3jkLiaS7KKrH8qe2DyOsiUhGlN4cdT77PEsfqXZl7ewDk1hsg+pJnPlnE50XtLxjR91iJOpmg==} + engines: {node: '>= 14.0.0'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + bail@2.0.2: + resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + birpc@2.9.0: + resolution: {integrity: sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==} + + brace-expansion@5.0.6: + resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} + engines: {node: 18 || 20 || >=22} + + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + + character-entities@2.0.2: + resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + + commander@7.2.0: + resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} + engines: {node: '>= 10'} + + commander@8.3.0: + resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} + engines: {node: '>= 12'} + + copy-anything@4.0.5: + resolution: {integrity: sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==} + engines: {node: '>=18'} + + cose-base@1.0.3: + resolution: {integrity: sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==} + + cose-base@2.2.0: + resolution: {integrity: sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + cytoscape-cose-bilkent@4.1.0: + resolution: {integrity: sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==} + peerDependencies: + cytoscape: ^3.2.0 + + cytoscape-fcose@2.2.0: + resolution: {integrity: sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==} + peerDependencies: + cytoscape: ^3.2.0 + + cytoscape@3.33.4: + resolution: {integrity: sha512-HIN5Pmd9MrX9BkV7tDwnOcEJCSFvCpc8X97h3f508J6I5FsqAY65wKOCvgH2CuP42CaahWaz4tuh32SOOIH7ww==} + engines: {node: '>=0.10'} + + d3-array@2.12.1: + resolution: {integrity: sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==} + + d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + + d3-axis@3.0.0: + resolution: {integrity: sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==} + engines: {node: '>=12'} + + d3-brush@3.0.0: + resolution: {integrity: sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==} + engines: {node: '>=12'} + + d3-chord@3.0.1: + resolution: {integrity: sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==} + engines: {node: '>=12'} + + d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + + d3-contour@4.0.2: + resolution: {integrity: sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==} + engines: {node: '>=12'} + + d3-delaunay@6.0.4: + resolution: {integrity: sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==} + engines: {node: '>=12'} + + d3-dispatch@3.0.1: + resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==} + engines: {node: '>=12'} + + d3-drag@3.0.0: + resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==} + engines: {node: '>=12'} + + d3-dsv@3.0.1: + resolution: {integrity: sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==} + engines: {node: '>=12'} + hasBin: true + + d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + + d3-fetch@3.0.1: + resolution: {integrity: sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==} + engines: {node: '>=12'} + + d3-force@3.0.0: + resolution: {integrity: sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==} + engines: {node: '>=12'} + + d3-format@3.1.2: + resolution: {integrity: sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==} + engines: {node: '>=12'} + + d3-geo@3.1.1: + resolution: {integrity: sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==} + engines: {node: '>=12'} + + d3-hierarchy@3.1.2: + resolution: {integrity: sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==} + engines: {node: '>=12'} + + d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + + d3-path@1.0.9: + resolution: {integrity: sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==} + + d3-path@3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + + d3-polygon@3.0.1: + resolution: {integrity: sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==} + engines: {node: '>=12'} + + d3-quadtree@3.0.1: + resolution: {integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==} + engines: {node: '>=12'} + + d3-random@3.0.1: + resolution: {integrity: sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==} + engines: {node: '>=12'} + + d3-sankey@0.12.3: + resolution: {integrity: sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==} + + d3-scale-chromatic@3.1.0: + resolution: {integrity: sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==} + engines: {node: '>=12'} + + d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + + d3-selection@3.0.0: + resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==} + engines: {node: '>=12'} + + d3-shape@1.3.7: + resolution: {integrity: sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==} + + d3-shape@3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + + d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + + d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + + d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + + d3-transition@3.0.1: + resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==} + engines: {node: '>=12'} + peerDependencies: + d3-selection: 2 - 3 + + d3-zoom@3.0.0: + resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==} + engines: {node: '>=12'} + + d3@7.9.0: + resolution: {integrity: sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==} + engines: {node: '>=12'} + + dagre-d3-es@7.0.14: + resolution: {integrity: sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg==} + + dayjs@1.11.20: + resolution: {integrity: sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decode-named-character-reference@1.3.0: + resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==} + + delaunator@5.1.0: + resolution: {integrity: sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + + dompurify@3.4.5: + resolution: {integrity: sha512-OrwIBKsdNSVEeubdJ1HBv/wNENRM9ytAVCv7YXt//A3vPdVMNuACRqK9mXCGCBW2ln7BT/A4X0jXHo2Gu89miA==} + + emoji-regex-xs@1.0.0: + resolution: {integrity: sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + entities@7.0.1: + resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} + engines: {node: '>=0.12'} + + es-toolkit@1.46.1: + resolution: {integrity: sha512-5eNtXOs3tbfxXOj04tjjseeWkRWaoCjdEI+96DgwzZoe6c9juL49pXlzAFTI72aWC9Y8p7168g6XIKjh7k6pyQ==} + + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + extend-shallow@2.0.1: + resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} + engines: {node: '>=0.10.0'} + + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + + fault@2.0.1: + resolution: {integrity: sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==} + + focus-trap@7.8.0: + resolution: {integrity: sha512-/yNdlIkpWbM0ptxno3ONTuf+2g318kh2ez3KSeZN5dZ8YC6AAmgeWz+GasYYiBJPFaYcSAPeu4GfhUaChzIJXA==} + + format@0.2.2: + resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==} + engines: {node: '>=0.4.x'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + gray-matter@4.0.3: + resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==} + engines: {node: '>=6.0'} + + hachure-fill@0.5.2: + resolution: {integrity: sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==} + + hast-util-to-html@9.0.5: + resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + + hookable@5.5.3: + resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} + + html-void-elements@3.0.0: + resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + import-meta-resolve@4.2.0: + resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==} + + internmap@1.0.1: + resolution: {integrity: sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==} + + internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + + is-extendable@0.1.1: + resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + + is-what@5.5.0: + resolution: {integrity: sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==} + engines: {node: '>=18'} + + js-yaml@3.14.2: + resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} + hasBin: true + + katex@0.16.47: + resolution: {integrity: sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==} + hasBin: true + + khroma@2.1.0: + resolution: {integrity: sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==} + + kind-of@6.0.3: + resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} + engines: {node: '>=0.10.0'} + + layout-base@1.0.2: + resolution: {integrity: sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==} + + layout-base@2.0.1: + resolution: {integrity: sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==} + + linkify-it@5.0.0: + resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} + + lodash-es@4.18.1: + resolution: {integrity: sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==} + + longest-streak@3.1.0: + resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + mark.js@8.11.1: + resolution: {integrity: sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==} + + markdown-it@14.1.1: + resolution: {integrity: sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==} + hasBin: true + + markdown-title@1.0.2: + resolution: {integrity: sha512-MqIQVVkz+uGEHi3TsHx/czcxxCbRIL7sv5K5DnYw/tI+apY54IbPefV/cmgxp6LoJSEx/TqcHdLs/298afG5QQ==} + engines: {node: '>=6'} + + marked@16.4.2: + resolution: {integrity: sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==} + engines: {node: '>= 20'} + hasBin: true + + mdast-util-from-markdown@2.0.3: + resolution: {integrity: sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==} + + mdast-util-frontmatter@2.0.1: + resolution: {integrity: sha512-LRqI9+wdgC25P0URIJY9vwocIzCcksduHQ9OF2joxQoyTNVduwLAFUzjoopuRJbJAReaKrNQKAZKL3uCMugWJA==} + + mdast-util-phrasing@4.1.0: + resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} + + mdast-util-to-hast@13.2.1: + resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} + + mdast-util-to-markdown@2.1.2: + resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} + + mdast-util-to-string@4.0.0: + resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + + mdurl@2.0.0: + resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} + + mermaid@11.15.0: + resolution: {integrity: sha512-pTMbcf3rWdtLiYGpmoTjHEpeY8seiy6sR+9nD7LOs8KfUbHE4lOUAprTRqRAcWSQ6MQpdX+YEsxShtGsINtPtw==} + + micromark-core-commonmark@2.0.3: + resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} + + micromark-extension-frontmatter@2.0.0: + resolution: {integrity: sha512-C4AkuM3dA58cgZha7zVnuVxBhDsbttIMiytjgsM2XbHAB2faRVaHRle40558FBN+DJcrLNCoqG5mlrpdU4cRtg==} + + micromark-factory-destination@2.0.1: + resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} + + micromark-factory-label@2.0.1: + resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==} + + micromark-factory-space@2.0.1: + resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==} + + micromark-factory-title@2.0.1: + resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==} + + micromark-factory-whitespace@2.0.1: + resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-chunked@2.0.1: + resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==} + + micromark-util-classify-character@2.0.1: + resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==} + + micromark-util-combine-extensions@2.0.1: + resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==} + + micromark-util-decode-numeric-character-reference@2.0.2: + resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==} + + micromark-util-decode-string@2.0.1: + resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-html-tag-name@2.0.1: + resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==} + + micromark-util-normalize-identifier@2.0.1: + resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==} + + micromark-util-resolve-all@2.0.1: + resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-subtokenize@2.1.0: + resolution: {integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.2: + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + + micromark@4.0.2: + resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} + + millify@6.1.0: + resolution: {integrity: sha512-H/E3J6t+DQs/F2YgfDhxUVZz/dF8JXPPKTLHL/yHCcLZLtCXJDUaqvhJXQwqOVBvbyNn4T0WjLpIHd7PAw7fBA==} + hasBin: true + + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + + minisearch@7.2.0: + resolution: {integrity: sha512-dqT2XBYUOZOiC5t2HRnwADjhNS2cecp9u+TJRiJ1Qp/f5qjkeT5APcGPjHw+bz89Ms8Jp+cG4AlE+QZ/QnDglg==} + + mitt@3.0.1: + resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.12: + resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + non-layered-tidy-tree-layout@2.0.2: + resolution: {integrity: sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw==} + + oniguruma-to-es@3.1.1: + resolution: {integrity: sha512-bUH8SDvPkH3ho3dvwJwfonjlQ4R80vjyvrU8YpxuROddv55vAEJrTuCuCVUhhsHbtlD9tGGbaNApGQckXhS8iQ==} + + package-manager-detector@1.6.0: + resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==} + + path-data-parser@0.1.0: + resolution: {integrity: sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==} + + path-to-regexp@6.3.0: + resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + + perfect-debounce@1.0.0: + resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + points-on-curve@0.2.0: + resolution: {integrity: sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==} + + points-on-path@0.2.1: + resolution: {integrity: sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==} + + postcss@8.5.15: + resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} + engines: {node: ^10 || ^12 || >=14} + + preact@10.29.2: + resolution: {integrity: sha512-7tNmwg/7mzzAoB/8kSg6Hl37JraAZw3Z3A0JSY7VXlZwo82Xn0G7wKbNNs2qoF4ZEEsQGTwDAroNdqKs1ofJxQ==} + + pretty-bytes@7.1.0: + resolution: {integrity: sha512-nODzvTiYVRGRqAOvE84Vk5JDPyyxsVk0/fbA/bq7RqlnhksGpset09XTxbpvLTIjoaF7K8Z8DG8yHtKGTPSYRw==} + engines: {node: '>=20'} + + property-information@7.1.0: + resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} + + punycode.js@2.3.1: + resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} + engines: {node: '>=6'} + + regex-recursion@6.0.2: + resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} + + regex-utilities@2.3.0: + resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==} + + regex@6.1.0: + resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==} + + remark-frontmatter@5.0.0: + resolution: {integrity: sha512-XTFYvNASMe5iPN0719nPrdItC9aU0ssC4v14mH1BCi1u0n1gAocqcujWUrByftZTbLhRtiKRyjYTSIOcr69UVQ==} + + remark-parse@11.0.0: + resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} + + remark-stringify@11.0.0: + resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} + + remark@15.0.1: + resolution: {integrity: sha512-Eht5w30ruCXgFmxVUSlNWQ9iiimq07URKeFS3hNc8cUWy1llX4KDWfyEDZRycMc+znsN9Ux5/tJ/BFdgdOwA3A==} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + + robust-predicates@3.0.3: + resolution: {integrity: sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==} + + rollup@4.60.4: + resolution: {integrity: sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + roughjs@4.6.6: + resolution: {integrity: sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==} + + rw@1.3.3: + resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + search-insights@2.17.3: + resolution: {integrity: sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ==} + + section-matter@1.0.0: + resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==} + engines: {node: '>=4'} + + shiki@2.5.0: + resolution: {integrity: sha512-mI//trrsaiCIPsja5CNfsyNOqgAZUb6VpJA+340toL42UpzQlXpwRV9nch69X6gaUxrr9kaOOa6e3y3uAkGFxQ==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + + speakingurl@14.0.1: + resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==} + engines: {node: '>=0.10.0'} + + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-bom-string@1.0.0: + resolution: {integrity: sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==} + engines: {node: '>=0.10.0'} + + stylis@4.4.0: + resolution: {integrity: sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==} + + superjson@2.2.6: + resolution: {integrity: sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==} + engines: {node: '>=16'} + + tabbable@6.4.0: + resolution: {integrity: sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==} + + tinyexec@1.1.2: + resolution: {integrity: sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==} + engines: {node: '>=18'} + + tokenx@1.3.0: + resolution: {integrity: sha512-NLdXTEZkKiO0gZuLtMoZKjCXTREXeZZt8nnnNeyoXtNZAfG/GKGSbQtLU5STspc0rMSwcA+UJfWZkbNU01iKmQ==} + + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + + trough@2.2.0: + resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + + ts-dedent@2.2.0: + resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==} + engines: {node: '>=6.10'} + + uc.micro@2.1.0: + resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} + + unified@11.0.5: + resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} + + unist-util-is@6.0.1: + resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-remove@4.0.0: + resolution: {integrity: sha512-b4gokeGId57UVRX/eVKej5gXqGlc9+trkORhFJpu9raqZkZhU0zm8Doi05+HaiBsMEIJowL+2WtQ5ItjsngPXg==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-parents@6.0.2: + resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==} + + unist-util-visit@5.1.0: + resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} + + uuid@14.0.0: + resolution: {integrity: sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==} + hasBin: true + + vfile-message@4.0.3: + resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + + vite@5.4.21: + resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + vitepress-plugin-llms@1.12.2: + resolution: {integrity: sha512-hdklo7di6E2/MwlYstH7R5QgdPHX+f5G/fp8QBq6MCiw4DWi3IOKMaous0S839FIGsPjK3QHwK6KcFwCf/XzjA==} + engines: {node: '>=18.0.0'} + + vitepress-plugin-mermaid@2.0.17: + resolution: {integrity: sha512-IUzYpwf61GC6k0XzfmAmNrLvMi9TRrVRMsUyCA8KNXhg/mQ1VqWnO0/tBVPiX5UoKF1mDUwqn5QV4qAJl6JnUg==} + peerDependencies: + mermaid: 10 || 11 + vitepress: ^1.0.0 || ^1.0.0-alpha + + vitepress@1.6.4: + resolution: {integrity: sha512-+2ym1/+0VVrbhNyRoFFesVvBvHAVMZMK0rw60E3X/5349M1GuVdKeazuksqopEdvkKwKGs21Q729jX81/bkBJg==} + hasBin: true + peerDependencies: + markdown-it-mathjax3: ^4 + postcss: ^8 + peerDependenciesMeta: + markdown-it-mathjax3: + optional: true + postcss: + optional: true + + vue@3.5.34: + resolution: {integrity: sha512-WdLBG9gm02OgJIG9axd5Hpx0TFLdzVgfG2evFFu8Rur5O/IoGc5cMjnjh3tPL6GnRGsYvUhBSKVPYVcxRKpMCA==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + +snapshots: + + '@algolia/abtesting@1.18.1': + dependencies: + '@algolia/client-common': 5.52.1 + '@algolia/requester-browser-xhr': 5.52.1 + '@algolia/requester-fetch': 5.52.1 + '@algolia/requester-node-http': 5.52.1 + + '@algolia/autocomplete-core@1.17.7(@algolia/client-search@5.52.1)(algoliasearch@5.52.1)(search-insights@2.17.3)': + dependencies: + '@algolia/autocomplete-plugin-algolia-insights': 1.17.7(@algolia/client-search@5.52.1)(algoliasearch@5.52.1)(search-insights@2.17.3) + '@algolia/autocomplete-shared': 1.17.7(@algolia/client-search@5.52.1)(algoliasearch@5.52.1) + transitivePeerDependencies: + - '@algolia/client-search' + - algoliasearch + - search-insights + + '@algolia/autocomplete-plugin-algolia-insights@1.17.7(@algolia/client-search@5.52.1)(algoliasearch@5.52.1)(search-insights@2.17.3)': + dependencies: + '@algolia/autocomplete-shared': 1.17.7(@algolia/client-search@5.52.1)(algoliasearch@5.52.1) + search-insights: 2.17.3 + transitivePeerDependencies: + - '@algolia/client-search' + - algoliasearch + + '@algolia/autocomplete-preset-algolia@1.17.7(@algolia/client-search@5.52.1)(algoliasearch@5.52.1)': + dependencies: + '@algolia/autocomplete-shared': 1.17.7(@algolia/client-search@5.52.1)(algoliasearch@5.52.1) + '@algolia/client-search': 5.52.1 + algoliasearch: 5.52.1 + + '@algolia/autocomplete-shared@1.17.7(@algolia/client-search@5.52.1)(algoliasearch@5.52.1)': + dependencies: + '@algolia/client-search': 5.52.1 + algoliasearch: 5.52.1 + + '@algolia/client-abtesting@5.52.1': + dependencies: + '@algolia/client-common': 5.52.1 + '@algolia/requester-browser-xhr': 5.52.1 + '@algolia/requester-fetch': 5.52.1 + '@algolia/requester-node-http': 5.52.1 + + '@algolia/client-analytics@5.52.1': + dependencies: + '@algolia/client-common': 5.52.1 + '@algolia/requester-browser-xhr': 5.52.1 + '@algolia/requester-fetch': 5.52.1 + '@algolia/requester-node-http': 5.52.1 + + '@algolia/client-common@5.52.1': {} + + '@algolia/client-insights@5.52.1': + dependencies: + '@algolia/client-common': 5.52.1 + '@algolia/requester-browser-xhr': 5.52.1 + '@algolia/requester-fetch': 5.52.1 + '@algolia/requester-node-http': 5.52.1 + + '@algolia/client-personalization@5.52.1': + dependencies: + '@algolia/client-common': 5.52.1 + '@algolia/requester-browser-xhr': 5.52.1 + '@algolia/requester-fetch': 5.52.1 + '@algolia/requester-node-http': 5.52.1 + + '@algolia/client-query-suggestions@5.52.1': + dependencies: + '@algolia/client-common': 5.52.1 + '@algolia/requester-browser-xhr': 5.52.1 + '@algolia/requester-fetch': 5.52.1 + '@algolia/requester-node-http': 5.52.1 + + '@algolia/client-search@5.52.1': + dependencies: + '@algolia/client-common': 5.52.1 + '@algolia/requester-browser-xhr': 5.52.1 + '@algolia/requester-fetch': 5.52.1 + '@algolia/requester-node-http': 5.52.1 + + '@algolia/ingestion@1.52.1': + dependencies: + '@algolia/client-common': 5.52.1 + '@algolia/requester-browser-xhr': 5.52.1 + '@algolia/requester-fetch': 5.52.1 + '@algolia/requester-node-http': 5.52.1 + + '@algolia/monitoring@1.52.1': + dependencies: + '@algolia/client-common': 5.52.1 + '@algolia/requester-browser-xhr': 5.52.1 + '@algolia/requester-fetch': 5.52.1 + '@algolia/requester-node-http': 5.52.1 + + '@algolia/recommend@5.52.1': + dependencies: + '@algolia/client-common': 5.52.1 + '@algolia/requester-browser-xhr': 5.52.1 + '@algolia/requester-fetch': 5.52.1 + '@algolia/requester-node-http': 5.52.1 + + '@algolia/requester-browser-xhr@5.52.1': + dependencies: + '@algolia/client-common': 5.52.1 + + '@algolia/requester-fetch@5.52.1': + dependencies: + '@algolia/client-common': 5.52.1 + + '@algolia/requester-node-http@5.52.1': + dependencies: + '@algolia/client-common': 5.52.1 + + '@antfu/install-pkg@1.1.0': + dependencies: + package-manager-detector: 1.6.0 + tinyexec: 1.1.2 + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/parser@7.29.3': + dependencies: + '@babel/types': 7.29.0 + + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@braintree/sanitize-url@6.0.4': + optional: true + + '@braintree/sanitize-url@7.1.2': {} + + '@chevrotain/types@11.1.2': {} + + '@docsearch/css@3.8.2': {} + + '@docsearch/js@3.8.2(@algolia/client-search@5.52.1)(search-insights@2.17.3)': + dependencies: + '@docsearch/react': 3.8.2(@algolia/client-search@5.52.1)(search-insights@2.17.3) + preact: 10.29.2 + transitivePeerDependencies: + - '@algolia/client-search' + - '@types/react' + - react + - react-dom + - search-insights + + '@docsearch/react@3.8.2(@algolia/client-search@5.52.1)(search-insights@2.17.3)': + dependencies: + '@algolia/autocomplete-core': 1.17.7(@algolia/client-search@5.52.1)(algoliasearch@5.52.1)(search-insights@2.17.3) + '@algolia/autocomplete-preset-algolia': 1.17.7(@algolia/client-search@5.52.1)(algoliasearch@5.52.1) + '@docsearch/css': 3.8.2 + algoliasearch: 5.52.1 + optionalDependencies: + search-insights: 2.17.3 + transitivePeerDependencies: + - '@algolia/client-search' + + '@esbuild/aix-ppc64@0.21.5': + optional: true + + '@esbuild/android-arm64@0.21.5': + optional: true + + '@esbuild/android-arm@0.21.5': + optional: true + + '@esbuild/android-x64@0.21.5': + optional: true + + '@esbuild/darwin-arm64@0.21.5': + optional: true + + '@esbuild/darwin-x64@0.21.5': + optional: true + + '@esbuild/freebsd-arm64@0.21.5': + optional: true + + '@esbuild/freebsd-x64@0.21.5': + optional: true + + '@esbuild/linux-arm64@0.21.5': + optional: true + + '@esbuild/linux-arm@0.21.5': + optional: true + + '@esbuild/linux-ia32@0.21.5': + optional: true + + '@esbuild/linux-loong64@0.21.5': + optional: true + + '@esbuild/linux-mips64el@0.21.5': + optional: true + + '@esbuild/linux-ppc64@0.21.5': + optional: true + + '@esbuild/linux-riscv64@0.21.5': + optional: true + + '@esbuild/linux-s390x@0.21.5': + optional: true + + '@esbuild/linux-x64@0.21.5': + optional: true + + '@esbuild/netbsd-x64@0.21.5': + optional: true + + '@esbuild/openbsd-x64@0.21.5': + optional: true + + '@esbuild/sunos-x64@0.21.5': + optional: true + + '@esbuild/win32-arm64@0.21.5': + optional: true + + '@esbuild/win32-ia32@0.21.5': + optional: true + + '@esbuild/win32-x64@0.21.5': + optional: true + + '@iconify-json/simple-icons@1.2.83': + dependencies: + '@iconify/types': 2.0.0 + + '@iconify/types@2.0.0': {} + + '@iconify/utils@3.1.3': + dependencies: + '@antfu/install-pkg': 1.1.0 + '@iconify/types': 2.0.0 + import-meta-resolve: 4.2.0 + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@mermaid-js/mermaid-mindmap@9.3.0': + dependencies: + '@braintree/sanitize-url': 6.0.4 + cytoscape: 3.33.4 + cytoscape-cose-bilkent: 4.1.0(cytoscape@3.33.4) + cytoscape-fcose: 2.2.0(cytoscape@3.33.4) + d3: 7.9.0 + khroma: 2.1.0 + non-layered-tidy-tree-layout: 2.0.2 + optional: true + + '@mermaid-js/parser@1.1.1': + dependencies: + '@chevrotain/types': 11.1.2 + + '@rollup/rollup-android-arm-eabi@4.60.4': + optional: true + + '@rollup/rollup-android-arm64@4.60.4': + optional: true + + '@rollup/rollup-darwin-arm64@4.60.4': + optional: true + + '@rollup/rollup-darwin-x64@4.60.4': + optional: true + + '@rollup/rollup-freebsd-arm64@4.60.4': + optional: true + + '@rollup/rollup-freebsd-x64@4.60.4': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.60.4': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.60.4': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.60.4': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.60.4': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.60.4': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.60.4': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-x64-musl@4.60.4': + optional: true + + '@rollup/rollup-openbsd-x64@4.60.4': + optional: true + + '@rollup/rollup-openharmony-arm64@4.60.4': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.60.4': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.60.4': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.60.4': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.60.4': + optional: true + + '@shikijs/core@2.5.0': + dependencies: + '@shikijs/engine-javascript': 2.5.0 + '@shikijs/engine-oniguruma': 2.5.0 + '@shikijs/types': 2.5.0 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + hast-util-to-html: 9.0.5 + + '@shikijs/engine-javascript@2.5.0': + dependencies: + '@shikijs/types': 2.5.0 + '@shikijs/vscode-textmate': 10.0.2 + oniguruma-to-es: 3.1.1 + + '@shikijs/engine-oniguruma@2.5.0': + dependencies: + '@shikijs/types': 2.5.0 + '@shikijs/vscode-textmate': 10.0.2 + + '@shikijs/langs@2.5.0': + dependencies: + '@shikijs/types': 2.5.0 + + '@shikijs/themes@2.5.0': + dependencies: + '@shikijs/types': 2.5.0 + + '@shikijs/transformers@2.5.0': + dependencies: + '@shikijs/core': 2.5.0 + '@shikijs/types': 2.5.0 + + '@shikijs/types@2.5.0': + dependencies: + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + + '@shikijs/vscode-textmate@10.0.2': {} + + '@types/d3-array@3.2.2': {} + + '@types/d3-axis@3.0.6': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-brush@3.0.6': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-chord@3.0.6': {} + + '@types/d3-color@3.1.3': {} + + '@types/d3-contour@3.0.6': + dependencies: + '@types/d3-array': 3.2.2 + '@types/geojson': 7946.0.16 + + '@types/d3-delaunay@6.0.4': {} + + '@types/d3-dispatch@3.0.7': {} + + '@types/d3-drag@3.0.7': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-dsv@3.0.7': {} + + '@types/d3-ease@3.0.2': {} + + '@types/d3-fetch@3.0.7': + dependencies: + '@types/d3-dsv': 3.0.7 + + '@types/d3-force@3.0.10': {} + + '@types/d3-format@3.0.4': {} + + '@types/d3-geo@3.1.0': + dependencies: + '@types/geojson': 7946.0.16 + + '@types/d3-hierarchy@3.1.7': {} + + '@types/d3-interpolate@3.0.4': + dependencies: + '@types/d3-color': 3.1.3 + + '@types/d3-path@3.1.1': {} + + '@types/d3-polygon@3.0.2': {} + + '@types/d3-quadtree@3.0.6': {} + + '@types/d3-random@3.0.3': {} + + '@types/d3-scale-chromatic@3.1.0': {} + + '@types/d3-scale@4.0.9': + dependencies: + '@types/d3-time': 3.0.4 + + '@types/d3-selection@3.0.11': {} + + '@types/d3-shape@3.1.8': + dependencies: + '@types/d3-path': 3.1.1 + + '@types/d3-time-format@4.0.3': {} + + '@types/d3-time@3.0.4': {} + + '@types/d3-timer@3.0.2': {} + + '@types/d3-transition@3.0.9': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-zoom@3.0.8': + dependencies: + '@types/d3-interpolate': 3.0.4 + '@types/d3-selection': 3.0.11 + + '@types/d3@7.4.3': + dependencies: + '@types/d3-array': 3.2.2 + '@types/d3-axis': 3.0.6 + '@types/d3-brush': 3.0.6 + '@types/d3-chord': 3.0.6 + '@types/d3-color': 3.1.3 + '@types/d3-contour': 3.0.6 + '@types/d3-delaunay': 6.0.4 + '@types/d3-dispatch': 3.0.7 + '@types/d3-drag': 3.0.7 + '@types/d3-dsv': 3.0.7 + '@types/d3-ease': 3.0.2 + '@types/d3-fetch': 3.0.7 + '@types/d3-force': 3.0.10 + '@types/d3-format': 3.0.4 + '@types/d3-geo': 3.1.0 + '@types/d3-hierarchy': 3.1.7 + '@types/d3-interpolate': 3.0.4 + '@types/d3-path': 3.1.1 + '@types/d3-polygon': 3.0.2 + '@types/d3-quadtree': 3.0.6 + '@types/d3-random': 3.0.3 + '@types/d3-scale': 4.0.9 + '@types/d3-scale-chromatic': 3.1.0 + '@types/d3-selection': 3.0.11 + '@types/d3-shape': 3.1.8 + '@types/d3-time': 3.0.4 + '@types/d3-time-format': 4.0.3 + '@types/d3-timer': 3.0.2 + '@types/d3-transition': 3.0.9 + '@types/d3-zoom': 3.0.8 + + '@types/debug@4.1.13': + dependencies: + '@types/ms': 2.1.0 + + '@types/estree@1.0.8': {} + + '@types/geojson@7946.0.16': {} + + '@types/hast@3.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/linkify-it@5.0.0': {} + + '@types/markdown-it@14.1.2': + dependencies: + '@types/linkify-it': 5.0.0 + '@types/mdurl': 2.0.0 + + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/mdurl@2.0.0': {} + + '@types/ms@2.1.0': {} + + '@types/trusted-types@2.0.7': + optional: true + + '@types/unist@3.0.3': {} + + '@types/web-bluetooth@0.0.21': {} + + '@ungap/structured-clone@1.3.1': {} + + '@upsetjs/venn.js@2.0.0': + optionalDependencies: + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + '@vitejs/plugin-vue@5.2.4(vite@5.4.21)(vue@3.5.34)': + dependencies: + vite: 5.4.21 + vue: 3.5.34 + + '@vue/compiler-core@3.5.34': + dependencies: + '@babel/parser': 7.29.3 + '@vue/shared': 3.5.34 + entities: 7.0.1 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-dom@3.5.34': + dependencies: + '@vue/compiler-core': 3.5.34 + '@vue/shared': 3.5.34 + + '@vue/compiler-sfc@3.5.34': + dependencies: + '@babel/parser': 7.29.3 + '@vue/compiler-core': 3.5.34 + '@vue/compiler-dom': 3.5.34 + '@vue/compiler-ssr': 3.5.34 + '@vue/shared': 3.5.34 + estree-walker: 2.0.2 + magic-string: 0.30.21 + postcss: 8.5.15 + source-map-js: 1.2.1 + + '@vue/compiler-ssr@3.5.34': + dependencies: + '@vue/compiler-dom': 3.5.34 + '@vue/shared': 3.5.34 + + '@vue/devtools-api@7.7.9': + dependencies: + '@vue/devtools-kit': 7.7.9 + + '@vue/devtools-kit@7.7.9': + dependencies: + '@vue/devtools-shared': 7.7.9 + birpc: 2.9.0 + hookable: 5.5.3 + mitt: 3.0.1 + perfect-debounce: 1.0.0 + speakingurl: 14.0.1 + superjson: 2.2.6 + + '@vue/devtools-shared@7.7.9': + dependencies: + rfdc: 1.4.1 + + '@vue/reactivity@3.5.34': + dependencies: + '@vue/shared': 3.5.34 + + '@vue/runtime-core@3.5.34': + dependencies: + '@vue/reactivity': 3.5.34 + '@vue/shared': 3.5.34 + + '@vue/runtime-dom@3.5.34': + dependencies: + '@vue/reactivity': 3.5.34 + '@vue/runtime-core': 3.5.34 + '@vue/shared': 3.5.34 + csstype: 3.2.3 + + '@vue/server-renderer@3.5.34(vue@3.5.34)': + dependencies: + '@vue/compiler-ssr': 3.5.34 + '@vue/shared': 3.5.34 + vue: 3.5.34 + + '@vue/shared@3.5.34': {} + + '@vueuse/core@12.8.2': + dependencies: + '@types/web-bluetooth': 0.0.21 + '@vueuse/metadata': 12.8.2 + '@vueuse/shared': 12.8.2 + vue: 3.5.34 + transitivePeerDependencies: + - typescript + + '@vueuse/integrations@12.8.2(focus-trap@7.8.0)': + dependencies: + '@vueuse/core': 12.8.2 + '@vueuse/shared': 12.8.2 + vue: 3.5.34 + optionalDependencies: + focus-trap: 7.8.0 + transitivePeerDependencies: + - typescript + + '@vueuse/metadata@12.8.2': {} + + '@vueuse/shared@12.8.2': + dependencies: + vue: 3.5.34 + transitivePeerDependencies: + - typescript + + algoliasearch@5.52.1: + dependencies: + '@algolia/abtesting': 1.18.1 + '@algolia/client-abtesting': 5.52.1 + '@algolia/client-analytics': 5.52.1 + '@algolia/client-common': 5.52.1 + '@algolia/client-insights': 5.52.1 + '@algolia/client-personalization': 5.52.1 + '@algolia/client-query-suggestions': 5.52.1 + '@algolia/client-search': 5.52.1 + '@algolia/ingestion': 1.52.1 + '@algolia/monitoring': 1.52.1 + '@algolia/recommend': 5.52.1 + '@algolia/requester-browser-xhr': 5.52.1 + '@algolia/requester-fetch': 5.52.1 + '@algolia/requester-node-http': 5.52.1 + + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + + argparse@2.0.1: {} + + bail@2.0.2: {} + + balanced-match@4.0.4: {} + + birpc@2.9.0: {} + + brace-expansion@5.0.6: + dependencies: + balanced-match: 4.0.4 + + ccount@2.0.1: {} + + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + + character-entities@2.0.2: {} + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + comma-separated-tokens@2.0.3: {} + + commander@7.2.0: {} + + commander@8.3.0: {} + + copy-anything@4.0.5: + dependencies: + is-what: 5.5.0 + + cose-base@1.0.3: + dependencies: + layout-base: 1.0.2 + + cose-base@2.2.0: + dependencies: + layout-base: 2.0.1 + + csstype@3.2.3: {} + + cytoscape-cose-bilkent@4.1.0(cytoscape@3.33.4): + dependencies: + cose-base: 1.0.3 + cytoscape: 3.33.4 + + cytoscape-fcose@2.2.0(cytoscape@3.33.4): + dependencies: + cose-base: 2.2.0 + cytoscape: 3.33.4 + + cytoscape@3.33.4: {} + + d3-array@2.12.1: + dependencies: + internmap: 1.0.1 + + d3-array@3.2.4: + dependencies: + internmap: 2.0.3 + + d3-axis@3.0.0: {} + + d3-brush@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + d3-chord@3.0.1: + dependencies: + d3-path: 3.1.0 + + d3-color@3.1.0: {} + + d3-contour@4.0.2: + dependencies: + d3-array: 3.2.4 + + d3-delaunay@6.0.4: + dependencies: + delaunator: 5.1.0 + + d3-dispatch@3.0.1: {} + + d3-drag@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-selection: 3.0.0 + + d3-dsv@3.0.1: + dependencies: + commander: 7.2.0 + iconv-lite: 0.6.3 + rw: 1.3.3 + + d3-ease@3.0.1: {} + + d3-fetch@3.0.1: + dependencies: + d3-dsv: 3.0.1 + + d3-force@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-quadtree: 3.0.1 + d3-timer: 3.0.1 + + d3-format@3.1.2: {} + + d3-geo@3.1.1: + dependencies: + d3-array: 3.2.4 + + d3-hierarchy@3.1.2: {} + + d3-interpolate@3.0.1: + dependencies: + d3-color: 3.1.0 + + d3-path@1.0.9: {} + + d3-path@3.1.0: {} + + d3-polygon@3.0.1: {} + + d3-quadtree@3.0.1: {} + + d3-random@3.0.1: {} + + d3-sankey@0.12.3: + dependencies: + d3-array: 2.12.1 + d3-shape: 1.3.7 + + d3-scale-chromatic@3.1.0: + dependencies: + d3-color: 3.1.0 + d3-interpolate: 3.0.1 + + d3-scale@4.0.2: + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.2 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + + d3-selection@3.0.0: {} + + d3-shape@1.3.7: + dependencies: + d3-path: 1.0.9 + + d3-shape@3.2.0: + dependencies: + d3-path: 3.1.0 + + d3-time-format@4.1.0: + dependencies: + d3-time: 3.1.0 + + d3-time@3.1.0: + dependencies: + d3-array: 3.2.4 + + d3-timer@3.0.1: {} + + d3-transition@3.0.1(d3-selection@3.0.0): + dependencies: + d3-color: 3.1.0 + d3-dispatch: 3.0.1 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-timer: 3.0.1 + + d3-zoom@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + d3@7.9.0: + dependencies: + d3-array: 3.2.4 + d3-axis: 3.0.0 + d3-brush: 3.0.0 + d3-chord: 3.0.1 + d3-color: 3.1.0 + d3-contour: 4.0.2 + d3-delaunay: 6.0.4 + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-dsv: 3.0.1 + d3-ease: 3.0.1 + d3-fetch: 3.0.1 + d3-force: 3.0.0 + d3-format: 3.1.2 + d3-geo: 3.1.1 + d3-hierarchy: 3.1.2 + d3-interpolate: 3.0.1 + d3-path: 3.1.0 + d3-polygon: 3.0.1 + d3-quadtree: 3.0.1 + d3-random: 3.0.1 + d3-scale: 4.0.2 + d3-scale-chromatic: 3.1.0 + d3-selection: 3.0.0 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + d3-timer: 3.0.1 + d3-transition: 3.0.1(d3-selection@3.0.0) + d3-zoom: 3.0.0 + + dagre-d3-es@7.0.14: + dependencies: + d3: 7.9.0 + lodash-es: 4.18.1 + + dayjs@1.11.20: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decode-named-character-reference@1.3.0: + dependencies: + character-entities: 2.0.2 + + delaunator@5.1.0: + dependencies: + robust-predicates: 3.0.3 + + dequal@2.0.3: {} + + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + + dompurify@3.4.5: + optionalDependencies: + '@types/trusted-types': 2.0.7 + + emoji-regex-xs@1.0.0: {} + + emoji-regex@8.0.0: {} + + entities@4.5.0: {} + + entities@7.0.1: {} + + es-toolkit@1.46.1: {} + + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + + escalade@3.2.0: {} + + escape-string-regexp@5.0.0: {} + + esprima@4.0.1: {} + + estree-walker@2.0.2: {} + + extend-shallow@2.0.1: + dependencies: + is-extendable: 0.1.1 + + extend@3.0.2: {} + + fault@2.0.1: + dependencies: + format: 0.2.2 + + focus-trap@7.8.0: + dependencies: + tabbable: 6.4.0 + + format@0.2.2: {} + + fsevents@2.3.3: + optional: true + + get-caller-file@2.0.5: {} + + gray-matter@4.0.3: + dependencies: + js-yaml: 3.14.2 + kind-of: 6.0.3 + section-matter: 1.0.0 + strip-bom-string: 1.0.0 + + hachure-fill@0.5.2: {} + + hast-util-to-html@9.0.5: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + comma-separated-tokens: 2.0.3 + hast-util-whitespace: 3.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.1 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + stringify-entities: 4.0.4 + zwitch: 2.0.4 + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.4 + + hookable@5.5.3: {} + + html-void-elements@3.0.0: {} + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + import-meta-resolve@4.2.0: {} + + internmap@1.0.1: {} + + internmap@2.0.3: {} + + is-extendable@0.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-plain-obj@4.1.0: {} + + is-what@5.5.0: {} + + js-yaml@3.14.2: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + + katex@0.16.47: + dependencies: + commander: 8.3.0 + + khroma@2.1.0: {} + + kind-of@6.0.3: {} + + layout-base@1.0.2: {} + + layout-base@2.0.1: {} + + linkify-it@5.0.0: + dependencies: + uc.micro: 2.1.0 + + lodash-es@4.18.1: {} + + longest-streak@3.1.0: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + mark.js@8.11.1: {} + + markdown-it@14.1.1: + dependencies: + argparse: 2.0.1 + entities: 4.5.0 + linkify-it: 5.0.0 + mdurl: 2.0.0 + punycode.js: 2.3.1 + uc.micro: 2.1.0 + + markdown-title@1.0.2: {} + + marked@16.4.2: {} + + mdast-util-from-markdown@2.0.3: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + mdast-util-to-string: 4.0.0 + micromark: 4.0.2 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-decode-string: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + unist-util-stringify-position: 4.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-frontmatter@2.0.1: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + escape-string-regexp: 5.0.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + micromark-extension-frontmatter: 2.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-phrasing@4.1.0: + dependencies: + '@types/mdast': 4.0.4 + unist-util-is: 6.0.1 + + mdast-util-to-hast@13.2.1: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.3.1 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + + mdast-util-to-markdown@2.1.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + longest-streak: 3.1.0 + mdast-util-phrasing: 4.1.0 + mdast-util-to-string: 4.0.0 + micromark-util-classify-character: 2.0.1 + micromark-util-decode-string: 2.0.1 + unist-util-visit: 5.1.0 + zwitch: 2.0.4 + + mdast-util-to-string@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + + mdurl@2.0.0: {} + + mermaid@11.15.0: + dependencies: + '@braintree/sanitize-url': 7.1.2 + '@iconify/utils': 3.1.3 + '@mermaid-js/parser': 1.1.1 + '@types/d3': 7.4.3 + '@upsetjs/venn.js': 2.0.0 + cytoscape: 3.33.4 + cytoscape-cose-bilkent: 4.1.0(cytoscape@3.33.4) + cytoscape-fcose: 2.2.0(cytoscape@3.33.4) + d3: 7.9.0 + d3-sankey: 0.12.3 + dagre-d3-es: 7.0.14 + dayjs: 1.11.20 + dompurify: 3.4.5 + es-toolkit: 1.46.1 + katex: 0.16.47 + khroma: 2.1.0 + marked: 16.4.2 + roughjs: 4.6.6 + stylis: 4.4.0 + ts-dedent: 2.2.0 + uuid: 14.0.0 + + micromark-core-commonmark@2.0.3: + dependencies: + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + micromark-factory-destination: 2.0.1 + micromark-factory-label: 2.0.1 + micromark-factory-space: 2.0.1 + micromark-factory-title: 2.0.1 + micromark-factory-whitespace: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-html-tag-name: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-frontmatter@2.0.0: + dependencies: + fault: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-destination@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-label@2.0.1: + dependencies: + devlop: 1.1.0 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-space@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-types: 2.0.2 + + micromark-factory-title@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-whitespace@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-chunked@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-classify-character@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-combine-extensions@2.0.1: + dependencies: + micromark-util-chunked: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-decode-numeric-character-reference@2.0.2: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-decode-string@2.0.1: + dependencies: + decode-named-character-reference: 1.3.0 + micromark-util-character: 2.1.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-symbol: 2.0.1 + + micromark-util-encode@2.0.1: {} + + micromark-util-html-tag-name@2.0.1: {} + + micromark-util-normalize-identifier@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-resolve-all@2.0.1: + dependencies: + micromark-util-types: 2.0.2 + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-subtokenize@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.2: {} + + micromark@4.0.2: + dependencies: + '@types/debug': 4.1.13 + debug: 4.4.3 + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-combine-extensions: 2.0.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-encode: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + transitivePeerDependencies: + - supports-color + + millify@6.1.0: + dependencies: + yargs: 17.7.2 + + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.6 + + minisearch@7.2.0: {} + + mitt@3.0.1: {} + + ms@2.1.3: {} + + nanoid@3.3.12: {} + + non-layered-tidy-tree-layout@2.0.2: + optional: true + + oniguruma-to-es@3.1.1: + dependencies: + emoji-regex-xs: 1.0.0 + regex: 6.1.0 + regex-recursion: 6.0.2 + + package-manager-detector@1.6.0: {} + + path-data-parser@0.1.0: {} + + path-to-regexp@6.3.0: {} + + perfect-debounce@1.0.0: {} + + picocolors@1.1.1: {} + + points-on-curve@0.2.0: {} + + points-on-path@0.2.1: + dependencies: + path-data-parser: 0.1.0 + points-on-curve: 0.2.0 + + postcss@8.5.15: + dependencies: + nanoid: 3.3.12 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + preact@10.29.2: {} + + pretty-bytes@7.1.0: {} + + property-information@7.1.0: {} + + punycode.js@2.3.1: {} + + regex-recursion@6.0.2: + dependencies: + regex-utilities: 2.3.0 + + regex-utilities@2.3.0: {} + + regex@6.1.0: + dependencies: + regex-utilities: 2.3.0 + + remark-frontmatter@5.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-frontmatter: 2.0.1 + micromark-extension-frontmatter: 2.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-parse@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.3 + micromark-util-types: 2.0.2 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-stringify@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-to-markdown: 2.1.2 + unified: 11.0.5 + + remark@15.0.1: + dependencies: + '@types/mdast': 4.0.4 + remark-parse: 11.0.0 + remark-stringify: 11.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + require-directory@2.1.1: {} + + rfdc@1.4.1: {} + + robust-predicates@3.0.3: {} + + rollup@4.60.4: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.60.4 + '@rollup/rollup-android-arm64': 4.60.4 + '@rollup/rollup-darwin-arm64': 4.60.4 + '@rollup/rollup-darwin-x64': 4.60.4 + '@rollup/rollup-freebsd-arm64': 4.60.4 + '@rollup/rollup-freebsd-x64': 4.60.4 + '@rollup/rollup-linux-arm-gnueabihf': 4.60.4 + '@rollup/rollup-linux-arm-musleabihf': 4.60.4 + '@rollup/rollup-linux-arm64-gnu': 4.60.4 + '@rollup/rollup-linux-arm64-musl': 4.60.4 + '@rollup/rollup-linux-loong64-gnu': 4.60.4 + '@rollup/rollup-linux-loong64-musl': 4.60.4 + '@rollup/rollup-linux-ppc64-gnu': 4.60.4 + '@rollup/rollup-linux-ppc64-musl': 4.60.4 + '@rollup/rollup-linux-riscv64-gnu': 4.60.4 + '@rollup/rollup-linux-riscv64-musl': 4.60.4 + '@rollup/rollup-linux-s390x-gnu': 4.60.4 + '@rollup/rollup-linux-x64-gnu': 4.60.4 + '@rollup/rollup-linux-x64-musl': 4.60.4 + '@rollup/rollup-openbsd-x64': 4.60.4 + '@rollup/rollup-openharmony-arm64': 4.60.4 + '@rollup/rollup-win32-arm64-msvc': 4.60.4 + '@rollup/rollup-win32-ia32-msvc': 4.60.4 + '@rollup/rollup-win32-x64-gnu': 4.60.4 + '@rollup/rollup-win32-x64-msvc': 4.60.4 + fsevents: 2.3.3 + + roughjs@4.6.6: + dependencies: + hachure-fill: 0.5.2 + path-data-parser: 0.1.0 + points-on-curve: 0.2.0 + points-on-path: 0.2.1 + + rw@1.3.3: {} + + safer-buffer@2.1.2: {} + + search-insights@2.17.3: {} + + section-matter@1.0.0: + dependencies: + extend-shallow: 2.0.1 + kind-of: 6.0.3 + + shiki@2.5.0: + dependencies: + '@shikijs/core': 2.5.0 + '@shikijs/engine-javascript': 2.5.0 + '@shikijs/engine-oniguruma': 2.5.0 + '@shikijs/langs': 2.5.0 + '@shikijs/themes': 2.5.0 + '@shikijs/types': 2.5.0 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + + source-map-js@1.2.1: {} + + space-separated-tokens@2.0.2: {} + + speakingurl@14.0.1: {} + + sprintf-js@1.0.3: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-bom-string@1.0.0: {} + + stylis@4.4.0: {} + + superjson@2.2.6: + dependencies: + copy-anything: 4.0.5 + + tabbable@6.4.0: {} + + tinyexec@1.1.2: {} + + tokenx@1.3.0: {} + + trim-lines@3.0.1: {} + + trough@2.2.0: {} + + ts-dedent@2.2.0: {} + + uc.micro@2.1.0: {} + + unified@11.0.5: + dependencies: + '@types/unist': 3.0.3 + bail: 2.0.2 + devlop: 1.1.0 + extend: 3.0.2 + is-plain-obj: 4.1.0 + trough: 2.2.0 + vfile: 6.0.3 + + unist-util-is@6.0.1: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-remove@4.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + + unist-util-visit@5.1.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + + uuid@14.0.0: {} + + vfile-message@4.0.3: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.3 + + vite@5.4.21: + dependencies: + esbuild: 0.21.5 + postcss: 8.5.15 + rollup: 4.60.4 + optionalDependencies: + fsevents: 2.3.3 + + vitepress-plugin-llms@1.12.2: + dependencies: + gray-matter: 4.0.3 + markdown-it: 14.1.1 + markdown-title: 1.0.2 + mdast-util-from-markdown: 2.0.3 + millify: 6.1.0 + minimatch: 10.2.5 + path-to-regexp: 6.3.0 + picocolors: 1.1.1 + pretty-bytes: 7.1.0 + remark: 15.0.1 + remark-frontmatter: 5.0.0 + tokenx: 1.3.0 + unist-util-remove: 4.0.0 + unist-util-visit: 5.1.0 + transitivePeerDependencies: + - supports-color + + vitepress-plugin-mermaid@2.0.17(mermaid@11.15.0)(vitepress@1.6.4(@algolia/client-search@5.52.1)(postcss@8.5.15)(search-insights@2.17.3)): + dependencies: + mermaid: 11.15.0 + vitepress: 1.6.4(@algolia/client-search@5.52.1)(postcss@8.5.15)(search-insights@2.17.3) + optionalDependencies: + '@mermaid-js/mermaid-mindmap': 9.3.0 + + vitepress@1.6.4(@algolia/client-search@5.52.1)(postcss@8.5.15)(search-insights@2.17.3): + dependencies: + '@docsearch/css': 3.8.2 + '@docsearch/js': 3.8.2(@algolia/client-search@5.52.1)(search-insights@2.17.3) + '@iconify-json/simple-icons': 1.2.83 + '@shikijs/core': 2.5.0 + '@shikijs/transformers': 2.5.0 + '@shikijs/types': 2.5.0 + '@types/markdown-it': 14.1.2 + '@vitejs/plugin-vue': 5.2.4(vite@5.4.21)(vue@3.5.34) + '@vue/devtools-api': 7.7.9 + '@vue/shared': 3.5.34 + '@vueuse/core': 12.8.2 + '@vueuse/integrations': 12.8.2(focus-trap@7.8.0) + focus-trap: 7.8.0 + mark.js: 8.11.1 + minisearch: 7.2.0 + shiki: 2.5.0 + vite: 5.4.21 + vue: 3.5.34 + optionalDependencies: + postcss: 8.5.15 + transitivePeerDependencies: + - '@algolia/client-search' + - '@types/node' + - '@types/react' + - async-validator + - axios + - change-case + - drauu + - fuse.js + - idb-keyval + - jwt-decode + - less + - lightningcss + - nprogress + - qrcode + - react + - react-dom + - sass + - sass-embedded + - search-insights + - sortablejs + - stylus + - sugarss + - terser + - typescript + - universal-cookie + + vue@3.5.34: + dependencies: + '@vue/compiler-dom': 3.5.34 + '@vue/compiler-sfc': 3.5.34 + '@vue/runtime-dom': 3.5.34 + '@vue/server-renderer': 3.5.34(vue@3.5.34) + '@vue/shared': 3.5.34 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + y18n@5.0.8: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + zwitch@2.0.4: {} diff --git a/docs/public/favicon.ico b/docs/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..9b4870b727cd87940afde740a161711f37831543 GIT binary patch literal 16958 zcmeHOd2m+cxj)I5eSttWAS?-62s>F=0%6~mK!6Ykge}CdTOc7UK_Val!GMS&dR0(S zJEB&5ooT%yPV2Oyt#GB<=`dqkxdboOBD8@@r+M!0_nh;doP2!2-h2PMbK^Xm_q=EM z-rw(8-{|WOEi1{gtX>dISv3&O>)&Y0>Vbmo7`*wtZX9=FOW+f3V^H{pG95U!7Gj>w~S^ zw|=^Bf9)5C4%b~i_429f|N8ci8&AFTq8zL{C~v*>rkp)%(Bk`pB=I!dCVq9h_BqA@l$_Ih%1 z^5yjO^e=LAb3d6pdD2f;uU`4es?{s^Po6TVbkwN4ym|BHbuK6gp7y7ysjrKW-g4a>P1t;DCj7SqC0IU_Dazh_$+WmG#`o=fZdI z*_~EaR<`)Q^0MQbt18dztl4>?qH@!vl9EOB^NQwuvtvi~je`ddG}P|fdkdKFoOu3( zy!7%ZIriie^4jaC<;^$Wlv6Lhs2rSn>6ASG{PXhEQ%@;&tWPL-r^Y8fnvBw{aJM`$G0^on{{`=Qo+^}K8%?BTRP--5mk=1Kf%lrlN zWyOk8*|Mcdwr$-an>KBf%Bo7)Tl!fmXg;c>7iWe=A+~K*>wOdz7OiYxH9Xm=|TAFn4-dzR^ z93XjlBV@{yNit_nfy^zOE0ZTrmdR5l3pltwVZwy-d3kv!moHylysx&l{jYxYt6=J# znDg`VE%0K2Bdb@hUepq_wlU+B*H}>9DFTRhjVdy6Wof^_w?W z3T$1fwrqw}$$IE$<*IVvTrDLfCE#Fz+;?A@ELvDBOBOGZ<;#{xaq)avx^#(@!Zuc} zTq)Jn)oSZ|YxgR5v?1F3p51$(?@EFHN(r7VnmOtOF!FJ6lpyQ>9(IcHp6_ zr1$J8ojZ4ibdrpWUXq=YEz_q@7uY`RV2)y6v}lpcojVu26krWYp_{e!`24z^J9lQ{ z-msZ7XQ~a1968b&G-!}jSWsZqRBpBkiVCgQo_W@{XV0!o`1#RAC5x^CV`KTsa$qQz z6=lG(Y9%l&m-$7|6R;!3;B#k7*?lVzvu45uC&`Tb=`wNBM72fgp$Pg|wrrW`d%#;+ z*$RE%CfF17qISQ$bh%LG&YdH}a)+wU6O$4p4VYo0inndswvv#LAc>U31fA>Fy_*a| zi~ttZJz^htg#CbnS+ffCvtf&kTeoe!3Y$H;apR^;IKA&m{S`O-F+Mz8yVsgBb!zaY zjT`#>;0NpL=FOjXb?n$Njk9OZ7Q_?bdx?uNsknH7%!9wp1>aMqOi@{YIpWV7IYM%C zhsm&E!z42+Q}r``!UP#TdNeqhB>DL>Wc>JXGJVE$LH+>#t*U$4EMp1$R)!22tj~4o z)JgrG`04jux^$7AJ$tDB4HqdX9h8S&>FJV_lcOAzu2=!RFO&Qk`NH+DL7alGHdaRj2b;kMgV&b?jM44&ShZTTr-cvFbKYzm6atKy)*Q_ zx!{uT%%7RB_JUY0BSs8Y;W#ThOXB0>B^mKQ74PBm!-fvgI6xmDcE-fcxHe?)5X8eg znJ{sJ`s3myOJu?P1!@O~b?O(0(~Zb0^@y$2i05$=rcbbH5c_imWm!3cvm!DF4J<}p z`KW8RZqf}gH$A;4BwhOV?=O>}JFbZw4;U~2HaAH6_UngZrp9C5OMmRsySMbid~d`j z#1^bEMUvsWDIGefFpd!u&%434spFpT>CDVQGHPU=WP|e@`23{F6JbL`)$bD!`}ID= zz5aMNV*>f*eSDUZJ1iHxjnEj#cz_(BF@SLZ{;n~i5H`y=RZ&s#bHsuI#Ds9>`>-By zL7lsHN=;4ce6nMwl-mjLEpkij9XqB-`}Xa1+^>H>#N9qByq1D%#K*OD2UhBcc1XTb z(^55NCt>ZaVp~aUOsqslMN4EvghWL~;yecPu@Z?fb35Z3aWK9Q9z0k^!^TF#rZ~s= z*8zN`rgqV|p9$`f+hKp>q*q3Uq{Fs|pZS3NkQZKOJ}~|{8Tz9R>BsN|(;2-!X-2`R3$7dOv1y%M2{RpLPA1V+ez!V*3t=fO<&2(%#`8C{p5pr zk2#LPcy6xhfjXeC^u;`Vf*ddxXik`2AjQa2$VnGb8esKYo^zay{K0rUZ0Ioc71n9QPcB$1p*FcNv!I|fzHQt6Ny*8V7-MLAs$1Y@j-_7J zZmCDdSB>5b?vN0lYtLrwwyoQ~ZRV5%VE6eb9{lPH&N<)Z*a-QgO)*w7wlH_l1{lYN z4anc^i195PzT$3JkY@aJG;=r2SYrGEe+PT4M-%s7!`nQj^U+6X3g@fJZ5BRYT z`WfR7IiS9oA10yB9D-Va^&x8`)(XTp2k~uYe!j;3X{bG!2j~-OBNHd$+Dz1L^H3Yj zU<|ECEPWN9^$ud&HRIc~#V{3He`fuGj>Jm+`+UI?926u$mIc8PxFvppE$C%N& zb)2r5n7q%Kv3XAQ4V>hjHmCS;&Lh{sSVLXJw2IXjKwVN;XQD=h-ZRw?x^zK&K#ZWC zx#ytnN1~o$o@5?i4cV(#y4K#akqak*n?ad_ZVbyE_Gw6H$QQnlkOuv1^6z{u*Qo3D z;Ma9KxOpu&I9UCJHpxBw(W6J@vBw_M{r$#`8?}z|V)l;tE^=;cocU65JJ`twc}Yl! z*POoud$|W5ctGx7zh3tV*~ov)hs=MhRakp)OgkVCi~+2R(tD;WcNrPIG(K|AJaOU# z>6zZ6Au~Jk3+Ui7KKGX9AAGj@qBllijb`0E8V)$7ozp*;px*xIqmShF?c37O&>-J^ z_no}=-g_FS`9AOS?mI9a9u|(g5~F#>#G7#B0j`C0D03J42S0q}6*+hAoc#Xxzt>(8 zYnfia(;IsU)}E}5Sd(f^4IiP+b02~pndV2v2X^Pm{WA{moc_w* z0(y4x)mLB1KmPF#_r+(=o<$5R)cmLWAO|n&fKDAdX`CQl){(3Q$q(y!=5yvaUSmB4 z`?!O>*mbOP5!@N!H$WQ|VyiAK+ubpz*Oz$y2jtPckxW(*uKP&r@>MK2I~ zp>Aw_dok;pzlRIvJnmasvVZmmTEfq|fp*9ENB!{~@4x@P{N*oy35c7Mw{G2%H{N&y zabbe$nX#vJYf5Xk9wmOp5(?{6`WCrmJRFI7AQO9b?kQM9anJ0J+5eor>z(u3i{i&; zmk03w-~8q`+B@Q&iSO|GPO!r_b;LTAas87|K9M_j?%ai)zJK%PP5IsLekVWr(T~tC zTCMwH*5%q42R>qCjiBdvCpqAlcF_)XBjY0X<*b>Ac_j9QhZ0&-qILOoF)buF1T;z9-iL?{NWFO2#B40T)K2gjvqgc9%qr}LB_^5 zac#7xK-@-%m-Q=!`!eWwj+eLcegx#PYL`)_&YopCH46JF?3Xb1vFF4+A~8_NgDb@A#03)@TpqA~Q~w$Ks{-iX zfTA6oK7Cs2(ZqyAwY&ZM_shkL7n|iZb3c!N`qQ8EnUcjN=VFTFAPV&F%W*`!{I=Em0WoJ!QsH* z4;25rc*wzPue~PK+pFb8d=GXN@x9S|gEOYRlNa>mbU&l}G2{kfXFMZ*ejmuVW^+Kl zw}tn5@zY)s;Q#DJac{@I2KP@r-~;?_fqf+UpR03^{uMvZ9n9(r$IL@E@KT$@y8#f^#X0c)-|~>Hc$PVxX=D0b3eJDFR%t; z9l-t$;|S|RV(^OL!{xxsgVzQy=3cLLE9AfOyYqhl^#+_`eE;&xFNNO?=A&23I$mo* z;AX#+`BC#3b?=C)f5+Ahe%b=@8$Tgl)`5%xe8$)Z$E=0fLnGdn@HgWiD1iR4-)#-M zTUlOySN#(ZtY?xo@YlcoRet^JU(2ptyVM?ugS8Mbvv;U{HG|*r0lTm0)Qi+Ruy}>G zLB9A7_6!*JSsNPeIM4fO16@%I8|+5!=6J7py$AZo7bi{lKl{vsPBfp<)&u2q-pKmy znoS|2;u$%F1Wzytll?%99? zj~{qBXmRZ5pFR}`KkI*fC(rN4xfkS~*!ySv{s{U7>}L`?Yh`j_V-D9N$9hgY&}n3Z zEsT%EV(gu`_1wYF{v?Hape{~-fcC)nz|Y~kSRc9gX$KAmUd;YBU|+Li0d41A_*q|( zWBd+5$cMi#Fa}(yuU9)6h`xpCsp{H+n_RH}qa4r&Tw!dmZC-5!^Tf}ZkM`#kfkLFAzW9NiLe;4z`xyNJOKszZy4S(j{cLQ?3nuzg&{VT3pb#L&~ z4!{TJ*%Q|z{6Omf=#cwegNZ&*;qwN6LIU~Yw8347l_TP#HXj(r^V z3cY_C53XFne(x#G3$$nAP#>TTAVz9$p1wg_fS$<-G1>R=Ub~M@yCe6!Zv26B6h23O zd5t#gFXV*wPrUx3eSPSjI>iqQoAUoX@ax{evv*`n=66u6cLQ;7?b=j->Z-6yAW1E))n+I^=m5Wc6ef+;?gU>ps{ zJnCoRKITluQrZxO_~HAv5Qm8U3rGVo8!dC-pRt6!lvS%%X&?6ie0Nc^b0>ZWx=eo;MC?K8KZt?Ux1$&D(d7WSn%~=2Z{Kde z3w`086ugh$iSV~8UD3;pK)fH@Ekb@=(pJu`jF%6};^mEnZDh+Re1DRHycpJG2kH|J z2jqdeXHAIlw}=Vnp~DX#-}vL#bJ(r10p-ENf!cx3{*IgVhv~5}u5(|>x`TQmej^5_ z7rW~Z4mZ>dpXZ*9!aa%^^E1eIR9J`<^^cPOs7#c`UCDCi!DP9;GfBRxN|Z;ZwUQnQ zj1AxbxVZ-AO1_u2&v@fY#| z9H3^tf<3`2z`q}IN%c*v{A>>vd;ab^D9Gst8DB71y+_kCHOFSx9DRFv2sG9hNnSkP z!-ryA&^uDr%UR$@`(RQeeE`;&=3I z?7^KkdRN^zwUGKllYMJ`aO|C!y?Fh{fv|e#j9<8O9{h~=W4cDj#dY!0NZdyMIR12L z8ySyy7{NH`pTV=FMCXaCK=DD%GQ7<39bBlaJ|_W_-OJqA(&>=%h2V^<6xUfeE5N7w%6 z=3b*~^Zki?&d+zG`5e}U^?5P2G&Xj?-;jl)297{HUpD^ixsTWA=j!X8 z>n?7G0~7ynybax-L_SRIl9EE~{0oN^Kz;^sF>`Fh;CJ=wr+;Gi;@7cTFW9lrP56(ny>JlR5fhH}{yM-p4rLpBMZv8r}0bgEbI-Gw0s#&yk~B#q8__b!_b1!L9q^ z(9lNI1NFh7p}T{8zV?I+E)+TN;*b9qrIi6L>VmDHt5}=M0oU1*w8)Kz19K0L-u{Uf zm%(cIFvmdmxc54CpuZ(`F~jH0xarskyT6J$u+A42)(2pNLn9-t%)&xN&VNER3|Ky3 z&jb@ku~wG@f9#6G;iMTh&zyP2|6ViikIQ?0FYLV5>|V72?)mL~ztO+xg8(;w-*pvo z6!9mr4L*0>0c^pGk@tDe>l?oa_7iur$Nv zcg=9(kDt#pwhFUqC*Ed;mLn z6|x^amQqL#=8}Ow255tVfisl&i8TggF7<5$i|60_`mg1yv5#2x|Ns48W}wCf7xcb; G^!*n?&1#?k literal 0 HcmV?d00001 diff --git a/docs/zh/configuration/config-files.md b/docs/zh/configuration/config-files.md new file mode 100644 index 000000000..b9fe6f981 --- /dev/null +++ b/docs/zh/configuration/config-files.md @@ -0,0 +1,228 @@ +# 配置文件 + +Kimi Code CLI 把全局配置保存在一份 TOML 文件中,包含 API 供应商、模型别名、Agent 循环参数、后台任务、外部服务等。这份文档介绍配置文件的位置、顶层字段、各嵌套结构,以及一份完整示例。 + +## 配置文件位置 + +默认配置文件位于 `~/.kimi-code/config.toml`。目录和文件会在首次运行时自动创建,并设置严格的访问权限。 + +如果你希望把数据目录放到别处,可以通过环境变量 `KIMI_CODE_HOME` 覆盖默认路径: + +```sh +export KIMI_CODE_HOME=/path/to/kimi-home +``` + +此时配置文件路径变为 `$KIMI_CODE_HOME/config.toml`。无论目录在哪里,文件名固定为 `config.toml`。 + +::: tip 提示 +TOML 中的字段名一律使用 snake_case(例如 `default_model`、`max_context_size`)。如果 key 中包含 `.`,需要使用带引号的 TOML key,否则 TOML 会把 `.` 当作嵌套表分隔符。 +::: + +## 顶层字段 + +| 字段 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| `default_model` | `string` | — | 默认使用的模型别名,必须在 `models` 中定义 | +| `default_thinking` | `boolean` | `false` | 新会话启动时 Thinking 开关的初始值,可在会话内通过模型菜单切换。即使该字段为 `true`,`[thinking].mode = "off"` 也会强制禁用 Thinking。详见下文 [`thinking`](#thinking) | +| `default_permission_mode` | `string` | `manual` | 新会话的默认权限模式,可选 `manual`、`auto`、`yolo` | +| `default_plan_mode` | `boolean` | `false` | 新会话是否默认以 Plan 模式启动;省略等同 `false` | +| `merge_all_available_skills` | `boolean` | `true` | 是否合并所有可用目录中的 Agent Skills | +| `extra_skill_dirs` | `array` | — | 额外的 Skill 搜索目录,会叠加到默认目录之上 | +| `telemetry` | `boolean` | `true` | 是否启用匿名遥测;仅在显式设为 `false` 时关闭 | +| `providers` | `table` | `{}` | API 供应商表,详见下文 | +| `models` | `table` | — | 模型别名表,详见下文 | +| `thinking` | `table` | — | Thinking 模式默认参数 | +| `loop_control` | `table` | — | Agent 循环控制参数 | +| `background` | `table` | — | 后台任务运行参数 | +| `services` | `table` | — | 内置外部服务配置 | +| `permission` | `table` | — | 权限规则配置,详见下文 | +| `hooks` | `array

    Wyn=k}U2a+Xg5ZA8Tgy9NK&cepWI^*2*5CrAL)A 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 oenl30EDSbPoxkDF3k#e@o$pvxW`lwc~2PBHO+ zhZ!wZsuvxJEbqaEXu=Ax#$vR{!?fL6~jou=YUD_mgSUm)kefrwQkb z6`HSgEc<+Lndq7RjTy0u{y}Wae9sNipc9Y8+-xuuaazVhiAEp} z0c?knX#N<=+(1k3QI%$hB3R^WPVBI%0A1$+>_xiKe%uD}#Zf%ntdX(;21o3nf~1}3 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 uv}^ITWDsq$ej+ zDzu7Dw4^OnRh9Ik(oGx6+gJ!K`loeJePB3fH;P zwXSx(D_--eSDdmms0-aIU;``I!4kHxhCM7|6RX%44%V-ZeJo_-x!B24wz8JJEN1_y zPpVo4l1-H==5VN4g6>s@Wr}RTh-%u>9?5Q*4Gn6;`C86K%B-SwOw%4qh5wn8wj^Y1 zReu{*eyuW^2|a0S!}*pd*(X9&l}}&8`I_juXGC+o5?T>wB^kkyugMK6ryyw0AE899 zV|8Iz2R5iu!gK}xkjb8d$p|`ZN>u#VAbot>QeCSrf>Ma@pp^*Ay;4TG za^?+V+!{VExi%&S<~B01;FdLWVd!C{_#_9C3NM+E;@KYXW>(7yh0jEx0rS~>DZ+~Y zX1j;n;F`lLD;pV7$Cm7IiU^HJMd$D{NQ(~~SR6cCL9vcy!lPLZ1Fc&9wsPu$*c>fN z#wXm^y%>X);c$W;`e6{bMrtihO@-dw6z+~rOkU24h|66fZ7F&njtmfdRM#oNdy=^- z4_3P@T~BM6fh4E;$`@(6E)GJdFuGZg(ehg)V31Gz%g)3x6gf>yz#`Gc2>$k@`+TXZ{;<)p~&l1r6_)l5Yipb z+&qJOK|yn!Du}ww&1pG61*wVQCHD=Qk`JWj=}tRJljf$rvfV^~h%Tn3kVeUBjduRs5XL$VTM|3r0ChR=7|f z^${eAoCO-#EXmHS{SS4xAJL)SxcE`jy~Kj`i*r0sr@ikp$h z?7152;h`O75&(8twVl*Zm|#T&q6&%+2Re>U%^@7B%w#axx%C=$E!ScQSq~QCOOTxw zwuShtmakRfR}F?3^ah!5$`?W+{m8}vqGG=Y4CBd&=)K%~h(xhn6&&tM&0rS`ij>sI zz?oo^`|VzGoCPoH4RjR74i1!o#9{E;5ZxKI}oNCLH3 zm#LI7(T%A*%#+n$D>9Z6`HU{Aj3ZXl8>PqlnTWk`NSlOMzJlJVIn6MoCZ3(I7_HR+`aLf`pZLpJxEl zry0{cW?53*gfa1B;AJE`reb?>7%b71gS}y}rCNFYC0sq;grU@M?PG26BhCesgpHKS z^`+u@+-&95GrHJJDI|iSi)5YuBwuu_kPq(<-KBONCEn z3YLb&rcdb}S6zvmm6UDirf{+*Rt=|d9;Z*iUu@kbbG{g2-sVCMS8tIOa#p8xUMF^D zr*>{9cXp?DerJt^6?l#(d6uVno+o;yr+TjEUv<@DvL}4Tr+m&Qeb%Ra&Sz>$m14e> zTSaGcg4Ra-r+^M9ffndn`KNaQ;8Cd`g4U!_nh_@r!X=!{!&G2@rk-EfpEsUKu$@&k z-Y1BLsECeed+O&B`W}thRDnrAl?;{M0TqTSS41v~$E4_YdFDgOT!qSJxWx!W87Pnj zsgMq7gGS)o*riCN;MjowVn34C=$s63{TPa(NN^lmRub8nIiIV+Wi1}*vN4<-WuQMQ zDFseV$B7Y145^x~DVxq#kq+Y>?gl~dQB-zgS+N9A*`JfD+ki~kTjtEn`N=psP7I+3 zwmjwVbj#M!X&F(>Wz;}{RGI039SmZHCR~%sprgkHAZoU$r+zA^3g%JqqiE@%8~UQQ z5lF1W-$|{Nk8;cG9i%IAsXAp`T1r&>xhB6jkJBNgl;({De2dVmsGX9_TGUZ(g{rbH zE3=AcTG<>oiA@<^1%9l~ifUYrW)NHA9Ea^PhB%@7G9q39iAI&#VYs^c4J>A8Yr zG_97|kc(l+39GjMT^|uwZjz|J?km5lCsN&Hz-3$Fm1eqDT<6qkZo~|E1zj+XjK0w1 z-=K>q>P(bbq79ZIW+rRB`X;5NNTw7BPEwPMS;t}M%1W=pDI-FePl zkcH?)i@c8Bw2mnO2HW!OU&aMeDFH~+l!nep=xu4j6JFhujM{|IhCQ(#pR9>|rJT#k zt0$Vz>3Er4tjkM~6JC^?B#vRC|Kc0$d>GKvIkud?b;pw&jCXNW5$gRlbsqc3V?usso8t>s=W=9-sL zt$;J?thhM;XJLBIgT0^+7#lgZtFcz*Q~~Fp-jpaQAHm~#MXJ+QB@=h=HRGy`O_>M36mTzL3FZ!mh`mQhgB5C`+FZ{-@{625W&aeI6FaG8)W!10#_OJi`F96$C z01q$$7q9_?75^Tv0xvKFFK_}kFa$@i1kdjSPp}1FFb02b1!ph^cd!R5>jr27ZPGVx5Ylj#ES`R-#x$ij?1=S6rPdQ?I%ECa0~ zFt#2QFMQ$Vs_yXZgd5}55Fd`a!S8ucRW7hj(nX0a$O0gzah#r0CCow{BXN%I;~vGx z|7c;0oq{YBKqNNIRuqGJog{=!vQg#nQmIPB(4wg(M;><|Q`YJy%dR^muUT+1USQcW zByA&?V>Iw_>>gCc@Q5Za-6_|j2;#CP$6YPIP^szw8sqF{?6PK^#4Cp`XJ!Qva57go zvI)+eCrfiC*Xsg}Vy6!969+ToCKV)0PUqRsCATG~*kT+89`SK3R|*9hhe9Y69T<}T zltZWii(cF^j~pq?@j(E{*L+JFhyv(o2A&QFNs_Cfpo5mtI8KF*c7}776PI z>O|-50^K3_jen0#}cr7Fx2R120I#SgUnMgo06joIwl&1cd^-wTC2x^)|q@ zLAZ26zp@@6gKI{~Tt8(89Rn;&1Sw2JTYrRI7l<>cbsl#RR|jGT-Sb3L%||@{Hblb~ zFF3V2A~m(RbaWJh0*x{+)Sy97^(Ak%EBx|DaP~HcHlWz{_Z&8ROz@j7gKztG-5O$a zq7!2anp8`GT-!7%dh{tR1EyLEZ5K6ZUvrP3t7jt-1;QZ*j&xBI5_9w)T6i`um_j5* zw-7~caz1OX7WL4-C?x7c6PY%K%jrjk-2b6_F2wsPwEA`=Q_ZaTSfwSWeQMXTfREuoFZ~yiu$hOj&N6fkiF9gFG zk5*Mcaso&IZA*h^*WEB2HyI1Ufv3kSJAyE*zyUb4iIX^`A@?mz@?5|FvIFolL@a|X zBx1Xg$87lagF|1bdO?n7SS~z+e4kB3OS1zgUxp)j96AM)kHRkIkljuBf&aqWA^8RH zLMWVamT$SM$pW4!$CkGnW@7S80y%_VM25HPf)llKN8&F}vXFyyY>PFQFF8En_G%A! zM+ieD7U^HpPL6@ zib9cly3y2kl5@%`+(N~|ci0B^;myVrh`EjqMlO&yr1IA>NHyL$#!1HuAh@|N*@CeX zyBXuQ`+}QpC+kEMx*NDToR4C%YXyP{f-6YF|C#2s{I1WX0AZEA(jQd3_ z_zgX?J-=O|w{~pX*W5k9wRe$Q|9Pm;bpto(fNS~}-lE4vgCG=u7^pkC>HBQIc#JO+ zmI>_FUYu6C!gjHX%Orb^rWS^}3G7lU>El5DIF98Q8OY5Hc$@2Ls z`MBdOb5gHB(mo?j<#@`myJK&{D1Op*YmuWLL>_w{%->ya5Qn5oeN~~NY)pAv2Sa=> zI(AF_DgB!EU4beKRDMEylF0qRr~Ixi!rqSwsN31XYv9SAa}PzxrBeY zdFw=En}@b{)LbWiRsS)8&xR35gC=xEpZ62evMi+i7cZn||2tA+zRWi`$@fK}H}%dd9@m_HJ$cx~;QK|ieYk$UKydb_ zS3O2DDW3%cOS=1BaPkuP{^!7D;1=)n4C`TzX;DS9H$D9cT#N$56dfJ56(m>?A;J}6+$d3~&_qOu62&A+ zcoCz)j0oXe>`)_*qm3h1U?@n%$HtNp5F*Qy&=H`K7!M{~IT7c=h7lc}EXd zLgbmI=S+hFKgxJX^rE0dVs7SCx{-)dkv6Y(jXJgeq``|DJn#%JAvat$!bXe*Nz8 zh}!=jzyJjtkiY^BgigK$<&!Qx1s!}aLFOQwkirTryb!|-HQbQH4v7oR!w^Lrk;D>B zJQ2kdRa}w97A12L#u#Osk;WQryb;G7MFdg*#~ywB5y&8g9FoW)wKEdQB$Zr}$tInA z5-KRAoRZ2at-KOT^|IWO%PzhA63iQU43o?<%{&v$G&xg~%{JY96HY0|jFZkf?YtAu z5a-O3&p!S96Hvhd9hA^Q4YhO6Lls?=(MFj})X_*Kos?1+A*~eCOf}sU!b>^*6x2{f zeQVTGO+EEgPf=Z!)mHyhHBAN&oR!vEZM_xOTy@=**Is@7)w>AS+%CWC$jc}x_UbbV z*k+x57TRc~otD~a3rx=>ghZLI7PAf$jK043;w3N&?Xwo$bk$v#-FDr5ce4b^)k-%d z3aHK?2lSn2GVJ{Fx2~49Sw^>&pxQV8qCB>p4?Bbv+V@C^1dxSUcrCsdJ(W<(W|6K9C?J|Z6>R^xPmO=r6!jPn$6}5o|1ryQe?^G zrnYR>`R9vpE-01l+%V;pL~0@op(z$V#nrC8rZY5BJ%|5j#GSVVPQ1_+wlvI}S`y^hZDXn78rJtVv`s%I6*mL!GsKoHIguAQwjq0|`^tGECX>G@MCpi6& zXIg%Drnk8rBlG3ohWntO{+j>(&4hJW7gzSy?gy zzo?9a7g#%-|2WveSfQr_BWsBdIxw%hMI-^;(^heKM6`kAq$g!77ebtu6x(2gXG%!h z?&7DC1T17G`}5!siTI?jo#%iTDIJ2gF|V{VktD}B28d>HK7kb^Z3GllS9*xWuH;XA zzi>q5u2)7go)L{{yq5cDlfPV8Eh3LFAxAdSGDCExFj}utb$ogV;&Qk(F#&Bnb}NdJ`Y`gMly_^c)*yEPzWNFK!T**cyP&Uddyrm4AVF1`O6G3 z#}3apr9@t#2Ifd;JSEE}V&n-K?Bpys{K~~GBxoH|5;Bwf{0u}#B8t%w>ngwF66#DoH7K%EuAKh>Ql6AciS8%_u*6p?G9sN)2W$4>d^O zS$Gmu5u%iLnk3c#Hf;E@L>8nHH?^4(01;HgNmVIFWtbZT0W+wOl&KGm=r+JwuT**j zt66Ot;x0(CuzJJ}V7<&**z*OuG7>0Mmif zX)Oq7hzXr6LMMT8*5rT}o#tkEq1laGw21@Rie+uF$R^57oL-@9U{qF`Oe}R52(em0 zE*jL-E_H?(;e}!qNkfcCk}cF8OR{VGH&IPaR}3z-6xvyRzOd5RDujz;4Uv-P>BxiP&8>5*ZKQGEWK z%PmZSFvmW*R~nK)D1_Ksqz0y21idZ}yXcXamP8E87;<8sw-Jlp525TG7$&}pPKhJ} zXSN}u8hYRg|5hRGDMIA+9@*~+x_9RhzVjD%$t%CU~}J+0e7<1Ho+RlVQ8%w9waw7rh%HSBjn9wNE?^kZ8V&m~Y$az_Rq(`~KLs&1C%Pj2R29&ZyQJzCFsqG;h4Q4~!pv9WHC5=Ss>5nOJj4k@5)Jyqtgb zxc;>0JS?Vx=rn}Jn9G}xi&y;T+xR+rOF1x;L1xQPjpW-H&@1xbt_Iv>s0|p9nUl`F#~p^aN*!b->e#Tf7yYq8f)`Tx8lE z(@<2x6|XJ$pboB{sD^U6dk;kq+YWFbZ)7VG^k_)x7hl-GQ7Rg@nHt7eJBIaK>SERX zCJT+-imra>GKhL%cGm$(9%m+`(1g@i5egF8MHEAW>PXT86hvQScDq--DA>GBafz5C zxCg&jC0U7(NM!jG-0acm{~r!LI63&D9r&HTNt7F*qD_So#5(8$;gSI{l2bYjm^h5}2q^1m} zM0#*gqHzAG>A$)ohM15GbI?P&5Dddm49D;}c7zPi5DnAt=)Uj?lMQT!Z}j3&4z*-M z4#L=o#QD~1MBva|`a%z3gqPG%5M^ygU}LQ~Dp*{vcq&gJ#;yYmDwY&N7{aeN`mJn` zeN!mqNXsnSj%h`B7^2F z=uAnF60GZ#LXc=H7%r+Ox(+kK5rbx~+?vQ1R%{nNkTN2tvg`sK!_K3$=qajhVXBO@ zoN5ZNvAJLc;-1kUr3qWs4yigLFW_;@^33*N5%|1IGpbCu(CpHZDv4~+qOhsBO5)7I zO|5RR9kJ#CA@0Gf$Zj?d_)M(l+D;76X9@5~9o_5d@JQ0+E_M!*Cu1ZUuW<-fqP*ya zf+R=y1jrTSYwi+9Z2%zv(@BUfj(||EGMr~1fNz{GA`?GLibyCU62azvaKS$6E@eg~mY~l6B)}D9p#Wg6zvO3T zo-Y{-BQY0KByaM{bSqz4#^PEdE#d|yWTvZRQIrxax7zIqtnuGct}`05-Y{qrQtlf? zku{x3LBz?jR;(Fm2^qe{vb?1jV#o>TPAtYNVsM7CB!J#13a8w#1>a)Z1Ylxv$Qo@X zZXhiPO-tXj41%uW6%U8*=%#yM?k&61C)r3t9C2*`D9&Kw6y}lxDuFjq(IaZ1yJmCy zToUc9D8t4nBKq+c3d;C|)8?ep-375&>y!a;j8R&4#KH;HYSt zPx*MvkXCU5eXr$6k|sofD%A2VPUihSE#s766ne}%mvjp2P@--K11WI#Ad;5iQ2;Hl zN{{nOEfAGz5dTi_IcU)MJSqdx%Ap3SFvL_#LzKO^@xLBX1|4QgACQw!a84r>Pwh!Z zWHBxM1e-)}>M-O%^OR5v6`pb=7!M^4sA2(`)KOU_E*+JD*w9cb)lxn4Q8SfN2lY|~ zu|xW_Nu7ib>GYNyL`qGhCLo1WJGBd)^iDbeFkq_0#`G~0WceE7R+s5DIkf{=4z7U9 z?=-beBJ~qn@gnQA?C(Y@9q%%Kp$YXwnsUp|xO-5fhNY$nm+Nt|n?y5fu~T#IDvlV~+mn zTbs*mR>-}0Ar&f%2+A|n45N=+p*b5vAIniCy&`EbsAXk)ez6(lhKH*mB@sVhTCMOTgpLgzYP&t>yT$rj7! zHVFx9uw;>C%LukueN-0qMe?)Yp9(k8>4fWiI%He1h5A-7y+6E-t;gtUzrX_aq7RX)~JI9exSM`Li43{0T| zJHBjV%_831YF+)a?2ghty8{w~Y^Lzha3-sH!Ag%34jewTTTwm^dqGzqO8PnYBBM)P9E)t-yroNn{1 zG$RTRF|-N;K2k8@F602sFgucX>+5L*6DH&rZKrfDfOgqRs?@~Eaei_6VsIO(VRtBq zYNs=Kl^29~R$p?GH@1tBoSlQgND^%mk6;$k8YF=oU=$Lf05I%s ziY#RgZiAKLj|FHi(=KwB)@DX)M{hSFgaLen)06;pG47Wyey4Na!g`-La}yu|Lid2i z4#YeVZn{n79?65R5obUPpdMI|FCc{_*m^o7bxeWKIMYI7a}efJ6NcuW4skh_QmpQa zbQ9@~8aF2FZZY)$m%#_L5UZ{KL_u<7>gQA?a&mQs*2%e`0+R5zk!K<;hJhW6_xz@Y zi?JwG731HgM$FC!L~u97oZ;dw5P9st5MVSg@?aOXcbhpws`5Y>DlO4ssw^nWz<%Km z4iWvr7%o+WUEww;uS{sQsF1xe6=w1R#9$V7VHfo5AQmp9_qK(@xxC%wQ`_(Q=zAo% zyVKwf!JXpn?pEANp*R!^5ZqmgySux)7AWp+r7aX_v+4S-wSIeMpM7S|lat9`kVz)< zzCYJ}y^591@r9HK>8~UGrmXx5LwZ!(M0{BqAsQ0W78&ns?{FMcYKWPlgM=`g*bA7pO`n3g>RBtV+`4=IvG8M9CekFJL4imd`o0LA}Csz@?@n|oM;^> zC<|f}X+&=Qu})zeT113iZzse`*xV~Y*B*Q!ozh$ zQk+-X&Qrp>MKndcMB%Hfn`ZM_WHXAOB&1*OZ9}unNQGW}5C=FG;ACvn%+AB7?Vre_*;LY{SyWIx|7>mq&{C>ILrdgKS&Sj5`UvNzl)Ec(`&1xe{5I2X0IK#9gAZWs3_UU4}Ue%-H#!hF_A~IWv<&VQGZ*k9#awTtHd0C>%yx+ zK4oGrp-Z*?O^~UmFjqRI@p6*y6sbPU5_{W6sA%%sF4a!AJ=k=?^$qiC=QEDRdlozc zTYdhJqO5|I!H{go^m>D5Bz{n)h~uHrg2D$M1$`?{{#s5NS6L|!&_GN8K8FG|Zhz(+ z`Iddx2Xlvfv%9i!mb;b5kdg=KVunFb@PYgO@le7{$ey8T!LeE59zLK|4lTQ>X>*l9 zUknsnj@im9#WZ%ktOz+0XJNKYh~hEl~cP7Mm4 zDQGY;Tw9Ol^!@g;U*F=`L5W>n93Q@BZ~9zD_!l*AhoGt}Azvjb7g&DHbtlE>>b&-E zQuA5R+HS7P2AS}roU-B(>&(0H;KIMov5SCOezESx5%ULZicweQ0R;l-NdXpnG#0D& zl)goaGJ7`Z{H7>(rQhIw5`~L8PL6u=Cy{115{G^|ov1vW5I-6dqY|`=p|&7&yQ9e` zt@zwnJQF$9)RU)0MBIFk8RZ+_Y1u!?WN86yk*N z`HtW6Zicl(QnysJb!~$3{tZnx6;9)Tms?wx)vBPsoXLU1mfs$lKRKst8UG{eXA7_8 zAUuv`PebL*q z;?GZ2fl7Qk%R?Sd7?V$lhjn#lLY+`FKd5mOpf%#q16Ly4+TP?Rfh0avshPXl?s z<^3dBT3kGt%4$>zmItW~5P+bLU%Mmm3IG|HZGuTMY&a6=LUa@4mGdQwCCb@C0jRu{ z+KGZyUsC3i_3B^ew?ecFlV8aRI2H8j4+EEL7NTvD0rGb?NZC_^;Ufv2VPBhc$I@2N zlFYXnKY7H>T%^`*M0qXD);xJL8V*KnDP9XFH<+}?aZvVl3%J>l#OjeN9Rl6tr(G>P zSCoFduk0i4p>+aeIUa2ly>=q1Nc-*HaP*iB#=V`U94;C%J~wD}M_tlcG07#9Pq`kE zskEXcx=~!C45u)N=_;Ld6Rv>>pB62E@c|CDgl*h0wOo0@>&hXT1ToPZ$><}%g2>Es z!^tucw+#8)JP=dhbO*|4cV<2*9EG_+U(m8pGJ>&9diKBaWV1S-GH^hVyFhPx$LP#>a zLXXmku){q#u6gjzsyB?*S=%3__=8I?ifxV?eW_*8G{*8K!@yxdPOABz61k*01 z(&x_co2M}@VknKuMLD|3Q`K-XLz^tt0XV4S+_LRjs;SaLTan1WaU#o87WlEfLM2+O zfiOa31(Jp6wBb1Lo6>W61pZj>^H~7_Cb^{Vr$3BQ2BLF~<@k}X<@vouMz~35%}-fXs7vD`oH1r19w1(@u|q z&+C5e#g)Z;Wjg($fKnjr?03svGG9qp)4~Rpbsv^-jQ7ia0-_0CT~=<1x?W{A zlE)yt&wz^kgfHn&MoG4tLsv}-n7r4S?3Vq#;xJ3d@-4a-QS6bVKmS>xhg%!>MF)Lo zK!G0A5Eqn2w>jtaZar<*Sc|xr7}dG^5$rxlJk*rpk^pA4Izx4nO!NWsCkmk^kL#Cd ztWCxspJ{m1wy@nHbLg%mqd_tEv2Q6*;;{@6TktL4^0M0)bV!V&RgI6bG^(ImRjbk` zTQX{fH1O7D8r3c)O=k04hz>39)wzT+_IcFNPbeo-#-C2o-3F=gMIkFoCK6J+jv=G$ z!6t`)i!x^1^9GPGAfiji$#q*hVe%9U@1V?;1Czkj@LH@sb}U;eH7q1xD^(F~c3^Ek z;XNrOM{mD~omg@rE?2PF0zkB9IEF^dTwYX;Cz-(;Cw+t1X3uIYRDq&dXRxb;1p|6G z-P>?VwGXz!^vTpL?_m(M&%}^ZbC~NryT#)WMfb6skY(V7%RnKSW{NdvZHfG-9|Az3 z^H7{CSM;dVE-6DP`KpQx*ZC=EvFMbf<5*4bPF z(WG`iRp&Rc)qaSNHr`*>ZJ)Mg|DA=lb*kvqxm;80QJ1}5W$D$mc~$Gxhqev9jq>VV zU19f`i{AcT>D6=TT<7=RarR?IGd z#d{!YizD<6`W~{9_aF{m0UBe>9=fCV5V`7mYhm<#>}c;{+S2p!5cPfhD(?|i7f$8a z(|zJ8?@=DH+X%;)1M&;+vCAIL_(1eSAliCIH$`J&e9R#&yzYduOJnlq&Cw%CpGoc7 z#soIj?L!BjDdX$L^gi@sP~Xpb1Hz`vxtL>~DxVpryqXl=p9TvBs?55zO}RHBEpT3! z+1V|Xn15bQhMts6SnqNO`H%HPSbP^yN8NJcs2fxjS1bmSjSaY#M=dF#q_bdNv+TDR z=jzeE%Xailqb{dIs*~4sZdczWP3$YiL_Emm9v7s(5fOC`v{(ZsI%f!+lmX4aPf-pwo{Q=+E2c5JLk;&KkY8CbyP?9S#w$b* zQOfCJ_*sqF(vw**+~_KL<<2wwZnB#^*BAh_qWev!}D13tZ8-OSg{@06aPn#E?9=bVZk$4kH?$;&oOV@M{!wV5la|6;J z(^jJQn1^+uJg!g?#3bY1YqHTV-)!Fev4({{rRfYt*QUidYSH{WG0Wwumw0PUWGK;L z)w4qI0S4F;4}G!aucRn@-)Okw2g?}rdL7e(P^Z2u5T!iG9|`&i?KB7rIXIjk7*9T! zpj?Umi+-upyQmg@gwx)kzdJ>o{>rT@f3ZLjw zK|%%BtbvDQrxJmduudPw>;>JFKmvFWDIAz#@4RD?Fn8Ma5Wh7~j{zQhY6idi2fo-~ zq1rvYlQBmg0DrcY2%PID-ttgeNON^)8C*P4^EeN>kpcLmD{rQGs zP>eX%#)!C}W|x9p7^H4r9#zH2Kt+p&*MJiX69F1PD;9@Rc2c7ax_)G_6#fm5Q>aci zU=sz~EQ8vBUeoekN>ASlLQVt#YIujqw9sxPO5^CDqQ1im&_*lgL_oJdWk~_L4-1Kc zy^P;!U1@Ptm&aWolLHSwex?cYEhf?r2_p{$guMHNc}H5DPxTS`OkbF;c5|ppF)F^s+6!IeXEb!z~d(1!Ex{)GvTI<$jOs55=UpBRB;ry z{J?NArp(&mM3F>Dy|1`7tY^Hczb=qSW`$}+i~h}yyA?NZ20~8ng(K_AMs9@{LG8S5 z5&Ld9`bRn%Z(}01fr4cb3EREqAG=_q2~rF@f<$vDk_%%r=2>)Oy3CZVp8=!221!b) zJF4j!VFnOA+mc$YK_Q`p_f$0(3x)2!g* zr-vPAj=(8^clOEt6o%fU5Jo15j*XBKZ6!)kpXn-#`dbs{x(JNQn>j}Uq@p!wJXQIb zPSFJ;(gLFRH1v8^+cyR|29AVcz*SoX-aRr+Vz*R1K|61%9+lHRd1Hwv@mo||BnxE{ zxoj#tCN_%by6qPpnvWiMFxI(n!LinYc_YQp#JaUNoofVrD7iQe1g>YwYhoIn;j;(aHnjv{h`ple82|-tp>ONG16FFou^Z;2+nY zc>H|KU7)+BNOzd5XOZboSQU2`f{RmiCFMzyq~(3!&JPIZ&|yrLep#kU96j zBl}SkX!vOHSI+GMgk9<_qyxZHI#p@60Y5>=5|S@GG{AU;v%3WB%oqyDHP z^(Lh(F{%=auY!Lq)!8>$4%wrL2^Yx#TEQAo@=+%c1y~$O7|ydCO!gtMqccFcGRwiB zJUgm-!XYS?DNRcc#%qww`xfABhc_wbsEv&Rpe3{^E)BT@AnT*3l^|hpIx31s1{j8L zE4oqR`6KOzYhl-Z$$B3SUx$-eP!UxJ(^^>$Ujtlt-Aj>=}XNVPZIrZW-!f1YbvzQ^RZ(2t+ot!uq~*Funi-IVM<4$)A%SA z!A$A;+B2FMq7q624xn3su9N6qFl{bHL43MV<-uKb4nrfG6wo+WHSU@7S|OeY=EG8# z@xvku*AivFCKKL%ZgDYGe4C5vS}{6kKhN>*W9~N_>jJ=_Yd>Z5fM{#)&~rxuiIf_! zPF{z)(6PXmHp2Ql!VMMSd=a8)KD->Ks@xz2r)HRa0wNVotr@n)k1RZ&puP@5l4t{h zk;9@YTt(Thgtvw4B-6v!2#t`RVY-hc^^;;TdhyNYJ=NJCDwaOwwyJu;b`dY+ghmHR zRfWA>Bj{lsRfLSFT#j!17@p4RRlFPZ@HhJqQ#mFy<~KbSVKo+rJ`N2V3sxQv6EO)j zE+Qn+`@>&8a2y_hK9N{$lb(uf;ZyScW2FfRi1`%1o|qvVeUcVDj+=q2G|#JiTADag zTmAaidsj8vBsB*EZM%2q^dqgRRvvJV1SiY1y*@jMs3;s z^ao5Q%aU%e%8Z>(+f@L8{hjkZxI2fCDf6SOsLQ8puRzvF2>%6#RB+3U zcsO1>w^H(olZuH_d#$m7D6lRV9q_%5A_y{C!B9`{;pH2k&nCz;IT^41_RyHt{YpeZ z5*UI^${Bk2GmXAY;iw8wogD|9B>;;!vBRM^?5f1=f1|V8&&RycGo7XSu9Zz3dn?t*2E3n z1-NZ>>*(0cb)C+aFN**AC?rpC>(E<5~{jcRuGU1{i3QW8=s=;tZOvQi$|TIvon? z>9`tp`jip;NmI07sk@0CR?VE8h_TS8JwWwR8fM!CP&Y%T|>mPEmdp7QN1_yM*$)CckC{gXcMl z#~XF3oJu?NqNYb?Z-9^O-sfJt^Vjg74_}`czOF$)_{_Cwd-ePJ7VC|X_~;cK7)+ow z^6qI_Blnin#i(KWi7i%f<6dTP>lN*;Hdm?Wpky&+CYr;r4cxZ4_F_bvJRd!^D_ zn>Jmg@q0FdX*_1^4D+*tL?X4XQws`?c*9J52l+0a-s&lp^=1%;6(O7c>E)0AQRM`+ ztZ)W}oQW`f7=_RuC}csPtb$*QU(<{}2d;$YTw9FgRC|@VgBnJt>p-akg8;L;+2)N5 z=3U^<*c=s-Us_5H6WPj@;vW>5ky~=4eHpniY$E>-C%wK(QMAJfDHAaIn=%KLwPLF0 z6ehErcq0Bx7yZ#{;2G0Z(A-lK%VQt~1_i^HCkV&8WaQb~nhH<@;uE>KnIR{?SPMLd zd)Pj0s!F;(j1XFzM=$XN3V+U7bP&Zw$|0g9^`(#0tdT@iEUSI|US7xcn(@Zd!JCP9 z9I{yJ)5GL=&dDg=O`1``?Bcy5{SM+mx0)sk=Xeg^9H1{S4Jy0hA%Od8P$E=)PZDpC zS;3sKKy$#kyD(Dm)-E$1jVJ`=kZvAFCJ8|IVn`QkNgv#Zn+7J)I#$l&b>1G%)H+ek z6AkzSAk{uqFO-R;lu>j?kRcN1JWS+u&!nN|Wk;VTh{~_l!`?zR{xF>gA2KD6LJGN! zEH_)A_DH!a|I%oHOrwZAQuQv+8t6=i`|%R*l~H?N3FuN^W*detBIW5AT&V8lZXtAX zf~$sCo^yb4)8(zE)$*mgQzQ_i9vaK&AyiJ${^|6!#d&8m*XY4zyC;C1fD!5P${NWm zgoiajeBY=HSKw5i%K|n5TLm8*dr6|`9Tz}XwM!Y#VvUdcSg#g0GTnHQ8ds18i5g*&ZeX&Ax=Z(6lM4S1m|Iow zq_{~{_-2?}P4wZgN$o8Zz@sjKMAtkY400;0z@TxXooPn&9Ue@xObYJPTa-jo=F0^y zW1?p;mnYHvDPoLYQxr<3mn*A2OVwjeq$*&N+(X5qz~8S^hMw&o{d(&wePwRI~#AzkxxhG5AGScDPTv|B{Hp%iErguz_n>tf!< zu#DwDZhsg5_VsvG2yGGjU9uXco>i)zO^0>5S@^n2B$I51O*TfHplz<#afj{a=bVni z>xh-|w*jJ?rgb{uCY z>vo&Emlau<;y>wjpR1WQUYL_(==r#mI`i^zMbEa!V@uOZ)N|dojDlg!aZJ=}%cO6s z`_vOj%zKYBeNzNg6`K(OH{7<@=cMm?t5m8VBpxO7>9l|Qr0t~ld9x7e8P1p_cP^Xs zef*E@k5${gfQOxkH#_?QYG%8<2RT25%+KQc0-;bOa~Ooj`d>$fNK9R8ogvn6n9lGb zE=It8WA{E--T4rrVRJ-Y;>6N*7dQjEp>kgWbPkPTUQF@@i;PAI!q)jP4zR^kNJs*i zxrMR}wZc#NMs!XXC4K41AtK5QrW=9#hEab#Yf+*KhoCc3@f&xlGh zqwiGGIz0c=ku7HDQvpg@iOh@T7&bvU+EV*xl?^JUX>% zwmt#{kL=aMo_Ic^0?MUi95f?zmxpI=<7>4Yv`ZW~GgMb9^<+$R8aj1q6(JRJJrk9k zLb{EGa#dnDjs`28U(!jj8SSkdjn|)6-lJ4$yNx&+pH&Wa9k0|`Z8<9UuCMkH5>^E} zS1Ip0=#NqpG%Qjj0f;vAC$GOZ+|W8%^E$3K9p7rJF;7_W3xAC@P0Na+b9Rt%_?iRJ zZK*?Gbo{Vyxc22|UL!`8vj>^_<~8(7Th$e-dKzex?&VASR+{^elio0Vb?D}nH zNqa^RZ|CY(Sy-01NrpaA*G_I(ak~-jO~`6@g$Pql>2AnX-fGX2G^1;)@b-D{YHwxn zZfTaU*&jmf-roqWPO)FNVTjuEzR}eO6I`kSmb?bA#B4&TL@eALRtNA$LAZMLrm8k; zLysmm5i&887+pXN8{)f$%Ph;5)W_kU7snPZEyGCKcyU3(lc?SaJ2sjtMg)~aG1j{L zUsdBOaNtz^?+#gu?jBXB5GOfC=&iXVCR}(qKd*VFa`2vL_1w`T>0rx=T4RG$89z(1ITQx{Dl$psF4c@p;lo2)$`p&qb8d`Q z*(K&0(8Ntu!#MtdJrcXIW~Piwg-Q?%9*V4=#HCFnm%#4I4)3YtWuMu<QLNp{hK75|ou|Q2z08*Zvj^cpw!~iW#{!-v z!}sfNnn%DX!IL5AXl+%_d<$P5l%XaKWROKKEp1Mx+hd%!=~;;&bZl4a`N6+uq(ATS zwd(5e2i32Qy#k($itoQ~F88P)wrK=iI1JlO(B|8WyDlP_z>6hGidAl^!8elE&E^aD zn;?ZQ%-zmPYBeed$}d056yUGICe-bx-yMJ+&FJyYv6JlTLX&T|Pc5M)2kv=-6}{jL z9V_Cwp;2F^5?Ak& zOw=V}#v1|q2om*#!O4+I8XXw4kWCh>YI7`C16*7b9*Zdnm4RN;7yi4AaN@`=Fi_~B zUaa3Irro>dYGMPn0El=_n-bAsyVzwVlqdln%{ z5NV0dm(Q6jzHjxjl3I(V4*)YWgqC}t%hzufo1Tp!N_VqZ$s2DpWDaMJucJ^?_bNmR z_3@yngal#Fty;R8m-dl#wCI|!r!1MJDb_ER#65cwaCT)X`$bdFMa@?dG6 zoW6p-MJ2^ZiS8_Hky$iMD5JEZau)+On}BftG?!94{#0#tChxCMYCMskW~Imv(j`!H z)vA~Huwemyn4U2AaTB~)J_S@vjft$Fw&5uB+vdot99paDzQAg=HWUS5G$(RrH)iKs z#hkR%WEt`WA(R#&WY#`1=5|%-o;Qxz66F9?a&kd@E;T1rrR8S8{#d8~*c0oN&+tf@ z61lR&1WUD|;$CpZx3`>!!pDxiVPxsN3gV{bLYi3qEpCyVu`kjuRBs`q zuiplM<#{9xchvI+1?x(A*K;II3C*0s5Fr~OyLH15DFqiVA;`Ld&tr+dWsW;_>6&v% zuth6bW67doNmvKOM}e=ay(qF3!W`_=EuyA}zq*pW;>!UwYcG9O0|d3f;Ioby$ni zvi3S_WHOk~frJNpWxzEP{k&M;=3owDQXeV0lBxq2Gs4-WFJ>S(LJpPDR%c`4&&GYg z%hpJKh0*#$E8C>0eGogdt+Son&=3$Q zE!nbhn!0^fvK_p>bzri6$=i5-w0-?!`(`GYq22U~&TRb7n8p@%=ey3%kJRnYiXrzN zxHKTRk!&~)ww-60EOa)>y!@R%BRf!0Qs|YM+ksiLN#A$)-I=dJ1^T;<_U6fJLCBwW z8(1}RPR$;3>i}O^6>-0+I|gGR?ya|%=M(SI37N;m?EFfN41)Q$J#PHh9!Gt($3x-5 z;k6oH?QwibCIV`vzu5Ty#*F_3#%0|8amD}5iE|3STH^GQ0t~ENfAMhJ)W34LcHk=; z&Z6l2j~V`|g}(~nuRQpx4K66F^$!XD>Vf~mfd2)+9TNX>zyD=@%lw!4R(6PbCBD_9 z+5a)U|5DzD7O!ykf7R}Pr0!Ry+s5I;tJLjisq&ZUp8Wo5bpMy;es#J35xHL>?pJgB zA8`AX+J1GmUy<#`Rn;}MnQ(QDP0cN>ZS5VM zUEMvsef@b21H&VuW8)K(Q`0lE6XJ0TbIU7VR@c_QZftgo@9ggFB){1@J~=%*zqq_A z*uP!cJi7kz^WpK=(=7hCh(FJ8sDJHkiQwPGHHBXBdjk>JG>Rz#)mno=m@?Km|Jd7T z8mH|IniC2Cr@bxUp))y(<;_^Doc6 zou0WqXhHJlcAz}M9|F7GUi_GXxk!h$zajAd2o z^5gM&_#~D|+PDlybjq@aw8m<(`jIhZ0w|^QK+4E*d7aWPUC(CtvDZ9dT$7$jBtGHg`?CY-E&PJE! zyDyF$vd5hV{XH*VS?KJO!O<${zX{QAx@W&O)eWF~MT453dRK3UTg8;^p`vb);&-Fb zh9Z#u&F|*$W3+l_oHai$)xS;BpZLY&u#Uy-O$hU9ghak&|K*yf=JLxl z)vWfHVdnem4*lFfb|Jljc$X)glFzlB+7&Fd1LvkKanS%`IUWR-=Aquy%U0k5>>Sl8 zCc%xYpaq0*Fi^Ey4Nbm58V&{joA0+Q4k#WzKs~VYhnO=}4}t16$^FewTUyGKr z_gk!AOaIeh8y56MK2;6+`+PDU`bS`W4hqG&djo@{j}3cl50AHw1&d)8oQNs>T$`W-yL0AHzlIPFvE~myfXj ziHR~rf9KcFILeh45^bYgw4_=-$~PGu;}SzdKhHSEzSt0F&`m>GRX!#v+YtZBi5du> zIV^$O2niRl!p5r@r{ro(6!fR1b!41S7`-*CZ7il293EBib%SE3zR;pbF)3mvHKve@ z&@oe$PwL>-r`7_1tnViJ3>I+%Tf6CgyHzMX-ZiElJkxy>oP{c6kN#_oz`4S zA7}kbPYan;<}{kJRzhj`&?;t}sT;GmjcEmj&c;6^aVH&%FwiYk%qlfD<+63ti(tq# z`S^a&&F?Q0%|5LSxW)_nqf8|Z|6hAsl4x>1Tcr+h}}M6*l)0%!okF01?Kd^K#B-RAk z_i?0xVc|9_SI5H(8po~5IV+Ig?lz}F0j(88lk@Fp2G>zsi8ak ziu}bv5~oqdS*bG%z2o#}TR%pvYlPC~KIwn$?PH;bj?P22k7Y_*=l^4GU;oG6-r&G= zitaXC8o&zg4@vz2!y~!7c9Vv?li|FJeEM4&XL>4_qrnSX1UUrVuRaLpyJe`71l*FW zI2dCs3}Czv1cm2mO>0M{LPS$13n8RWr|kB!=T;}tBkiZ_f%L<|ZG&*o(I?~b*O>T` zVc}No2vFxv01W16(PSybMBGEBy5g!m`_Q z(qp17t!?^<-S5|w`LR_7)qWLyG6{hIvAcAFh@pGE!HoPbS4Ov6D@;3YTvV}9@C`aD z**M88LMEmB#l;8<{L?%{wYiXJ=2TYrX(1u5xtKxaOws6RF=Mv5lz-+-)$eI3AG4)g zPUT!P`)RpcwWU&T=3KYyX{A1{rP@~I!f@m1OZ#j~t=G(j>GRWSKW1xvgvzBQ+4I`C zYHMRUbmr1V_<4OkuC=*L<;ub6`Rm$jYirxgm5bl=#x7=C`ddw0 z{S(Y}TwC{v%8l>F^Y-IxTkpfn&8O$*9T>gQ>>``vpeev0V%r1(u4yto=w(k?>&>t= z^@l?)aktTI@1l?Dp(Ktk2SBWjaXHm*N!c%ljA|W|db8isx?YYz@q*q8toK9gNUg@2!^$NM&k0)JG#ck0#k3Lyksp?i;Q zz^dkltTZ)*G&mLkjK-VRtM(3B-VGhN#CL6;s6KRV{Jz%V5a9hrLs;i%gyPK+o`mvx z_R-H$bSu~qyhk+mI4b<-E+M}AkU{O&2E#@t_g``4`BpZk2Qo>Musr^W0)-%(bC z&riC2H9-YGO4NauwrbBFz`CF9b3NBybI;q)+q%Dhi_#CXzwDDiAIH^tzopmt@M-1# zS~Lo}EKvJ>ZUlW=o9q4AHuwA55Bl77B#F^~1EETQzMQJ{J+03D`Pl{ieaj){e4qw> z+JOFfoa_7ZFbDnf428n1Y2l}!wsAXw34@UB0w807`|vm#^Y6o3QKHg=@b!ZUy@QD} zf=Szh$=8A@AA^C!A=CmPw2yvEasj`nV2eWhnHZ6O0DRd5)H}RF)*eFWyh8;tVnc=6 zLq*m?#U4Y&iNTTrU}^n-;oBKt#dfgr8d&uatWF%JDG;WuAExUark@dJ*dAuQ7H0Yw zW=rrz7Vh#G?n)ftCJ^yaKf=>H!aF0vw>`ptE#lK-L=Z6~ zWKCm{7ZUCbfp`mqWk6!qAaRcn#r=S2?||z)zu^BA!p+}8fO##1Shxsyt)|yPKr^Dn zz$3-WtQDO!V|jtX!{+!OSke!oc)TX0Qr~vMRc;aS3v;&~j*cf!^0~@(OdYa&htS za*gTp2#WEfKfD$QzW_hKeVRbPfFK{=-x`Tr6B6JT5|b1%0}J!e3cr;Xc`Xw@fq&ve zOxxkD_*-!?G0C^$l2S5~a_W)}iBhtP(q82IxM^RBlQAJbH=97wswu(cPs+NIz1yn;tSwl@-LtRZ%*HF_VOVhXLZ`oBnD zlzi8f2I?9Z>qhqKDS=*7M_*T0KXuMPPv1aa&mjNApyu~K@nd9eYMirSVqtCi&fdK5 z!ot+l!p!`&g)Fs&EUg_Zdw@Cl?28S_eyY2kUpQUF2*e@Y+T1-F^P1k*BYx zkH42+u#cyQkC(TvyS=Y}fS-esUqDEJ4R1hD;3qboPr+fqpFV{K28D)4y|z<0Gh29M zOr!%=WLQ{abZq3zSyXINw5LYAt3Z5GYEo)eT4ru$PF{9iVO~K=K~ZT@q(M>XYh~4x zhs#u^zJG14svzX5+Q!<3=7#3h#+Hs&I_B1n?lvy|wlv-L4Egq4$IkAauHJ#(f#Jd7 z(c#gF(TSZ-QR7~du^J% zKHt|`JLq&cIypNzJAci#%MqWeo4cD{l$*P6_uqf~eEjt=>hLfd_&oFR_nyHYV{_>1 zZ}|WF3mFQdgF-GppE3}NfJbdS?wT?bkwdMRlTlGP5>3ovzuDd}pB)dBib7v!#4Ao@ z)KGOAZ!DQg2fdrjQD`ci$rf-w-~3l0iHD+6DB>4tK(aNN5jq;nm&#BhJi#B!&p%h` zH#uxYl~t{lm=8pe7!_BqH`#?T8Z^m9erB?qEEb#cZ~_le+k>gkYJI)GpC9ipcINu}UO!bt#@!$Q;cFow)0gjtp!1#XhGNSy?t$_D zl$jEq#umuYmGdr-`&?uK_aU@tcKeZxrRDoktgWZ}(V$VrgBYGKb_cNn$K?lcB0o8h}4ilsa?X41YQaD$YBN99glhtLJj#9LB?T=FRZ7Pn^j6DSpiIoPItq57Y?T<6< zN-K`DoLbM0vt4~N58rSE$lK(2A6J~@`TsmS`5d$_Sf-?k=x|y9p|3nGjNxN0BPQpw zGWGkUXkJm2VN-cln&Ww19$reqqZ?(6{O+8|IAz_ExZ?Yw60>^@ba!7Eu&fvai{HOC zBHV45I0)a5cyU%gOz3#oF#a?SC`RX1u#fzJeXlCK+FP=~o+6_>X$XNlW<_1T;yrzm6*S^LWWM@IE1; z(qZnSv8FQ&j9yk1!5kkHm!0_RQRm_KtdBF)`Imv;holg*#52Zh2$VS#(rm%8N7mDj z=09v7|E$^H3F@PHgd{#Leqe1+h*M&*JskXVMKN|0{nB$(7|8mqL1XH7Gw~4O%kA5j z%%Fhg_BZtM!(z!Ak^ww+VSijDQ1JTq9C0%SpXH^+VUFQEDT>X`>E+Sz!63qNmXYUA z4dOVFmSJYuW(Wa5iQm60d<3QoFk$Hs9Qz)Kv7%4cvsc_pLg>h+puE23T!gW z-f~@~YwY)&u^7Gi(gfY3$YnB;Xne!cN3@{G%JsNFEb!-3Y1(LGP>5R%2$p^~9a2Vd zYaerKc*b`tF1v|irPd_{7n?qUZs{tG(k%TXW(S61P{)1?rtx*qKzl2$8P*^VhB1-v zgZdQ4(cvEuZH#wPWPM91=h5ne`5A7j@E)?_D~F@@-fewhe`88l&-%q zTczwoLIqY#35XV|OoXB~9*=P6NOh7Y*vYk3kZI-)%jVFKhZv`BJ-2CtM=UO=Xug0O zu6)e%iy@c@7uSIY`-C5B7fE;&kBCQmr$oR98t!>)i5i$<&XiNZYg+m(A6cb5Q49_z zzFkwgEQqef%7J&W}e**ZyBR2cb!43i&!?a;g zB;hV=0g%vZDbUR|Fiy*Yh)w{xtN=R+GCbmlxDI$RsRv7XI1!vPMI)1bsptifsNO(A z%M9J(4OD6z^rERTcr8WfNrt4To>tDGCCGjXED>|+hduP4Qe@;=cD~5!tIHv_G`>iM zAqg9;Ps${LsjK#&`}+FJC`)`8qz`|g<2B~cND&~C zvuuL&qTSW0pA+c=Iucshnky);CLn$KkYbV#37>y$_Rf6#Q2qZSY)jRFfhx!vy``0x8xRTVVIUfz4N~7QB`g5IM+G;(3 z^1m<4vAWjARDU*RQ@mn_zF+Ofe)_ijzH*89Zy|Zlb!|V)KJ-5Sz6rwW-bGS-93=a5 z3sLLde+{JW&|O6A_UFsKb- z2lw60N6H*AlAfA70ZBgr$;pdW6$Ieg2Vm&KSQBIG z<rYKu$SC&YM~{D9mIY>FX-2oOo2C96VNOpx&I&m_}V<=-1l1n{8HB>(a2t<;Gfg=J&owdawW+IW> z!-3ae3D#hR4Uk@!@Wctg>%+hk1R+W!z)8r5b&MhD`^30mhj*?ag#0f6Q$Vc0mWrZh zka)O{4haNW2$ft&lH-_rI7yaE`2k;vlvpr`=BNSX2m=nF0%l2)E_jlFSzjsHS}S=O zc<>f3Ns}sqji{%UZr}!9(0w*}llho>+sGnyaE9&}l`}ApM9Gk4B9!fD22VHyUFk8I z*^N|Cjynkh9{HJdnU!1V1ZbcFoL7LbB2>_q@W`55_%myGg3KrfWT}f6;E-F; zjl77IY>1PAK$KJ&ESX7yfq9*%G?v>83?P%gi+a>H(&!EfOL3+B3d~j zv}v0r@GfFFBB}Uz;5Y$Y=$o&|guy8y!-K*%c+|h+Kf1<0{7XFB0`_m ziJdf>L)vK~&DNdYnV38HeddXtsHdJ~(w>&7m?pxPSWp3c0FJGBdC%gAN7#)bNLM@n z0e1-jD0+_t+M4_cdrCGUD%y^lx0730A{QEx2HBf1`kNlwiXjSwtk{bgm4cX* zaaoS(C?5=3qkf7|H(DY&>Ydlen=d(@2Kb}hC!`JkK%}Vmo+=WAVF(6t;5exW1_;>( z5NeKKkeo=0kb<*{W|*M_3Y?pVlkh^IKZvT!S%O^|dV9*77)hI9cmzv`2AFE6rmBv& zimAj2qGc$Kgjff!YMgjktZcZDL9m3Yx06Nrr_gFjfl4BSx}(*2s4zmFrl+Xs344yJ zdPd5EX_$;9AS)s01-?Qe9GI>RpoHl9i&S8SlV}1|Fariy8|Cf@q<_ z7_BCYL(`f%D`0(u3ZCMbt%+)pKze$N`mLt_2dOktp(&!XJWH`T+p{jBp*sS!Bto<& zd$dkAlG4bs)`}u8tA5&AdfXbM-s+y=inLgpwalipD#^5knuFL1wJ|FK2{5y!C!`W! zvwnEBTHCg6%OxqxIU)cBbXy2bi-KY6B2i0v1o~i1ToW3p8vAx4MLgjy-mVKG1>n!BydQ%JRRtq2)_a_g zzPtM)i}1ck=DhlABhdR1j61zOdc6oZxj4!R@ZqGrBdFoK9zI~eEOH5#V+p8JO4YPp z52YTwR|XL5N=-1lEYQKZ_ZlB8X_hC!3jC#y6M?1^zGGm(3+%zmqAD271TtI&sAU8{ z^}@AA!6Tf1`x}F%D;}H6qk1U7JV?NT3N{Gb5xbiO+)GtC_C^lef8#4FPwc%hu)V_T zfK#l!9HDzA7BWJ{y^A2jl>l3rMqy8k1F}K~mrxR$R3;`I#gj0{C=$jZ7RJ^k2psZS zJYdDm)Cl!k1!t^CQTD_FM98H7!Nyt;$RLJ3iqu^?W(h!q$TRxGg&V|JF~rxJy)9s+ zMm)gTyAe|s3FBqT8XyLyK*Q_{#dLzgE@WqsjKhCyN!C=wNcJomA;)oyX_X)_eoO=N zD=S%m3T3c+r)6Gmkjf5B$hw@yBjCn|xCmuT#i_H((Tv4Xa0&Hm0wxyA#B9u}(!!F8t__|R|(5(G#Y%(yQ|Kn#k@}3&6wQ0n5zSttGZ9Cx1oH1NL;`nQzl}p#mt;z zhSSVl?90HM#+*RI9kI)t)5YxUF;(${^=y$3-NxRV(JCUs>HG&8Kqp#^&AjYm#LUgs zjL-)?(cQekA?-2FEMjv1)I1z*(BzxM=pzUz4bD8U(DyvL`Ml2nT*UwEgEoTACDS>Q z11Shi&bh22cC5@+4AFkT)Mot57G2G)^~)M91spBYUj5PDn+PJ!$5MdRC(Y6`JvC$P z)Y(ka>wG7FP{nP1VKqGlWvsjMOw~G_(@tGlhxpUs>%&2fYeT)s{LH$GdDKW!PNz_CPla3v_@XO9Y7TnQ1 zzh%&4lx^K3rokrveIlkkA{#Bt6O7#;JvqKDN7xP2e|^hufYv>|BJ3;A-RoL9;A23j*@?YzQD{^5t4*#EcKo(#&-U8G$CygCxzYeENte94|{duYDrIiluJ@B(Zez;BMd zjB>wtekJe!o#%YM=y1#2+qubrUeq#DnS)N~jDG2)>*taF+%ckvkNN4K9_pe#>ZCsE zr-({yJl1}Tmp6h1g=s2p+tFh^wtn0!)?EK5?3hWVf z3+yE>>%`vdvu^90?(EV&?Wtqz)avU^qU?Cf?9|@v$_?$o?(O1E>}uXhtUm7Oj_c)q zwHh9~4(`hi@AQ7@^1ki$p6`2p@9l2l`u^{h%kS9d?*L!$AfCbc?Bcg>@C+aD z)~>SEr|tdD?G0b?fjQI)AMF;u@mh=V*?#UEAM#l1@qZ5Tv>pK{FY@vJ@N&xz zFi>3WLSua+syx_HG7G#wPTFAe*nrFS3k^XZB;IfcteqmZz+#wl4Qvnr%ODQa01c#l ze31YP=kWGJVhLo=0Jwk*YUTsfFys{0_dCM(W#9A%_4Jz?^(zt)#k&gHEa$|`1Dtjt z2pq#Vz6xbeR!YpJvy$^D>RjIFJoD zG`u%_4e+#H%iuB8Fbh8|4ppzdw|_O>p!X?E`#}%FyJAMB$id@ZoJz1RlVkHABXOp|obr*j$V;tQ53s zybj_T$*jsjh>@_F^uuhVFKm}Yu$V-rV_f=+UK5Pd)@p1J}S8Lg5bO#m(SmZm=}YsnS6~ z4k1?YbUq@dRfmkw;Xx-pk4B|T<}Xu!yB~#^FsY3DDlr9zgG_OV3HlZhMF$fi8OSPF z%D^c&x9$?>CL=sLz!Yr+^aDXQv`K^oRH{Lc25qJhBnzh^+0efWmw^SYMF{f-mQ^gO zgs>_Q14)}!c!(t%lY-Hvj1n7(pbBvoiGhvnv>ApVa5g%!iZZ+r1*9yhLB$9oBuWt{ zx&9h~iJ(}}>85E)fh$f17|CX(t|THXENZv}$C5hTSp^504zh7d6Q4T&N0cgFH0(ys zfC+Q5_2>}ijf=Vx5olZe2LqSZ}W zj=H@t3-!!}hz&=DJ9FJjHL&#~PQ99AXblll$y%92pARs>V`y=(_Y?wXb};_y#^Ujio>1jRz7 zY5n$^{5`akc^sLq4^B9er9?P%>PxO!F&ig?d6)%gKJC<-Yv>sAqb&Apb2Mm5r3Mqb zJ)tH$A)|Jh_EH{#vI<#Ri)&-reHg5;7(B$@Xl-;O>=1Hfp-hsp_=itqX z))O>Z(4uRzAj`pA;|cx6LV_!~)nGXBmjsaQZQt{QIU!V*@$#Qjr5>u>dToAgLr!y)VftXc)1G!h)9?h)L{dQ3S`uls6Q#VT~kgydbNj z2Mt*OtTO^sSr#LLwz7opC=4hbT_DDiwMgS568MEL2B5{P$#E4BYh;}?GBE~uqY7K} zqXhbAH&*>DQgQi(sYdpY2qbWU4XhJ3E@Vi$Tm+AubXgX2;WjLONJZ5X11JB7LSP0{ zn8PF{2~`-JWBtK}iDTB{j$@u5c5V!8Fa;-=B)M<@#YYT(m;_H+@uE~61YMD6VGvW* zo)wisD`pZRg|r47KmyHdwLp@qM8O72>`4p$!e=aw=Pji;L?@!rAgLmOFqH)_N0%tr zr>Zf}+yKxPbdkot#)+3aO0fxT{E$Z`5ssFLB%Ee3UfPi139tO@DBO$%crJmClKPNH zbeIGzssa&ZEQ6ddJ48O;k_dFzVln<>K`@pfi1b{{lSHM+@+jgzFmgl?A1w$CYQZ&c z7|AaTf<-tichlG8lcD8{m`6eSBye;q8zmJBQokv+V#ZajbERutjdM)xRO}7PY=IJA z6T@ekXA+r_ka?<#M1sMmTM&vtB8(^nCpP5&Vk`hqDemi zSw>1$Hq?!|1{b4US58wa#>~=zEgySBVn)CTpV(BKya|Trt|be-Fu|zJP~c236%sA{ z<+bav#qee+k+!*HTJlN_aaUvnmykygp{*TrLsPDicmh5CVuqw}(l(O#mONd0CxtFD z3185l3d2nXGRk0y3-SU74RKX{a~faZSfaE@In{D20S%WFHwG0+?_r}Ime{N^ZE`88M0GU60had75yaL&NyJ_lORgC?}! zTGErIG@`F_=0qQE%cSaZr9J&=P={L7qu$%0zoi2VEBel) zW_7E*h~y=U+10bAb(l@y4 zXlZjK*|p|(zt?kVW93@aycW0rVnG-k136o+uqYbBpXOq-*b6QtvYPX-aq+;k_TuX# zb5{$aq_xTfaMOSuX6|yC>rLX~-YCh$)~XG!;+Yol5)Q^blhu186DH8p%FeOjF|e_` zNWtkUFDQ|zwPqX#!zlU6n7G!-LI!#n4ar)?3pi#p{V_X--MxdfahBvO8@Ba#*=K$0 z^$1*gUV^@oti}2NHK}Kt~B(rcZiBr8cd-)@c_IRgy?XKr?qo38X9eQGRqg8?>W;Y?qmSX;o5e2&O zGrMk*m-g5{&rzqGd#0Me6zcK`Z|Mq>ND2XCz{fBuNI4BWT905rh0>ZV-xvgBfSv&2 zj1>8jk-@y@nj$o+o|RArcfh>GFfBw96%!n~CIgNL$R`}=kkO)se<76ua2UHuh9mP0 zWtbWGVGTg3n#TAVR_Pz5v$E1!h9NwFH}N9xsfxvbIo@GEnfbtJIH?2pBuoOac-f;9 z^8+Liv7lfZL7@x(;Yx`LoVeqY!U9NuS;DTA@P&=&7^gsn7PL7|aEDSdKSBgG^vkqe zV>I$37=ai)Y_lw`S_lUjH$fPXgJTz*IJcFN5GK(aYO;hZ;4jaC1(9)%xEZXSiUI4& zr!gQTAc+K%=m>>K1o%0FU)Vk1L#kF$np)T~gqgO;yCyQ)3KB%Tm=GXQXe&(8z%^u( zS%8trsSF*IrIN$EBpam;Nd$n=2B;y3#<+v^3P1ArpIESk^N}Sz%!rHGl9$S(H&~i1 zpr@PqpZglO6Ql%Mh^NGRnffp)VVfemX#zSZy)w87SwNTss04I)p?Zm(TSTypD4tt# z5svVxLl`jsfGMv|ys1biiXOa3s7QwG=)CR|flv|!-jI_xF@k1jj72O&nB=oVRJ%m< z4Yzwc1t~rNVFW+;p`TiiOavjnO1K0Gq0iwVlN=LqN+ReO19IptrFhErNUMa55)liB zmXaFMK%}oZnVh+#rIx&~9oh5mD;n4C^Xn@RPn$-i>H<6w^iaga&)&Qxp& zRH%d%iKfoetfG9c13^k^l0AewJl!EAxxodp;UBBvnv-C;%)%m}feTSVtBnX78+;?H zK%IQbh3!~E0wt9~=^$tjFMkO`TvV;lm>F1rtP8Qq{%NE}(i(Nd%UkT9A@l{ev7p2( z8x9Q#WN;937?Vx$1^TQ7DM`zOVM3e>nH}R$AY2NEL5WvmMco_6S{Ov7&{48P6YjyC zWN;erh?NSQBa#BYSo8+@@CME7KKdCmOL_>!1BvoNE8g*n&`T}nWB`=#zG-+#>Ri+R zI0HCjqDi*XfSZK1KX6f~ypZ~MtN2?PnZcp`GD`6}gW9o=q!F&WDEd&> z2om)%fs-lGTJp-+!-&uF4FWX_|2l_iu!L9J9`c~Vtb!7#ikG{5ita-Tg>e>%gQXZl z!()pj8l9rMY=ldCwHT`i-arRtppq=1rOdQZ&fH5_)tIjtfI`6uwEBa`XqD_3(*KDy zMS`(dDx|&0qp)bW|6HSKyaws~3i<&yR~-ZvU>!@{qXD?W4zY-Atc%$E!Qo?5dhIjo z)U`OB(*TnXF)$CkgTzKL4L=w?`)E9Vt)|;+4>5o&PUM`Q+z3L=2~upzd$AS&NI8mW z@`SjIghpwja+#*cgNt^{1;)Gy87vVwsUj#6P~aGbAp{x{+^aIch0VaPX#zd$8-rzq zk_+ug@_-VpP$xF<%fLIJU+Bq7LuBQneU zrC^wx9Ki3Kdm1%h17t9Ji zp~&5$4L0C9Vw*3cm@y?;QULRT!(gox5t9RqTn~P*dp(5pTea#S*X3KZx|2IR%|Q_p z5H8fV>2OcL%eTH;4kn=k9L8a>L?)@JVfUoIyZPbf*x|>`IU=4iA1>k#Ug9SU;SnY{ zCMJ$xU1R8F3wl~%spc^ZeuroW9dj^ z%Wc0ehGRRvV>~WnIVRye4koGXV?Yk%CZ1di+p9w84n8i9YZGEb15Uf655KvCAI`SS zO=24^T3`DON33MxxC4&G8!E9kK^|pNHbp|VGQu$&M22EIRhLV^KcYJ}iEu2<;Tu~4 znWp;V#5*GBs8#KVPYwKyYA|2z$W#_n3l0nxW}uXm;pI|(W@tvHQ=YR_K4dk1+|Qj7 zfdDr4^FP@O6>zlWF`B!8qYmLLvaz9yU`941YcJp!=Q>V~16VG3so{8zW_;G;X-2ea zj)H4Gj%YH}%{@2ELM4K*tk>-)(tXTuUXRLAxM1582m)UJ5>3J;7}8(Rz-xgo^FpTT zI=bPMLaM>TBN2^G8J2FM@`jYy!B^pc9QDBM_R zL&mOaF3O7UD+LoV1$Q93sjlrau4=@w<`niw8VX>85xDz3TnlexkvJ8@B6II5}Nh2aDu=4KB?1+ou!vk2$M9@ufN)!u>)b~+U z4Y-q10I)B*Q3vTi1&KOr0FMX|zAUPU$6!5?wQnOrh+G}7)iKv>xRx?)O)e@%wCuqX zv~3C3V%#nq-A-hj1n2ey5URP4T7Kw;s0921#pUi`8jBmKo9>DGp6i~5$Dz?hh{FF` zsvL?ai@D{`dkdJ*(GH)`h;awyrMp`}1=z{&_{Pf{B@NeFhO67B%y=$r#qri!a@f&{ z@3xbFVv)(>Ut6MpYQx@SGfmAVni2->BAh&5-k#*7% zYetUR5=QVKU6yr)FF#uM=iS!V4#dCz_AH|NcIf-liuy*_$Wj+gglJP_%r6?|rMlmi za+{W(uQ0|9jI-+;!wZt5l;UBE!WhV-XjKzv1(dU`>v@l^V^`j25i=qSo45qn>j{#B zNUxLLs(GkF4fNqC1LEwRrH}@ZM+l$5jcZYPqtJ_^`XZHp3!0tjgkN~F=eLHx>brtr z=-oUXu(oT{JPq5kcNZqgV`ou*HoHG4W6!(2@B6jDt>^yzeuYKE>@OvFNTfcq(-tT?N-{UkdHNf|M;xB%)%YE7R;_du=g2-ONzpbl7 zv)nqsJ7^?hw}q*oHsjxZ?w^i(Rb{jwfr;e^V}& zH~oWmP=e&AV;QQ7&jAPxOeUG)lw|}97K;u=5>*Muh&P-H9vo5S(j-9%5egC|N~1tZ zi9A+}cCYr4Oi(pqQ#h^>KMptl9$LlFv4}UVJpEbB5mqCrz<@;vh78+IqLy^pY*|KJl|<06 zm5T87HSE~3XVb22`!?>}x_5I8JJzUKv})P9h0D|ksQ7s; z8VGK*Tmd^5kygG-zDUODTk!gh$%5h##e=opwb)Q1T%{S)U2|ffkhg5B$7!gxg?XbAqboZ3=a38OA$8Jm?aO5 zFcM!>a?&ALOem)R<%!^JfI^8IY&YGC%#~uE2~3pH1XZ%QW6A)fP{Wugbgq=6N*Pke zggAS~;M0yxD!M46jXL@$q_I6&)Im}@Slo?DWq}H0CYi{?8C`z4k`ZIHHPMJZWLcL& z23-`1n=I0GB4#=|b%!|`O(Tm431Q<7tSXV{4RIS};m$e5E`!W=#76oow9!gCEwx2X zT2_?8T}qUd$4%6w30Q(?7nmj$WJ(!jp8L>6=gL|a8*kDYU7gxsqYRuTKywaWDUETB zH{y813u1*zp{798C|pUV)jIqz#1Tt8aam#|Xj--n77L+qO_j=$oO24BufK1IoKmSx zRI&1iCff-AF3cNG#WJZR$6y4`tfrda$rStiGtfZ`ZLP&nS}Jo(-44Ap(@i`5G}QlH z%TuM=BF&Q0QER<5*Ij%4HK|2Yn(>sFf_*mHX{){Vqf;v=h1FCh-7(vB+kH3QdBf&5 z#(0=qHn()^JviZo8-BP&Vwa_Z-+utkal?p9J~`!;56w5oj5oe5Iq0EBih0eB zJI;BPf{VU7>#e)K8|gf1e)`obW&Jwtx$D0B%!*Sz`{Suc&O7nN8&7=f{sNEs+`}LL zJoM35o_u)A*RIm-(rdpx_jXfnmg&r6t$O$6n}0sk-&-&D_|BvMKK$`ltG?;MH!nZ_ z`Rfn=DgEJJpHlYrC%^#`5J3Gag#FrAzy&g}fwwte0&Pb=2U4(t7L*DE#V0}cU9f{5 z{9xFAL#g$d4TL5<;RiGLKMe#Bg)V&I{#3}m4Z^U7Hq77m$fiN;P4I?3{2}ymc(fVv zkb^)xA`)FE#JH6(iB5cC>t?V85n^kIMUf%|8m7f9aQb-m0W=T z3T?P0E_12NUGlP*zWgOHgDK2m60?}bJSH-esmx_Evzg9(CN!fd&1q7zn%2A~HnXYC zZ6@;wuY{!(Hn&2S^s1cZJSRHSsm^t>vz_jICp_aR&w0|bp7y*aKJ%&1ee$!P{`@CE z11ivg611QOJt#u&sj0}wa-2|nC`641K8+DEq87bq?t&A}Sz@%K9`&3?Blyvgk`!@O zqi9J}s#3$9RHQ3?DNMiCQjW&7rZ!#LL|uB*o${1o-~*&Xdn(kSLhGkGB`Q*r$|$4K zRH;sVs8e@Ns#dI^b**myeXCR2 z3fH;Pb#e7;YF+bcR=Mi6uW;q7Ujv)ggCG!rcoi&SpGsK&c{Q<)RjlR?an#3BmZ*!B z>|rab*_vKUkV0EvnT^x>eyWSOGbdg{O z7ahzgc6bINk}}>#?P3#&Qi>gp@r+%x;9}ps?@^-r31eI#L^WuN0vxf9P8{I9_LV|6 z2#heJ7+8!-Y4C!x~oteT`xVM}XJ_ zc0h!HUGa%e6hS33{zQLS;D}eK!x*1PZ&4)i3LK}z4nqbq5+q^dSA>BmhyXGP906oR z3^~VP0P>aXTjU{A_yHS8@++Y15+kR>8A?tFm?g0iB@0>2W4^JFbFgJ5k9fD2O~VTQ zhlEs=1`*vrLWId_jBfzCSMrraI=YdLbwmTuyI9AfzcJnoL%PgFL_#qtU12{jdL6oW z!4KrJLLmVpn>=8IXjc*U=3z*ZH+~ZovuxeKSNjgB|>Z4^LG8odz*TV#2o&@G+2Z z3w5OT5q3!OI8uxVb$mLk&L78X?fdH#|;pgnL@oT`mTp6`*Q2S3KkhyG1(s zyL18zyuwA9@4UHf=0tCO9ro6_YOjq|OtZt-Mfrv^wlNO>fG2(FdzkPyIvnp_5nbU$ zx%x_YTnty7-0NT$yGve?4ty(o-fM6B+vBc*xwB;HzBcy*%>Lg^sN)mPLxSIj9ry=4 zg6gh6{4o;mjKw>{71n1tu(PGmfXDNO-{S9QP|rp--gjzk%im=v|p}eT7RwEiM0gsS)MMWgB={e z-bujo5e4G0+w`^HrzJoLU|sg5mgE^=?|mB-q#J{|LglGiF-QQS5t!#e!8m+_)?plC zC|ZXRQ5w`B>j~Ss-NL%5gBawWI_zIdtlJA(ou_U87=gLqqs1CeI71V%!=^FXxd6@=A9|z7{rmbEy7~Tky;Pzpg38tQjrG$qaMHoaM$EleeIKz0w z*B3~@BRm6zt)3Sa!x~nUBy5-s3dAwsoGuiD5@4F~6~Qq)LZH=}C8or`iCP`D0Nh1` zE~Hq+;hi%WgXzT_g(=&|`Phwo!iKdPLP(-fSl=BIT)n{^&UHc?M&28a(-p8-k+qnk zh?nr~qA~JE2?kBU!QU}9BW@%k(EQ>9IwLhYNiW7>HD=@FK%+KxVeKn`R;78dtm zBS9wQ!}y~@Hst&Wq(esJLmK3QAf!ZIWTRw(5KyE%Vx&h}6-0idNG=LPj-*L~Bu850 zNv@=Tlw@4t0Ci}AO6G>j^oEVpOtr89PVmOZY=unHhDK%uOg5HJZXu#aDrvFL~14_RvZRX7$;RMgoj{9Yv$%=u4cI;rhp^@Tj~aK zRz+|6WM;lbNgl#Pd8r-yl0mr#(M%LbNIoSsArj& zXLx+acCthn@aG%w$a{9?mi&PPqz0?_!Fy(h5flb|BE&#c0%nzFBJgK$j^=l?XPh*` zgBGZFf@fEX$Y`SHNrVJ>k_vF@<$G%AA`B&%2*QcF=XbD5LwpHnDrmpR=!*h|ZfaSD zb|`t4sE&3AxG};`RA`BeB~P^f=%3I4V8G~$TBy2U=7pk+c&g}Mqy&zV$!C7(7DlLd zcFC3&#T^hthZ=^2E@zCIMvW>d3;e|*^yq$OsY$?TWNONlZb>6}sf_Zad{zQ|-UO8j z3y0F_l1eF2&;g=0D4>ofhdyaY@M)lIjG`K#fLuUdB*6g*l2b@DWT#hoV*H*;^*(A1-?pwDX^+(#H-Op!q4`Jza&Ri z3aY^pEDm67xO|Fko&=ZFW_}SUxiss8oNJtTMq$`OE}ZJp=1Z1Nt!c=tXhNnEpsTR- z=(?f{#wrF|phw-#2X)Hq^<-(BLWFThD*8`P#4=&p^}E67}_cw+5BNC4&PEu9g`e<* zeM$(4z{#|BY0&t3E$)=vJUHHLO-fZ83D+q5#T1G2k zit%8ChH_%)_To(LM)3Ty?5R32TXZn;DoA0SMeQht>~cy4J8r^WuHJrTRkEpjqD-9j zC}j{bU;^>ij%}eDv4GkqotA2b5C?Shr<`Ij;39+>%kgI>tqAu^!^R2tex^*WaCEFC zD$}Ut{%>3kYD&ndpo(#{&Z>k;G3_#H%f|4uDkhPVF|ArIj=ZWUV{IubYXB24U@~wV z$H%Lnr?sB(qyDZbwI~2Fo%t^Bp7b<*IWv)4^uy z!2&D)@H&h0As1+f*s6WZ!p|N;emDGq`~<4Zn0b4=_Y?Wq0vN z?D{eC+VG@$@?sH5qL#4`d$LAo^u+}9W!g$Mhw+=LZ5Q*jT?A@&Uh^4q@+d>-0Jns| z?nnj$b6g}!;d(Q@7#K=dL{@|Jl5#4&Q1t*qn2_Rg`Z5MOXEAZeZ^FuiTo1D_Ge^%- zwN%%xtn!6X-!CbLr83i|S4A~Qudji+wKiw(X6m(3T(wE}Y&u6at4`}(?}%0t?Q6RK zDMzR9h&uI73rjH27dGgpJ)>wGD)UuSFkC+NMdLHW05M|Es%Tf;LsxAQ!ZfpBdO$SQsUbh98237xV&&aVQ zd#|Za zZlsp1YJPWPzPDLZ>zs51b1sdu-8Iv zsoX{LHi8d4tMRrtW^1H~ZZ6Hpw-=|$Qad%34mgTWIH}A?QlqH|%K8W&vRCB=wRAgDX$z5^#$u zDs~}jKhNAy(3X^ ztWPSa2!MFtA-0EmseNfXlY*bt0p%Ek8j0tE3k_RT%YOn zKIuZUG0tY_0GB5@lV@7`I%Eb0N04U+lPafPZ@AxqAB;y6P^U-?3L_~0IjX8{h1M)* zLMx5##F(%6XK$#iFYSPqSCa#&NWe2gTLfqdDs{qkdfxG-7PGEL1Tnk1_ZH^E`}^l= zgcmEpz)JT|6Kbvhr?(?~B*a3QpE;XWb3%-~tM@y=lWM#_s*~1ncvLQCQnARZafVhD znr5)SLvg^Lhv8PSuE+eIdhd$D$zEzHiyUvSiY~E}*MQ0;sOAfhV=}oDmY)=?dv<9( zXHFjf>|uBLV0f3}oB7+r zeNn>wQqtt#>my6*Pw1n(Wnup66Hx1qK2NEB?90#WCzb6hC2_iLm6|4MLVoqGcYl0- zel5DBY(DRUh42GEd$Sczo=s5Ye)PAFz`HK;?@jjiy+QuHf+)2N6Xz#mMKN}yi5Yy^4ZOGRNHepLVj8x|%A7BqMeVM2uq88&o?LtsH4 zmvA_Ya0nU30|o{C(J*jf#D^mj(xE{TURyc*O0&!MMZ$@1j1)go1kjr=6! zp|&i^NG3tD9UB)e$eAba;ypO2j91s6`)l`G-C2+J@tB?8-2lq)M5HX${yVIGkp zy|p}fC*Z}O2_Di3nrP9ZU$ZQ7*;WJRLzP4(Y9#uy>zt=c$|k&dQs;tQC&A7ge6^pY zmPC&5!)fsAyiyl=X3^1gP#TGR5UirJb#bHxX^$1etut^x}M(BP(B{c<17me!usg{zT-k1@iz1Z46BK%=CaB~0Xf`o zI~>PqPqgrMyRb$DfBG)L5(6A>KHKO@XeRMWTdui-!kkh^w#vg#%sZ$#>ku)Pk;OR> zmlVzi+e9hQyBEn5!$}1leDXE{o6wCm(A`>Ys3e9nZLrK)K$MDel zjxC?I`tLyFGChmD%Cs<5KR}s#iqhPktIi-xTI{q(#)SIAG^0XHDhp4>U_yzfJmK!S zftVOJtJRu-lvucys-g^3=wc4I0gY%@gO!+lZ_oA&YNACPW-24vNkys5h*JCzH^tRn zl`0Ev3qqBkS6P^=+wyEviCJO0byo>71&%@g-w$iEbquZg0e=*inyq z13GQfjX?@X&1=L6HrCa#6m$<(NQq>R3jJv39rtR!P$VwCAHI$j<)KI!RgwSZOFbfZSnHKzj$R#_w& zyQE%G`fGQ@dKI=eT%6QJw^$Z(TGo?FMe`S$Rj9#+m&CjM_vK+lrXlHf(IV2@m*3Um zXkJO%9tmk`a|S#{nFDWVKVlB|r@JH^j)j^}Dpd~JDAUGCBL8XZ#>Au z0Y!FnHEPwbPm-#YXV})SV)1K!vK!86yoaI>X>dmftk$q{XF7cFmz=Oj zg9xml3JLQyg@6!*W4eo&1~k6%#Vms(OApgjX`0NnRvcVn*hP@PRj}jT^71vSM}6Z)seFNB*Qn{&^`*hE&t(2zft~ zu+NW4RG;*gH#9hADTa&;Pa^&n$;2pek2*k7AEg*PeUYyLl%B!QzrrQWHM;DQ6BX1xp$(v^U)>hyhP^9j8rY zqzo(GCkksuCNRUkyR9~Y+C`~#t)`UTmr~Blg z;4*_#^o4VMeKQugDgsT@#d2_Xt?0A3^fTl^mIsejo+p4OyYscJGAzC3Y9+NwkWRL* zeI?YTR6K?Y4D^-!wV3(|zLFXa~EES*L6ht?_swnlDqNu{S!fS5X;k zl=3JqxsQ5R1ch|EJE(V!Zqb|T^qR{8Rp!qE=4XAw8R0mtam1GUV`ZTRu>?b|!wlIH zfuE(iQf$S!W#-C^+{QB&PsOU1ph(Ed6+3(8^)Ip%@*%WRu;zsMWf{c^e4()0osfun z{~6Gc0!keJ^Zq43c)BwYS0ZX!QOCH_R1$-|np0!B2gdLuQYC4Q&=MC zl5k=!$DXzcq)lb4aK_l(!4r?i>K`lTN;s*aBdvTBVi&DDIKe?MlSX78TkrbXI_{m>> z^PeC2w}(9X+24NmzaRebm;dg&4@%qvOYa(1{OXg>vb;=9^8hgarc5H%PiFd0`2vUl z=Z^s!&;hTn{tDsy$l~V)jVGG#?DXdXiVF5 z3!Db2C`4@m`^#oVC+)s%SjNpZzQfLj$J987UEU~coJLz{t0bx*a*QPs>WE|ivVab( zMTEL78!IAmrV+Kw15&C*NWw&P>S<+QPZQUX9oy0NqVFnjk-eHo9K}X5kVv(LLj(E6 zdQxKY5C*Tt;^PX5r;eyQaB2^4Y&J&Fl*o~Es4KnbjH+%aE1s|dgE38{;2{CVs^H8j z9t`&u2q~&&V)_vvKVrt-FxM;*AKlRSL33WRuoQ zcycaS1}Y(+WSj!ZXD}$S4h$yaQZ6-7Ca+>FpH7SDLkC5muTU^6F{<_dwC?UiP$n*I z$7tvzdCG)F$|ILg#?s0tt1ActODQG8dQR~0ux8RuM>4HP37E;ZoT~)o>XmNntI#gI z^v5L8h}!z1=H?PMV^jX>5?DU+#!h0Ad@}BMKuF5RsaED*&`=(tmjmeY(6*h*H!tDOEtS{Ag3EIPQujp(Svr-2iT3YVY2r zW>(n6Fehy7Hts-gkU%D(v<%B7RAo=Fl9ph@H3~&bY7g0R#dNHWI570$|Ef{p*5p8} zY+er3I5=kE3K5HXC^du4F{|k}5ap!6Z6p^3+f?+z-m=&RZa4u{Ntaan1QhX}&U zOVklBqSTY9a3%{dIfHTmD+%(TbW4vgCajdP$aGw!R7{yvP1n@*oOJrA15MfV`lwV+ z>(oxg&rPMT2k$ieSdmZv6i^|M1_PB)3)N5$75nlOQ5Tg_8`V(}Pf;ILQYV#CD>Wt} z)lxH6Q#W-|F_lw46;wmjPCZprOVw0QH7-dNRacc&TQvq%)m39vR%g}xV3k&H6<2fh z_-s{Id(~HemGgKNScjEZi}f>v)mW2NS(o)N29;T#6`J zRHdUoDPNQc>XALjtwO;)$UqMw~R{;rUbo z8jv#@Q72+0mSgj-6oMgEh1R&ph!avgVLC%5nfS8lTuA-sVO7@%^mVG%YWBw7I*uAvot z)+PdWUMqJY5`bp|7k1B;TCGzXz4j6kpx~;d7A&(BYGI5%qBZVd79unuY+)FpmMMR? zv#J5ZEP)o-t)HYR7N+;C5R)*ksT7FjUTVP{Vqq0Rw+ysF5X84{ML~GYu(+@b6@U{J zW+4)^VQ#LMeg8y#B}08>LH3vf7Rm^$rk6zr#(5`&765o?n4ucFcQBd0?g?89vGrav^+k_It=7 zkr5}j{2-HIp^M|n7t;76NS6T?VUlw&7^I;U|C)guBE?OF_m~%=8k%7kDw*7pA$YUl zptQgoz_}WnwwAR47J>mCo{R}L*>D#3halM(F1QJJ78}I3fqD6W$1NF-cN%EHqdY;K zuQz?PAy2Zwlzm|uOu-iX*as7V9Lf%h58#@OSs{Tr7s!|aG8vsm>T4nl}Gwx*}xoXni(*; znH?E*Be@{Vx3v%lqdy|49eR|xVvwI1Zc!l_u(%a05*t851U4E6v|&!5fsP~K5)v5~ zeA=1AAsAx863_T!vErOPmJu$Zm=*e$|1%kvsV=L-VHuvHt*1GcVZ|K=ds7wFP59Wc zjg}w)nIWJ-py}2J7I>+vU;w6Bgla(=G~fW{StAYy08wPi*dOrgT%I|6t%nVXAn*`OLm zAd?$oUl7;0%R91-!@;*38X~|5%%OADxg1# znK4#k16j4DTms|^l~q~2xBRu!+P|v@9Ev+`!6TFL_;$x!8yvf$wYfS!H$Uz`fu$jQ zn;E&Adm(UPxldW3XThd>R#H;CmecyU4bW_#Wg0@fn|b7HW&4$p*%Mj;ai`%lVgb0_ zyx0D=rNK0sp}`wQoh$fO8*pI}I(*16-5Wdi&e1w(Z}3g9HVa%K8`mi|WnZ?>44u|JA(gR#WBxtcaXm`M;2fp_*DL#j zf&3}(xf-Th$9Y_CM%EILd$?N!s*xQVmOU9BeAT~tAuN2vuYo+eJLKP*%7-O${UDa< zV8K*aKX=bbd1@2Yks9Pld0CDf}yUzUei-PuGSeB$hg_(Rn%R#d`lfY zgr2VX8`=}1p%cfO|D&7MNqq$Px=Zv|y?|H4suE1;d{b31T<9<;ljN^i6doE7St-W)gVn410L8GYRRgU zi3w`6w4R%EKqn3fCbY1msgN>b$}REktxVPG+-gK|z0^>JMS(1**_34BO%&1812}0r zNXuO=&P5Y}Q!EzMfxB%}Zbf)axP*pXP3=^h8nEz#l4oD$sQ5q(xi%HYqNYLAFu-Lz zA>(;aFU@JOtoi^xt<=$>m%PPrDwNH%5y?!IkrsnZeiX3EH8C7iO)D%B$Iw+03}6d0 zym2JU|9%@N;|ph)0QgyW;mop#H@qx|SXQwmaF}2QQBw*?9r6dvi>PG7oDp8tqFps) zHBn-8G_^w7VmqGb$7ve85zdG&F3Dt*PCf}`lu}NKkV$`BS%?xo{-{kjHz~LmF4&;s z4K-zyMN<~i=-7-&w0M(DGRcvY(MDl@6w*j0%u<*%%kXfJZ`icwk}}HR^jA@EwuQr? z%Va|bQL%^6arwsPirslUh|D&2(A1|v<3 z&Gq>wLC+qVt9ay@M=Q0}G`5!?It~n(Oz&=F1T|v~oP~b;iSl27lyY3GHUce|ZLq#B z1F^8_0c&Pk8@{N;V?3B+%3#{CLE@Q{A~UdxA~*+4zD3n%4mmG>CheoSQnD(msP%{@ zLO$FpCsd-qsg2TfxZ^67YOl?9+io{$C6-!7m9Z?c_|Y|XbiBa^9g}q^$RtA<6gU%x zJ1Es>Qbbn;a7Rv(h~rYx0n<}eL?ZcKZ%udz=8Oi;gL-KKngv&t?alY#lLM!^|LR4R zA$aJDr_?&?lwZ||5tfw|xF3J-RXRs(#2#Ji2QK#pVS=VD!#k*1C4k$tL&zJf_qHGkafn3hP}~ZUB_I}YiBggU zSw84PU|5KWP^=;qt$0O2$fp(@x}q1q2*xlHu|8N7qZ!M1#x$yNjYK?R{}SoO#xOQw zIx@nA=;DY+DzZT)wNPRn{|LxH0?uiIETkb1iAW~8ac+rhq$3~6k#0e9l9a5ZB`=A| zOpXpqZ`7nGKMBfEigJ{sETt*0xJW;qa+R!Xr7K?v%UH@%N~wh7EN_X+TY;Z-|qU|lYFF$sb+;VENn94x4z8xS3$6yQ^kAZ!5) zSvc+zmbgS{O$Uq90?!;|nFQL(vI^DG0T$cR;$*Tg3t*T6e2WbfFLHsmBdvlHx}8iS zQj3aG6!M|8-b==`sr;7a7RzXircpVFX^dL#~48 z?a!)_7fKz#!tKFDe0_nIDxZbML#+a@WTl&!#Fr?;Fw%S|MplKL&Ca#13n%Vk;vg!>g4jcUAmQWsz+y8y$ok>qw1iW~f@% zt>y)!&JpWbTf5wgU_nrpCkkE1#Ku1rw!shXHe#Qyd#hSZR^D6?3+si@ZL!5$HEiQm z4*(U})-lnRV^pbpyV_*2a6}?dzZ&VI z^|pkwA_WXH109iAc3mDEjn7r@HINU-Z6OfBlTdzB3Q~{rJnD{q#q=`<;2m`s;uH zaC3ss<2un3|3M=}ZMUU|u&0VKV2Zkf-Di6a$@u!^fwgEWwfI(UVy74eOf?zhN2is@hH{e6RmO$ z!9Wm`&?mLfA(jUbaHtT4p*Smf4Q#<5@;Dj8kT0&`N4{W>(036^8I&ZEG_%ns?Lrh9 zlM5mv9JUaAn{jM{*Asj){|#e-c2&a?U3eGJQYZ$QjBs?72&0e;32`RSmiH1B%FrGm z6IqBDC(=+pd{Qam<0rooL)|D#?iXM?878qn7oIQ+qVkc%xH-5%lgE<_1L-kNQkgC% zA`*ZVcQ62*2^bs_ESC@@w9pe{u#@oDe+)1cL^CM^)*g-U8+@mX&EZuhFjLS1Ey}|v z+2KS4!9};ll>2fZ^I>R!s3VGzjX*UM!P5*Y0UJ^E9S7npTaznB04;&|lq-@Upb0#K zMnO6-0w~#o(ssT$b%2RQH+v+ySs86kF+l2;oUR+A76a$+Id=|jZX8CMY+i;-7> zn4OwAJ?NMKb~$LK8373PqnSci)gXdTi76ZzNuTwX<2fCAx;Zn78I56ROwpcX`UjRW zCnZ@hO4^&b<)r-Si7~>L*M*%p;G*7_q=_1Rt>uNMf_@mWILM(RCqi>N2 z*2oocNEuK{nv~HBNE##p%8kfj3(+bUzi9#^`kQlFrLTgk8L?i2U_dKUto3mTpB1J; zIi?~srJA`r&8j%yaD)0;8W}RA|GH~v5v_u-oHClHl;@DDA|EqiBR`m-N7^q|$ajYN ziJr6Lzt>O%Bm7TMg_y4 zln@zs>IdmKu(a9`nWC)om#d_dg{ndpRwJxybq(!kteo)`18@L9SSR_X5@A6tMtWAu zfF~G2|0e~I3tY#qwP7SpK%i^+nW#Eu95SLiLm))BFrQNr2{5(DQLJtXAqH|Ib$dLn z@jFS{9X>&?+Sv+p+ZI#97=U#iUSSw>%eNiU7eFYeeA*PBNVe08p^o|=9lIAuBce*u zg=0zw34omx2C-D>vFY&=Y1?-5NV1n23z>?t;5kJ%3xbeZA5BGrQ(Gj1)iY1ZwjoME zJ}_p=*bIgHGmvq-Qahi6Fq>o35D6l+gjKY#bhK@i3xGv*I7=Na;R~(6W1PZS5pWc= zAPV7V5Nj8TAoL-!fDZ98cA{W85@0K_@Fqla7`Ol=62b!d7;aawcl%19b;by5OS2*K z|Gk4pu|Se2QY62@P(D83sK>bq-vN+CvQ)r@AfZ(igfR?bqcl^Kli)B5v1SqlqFh9% z6ti$Nr4b*owZ7d;4g^Xx+K`dc)2HHKH_{-XcR9hzP<5TF!_d*H2%(hbVJA(=3wD~i z2!afz;A@jWsh?O4W#FQ+I})?yYppO4o?ss>!N2}Hn?D(cGyeSM7hGN0PS2pLHrq%0>CrYsvNHqHsC}sC9_6rNi$~%g=Emq76vJk-v{32 z@`B6JYze|>0+^sWYD_ietI6Is2D$etO9g#Kk*w6}awx{6-@48Irax_W%#FBPoT!3^ zT%bi2iQOB@M$`(itInmYEN$1Gs+?S&aHXxF$?+Tm$|VZhNXwI4o1cZte9TgQEJ&i^ zSK0SqgZm zN0fZosr^Nnoe`s)hwy6Jlzp#e(SwM@B9-k$i%_F=6x(d1b-1nCcgEWMfszff*|s!> zMrFgjNrz`>I#rotJY&)XM2`w|vCv&{E78^x#0EwMjmX{Iy*Y{T7do|A8PzQoTCF$B z>JSyUaKCfh?L9fLINnm>uzhAY&z%>kNH?Uj*SaG@W6+FVV?Jn-|KF4tfy^D=xQUvU zfIsfD-NAi*8tqUTXhb#)w;q8oP~m1^)7H!Fu2tC#b;%5l0<-rbDuL*cbA2ShBGxpp z;R!>#m*>JJjv6@ALxONF2@#fUqOOOc-m04|(?~7y#(HP4F(;BS*b*27aj(CSFFXa0 z`cmTaSmS(ICqTX&W5-%-4Ml8lDD^o8J?@s~Id(HTq+zKrD?S+vO%(jOJSvIe>B}n{ zPB_!>DC4W)IuaGxdMj{B-rNbU5$IY=2|HkN&6C)lHxEI9vwU%F^GNE^$pGiR=u_e7jb2mFIqiZy=vY7^98+Y&*AR2SOwV7HtA#$;*fL^2src`KFxWqx}>U@@yQ45Kv zIwGS~^0uD7cd(qNluW@RfvgRw!LM$?=^-Hy-wa?Q0i{2y?L;9L(Dqnr`70h2C_xg} zqLMPF)$L!r^Xa{{^?9+-Fc5A!6&$+qd_jc^0S&_=|8`1+Zw%n@e6giCOq;{J?-Op- z)=tdG5M}#X7|}+&MmZQedJ)idm6tIz4POcheHrGmL77ro4&7G$dZu@A0PO5UB;g5v zKNQ$LtIit>ln|Uw{*pGD3xBW5=Az_JBM@H7^6K*P{>r`i8`nV5*9sprN;6Q=RVhtGZ|4W&MCf>?KNTjWbiENP$;PA#t0zr-k z5>yhWB*LtP2Mi`aHEl$KS=CH133q2e8FDg_;Yz?Df`)+}V_{<<(9*S?9F4N9<*|%P zV6d`G!x6Dc0y^NB@Wi%=r@&-53*bt|a}2gt(t;ZCmZ*SEtZYmnV*!oLA#FTzvK=;y zfevb=M2z68mY_#VA&nkJoLKQ<#*H06h8$V)WXhE-U&fqS^JdPSGiTB9NAw>h6FnbB zhRmbMU?yo@yw>VKt8kY@X}d;aV6ND70AsW&5bZEer6L8TRqYZbTUrs{Hco5ePfM~K zY7hiYR4HXbO*h|!Gfp|@q!TgFMkCEMH%r_~KMhpe zXg)K$Vx*NA2!aKea>x zaEZ_?fmZP)l5E%z3s>h_No|%?2TJ78F@7_u6i6Wz#Rx-HbT@{G-lBzvfns@xAWBlB zgrG&79f;XWJ#B)<__EMdR#6re)Kp+frL!i41i!a7FV~sbS?9S0j>vT>5izIpE zj$9;iu%b?O%;b_scKKzQXJ)x(#bT}*XPfO@=@(u$Vgd&Sw6%BAm1tJ^=ABknc@g-S zhC1nQ>VdYV{nW7FrTPcd>Ta%!2vMeDFm1j7=H$v@-T9e+*aSG}y5+pnpl06c1x! zd*ScL|1#p?ge4nkvT2QCy7V<^a7!`+)Qly(p^U`F^sK%FH49cnwoGih~DMThL_u)o2r7ns2I%%k#k&H ziuah>8Krp~`JoSs|K_+OI!=5}Y#}yW^~5X`aWOssB0-ucmMq5L5_MT33#zb1ibyah z38~;>CbF#-Hc>Z}Gh7SbcqlC*sEtLMqup$ky)JSQk7M*95yP0rI$lRHajc^pUqVMw z!6%VgeBuxv>Bm6c3odS~Bo44R7*S9NiP@1+V#W|j`XS75Qe@qfs6Y=DaB<|61}Yd5jB1!e z8O!huHk*+Q|8E9G8)h_tdD1u|VEO_aq58={5BHQ@l#?eu0FN|kAu46mVwdHiR5hCM z3yH*Um%iu~9hi}vYnU;j&BzxmBuXxPI!BplXBWMCYy8{dv;ff$0>Pm)`@sOzQQGWb{##Jr>Ou6W;CD6Qz7NVKdqREqF zDM1$|L`u?Da-=NaJOo76Is)r$M42;XmN`T!3v~)K7P`EKhbkBevO)}&kL8#wD+8hn z8qphld4oFnddZXUU@VqMB7~~srKI?QNjOMOPVeQ8NEl(IVmU`J$}k4sq{au?5sVLL z;v2wB|HDvzIH&|@627^JMT()dlo$;N8F%GP6zFKJMmI7IcRuc-p2$WMOv8jk^y*Pj zM5scwMh$Vr0v%VvuC090nlbzc2dK!d2GZmM-wutn7!6QE!uJbuOotgieC;4knZ+9DQVR z3>b%%Npx5(g&AY=029Iw1Cd2yY_K+<#LR0P&YISRY??DBS;{~I97z(TEv`WhchbNc z|H0VLGPlyw-`$HDJi#*qfSXoud=OD)iNF`nB93OJ>?SJp*v3e zvp5^+FKwiVpCOBM*4Qn|fyaPIz>Im_#!&30YiQbx=p|1+IBs^IWjg1Zr492yz(p`!6BT?=MiLZnoSC9GM0F80aYbJ5jwEP z6V>q?*GJ4LG0e{}c}RNoC~w zuN96VL3x)!X%@WSpOotjb1lCo<^Z2wnozSko?r{>m=shaz(`pOz8i?JOQ=`V9W4ll zbHOkMxV+U6I0JepJ6nY9+XzlTzs~cy1CR=e5Csp!j^GHgWv~VO)1rY(G?&1@+*t&E z<39?EKbc^FFloFPOhLQwy>gqi`!hk5&^*o?3eN*HkUN0+nztbYhNSSbwQ#`!sk(6M zuv8JYqWM3A>%0#fHgCf{oxp*S5|FNty`H$H_9zPCGcGou!fR4Kkh7;m)pAwKta0g%L6I3Y03z`r^|J1}o5QLVTlI5EcV^PFO z)WfCRL)yVX5W*^Ggt$b!6=l$;nllJka24RnJgUOOgZmXaVuF<;NW8)$qA~)oNG555 zro1`}uuzzRA}ANhN$69c)Oa*MQI$cNsRINRKm!m=m<1oQg=N4v2m3NR0gpuSrV?@+ zR~at1X`H)~NJbd$^{@2m}J17GH@Wv6pDZHN;bet+Y+t`fxodb%T~liJ8_%+ zsIp_c%)#6z&1A^L$UOM#54VvZC`rHKAeTBDO}ykdBiR*OU_#ThN?U-N!%V8!%%ne= zg~B{6fmj4+AdZci72{+iO*yjI#07hF6mYW5_-h0g|8W}|$sX%`6}O=()KV~gk`5M- zDHQukrqs__qCV@xm_BQa;_FBI6Rj$Y47&s{a0f#}jDM55cZ$&80Z^&YBN$Xr zrAbf?olv1j#ttRW3=L4k6HzscnRMDv$f(e(5z!ST(U5wg6mroL-On23Pd>CFz5%X} zq`e6B7iI88_EIF-{meXKX7Q;qS_JJr)Y|NMOAgDJV9O4=rVcRqE48|E1L6u~W@>ibkadG9n0ODiYhs)5(}W zIGKd2WT5NRjO>tW`*QQ|1RpD@)1B)Q9|gj$7+gIv=}_0?ZBpZ@GaHc6R< z6D7h~I>ZB1BYKRgkW@Aqwdov)J_<5vg$#fDqb4Q0MPvw4LL`A002H01%ZOGj(xX(W zR&xc`b!Asf71kVe3?B%@{L}!tX@XivvRP0UYVZz$F(A{_28%?ggKGvMeU4oUkKoie z?F1rC@sMX7&Tu=*_#DfLScT2gC>$JvVZerPK+TZCPvi_I35cqGN)2z4*thA`#o4__ zyG>^l2z}iNV=^mWqDp{WDWt-VEIHKA|AX1$Ot6PZ2D>Ma) zsIrn3hm%#Ol-1I9#oDYrq<8I7$KZu<@IJ)AvrAyDSZLK`7{*Glmn!(Ns-&%PoRVoe zPooG#1tY|DT#94TjxlHk3{@d_s0BJ`gfifP>!3G$6G}KcjOu|Y!ks$EVZo6ki!2q3 zmUvr%ON4@fSEMDVMpeoTx&vXWI3v=QNO0Wu86y{a31mW5uK3xPz^#j#+A-)K?+LfxwV1d8}7Niz!GD+M}7KtN>PP#J|W{k~q87?M>%h~`nVxxrf zh5KA!_jO&9DcnDh-5isMHRcIu(BUa;Kq$`Plsd_yw7n~KvlTYsKh9bdZVZU!y0>A5 zF=pTy&aDC5GfzN4`q{tm|2;w>Hs6Ci%h5ZR_0owY{@(>kW1Vn8k_8U}?4(@!zO_Tu zQiNA8?g;^|g`q`bNG>QjUW>8&WS=$A2sYd|ZZH#MVo$cG>#MTAmWUf>} z-k{h@fPnNe8HUJ=7$v4UD942_LC69{{mRH=o>^cm^#fv(rRKgRj5{21{6aiJlpW}qbxx``$=bK4Z;n-Ma7z2bs-_J@R2`s3SJ(8>ATU@B- zz>`H9psn^GT%kMMmN>+HJm0i%+}@pMxwB{QNWpwwr%T{xzfk6trc`Fej8^%-V3-tI zc#!f$Sei5As>LTL|K+4>kmGy=xQ$e`-~^^wSlUckD3JvhggKEK?msKsD;3Tszyzkz zy(^1|VKAkZ$UIZf znzND-sn$Bx^kY9G=;+jHmL_aIb?H7OPsE9&sa;v$(NG$mrN%Do$-dLW2IOh7QqM_* zf(=>9_UzAwBs>sp{{-#V5yclR?bKdr%U12ycJ0?LoaKe>*{1EFTwFGa^JZ|q5pRxw=Z+Q{Hn71@K~X+Ik%`CxpK+{+ zNhe2TBqg^Kr&^yfpq1uHOKO6iL6Dg9ZP^$XS%XE&332Ba>`HHv1?MQqLD4w8zD>iF z>=fZyQvN4d>AmK#sLYEkU@$uAbd>Cb+9Dr@h1H6&#Bby7|2iB4bOR8}R+ z%+!*L|9WK<)yrHh&-ykNx8Y96xGTS_9>;V#XJV1utO8tV96x9q-w;i`6qZGpo2v{< zrpj_d*V*m(N+PvS-xzZ<5s^KPpA~8bGXion$BtQGP4t3K0|E>8#Li#{hGr_GkpY)R z{}-@bRn)5XEms~fP|cdYajwbnP1EBd?v>h=fI_4Tjkq+T?i75XMG? zLkkzXk!vv?}nY1`0uQMcF1c-|)BtgaiA3`7^T*LI!-6tY}0V|75sO zA`ziEVBVMrwy@%*tHI^CC2^Ad3}=kqHxKJ&{#x~{F%YE92gsHmu`Oai3z>)!&|*dK0W+fn zRM1LN1otO_N+~h4)moGY*-sIb#8puUv`iz1Lvb-C$(=WI9%lf}l)_xV7ABIGjTvsk!o8Nh3~aQl z7#cY8aH%C*@sHmdKBU1Is(m!bPs~_in`6@~^9wauDe>NbWysW;X%gh(&2s(uHDGfE zQnMZ#3uTcEHqV)6N`Sl!!+lSa`mnw~}D9T!R!#buH&d1j)=I z)kh9_G*d|xjp&j}9)aXpO&`&5RxY^g<p5@<1&=F~M_;x&vo zMw&+wlU0J%l1O`1L7;zawqyxhA1QOqa?J?>&54O**O;S^LK>;0|C3Tm>0^<#FxjJ; z{$+_b_KD)7a0O^ZQbWyn(v?XTT8WXJ$@l^bRv&KGkw%`{*C!jP8F^g-!Qe80LTvqo z;zwMWwg@;z7#9;@+o2WVQ2%Lz94+OYH&rl$n&1N?QU2u(b~cgMY67NCsULqpg|-l~ z0{WTWX$k%$npUg)B&`Tp78i@E5zHiPsn7aJ5*y4cqmdMKp(P4$6}t7%z!w@J$~kl} zO!tY;7hg)3F&O|Koo6yRy49DswkJnhDi zuKTJs>co}&1`>V+@G8?pKIJyeC3{sfbxVD6x-+pL0yRu7{{o*&u`fDTTDI9|qn)R6O{=q7Oq}BSzja4H!(H6ZJ)AeOWMSmMJ;_ z=@Bz|nc@y~$__V%CH56N7M2C!z!XqB{#AxMwBTS_e)Cm~4s@IdcZfK?pf}mO_qo#| zVBRc~H&jlKfdd^QeM}`t`*En^7=7Y2!h}j6c!?jp`N9kjWn4yMniO*HiSb5KH_7&& z|7Cm4b63!cmdk+f^Moe<4@P9C58s^9vUt)ocDh!A`$qURF60tvQ z@Jw=mvjn0@#|Dh~VNO}7M#6#uu8;8VEOa}KEtsGL|DbRJ9oh?wqPS!l*jYk$Jh9y( znlm(xLpGTtt|5(Dlmtj`gwK&+(WREy zW*XOMQzWV($X<=1h3u(`idq<$VK{3^c4P)xN}`PRqz*6YaTXg4$r|Jt5H<_3(StC# zh}Hd3Poz{J3us};YVhnnE4&(Ja#IUC=H*Gypp_p`@{35YAs8_bgQAdxyV4b>L|W+4 z9#7@S4mG2Wlw^U4O6jf>L9lDe;RPOLsUBrS|AltyIik-%_7?|X!xk4hnDTJ;Om9+S z7#~WNqMVf>WYofiN@xa!s3%BifU_iiNY0po;|oN-iDpPk939nQo-U#>QyVo8mO5iYem@ z&PYbkA;zbKk-`W`lp~||W`9i#>QS2tn4&%@siTY@MF?1nb5t^1hiK!pl4m`S5pVoZrzoDY^|$T|B@0v9cHhA?JHTEnpeV3 zDyybpf?{>**vCRPr#v-lWGidg#o|*g|7M*{qg)c9FkLpZVdZNiltEX~@|7?Ktj}ms z>)O}CHnyUjY}{mP+b~9=6`~MqVzhYM-vU=LmRLk^i)-BDBA2O)!WTN(bg`E_~~2-}~Y> zzxt)_cK7Sw{{lF`0v>Rr`b*#gBRIhdUT}L2%-{z@IKmR1uxs0v8Ol;P!y4W&hdXQ- z3kw0m9v(4?OKf5UgIL5TUNMVX>|*hz7_l#&F^y|%m|A4HKWNYEm&zha3rJFtp&#Fkbz1{9Y9WS1);B%o!V=&I@{Xb zsjF8YWlFD3Sh-4uCo-aiWf+7G$vJ5=z~K%%mitt|D~?Z;VQqNN0gG%;Aa8qk1!*J`y^3 zaC%D&pcO)lMDGM~{|Q7}(%LhrF&?!`@N z#9pRZT}7b7{pH^uHNvxjof4cLg~X62oL)FM;0h@UP$kki%-!>)40q&6BIF(}xIY=LHpsv-zu9%?JWl$IL&k>4XETo}*{6Zp54HBM%5`GHX zXdMD|TD* zH6-B@>RvShp=lN1jDQ50gab7S6R?@xfKcP`ESl>L&*?Q_7hcugNuW6qgzJf*?Ht-# zGy?unW4Vx{X|y2l6hP*LAdU>+D)B^g1<0N20e^3$Boq_cD zStJ<2|1WlkEXcwIQKWwO7Dx<)C0xSo0TN5n2X_Z244T3pQXIiG` zdV-~Tx~ExMCwsbQz$oW?3g`F{XWHP(0YrjbkO(Z)WJb`yB#5L`E(t6!0$>G(P%eoI z3IZF<0xIN(7Kj1`Ed(u&jVPD`r-*_q&{|>SRxH#MFkv8`c_==uBaXA)}o-8l{&)CjSoC`Z=leTyuajiE8x+h|JvRbZuTX0-|>+ zp9kIR)w$Ae%3K%@4Fy$WDYE@^$93T`6bpO8g{l%?PS?Jd6BrUpe@yRNK}lyeo^#>L z>Uz>7HwvaYYvtlG>09RuJguJ@+nU)XnoQp3W%?cpt1MA%wulX6%u<1pQsU5Lsu@aM z&rbDHHHX@~$^wc9n)bWki}*q~W@4pU_k{nZmh7s;(-13z(t)W+)}+JOTIx)%RBc~3 zuNgk_FFa1XbM0RmUP~&@63DrRk6U)u@D$5uC~$+&62>QYk8v8b)zZqyN>e0di9k$ z{M-9WM&&OnYGuL17R&l%q4v>UXVK9Il-hRBaJ77v$BI5!nJwc5L~0AFRh`0>+%M{` zZs+4#iQaUj0M!J;>r0)#Je&Le)iLo*|FFS^+0fFRWtT5O3$Ds?jfj>&qVT4yopEIg zQGCwxKdL7u0t-Td2Y(5Bvej#01j?C$Uv0j3 zL6fi@u9J&%-prN{F4t*kWr82IuUKDy*Y_Kv(M~FJjQbPGp2v{+V^E7td15*9vk7CC zaLI&n(E1}j{HM5peTbc<))(*P_r>ev!6y?2-E)^#uUUEXUbuUQgs067HwVXhFQCW* z{u9=g`2uE;J3%#T=?9+g zz|83DxvLd{jLPGyWc%xEE6(KOjo=(=(UHhZ0x^ZyRf5mK##*ZsE2o z9>YD-6W^1!H-r<2#8;{qf->aWQ_425D*hZCT|8!FSiDV{dBeYFQ zz&^U2WI=vY|1djQt3Po~GT^Pg3de^bK2P@Q&O-$9MCJ#!bb$0eos#O}YEJ!IfHfCF zgB#yZY3Z4(7kk?xo!6jvW`REv0Lcs8f1xzHMezce{1Qr&L>8X}p0-N4D+wgV{2>yA zFQ`qhjjIRhND!Km*;K_e>oP;sSQCC?mQ5|U$QJzFWCBI0?NrD0^k{uSAbVc{oHl5;RzBuB*osSemoI%j3OQ4h%;7cmWNJ3kMk`V}q zSQwinA(DJc7`iN|d3xjncc!cplZ;GcpS0uIn~cRPOH+Hh>MRzu+OTsoLhJy+Vr_); za*@~0ovRZzzIaT`UEX3@5_Uz^xvVO#0{K${d6#K*mfi2mUj@Py8S@#$0*Bw3SRPJa zW;}vsDzV_{%pbCl5#!_L^(eR-wOpE!bHPb-c_wTujd{PC(E?_j?f+id3E&xjnPMx< zpkrva*VfXka(?O-)XB^y!2FixveQv?l%gicxG7NG`U;`p>btG-_#jt$Vl}mNwCYpd z3$z|Jla$^35%;*6fxMaaTZU8IAm<7cKL2lDyDOJxzg-RJodSNl#r%F27VMrA?D5vp zwN7sW{oAuc>OJQ7b~E2|gmCpYn^IJ!+_}j1D)Ygob(UtbhCCaO_+MH*!C1e~>+YH) zLXDo@^2b~Wb;NF0Fv=+l?(RqpGH$nH1m}o?TTf7kmcT>IX@9F<&L4U<>1u_^cPj>5 z9~sj=DAStq;(PGQpoDnKs*i2PjGw}N5PF`g?zWqfymId26q|---82Qbs-MUNt*ZZN zQ^G<&b@<}J5Uc-gl=-S9s6W`TnV-UTnRp$0SA8YeB~j&kX)oRc_TJ@F+Kj>` z1e76o_T<9uV#h`1zt)H!N9QgHa2lW5oU3A((s<7d89)>hqNIN$)l9{@pT~L%fFr$n zNcqQ$0D^1LOBjF0oQ_8kijKGPk}q`7e3 zq{yp*CPZjF>aOG@n}4QbFRV<3>J%?}6oHgWsr{|e!2ajoK6PWk;Jc21$OLHrYc^hw zQ@(Frf(>$FliI5$E-qs65gg8JG~|E!ehES=cn0h>2k}*PqhOrx?g#6LV21m{#IN48 z_>L5)G5W^E=r}Iyy&ij}H-5bL?wf{~eufUiJpQ!%JNrTVHOI0Tv+091X#F}xH! zRd4Q7KPR@VG;tvwDb2t4k-~YT!D~=I5-t;uFSEap@MGFgX};ljP~slv?VAYH!cQCt zwkn*#H|NKKlZ!atAkM^d0Lo|{vWm5UuWzK|vJ+B?dLK`FlYf6P1oZV?d%epAeF7Yg z^QTZcivRcZ>-2QMPl4kVgU*2MhQ|UO|6D()Rj3jEcs-NI;bX4vT5YXWI3v*(2KgR z_hvb1W;{}kr;(*{pksMy_@9w+9MNoyE>#O(J2?r zm|OR)U2lZT*qNc%R}qyu8uY8mah?mh)LB+~f&d8~igw+|lA~?Y6ex$zY3d6n!`ymN zC68ow63G*ofnvp%TRz zE}7wzduM)w5qz+&;V2=a*YfB$z{L3|kgs`pj8t^NcO+-3SZ|zCnapSctR*0-LaBwG zplo6H*q1V}YFlD^1a}&v|E)AvQ*^C?pCZHHQe7{;?)$BNhAKkd@98 zz+I6pt?wA=$3TSoz>5+X5#_5E;0C(QWKmfVq90*94inL*`p>w*LPLX`jB*7FCT}12 zJoA^}oyWk%V5}pD zv?5*DIFo3H+&AMIx|)V$u_uy%4skZxAX8ReWzSdgu{!l&DMOFiMrmL5`D|LIXPq$f z7wxfzimi_LR%4M22if8pU3pV+v8a4qA4Fdjzt^cRt)LgB6@is*+X@YgW675O<1?l( zm_$?H(c0&ixKT;t_lBMDOnZdi>V~iL=oc{+O5Pf@J;pLV0T4AhY53d%!cpiIIhvN{ zm8}*3LAZvGkx6HmpcdknxtEpTjJYke{DgI^h<9|xWdEjjhHfC}yigvl?+~P8N00(_ zHURo5)dj~V0K}{>RVRa0hsl}SOnct}Q{jqfX=>oAcMYDH+ON_5;v#LGYpLNa8p{n~ zGG#c}Ig)j<{KDMF&*pyO(CN_+bxF>bC$jf%5bQ4vU2{CG{5(c*f_}eliAHl}y6B`z z+MPlA1iP~!g$bu~G)@f^@fes=84<*afguLnTOr!$ZeML#Js*vv{7+R;@+T8>J_ip? zky&IkgQll~ECQThP5CNckdl&7+k!a*ifd56Er2IUiH<7*v*&a3Quo>v;d*>VK>vE5$V|_!nkRllu3#KsqOrnk>fAPbmMf18A27GY}b_RwNZUc{c}4N)%ZW5 zD>BpzKY~mxSx9f#+ja`igN^|#X}QYsL=(-yUl_)5V++&;vlBxZjpAj}dzg7yR6v^} zx)rHPDZPX+JQ?3dSD*4ny-c<6)z(KiM>NvF-HwdU6+Ubnx{hSqy!EM(8Q~f;AENdC z;sh{_-~ack9-rZ0M-g004Am+*_f9qQ()V5YD6Mg+>2ys zGR}2mT`PydnC*S(QOgP;$5`@iC|56Zip(mu&3PRiFX%U#l0|zO-dj|)Mm3W-mZ$9a zdB5rrQY%Hw{mh1>q3F>kL-Ij~Y|puw8eQ)rxC@ynPvj*5mz$#&P@p}U>+p!jl72B1 zd}R$3<${YAE<7Det8|H;TT900u1YTS|D`YK){EzV7&5XuI zH;gxT5WK{??Il(BeTVwR5DJA(<*YPpn)`PU8n33L@r=>1z41**Zplq0Ar8XVqHa$m zWzT$(p1(%(cS;R|JsGF9-Y!nGYROI=%;#|Mk44en&|#C z&Sy6Cp|U7OGoy~TCd8gfu`NNWswp&8wi8f3iOP;mtfVW7e+OF#YcuKD7nR{9te6)U zh}ES=?;2kjdx!uP>8p(a9t47>oQ?7`m2~yLXU(=a0Pdeh%?p;hUM(-sqC)Z3(%gUb zt$fFpN~aJRm^D&(d$M;G?-60%t-SU^-pfaBTFZ)|-UobtWt-UhzO8$2b=Z8C86|AP zXJryFoDky+h6?VJZT3vAmHMS^x+4f3S7VF)^s%NEpULf)ouwZj7(RXCW7;EYGKj| zcb{?JvNSLOiyh`K-?dQVPLz?)Qm?r2MRq-8h{p9ZXF;>wPSVi+CIP_pXx?v4X_i>s zHJ04csa3`EwBCOxs1Sbf*XpN^&%GXBboKo6B=QO4b0Yet(c5b6)y4Pz!{j<)Sd8{k zl-uQ{8feT1psor3yGyy*Z%S@VYav%H5X#%-EeWTNZprPGc_dre+uYHRUz%%_uu(Z`4bMG^TIh_f*#k1W+ zhDn~U7-Sk<3e_e-`2Mp~nFNN0=Boc@X^#9@zrH@Og3^AaV4&jWZNGpkU_J5%DcS>{ z)mM7GXWtV=M-x0+(r+fa6i9)Fo{>&d95tqhB8Yc{lzTGADa)&T>{?&7iYBajgSW=1 z0LK?JE5DY{iGIPD9Wld8aUK7qQKLrz#QKbJr_8KQ5O(F+y9`$3Pb}b&^&bT%XTAN| zOg`(+i1tyGN*&O=soR+ymr6;Ko8kpYG{w?re^fOeDF&Q}h(UdQFqyG;RmA_N#N!gn zL9IVjoxSB7Q?ZkV+iol1y%eP^Rb%Xfe0f{X zm#E?U!*z_)|HB8ith19DRt#J3mrV&*aM{#f3?I0Ajg2GVv6i1$QoF<;-}+q*nHQ2? zrebdn=n%L_x!*u{(JW$M^3XIB^*86V9a;t%r3sAY1_fFQP|-G$i!mG<+EK9Cyn=~5 zr*mw6Af^g?FFbKUDFVs1eGT~@BmF}8NuPAm%vI4uveCJ7QvIM#N<`J8#YGvWkv3QA zZwcs{(;g!`9}~xm2?j`VrthbB?&`kAzHZh?n?@9~xXrK$eMUwXef4gLi_ z{GF%&R1sNv@8S`B~z)Qanh?vNBnGDPsKbACA ze>b2MWU6;<8dqm(6!g?m(!jLqse`1M^5IjLF0<#?%I?=@K|$vDoaSNI=Eyv)GYN~N zE{oG~^E64zylc74r))wa_yj?gH9=MlAj-@P18NlD4hnAQvL1TMj+SIgA0acuSxR>n z8J4tFm$ac>;+6G~S^g&aF39da*VZ1-Khax8B8id>+gN?o%6XV)M@T33{T*~fiK3Tw zSmj`o#wVZ<*CN|>R+}f*zk*94e&&f!RILL;_DS&mDG{-h^SeOaHd;1sDBy1w%w{I1 zoy`FfFRc3o=7=hqgSxeDIp{uv0ZYf#pmzVkn@`6I!p`d05rqk-=x*^v|GQi0u!4`^9*BzfuAl zcf+(Z$hBM2phjecCXo^W((XE%crg?swUwFJR;gYu!$_KX1?d36P0z)>A!tZ&*u%2d zz|0E%c%1KQh9OxdewwoJ@KggbeQ1!0^$ zVs@?AeH%m(ucRX#yiHbAztz%5PD;OcnjZO z+~`lwOWaUHtaLPx!GO3o8qA zQI?5S4~^C8jn%t}HIj*Y8X9NW8)thH=O7dB5*qK(8~^+!-tPhUyd|lFM@x8((sEah z3r$SwO-#E<%#umU3r#BOO)9-fs=&%5*MufF^d`T$Np6!#=?YEh>rENDNg0(%oeWK# z?M*q_JJ=y;!o_?M<)A^tKyMU{WEe9!dYlfWt&-b0x!jx0Qyy z$viSorY#R`qP^&j>>pkl)-HjA%p!H{@#-Y)QM9VI+);$1OOA^QiCZzs z{WZ^bB<;eputmkc`f7dd>e`mdEBY#YR4cQ#>yUB{S{!vpMcHBDm04B|HQ}6v==>0? z+H&E76^Eu3Wk}s!Q&&}ESCMUNcuo@rxV+;%8vZImzka^2d0X!F9!FixNMnN4>r05z z$il1o9++;_R@SHqm0sw@jx@L9TU#`28?(>@eZ`j>~~JwpMS5Bl`YD zjC|(k`(r%Ea{T?>%{pRCR$-#3i%@}i zLjCW=*O&>#4-^X8!lABLYuVwvzj%OO&urgGk+g?nR@~7$rOVftJ!M#mJ!_rhjibx7Q+}E5B51 zJZ(XR#9|5#voB-uy9PO>(QQ+VoS@B?H&$7Rx$zcP_7&a>>~QFLWjB@MeZ)6>nAdDUnR=f*9N!H&8c99A`LX-{gpfP?c6~+COhbM0 zB+iN`8CGqu1dL(^H^KpPR0^!OXZk!hLPVZ}ca3xW%Q8QRK5l1k6DvL0_GVc!kv!o4 zbXYWSL_730!qcodC0)8`-`eh8h$C%!dynhL^db}o`Egr6F(d2KO>LBb456Uf`+PTo zL>Cp{I{T>y`&3LJTlF-Wj?Di~OI*EO^^&-Pi6?z)c<#a}Y z{fG6*fc(CIi4Aia%#K{C02Pqw6-117zp?@ zFEyNtHGw#0(=`w7pk9)f=|TCWYz(n0YhP$^~2|XlP81 zprmv!3DQL22YYJqP9&_=GMQ;AD5$&`P)p2VT9KM?a=B2QeNvyD|A#S-^FGT-Ukl|>Xr^%KxbR4+Wx^^rkNsM#tN}g7 zqMfZwr;B?+b4?rIRE$zyWt<8$414t4S$H5-NUcZcI0LGMQ|mZ4G*F|ZI9pTIxS+$R)v<@`6pM2_-&3J`EkAH z_C}*-(<)O^hmZ72#HLzHt}?&Cqtf$d-%n7Rv6j)?ow6LP+Q{X5x?Vn6R#?^8&_ryG zOwg>~NscQv=i1^k8A-BnWkwK=N^3<&fZv-=@`PK(^^Y|3%;38 z#a>DtTZC^h8qI3M+x#3I4?H&Z zA4B#^U3Q=F?^>5-fw0wSR*HR8!R!7bjr|WumLf-zumV1J+X1#Gh3i}(tZIo7*CXz~ zL_xtuHEzH(n4}n zV@Seyl%G~4abmLex0XXy(~8zL7mFCLC)rtoo(Aa(_Z=@O-_LZqgxy*4Wkj*)O_zj;GOvk^nYankR1FEKSp1oY4CwJqd=dye z$d)GLm>8@t@IL)T_W64O?B6TWp{RE`3G2>F^6%dUkUGdgjug6aOD+x6jGc5!&VnW9 zE`{{*Vj1%be-=u{eYeHMgE)5vkNnqg_#CQ`Q5-2I-EQvXEhYC>CpO_`jr zO-W|Qj=5oN;G`keVg3G-j;A?TQKTvT2Z`<XXNJCBE;Qpz zbIus;{|p*7pS80k-oHz!OFCGJ2MnuI(R0^vBGKL#IC-E`fzmv5T!h{PC>F~IvNH@8 zu)734(%OEm=UAwv8d$4{w~@`ymP>-^EN)aF z^B07{+W4ue%cJy0qm#m~!DM!(3ptY>lL*5-a+l6Ix+L6qX&6nT0|7qc`ub4P+`LelgeHk;2NbOwVJJ#Mr3x22Ir-{YSGp6E|CDMU?cqY))N9*hQ zJ)b);)s8P@vz_m>9EImfY!oKh%HSL)h`XcA49C|s_HBFK4;3IKUHS`Jl}(X!UfZRM z@k-Y7)^BX~>QUj_nW`@lJhYazTwh3myX!h>C(>>BH=uOgVj?{7EJlBF)shtmJRpRrj*3H~#5bp0K5UZqLJkgN9 zTggoCto}OZ>f9f;TDa6%E4_HCA91&a*$`&cc(G(ye79b&-_>ZocxKj-xgHU^8L8EW8U0sD z-qEGSWGeZUCY-WPp+-p3X9=!gLrzMjM0`?)uLQI{R0Kk`whZdjkZog&A02-d&!g_; z*T!m}c77{8;U=?z&(ssbFv?5mN>#L-C*2w>jpsIQfkBm4@y!3kSF^*}>B{kRJq=fi zB3B*7=36fTRx7Dq&ZOdNWn`m&Qhd7p-G7mJ+qX1ICM;uxHy+{dSzuB2TIx4*xHBY{ zw^n&z3db1Pm^3N@LR0h7MU(Fo#?pZTk_tNF{4%OE)RExpSXX|f7b|&S^j-qqx8#uJ zo2r~iM34-Z6Bjcs^;T7FEc$CG_BfhyaF_UVl?@sGe)=z@s&Z6QJ_(Je1c*?m5nLj^ zEW%I^*WlM(!ci2d5`9z`DX?*u;(L_^?Pw)zA(MNKRP!b{uO_5;s3#lA5kuCu>A13y z_pL2|Qc5-BA6d0sn!f5fd)*dcT&V#1($O1d-zvLTXYlXre2|LxEhV*Pz(aUMo7r;( z8CQ&dd=~79WNK&$L(s>j#99hEuS~adt+8OE5!8o0@}(XlBfr9)If zIG~c;&dryW_IZ7XoT}(cY|kWP;%Ib?;qw|C(m-yG{RC#72Umy$%A^v7tRQz8!CAtC zrTjvTeu+-;CI9QpH)HsCDbMGiir5qM46+qxcN14Wg=`Y%|BoKEryk3Gq>V?*wkioV zG3SDEaklRIf0(ugh>^WpW-?rtw{ajnbdA$hiOIr{*P9TniO{o;M5~|3oU@bTwv#ca z#I~#>h3VtUq+yHn3V88xD|K;3yHN;+eoZs@=&K;|p182M`*Plm^H!)2M}x z1p^KFlh~6uErPMRfRvEquzXDv?3m$09#OfwhX_3R+gAFIdY3K?*~F+o$EJZsJ?hns z!LX7agC_GZ)X{2!VA>rITtEsqAl>y1uP-3GXNanOxKorge$mC$I{MsfvOcs|*G3!E zn;WM5p)I+9_3elAoEXlF6A{KCvJmuZ&`7e@Z2q?Rxwh0*sV9a|LU5MP%RlJ~+c{4> zv_pJ>gPj1YV|K%1`n-d&X5Xz^B%kzw&TkNY$0DF7Emm5*J zupF7wBUN>qMo2_JVPH{Vx@FOW=upU4uuNZ!V(^vXoyY$m-=`Lg0zHHf{W!aaovXi2+v%a!# z4XpV2B{;c6KRrrXwq0M2B@NdURrQts-7f#{wj2k7!IQ%fSYe35F#t4%q#r}JgQ2*? zfFKoAauqaI6?EYh4Csd>oC@ZCjD2N^=dwy`S23LKh0z6%3XZdVTP6Q;rO;iaF{MXp-Ss#+tw8jh~k?yuI}sn)-%)<@$Q%hi}# z)tGr!nWJkA`)e$BYD|)A*g&-sceH#Yl~8d8svd+4FBj}Na;MAs+x*9Xj13SZP`LK?E~>RqfFlxiw-!W+<@ z4aIW}rE~R>ISrNlmHyg|<^2S%uu3o(gu`}AKuC9r!P?q##-l zEi;WOA1xC*9V0U%3nzsCF+@y@iA$WBjhlr7#sZUI(eQ^FMzM1Xu=9wri>q>o!Z?)e zIXSsF`2@N7xVcqicm%|FM5K7+^?4;!A13$ ga{5*X8+TQ#gwF072f?~45{2an2 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

    ` | — | 生命周期 hook 配置,详见 [Hooks](../customization/hooks.md) | + +## 完整示例 + +```toml +default_model = "kimi-code/kimi-for-coding" +default_thinking = true +default_permission_mode = "manual" +default_plan_mode = false +merge_all_available_skills = true +telemetry = true + +[providers."managed:kimi-code"] +type = "kimi" +base_url = "https://api.kimi.com/coding/v1" +api_key = "" + +[models."kimi-code/kimi-for-coding"] +provider = "managed:kimi-code" +model = "kimi-for-coding" +max_context_size = 262144 + +[thinking] +mode = "auto" + +[loop_control] +max_steps_per_turn = 1000 +max_retries_per_step = 3 +reserved_context_size = 50000 + +[background] +max_running_tasks = 4 +keep_alive_on_exit = false +agent_task_timeout_s = 900 + +[[permission.rules]] +decision = "allow" +pattern = "Read" + +[[permission.rules]] +decision = "deny" +pattern = "Bash(rm -rf*)" + +[[hooks]] +event = "PreToolUse" +matcher = "Bash" +command = "node ~/.kimi-code/hooks/check-bash.mjs" +timeout = 5 +``` + +## `providers` + +`providers` 表中的每一项定义一个 API 供应商连接信息,以唯一的名称作为 key。 + +| 字段 | 类型 | 必填 | 说明 | +| --- | --- | --- | --- | +| `type` | `string` | 是 | 供应商类型,可选 `anthropic`、`openai`、`kimi`、`google-genai`、`openai_responses`、`vertexai` | +| `api_key` | `string` | 否 | API 密钥 | +| `base_url` | `string` | 否 | API 基础 URL | +| `oauth` | `table` | 否 | OAuth 凭据引用,详见下文 | +| `env` | `table` | 否 | 按供应商约定的键读取的配置子表,作为 `api_key` / `base_url` 等字段的备用来源(如 `KIMI_API_KEY`、`ANTHROPIC_API_KEY`、`GOOGLE_CLOUD_PROJECT` 等)。这只是写在配置文件里的子表,**不会真正写入到终端的环境变量**;仅在 `[providers.]` 上直接字段缺省时被 CLI 读取 | +| `custom_headers` | `table` | 否 | 请求时附加的自定义 HTTP 头 | + +OAuth 凭据引用结构: + +| 字段 | 类型 | 必填 | 说明 | +| --- | --- | --- | --- | +| `storage` | `string` | 是 | 凭据存储位置;目前只支持 `"file"` | +| `key` | `string` | 是 | 凭据条目的唯一标识 | + +```toml +[providers.openai] +type = "openai" +base_url = "https://api.openai.com/v1" +api_key = "sk-xxx" +custom_headers = { "X-Custom-Header" = "value" } +``` + +## `models` + +`models` 表中的每一项定义一个模型别名,以唯一的名称作为 key。 + +| 字段 | 类型 | 必填 | 说明 | +| --- | --- | --- | --- | +| `provider` | `string` | 是 | 使用的供应商名称,必须在 `providers` 中定义 | +| `model` | `string` | 是 | 调用 API 时使用的模型标识符 | +| `max_context_size` | `integer` | 是 | 最大上下文长度(token 数),必须大于等于 1 | +| `max_output_size` | `integer` | 否 | 单次请求的输出预算上限(即请求层面的 `max_tokens`)。目前仅 `anthropic` 供应商会读取该字段。当别名能识别到具体的 Claude 家族时,覆盖值不会超过模型允许的上限,避免超出服务端限制。省略时使用模型推导出的默认值,详见 [`providers.md`](./providers.md#anthropic)。 | +| `capabilities` | `array` | 否 | 显式追加的模型能力标签,例如 `thinking`、`image_in`、`video_in`、`audio_in`、`tool_use` | +| `display_name` | `string` | 否 | 在 UI 中显示的名称,未设置时回退到 `model` | + +`capabilities` 与供应商 capability registry 按模型名前缀自动匹配出来的能力做并集 —— 只能追加、不能移除。通常无需手写;只有当模型未被 registry 覆盖、或希望强制启用某项能力时才用得到。 + +如果模型别名包含 `.`,需要使用带引号的 key: + +```toml +[models."gpt-4.1"] +provider = "openai" +model = "gpt-4.1" +max_context_size = 1047576 +``` + +## `thinking` + +`thinking` 控制 Thinking 模式的默认行为。即便顶层 `default_thinking = true`,将 `mode` 设为 `"off"` 也会强制禁用 Thinking。 + +| 字段 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| `mode` | `string` | — | 触发策略,可选 `auto`、`on`、`off`。设为 `"off"` 时强制禁用 Thinking;其它取值或省略时不禁用,由会话内的 Thinking 开关与 `effort` 共同决定 | +| `effort` | `string` | `high` | 启用 Thinking 时使用的默认强度,可选 `low`、`medium`、`high`、`xhigh`、`max`,实际可用等级由供应商决定 | + +## `loop_control` + +`loop_control` 控制 Agent 执行循环的步数、重试和上下文压缩阈值。 + +| 字段 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| `max_steps_per_turn` | `integer` | `1000` | 单轮最大步数 | +| `max_retries_per_step` | `integer` | `3` | 单步最大重试次数 | +| `reserved_context_size` | `integer` | — | 预留给响应生成的 token 数;上下文逼近该阈值时触发压缩 | + +## `background` + +`background` 控制后台任务的运行限制。后台任务通过 `Bash` 工具或 `Agent` 工具的 `run_in_background=true` 参数启动。 + +| 字段 | 类型 | 默认值 | 说明 | +| --- | --- | --- | --- | +| `max_running_tasks` | `integer` | — | 同时运行的最大后台任务数 | +| `keep_alive_on_exit` | `boolean` | `true` | 会话关闭时是否保留仍在运行的后台任务。设为 `false` 后,`kimi -p` 完成退出、SDK 关闭 session 或 harness 关闭时会请求停止后台任务 | +| `agent_task_timeout_s` | `integer` | — | 后台 Agent 任务的最大运行时间(秒) | + +`keep_alive_on_exit` 可以被环境变量 `KIMI_CODE_BACKGROUND_KEEP_ALIVE_ON_EXIT` 覆盖;环境变量优先级高于 `config.toml`。schema 还预留了 `kill_grace_period_ms`、`print_wait_ceiling_s` 两个字段,目前仅 schema 校验通过,CLI 运行时不会读取。 + +## `services` + +`services` 配置 Kimi Code CLI 调用的内置外部服务。当前仅识别 `moonshot_search`(网页搜索)和 `moonshot_fetch`(网页抓取)两个固定 key,其他 key 会被忽略。两项的字段相同: + +| 字段 | 类型 | 必填 | 说明 | +| --- | --- | --- | --- | +| `base_url` | `string` | 否 | 服务 API URL | +| `api_key` | `string` | 否 | API 密钥 | +| `oauth` | `table` | 否 | OAuth 凭据引用,结构同 `providers.*.oauth` | +| `custom_headers` | `table` | 否 | 请求时附加的自定义 HTTP 头 | + +```toml +[services.moonshot_search] +base_url = "https://api.moonshot.cn/v1/search" +api_key = "sk-xxx" + +[services.moonshot_fetch] +base_url = "https://api.moonshot.cn/v1/fetch" +api_key = "sk-xxx" +``` + +## `permission` + +`permission` 配置会话启动时加载的初始权限规则,控制工具调用的默认审批行为。新会话的默认权限模式由顶层 `default_permission_mode` 控制;启动时显式传入的权限模式(例如 CLI 的 `--yolo`)会覆盖这个默认值。 + +规则通过 `[[permission.rules]]` 数组表写出,每条规则包含以下字段: + +| 字段 | 类型 | 必填 | 说明 | +| --- | --- | --- | --- | +| `decision` | `string` | 是 | 决策结果,可选 `allow`、`deny`、`ask` | +| `scope` | `string` | 否 | 规则作用域,可选 `turn-override`、`session-runtime`、`project`、`user`;默认 `user` | +| `pattern` | `string` | 是 | 匹配模式,格式为 `ToolName` 或 `ToolName(arg-pattern)`。`ToolName` 必须与运行时真实工具名一致——内置工具是 `Read`、`Write`、`Edit`、`Bash`、`Grep` 等(详见 [内置工具](../reference/tools.md)) | +| `reason` | `string` | 否 | 规则说明,供调试或审计使用 | + +示例: + +```toml +[[permission.rules]] +decision = "allow" +pattern = "Read" + +[[permission.rules]] +decision = "allow" +pattern = "Grep" + +[[permission.rules]] +decision = "deny" +pattern = "Bash(rm -rf*)" + +[[permission.rules]] +decision = "ask" +pattern = "Bash" +``` + +::: tip 提示 +MCP server 的声明配置写在 `~/.kimi-code/mcp.json` 或项目内 `.kimi-code/mcp.json` 中,不在 `config.toml` 里。交互式配置入口是 `/mcp-config`,详见 [Model Context Protocol](../customization/mcp.md)。 +::: diff --git a/docs/zh/configuration/data-locations.md b/docs/zh/configuration/data-locations.md new file mode 100644 index 000000000..519ad5029 --- /dev/null +++ b/docs/zh/configuration/data-locations.md @@ -0,0 +1,131 @@ +# 数据路径 + +Kimi Code CLI 将运行时数据集中存储在用户主目录下的 `~/.kimi-code/` 目录中。本页介绍各类数据的存放位置、用途,以及如何自定义和清理。 + +## 数据根目录 + +默认数据根是 `~/.kimi-code/`。`~` 由 Node.js 的 `os.homedir()` 解析,因此实际路径在不同平台略有差异:macOS 上是 `/Users//.kimi-code`,Linux 上是 `/home//.kimi-code`,Windows 上是 `C:\Users\\.kimi-code`。 + +可以通过 `KIMI_CODE_HOME` 环境变量覆盖到任意路径: + +```sh +export KIMI_CODE_HOME="$HOME/.config/kimi-code" +``` + +设置后,配置、会话、日志、输入历史、更新缓存、OAuth 凭据等运行时数据都会落到该路径下。`KIMI_CODE_HOME` 与其他环境变量的完整说明见 [环境变量](./env-vars.md)。 + +::: tip 例外 +**内置工具缓存**(例如自动下载的 ripgrep 二进制)不走 `KIMI_CODE_HOME`,而是走 `KIMI_CODE_CACHE_DIR`;未设置时使用平台缓存目录——macOS 上是 `~/Library/Caches/kimi-code`,Linux 上是 `$XDG_CACHE_HOME/kimi-code`(缺省 `~/.cache/kimi-code`),Windows 上是 `%LOCALAPPDATA%\kimi-code`。 + +用户级 Agent Skills 的搜索目录位于 `~/.kimi-code/skills` 与 `~/.agents/skills`;项目级则是工作目录下的 `.kimi-code/skills` 与 `.agents/skills`。详见 [Agent Skills](../customization/skills.md)。 +::: + +## 目录结构 + +数据根下的典型布局如下: + +``` +$KIMI_CODE_HOME (默认 ~/.kimi-code) +├── config.toml # 用户配置 +├── mcp.json # 用户级 MCP server 声明(可选) +├── session_index.jsonl # 会话索引 +├── credentials/ # OAuth 凭据根目录(目录 0o700、文件 0o600) +│ ├── .json # 托管 Kimi / Open Platform 等 provider OAuth 凭据 +│ └── mcp/ # MCP server OAuth 凭据 +│ └── -.json +├── sessions/ # 会话数据 +│ └── / +│ └── / +│ ├── state.json +│ ├── logs/ +│ │ └── kimi-code.log +│ ├── tasks/ # 后台任务持久化 +│ │ ├── .json +│ │ └── / +│ │ └── output.log +│ └── agents/ +│ ├── main/ +│ │ ├── wire.jsonl +│ │ └── plans/ # Plan 模式计划文件 +│ └── agent-0/ +│ └── wire.jsonl +├── bin/ +│ └── rg # ripgrep 缓存(Windows 为 rg.exe) +├── logs/ # 全局诊断日志 +│ └── kimi-code.log +├── updates/ +│ └── latest.json # 更新检查状态 +└── user-history/ + └── .jsonl +``` + +::: tip +上面的目录树展示的是默认数据根(`~/.kimi-code/`)下的典型布局。Agent Skills 与内置工具缓存的路径略有特殊性,详见上方"例外"提示。 +::: + +## 配置文件 + +`config.toml` 是 Kimi Code CLI 的主配置文件,存放供应商、模型、循环控制等用户级设置。详见 [配置文件](./config-files.md)。 + +`mcp.json` 是用户级 MCP server 声明,会与项目内的 `.kimi-code/mcp.json` 合并加载。字段与项目级文件相同,详见 [MCP](../customization/mcp.md)。 + +OAuth 凭据以文件形式存放在数据根下的 `credentials/` 子目录,目录权限 `0o700`、文件权限 `0o600`,仅当前用户可读写。其中: + +- **托管 Kimi / Open Platform 等供应商的 OAuth 凭据**位于 `credentials/.json`,例如 `~/.kimi-code/credentials/managed:kimi-code.json`。 +- **MCP server 的 OAuth 凭据**位于 `credentials/mcp/` 子目录下,文件名按 server key 自动生成,例如 `credentials/mcp/-.json`。 + +凭据写入采用 `tmp → fsync → rename` 的原子流程;POSIX 下严格保证原子性,Windows 上则尽最大努力保证。 + +## 会话数据 + +会话相关的数据集中在 `sessions/` 下,并通过顶层 `session_index.jsonl` 维护一份 JSONL 索引:每行一条记录,包含 `sessionId`、`sessionDir`、`workDir` 三个字段。索引在创建会话时追加写入,加载时会校验 `sessionDir` 是否仍在 `sessions/` 下、且最后一级目录名等于 `sessionId`,以防止外部篡改指向非法路径。 + +每个会话目录的路径形如 `sessions///`,其中 `workDirKey` 是按工作目录编码出来的桶名,格式为 `wd__`(例如 `wd_myproject_a3f8c1d20e9b`),`sessionId` 是会话的唯一标识。`sessions/` 整条路径包括 `/` 桶都按 `0o700` 权限创建,仅当前用户可访问。 + +会话目录的内部结构包含: + +- `state.json`:会话标题、`lastPrompt`、`createdAt`、`updatedAt`、`isCustomTitle`、`forkedFrom` 以及各个 Agent 的元数据。 +- `agents/main/wire.jsonl`:主 Agent 的 Wire 事件流(内部通信记录),用于回放和恢复。`main` 是主 Agent 的固定 id。 +- `agents/main/plans/`:Plan 模式下主 Agent 写入的计划文件,按计划 id 命名为 `.md`。 +- `agents/agent-0/`、`agents/agent-1/` 等:子 Agent 实例的目录,各自包含 `wire.jsonl`。子 Agent id 由会话内的递增计数器生成(`agent-` 加从 0 起的整数)。 +- `logs/kimi-code.log`:该会话的诊断日志。只有发生被记录的诊断事件时才会出现;普通对话不一定产生这个文件。 +- `tasks/`:后台任务持久化目录。每个任务在 `tasks/.json` 保存元信息(状态、pid、退出码等),标准输出与标准错误写入 `tasks//output.log`。任务 id 格式为 `bash-` 或 `agent-` 前缀加 8 位随机字母数字(如 `bash-a1b2c3d4`)。 + +`sessionId` 仅允许 `[A-Za-z0-9._-]+` 且不能为 `.` 或 `..`,以避免路径注入。会话列表按 `updatedAt` 倒序排序,`updatedAt` 取目录与各关键文件 mtime 的最大值。详见 [会话管理](../guides/sessions.md)。 + +## 内置工具缓存 + +Kimi Code CLI 在首次需要 ripgrep 时会自动下载并缓存。下载过程中,压缩包写入系统临时目录,校验 SHA-256 后解压,二进制直接安装到数据根下的 `bin/rg`(Windows 上为 `bin/rg.exe`)并赋予 `0o755` 执行权限。后续在同一数据根下直接复用,无需再次下载。如果系统 `PATH` 中本来就有 `rg`,会优先使用系统版本;删除 `bin/` 会在下一次需要时触发重新下载。 + +## 日志与更新状态 + +顶层 `logs/kimi-code.log` 是全局诊断日志,主要记录启动、登录、导出等不属于单个会话的问题。单个会话自己的诊断日志在 `/logs/kimi-code.log`。 + +如需报告 bug,优先使用 `kimi export` 导出相关会话(详见 [kimi 命令](../reference/kimi-command.md));如果会话日志存在,它会默认包含在导出包里。全局诊断日志默认也会打包;因为它可能包含其它会话或其它项目的事件,不想分享时使用 `--no-include-global-log` 排除。 + +`updates/latest.json` 记录通过 npm 检查到的版本更新状态,由 CLI 自动维护,通常无需手动编辑。 + +## 输入历史 + +终端中的命令输入历史按工作目录分别保存。每个工作目录对应一个文件,路径为 `user-history/.jsonl`,其中文件名是工作目录字符串的 MD5 哈希值(UTF-8 编码)。文件格式为 JSONL,每行一条历史记录。 + +输入历史用于在终端界面下浏览和搜索此前输入过的提示词。 + +## 清理数据 + +直接删除数据根目录(默认 `~/.kimi-code/`,或 `KIMI_CODE_HOME` 指定的路径)可以完全清理 Kimi Code CLI 的所有运行时数据,包括配置、会话、日志、输入历史和内置工具缓存。 + +如只需清理部分数据: + +| 需求 | 操作 | +| --- | --- | +| 重置配置 | 删除 `~/.kimi-code/config.toml` | +| 清理所有会话 | 删除 `~/.kimi-code/sessions/` 与 `~/.kimi-code/session_index.jsonl` | +| 清理诊断日志 | 删除 `~/.kimi-code/logs/` 目录 | +| 清理输入历史 | 删除 `~/.kimi-code/user-history/` 目录 | +| 重置更新检查状态 | 删除 `~/.kimi-code/updates/latest.json` | +| 强制重新下载 ripgrep | 删除 `~/.kimi-code/bin/` 目录 | +| 清除托管 Kimi / Open Platform OAuth 登录态 | 运行 `/logout`(仅清理当前供应商的 OAuth),或删除对应 `~/.kimi-code/credentials/.json` | +| 清除 MCP server OAuth 登录态 | 删除 `~/.kimi-code/credentials/mcp/` 目录;`/logout` **不会**清理 MCP 的 OAuth 凭据 | +| 移除用户级 MCP 声明 | 删除 `~/.kimi-code/mcp.json` | +| 清空用户级 Skills | 删除 `~/.kimi-code/skills/` 目录 | diff --git a/docs/zh/configuration/env-vars.md b/docs/zh/configuration/env-vars.md new file mode 100644 index 000000000..7116ab400 --- /dev/null +++ b/docs/zh/configuration/env-vars.md @@ -0,0 +1,125 @@ +# 环境变量 + +Kimi Code CLI 通过环境变量来覆盖默认路径、切换 OAuth 端点以及调整运行时行为。大部分变量在 `kimi` 进程启动时读取,少数(如遥测开关、OAuth 锁、诊断日志)在相关子系统初始化时读取。Kimi 自有变量使用 `KIMI_*` 前缀;此外,CLI 也会读取若干系统标准变量。 + +::: warning 注意 +**供应商凭证不在此列**:`KIMI_API_KEY`、`ANTHROPIC_API_KEY`、`OPENAI_API_KEY`、`GOOGLE_API_KEY` 等密钥变量**不会**从 `process.env` 自动读取。它们必须写在 `config.toml` 的 `[providers.]` 段(`api_key` / `base_url`)或 `[providers..env]` 子表中;仅在 shell 中 `export` 不会让某个供应商自动获得凭证。详见 [配置覆盖](./overrides.md#供应商凭证) 与 [供应商](./providers.md)。 +::: + +## 核心路径 + +`KIMI_CODE_HOME` 用于覆盖 Kimi Code CLI 的数据根目录,默认值是 `~/.kimi-code`。CLI 自身的应用数据、`kimi-core` 的配置、ripgrep 缓存以及 OAuth 凭证都会落在这个目录下。 + +```sh +export KIMI_CODE_HOME="/path/to/custom/kimi-code" +``` + +数据布局的详细说明请参阅 [数据路径](./data-locations.md)。 + +::: warning 注意 +设置后请确保目录可写。多个 `kimi` 实例如果共享同一个 `KIMI_CODE_HOME`,会共享配置与凭证文件。 +::: + +## 供应商凭证键名 + +下列键名出现在 `config.toml` 的 `[providers..env]` 子表中,用作供应商 `api_key` / `base_url` 的回退来源。**`kimi` 主进程不会从 `process.env` 直接读取它们**;只有 `[providers..env]` 子表内对应键的值才会被供应商客户端识别。详细解析顺序见 [配置覆盖:供应商凭证](./overrides.md#供应商凭证)。 + +| 键名 | 适用供应商 | 用途 | 默认值 | +| --- | --- | --- | --- | +| `KIMI_API_KEY` | Kimi / Moonshot | API 密钥 | 无 | +| `KIMI_BASE_URL` | Kimi / Moonshot | API 基础 URL | `https://api.moonshot.ai/v1` | +| `ANTHROPIC_API_KEY` | Anthropic | API 密钥 | 无 | +| `ANTHROPIC_BASE_URL` | Anthropic | API 基础 URL | 跟随 Anthropic SDK 默认值 | +| `OPENAI_API_KEY` | OpenAI(`openai` 与 `openai_responses` 均使用) | API 密钥 | 无 | +| `OPENAI_BASE_URL` | OpenAI(`openai` 与 `openai_responses` 均使用) | API 基础 URL | `https://api.openai.com/v1` | +| `GOOGLE_API_KEY` | Google GenAI、Vertex AI(作为 `VERTEXAI_API_KEY` 的备用) | API 密钥 | 无 | +| `VERTEXAI_API_KEY` | Vertex AI | API 密钥(未使用 ADC 时) | 无 | +| `GOOGLE_CLOUD_PROJECT` | Vertex AI | GCP 项目 ID | 无 | +| `GOOGLE_CLOUD_LOCATION` | Vertex AI | GCP 区域 | 无 | + +例如在 `config.toml` 中预置 Kimi 凭证: + +```toml +[providers.kimi.env] +KIMI_API_KEY = "sk-xxx" +KIMI_BASE_URL = "https://api.moonshot.ai/v1" +``` + +::: warning 注意 +`GOOGLE_APPLICATION_CREDENTIALS`(服务账号 JSON 路径)由 Google SDK 自身从终端环境变量中读取,是这组键名中**唯一**走系统环境变量的;它走的是 Google Cloud 标准的 ADC 流程,CLI 不参与解析。其它键名都需要写在 `[providers..env]` 子表里才会生效。 +::: + +供应商类型与字段的完整说明请参阅 [供应商](./providers.md)。 + +## OAuth 与托管服务 + +OAuth 流程默认连接 Kimi 官方的认证与托管端点,下列变量可以将它们指向自建或测试环境。 + +| 环境变量 | 用途 | 默认值 | +| --- | --- | --- | +| `KIMI_CODE_OAUTH_HOST` | OAuth 认证 host,优先级最高 | —(未设置时回退到 `KIMI_OAUTH_HOST`,再回退到下面的硬编码默认) | +| `KIMI_OAUTH_HOST` | OAuth 认证 host,作为 `KIMI_CODE_OAUTH_HOST` 的 fallback | —(未设置时回退到下面的硬编码默认) | +| `KIMI_CODE_BASE_URL` | 托管 Kimi API 的 base URL,用于 OAuth 登录后的 API 调用 | `https://api.kimi.com/coding/v1` | + +当 `KIMI_CODE_OAUTH_HOST` 和 `KIMI_OAUTH_HOST` 都未设置时,OAuth 认证 host 使用硬编码常量 `https://auth.kimi.com`。 + +::: warning 注意 +`KIMI_CODE_BASE_URL` 与上一节的 `KIMI_BASE_URL` 是两个不同变量:前者面向 OAuth 登录的托管服务,默认指向 `kimi.com`;后者面向直接使用 Kimi API 密钥的供应商,默认指向 `moonshot.ai`。请按场景区分。 +::: + +## 运行时开关 + +| 环境变量 | 用途 | 合法值 / 默认值 | +| --- | --- | --- | +| `KIMI_DISABLE_TELEMETRY` | 关闭遥测上报 | `1`、`true`、`t`、`yes`、`y`(不区分大小写) | +| `KIMI_CODE_BACKGROUND_KEEP_ALIVE_ON_EXIT` | 覆盖 `[background].keep_alive_on_exit`,控制会话关闭时是否保留仍在运行的后台任务 | 真值:`1`、`true`、`yes`、`on`;假值:`0`、`false`、`no`、`off`;未设置时读取 `config.toml`,再回退到 `true` | +| `KIMI_SHELL_PATH` | 覆盖 Windows 上 Git Bash (`bash.exe`) 的绝对路径,仅在 Windows 自动探测失败时需要 | 无 | +| `KIMI_MODEL_MAX_COMPLETION_TOKENS` | 单步 LLM 请求 `max_completion_tokens` 的期望预算(实际值按上下文窗口与输入大小再做 clamp);设为 `0` 或负数则完全禁用 clamp。**目前只对 `kimi` 类型的供应商生效**;Anthropic 等其它供应商请改用 `[models.].max_output_size`(详见 [配置文件](./config-files.md#models)) | 默认 32000,受 `loop_control.reserved_context_size` 影响 | + +例如在共享主机上禁用遥测: + +```sh +export KIMI_DISABLE_TELEMETRY="1" +``` + +`KIMI_CODE_BACKGROUND_KEEP_ALIVE_ON_EXIT` 的优先级高于 `config.toml`。例如临时运行 `KIMI_CODE_BACKGROUND_KEEP_ALIVE_ON_EXIT=0 kimi -p "..."` 时,即使配置文件里写了 `keep_alive_on_exit = true`,本次进程退出前也会请求停止后台任务。 + +## 诊断日志 + +下列变量控制 `kimi` 的诊断日志。日志会写入两个位置:全局诊断日志在 `$KIMI_CODE_HOME/logs/kimi-code.log`,每个会话自身的诊断日志在 `/logs/kimi-code.log`(路径细节见 [数据路径](./data-locations.md#日志与更新状态))。所有变量都只在进程启动时读取一次。 + +| 环境变量 | 用途 | 默认值 | +| --- | --- | --- | +| `KIMI_LOG_LEVEL` | 日志级别,可选 `off`、`error`、`warn`、`info`、`debug` | `info` | +| `KIMI_LOG_GLOBAL_MAX_BYTES` | 全局日志文件单个最大字节数 | `6291456`(6 MB) | +| `KIMI_LOG_GLOBAL_FILES` | 全局日志文件保留份数 | `5` | +| `KIMI_LOG_SESSION_MAX_BYTES` | 会话级日志文件单个最大字节数 | `5242880`(5 MB) | +| `KIMI_LOG_SESSION_FILES` | 会话级日志文件保留份数 | `3` | + +整数类变量解析失败(非正整数、非数字)时静默回落到默认值。 + +## 剪贴板桥接 + +`KIMI_WSL_CLIPBOARD_IMAGE_PATH` 由 CLI 在调用 WSL 剪贴板辅助子进程时自动注入,用于传递临时图片路径。该变量写入到 PowerShell 子进程的环境中,由子进程脚本内部读取;kimi 主进程自身不读取此变量。在外部 shell 中设置它对 kimi 主进程**无效**,用户无需手动管理此变量。 + +## 系统环境变量 + +Kimi Code CLI 也会读取一些标准的系统环境变量,用于检测运行环境与默认行为: + +- `HOME`:用户主目录,用于解析默认数据路径。 +- `VISUAL`、`EDITOR`:调用外部编辑器时的可执行命令,`VISUAL` 优先。 +- `PATH`:定位 `rg`、`git` 等外部依赖。 +- `NO_COLOR`:设置且非空时,强制关闭颜色与主题检测,界面回退到深色主题。遵循 [no-color.org](https://no-color.org) 约定。 +- `FORCE_COLOR`:值为 `"0"` 时,同样关闭颜色与主题检测,界面回退到深色主题。 +- `CI`:非空且非 `"0"` 时,关闭主题检测并回退到深色主题;遥测模块也会读取此变量以标记 CI 环境。 +- `LANG`:用于在遥测上下文中标记 locale(仅作为标记,不改变 CLI 行为)。 +- `TERM_PROGRAM`:用于检测终端对 OSC 9 通知的支持(iTerm2、WezTerm、ghostty、WarpTerminal 等);也会写入遥测上下文。 +- `TERM`:用于检测终端对 OSC 9 通知的支持(xterm-kitty、xterm-ghostty 等)。 +- `TMUX`:检测是否运行在 tmux 内,用于终端通知路径的判断。 +- `COLORFGBG`:检测终端配色(深色 / 浅色)。 +- `DISPLAY`、`WAYLAND_DISPLAY`、`XDG_SESSION_TYPE`:检测 Linux 图形会话,用于剪贴板与图片相关功能。`XDG_SESSION_TYPE` 值为 `wayland` 时也判定为 Wayland 会话。 +- `WSL_DISTRO_NAME`、`WSLENV`:检测是否运行在 WSL 内,用于剪贴板的 PowerShell 桥接回退。 +- `TERMUX_VERSION`:检测是否运行在 Termux 中。 +- `LOCALAPPDATA`:Windows 上探测 Git Bash 安装路径时使用。 + +这些变量遵循各操作系统的常规约定,`kimi` 仅读取不修改。 diff --git a/docs/zh/configuration/overrides.md b/docs/zh/configuration/overrides.md new file mode 100644 index 000000000..88ab6284e --- /dev/null +++ b/docs/zh/configuration/overrides.md @@ -0,0 +1,110 @@ +# 配置覆盖 + +Kimi Code CLI 的运行参数来自用户配置文件、命令行选项,以及若干在进程级环境变量上读取的运行时路径、端点与开关。三者面向不同场景 —— 配置文件保存长期偏好,命令行选项做本次启动的临时切换,环境变量主要负责定位数据目录、切换 OAuth 端点和少量运行时开关。 + +环境变量在 Kimi Code CLI 中**不是配置字段的通用后备来源**:它们分成下面三类,作用范围互不相同,不能简单合并成一条线性优先级。 + +## 环境变量的三类作用 + +1. **配置文件定位**:`KIMI_CODE_HOME` 决定配置文件、会话、日志等所在的数据根目录,配置文件路径变为 `$KIMI_CODE_HOME/config.toml`,否则使用 `~/.kimi-code/`。这是先于其它解析的"在哪里找配置"步骤,不是普通参数的后备来源;也无法通过 `KIMI_CONFIG_PATH` 之类的变量任意切换配置文件路径。 +2. **运行时开关**:`KIMI_DISABLE_TELEMETRY` 等少量开关会直接关闭对应子系统。即便 `config.toml` 中 `telemetry = true`,只要这个变量被设置成真值,遥测仍会被关闭——它对相关子系统的语义是"额外禁用",而不是"普通覆盖"。 +3. **运行端点与诊断**:`KIMI_CODE_OAUTH_HOST`、`KIMI_OAUTH_HOST`、`KIMI_CODE_BASE_URL`、`KIMI_LOG_LEVEL` 等供 OAuth 与诊断子系统初始化时读取。完整列表见 [环境变量](./env-vars.md)。 + +## 普通运行参数的优先级 + +对其它运行参数(模型别名、Plan / yolo 模式、Skills 目录等),按下面顺序解析: + +1. **命令行选项**:本次启动指定的参数,覆盖所有其他来源;仅对本次启动生效。 +2. **用户配置文件**:`$KIMI_CODE_HOME/config.toml`(缺省为 `~/.kimi-code/config.toml`),保存长期偏好。 + +少数环境变量会明确覆盖配置文件中的相关字段,例如 `KIMI_CODE_BACKGROUND_KEEP_ALIVE_ON_EXIT` 的优先级高于 `[background].keep_alive_on_exit`。这类例外会在 [环境变量](./env-vars.md) 与对应的 [配置文件](./config-files.md) 字段说明里标出。 + +::: warning 注意 +普通运行参数**不会**从 shell 环境变量取后备值。例如供应商 `api_key` / `base_url` 只读取 `config.toml` 中的字段(包括 `[providers..env]` 子表),不会回退到 `export KIMI_API_KEY` 这类终端变量;详见下文 [供应商凭证](#供应商凭证)。 +::: + +Kimi Code CLI 目前只读取一份用户级配置文件,没有项目级(仓库内)配置文件机制。如果需要在不同项目之间隔离配置,可以通过 `KIMI_CODE_HOME` 指向不同的数据目录(详见下文 [典型场景](#典型场景)),或在启动时用 CLI 选项临时覆盖具体字段。 + +## 配置文件 + +配置文件位置由 `KIMI_CODE_HOME` 环境变量控制,未设置时使用 `~/.kimi-code/`。文件名固定为 `config.toml`,目录会以 `0o700` 权限创建。文件内可声明 `default_provider`、`default_model`、`providers`、`models`、`thinking`、`loop_control` 等长期偏好。具体字段见 [配置文件](./config-files.md)。 + +## 供应商凭证 + +供应商凭证(`api_key`、`base_url`)的解析有自己的规则:Kimi Code CLI 只从 `config.toml` 中读取供应商字段,**不会**从 shell 环境变量取后备值。仅在终端里 `export KIMI_API_KEY` 不会让某个 `[providers.]` 自动获得凭证,必须显式写到配置文件里。 + +对单个供应商而言,凭证按以下顺序解析: + +1. `[providers.].api_key` —— 配置文件中显式写入的密钥,优先级最高。 +2. `[providers..env]` 子表中的对应键(如 `KIMI_API_KEY`、`ANTHROPIC_API_KEY`、`OPENAI_API_KEY`、`GOOGLE_API_KEY`)—— 把习惯写在 shell 里的环境变量名搬到 TOML 子表里,仅在 `api_key` 留空时生效。这只是配置子表的形式,不会真正修改进程环境。 +3. 若两者都缺,启动时会报错并提示对应的供应商缺少凭证。 + +`base_url` 的解析方式与 `api_key` 类似:先读 `[providers.].base_url`,再读 `[providers..env]` 中的 `*_BASE_URL` 键(如 `ANTHROPIC_BASE_URL`、`OPENAI_BASE_URL`、`KIMI_BASE_URL`)。供应商类型与字段的完整说明见 [供应商](./providers.md)。 + +## 进程级环境变量 + +`process.env` 中的变量在 Kimi Code CLI 启动时被读取,作用分成上文 [环境变量的三类作用](#环境变量的三类作用) 中已说明的三类: + +- **数据根目录与日志路径**:`KIMI_CODE_HOME` 切换 `~/.kimi-code/`;`KIMI_LOG_LEVEL` 等控制诊断日志。 +- **运行时开关**:`KIMI_DISABLE_TELEMETRY` 关闭遥测(会覆盖 `config.toml` 中 `telemetry = true` 的设置)。 +- **OAuth 端点与诊断**:`KIMI_CODE_OAUTH_HOST`、`KIMI_OAUTH_HOST`、`KIMI_CODE_BASE_URL` 控制托管 Kimi 登录端点;`KIMI_LOG_LEVEL` 等控制诊断日志。 +- **后台任务退出策略**:`KIMI_CODE_BACKGROUND_KEEP_ALIVE_ON_EXIT` 覆盖 `[background].keep_alive_on_exit`,用于临时决定本次进程退出时是否保留后台任务。 + +完整变量与适用范围见 [环境变量](./env-vars.md)。 + +## 命令行选项 + +启动时通过 CLI 选项指定的参数优先级最高,仅对本次启动生效。常用选项: + +| 选项 | 作用 | +| --- | --- | +| `-S, --session [id]` | 恢复指定 session;不带 id 时进入交互式选择 | +| `-C, --continue` | 续上当前工作目录的上一次 session | +| `-y, --yolo` | 自动批准普通工具调用(别名 `--yes`、`--auto-approve`) | +| `--plan` | 以 Plan 模式启动 | +| `-m, --model ` | 指定本次启动使用的模型别名 | +| `-p, --prompt ` | 以非交互模式执行单次提示词后退出 | +| `--output-format ` | 指定 `-p` 模式的输出格式,可选 `text` 或 `stream-json` | +| `--skills-dir ` | 替换自动发现的 Skills 目录(可重复指定多个,仅对本次启动生效) | + +选项互斥规则: + +- `--output-format` 只能在 prompt 模式(`-p / --prompt`)下使用。 +- `--prompt` 不能与 `--yolo` 同用,也不能与 `--plan` 同用。 +- `--prompt` 模式下使用 `-S / --session` 必须给出 session id,不接受不带 id 的交互式选择。 +- `--continue` 与 `--session` 不能同用。 +- 在非 prompt 模式下,`--yolo` 不能与 `--continue` 或 `--session` 组合;`--plan` 不能与 `--continue` 或 `--session` 组合。 +- `--yolo` 与 `--plan` 可以同时使用。 + +::: tip 提示 +`--skills-dir` 替换本次启动自动发现的 Skills 目录,适合一次性指定;若需长期追加搜索目录,可在 `config.toml` 顶层写 `extra_skill_dirs`(详见 [Agent Skills](../customization/skills.md)),两者语义不同,可按需选用。 +::: + +## 典型场景 + +**切换数据目录用于隔离测试。** `KIMI_CODE_HOME` 会同时影响配置文件、session 存档、ripgrep 缓存等所有数据位置: + +```sh +KIMI_CODE_HOME="$PWD/.kimi-sandbox" kimi +``` + +**在配置文件中预置临时凭证。** 由于供应商凭证只读取 `config.toml`,若要在一次启动里使用另一个 API key,可以预先把它写入 `[providers..env]` 子表: + +```toml +[providers.kimi.env] +KIMI_API_KEY = "sk-test" +``` + +也可以直接为该供应商写 `api_key`;两者优先级见上文 [供应商凭证](#供应商凭证)。 + +**本次启动跳过审批。** 适用于已知安全的批处理任务: + +```sh +kimi --yolo +``` + +**本次启动进入 Plan 模式。** 若希望默认行为也如此,可在配置文件中设置 `default_plan_mode = true`: + +```sh +kimi --plan +``` diff --git a/docs/zh/configuration/providers.md b/docs/zh/configuration/providers.md new file mode 100644 index 000000000..bb7e79d78 --- /dev/null +++ b/docs/zh/configuration/providers.md @@ -0,0 +1,135 @@ +# 平台与模型 + +Kimi Code CLI 通过统一的供应商抽象对接多家 LLM 平台。每个供应商负责一种 API 协议,模型则在供应商之上声明自己的名称、上下文长度和能力。本页介绍当前支持的所有供应商类型,以及如何在 `~/.kimi-code/config.toml` 中配置它们。 + +## 概述 + +`providers` 表里的 `type` 字段决定使用哪一种实现。目前支持的类型有: + +| 类型 | 协议 | 典型平台 | +| --- | --- | --- | +| `kimi` | OpenAI 兼容(chat completions 风格) | Kimi Code、Moonshot AI 开放平台 | +| `anthropic` | Anthropic Messages | Claude API | +| `openai` | OpenAI Chat Completions | OpenAI 及其兼容服务 | +| `openai_responses` | OpenAI Responses API | OpenAI 较新的 Responses 接口 | +| `google-genai` | Google GenAI | Gemini API | +| `vertexai` | Google GenAI on Vertex | Google Cloud Vertex AI | + +所有供应商默认以流式方式与模型交互;thinking、视觉、工具调用等能力按模型名前缀自动匹配,无需在配置里手写。 + +API 密钥可以写在 `api_key` 字段,也可以放在 `[providers..env]` 子表里。优先级为 `api_key` > 子表键 > 若均未配置,启动时将报错。**Kimi Code CLI 不会从 shell 环境变量自动取后备值**——仅在终端里 `export KIMI_API_KEY` 不会让某个供应商自动获得凭证,需要显式写入 `config.toml`(详见 [配置覆盖:供应商凭证](./overrides.md#供应商凭证))。`api_key` 与 `oauth` 在同一个供应商上互斥,同时设置会在解析模型时报错;OAuth 由内置登录流程自动注入,无需手写。 + +`[providers..env]` 子表可以在 `config.toml` 内直接提供凭证或端点覆盖,这些值仅对当前供应商生效,不会泄漏到全局 shell 环境: + +```toml +[providers.my-anthropic.env] +ANTHROPIC_API_KEY = "sk-ant-xxxxx" +ANTHROPIC_BASE_URL = "https://my-proxy.example.com" +``` + +切换供应商最常见的方式有两种:在 TUI 里用 `/model` 斜杠命令选择已配置的模型,或者直接编辑 `config.toml` 调整 `[providers.*]` 与 `[models.*]` 表。完整字段说明见 [配置文件](./config-files.md)。 + +## `kimi` + +`kimi` 通过 OpenAI 兼容协议对接 Moonshot AI。 + +- 默认 `base_url`:`https://api.moonshot.ai/v1` +- 环境变量:`KIMI_API_KEY`、`KIMI_BASE_URL` +- 额外能力:支持视频上传 + +```toml +[providers.kimi] +type = "kimi" +base_url = "https://api.moonshot.ai/v1" +api_key = "sk-xxxxx" +``` + +Kimi Code 托管服务在 OAuth 登录后会自动配置 `base_url` 与凭证,无需手动填写;详见 [OAuth 与凭证注入](#oauth-与凭证注入) 与 [环境变量](./env-vars.md)。 + +## `anthropic` + +`anthropic` 用于对接 Claude API。标准 Claude 模型会自动启用视觉、工具调用及 Thinking(如支持)。若使用自定义或尚未覆盖的模型,需在 `[models.]` 中显式声明 `capabilities`。 + +Thinking 可通过 `/model`、`/settings` 或配置控制。 + +- 默认 `base_url`:跟随 Anthropic SDK 默认值 +- 环境变量:`ANTHROPIC_API_KEY`、`ANTHROPIC_BASE_URL` +- 默认 `max_tokens`:按模型自动设置。如需覆盖(例如测试或为尚未识别的别名指定值),在模型别名上设置 `max_output_size`(详见 [`config-files.md`](./config-files.md#models))。已识别别名的覆盖值会被限制在服务端允许的上限内。 + +```toml +[providers.anthropic] +type = "anthropic" +api_key = "sk-ant-xxxxx" + +[models."claude-opus-4-7"] +provider = "anthropic" +model = "claude-opus-4-7" +max_context_size = 200000 +# 可选:在测试时降低输出预算,或为本 CLI 尚未识别的模型指定一个值。 +# 省略则使用上述按模型推导出的默认值。 +# max_output_size = 32000 +``` + +## `openai` + +`openai` 对应 OpenAI Chat Completions 协议,也可用来连接任何兼容该协议的第三方服务(自行覆盖 `base_url` 即可)。thinking、视觉、工具调用等能力按模型名自动推断。 + +- 默认 `base_url`:`https://api.openai.com/v1` +- 环境变量:`OPENAI_API_KEY`、`OPENAI_BASE_URL` + +```toml +[providers.openai] +type = "openai" +base_url = "https://api.openai.com/v1" +api_key = "sk-xxxxx" +``` + +## `openai_responses` + +`openai_responses` 对应 OpenAI 较新的 Responses API。它始终以流式方式工作,能力按模型名自动推断。 + +- 默认 `base_url`:`https://api.openai.com/v1` +- 环境变量:`OPENAI_API_KEY`、`OPENAI_BASE_URL` + +```toml +[providers.openai-responses] +type = "openai_responses" +base_url = "https://api.openai.com/v1" +api_key = "sk-xxxxx" +``` + +## `google-genai` + +`google-genai` 用于直连 Google Gemini API。thinking、视觉及多模态能力按模型名自动推断。 + +- 环境变量:`GOOGLE_API_KEY` + +```toml +[providers.gemini] +type = "google-genai" +api_key = "xxxxx" +``` + +## `vertexai` + +`vertexai` 与 `google-genai` 共用同一份实现,`type = "vertexai"` 时切换到 Vertex AI 的访问路径。 + +认证遵循 Google Cloud 的标准流程:通过 `gcloud auth application-default login` 或设置 `GOOGLE_APPLICATION_CREDENTIALS` 指向服务账号 JSON 完成鉴权(这一步是 Google SDK 的通用机制,与 Kimi Code 配置无关)。**项目与区域必须写在 `[providers.vertexai.env]` 子表中**——直接 `export GOOGLE_CLOUD_PROJECT`、`export GOOGLE_CLOUD_LOCATION` 不会被 CLI 读取。`GOOGLE_CLOUD_LOCATION` 缺失时,CLI 会尝试从 `base_url` 自动推断。API 密钥(`VERTEXAI_API_KEY` 或 `GOOGLE_API_KEY`)同样写在子表内。 + +```toml +[providers.vertexai] +type = "vertexai" + +[providers.vertexai.env] +GOOGLE_CLOUD_PROJECT = "my-gcp-project" +GOOGLE_CLOUD_LOCATION = "us-central1" +``` + +```sh +gcloud auth application-default login # 一次性 +kimi +``` + +## OAuth 与凭证注入 + +部分平台(如 Kimi Code 托管服务)使用 OAuth 而非静态 API 密钥。凭证由内置的 kimi-oauth 工具链在运行时注入,登录流程会自动负责写入与刷新,普通配置文件无需手工配置这部分内容。 diff --git a/docs/zh/customization/agents.md b/docs/zh/customization/agents.md new file mode 100644 index 000000000..5cf2a6ef3 --- /dev/null +++ b/docs/zh/customization/agents.md @@ -0,0 +1,45 @@ +# Agent 与子 Agent + +Kimi Code CLI 中的 Agent 是驱动会话的核心。一次会话始终由一个主 Agent 主持,主 Agent 在执行任务过程中可以根据需要派发子 Agent 来处理更聚焦的子任务。 + +主 Agent 负责理解用户的整体意图、规划步骤、与用户对话、调用工具,并最终汇总结果。它的上下文贯穿整个会话,是用户在终端中直接交互的对象。 + +子 Agent 则是主 Agent 临时派生出来的"助手"。它接受一份明确的任务描述,在自己的上下文里独立完成工作,再把结论返回给主 Agent。子 Agent 不会与用户直接对话,也不会混入主 Agent 的上下文历史。这种分工特别适合"探索代码库"、"审阅大段实现"、"规划复杂改动"这类边界清晰、需要大量阅读但产物较短的工作。 + +## 内置子 Agent + +Kimi Code CLI 内置了三种子 Agent,分别面向不同的任务形态,开箱即用: + +- **`coder`**:默认子 Agent,通用的软件工程助手,可以读写文件、执行命令、搜索代码并落地具体改动。 +- **`explore`**:代码库探索专用,只做只读操作,不会修改任何文件。适合在不修改文件的前提下快速搜索、阅读和总结仓库。 +- **`plan`**:实现规划与架构设计专用,工具集进一步收窄,连 Shell 命令都不提供,专注于"想清楚怎么做"而不是"动手做"。 + +## 调用方式 + +子 Agent 由主 Agent 自动调度。主 Agent 会根据任务复杂度、上下文消耗以及子任务的独立性,在需要时自动派发,无需用户手动指定。 + +每次派发都会在终端中以审批请求的形式呈现(除非命中已有的 allow 规则或处于 YOLO 模式),方便你审视任务描述。你可以在与主 Agent 的对话中直接指定子 Agent 类型,例如"先用 explore 把相关文件梳理一遍再动手"。 + +子 Agent 也可以放在后台运行,完成后结果会自动回到主 Agent,无需手动轮询。也可以唤回已有的子 Agent 实例继续推进同一任务。 + +## 上下文隔离与资源开销 + +每个子 Agent 都拥有完全独立的上下文窗口。它看不到主 Agent 的对话历史,只能看到主 Agent 显式传入的任务描述。子 Agent 自己的中间思考和工具调用记录不会回流到主 Agent,只有最终结果会出现在主 Agent 的上下文里。 + +这种隔离带来两个好处:一是主 Agent 的上下文保持精炼,长会话中不会被大量探索性日志撑满;二是多个子 Agent 可以并行运行,互不干扰。 + +需要注意的是,每个子 Agent 都会独立消耗模型 token,因此简单任务上没有必要派发子 Agent,主 Agent 直接处理反而更经济。子 Agent 也不支持继续嵌套调度。 + +## 权限继承 + +子 Agent 的权限规则继承自主 Agent:主 Agent 通过 `/permission` 或在审批中接受的"始终允许"规则,会自动覆盖到它派发出的所有子 Agent,子 Agent 不需要重新审批同一类工具调用。`Agent` 工具本身默认放行,因此主 Agent 可以在不打断用户的前提下完成多次委派。 + +如果你希望某一类工具在子 Agent 中始终不可用,应该收紧主 Agent 的权限规则。 + +## 会话目录中的存储位置 + +子 Agent 的运行状态会持久化到当前会话目录的 `agents/` 子目录下,每个子 Agent 实例对应一个独立的子目录,其中包含按时间顺序记录提示词、消息历史与最终状态的 `wire.jsonl` 文件。后台子 Agent 还会通过 `tasks/` 子目录暴露生命周期状态。 + +::: warning 注意 +会话目录、wire 文件和任务记录都属于本地调试材料,可能包含用户 prompt、命令输出、仓库路径、工具返回内容或凭证痕迹。不要把这些文件直接提交到公开仓库、issue 或聊天记录里;确实需要分享时,请先脱敏。 +::: diff --git a/docs/zh/customization/hooks.md b/docs/zh/customization/hooks.md new file mode 100644 index 000000000..f55c36e52 --- /dev/null +++ b/docs/zh/customization/hooks.md @@ -0,0 +1,150 @@ +# Hooks + +Hooks 让你在 Kimi Code CLI 的关键生命周期点运行本地命令。它适合做轻量的策略检查、审计记录、桌面通知或与本地自动化脚本联动,例如在危险工具调用前拦截,或在后台子 Agent 完成后触发通知。 + +Hook 命令在本地 Shell 中运行,Kimi Code CLI 会把事件 payload 以 JSON 写入命令的 stdin。命令的 stdout、stderr 和退出码决定 hook 的结果;除明确阻断的情况外,hook 失败时默认放行(fail-open),不会让主流程因为脚本异常而中断。 + +::: warning 注意 +Hooks 适合做本地提醒和轻量拦截,不应作为唯一安全边界。脚本报错、超时或返回普通非零退出码时会默认放行(fail-open);高风险工具调用仍应依赖权限审批和人工确认。 +::: + +## 配置 + +在 `~/.kimi-code/config.toml` 中使用 `[[hooks]]` 数组表声明 hook: + +```toml +[[hooks]] +event = "PreToolUse" +matcher = "Bash" +command = "node ~/.kimi-code/hooks/check-bash.mjs" +timeout = 5 + +[[hooks]] +event = "Notification" +matcher = "task\\.completed" +command = "terminal-notifier -title Kimi -message 'Background task finished'" +``` + +字段含义如下: + +| 字段 | 类型 | 必填 | 说明 | +| --- | --- | --- | --- | +| `event` | `string` | 是 | 事件名,取值必须是下文「事件」表中的某一项;其他值会让整份配置加载失败 | +| `matcher` | `string` | 否 | 用于匹配事件目标的正则表达式;缺省或空字符串表示匹配全部 | +| `command` | `string` | 是 | 要运行的 Shell 命令,长度不能为零 | +| `timeout` | `integer` | 否 | 超时时间,单位秒,范围 1–600;未设置时默认为 30 秒 | + +每个 `[[hooks]]` 表只允许出现这四个字段,写错或多写字段会导致配置文件解析失败。 + +同一次事件触发时,命中的多个 hook 会并行运行;如果多个配置项的 `command` 完全相同,只会运行一次。`matcher` 使用 JavaScript 正则表达式语义;非法正则会被静默跳过,等同于不匹配。 + +Hook 命令通过 Shell 启动(等价于 `sh -c `),子进程的工作目录就是当前会话的 `cwd`。在非 Windows 平台上,子进程会被放入独立的进程组,超时或会话被中断时会先发送 `SIGTERM`、100 毫秒后再发送 `SIGKILL`,确保 hook 内部 fork 出的子进程也能被一并清理。 + +传给 hook 的 JSON 字段统一使用 snake_case。每个 payload 都包含: + +```json +{ + "hook_event_name": "PreToolUse", + "session_id": "session_abc", + "cwd": "/path/to/project" +} +``` + +其余字段由事件类型决定,见下文事件表。 + +## 返回值 + +Hook 命令的退出码和 stdout 会被解释为以下结果: + +| 结果 | 行为 | +| --- | --- | +| 退出码 `0` | 放行;如果 stdout 是 JSON,可从 `message` 或 `hookSpecificOutput.message` 读取文本 | +| 退出码 `2` | 阻断;stderr 会作为阻断原因 | +| 其他非零退出码 | 默认放行(fail-open) | +| 超时或进程异常 | 默认放行(fail-open) | + +当 stdout 是 JSON,并且 `hookSpecificOutput.permissionDecision` 为 `deny` 时,也会被视为阻断: + +```json +{ + "hookSpecificOutput": { + "permissionDecision": "deny", + "permissionDecisionReason": "Use rg instead" + } +} +``` + +阻断只对支持控制流的事件生效。例如 `PreToolUse` 可以阻断工具调用,`Stop` 可以让当前轮次追加一次继续消息。观察型事件(例如 `PostToolUse`、`PostToolUseFailure`、`PostCompact`、`SubagentStop`、`StopFailure`、`Notification`)以「即发即忘(fire-and-forget)」方式异步触发,返回值被忽略,不会改变主流程。`PreCompact` 使用 `trigger`(而非 `triggerBlock`)调用,返回值同样被完全忽略,不属于可阻断事件。 + +阻断生效时,如果脚本未通过 stderr 或 JSON 输出提供原因,CLI 会回退到 `Blocked by hook` 作为占位原因。`PreToolUse` 阻断会作为工具失败结果写回上下文,模型可以根据原因选择替代方案。 + +## 事件 + +当前会自动触发的事件如下: + +| 事件 | Matcher | 主要 payload | 行为 | +| --- | --- | --- | --- | +| `UserPromptSubmit` | 用户提交的文本内容 | `prompt`(`ContentPart[]` 数组) | 仅对真实 User 消息触发。hook 返回的文本会包裹为 hook 结果,写入会话历史用于 transcript/replay,并展示给用户;当前 LLM 轮次会继续,但不会把 hook 结果发给模型;若 hook 阻断,阻断原因会作为 Assistant 消息返回给用户,且不再调用模型;若所有 hook 均无输出,正常 LLM 轮次继续 | +| `PreToolUse` | 工具名 | `tool_name`、`tool_input`、`tool_call_id` | 在权限检查前触发;阻断后工具不会执行 | +| `PostToolUse` | 工具名 | `tool_name`、`tool_input`、`tool_call_id`、`tool_output` | 工具成功后触发;`tool_output` 被截断至前 2000 个字符 | +| `PostToolUseFailure` | 工具名 | `tool_name`、`tool_input`、`tool_call_id`、`error` | 工具失败或被 hook 阻断后触发 | +| `Stop` | 空字符串 | `stop_hook_active` | 模型准备停止时触发;阻断后会把原因直接作为系统触发的 User 消息追加进上下文,并最多继续一次 | +| `StopFailure` | 错误类型 | `error_type`、`error_message` | 当前轮次因非取消错误失败后触发 | +| `SessionStart` | `startup` 或 `resume` | `source` | 新会话主 Agent 创建后,或历史会话恢复完成后触发 | +| `SessionEnd` | `exit` | `reason` | 会话关闭并 flush 元数据后触发 | +| `SubagentStart` | 子 Agent 名称 | `agent_name`、`prompt` | 子 Agent 配置完成、真正开始运行前触发;`prompt` 被截断至前 500 个字符 | +| `SubagentStop` | 子 Agent 名称 | `agent_name`、`response` | 子 Agent 成功完成后异步触发,失败时不触发;`response` 被截断至前 500 个字符 | +| `PreCompact` | `manual` 或 `auto` | `trigger`、`token_count` | 上下文压缩真正开始前触发;此事件使用 `trigger`(非 `triggerBlock`)调用,返回值被完全忽略,阻断决策不会被读取 | +| `PostCompact` | `manual` 或 `auto` | `trigger`、`estimated_token_count` | 上下文压缩成功写入后异步触发;阻断结果不会改变主流程 | +| `Notification` | 通知类型 | `sink`、`notification_type`、`title`、`body`、`severity`、`source_kind`、`source_id` | 当前在后台子 Agent 结果写入上下文时触发;`notification_type` 取值为 `task.completed`、`task.failed`、`task.killed` 或 `task.lost`,sink 为 `context` | + +`UserPromptSubmit` 的返回文本会被包裹成一条 hook 结果: + +```xml + +hook response + +``` + +如果多个 `UserPromptSubmit` hook 返回文本,每个结果都会拥有独立的 `` 标签。这条消息会带有 hook 结果来源,用于 transcript/replay,但不会发给模型。模型只看到原始用户输入,当前轮次继续。 + +如果 `UserPromptSubmit` hook 阻断请求,阻断原因会使用同样格式返回给用户,但本轮不会继续请求模型。 + +`Stop` 的阻断原因会直接作为系统触发的 User 消息写入上下文,让当前轮次继续: + +```text +continue from hook +``` + +## 示例:阻断危险 Shell 命令 + +下面的 hook 会在 `Bash` 工具调用前读取 stdin 中的 `tool_input.command`。如果命令包含 `rm -rf`,脚本以退出码 `2` 结束并把原因写到 stderr: + +::: warning 注意 +这个示例只演示 hook 如何阻断工具调用,不是完整的 Shell 安全解析器。真实策略更适合使用 allowlist,或用专门的 Shell 解析逻辑处理引号、变量展开、别名和多段命令。 +::: + +```toml +[[hooks]] +event = "PreToolUse" +matcher = "Bash" +command = "node ~/.kimi-code/hooks/block-dangerous-bash.mjs" +timeout = 5 +``` + +```js +let input = ''; +process.stdin.on('data', (chunk) => { + input += chunk; +}); +process.stdin.on('end', () => { + const payload = JSON.parse(input); + const command = payload.tool_input?.command ?? ''; + if (command.includes('rm -rf')) { + console.error('Blocked dangerous shell command'); + process.exit(2); + } +}); +``` + +当 hook 阻断工具调用时,Kimi Code CLI 会把阻断原因作为工具失败结果写回上下文,模型可以据此选择更安全的替代方案。 diff --git a/docs/zh/customization/mcp.md b/docs/zh/customization/mcp.md new file mode 100644 index 000000000..997032f79 --- /dev/null +++ b/docs/zh/customization/mcp.md @@ -0,0 +1,83 @@ +# Model Context Protocol + +[Model Context Protocol (MCP)](https://modelcontextprotocol.io/) 是一个开放协议,让模型可以安全地调用外部进程或服务暴露的工具。Kimi Code CLI 作为 MCP client 接入这些外部工具,并把它们与内置工具(`Read`、`Bash`、`Grep` 等)一起暴露给 Agent 使用。 + +## 集成范围 + +Kimi Code CLI 支持通过 stdio(本地子进程)和 HTTP 两种方式接入外部 MCP 服务器。接入的 MCP 工具与内置工具一样,可以被 Agent 调用、受权限规则约束、参与审批流程,行为上没有差异。 + +## 配置与登录 + +MCP server 配置写在 `mcp.json` 中,分为两层: + +- 用户级:`~/.kimi-code/mcp.json`(或 `$KIMI_CODE_HOME/mcp.json`),跨项目共享 +- 项目级:`.kimi-code/mcp.json`,仅当前仓库 + +项目级覆盖用户级同名条目。 + +最方便的入口是在 TUI 中运行 `/mcp-config`,它会引导你新增、编辑或删除 server。要查看当前连接状态,可运行 `/mcp`。 + +`mcp.json` 的顶层结构如下: + +```json +{ + "mcpServers": { + "filesystem": { + "command": "npx", + "args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"] + }, + "linear": { + "url": "https://mcp.linear.app/mcp" + } + } +} +``` + +含 `command` 字段的条目为 stdio server,含 `url` 字段的条目为 HTTP server,通常不需要手写 `transport` 字段。HTTP server 支持通过 `headers` 或 `bearerTokenEnvVar` 提供静态凭证;需要 OAuth 时,可运行 `/mcp-config login ` 完成浏览器授权。 + +可选字段: + +| 字段 | 类型 | 适用 transport | 说明 | +| --- | --- | --- | --- | +| `env` | `Record` | stdio | 注入子进程的环境变量 | +| `cwd` | `string` | stdio | 子进程工作目录 | +| `headers` | `Record` | HTTP | 附加到每次请求的静态请求头 | +| `enabled` | `boolean` | 两者 | 设为 `false` 可禁用该 server | +| `startupTimeoutMs` | `number` | 两者 | 连接超时,默认 `30000` | +| `toolTimeoutMs` | `number` | 两者 | 单次工具调用超时 | +| `enabledTools` | `string[]` | 两者 | 白名单 | +| `disabledTools` | `string[]` | 两者 | 黑名单 | + +::: warning 注意 +项目级 `.kimi-code/mcp.json` 中的 stdio 条目会在会话启动时执行本地命令,只在你信任的仓库里启用。 +::: + +## 工具命名与权限 + +MCP 工具按 `mcp____` 命名。权限匹配支持 `*` 和 `**` 通配,例如 `mcp__github__*` 命中该 server 下所有工具。 + +未命中权限规则的调用会触发审批请求;在审批弹窗中选择 "Approve for this session" 后,后续同类调用将自动放行。 + +也可以在 `config.toml` 的 `[[permission.rules]]` 中预置永久规则: + +```toml +[[permission.rules]] +decision = "allow" +pattern = "mcp__github__*" + +[[permission.rules]] +decision = "deny" +pattern = "mcp__filesystem__write_file" +``` + +`pattern` 的完整语法及 `decision`、`scope` 等字段的取值详见 [配置文件](../configuration/config-files.md#permission)。 + +## 安全性 + +- 只接入可信来源的 MCP server +- 在审批请求中检查工具名与参数是否合理 +- 对高风险工具维持手动审批,避免宽泛的 `mcp__*` 通配放行 + +::: warning 注意 +在 YOLO 模式下,MCP 工具调用会被自动批准。仅在完全信任所接入的 MCP server 时使用此模式。 +::: diff --git a/docs/zh/customization/skills.md b/docs/zh/customization/skills.md new file mode 100644 index 000000000..9b0415675 --- /dev/null +++ b/docs/zh/customization/skills.md @@ -0,0 +1,127 @@ +# Agent Skills + +Agent Skills 是 Kimi Code CLI 用来扩展模型能力的轻量机制。一个 Skill 就是一份带 YAML frontmatter 的 Markdown 文档,描述某项专业知识或工作流程。Kimi Code CLI 在启动时自动扫描已知目录,把发现的 Skill 注入到系统提示词中,让 Agent 知道当前会话里有哪些可用的 Skill。 + +相比把同样的指引每次都粘到提示词里,Skill 的好处在于:内容沉淀在文件里、可以跨项目和团队复用、可以通过斜杠命令一键加载,也可以让模型在需要时自动调用。常见用法是把代码风格、提交规范、审查流程等固化为 Skill。 + +## 创建 Skill + +Skill 文件需要放在 [已知的扫描目录](#skill-存放位置) 中。一个 Skill 可以使用两种文件结构: + +- **目录形式(推荐)**:在 Skills 目录下创建一个子目录,主文件命名为 `SKILL.md`,必要时可在同目录下放置脚本、参考资料等辅助文件。如果同一目录下同时存在 `/SKILL.md` 和同名的 `.md`,以子目录为准。 +- **扁平形式**:直接使用单个 `.md` 文件,Skill 名称取文件名(去掉 `.md`)。 + +### 文件格式 + +`SKILL.md` 由 YAML frontmatter 和 Markdown 正文两部分组成。 + +```markdown +--- +name: code-style +description: 项目代码风格规范,定义命名、缩进、注释和文件组织 +type: prompt +whenToUse: 当用户让我编写、修改或审查项目源代码时 +disableModelInvocation: false +arguments: + - target + - mode +--- + +请按下述规范处理代码: + +- 缩进使用 2 空格 +- 变量名使用 `camelCase`,类型名使用 `PascalCase` +- 公开函数必须带 TSDoc 注释 +- 单行不超过 100 字符 +``` + +### Frontmatter 字段 + +| 字段 | 说明 | +| --- | --- | +| `name` | Skill 名称。目录型 `SKILL.md` 中为必填;扁平 `.md` 文件省略时使用文件名。名称大小写不敏感。 | +| `description` | 一行总结。模型用它来判断何时使用这个 Skill。目录型 `SKILL.md` 中为必填;扁平 `.md` 文件省略时回退到正文第一行非空内容(截到 240 字符)。 | +| `type` | Skill 类型。可选 `prompt`(默认)、`inline`(与 `prompt` 语义相同)、`flow`(仅支持手动调用,不支持模型自动调用)。其它值会被跳过。 | +| `whenToUse` | 触发场景描述。也接受 `when-to-use`、`when_to_use` 写法。 | +| `disableModelInvocation` | 设为 `true` 禁止模型自动调用此 Skill。也接受 `disable-model-invocation`、`disable_model_invocation` 写法。 | +| `arguments` | 命名参数列表,可写成字符串数组或空白分隔的字符串(如 `arguments: target mode`)。声明后,正文可用 `$` 读取参数;纯数字或空字符串会被忽略。 | + +::: warning 注意 +目录型 `SKILL.md` 中 `name` 和 `description` **必须**显式填写,省略任意一项均会导致解析失败。 +::: + +### 正文占位符 + +正文在发送给模型前会展开少量占位符: + +- `$ARGUMENTS`:调用时附带的完整原始参数字符串 +- `$ARGUMENTS[0]`、`$ARGUMENTS[1]` 及简写 `$0`、`$1`:按空白分词后的位置参数(从 0 开始) +- `$`:`arguments` 中声明的命名参数 +- `${KIMI_SKILL_DIR}`:当前 Skill 文件所在目录 + +位置参数支持单双引号包裹,如 `/skill:commit "fix login" patch` 中 `$0` 展开为 `fix login`。若正文不含任何参数占位符,调用时附带的文本会以 `\n\nARGUMENTS: <文本>` 的形式追加到正文末尾。 + +## Skill 存放位置 + +Kimi Code CLI 按作用域分四档扫描,越具体的作用域优先级越高: + +**Project > User > Extra > Built-in** + +用户级: + +- `~/.kimi-code/skills/` +- `~/.agents/skills/` + +项目级(项目根 = 工作目录向上最近的包含 `.git` 的目录): + +- `.kimi-code/skills/` +- `.agents/skills/` + +额外目录通过 `config.toml` 顶层的 `extra_skill_dirs` 字段声明: + +```toml +extra_skill_dirs = ["~/team-skills", ".agents/team-skills"] +``` + +内置 Skills 随 CLI 一起分发,优先级最低。 + +## 调用 Skill + +用户可以通过斜杠命令主动调用: + +``` +/skill:code-style +/skill:git-commits 修复登录接口的并发问题 +``` + +模型也可以根据 `description` 和 `whenToUse` 自动调用 Skill(除非 `disableModelInvocation` 设为 `true` 或 `type` 为 `flow`)。模型调用时,正文先展开占位符,再注入到系统提示中。Skill 调用时最多允许嵌套 3 层,超过后会被终止。 + +## 完整示例 + +```markdown +--- +name: review-pr +description: 按团队标准审查一个 Pull Request,输出结构化的 review 报告 +type: prompt +whenToUse: 当用户让我审查 PR、检查代码变更或评估提交质量时 +arguments: + - pr_ref +--- + +请按照以下流程审查用户指定的 PR:$pr_ref + +1. 拉取并阅读 `$pr_ref` 的全部 diff。 +2. 对照以下检查项逐条核对: + - 是否包含对应的测试用例 + - 公开 API 是否有文档更新 + - 是否引入了新的依赖;若有,说明引入理由 + - 错误处理是否覆盖了边界情况 +3. 参考同目录下的检查清单:`references/checklist.md` +4. 输出一份 review 报告,包含: + - 总体结论(approve / request changes / comment) + - 必须修改项(blocking) + - 建议改进项(non-blocking) + - 值得肯定的地方 +``` + +保存为 `~/.kimi-code/skills/review-pr/SKILL.md`,检查清单放在同目录的 `references/checklist.md`,然后重开会话,即可通过 `/skill:review-pr #1234` 调用,其中 `#1234` 会展开到 `$pr_ref`。 diff --git a/docs/zh/faq.md b/docs/zh/faq.md new file mode 100644 index 000000000..f575d64b3 --- /dev/null +++ b/docs/zh/faq.md @@ -0,0 +1,3 @@ +# 常见问题 + +本页汇总新用户最常遇到的问题与简短回答。如果某一项需要更完整的解释,会在答案末尾给出相应文档的链接。 diff --git a/docs/zh/guides/getting-started.md b/docs/zh/guides/getting-started.md new file mode 100644 index 000000000..36d519e22 --- /dev/null +++ b/docs/zh/guides/getting-started.md @@ -0,0 +1,135 @@ +# 开始使用 + +## Kimi Code CLI 是什么 + +Kimi Code CLI 是一个运行在终端中的 AI Agent,帮助你完成软件开发任务和日常的终端操作。它能阅读和编辑代码、执行 Shell 命令、搜索文件与抓取网页,并在执行过程中根据反馈自主规划和调整下一步行动。 + +它适用于以下场景: + +- **编写和修改代码**:实现新功能、修复 bug、完成重构 +- **理解项目**:探索陌生的代码库,解答架构和实现层面的问题 +- **自动化任务**:批量处理文件、运行构建与测试、串联多个脚本 + +整套 CLI 以 TypeScript 编写,通过 npm 分发,运行在 Node.js 之上。 + +## 安装 + +### 脚本安装(推荐) + +最快的安装方式是使用官方安装脚本,无需预装 Node.js: + +- **macOS / Linux**: + +```sh +curl -fsSL https://code.kimi.com/kimi-code/install.sh | bash +``` + +- **Windows(PowerShell)**: + +```powershell +irm https://code.kimi.com/kimi-code/install.ps1 | iex +``` + +脚本会自动下载最新版本、校验 checksum,并把 `kimi` 可执行文件放到你的 `PATH` 中。 + +### npm 安装 + +如果你更习惯通过 npm 安装,需要 Node.js 24.15.0 或更高版本: + +```sh +node --version +``` + +包名是 `@moonshot-ai/kimi-code`: + +```sh +npm install -g @moonshot-ai/kimi-code +``` + +或用 pnpm: + +```sh +pnpm add -g @moonshot-ai/kimi-code +``` + +## 升级与卸载 + +安装完成后,验证可执行文件是否就绪: + +```sh +kimi --version +``` + +**升级**:脚本安装的用户重新运行脚本即可;npm 安装的用户执行: + +```sh +npm install -g @moonshot-ai/kimi-code@latest +``` + +**卸载**:脚本安装的用户删除 `kimi` 可执行文件即可;npm 安装的用户执行: + +```sh +npm uninstall -g @moonshot-ai/kimi-code +``` + +## 第一次启动 + +进入你想要工作的项目目录,直接运行 `kimi` 启动交互界面: + +```sh +cd your-project +kimi +``` + +如果只想执行一条指令而不进入交互界面,可以使用 `-p` 选项: + +```sh +kimi -p "帮我看一下这个项目的目录结构" +``` + +如需继续上一次会话,添加 `-C` 选项即可: + +```sh +kimi -C +``` + +首次启动时,Kimi Code CLI 尚未配置任何凭证,需要配置 API 来源才能开始对话。在交互界面中输入斜杠命令 `/login` 进入登录流程: + +``` +/login +``` + +`/login` 会弹出平台选择器,支持: + +- **Kimi Code** — OAuth 验证码流程,在任意设备打开链接、登录并输入验证码即可授权 +- **Moonshot AI Open Platform** — 直接输入 API key 登录 + +需要退出登录时,输入 `/logout` 即可清除当前凭证。 + +::: tip 提示 +如果你想使用 Anthropic、OpenAI、Google 等其他供应商,需要直接编辑 `config.toml` 配置 API 密钥,详见 [平台与模型](../configuration/providers.md)。模型、供应商等运行时配置也写入 `config.toml`。配置项说明见 [配置文件](../configuration/config-files.md)、[环境变量](../configuration/env-vars.md) 和 [配置覆盖](../configuration/overrides.md)。 +::: + +## 第一个对话 + +登录完成后,你就可以直接用自然语言向 Kimi Code CLI 描述任务。例如,让它先帮你熟悉一下当前项目: + +``` +帮我看一下这个项目的目录结构,简单介绍一下每个目录是做什么的 +``` + +Kimi Code CLI 会自动调用文件读取、搜索和网页抓取工具,浏览相关内容之后再给出回答(读取文件、搜索网页等只读操作默认自动执行,无需确认)。对于会修改文件或执行 Shell 命令的操作,它默认会在执行前征求你的确认,你可以根据需要批准或拒绝。 + +也可以让它做一些更具体的事,比如: + +``` +在 src/utils 里新增一个函数,用来把任意字符串转成 kebab-case,并补一个单元测试 +``` + +Kimi Code CLI 会规划步骤、修改代码、运行测试,并在每一步告诉你它做了什么。 + +在交互界面中,输入 `/help` 可以查看所有可用的 [斜杠命令](../reference/slash-commands.md),以及常用的快捷键提示。如果想退出 Kimi Code CLI,可以输入 `/exit`;也可以按 `Ctrl-C`,界面会先清空输入框并提示再按一次,再次按下即退出;还可以在输入框为空时连按两次 `Ctrl-D` 退出。 + +## 数据存放在哪里 + +Kimi Code CLI 的本地数据默认保存在 `~/.kimi-code/` 目录下,包含配置文件、会话记录、日志和更新缓存等。如果你想改到别的位置,可以通过环境变量 `KIMI_CODE_HOME` 指定一个新的根目录。完整的目录结构和环境变量说明见 [数据路径](../configuration/data-locations.md) 和 [环境变量](../configuration/env-vars.md)。 diff --git a/docs/zh/guides/interaction.md b/docs/zh/guides/interaction.md new file mode 100644 index 000000000..81985f119 --- /dev/null +++ b/docs/zh/guides/interaction.md @@ -0,0 +1,67 @@ +# 交互与输入 + +Kimi Code CLI 以交互式 TUI 运行在终端中,核心是一个输入框、对话视图和状态栏。本节介绍输入操作、模式切换、审批流程和常用快捷键,完整清单可通过 `/help` 查看。 + +## 输入框基本操作 + +输入框接受自由文本,按 `Enter` 发送,`Shift-Enter` 换行。输入框为空时按 `↑` / `↓` 可以浏览当前工作目录的输入历史。 + +按 `Ctrl-D` 两次退出 CLI。`Ctrl-C` 在流式输出中会中断当前轮次,空闲状态下连按两次退出。`Esc` 关闭弹窗,流式输出中也会中断轮次。 + +## 斜杠命令与 `@` 提及 + +以 `/` 开头的内容会识别为斜杠命令,涵盖会话管理、模式切换、配置等场景,如 `/help`、`/new`、`/sessions`、`/model` 等。完整清单见 [斜杠命令参考](../reference/slash-commands.md)。 + +输入 `/` 后会弹出命令补全菜单,也包括来自 [Agent Skills](../customization/skills.md) 的命令。若 skill 与内置命令重名,需要用完整形式 `/skill:` 调用。按 `Esc` 关闭菜单。 + +部分命令仅在 Agent 空闲时可用,流式输出中需先中断才能调用。`/yolo`、`/plan`、`/help` 等模式切换类命令则始终可用。 + +键入 `@` 触发文件路径补全,选中后插入相对路径,Agent 可直接读取对应文件。以点开头的目录默认隐藏,如需可显式写成 `@.github/`。 + +## Plan 模式 + +Plan 模式下,Agent 会先把计划写清楚再动手。按 `Shift-Tab` 或 `/plan` 切换;`/plan clear` 清除当前计划文件(仅限空闲状态)。 + +Agent 会先输出方案等待确认,不会直接修改项目文件。计划完成后会展示内容供你审批,可选择拒绝(保持 Plan 模式)或要求修改。退出 Plan 模式仍需你确认,即使开启了 YOLO 模式也如此。 + +## YOLO 模式 + +YOLO 模式自动批准大多数工具调用,跳过审批确认。输入 `/yolo`(或 `/yes`)切换,空闲或流式状态下都可用。 + +::: warning 注意 +YOLO 模式跳过普通审批,但不会跳过 Plan 模式的退出审批。 +::: + +## 审批流程 + +Agent 调用可能产生副作用的工具(如修改文件、执行命令)时,会弹出审批面板让你确认。YOLO 模式和 Plan 模式下的计划文件写入除外。 + +使用方向键选择选项,`Enter` 确认;按数字键 `1`/`2`/`3` 可直接选择对应位置。`Esc`、`Ctrl-C`、`Ctrl-D` 等同于拒绝。面板中通常有「Approve for this session」选项,选择后同类调用将自动放行。 + +## 流式输出期间的操作 + +Agent 思考或调用工具(流式输出)时,输入框仍可用: + +- `Esc` — 中断当前轮次。 +- `Ctrl-C` — 同样可以中断;空闲状态下连按两次退出 CLI。 +- `Ctrl-S` — 把输入框中的内容作为追加消息插入当前轮次。 +- `Ctrl-O` — 全局切换工具输出的折叠状态。 + +## 外部编辑器 + +按 `Ctrl-G` 把当前输入交给外部编辑器处理,保存后内容回填到输入框。 + +编辑器优先级:`/editor` 配置 > `$VISUAL` > `$EDITOR`。未配置时可先运行 `/editor` 选择默认编辑器。 + +## 粘贴图片与视频 + +支持在输入框中直接粘贴剪贴板里的图片或视频,交给多模态模型处理: + +- Unix(macOS / Linux):`Ctrl-V` +- Windows:`Alt-V` + +粘贴后输入框中会出现占位符,可像普通文本一样编辑,发送时自动替换为实际内容。纯文本剪贴板会回退到普通粘贴。媒体附件仅保留在当前会话中。 + +## 查看完整快捷键 + +输入 `/help` 弹出包含全部快捷键和斜杠命令的面板。`↑` / `↓` 滚动,`PageUp` / `PageDown` 翻页,`Esc` / `Enter` / `q` 关闭。 diff --git a/docs/zh/guides/migration.md b/docs/zh/guides/migration.md new file mode 100644 index 000000000..7a1b25819 --- /dev/null +++ b/docs/zh/guides/migration.md @@ -0,0 +1,37 @@ +# 从 kimi-cli 迁移 + +Kimi Code CLI 是新一代的终端 Agent,也是一个全新的起点。如果你一直在使用上一代的 kimi-cli,不必从头开始——一条命令就能把配置、MCP server 与会话历史一起迁移到新版本。 + +## 为什么迁移 + +kimi-code 基于 Node.js 重写,不再依赖 Python 或 `uv` 工具链,安装和升级更简单,也提供开箱即用的原生二进制。 + +终端界面重新设计,启动更快,运行更轻量。 + +kimi-cli 会逐步过渡到 kimi-code,迁移后你可以继续使用已有的配置和会话历史。 + +## 如何迁移 + +迁移有两种方式。 + +装好 kimi-code 之后**第一次运行 `kimi`** 时,它会自动检测 `~/.kimi/` 下是否存在 kimi-cli 的数据。一旦检测到,就会弹出迁移提示,你可以选择立即迁移、稍后再说,或不再提示。 + +你也可以**随时手动运行**: + +```sh +kimi migrate +``` + +你可以选择是否同时迁移聊天会话。如果暂时不需要历史记录,选 **Config only**;否则选 **Config + N sessions** 一并迁移。结束后会显示结果摘要。 + +## 迁移会发生什么 + +**会被迁移的内容**:配置(`config.toml`)、MCP 服务配置、输入历史,以及你选择迁移的聊天会话。 + +**不会被迁移的内容**:OAuth 登录凭证和 MCP 服务的授权都不会被复制,迁移后需要在 kimi-code 里重新执行 `/login` 和重新授权 MCP 服务。kimi-cli 的插件也不在迁移范围内。 + +::: tip 提示 +迁移**不会改动或删除** `~/.kimi/` 下的任何旧数据。kimi-cli 仍可照常使用,两者互不影响。迁移也可以重复运行,已经迁移过的会话不会被重复导入。 +::: + +迁移完成后,从 kimi-cli 导入的会话会带上 `[imported]` 标记,方便你与新建的会话区分。 diff --git a/docs/zh/guides/sessions.md b/docs/zh/guides/sessions.md new file mode 100644 index 000000000..03bcdd9cd --- /dev/null +++ b/docs/zh/guides/sessions.md @@ -0,0 +1,110 @@ +# 会话与上下文 + +Kimi Code CLI 把每次对话持久化为一个「会话」,保留消息历史和元数据,可以随时关闭终端后再回来继续。本节介绍恢复会话、上下文压缩和 TUI 内的管理方法。 + +## 会话存储 + +所有会话保存在 `$KIMI_CODE_HOME/sessions/` 下(默认 `~/.kimi-code/sessions/`),按工作目录分组存放: + +```text +~/.kimi-code/ +├── config.toml +├── session_index.jsonl +└── sessions/ + └── / + └── / + ├── state.json + └── agents/ + ├── main/ + │ └── wire.jsonl + └── / + └── wire.jsonl +``` + +- `state.json` — 会话标题、元数据等。 +- `agents/*/wire.jsonl` — Agent 事件流。 + +::: warning 注意 +`sessions/` 目录下的文件手动修改后可能导致会话无法恢复,建议不要手工编辑。 +::: + +## 启动与恢复会话 + +默认每次执行 `kimi` 都会创建新会话。如果想接着上一次继续: + +**继续当前目录最近的会话:** + +```sh +kimi --continue +``` + +**恢复指定会话:** + +```sh +kimi --session abc123 +``` + +也可以带 `-r` / `--resume`,效果相同。 + +**交互式选择:** + +```sh +kimi --session +``` + +::: warning 注意 +`--continue` 与 `--session` 互斥;`--yolo` 和 `--plan` 也不能与它们共用。 +::: + +## 在 TUI 中切换会话 + +- `/new`(`/clear`):切换到新会话。 +- `/sessions`(`/resume`):浏览并恢复历史会话。 +- `/fork`:派生当前会话(详见下文)。 +- `/title `(`/rename`):设置会话标题,方便识别。不带参数时显示当前标题。 + +`/sessions` 在流式输出期间也能浏览,但切换前需先按 `Esc` 或 `Ctrl-C` 中断。`/new`、`/fork`、`/compact` 仅在空闲时可用。 + +## 上下文压缩 + +对话变长时,Kimi Code CLI 会在上下文接近窗口上限时自动压缩历史消息。你也可以手动触发: + +```text +/compact +``` + +带上自定义指引,告诉模型压缩时优先保留哪些信息: + +```text +/compact 保留与数据库迁移相关的讨论 +``` + +## 派生会话 + +想在不破坏当前对话的前提下尝试新思路,使用 `/fork`: + +```text +/fork +``` + +派生后的会话彼此独立,不影响原会话,你随时可以切回。 + +## 导出会话 + +用 `kimi export` 打包会话为 ZIP: + +```sh +kimi export +``` + +不传 `sessionId` 时导出当前目录最近的会话(会交互式确认,加 `-y` 跳过)。用 `-o` 指定输出路径: + +```sh +kimi export -o ~/Desktop/my-session.zip +``` + +未指定 `-o` 时,ZIP 写入当前工作目录。会话目录里的诊断日志会一并打包;此外,全局诊断日志 `$KIMI_CODE_HOME/logs/kimi-code.log`(记录 TUI 启动、登录等不属于任何会话的事件)默认也会包含进来,不需要时加 `--no-include-global-log` 跳过。 + +::: tip 提示 +导出文件可能包含敏感信息,分享前请确认内容。 +::: diff --git a/docs/zh/guides/use-cases.md b/docs/zh/guides/use-cases.md new file mode 100644 index 000000000..3c70365ba --- /dev/null +++ b/docs/zh/guides/use-cases.md @@ -0,0 +1,118 @@ +# 常见使用案例 + +以下是 Kimi Code CLI 的典型使用场景和 prompt 示例。 + +## 理解陌生项目 + +接手陌生仓库时,用 `kimi --plan` 或按 `Shift-Tab` 进入 Plan 模式,让 Agent 先输出调研计划: + +``` +帮我梳理这个仓库的整体架构。重点说清楚: +1. 入口在哪里,启动后做了什么 +2. 主要模块之间的依赖关系 +3. 配置和数据的加载流程 +最后画一张简单的模块关系图。 +``` + +聚焦问题直接问: + +``` +src/runtime 下的 event loop 是怎么工作的?事件从哪里产生、又被谁消费? +``` + +``` +这个项目里「权限审批」是怎么实现的?涉及哪些文件,关键类型是什么? +``` + +大型调研可以让主 Agent 派生 **子 Agent** 并行处理子任务,详见 [Agents](../customization/agents.md)。 + +## 实现新功能 + +描述清楚需求和验收标准即可。复杂需求建议先用 Plan 模式生成方案。 + +``` +在 src/utils 下新增一个 retry 工具: +- 函数签名 retry(fn: () => Promise, options): Promise +- 支持 maxAttempts、initialDelayMs、backoffFactor 三个选项 +- 失败时抛出最后一次的错误 +- 并补一组单元测试覆盖成功、重试后成功、全部失败三种情况 +``` + +如不满意,直接告诉助手即可,无需手动编辑: + +``` +backoff 算了一个固定值,我希望加一点抖动,避免雷击效应。改一下并更新测试。 +``` + +## 修复 bug + +把现象、复现条件和期望行为一次性说清楚: + +``` +跑 npm test 时偶发地报这个错: + + TypeError: Cannot read properties of undefined (reading 'id') + at SessionStore.update (src/session/store.ts:142:18) + +只在并发触发多个 update 的用例里出现。帮我定位原因并修复,最后跑一次完整测试确认。 +``` + +不确定原因时,让它先调查: + +``` +用户反馈:登录成功后第一次刷新页面会回到登录页,再刷一次就正常了。先帮我排查可能的原因,列出几个最可疑的位置,等我确认方向后再动手改。 +``` + +纯机械任务直接放手: + +``` +跑一遍测试,失败的用例都修掉,跑完再跑一次确认全绿。 +``` + +## 写测试与重构 + +边界清晰、验收标准明确的任务特别适合交给 Agent: + +``` +src/parser/markdown.ts 目前几乎没有测试。请补一组单元测试,覆盖正常段落、嵌套列表、代码块、表格、引用块和混合场景。用项目里已有的测试风格。 +``` + +``` +把 src/handlers 下重复的「读 body → 校验 → 写日志 → 返回」逻辑抽成一个中间件。改完跑一遍测试,保证现有行为不变。 +``` + +多文件重构建议先用 Plan 模式确认方案,可用 `/fork` 试探替代方案。 + +## 一次性脚本与自动化任务 + +批量改文件、跑统计、调研对比等任务用一段 prompt 就能完成: + +``` +把 src 目录下所有 .js 文件里的 var 声明改成 const 或 let,能用 const 的优先用 const。改完跑一次 lint 确认。 +``` + +``` +分析 logs/ 下最近 7 天的访问日志,按接口路径统计调用次数、p50 和 p99 响应时间,结果输出成一个 markdown 表格。 +``` + +``` +帮我调研一下 TypeScript 里几种主流的依赖注入方案(tsyringe、inversify、awilix),从 API 风格、装饰器依赖、运行时开销三个维度对比,给一份不超过一页的建议。 +``` + +用 `--yolo` 或 `/yolo` 跳过审批,或用 [permission 配置](../configuration/config-files.md#permission) 给特定工具加白名单。 + +## 生成与维护文档 + +``` +我刚改了 src/auth/login.ts 的接口签名,把对应的 JSDoc、README 里的示例代码、还有 docs/zh/guides 下提到这个接口的段落都同步更新一遍。 +``` + +``` +src/api 下所有公开函数里,凡是没有 docstring 的都补上文档注释,风格参考已有的注释。 +``` + +``` +根据 src/cli 下的命令实现,生成一份命令参考的草稿,列出每个子命令、参数和默认值,放到 docs/zh/reference 下我后续审阅。 +``` + +需要留档或复盘时,用 `kimi export ` 打包为 ZIP。 diff --git a/docs/zh/index.md b/docs/zh/index.md new file mode 100644 index 000000000..b93405a77 --- /dev/null +++ b/docs/zh/index.md @@ -0,0 +1,13 @@ +--- +layout: home +hero: + name: Kimi Code CLI + text: The Starting Point for Next-Gen Agents + actions: + - theme: brand + text: 开始使用 + link: guides/getting-started + - theme: alt + text: GitHub + link: https://github.com/MoonshotAI/kimi-code +--- diff --git a/docs/zh/reference/keyboard.md b/docs/zh/reference/keyboard.md new file mode 100644 index 000000000..d59b249e0 --- /dev/null +++ b/docs/zh/reference/keyboard.md @@ -0,0 +1,112 @@ +# 键盘快捷键 + +Kimi Code CLI 的 TUI 交互模式支持以下键盘快捷键。键位主要在输入框中生效,部分键位在弹窗(如 `/help` 帮助面板、审批面板)或流式输出(streaming)期间有不同的行为。 + +在 TUI 中输入 `/help` 可以随时打开内置的快捷键清单。 + +## 通用快捷键 + +下列键位在输入框中始终可用: + + +| 快捷键 | 功能 | +| ------------- | ------------------------------------------- | +| `Enter` | 提交当前输入 | +| `Shift-Enter` | 在输入中插入换行 | +| `↑` / `↓` | 浏览输入历史 | +| `Esc` | 关闭弹窗 / 取消补全 / 中断流式输出或正在进行的上下文压缩(compaction) | +| `Ctrl-C` | 中断当前流式输出,或清空输入框 | +| `Ctrl-D` | 在输入框为空时退出 Kimi Code CLI | + + +**流式输出期间**按 `Ctrl-C` 会立即取消,无需二次确认。 + +**退出程序**(输入框为空时按 `Ctrl-C`,或按 `Ctrl-D`)使用「双击确认」机制:第一次按下时,状态栏会显示提示(例如 `Press Ctrl+C again to exit`),再按一次相同的键才会真正退出。如果中途按了其他键,确认状态会自动清除。 + +## 模式切换 + + +| 快捷键 | 功能 | +| ----------- | ---------- | +| `Shift-Tab` | 切换 Plan 模式 | + + +按 `Shift-Tab` 可以开启或关闭 Plan 模式。开启后,Agent 会优先使用只读工具进行研究和规划,并可以写入当前计划文件;必要时也能调用 `Bash`,它会按当前权限模式和普通规则处理,不会因为处于 Plan 模式而额外发起独立审批。单纯切换模式不会创建空计划文件。再次按 `Shift-Tab` 即可退出 Plan 模式。 + +## 输入与编辑 + + +| 快捷键 | 功能 | +| -------- | --------------------------------------- | +| `Ctrl-G` | 在外部编辑器中编辑当前输入 | +| `Ctrl-V` | 粘贴剪贴板中的图片或视频(Unix / macOS) | +| `Alt-V` | 粘贴剪贴板中的图片或视频(Windows) | +| `Ctrl-E` | 展开或折叠 Plan 卡片(无 Plan 卡片时按系统默认行为将光标移到行尾) | +| `Ctrl--` | 撤销(Undo) | + + +按 `Ctrl-G` 会打开外部编辑器,编辑当前输入内容。编辑器按以下优先级选择: + +1. `/editor` 命令配置的编辑器 +2. `$VISUAL` 环境变量 +3. `$EDITOR` 环境变量 + +保存并退出编辑器后,编辑后的内容会替换输入框中的内容;不保存退出则保持原样。 + +粘贴图片或视频时,会在输入框中显示为占位符,实际媒体数据在提交时一并发送给模型。粘贴的优先来源是系统剪贴板里的图片或文件路径;Linux 上 Wayland 与 X11 都会被尝试,WSL 下还会通过 PowerShell 兜底读取 Windows 剪贴板。 + +## 流式输出期间 + +流式输出(streaming)期间,输入框依然可以接收输入,并支持以下额外操作: + + +| 快捷键 | 功能 | +| -------- | ---------------------- | +| `Ctrl-S` | Steer:将当前输入立即注入正在运行的轮次 | +| `Esc` | 中断当前流式输出 | +| `Ctrl-C` | 中断当前流式输出 | + + +按 `Ctrl-S` 时,模型会在下一个可中断的时机立刻看到你的消息,无需等待当前轮次结束。 + +## 工具输出 + + +| 快捷键 | 功能 | +| -------- | --------- | +| `Ctrl-O` | 展开或折叠工具输出 | + + +当历史中存在折叠的工具调用结果时,按 `Ctrl-O` 可以在折叠和展开之间切换,便于查看完整的工具输出。 + +## 审批面板 + +当 Agent 发起需要确认的工具调用时,TUI 会弹出审批面板。详细的审批流程见 [交互与输入](../guides/interaction.md#审批流程),下表给出面板内可用的键位: + + +| 快捷键 | 功能 | +| --------------------------- | ----------------------------- | +| `↑` / `↓` | 在候选选项之间移动光标 | +| `Enter` | 确认当前选中的选项 | +| `1` ~ `9` | 直接选择对应序号的选项 | +| `Esc` / `Ctrl-C` / `Ctrl-D` | 拒绝当前请求 | +| `Ctrl-E` | 当面板包含 diff 或文件内容预览时,展开或折叠完整内容 | +| `Ctrl-O` | 切换其他工具输出的折叠状态 | + + +需要附带反馈的选项(例如「Reject」「Revise」)会在确认后切换到反馈输入态:直接输入反馈文本,按 `Enter` 提交;按 `Esc` 退出反馈输入并回到候选列表。 + +## 弹窗模式 + +输入 `/help` 打开帮助面板后,可使用以下键位浏览和关闭面板: + + +| 快捷键 | 功能 | +| --------------------- | ---------- | +| `↑` / `↓` | 单行滚动 | +| `PageUp` / `PageDown` | 每次滚动 10 行 | +| `Esc` | 关闭面板 | +| `Enter` | 关闭面板 | +| `q` / `Q` | 关闭面板 | + + diff --git a/docs/zh/reference/kimi-command.md b/docs/zh/reference/kimi-command.md new file mode 100644 index 000000000..78365bda8 --- /dev/null +++ b/docs/zh/reference/kimi-command.md @@ -0,0 +1,154 @@ +# kimi 命令 + +`kimi` 是 Kimi Code CLI 的主命令,用于在终端中启动一次交互式会话。不带任何参数运行时,它会在当前工作目录下开启一个新会话;配合不同的 flag,可以续上历史会话、跳过审批、从 Plan 模式开始,或者指定自定义的 Skills 目录。 + +```sh +kimi [options] +kimi [options] +``` + +## 主命令选项 + +下表列出 `kimi` 主命令支持的全部选项。所有 flag 都是可选的,直接运行 `kimi` 即可进入交互式会话。 + +| 选项 | 简写 | 说明 | +| --- | --- | --- | +| `--version` | `-V` | 打印版本号并退出。 | +| `--help` | `-h` | 显示帮助信息并退出。 | +| `--session [id]` | `-S` | 恢复一个会话。带 ID 时直接打开指定会话;不带 ID 时进入交互式选择器,从历史会话中挑选。 | +| `--continue` | `-C` | 继续当前工作目录下最近一次的会话,无需手动指定 ID。 | +| `--model ` | `-m` | 为本次启动指定模型别名。省略时,新会话使用配置文件中的 `default_model`,恢复会话使用会话当前模型。 | +| `--prompt ` | `-p` | 非交互执行单次 prompt,并把 Assistant 输出流式写到 stdout。该模式会使用 `auto` 权限处理工具调用,不会打开 TUI。 | +| `--output-format ` | | 设置非交互输出格式,支持 `text` 与 `stream-json`。仅可与 `--prompt` 一起使用,默认 `text`。 | +| `--yolo` | `-y` | 自动批准普通工具调用,跳过审批请求;Plan 模式中的 `Bash` 审批和退出审批不会被跳过。 | +| `--plan` | | 以 Plan 模式启动新会话,AI 会优先使用只读工具进行探索和规划,可以写入当前计划文件;Plan 模式中的 `Bash` 按权限模式单独处理。 | +| `--skills-dir ` | | 从指定目录加载 Skills,替换自动发现的用户和项目目录。可重复传入以叠加多个目录。详见下文 [自定义 Skills 目录](#自定义-skills-目录)。 | + +`-r` / `--resume` 是 `--session` 的隐藏别名;`--yes` 和 `--auto-approve` 是 `--yolo` 的隐藏别名。它们在帮助信息中不会显示,行为与对应的官方 flag 完全一致。 + +::: warning 注意 +`--yolo` 会跳过普通工具调用的人工确认,包括文件写入和 Shell 命令执行。请只在受信任的工作目录下使用。Plan 模式的退出审批不会被 `--yolo` 跳过;Plan 模式下的 `Bash` 也按 `--yolo` 的普通放行规则处理。 +::: + +### flag 冲突规则 + +以下组合会在启动时被拒绝: + +- `--continue` 与 `--session` 互斥:两者都表示"恢复历史会话",含义重叠。 +- `--yolo` 不能与 `--continue` 或 `--session` 同时使用:恢复会话时会沿用原会话的审批设置。此规则仅适用于交互式模式;在 `--prompt` 模式下,`--yolo` 已因与 `--prompt` 互斥而被更早拦截。 +- `--plan` 不能与 `--continue` 或 `--session` 同时使用:Plan 模式只对新会话生效。 +- `--prompt` 不能与 `--yolo` 或 `--plan` 同时使用:非交互模式固定使用 `auto` 权限,并且不进入 Plan 模式。 +- `--prompt` 可以与 `--continue` 或带 ID 的 `--session ` 一起使用;不带 ID 的 `--session` 会尝试打开选择器,因此不能用于非交互模式。 +- `--output-format` 只能与 `--prompt` 一起使用;交互式 TUI 不支持把完整事件流写成 stdout JSONL。 + +如果需要在恢复会话时强制使用 YOLO 或 Plan 模式,请改在交互式会话内通过斜杠命令切换。 + +## 典型用法 + +最常见的入口是直接运行 `kimi`,在当前目录开启一次全新的会话: + +```sh +kimi +``` + +如果上一次会话被打断(关闭终端、网络断开等),想从断点继续,使用 `--continue`: + +```sh +kimi --continue +``` + +它会自动找到当前工作目录下时间最近的那个会话并恢复。若想挑选其他历史会话,运行 `kimi --session` 进入交互式选择器,或者直接传入已知的 session ID: + +```sh +kimi --session 01HZ...XYZ +``` + +当任务比较琐碎,不希望被频繁的审批请求打断时,可以加上 `--yolo`: + +```sh +kimi --yolo +``` + +需要先让 AI 阅读代码、产出实现计划,而不是立刻动手修改文件时,使用 `--plan` 进入 Plan 模式: + +```sh +kimi --plan +``` + +### 自定义 Skills 目录 + +如果需要加载自定义的 Skills 目录,可以通过两种方式指定: + +- **CLI flag `--skills-dir `**:可重复传入,会**替换**自动发现的用户和项目目录,适合临时切换或在脚本中使用。例如同时挂载两个目录: + + ```sh + kimi --skills-dir /path/to/team-skills --skills-dir ./local-skills + ``` + +- **`config.toml` 的 `extra_skill_dirs`**:在配置文件中追加额外目录,与自动发现的目录**叠加**生效,适合长期配置团队共享 Skills(详见 [Agent Skills](../customization/skills.md))。 + +## 非交互执行 + +需要在脚本或 CI 中运行单次 prompt 时,使用 `-p`: + +```sh +kimi -p "Summarize the current repository status" +``` + +输出采用 transcript 样式:thinking 内容和 Assistant 正文都会以 `• ` 开头,换行后使用两个空格缩进。Assistant 正文会输出到 stdout;thinking、工具进度和 `To resume this session: kimi -r ` 提示输出到 stderr。`-p` 模式不会请求人工审批,普通工具调用、Plan 审批和 Agent 提问都会按 `auto` 权限策略处理。静态 deny 规则仍然会阻止匹配的工具调用。 + +需要临时切换模型时,加上 `-m`: + +```sh +kimi -m kimi-code/kimi-for-coding -p "Explain the latest diff" +``` + +如果脚本需要结构化读取输出,可以使用 JSONL: + +```sh +kimi -p "List changed files" --output-format stream-json +``` + +`stream-json` 模式下,stdout 每行都是一个 JSON 对象。普通回复会输出 Assistant 消息;如果模型调用工具,会先输出带 `tool_calls` 的 Assistant 消息,再输出对应的 Tool 消息,最后继续输出后续 Assistant 消息。thinking 内容不会写入 JSONL;工具进度和恢复会话提示仍然写到 stderr。 + +## 子命令 + +### `kimi export` + +把一个会话打包成 ZIP 文件,便于分享、归档或者提交问题反馈。导出的压缩包包含会话目录下的所有文件,例如上下文记录、状态文件和会话诊断日志(如果该会话已经产生 `logs/kimi-code.log`)。 + +```sh +kimi export [sessionId] [options] +``` + +| 参数 / 选项 | 简写 | 说明 | +| --- | --- | --- | +| `sessionId` | | 要导出的会话 ID。省略时会自动选择当前工作目录下最近一次的会话,并要求确认。 | +| `--output ` | `-o` | 输出 ZIP 文件路径。省略时写入当前目录下的默认文件名。 | +| `--yes` | `-y` | 跳过默认会话的确认提示,直接导出。 | +| `--no-include-global-log` | | 不打包当前活动的全局诊断日志,即 `~/.kimi-code/logs/kimi-code.log`。默认包含。 | + +默认导出包含目标会话目录内的文件;如果会话目录里有 `logs/kimi-code.log`,会一并出现在 ZIP 的 `logs/kimi-code.log`。全局诊断日志 `~/.kimi-code/logs/kimi-code.log` 默认也会打包,因为它可能包含其它会话或其它项目的事件,如果不想分享可以加 `--no-include-global-log`。加上后,ZIP 内路径是 `logs/global/kimi-code.log`,不会包含轮转出来的 `kimi-code.log.1` 等旧文件。 + +省略 `sessionId` 时,命令会先打印待导出的会话信息并请求确认;用 `-y` 可以跳过确认,适合在脚本里使用: + +```sh +# 导出当前工作目录最近一次会话,跳过确认 +kimi export -y + +# 导出指定会话到自定义路径 +kimi export 01HZ...XYZ -o ./bug-report.zip + +# 排除全局诊断日志,避免分享其它会话的事件 +kimi export 01HZ...XYZ -o ./bug-report.zip --no-include-global-log +``` + +### `kimi migrate` + +将旧版 kimi-cli 的本地数据迁移到 kimi-code。该命令无任何 flag,纯交互式运行,会引导你完成数据迁移的全流程。 + +```sh +kimi migrate +``` + +如果你之前使用过旧版 kimi-cli,可以运行此命令将历史会话、配置等数据迁移到 kimi-code 中,避免数据丢失。完整的迁移流程、迁移内容与注意事项见 [从 kimi-cli 迁移](../guides/migration.md)。 diff --git a/docs/zh/reference/slash-commands.md b/docs/zh/reference/slash-commands.md new file mode 100644 index 000000000..ddecaed21 --- /dev/null +++ b/docs/zh/reference/slash-commands.md @@ -0,0 +1,86 @@ +# 斜杠命令 + +斜杠命令是 Kimi Code CLI 在交互式 TUI 中提供的内置控制命令,用于切换模式、管理会话、查看状态等。在输入框中输入 `/` 即可触发命令补全,候选列表会随后续字符实时过滤;命令的别名(alias)也会一并参与匹配。 + +输入完整命令名(如 `/help`)后按 `Enter` 即可执行。如果输入的 `/` 开头内容不匹配任何内置或 Skill 命令,则按普通消息发送给 Agent。 + +::: tip 提示 +部分命令仅在空闲(idle)状态下可用。会话正在流式输出或正在压缩上下文时执行这些命令会被拦截,并提示先按 `Esc` 或 `Ctrl-C` 中断当前操作。下表的 「随时可用」 列标注了在流式输出 / 上下文压缩期间也可用的命令。 +::: + +## 账号与配置 + +| 命令 | 别名 | 说明 | 随时可用 | +| --- | --- | --- | --- | +| `/login` | — | 选择账号或平台并登录:Kimi Code 走 OAuth device code 流程,Moonshot AI 开放平台通过 API 密钥登录。 | 否 | +| `/logout` | — | 清除当前所选账号的凭据(Kimi Code OAuth 凭据,或对应开放平台的供应商配置)。 | 否 | +| `/model` | — | 切换当前会话使用的 LLM 模型。 | 是 | +| `/settings` | `/config` | 打开 TUI 内的设置面板。 | 是 | +| `/permission` | — | 选择权限模式(permission mode)。 | 是 | +| `/editor` | — | 配置 `Ctrl-G` 调起的外部编辑器。 | 是 | +| `/theme` | — | 切换终端 UI 配色主题。 | 是 | + +## 会话管理 + +| 命令 | 别名 | 说明 | 随时可用 | +| --- | --- | --- | --- | +| `/new` | `/clear` | 开启一个全新会话,丢弃当前上下文。 | 否 | +| `/sessions` | `/resume` | 浏览历史会话并切换/恢复。 | 是 | +| `/tasks` | `/task` | 浏览后台任务列表。 | 是 | +| `/fork` | — | 基于当前会话 fork 一份新会话,保留完整对话历史。 | 否 | +| `/title []` | `/rename` | 不带参数时显示当前会话标题;带参数时将其设置为新标题(最长 200 个字符)。 | 是 | +| `/compact []` | — | 压缩当前对话上下文,释放 token 占用;可选附带一段自定义指令,提示模型在压缩时保留哪些信息。 | 否 | +| `/init` | — | 分析当前代码库并生成 `AGENTS.md`。 | 否 | + +## 模式与运行控制 + +| 命令 | 别名 | 说明 | 随时可用 | +| --- | --- | --- | --- | +| `/yolo [on\|off]` | `/yes` | 切换自动批准模式(auto-approve)。不带参数时按当前状态翻转;显式传 `on`/`off` 时强制设为对应状态。开启后跳过普通工具调用审批;Plan 模式的退出审批不会被跳过。 | 是 | +| `/plan [on\|off]` | — | 切换 Plan 模式。不带参数时按当前状态翻转;显式传 `on`/`off` 时强制设为对应状态。单纯切换不会创建空计划文件。 | 是 | +| `/plan clear` | — | 清除当前 plan 方案。 | 否 | + +::: warning 注意 +`/yolo` 会跳过普通工具调用的审批确认,使用前请确保了解可能的风险。Plan 模式的退出审批不会被 `/yolo` 跳过;Plan 模式下的 `Bash` 也按 `/yolo` 的普通放行规则处理。 +::: + +## 信息与状态 + +| 命令 | 别名 | 说明 | 随时可用 | +| --- | --- | --- | --- | +| `/help` | `/h`、`/?` | 显示快捷键和所有可用命令。 | 是 | +| `/usage` | — | 显示 token 用量、上下文占用以及配额信息。 | 是 | +| `/status` | — | 显示当前会话运行时状态,包括版本、模型、工作目录和权限模式等。 | 是 | +| `/mcp` | — | 列出当前会话中的 MCP server 及其连接状态。 | 是 | +| `/version` | — | 显示 Kimi Code CLI 版本号。 | 是 | +| `/feedback` | — | 提交反馈以改进 Kimi Code CLI。 | 是 | + +## 退出 + +| 命令 | 别名 | 说明 | 随时可用 | +| --- | --- | --- | --- | +| `/exit` | `/quit`、`/q` | 退出 Kimi Code CLI。 | 否 | + +## Skill 动态命令 + +除内置命令外,用户可激活的 Skill 会自动注册为斜杠命令,统一以 `skill:` 作为命名空间前缀: + +``` +/skill: [附加文本] +``` + +例如 `/skill:code-style` 会加载名为 `code-style` 的 Skill 内容并发送给 Agent;命令后附带的文本会拼接到 Skill 提示词之后,例如 `/skill:git-commits 修复登录失败的问题`。 + +为方便输入,Skill 命令同时支持省略 `skill:` 前缀的简写形式 `/`,前提是该名称未被内置命令占用。也就是说,`/code-style` 会回退匹配到 `/skill:code-style`。 + +Kimi Code CLI 随包内置了 `mcp-config` Skill,用于配置 MCP server 和处理 MCP OAuth 登录。它在补全和帮助里仍属于 Skill 命名空间(`/skill:mcp-config`),也可以直接输入 `/mcp-config` 调用。 + +可作为斜杠命令暴露的 Skill 类型包括 `prompt`、`inline`、`flow` 以及未显式声明类型的 Skill。Skill 的安装与编写详见 [Agent Skills](../customization/skills.md)。 + +::: info 说明 +所有 Skill 命令仅在空闲状态下可用,流式输出或上下文压缩期间需先按 `Esc` 或 `Ctrl-C` 中断当前操作。 +::: + +::: info 说明 +Flow 类型的 Skill 同样通过 `/skill:` 暴露,没有独立的 `/flow:` 命名空间。 +::: diff --git a/docs/zh/reference/tools.md b/docs/zh/reference/tools.md new file mode 100644 index 000000000..06ef48f76 --- /dev/null +++ b/docs/zh/reference/tools.md @@ -0,0 +1,104 @@ +# 内置工具 + +内置工具是 Kimi Code CLI 随核心引擎一起提供的工具集,无需安装 MCP server 即可使用。Agent 在每次对话中会根据任务需要自动选择并调用这些工具;用户也可以通过权限审批界面查看每次工具调用的细节。 + +与 MCP 工具相比,内置工具由运行时直接管理,生命周期与会话绑定,无需外部进程。两者都遵循统一的审批机制:**只读类工具**(如 `Read`、`Grep`、`Glob`、`WebSearch` 等)默认自动放行,**写入与执行类工具**(如 `Write`、`Edit`、`Bash`、`TaskStop`)默认需要用户审批。在 YOLO 模式下,普通工具调用的审批会被跳过,但 Plan 模式下的退出审批不受影响。 + +## 文件类 + +文件类工具负责读取、写入、搜索本地文件系统,是代码分析和修改任务的基础工具。 + +| 工具 | 默认审批 | 说明 | +| --- | --- | --- | +| `Read` | 自动放行 | 读取文本文件内容 | +| `Write` | 需审批 | 创建或覆盖文件 | +| `Edit` | 需审批 | 精确字符串替换 | +| `Grep` | 自动放行 | 基于 ripgrep 的全文搜索 | +| `Glob` | 自动放行 | 按 glob 模式查找文件 | +| `ReadMediaFile` | 自动放行 | 读取图片或视频文件 | + +**`Read`** 接受文件路径(`path`)以及可选的 `line_offset`(起始行号,支持负数从末尾倒数)和 `n_lines`(读取行数上限)。单次最多返回 1000 行或 100 KB,超出部分会附带截断提示。如果文件是图片或视频,工具会提示改用 `ReadMediaFile`。 + +**`Write`** 接受 `path`、`content` 和可选的 `mode`(`overwrite` 或 `append`,默认覆盖)。父目录必须已存在;`append` 模式将内容追加到文件末尾,不自动添加换行。 + +**`Edit`** 接受 `path`、`old_string`(要替换的精确文本)和 `new_string`(替换后的文本)。默认只替换唯一一处匹配,若文件中存在多处相同内容会报错并提示使用 `replace_all: true`。`old_string` 与 `new_string` 不能相同。 + +**`Grep`** 调用 ripgrep 搜索文件内容,支持正则表达式(`pattern`)、搜索路径(`path`)、文件类型过滤(`type`,如 `ts`、`py`)、glob 过滤(`glob`)和输出模式(`output_mode`:`files_with_matches` / `content` / `count_matches`,默认 `files_with_matches`)。`content` 模式支持 `-A`、`-B`、`-C` 上下文行参数与 `-i`(忽略大小写)、`-n`(行号,默认 true)、`multiline`(跨行匹配)。所有模式均支持 `offset` + `head_limit` 分页,`head_limit` 默认 250、传 0 表示不限。`files_with_matches` 模式的结果按文件最近修改时间倒序排列;其他模式保持 ripgrep 原始输出顺序。`.env`、私钥、`.aws/credentials`、`.gcp/credentials` 等敏感文件会被自动过滤;`include_ignored=true` 可同时搜索被 `.gitignore` 等忽略的文件,但敏感文件仍保持过滤。 + +**`Glob`** 按 glob 模式(`pattern`)在指定目录(`path`,默认为工作目录)中匹配文件,结果按修改时间倒序排列,最多返回 1000 条。可选参数 `include_dirs`(默认 true)控制是否返回目录条目。纯通配符模式(如 `**`、`**/*`)会被拒绝并提示添加字面锚点;包含花括号扩展(`{a,b,c}`)的模式同样会被拒绝——底层 glob 引擎把 `{`、`}` 当字面量处理,这类模式会静默匹配 0 个文件。 + +**`ReadMediaFile`** 将图片或视频文件以多模态内容发送给模型,仅接受 `path`。文件大小上限为 100 MB。工具是否可用取决于当前模型的视觉能力(`image_in` / `video_in`)。 + +## Shell + +| 工具 | 默认审批 | 说明 | +| --- | --- | --- | +| `Bash` | 需审批 | 执行 Shell 命令 | + +**`Bash`** 是最通用也是权限要求最严格的工具。它接受 `command`(必填)以及可选的 `cwd`(工作目录)、`timeout`(毫秒)、`description`(后台任务描述,`run_in_background=true` 时必填)、`run_in_background`(是否以后台任务运行)和 `disable_timeout`(后台任务是否取消超时)。前台 `timeout` 默认 60 秒、最长 5 分钟;后台 `timeout` 默认 10 分钟、最长 10 分钟。 + +前台模式下 `Bash` 会阻塞当前轮次,直到命令结束或超时;后台模式会立即返回任务 ID。后台任务默认 10 分钟后超时;如果确实需要让任务不受超时限制,可以设置 `disable_timeout=true`。任务结束、失败或被停止时会自动通知 Agent 继续处理,过程中也可通过 `TaskOutput` 主动查看结果。stdin 始终被关闭,交互式命令会立即收到 EOF。两阶段终止策略(SIGTERM → 5 秒宽限期 → SIGKILL)确保超时后进程能可靠结束。Windows 平台下默认使用 Git Bash 作为 shell。 + +## 网络类 + +| 工具 | 默认审批 | 说明 | +| --- | --- | --- | +| `WebSearch` | 自动放行 | 网络搜索 | +| `FetchURL` | 自动放行 | 获取指定 URL 的内容 | + +**`WebSearch`** 接受 `query`(搜索词)和可选的 `limit`(返回结果数,1–20,默认 5)及 `include_content`(是否返回网页正文,默认 false,开启后消耗 token 较多)。该工具需要宿主提供搜索实现,未注入实现时不会出现在工具列表中。 + +**`FetchURL`** 接受单个 `url` 参数,返回页面内容。对于 HTML 页面,宿主会提取正文文章(`extracted`)而非返回完整 HTML;纯文本或 Markdown 页面则直接透传(`passthrough`)。同样需要宿主注入实现。 + +## Plan 模式 + +| 工具 | 默认审批 | 说明 | +| --- | --- | --- | +| `EnterPlanMode` | 自动放行 | 进入 Plan 模式 | +| `ExitPlanMode` | 自动放行(需用户确认计划) | 退出 Plan 模式并提交计划 | + +Plan 模式是一种受约束的工作状态:进入后 `Write` 与 `Edit` 工具被收紧——只允许写入当前的计划文件,其它路径会被阻断;`TaskStop` 也被完全拦截。其余工具(包括 `Bash`)仍按当前权限规则处理,因此通过 `Bash` 调用的命令理论上仍可能修改文件,是否放行取决于当前的审批策略。 + +**`EnterPlanMode`** 不接受任何参数,进入成功后返回工作流指引,包括计划文件路径(如果宿主提供了的话)。 + +**`ExitPlanMode`** 读取当前计划文件内容,将计划呈现给用户审批后退出 Plan 模式。可选参数 `options` 允许 Agent 提供 1–3 个备选方案(每项含 `label` 与 `description`,`label` 最长 80 字符),供用户在审批时选择;标签需互不重复,且不能使用 `Approve`、`Reject`、`Reject and Exit`、`Revise` 这些保留词(系统用它们标记审批结果)。用户批准后,所有工具重新可用;若用户要求修改,Agent 将继续留在 Plan 模式中。 + +## 状态管理 + +| 工具 | 默认审批 | 说明 | +| --- | --- | --- | +| `TodoList` | 自动放行 | 管理任务待办列表 | + +**`TodoList`** 用于在多步骤操作中维护一份可见的子任务列表,状态存储在 Agent 会话内。`todos` 参数接受一个数组,每项含 `title`(标题)和 `status`(`pending` / `in_progress` / `done`);省略 `todos` 则仅查询当前列表,传入空数组则清空列表。 + +## 协作类 + +协作类工具负责 Agent 间协作、用户交互和 Skill 调用。 + +| 工具 | 默认审批 | 说明 | +| --- | --- | --- | +| `Agent` | 自动放行 | 派生子 Agent 执行子任务 | +| `AskUserQuestion` | 自动放行 | 向用户提问以获取结构化输入 | +| `Skill` | 自动放行 | 调用已注册的 inline Skill | + +**`Agent`** 用于将子任务委托给子 Agent 执行。必填参数为 `prompt`(完整任务描述)和 `description`(3–5 个词的简短说明,用于 UI 展示)。可选参数包括 `subagent_type`(Agent 类型,默认 `coder`)、`resume`(恢复已有 Agent 的 ID)、`run_in_background`(是否后台运行,默认 false)和 `timeout`(超时秒数,30–3600)。`subagent_type` 与 `resume` 互斥:恢复已有 Agent 时只通过 ID 寻址。前台 `timeout` 缺省表示不超时,子 Agent 运行至完成;后台 `timeout` 缺省时回落到 `config.toml` 的 `[background] agent_task_timeout_s`,该字段也未设置则无时间上限。前台模式下父 Agent 会等待子 Agent 完成再继续;后台模式立即返回任务 ID,完成时通过合成 user 消息自动回到主 Agent,无需轮询。子 Agent 体系细节参见 [子 Agent](../customization/agents.md)。 + +**`AskUserQuestion`** 以结构化多选题的形式向用户提问,适用于需要用户消歧或选择方案的场景。`questions` 参数接受 1–4 道题,每道题需提供 `question`(问题文本,以 `?` 结尾)、`options`(2–4 个选项,每项含 `label` 和 `description`)以及可选的 `header`(最多 12 字符的短分类标签,如 `Auth`、`Style`)和 `multi_select`(是否多选,默认 false)。系统会自动附加「其他」选项,无需在 `options` 中手工提供。若宿主未实现交互式提问能力,本工具会返回失败提示,Agent 应改为直接在文本回复中向用户提问。 + +**`Skill`** 允许 Agent 主动调用已注册的 inline 类型 Skill。接受 `skill`(Skill 名称)和可选的 `args`(附加参数文本)。只有 `type = "inline"` 的 Skill 能通过此工具调用;其他类型(如 `prompt`、`flow`)以及在 frontmatter 中设置了 `disableModelInvocation: true` 的 Skill 会被拒绝。为防止递归死循环,Skill 嵌套调用深度上限为 3 层。Skill 体系细节参见 [Skills](../customization/skills.md)。 + +## 后台任务 + +后台任务工具用于管理通过 `Bash` 或 `Agent` 启动的后台任务。后台任务进入完成、失败、停止或丢失等终止状态时,会把状态和末尾输出自动送回 Agent;如果只想提前检查进度,再使用 `TaskOutput`。 + +| 工具 | 默认审批 | 说明 | +| --- | --- | --- | +| `TaskList` | 自动放行 | 列出后台任务 | +| `TaskOutput` | 自动放行 | 查看后台任务的输出 | +| `TaskStop` | 需审批 | 停止正在运行的后台任务 | + +**`TaskList`** 返回后台任务列表,每条记录包含任务 ID、状态、命令、描述和 PID。可选参数 `active_only`(默认 true,仅列出运行中的任务)和 `limit`(最多返回条数,默认 20,取值范围 1–100)。已进入终止状态的任务还会附带 `exit_code`,被 `TaskStop` 显式终止的任务会附带 `reason`。 + +**`TaskOutput`** 根据 `task_id` 返回指定任务的状态与输出。内联预览最多包含最近 32 KB 的内容;完整日志保存在磁盘上,工具会一并返回 `output_path` 并提示通过 `Read` 分页读取(建议每页约 300 行)。可选 `block`(默认 false)和 `timeout`(等待秒数,默认 30,取值范围 0–3600)参数可用于等待任务完成后再返回。返回结构中 `retrieval_status` 取 `success` / `timeout` / `not_ready`;任务因超时被外部 deadline 中止时会附带 `timed_out: true` 与 `terminal_reason: timed_out`,被 `TaskStop` 显式终止时会附带 `stop_reason` 与 `terminal_reason: stopped`。 + +**`TaskStop`** 接受 `task_id` 和可选的 `reason`(停止原因,默认 `Stopped by TaskStop`)。对已处于终止状态的任务也能安全调用,会直接返回当前状态而不报错。 diff --git a/docs/zh/release-notes/changelog.md b/docs/zh/release-notes/changelog.md new file mode 100644 index 000000000..d169d59f4 --- /dev/null +++ b/docs/zh/release-notes/changelog.md @@ -0,0 +1,5 @@ +# 变更记录 + +::: info 说明 +本页将随首个正式版本同步发布详细变更记录。 +::: diff --git a/flake.lock b/flake.lock new file mode 100644 index 000000000..689872643 --- /dev/null +++ b/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1779102034, + "narHash": "sha256-vZJZjLo513IeI8hjzHFc6TDezUd4uCE2Eq4SNO3DNNg=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "687f05a9184cad4eaf905c48b63649e3a86f5433", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-25.11", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 000000000..b4de4f080 --- /dev/null +++ b/flake.nix @@ -0,0 +1,302 @@ +{ + description = "Kimi Code CLI"; + + inputs = { + # Pinned to the 25.11 release channel because nixpkgs-unstable currently + # ships nodejs_24 = 24.14.1, which trips the >= 24.15.0 floor that the + # native SEA build enforces (see apps/kimi-code/scripts/native/build.mjs). + nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11"; + }; + + outputs = + { self, nixpkgs }: + let + lib = nixpkgs.lib; + + systems = [ + "x86_64-linux" + "aarch64-linux" + "x86_64-darwin" + "aarch64-darwin" + ]; + + forAllSystems = + f: + lib.genAttrs systems ( + system: + f (import nixpkgs { + inherit system; + }) + ); + + minNodeVersion = "24.15.0"; + requiredNodeMajor = "24"; + + # nodejsFor picks pkgs.nodejs_24 when it satisfies the minimum, otherwise + # falls back to pkgs.nodejs_latest. The build pipeline (tsdown target + # `node24`, SEA flags) assumes a 24.x runtime, so we hard-fail if neither + # candidate is in the 24.x line. + nodejsFor = + pkgs: + let + candidate = + if lib.versionAtLeast pkgs.nodejs_24.version minNodeVersion then + pkgs.nodejs_24 + else + pkgs.nodejs_latest; + major = lib.versions.major candidate.version; + in + if major == requiredNodeMajor then + candidate + else + throw '' + Kimi Code requires Node.js ${requiredNodeMajor}.x (>= ${minNodeVersion}), + but nixpkgs only offers ${candidate.version}. + Pin a newer nixpkgs revision or update minNodeVersion in flake.nix. + ''; + + pnpmFor = + pkgs: + pkgs.pnpm_10.override { + nodejs = nodejsFor pkgs; + }; + + # --------------------------------------------------------------------- + # Derive workspace members from pnpm-workspace.yaml + each package.json. + # + # Source of truth is pnpm-workspace.yaml's `packages:` section. We expand + # each glob (or literal path) against the repo root, keep only entries + # that contain a package.json, and read their `name` field. Both the + # `src` fileset and the `pnpmWorkspaces` filter for pnpmConfigHook are + # derived from this list, so adding/removing a workspace only requires + # updating pnpm-workspace.yaml (followed by `nix run .#update-pnpm-deps`). + # --------------------------------------------------------------------- + + # Minimal parser for the `packages:` list in pnpm-workspace.yaml. Only + # handles top-level `- ` items under the `packages:` key; other + # sections (catalog, overrides) are ignored. Sufficient for the format + # pnpm produces and we maintain. + parsePnpmWorkspaceGlobs = + file: + let + lines = lib.splitString "\n" (builtins.readFile file); + isPackagesHeader = l: builtins.match "^packages:[[:space:]]*$" l != null; + isTopLevelKey = l: builtins.match "^[^[:space:]#].*:.*$" l != null; + extractItem = + l: + let + m = builtins.match "^[[:space:]]+-[[:space:]]+['\"]?([^'\"#[:space:]]+)['\"]?[[:space:]]*$" l; + in + if m == null then null else builtins.head m; + step = + state: line: + if state.done then + state + else if !state.inSection then + if isPackagesHeader line then state // { inSection = true; } else state + else + let + item = extractItem line; + in + if item != null then + state // { items = state.items ++ [ item ]; } + else if isTopLevelKey line then + state // { inSection = false; done = true; } + else + state; + result = lib.foldl' step { + inSection = false; + items = [ ]; + done = false; + } lines; + in + result.items; + + expandWorkspaceGlob = + root: glob: + if lib.hasSuffix "/*" glob then + let + dir = lib.removeSuffix "/*" glob; + absDir = root + "/${dir}"; + in + if builtins.pathExists absDir then + map (n: "${dir}/${n}") ( + builtins.attrNames (lib.filterAttrs (_: t: t == "directory") (builtins.readDir absDir)) + ) + else + [ ] + else if builtins.pathExists (root + "/${glob}") then + [ glob ] + else + [ ]; + + workspaceMembers = + let + globs = parsePnpmWorkspaceGlobs ./pnpm-workspace.yaml; + paths = lib.unique (lib.concatMap (expandWorkspaceGlob ./.) globs); + in + builtins.filter (p: builtins.pathExists (./. + "/${p}/package.json")) paths; + + workspacePaths = map (p: ./. + "/${p}") workspaceMembers; + + workspaceNames = map ( + p: (builtins.fromJSON (builtins.readFile (./. + "/${p}/package.json"))).name + ) workspaceMembers; + in + { + packages = forAllSystems ( + pkgs: + let + nodejs = nodejsFor pkgs; + pnpm = pnpmFor pkgs; + appPackageJson = builtins.fromJSON (builtins.readFile ./apps/kimi-code/package.json); + nativeTarget = + if pkgs.stdenv.hostPlatform.isLinux && pkgs.stdenv.hostPlatform.isAarch64 then + "linux-arm64" + else if pkgs.stdenv.hostPlatform.isLinux then + "linux-x64" + else if pkgs.stdenv.hostPlatform.isDarwin && pkgs.stdenv.hostPlatform.isAarch64 then + "darwin-arm64" + else if pkgs.stdenv.hostPlatform.isDarwin then + "darwin-x64" + else + throw "Unsupported Kimi Code native target for ${pkgs.stdenv.hostPlatform.system}"; + + kimi-code = pkgs.stdenv.mkDerivation (finalAttrs: { + pname = "kimi-code"; + version = appPackageJson.version; + + src = lib.fileset.toSource { + root = ./.; + fileset = lib.fileset.unions ( + [ + ./build + ./.npmrc + ./.nvmrc + ./package.json + ./pnpm-lock.yaml + ./pnpm-workspace.yaml + ./tsconfig.json + ./vitest.config.ts + ./LICENSE + ] + ++ workspacePaths + ); + }; + + pnpmWorkspaces = [ "." ] ++ workspaceNames; + + pnpmDeps = pkgs.fetchPnpmDeps { + inherit (finalAttrs) pname version src pnpmWorkspaces; + inherit pnpm; + fetcherVersion = 3; + hash = "sha256-dht627zodv/xeefY7fEvSaKXavTxi+JWA3gwL18U8dk="; + }; + + nativeBuildInputs = [ + nodejs + pnpm + (pkgs.pnpmConfigHook.override { inherit pnpm; }) + ] + # The SEA inject step (postject) invalidates the macOS code + # signature on the copied Node executable; build.mjs then re-applies + # an ad-hoc signature via `codesign`. The Nix darwin sandbox does + # not expose /usr/bin/codesign, so we supply nixpkgs' ad-hoc-only + # replacement instead. + ++ lib.optionals pkgs.stdenv.hostPlatform.isDarwin [ + pkgs.darwin.sigtool + ]; + + # The SEA binary is produced by `postject`-injecting a blob into a + # plain Node executable. Stripping rewrites section tables and can + # invalidate the injected blob's offsets, so leave the binary + # untouched after the build. + dontStrip = true; + + buildPhase = '' + runHook preBuild + export KIMI_CODE_BUILD_TARGET=${nativeTarget} + ${lib.optionalString pkgs.stdenv.hostPlatform.isDarwin '' + # pkgs.darwin.sigtool's codesign supports `--sign -` (ad-hoc) + # but not the inspection mode (`-dv`) that 05-verify.mjs runs + # afterwards. Disable the verify step for the Nix build; the + # release CI keeps it via the unmodified script. + substituteInPlace apps/kimi-code/scripts/native/build.mjs \ + --replace-fail \ + "await runVerifyStep({ requireGatekeeper: false });" \ + "// runVerifyStep skipped in nix sandbox (sigtool lacks -dv)" + ''} + pnpm --filter=@moonshot-ai/kimi-code run build:native:sea + runHook postBuild + ''; + + installPhase = '' + runHook preInstall + + install -Dm755 \ + "apps/kimi-code/dist-native/bin/${nativeTarget}/kimi" \ + "$out/bin/kimi" + + runHook postInstall + ''; + + meta = { + description = "Kimi Code CLI"; + homepage = "https://github.com/MoonshotAI/kimi-code"; + license = lib.licenses.mit; + mainProgram = "kimi"; + platforms = systems; + }; + }); + + # Expose pnpmDeps as a top-level package so `nix build .#kimi-code-pnpm-deps` + # (used by the update-pnpm-deps app) is a stable selector that doesn't + # depend on attribute drilling into a derivation. + kimi-code-pnpm-deps = kimi-code.pnpmDeps; + + update-pnpm-deps = pkgs.writeShellApplication { + name = "update-pnpm-deps"; + runtimeInputs = [ + pkgs.nix + pkgs.git + nodejs + pkgs.gnused + pkgs.gnugrep + pkgs.coreutils + ]; + text = builtins.readFile ./build/nix/update-pnpm-deps.sh; + }; + in + { + inherit kimi-code kimi-code-pnpm-deps update-pnpm-deps; + default = kimi-code; + } + ); + + apps = forAllSystems (pkgs: { + kimi-code = { + type = "app"; + program = "${self.packages.${pkgs.system}.kimi-code}/bin/kimi"; + }; + update-pnpm-deps = { + type = "app"; + program = "${self.packages.${pkgs.system}.update-pnpm-deps}/bin/update-pnpm-deps"; + }; + default = self.apps.${pkgs.system}.kimi-code; + }); + + devShells = forAllSystems (pkgs: { + default = + let + nodejs = nodejsFor pkgs; + pnpm = pnpmFor pkgs; + in + pkgs.mkShell { + packages = [ + nodejs + pnpm + ]; + }; + }); + }; +} diff --git a/package.json b/package.json new file mode 100644 index 000000000..a3ba656e3 --- /dev/null +++ b/package.json @@ -0,0 +1,57 @@ +{ + "name": "@moonshot-ai/monorepo", + "version": "0.1.1", + "private": true, + "license": "MIT", + "type": "module", + "scripts": { + "build": "pnpm -r run build", + "build:packages": "pnpm -r --filter './packages/*' run build", + "dev:cli": "pnpm -C apps/kimi-code run dev", + "vis": "pnpm -C apps/vis run dev", + "dev:docs": "pnpm -C docs install --ignore-workspace && pnpm -C docs run dev", + "typecheck": "pnpm run build:packages && pnpm -r --filter './packages/*' run typecheck && pnpm --filter @moonshot-ai/kimi-code run typecheck", + "lint": "oxlint --type-aware", + "lint:fix": "pnpm run lint --fix", + "lint:pkg": "pnpm -r --filter '!@moonshot-ai/monorepo' exec publint && pnpm -r --filter './packages/*' exec attw --pack . --profile node16", + "sherif": "sherif", + "test": "vitest run", + "test:watch": "vitest", + "test:coverage": "vitest run --coverage", + "clean": "pnpm -r run clean", + "changeset": "changeset", + "version": "changeset version", + "publish": "pnpm run typecheck && pnpm run lint && pnpm run sherif && pnpm run test && pnpm run build && pnpm run lint:pkg && changeset publish", + "prepare": "simple-git-hooks" + }, + "devDependencies": { + "@arethetypeswrong/cli": "0.18.2", + "@changesets/changelog-github": "0.7.0", + "@changesets/cli": "2.30.0", + "@microsoft/api-extractor": "7.58.7", + "@types/node": "^22.15.3", + "@vitest/coverage-v8": "4.1.4", + "lint-staged": "16.4.0", + "oxlint": "1.59.0", + "oxlint-tsgolint": "0.20.0", + "publint": "0.3.18", + "sherif": "1.11.1", + "simple-git-hooks": "2.13.1", + "tsdown": "0.22.0", + "tsx": "^4.21.0", + "typescript": "6.0.2", + "vitest": "4.1.4" + }, + "simple-git-hooks": { + }, + "lint-staged": { + "*.{js,jsx,ts,tsx,mjs,cjs,mts,cts}": [ + "oxlint --fix --quiet", + "oxlint --type-aware --quiet" + ] + }, + "engines": { + "node": ">=24.15.0" + }, + "packageManager": "pnpm@10.33.0" +} diff --git a/packages/agent-core/README.md b/packages/agent-core/README.md new file mode 100644 index 000000000..ce786952b --- /dev/null +++ b/packages/agent-core/README.md @@ -0,0 +1,11 @@ +# @moonshot-ai/agent-core + +The unified agent engine for Kimi Code. + +Part of the [Kimi Code](https://github.com/MoonshotAI/kimi-code) monorepo. + +See the main repository for documentation, issues, and contribution guidelines. + +## License + +MIT diff --git a/packages/agent-core/package.json b/packages/agent-core/package.json new file mode 100644 index 000000000..87631862c --- /dev/null +++ b/packages/agent-core/package.json @@ -0,0 +1,85 @@ +{ + "name": "@moonshot-ai/agent-core", + "version": "0.1.1", + "private": true, + "description": "The unified agent engine for Kimi", + "license": "MIT", + "author": "Moonshot AI", + "homepage": "https://github.com/MoonshotAI/kimi-code/tree/main/packages/agent-core#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/MoonshotAI/kimi-code.git", + "directory": "packages/agent-core" + }, + "bugs": { + "url": "https://github.com/MoonshotAI/kimi-code/issues" + }, + "keywords": [ + "kimi", + "agent", + "ai", + "llm", + "session", + "tools" + ], + "files": [ + "dist" + ], + "type": "module", + "imports": { + "#/*": [ + "./src/*.ts", + "./src/*/index.ts" + ] + }, + "exports": { + ".": { + "types": "./src/index.ts", + "default": "./src/index.ts" + }, + "./session/store": { + "types": "./src/session/store/index.ts", + "default": "./src/session/store/index.ts" + } + }, + "scripts": { + "build": "tsdown", + "test": "vitest run", + "test:baseline": "vitest run", + "typecheck": "tsc -p tsconfig.json --noEmit", + "clean": "rm -rf dist" + }, + "dependencies": { + "@antfu/utils": "^9.3.0", + "@modelcontextprotocol/sdk": "^1.29.0", + "@moonshot-ai/kaos": "workspace:^", + "@moonshot-ai/kosong": "workspace:^", + "@mozilla/readability": "^0.6.0", + "ajv": "^8.18.0", + "ajv-formats": "^3.0.1", + "js-yaml": "^4.1.1", + "linkedom": "^0.18.12", + "nunjucks": "^3.2.4", + "open": "^10.2.0", + "picomatch": "^4.0.4", + "proper-lockfile": "^4.1.2", + "regexp.escape": "^2.0.1", + "retry": "0.13.1", + "smol-toml": "^1.6.1", + "tar": "^7.5.13", + "yauzl": "^3.3.0", + "zod": "catalog:" + }, + "devDependencies": { + "@types/js-yaml": "^4.0.9", + "@types/nunjucks": "^3.2.6", + "@types/picomatch": "^4.0.3", + "@types/proper-lockfile": "^4.1.4", + "@types/regexp.escape": "^2.0.0", + "@types/retry": "0.12.0", + "@types/tar": "^7.0.87", + "@types/yauzl": "^2.10.3", + "@types/yazl": "^2.4.6", + "yazl": "^3.3.1" + } +} diff --git a/packages/agent-core/src/agent/background/index.ts b/packages/agent-core/src/agent/background/index.ts new file mode 100644 index 000000000..14af9fa52 --- /dev/null +++ b/packages/agent-core/src/agent/background/index.ts @@ -0,0 +1,191 @@ +import type { ContentPart } from '@moonshot-ai/kosong'; + +import type { Agent } from '../..'; +import type { TelemetryPropertyValue } from '../../telemetry'; +import { + BackgroundProcessManager, + type BackgroundProcessManagerOptions, + type BackgroundTaskInfo, + isBackgroundTaskTerminal, + type ReconcileResult, +} from '../../tools/builtin'; +import type { BackgroundTaskOrigin } from '../context'; +import { renderNotificationXml } from '../context/notification-xml'; + +type BackgroundTaskNotification = Record & { + readonly id: string; + readonly category: 'task'; + readonly type: string; + readonly source_kind: 'background_task'; + readonly source_id: string; + readonly title: string; + readonly severity: 'info' | 'warning'; + readonly body: string; + readonly tail_output: string; +}; + +interface BackgroundTaskNotificationContext { + readonly content: readonly ContentPart[]; + readonly origin: BackgroundTaskOrigin; + readonly notification: BackgroundTaskNotification; +} + +const NOTIFICATION_TAIL_BYTES = 3_000; + +export class BackgroundManager extends BackgroundProcessManager { + private readonly scheduledNotificationKeys = new Set(); + private readonly deliveredNotificationKeys = new Set(); + + constructor( + public readonly agent: Agent, + options: BackgroundProcessManagerOptions = {}, + ) { + super(options); + + this.onLifecycle((event, info) => { + switch (event) { + case 'started': + this.agent.emitEvent({ type: 'background.task.started', info }); + this.agent.telemetry.track('background_task_created', { + kind: info.taskId.startsWith('agent-') ? 'agent' : 'bash', + }); + return; + case 'updated': + this.agent.emitEvent({ type: 'background.task.updated', info }); + return; + case 'terminated': { + this.agent.emitEvent({ type: 'background.task.terminated', info }); + const success = info.status === 'completed'; + const duration_s = + info.endedAt !== null ? (info.endedAt - info.startedAt) / 1000 : null; + const properties: Record = { + kind: info.taskId.startsWith('agent-') ? 'agent' : 'bash', + success, + duration_s, + }; + if (!success) { + properties['reason'] = + info.timedOut === true + ? 'timeout' + : info.status === 'killed' + ? 'killed' + : 'error'; + } + this.agent.telemetry.track('background_task_completed', properties); + return; + } + } + }); + } + + override async reconcile(): Promise { + const result = await super.reconcile(); + await this.restoreBackgroundTaskNotifications(); + return result; + } + + protected override onLiveTaskTerminal(info: BackgroundTaskInfo): void | Promise { + return this.notifyBackgroundTask(info); + } + + private async restoreBackgroundTaskNotifications(): Promise { + for (const info of this.list(false)) { + if (!isBackgroundTaskTerminal(info.status)) continue; + await this.restoreBackgroundTaskNotification(info); + } + } + + private async notifyBackgroundTask(info: BackgroundTaskInfo): Promise { + const context = await this.buildBackgroundTaskNotificationContext(info); + if (context === undefined) return; + this.agent.turn.steer(context.content, context.origin); + this.fireNotificationHook(context.notification); + } + + private async restoreBackgroundTaskNotification(info: BackgroundTaskInfo): Promise { + const context = await this.buildBackgroundTaskNotificationContext(info); + if (context === undefined) return; + this.agent.context.appendUserMessage(context.content, context.origin); + this.fireNotificationHook(context.notification); + } + + private async buildBackgroundTaskNotificationContext( + info: BackgroundTaskInfo, + ): Promise { + const origin: BackgroundTaskOrigin = { + kind: 'background_task', + taskId: info.taskId, + status: info.status, + notificationId: `task:${info.taskId}:${info.status}`, + }; + const notificationId = origin.notificationId; + const key = notificationKey(origin); + if (this.scheduledNotificationKeys.has(key)) return; + if (this.hasDeliveredNotification(origin)) return; + + this.scheduledNotificationKeys.add(key); + const tailOutput = (await this.getOutputSnapshot(info.taskId, NOTIFICATION_TAIL_BYTES)) + .preview; + if (this.hasDeliveredNotification(origin)) return; + const label = info.taskId.startsWith('agent-') ? 'agent' : 'task'; + const notification: BackgroundTaskNotification = { + id: notificationId, + category: 'task', + type: `task.${info.status}`, + source_kind: 'background_task', + source_id: info.taskId, + title: `Background ${label} ${info.status}`, + severity: info.status === 'completed' ? 'info' : 'warning', + body: `${info.description} ${info.status}.`, + tail_output: tailOutput, + }; + const content = [ + { + type: 'text', + text: renderNotificationXml(notification), + }, + ] as const; + return { content, origin, notification }; + } + + private fireNotificationHook(notification: BackgroundTaskNotification): void { + void this.agent.hooks?.fireAndForgetTrigger('Notification', { + matcherValue: notification.type, + inputData: { + sink: 'context', + notificationType: notification.type, + title: notification.title, + body: notification.body, + severity: notification.severity, + sourceKind: notification.source_kind, + sourceId: notification.source_id, + }, + }); + } + + markDeliveredNotification(origin: BackgroundTaskOrigin): void { + this.deliveredNotificationKeys.add(notificationKey(origin)); + } + + private hasDeliveredNotification(origin: BackgroundTaskOrigin): boolean { + return this.deliveredNotificationKeys.has(notificationKey(origin)); + } + + override stop(taskId: string, reason?: string) { + this.agent.records.logRecord({ + type: 'background.stop', + taskId, + }); + return super.stop(taskId, reason); + } + + override _reset(): void { + super._reset(); + this.scheduledNotificationKeys.clear(); + this.deliveredNotificationKeys.clear(); + } +} + +function notificationKey(origin: BackgroundTaskOrigin): string { + return `${origin.taskId}\0${origin.status}\0${origin.notificationId}`; +} diff --git a/packages/agent-core/src/agent/compaction/compaction-instruction.md b/packages/agent-core/src/agent/compaction/compaction-instruction.md new file mode 100644 index 000000000..68a3f3b12 --- /dev/null +++ b/packages/agent-core/src/agent/compaction/compaction-instruction.md @@ -0,0 +1,67 @@ + +--- This message is a direct task, not part of the above conversation --- + +You are now given a task to compact this conversation context according to specific priorities and output requirements. + +Output text only. DO NOT CALL ANY TOOLS. Calling tools will be rejected and fails the task. You already have all the information you need in the conversation history. You have only one chance. + +The goal of compaction is to keep essential code patterns, technical details, and architectural decisions for continuing development without losing context after the above messages are cleared work. + +{{ customInstruction }} + + + +1. **Current Task State**: What is being worked on RIGHT NOW +2. **Errors & Solutions**: All encountered errors and their resolutions +3. **Code Evolution**: Final working versions only (remove intermediate attempts) +4. **System Context**: Project structure, dependencies, environment setup +5. **Design Decisions**: Architectural choices and their rationale +6. **TODO Items**: Unfinished tasks and known issues + + + +## Current Focus + +[What we're working on now] + +## Environment + +- [Key setup/config points] +- ... + +## Completed Tasks + +- [Task]: [Brief outcome] +- ... + +## Active Issues + +- [Issue]: [Status/Next steps] +- ... + +## Code State + +### [Critical file name] + +[Brief description of the file's purpose and current state] + +``` +[The latest version of critical code snippets in this file, <20 lines] +``` + +### [Critical file name] + +- [Useful classes/methods/functions]: [Brief description/usage] +- ... + + + +## Important Context + +- [Any crucial information not covered above] +- ... + +## All User Messages + +- [Detailed non tool use user message] +- ... diff --git a/packages/agent-core/src/agent/compaction/config.ts b/packages/agent-core/src/agent/compaction/config.ts new file mode 100644 index 000000000..50d92f306 --- /dev/null +++ b/packages/agent-core/src/agent/compaction/config.ts @@ -0,0 +1,19 @@ +export interface CompactionConfig { + triggerRatio: number; + blockRatio: number; + reservedContextSize: number; + maxCompactionPerTurn: number; + maxRecentSteps: number; + maxRecentUserMessages: number; + maxRecentSizeRatio: number; +} + +export const DEFAULT_COMPACTION_CONFIG: CompactionConfig = { + triggerRatio: 0.85, + blockRatio: 0.85, // Same as triggerRatio to disable async compaction + reservedContextSize: 50_000, + maxCompactionPerTurn: 3, + maxRecentSteps: 3, + maxRecentUserMessages: Infinity, + maxRecentSizeRatio: 0.2, +}; diff --git a/packages/agent-core/src/agent/compaction/full.ts b/packages/agent-core/src/agent/compaction/full.ts new file mode 100644 index 000000000..80f23f2bd --- /dev/null +++ b/packages/agent-core/src/agent/compaction/full.ts @@ -0,0 +1,557 @@ +import { + ErrorCodes, + KimiError, + isKimiError, + makeErrorPayload, + toKimiErrorPayload, +} from '#/errors'; +import { + APIConnectionError, + APIEmptyResponseError, + APIStatusError, + APITimeoutError, + inputTotal, + type GenerateResult, + type Message, + type TokenUsage, +} from '@moonshot-ai/kosong'; + +import type { Agent } from '..'; +import { isAbortError } from '../../loop/errors'; +import { + DEFAULT_MAX_RETRY_ATTEMPTS, + retryBackoffDelays, + sleepForRetry, +} from '../../loop/retry'; +import type { TelemetryPropertyValue } from '../../telemetry'; +import { + applyCompletionBudget, + resolveCompletionBudget, +} from '../../utils/completion-budget'; +import { renderPrompt } from '../../utils/render-prompt'; +import { + estimateTokens, + estimateTokensForMessage, + estimateTokensForMessages, +} from '../../utils/tokens'; +import { sliceCompleteMessages } from '../context/complete-slice'; +import { project } from '../context/projector'; +import compactionInstructionTemplate from './compaction-instruction.md'; +import { DEFAULT_COMPACTION_CONFIG, type CompactionConfig } from './config'; +import { renderMessagesToText } from './render-messages'; +import type { CompactionBeginData, CompactionResult } from './types'; + +export interface CompactionStrategy { + shouldCompact(usedSize: number, maxSize: number): boolean; + shouldBlock(usedSize: number, maxSize: number): boolean; + computeCompactCount(messages: readonly Message[], maxSize: number): number; + readonly checkAfterStep: boolean; + readonly maxCompactionPerTurn: number; +} + +export class DefaultCompactionStrategy implements CompactionStrategy { + constructor(protected readonly config: CompactionConfig = DEFAULT_COMPACTION_CONFIG) {} + + shouldCompact(usedSize: number, maxSize: number): boolean { + if (maxSize <= 0) return false; + return ( + usedSize >= maxSize * this.config.triggerRatio || + this.shouldUseReservedContext(maxSize, usedSize) + ); + } + + shouldBlock(usedSize: number, maxSize: number): boolean { + if (maxSize <= 0) return false; + return ( + usedSize >= maxSize * this.config.blockRatio || + this.shouldUseReservedContext(maxSize, usedSize) + ); + } + + private shouldUseReservedContext(maxSize: number, usedSize: number): boolean { + const reservedSize = this.config.reservedContextSize; + return reservedSize > 0 && reservedSize < maxSize && usedSize + reservedSize >= maxSize; + } + + computeCompactCount(messages: readonly Message[], maxSize: number) { + let splitAt = messages.length; + let recentSize = 0; + let userMessageCount = 0; + let onlySeenTrailingUsers = true; + for (let i = messages.length - 1; i >= 0; i--) { + const m1 = messages[i - 1]; + const m2 = messages[i]; + if (m2 === undefined) continue; + const isTrailingAssistantPlaceholder = + onlySeenTrailingUsers && + m2.role === 'assistant' && + m2.content.length === 0 && + m2.toolCalls.length === 0; + if (isTrailingAssistantPlaceholder) { + splitAt = i; + continue; + } + const isTrailingUserMessage = onlySeenTrailingUsers && m2.role === 'user'; + if (!isTrailingUserMessage && messages.length - i >= this.config.maxRecentSteps) break; + + if (m2.role === 'user') { + userMessageCount++; + if (!isTrailingUserMessage && userMessageCount > this.config.maxRecentUserMessages) { + break; + } + } + + recentSize += estimateTokensForMessage(m2); + if (isTrailingUserMessage) { + splitAt = i; + continue; + } + if (recentSize > maxSize * this.config.maxRecentSizeRatio) { + break; + } + const canSplitBeforeMessage = + m1?.role !== m2.role && !(m1?.role === 'user' && m2.role === 'assistant') && m2.role !== 'tool'; + if (canSplitBeforeMessage) { + splitAt = i; + } + if (m2.role !== 'user') { + onlySeenTrailingUsers = false; + } + } + + return splitAt; + } + + get checkAfterStep(): boolean { + return this.config.triggerRatio !== this.config.blockRatio; + } + + get maxCompactionPerTurn(): number { + return this.config.maxCompactionPerTurn; + } +} + +export interface CompactedHistory { + text: string; +} + +type CompactionTelemetryTrigger = CompactionBeginData['source'] | 'manual-with-prompt' | 'unknown'; + +export class FullCompaction { + protected compactionCountInTurn = 0; + protected compacting: { + abortController: AbortController; + startedAt: number; + telemetryTrigger: CompactionTelemetryTrigger; + promise: Promise; + } | null = null; + protected _compactedHistory: CompactedHistory[] = []; + protected readonly strategy: CompactionStrategy; + + constructor( + protected readonly agent: Agent, + strategy?: CompactionStrategy, + ) { + this.strategy = + strategy ?? + new DefaultCompactionStrategy({ + ...DEFAULT_COMPACTION_CONFIG, + reservedContextSize: + agent.providerManager?.config.loopControl?.reservedContextSize ?? + DEFAULT_COMPACTION_CONFIG.reservedContextSize, + }); + } + + get isCompacting(): boolean { + return this.compacting !== null; + } + + begin(data: Readonly): void { + this.agent.records.logRecord({ + type: 'full_compaction.begin', + ...data, + }); + if (this.compacting) return; + if (data.source === 'manual') { + this.compactionCountInTurn = 0; + } else { + this.compactionCountInTurn += 1; + } + if (this.compactionCountInTurn > this.strategy.maxCompactionPerTurn) return; + if (!this.agent.records.restoring) { + this.startCompactionWorker(data); + } + } + + private startCompactionWorker(data: Readonly): void { + const abortController = new AbortController(); + this.agent.emitEvent({ + type: 'compaction.started', + trigger: data.source, + instruction: data.instruction, + }); + const active = { + abortController, + startedAt: Date.now(), + telemetryTrigger: compactionTelemetryTrigger(data.source, data.instruction), + promise: Promise.resolve(), + }; + this.compacting = active; + active.promise = this.compactionWorker(abortController.signal, data); + } + + cancel(): void { + this.markCanceled(); + } + + private markCanceled(): void { + if (!this.compacting) return; + this.agent.records.logRecord({ + type: 'full_compaction.cancel', + }); + this.compacting.abortController.abort(); + this.compacting = null; + this.agent.emitEvent({ type: 'compaction.cancelled' }); + } + + complete( + result: CompactionResult, + llmUsage?: TokenUsage | undefined, + retryCount: number = 0, + ): void { + this.agent.records.logRecord({ + type: 'full_compaction.complete', + ...result, + }); + const active = this.compacting; + this.compacting = null; + const history = this.agent.context.history; + this._compactedHistory.push({ + text: renderMessagesToText(history), + }); + this.agent.emitEvent({ type: 'compaction.completed', result }); + if (active !== null) { + const properties: Record = { + trigger_type: active.telemetryTrigger, + before_tokens: result.tokensBefore, + after_tokens: result.tokensAfter, + duration_ms: Date.now() - active.startedAt, + compacted_count: result.compactedCount, + retry_count: retryCount, + }; + if (llmUsage !== undefined) { + properties['llm_input_tokens'] = inputTotal(llmUsage); + properties['llm_output_tokens'] = llmUsage.output; + } + this.agent.telemetry.track('compaction_finished', properties); + } + } + + private get tokenCountWithPending(): number { + return this.agent.context.tokenCountWithPending; + } + + private get maxContextSize() { + return this.agent.config.modelCapabilities.max_context_tokens; + } + + private get shouldCompact(): boolean { + return this.strategy.shouldCompact(this.tokenCountWithPending, this.maxContextSize); + } + + private get shouldBlock(): boolean { + return this.strategy.shouldBlock(this.tokenCountWithPending, this.maxContextSize); + } + + resetForTurn(): void { + this.compactionCountInTurn = 0; + } + + async handleOverflowError(signal: AbortSignal, error: unknown) { + const didStartCompaction = this.beginAutoCompaction(); + if (!didStartCompaction && !this.compacting) throw error; + // Always block on overflow errors + await this.block(signal); + } + + async beforeStep(signal: AbortSignal): Promise { + this.checkAutoCompaction(); + if (this.shouldBlock) { + await this.block(signal); + } + } + + async afterStep(): Promise { + if (this.strategy.checkAfterStep) { + this.checkAutoCompaction(false); + } + // Do not block after the step + } + + private checkAutoCompaction(throwOnLimit: boolean = true): boolean { + if (this.compacting) return true; + if (!this.shouldCompact) return false; + + return this.beginAutoCompaction(throwOnLimit); + } + + private beginAutoCompaction(throwOnLimit: boolean = true): boolean { + if (this.compacting) return true; + const maxCompactions = this.strategy.maxCompactionPerTurn; + if (this.compactionCountInTurn >= maxCompactions) { + if (throwOnLimit) { + throw new KimiError(ErrorCodes.CONTEXT_OVERFLOW, `Compaction limit exceeded (${String(maxCompactions)})`, { + details: { maxCompactions }, + }); + } + return false; + } + const history = this.agent.context.history; + const compactedCount = this.computeCompactableCount(history); + if (compactedCount === 0) return false; + if ( + this.maxContextSize > 0 && + estimateTokensForMessages(project(history.slice(compactedCount))) >= this.maxContextSize + ) { + return false; + } + this.agent.fullCompaction.begin({ source: 'auto', instruction: undefined }); + return true; + } + + private async block(signal: AbortSignal): Promise { + const active = this.compacting; + if (active) { + signal.addEventListener('abort', () => { + if (this.compacting === active) { + this.cancel(); + } + }); + this.agent.emitEvent({ + type: 'compaction.blocked', + turnId: this.agent.turn.currentId, + }); + await active.promise; + } + } + + private async compactionWorker( + signal: AbortSignal, + data: Readonly, + ): Promise { + const startedAt = Date.now(); + let tokensBeforeForError = 0; + let retryCountForTelemetry = 0; + try { + const originalHistory = [...this.agent.context.history]; + const tokensBefore = this.agent.context.tokenCount; + tokensBeforeForError = tokensBefore; + const compactedCount = this.computeCompactableCount(originalHistory); + if (compactedCount === 0) { + this.markCanceled(); + return undefined; + } + signal.throwIfAborted(); + await this.triggerPreCompactHook(data, tokensBefore, signal); + signal.throwIfAborted(); + + const model = this.agent.config.model; + const messages = [ + ...project(originalHistory.slice(0, compactedCount)), + { + role: 'user', + content: [ + { + type: 'text', + text: COMPACTION_INSTRUCTION(data.instruction), + }, + ], + toolCalls: [], + } satisfies Message, + ]; + const { response, retryCount } = await this.generateCompactionResponse({ + messages, + signal, + onRetry: (count) => { + retryCountForTelemetry = count; + }, + }); + if (response.usage !== null) { + this.agent.usage.record(model, response.usage); + } + + const summary = + typeof response.message.content === 'string' + ? response.message.content + : response.message.content + .map((part) => (part.type === 'text' ? part.text : '')) + .join(''); + + const newHistory = this.agent.context.history; + for (let i = 0; i < originalHistory.length; i++) { + if (newHistory[i] !== originalHistory[i]) { + // History changed during compaction, likely due to undo + this.cancel(); + return undefined; + } + } + + const recent = originalHistory.slice(compactedCount); + const tokensAfter = estimateTokens(summary) + estimateTokensForMessages(project(recent)); + + const result: CompactionResult = { + summary, + compactedCount, + tokensBefore, + tokensAfter, + }; + + this.complete(result, response.usage ?? undefined, retryCount); + this.agent.context.applyCompaction(result); + this.triggerPostCompactHook(data, result); + } catch (error) { + if (!isAbortError(error)) { + this.agent.log.error('compaction failed', { + code: isKimiError(error) ? error.code : undefined, + error, + }); + this.markCanceled(); + const payload = + isKimiError(error) && error.code === ErrorCodes.AUTH_LOGIN_REQUIRED + ? toKimiErrorPayload(error) + : makeErrorPayload(ErrorCodes.COMPACTION_FAILED, String(error)); + this.agent.emitEvent({ + type: 'error', + ...payload, + }); + this.agent.telemetry.track('compaction_failed', { + trigger_type: compactionTelemetryTrigger(data.source, data.instruction), + before_tokens: tokensBeforeForError, + duration_ms: Date.now() - startedAt, + retry_count: retryCountForTelemetry, + error_type: error instanceof Error ? error.name : 'Unknown', + }); + } + } + } + + private async generateCompactionResponse({ + messages, + signal, + onRetry, + }: { + readonly messages: Message[]; + readonly signal: AbortSignal; + readonly onRetry?: ((retryCount: number) => void) | undefined; + }): Promise<{ readonly response: GenerateResult; readonly retryCount: number }> { + const maxAttempts = + this.agent.providerManager?.config.loopControl?.maxRetriesPerStep ?? + DEFAULT_MAX_RETRY_ATTEMPTS; + const delays = retryBackoffDelays(maxAttempts); + let retryCount = 0; + + // Clamp the completion budget against the compaction input. Compaction + // is triggered when context is already near full, so an unbounded + // default cap is most at risk of either exceeding the model limit or + // returning empty `content` on reasoning models. The cloned provider + // is local to this call and never persisted back to agent state. + const completionBudget = resolveCompletionBudget({ + reservedContextSize: + this.agent.providerManager?.config.loopControl?.reservedContextSize, + }); + const effectiveProvider = applyCompletionBudget({ + provider: this.agent.config.provider, + budget: completionBudget, + capability: this.agent.config.modelCapabilities, + messages, + systemPrompt: this.agent.config.systemPrompt, + tools: this.agent.tools.loopTools, + }); + + for (let attempt = 1; ; attempt += 1) { + try { + const response = await this.agent.generate( + effectiveProvider, + this.agent.config.systemPrompt, + [...this.agent.tools.loopTools], + messages, + undefined, + { signal }, + ); + return { response, retryCount }; + } catch (error) { + if (attempt >= maxAttempts || !isRetryableCompactionError(error)) { + throw error; + } + retryCount += 1; + onRetry?.(retryCount); + await sleepForRetry(delays[attempt - 1] ?? 0, signal); + } + } + } + + get compactedHistory(): readonly CompactedHistory[] { + return this._compactedHistory; + } + + private async triggerPreCompactHook( + data: Readonly, + tokenCount: number, + signal: AbortSignal, + ): Promise { + await this.agent.hooks?.trigger('PreCompact', { + matcherValue: data.source, + signal, + inputData: { + trigger: data.source, + tokenCount, + }, + }); + } + + private triggerPostCompactHook( + data: Readonly, + result: CompactionResult, + ): void { + void this.agent.hooks?.fireAndForgetTrigger('PostCompact', { + matcherValue: data.source, + inputData: { + trigger: data.source, + estimatedTokenCount: result.tokensAfter, + }, + }); + } + + private computeCompactableCount(history: readonly Message[]): number { + return sliceCompleteMessages( + history, + this.strategy.computeCompactCount(history, this.maxContextSize), + ); + } +} + +export const COMPACTION_INSTRUCTION = (customInstruction = ''): string => + renderPrompt(compactionInstructionTemplate, { customInstruction }); + +function compactionTelemetryTrigger( + trigger: CompactionBeginData['source'] | undefined, + instruction: string | undefined, +): CompactionTelemetryTrigger { + if (trigger === undefined) return 'unknown'; + if (trigger === 'manual' && instruction !== undefined && instruction.length > 0) { + return 'manual-with-prompt'; + } + return trigger; +} + +function isRetryableCompactionError(error: unknown): boolean { + if (error instanceof APIConnectionError || error instanceof APITimeoutError) { + return true; + } + if (error instanceof APIEmptyResponseError) { + return true; + } + if (!(error instanceof APIStatusError)) return false; + const statusCode = (error as { readonly statusCode?: unknown }).statusCode; + return typeof statusCode === 'number' && [429, 500, 502, 503, 504].includes(statusCode); +} diff --git a/packages/agent-core/src/agent/compaction/index.ts b/packages/agent-core/src/agent/compaction/index.ts new file mode 100644 index 000000000..d8da8bbd9 --- /dev/null +++ b/packages/agent-core/src/agent/compaction/index.ts @@ -0,0 +1,3 @@ +export * from './full'; +export * from './config'; +export * from './types'; diff --git a/packages/agent-core/src/agent/compaction/render-messages.ts b/packages/agent-core/src/agent/compaction/render-messages.ts new file mode 100644 index 000000000..81a298a7d --- /dev/null +++ b/packages/agent-core/src/agent/compaction/render-messages.ts @@ -0,0 +1,111 @@ +import type { Message } from '@moonshot-ai/kosong'; + +export function renderMessagesToText(messages: readonly Message[]): string { + return messages.map((message, index) => renderMessageToText(message, index)).join('\n\n'); +} + +function renderMessageToText(message: Message, index: number): string { + const header = [`message ${String(index + 1)}`, `role=${message.role}`]; + if (message.name !== undefined) { + header.push(`name=${JSON.stringify(message.name)}`); + } + if (message.toolCallId !== undefined) { + header.push(`toolCallId=${JSON.stringify(message.toolCallId)}`); + } + if (message.partial === true) { + header.push('partial=true'); + } + + const lines = [`--- ${header.join(' ')} ---`]; + if (message.content.length === 0) { + lines.push('[empty content]'); + } else { + lines.push(...message.content.map(renderContentPartToText)); + } + + if (message.toolCalls.length > 0) { + lines.push('tool calls:'); + for (const toolCall of message.toolCalls) { + lines.push(renderToolCallToText(toolCall)); + } + } + + return lines.join('\n'); +} + +function renderContentPartToText(part: Message['content'][number]): string { + switch (part.type) { + case 'text': + return renderBlock('text', part.text); + case 'think': + return renderBlock('think', part.think); + case 'image_url': + return renderMediaPart('image_url', part.imageUrl.url, part.imageUrl.id); + case 'audio_url': + return renderMediaPart('audio_url', part.audioUrl.url, part.audioUrl.id); + case 'video_url': + return renderMediaPart('video_url', part.videoUrl.url, part.videoUrl.id); + default: + return renderBlock('content', stringifyJsonish(part)); + } +} + +function renderToolCallToText(toolCall: Message['toolCalls'][number]): string { + const lines = [ + `- ${toolCall.id}: ${toolCall.function.name}`, + renderBlock('arguments', renderToolCallArguments(toolCall.function.arguments)), + ]; + + if (toolCall.extras !== undefined) { + lines.push(renderBlock('extras', stringifyJsonish(toolCall.extras))); + } + + return lines.join('\n'); +} + +function renderToolCallArguments(args: string | null): string { + if (args === null) return 'null'; + + try { + return stringifyJsonish(JSON.parse(args)); + } catch { + return args; + } +} + +function renderMediaPart(type: string, url: string, id?: string | undefined): string { + if (id === undefined) return `${type}: ${url}`; + return `${type}: ${url} (id=${id})`; +} + +function renderBlock(label: string, value: string): string { + return `${label}:\n${indentBlock(value)}`; +} + +function indentBlock(value: string): string { + if (value.length === 0) return ' '; + return value + .split('\n') + .map((line) => ` ${line}`) + .join('\n'); +} + +function stringifyJsonish(value: unknown): string { + const seen = new WeakSet(); + const replacer = (_key: string, nested: unknown): unknown => { + if (typeof nested === 'bigint') return `${nested.toString()}n`; + if (typeof nested === 'function') return `[Function ${nested.name || 'anonymous'}]`; + if (typeof nested === 'symbol') return nested.toString(); + if (nested !== null && typeof nested === 'object') { + if (seen.has(nested)) return '[Circular]'; + seen.add(nested); + } + return nested; + }; + + try { + return JSON.stringify(value, replacer, 2) ?? String(value); + } catch { + return String(value); + } +} diff --git a/packages/agent-core/src/agent/compaction/types.ts b/packages/agent-core/src/agent/compaction/types.ts new file mode 100644 index 000000000..3936faef7 --- /dev/null +++ b/packages/agent-core/src/agent/compaction/types.ts @@ -0,0 +1,11 @@ +export interface CompactionResult { + summary: string; + compactedCount: number; + tokensBefore: number; + tokensAfter: number; +} + +export interface CompactionBeginData { + instruction?: string; + source: 'manual' | 'auto'; +} diff --git a/packages/agent-core/src/agent/config/index.ts b/packages/agent-core/src/agent/config/index.ts new file mode 100644 index 000000000..18a33d14b --- /dev/null +++ b/packages/agent-core/src/agent/config/index.ts @@ -0,0 +1,134 @@ +import { + createProvider, + UNKNOWN_CAPABILITY, + type ChatProvider, + type ModelCapability, + type ProviderConfig, +} from '@moonshot-ai/kosong'; + +import type { Agent } from '..'; +import type { ResolvedRuntimeProvider } from '../../providers/runtime-provider'; +import type { AgentConfigData, AgentConfigUpdateData } from './types'; +import { resolveThinkingEffort, type ThinkingEffort } from './thinking'; + +export * from './types'; +export { resolveThinkingEffort, type ThinkingEffort } from './thinking'; + +export class ConfigState { + private _cwd: string = ''; + private _modelAlias: string | undefined; + private _profileName: string | undefined; + private _thinkingLevel: ThinkingEffort = 'off'; + private _systemPrompt: string = ''; + + constructor(protected readonly agent: Agent) {} + + update(input: AgentConfigUpdateData): void { + const changed = { ...input }; + if (Object.keys(changed).length === 0) return; + + if (changed.thinkingLevel !== undefined) { + changed.thinkingLevel = resolveThinkingEffort( + changed.thinkingLevel, + this.agent.providerManager?.config.thinking, + ); + } + this.agent.records.logRecord({ + type: 'config.update', + ...changed, + }); + this.agent.replayBuilder.push({ + type: 'config_updated', + config: changed, + }); + if (changed.cwd !== undefined) this._cwd = changed.cwd; + if (Object.hasOwn(changed, 'modelAlias')) { + this._modelAlias = changed.modelAlias ?? undefined; + } + if (Object.hasOwn(changed, 'profileName')) this._profileName = changed.profileName; + if (changed.thinkingLevel !== undefined) + this._thinkingLevel = changed.thinkingLevel as ThinkingEffort; + if (changed.systemPrompt !== undefined) this._systemPrompt = changed.systemPrompt; + if (this.hasProvider && (changed.cwd !== undefined || Object.hasOwn(changed, 'modelAlias'))) { + this.agent.tools.initializeBuiltinTools(); + } + this.agent.emitStatusUpdated(); + } + + data(): AgentConfigData { + const resolved = this.tryResolvedProviderConfig(); + return { + cwd: this.cwd, + provider: resolved?.provider, + modelAlias: this._modelAlias, + modelCapabilities: resolved?.modelCapabilities ?? UNKNOWN_CAPABILITY, + profileName: this.profileName, + thinkingLevel: this.thinkingLevel, + systemPrompt: this.systemPrompt, + }; + } + + get cwd(): string { + return this._cwd; + } + + get hasModel(): boolean { + return this._modelAlias !== undefined; + } + + get hasProvider(): boolean { + return this.tryResolvedProviderConfig() !== undefined; + } + + get providerConfig(): ProviderConfig { + const provider = this.resolvedProviderConfig?.provider; + if (provider === undefined) { + throw new Error('Provider not set'); + } + return provider; + } + + get provider(): ChatProvider { + return createProvider(this.providerConfig); + } + + get model(): string { + if (this._modelAlias === undefined) { + throw new Error('Model not set'); + } + return this._modelAlias; + } + + get modelAlias(): string | undefined { + return this._modelAlias; + } + + get thinkingLevel(): ThinkingEffort { + return this._thinkingLevel; + } + + get profileName(): string | undefined { + return this._profileName; + } + + get systemPrompt(): string { + return this._systemPrompt; + } + + get modelCapabilities(): ModelCapability { + return this.tryResolvedProviderConfig()?.modelCapabilities ?? UNKNOWN_CAPABILITY; + } + + private get resolvedProviderConfig(): ResolvedRuntimeProvider | undefined { + if (this._modelAlias === undefined) return undefined; + return this.agent.providerManager?.resolveProviderConfigForModel(this._modelAlias); + } + + private tryResolvedProviderConfig(): ResolvedRuntimeProvider | undefined { + try { + return this.resolvedProviderConfig; + } catch { + return undefined; + } + } +} diff --git a/packages/agent-core/src/agent/config/thinking.ts b/packages/agent-core/src/agent/config/thinking.ts new file mode 100644 index 000000000..f6a7d924a --- /dev/null +++ b/packages/agent-core/src/agent/config/thinking.ts @@ -0,0 +1,31 @@ +import type { ThinkingEffort } from '@moonshot-ai/kosong'; + +import type { ThinkingConfig } from '../../config/schema'; + +export type { ThinkingEffort }; + +const DEFAULT_THINKING_EFFORT: ThinkingEffort = 'high'; + +const THINKING_EFFORTS = new Set(['low', 'medium', 'high', 'xhigh', 'max']); + +export function resolveThinkingEffort( + requested: string | undefined, + defaults: ThinkingConfig | undefined, +): ThinkingEffort { + const configEffort = parseEffort(defaults?.effort) ?? DEFAULT_THINKING_EFFORT; + const normalized = requested?.trim().toLowerCase(); + if (!normalized) { + if (defaults?.mode === 'off') return 'off'; + return configEffort; + } + if (normalized === 'off') return 'off'; + if (normalized === 'on') return configEffort; + return parseEffort(normalized) ?? configEffort; +} + +function parseEffort(value: string | undefined): ThinkingEffort | undefined { + const normalized = value?.trim().toLowerCase(); + return normalized !== undefined && THINKING_EFFORTS.has(normalized as ThinkingEffort) + ? (normalized as ThinkingEffort) + : undefined; +} diff --git a/packages/agent-core/src/agent/config/types.ts b/packages/agent-core/src/agent/config/types.ts new file mode 100644 index 000000000..fc7a785f5 --- /dev/null +++ b/packages/agent-core/src/agent/config/types.ts @@ -0,0 +1,19 @@ +import type { ModelCapability, ProviderConfig } from '@moonshot-ai/kosong'; + +export interface AgentConfigData { + cwd: string; + provider?: ProviderConfig; + modelAlias?: string; + modelCapabilities: ModelCapability; + profileName?: string; + thinkingLevel: string; + systemPrompt: string; +} + +export type AgentConfigUpdateData = Partial<{ + cwd: string; + modelAlias: string; + profileName: string; + thinkingLevel: string; + systemPrompt: string; +}>; diff --git a/packages/agent-core/src/agent/context/complete-slice.ts b/packages/agent-core/src/agent/context/complete-slice.ts new file mode 100644 index 000000000..258cdf90e --- /dev/null +++ b/packages/agent-core/src/agent/context/complete-slice.ts @@ -0,0 +1,55 @@ +import type { Message } from '@moonshot-ai/kosong'; + +export function sliceCompleteMessages( + messages: readonly Message[], + requestedEnd: number, +): number { + let normalized = Math.max(0, Math.min(messages.length, requestedEnd)); + + for (let i = 0; i < messages.length; i += 1) { + const message = messages[i]; + if (message?.role !== 'assistant' || message.toolCalls.length === 0) continue; + + const end = findToolExchangeEnd(messages, i); + if (end === undefined) { + if (normalized > i) { + normalized = includePromptForAssistant(messages, i); + } + continue; + } + + if (normalized > i && normalized < end) { + normalized = includePromptForAssistant(messages, i); + } + } + + return normalized; +} + +function findToolExchangeEnd( + messages: readonly Message[], + assistantIndex: number, +): number | undefined { + const assistant = messages[assistantIndex]; + if (assistant?.role !== 'assistant') return undefined; + + const pending = new Set(assistant.toolCalls.map((call) => call.id)); + if (pending.size === 0) return assistantIndex + 1; + + for (let i = assistantIndex + 1; i < messages.length; i += 1) { + const message = messages[i]; + if (message?.role !== 'tool') return undefined; + if (message.toolCallId !== undefined) { + pending.delete(message.toolCallId); + } + if (pending.size === 0) return i + 1; + } + + return undefined; +} + +function includePromptForAssistant(messages: readonly Message[], assistantIndex: number): number { + const previous = messages[assistantIndex - 1]; + if (previous?.role === 'user') return assistantIndex - 1; + return assistantIndex; +} diff --git a/packages/agent-core/src/agent/context/index.ts b/packages/agent-core/src/agent/context/index.ts new file mode 100644 index 000000000..fd9141ed2 --- /dev/null +++ b/packages/agent-core/src/agent/context/index.ts @@ -0,0 +1,268 @@ +import { createToolMessage, type ContentPart, type Message } from '@moonshot-ai/kosong'; + +import type { Agent } from '..'; +import type { ExecutableToolResult, LoopRecordedEvent } from '../../loop'; +import { estimateTokensForMessages } from '../../utils/tokens'; +import type { CompactionResult } from '../compaction'; +import { project } from './projector'; +import { + USER_PROMPT_ORIGIN, + type AgentContextData, + type ContextMessage, + type PromptOrigin, +} from './types'; + +export * from './types'; + +const TOOL_ERROR_STATUS = 'ERROR: Tool execution failed.'; +const TOOL_EMPTY_STATUS = 'Tool output is empty.'; +const TOOL_EMPTY_ERROR_STATUS = + 'ERROR: Tool execution failed. Tool output is empty.'; +const TOOL_OUTPUT_EMPTY_TEXT = 'Tool output is empty.'; + +export class ContextMemory { + private _history: ContextMessage[] = []; + private _tokenCount = 0; + private tokenCountCoveredMessageCount = 0; + private openSteps: Map = new Map(); + private pendingToolResultIds = new Set(); + private deferredMessages: ContextMessage[] = []; + + constructor(protected readonly agent: Agent) {} + + appendUserMessage( + content: readonly ContentPart[], + origin: PromptOrigin = USER_PROMPT_ORIGIN, + ): void { + this.appendMessage({ + role: 'user', + content: [...content], + toolCalls: [], + origin, + }); + } + + appendSystemReminder(content: string, origin: PromptOrigin): void { + const text = `\n${content}\n`; + this.appendMessage({ + role: 'user', + content: [{ type: 'text', text }], + toolCalls: [], + origin, + }); + } + + markLastUserPromptBlocked(hookEvent: string): void { + this.agent.records.logRecord({ + type: 'context.mark_last_user_prompt_blocked', + hookEvent, + }); + for (let i = this._history.length - 1; i >= 0; i--) { + const message = this._history[i]; + if (message?.role !== 'user' || message.origin?.kind !== 'user') continue; + this._history[i] = { + ...message, + origin: { ...message.origin, blockedByHook: hookEvent }, + }; + return; + } + } + + clear(): void { + this.agent.records.logRecord({ type: 'context.clear' }); + this._history = []; + this._tokenCount = 0; + this.tokenCountCoveredMessageCount = 0; + this.openSteps.clear(); + this.pendingToolResultIds.clear(); + this.deferredMessages = []; + this.agent.injection.onContextClear(); + this.agent.emitStatusUpdated(); + } + + applyCompaction(summary: CompactionResult): void { + this.agent.records.logRecord({ + type: 'context.apply_compaction', + ...summary, + }); + this._history = [ + { + role: 'assistant', + content: [{ type: 'text', text: summary.summary }], + toolCalls: [], + origin: { kind: 'compaction_summary' }, + }, + ...this._history.slice(summary.compactedCount), + ]; + this.openSteps.clear(); + this.flushDeferredMessagesIfToolExchangeClosed(); + this._tokenCount = summary.tokensAfter; + this.tokenCountCoveredMessageCount = this._history.length; + this.agent.injection.onContextCompacted(summary.compactedCount); + this.agent.emitStatusUpdated(); + } + + data(): AgentContextData { + return { + history: this.history, + tokenCount: this.tokenCount, + }; + } + + get tokenCount(): number { + return this._tokenCount; + } + + get tokenCountWithPending(): number { + const pendingMessages = this._history.slice(this.tokenCountCoveredMessageCount); + return this._tokenCount + estimateTokensForMessages(project(pendingMessages)); + } + + get history(): readonly ContextMessage[] { + return this._history; + } + + get messages(): Message[] { + return project(this.history); + } + + appendLoopEvent(event: LoopRecordedEvent): void { + this.agent.records.logRecord({ + type: 'context.append_loop_event', + event, + }); + switch (event.type) { + case 'step.begin': { + const message: ContextMessage = { + role: 'assistant', + content: [], + toolCalls: [], + }; + this.pushHistory(message); + this.openSteps.set(event.uuid, message); + return; + } + case 'step.end': { + const openStep = this.openSteps.get(event.uuid); + this.openSteps.delete(event.uuid); + if (event.usage !== undefined) { + const openStepIndex = openStep === undefined ? -1 : this._history.indexOf(openStep); + this._tokenCount = + event.usage.inputCacheRead + + event.usage.inputCacheCreation + + event.usage.inputOther + + event.usage.output; + this.tokenCountCoveredMessageCount = + openStepIndex === -1 ? this._history.length : openStepIndex + 1; + } + this.flushDeferredMessagesIfToolExchangeClosed(); + return; + } + case 'content.part': { + const openStep = this.openSteps.get(event.stepUuid); + if (openStep === undefined) { + throw new Error( + `Received content_part for unknown step_uuid '${event.stepUuid}' (no open step_begin)`, + ); + } + openStep.content.push(event.part); + return; + } + case 'tool.call': { + const openStep = this.openSteps.get(event.stepUuid); + if (openStep === undefined) { + throw new Error( + `Received tool_call for unknown step_uuid '${event.stepUuid}' (no open step_begin)`, + ); + } + openStep.toolCalls.push({ + type: 'function', + id: event.toolCallId, + function: { + name: event.name, + arguments: event.args === undefined ? null : JSON.stringify(event.args), + }, + }); + this.pendingToolResultIds.add(event.toolCallId); + return; + } + case 'tool.result': { + const message = createToolMessage(event.toolCallId, toolResultOutputForModel(event.result)); + this.pushHistory({ + ...message, + role: 'tool', + isError: event.result.isError, + }); + this.pendingToolResultIds.delete(event.toolCallId); + this.flushDeferredMessagesIfToolExchangeClosed(); + return; + } + } + } + + appendMessage(message: ContextMessage): void { + this.agent.records.logRecord({ + type: 'context.append_message', + message, + }); + if (this.hasOpenToolExchange()) { + this.deferredMessages.push(message); + return; + } + this.pushHistory(message); + } + + private flushDeferredMessagesIfToolExchangeClosed(): void { + if (this.pendingToolResultIds.size > 0 || this.deferredMessages.length === 0) { + return; + } + this.pushHistory(...this.deferredMessages); + this.deferredMessages = []; + } + + private hasOpenToolExchange(): boolean { + return this.pendingToolResultIds.size > 0; + } + + private pushHistory(...messages: ContextMessage[]): void { + this._history.push(...messages); + for (const message of messages) { + if (message.origin?.kind === 'background_task') { + this.agent.background.markDeliveredNotification(message.origin); + } + this.agent.replayBuilder.push({ + type: 'message', + message, + }); + } + } +} + +function toolResultOutputForModel(result: ExecutableToolResult): string | ContentPart[] { + const output = result.output; + if (typeof output === 'string') { + if (result.isError === true) { + if (output.length === 0) return TOOL_EMPTY_ERROR_STATUS; + if (output.trimStart().startsWith('ERROR:')) return output; + return `${TOOL_ERROR_STATUS}\n${output}`; + } + return isEmptyOutputText(output) ? TOOL_EMPTY_STATUS : output; + } + + if (output.length === 0) { + return [ + { + type: 'text', + text: result.isError === true ? TOOL_EMPTY_ERROR_STATUS : TOOL_EMPTY_STATUS, + }, + ]; + } + if (result.isError === true) { + return [{ type: 'text', text: TOOL_ERROR_STATUS }, ...output]; + } + return output; +} + +function isEmptyOutputText(output: string): boolean { + return output.length === 0 || output.trim() === TOOL_OUTPUT_EMPTY_TEXT; +} diff --git a/packages/agent-core/src/agent/context/notification-xml.ts b/packages/agent-core/src/agent/context/notification-xml.ts new file mode 100644 index 000000000..72e584b48 --- /dev/null +++ b/packages/agent-core/src/agent/context/notification-xml.ts @@ -0,0 +1,72 @@ +/** + * Notification XML rendering — produces the chat-history injection text + * shared between the live ContextMemory and the projector. + * + * Output shape: + * + * Title: ... + * Severity: ... + * + * (only when source_kind === 'background_task' and tail_output is non-empty) + * + * + * + * + * The opening-tag names (``) are + * load-bearing for the projector's `mergeAdjacentUserMessages` detector + * — rename requires updating the detector too. + */ + +export function renderNotificationXml(data: Record): string { + const id = stringAttr(data['id'], 'unknown'); + const category = stringAttr(data['category'], 'unknown'); + const type = stringAttr(data['type'], 'unknown'); + const sourceKind = stringAttr(data['source_kind'], 'unknown'); + const sourceId = stringAttr(data['source_id'], 'unknown'); + const title = typeof data['title'] === 'string' ? data['title'] : ''; + const severity = typeof data['severity'] === 'string' ? data['severity'] : ''; + const body = typeof data['body'] === 'string' ? data['body'] : ''; + + const lines: string[] = [ + ``, + ]; + if (title.length > 0) lines.push(`Title: ${title}`); + if (severity.length > 0) lines.push(`Severity: ${severity}`); + if (body.length > 0) lines.push(body); + + if (data['source_kind'] === 'background_task') { + const tailRaw = typeof data['tail_output'] === 'string' ? data['tail_output'] : ''; + if (tailRaw.length > 0) { + const truncated = truncateTailOutput(tailRaw, 20, 3000); + lines.push(''); + lines.push(truncated); + lines.push(''); + } + } + + lines.push(''); + return lines.join('\n'); +} + +/** + * Truncate tail output to at most `maxLines` lines and `maxChars` + * characters. Takes the *last* N lines, then trims from the front if + * the character budget is exceeded. + */ +function truncateTailOutput(raw: string, maxLines: number, maxChars: number): string { + const allLines = raw.split('\n'); + const tailLines = allLines.length > maxLines ? allLines.slice(-maxLines) : allLines; + let result = tailLines.join('\n'); + if (result.length > maxChars) { + result = result.slice(-maxChars); + } + return result; +} + +function stringAttr(value: unknown, fallback: string): string { + if (typeof value !== 'string' || value.length === 0) return fallback; + // Attribute boundary safety: escape `&` and `"`. Body-text `<` / `>` + // stay untouched — the injection target is an LLM-visible transcript + // where double-escaping would be noisier than literal punctuation. + return value.replaceAll('&', '&').replaceAll('"', '"'); +} diff --git a/packages/agent-core/src/agent/context/projector.ts b/packages/agent-core/src/agent/context/projector.ts new file mode 100644 index 000000000..3b5ac10a7 --- /dev/null +++ b/packages/agent-core/src/agent/context/projector.ts @@ -0,0 +1,173 @@ +import type { ContentPart, Message, TextPart } from '@moonshot-ai/kosong'; + +import { renderNotificationXml } from './notification-xml'; + +type ProjectableMessage = Message & { + readonly origin?: + | { + readonly kind: string; + readonly event?: string | undefined; + readonly blockedByHook?: string | undefined; + } + | undefined; +}; + +const TRANSCRIPT_ONLY_HOOK_RESULT_EVENTS = new Set(['UserPromptSubmit']); + +export interface EphemeralInjection { + kind: 'memory_recall' | 'system_reminder' | 'pending_notification'; + content: string | Record; + position?: 'before_user' | 'after_system'; +} + +export function project( + history: readonly ProjectableMessage[], + ephemeralInjections?: readonly EphemeralInjection[], +): Message[] { + // Keep partial or empty assistant placeholders away from providers. + // They can appear when a turn is aborted or errors before any content + // or tool call is appended. + const usable = history.filter((message) => { + if (isBlockedUserPrompt(message)) return false; + return ( + !isTranscriptOnlyHookResult(message) && + message.partial !== true && + !(message.role === 'assistant' && message.content.length === 0 && message.toolCalls.length === 0) + ); + }); + const merged = mergeAdjacentUserMessages(usable); + + const injectionMessages = ephemeralInjections?.map((injection) => renderInjection(injection)); + + // Ephemeral injections sit before the first history message + // (before_user) so things like system_reminder land right before the + // user turn they contextualise. + return injectionMessages ? [...injectionMessages, ...merged] : merged; +} + +function isTranscriptOnlyHookResult(message: ProjectableMessage): boolean { + return ( + message.origin?.kind === 'hook_result' && + TRANSCRIPT_ONLY_HOOK_RESULT_EVENTS.has(message.origin.event ?? '') + ); +} + +function isBlockedUserPrompt(message: ProjectableMessage): boolean { + return message.role === 'user' && message.origin?.blockedByHook === 'UserPromptSubmit'; +} + +/** + * Render an EphemeralInjection into a synthetic user message. System + * reminders and pending notifications use XML wrappers so the model can + * distinguish host annotations from genuine user text. `memory_recall` + * stays as free text. + * + * The merge-guard logic downstream (`mergeAdjacentUserMessages`) uses + * the `` opening tag to detect + * these messages, so the exact tag names are load-bearing for + * projector correctness — do not rename without also updating + * `isInjectionUserMessage` below. + */ +function renderInjection(injection: EphemeralInjection): Message { + const text = renderInjectionText(injection); + return { + role: 'user', + content: [{ type: 'text', text }], + toolCalls: [], + }; +} + +function renderInjectionText(injection: EphemeralInjection): string { + const { kind, content } = injection; + if (kind === 'pending_notification') { + // Production callers pass notification metadata, but accepting a + // string keeps older embedders from crashing on replay/projection. + if (typeof content === 'string') { + return `\n${content}\n`; + } + return renderNotificationXml(content); + } + if (kind === 'system_reminder') { + const body = typeof content === 'string' ? content : JSON.stringify(content); + return `\n${body}\n`; + } + const body = typeof content === 'string' ? content : JSON.stringify(content); + return body; +} + +/** + * Detect whether a user message was produced by the ephemeral injection + * pipeline (system_reminder or notification XML tag). Such messages + * must never be merged with an adjacent real user turn — doing so would + * smear the injection's XML wrapper into the user's actual prompt and + * confuse the LLM about where the system annotation ends. + * + */ +function isInjectionUserMessage(message: Message): boolean { + if (message.role !== 'user') return false; + const text = extractTextOnly(message); + // Cheap leading-fragment check — injections always have the opening + // tag at the start. We use `trimStart()` so leading whitespace + // doesn't defeat the check, and require `'` tag (no attributes) is not misidentified. + const trimmed = text.trimStart(); + if (trimmed.startsWith('')) return true; + if (trimmed.startsWith(' p.type !== 'text'), + ...b.content.filter((p) => p.type !== 'text'), + ]; + const mergedText: TextPart = { type: 'text', text: `${aText}\n\n${bText}` }; + const content: ContentPart[] = [mergedText, ...nonTextParts]; + return { + role: 'user', + content, + toolCalls: [], + }; +} + +function extractTextOnly(message: Message): string { + return message.content + .filter((p): p is TextPart => p.type === 'text') + .map((p) => p.text) + .join(''); +} + +function cloneMessage(message: Message): Message { + return { + role: message.role, + name: message.name, + content: message.content.map((p) => ({ ...p })) as ContentPart[], + toolCalls: message.toolCalls.map((tc) => ({ ...tc })), + toolCallId: message.toolCallId, + partial: message.partial, + }; +} diff --git a/packages/agent-core/src/agent/context/types.ts b/packages/agent-core/src/agent/context/types.ts new file mode 100644 index 000000000..b8d077009 --- /dev/null +++ b/packages/agent-core/src/agent/context/types.ts @@ -0,0 +1,78 @@ +import type { ContentPart, Message } from '@moonshot-ai/kosong'; + +import type { SkillSource } from '../../skill'; +import type { BackgroundTaskStatus } from '../../tools/background'; + +export interface UserPromptOrigin { + readonly kind: 'user'; + readonly blockedByHook?: string | undefined; +} + +export const USER_PROMPT_ORIGIN: UserPromptOrigin = { kind: 'user' }; + +export interface SkillActivationOrigin { + readonly kind: 'skill_activation'; + readonly activationId: string; + readonly skillName: string; + readonly skillArgs?: string | undefined; + readonly trigger: 'user-slash' | 'model-tool' | 'nested-skill'; + readonly skillType?: string | undefined; + readonly skillPath?: string | undefined; + readonly skillSource?: SkillSource | undefined; +} + +export interface InjectionOrigin { + readonly kind: 'injection'; + readonly variant: string; +} + +export interface CompactionSummaryOrigin { + readonly kind: 'compaction_summary'; +} + +export interface SystemTriggerOrigin { + readonly kind: 'system_trigger'; + readonly name: string; +} + +export interface BackgroundTaskOrigin { + readonly kind: 'background_task'; + readonly taskId: string; + readonly status: BackgroundTaskStatus; + readonly notificationId: string; +} + +export interface HookResultOrigin { + readonly kind: 'hook_result'; + readonly event: string; + readonly blocked?: boolean; +} + +export type PromptOrigin = + | UserPromptOrigin + | SkillActivationOrigin + | InjectionOrigin + | CompactionSummaryOrigin + | SystemTriggerOrigin + | BackgroundTaskOrigin + | HookResultOrigin; + +export type ContextMessage = Message & { + readonly origin?: PromptOrigin | undefined; + readonly isError?: boolean; +}; + +export interface UserMessageRecord { + content: readonly ContentPart[]; + origin: PromptOrigin; +} + +export interface SystemReminderRecord { + content: string; + origin: PromptOrigin; +} + +export interface AgentContextData { + history: readonly ContextMessage[]; + tokenCount: number; +} diff --git a/packages/agent-core/src/agent/hooks/engine.ts b/packages/agent-core/src/agent/hooks/engine.ts new file mode 100644 index 000000000..832ecd620 --- /dev/null +++ b/packages/agent-core/src/agent/hooks/engine.ts @@ -0,0 +1,186 @@ +import { runHook } from './runner'; +import type { + HookBlockDecision, + HookDef, + HookEngineOptions, + HookEngineTriggerArgs, + HookMatcherValue, + HookResult, +} from './types'; + +const DEFAULT_HOOK_TIMEOUT_SECONDS = 30; + +export class HookEngine { + private readonly byEvent = new Map(); + private readonly pendingTriggers = new Set>(); + + constructor( + hooks: readonly HookDef[] = [], + private readonly options: HookEngineOptions = {}, + ) { + for (const hook of hooks) { + const entries = this.byEvent.get(hook.event) ?? []; + entries.push(hook); + this.byEvent.set(hook.event, entries); + } + } + + get summary(): Record { + const result: Record = {}; + for (const [event, hooks] of this.byEvent.entries()) { + result[event] = hooks.length; + } + return result; + } + + trigger(event: string, args: HookEngineTriggerArgs = {}): Promise { + try { + return this.triggerInner(event, args).catch((): HookResult[] => []); + } catch { + return Promise.resolve([]); + } + } + + async triggerBlock( + event: string, + args: HookEngineTriggerArgs = {}, + ): Promise { + return blockDecision(event, await this.trigger(event, args)); + } + + fireAndForgetTrigger( + event: string, + args: HookEngineTriggerArgs = {}, + ): Promise { + let promise: Promise; + try { + promise = this.trigger(event, args).catch((): HookResult[] => []); + } catch { + promise = Promise.resolve([]); + } + this.pendingTriggers.add(promise); + void promise.finally(() => { + this.pendingTriggers.delete(promise); + }); + return promise; + } + + private async triggerInner( + event: string, + args: HookEngineTriggerArgs, + ): Promise { + const matcherValue = matcherValueText(args.matcherValue); + const inputData = toHookInputData({ + hookEventName: event, + sessionId: this.options.sessionId ?? '', + cwd: this.options.cwd ?? '', + ...args.inputData, + }); + const matched = this.matchingHooks(event, matcherValue); + if (matched.length === 0) return []; + + this.emitTriggered(event, matcherValue, matched.length); + const startedAt = Date.now(); + const results = await Promise.all( + matched.map((hook) => + runHook(hook.command, inputData, { + timeout: hook.timeout ?? DEFAULT_HOOK_TIMEOUT_SECONDS, + cwd: this.options.cwd === '' ? undefined : this.options.cwd, + signal: args.signal, + }), + ), + ); + const { action, reason } = aggregateResults(event, results); + this.emitResolved(event, matcherValue, action, reason, Date.now() - startedAt); + return results; + } + + private matchingHooks(event: string, matcherValue: string): HookDef[] { + const seenCommands = new Set(); + const matched: HookDef[] = []; + + for (const hook of this.byEvent.get(event) ?? []) { + if (!matches(hook.matcher ?? '', matcherValue)) continue; + if (seenCommands.has(hook.command)) continue; + seenCommands.add(hook.command); + matched.push(hook); + } + + return matched; + } + + private emitTriggered(event: string, target: string, count: number): void { + try { + this.options.onTriggered?.(event, target, count); + } catch {} + } + + private emitResolved( + event: string, + target: string, + action: string, + reason: string | undefined, + durationMs: number, + ): void { + try { + this.options.onResolved?.(event, target, action, reason, durationMs); + } catch {} + } +} + +function matches(pattern: string, value: string): boolean { + if (pattern.length === 0) return true; + try { + return new RegExp(pattern).test(value); + } catch { + return false; + } +} + +function matcherValueText(value: HookMatcherValue | undefined): string { + if (value === undefined) return ''; + if (typeof value === 'string') return value; + return value + .filter((part) => part.type === 'text') + .map((part) => part.text) + .join(' '); +} + +function aggregateResults( + event: string, + results: readonly HookResult[], +): { + readonly action: 'allow' | 'block'; + readonly reason?: string; +} { + const block = blockDecision(event, results); + if (block !== undefined) { + return { action: 'block', reason: block.reason }; + } + return { action: 'allow' }; +} + +function blockDecision( + event: string, + results: readonly HookResult[], +): HookBlockDecision | undefined { + const block = results.find((result) => result.action === 'block'); + if (block === undefined) return undefined; + const reason = block.reason?.trim(); + return { + block: true, + reason: reason === undefined || reason.length === 0 ? `Blocked by ${event} hook` : reason, + }; +} + +function toHookInputData(input: Record): Record { + const result: Record = {}; + for (const [key, value] of Object.entries(input)) { + result[camelToSnake(key)] = value; + } + return result; +} + +function camelToSnake(value: string): string { + return value.replaceAll(/[A-Z]/g, (ch) => `_${ch.toLowerCase()}`); +} diff --git a/packages/agent-core/src/agent/hooks/index.ts b/packages/agent-core/src/agent/hooks/index.ts new file mode 100644 index 000000000..4330aacc5 --- /dev/null +++ b/packages/agent-core/src/agent/hooks/index.ts @@ -0,0 +1,4 @@ +export * from './engine'; +export * from './runner'; +export * from './types'; +export * from './user-prompt'; diff --git a/packages/agent-core/src/agent/hooks/runner.ts b/packages/agent-core/src/agent/hooks/runner.ts new file mode 100644 index 000000000..4e3c30eda --- /dev/null +++ b/packages/agent-core/src/agent/hooks/runner.ts @@ -0,0 +1,228 @@ +import { spawn, type ChildProcessWithoutNullStreams } from 'node:child_process'; + +import { z } from 'zod'; + +import type { HookResult } from './types'; + +export interface RunHookOptions { + readonly timeout: number; + readonly cwd?: string; + readonly signal?: AbortSignal; +} + +const DEFAULT_TIMEOUT_SECONDS = 30; +const KILL_GRACE_MS = 100; +const OptionalStringSchema = z.preprocess( + (value) => { + if (value === undefined || value === null) return undefined; + if (typeof value === 'string') return value; + if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') { + return String(value); + } + return undefined; + }, + z.string().optional(), +); +const HookSpecificOutputSchema = z.preprocess( + (value) => (isRecord(value) ? value : undefined), + z + .looseObject({ + message: OptionalStringSchema, + permissionDecision: z.unknown().optional(), + permissionDecisionReason: OptionalStringSchema, + }) + .optional(), +); +const HookJsonOutputSchema = z.looseObject({ + message: OptionalStringSchema, + hookSpecificOutput: HookSpecificOutputSchema, +}); + +export async function runHook( + command: string, + input: Record, + options: RunHookOptions, +): Promise { + let child: ChildProcessWithoutNullStreams; + try { + child = spawn(command, { + shell: true, + cwd: options.cwd, + stdio: 'pipe', + detached: process.platform !== 'win32', + }); + } catch (error) { + return allowResult({ stderr: errorMessage(error) }); + } + + return new Promise((resolve) => { + let stdout = ''; + let stderr = ''; + let settled = false; + const timeoutMs = timeoutSeconds(options.timeout) * 1000; + + const cleanup = () => { + clearTimeout(timeout); + options.signal?.removeEventListener('abort', onAbort); + }; + + const settle = (result: HookResult): void => { + if (settled) return; + settled = true; + cleanup(); + resolve(result); + }; + + const timeout = setTimeout(() => { + killProcess(child); + settle(allowResult({ stdout, stderr, timedOut: true })); + }, timeoutMs); + + const onAbort = (): void => { + killProcess(child); + settle(allowResult({ stdout, stderr })); + }; + + options.signal?.addEventListener('abort', onAbort, { once: true }); + if (options.signal?.aborted === true) { + onAbort(); + return; + } + + child.stdout.setEncoding('utf8'); + child.stderr.setEncoding('utf8'); + child.stdout.on('data', (chunk: string) => { + stdout += chunk; + }); + child.stderr.on('data', (chunk: string) => { + stderr += chunk; + }); + child.on('error', (error) => { + settle(allowResult({ stdout, stderr: stderr + errorMessage(error) })); + }); + child.on('close', (code) => { + settle(resultFromExitCode(code ?? 0, stdout, stderr)); + }); + + child.stdin.on('error', () => {}); + child.stdin.end(JSON.stringify(input)); + }); +} + +function timeoutSeconds(timeout: number): number { + return Number.isFinite(timeout) && timeout > 0 ? timeout : DEFAULT_TIMEOUT_SECONDS; +} + +function resultFromExitCode(exitCode: number, stdout: string, stderr: string): HookResult { + if (exitCode === 2) { + const message = stderr.trim(); + return { + action: 'block', + message, + reason: message, + stdout, + stderr, + exitCode, + }; + } + + const structured = exitCode === 0 ? structuredOutput(stdout) : undefined; + if (structured?.action === 'block') { + return { + action: 'block', + message: structured.message ?? structured.reason, + reason: structured.reason, + stdout, + stderr, + exitCode, + structuredOutput: structured.structuredOutput, + }; + } + + return allowResult({ + message: structured?.message, + stdout, + stderr, + exitCode, + structuredOutput: structured?.structuredOutput, + }); +} + +function structuredOutput( + stdout: string, +): { action?: 'block'; reason?: string; message?: string; structuredOutput: true } | undefined { + const text = stdout.trim(); + if (text.length === 0) return undefined; + + try { + const parsed = JSON.parse(text) as unknown; + const output = HookJsonOutputSchema.safeParse(parsed); + if (!output.success) return undefined; + + const { message, hookSpecificOutput } = output.data; + const result = { + message: message ?? hookSpecificOutput?.message, + structuredOutput: true as const, + }; + if (hookSpecificOutput?.permissionDecision !== 'deny') { + return result; + } + return { + action: 'block', + message: result.message, + reason: hookSpecificOutput.permissionDecisionReason, + structuredOutput: true, + }; + } catch { + return undefined; + } +} + +function allowResult(input: { + readonly message?: string; + readonly stdout?: string; + readonly stderr?: string; + readonly exitCode?: number; + readonly timedOut?: boolean; + readonly structuredOutput?: boolean; +}): HookResult { + return { + action: 'allow', + message: input.message, + stdout: input.stdout, + stderr: input.stderr, + exitCode: input.exitCode, + timedOut: input.timedOut, + structuredOutput: input.structuredOutput, + }; +} + +function killProcess(child: ChildProcessWithoutNullStreams): void { + tryKillProcess(child, 'SIGTERM'); + const killTimer = setTimeout(() => { + tryKillProcess(child, 'SIGKILL'); + }, KILL_GRACE_MS); + killTimer.unref(); +} + +function tryKillProcess(child: ChildProcessWithoutNullStreams, signal: NodeJS.Signals): void { + try { + if (process.platform !== 'win32' && child.pid !== undefined) { + process.kill(-child.pid, signal); + } else { + child.kill(signal); + } + } catch { + try { + child.kill(signal); + } catch {} + } +} + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + +function errorMessage(error: unknown): string { + return error instanceof Error ? error.message : String(error); +} diff --git a/packages/agent-core/src/agent/hooks/types.ts b/packages/agent-core/src/agent/hooks/types.ts new file mode 100644 index 000000000..f1ee9ce2b --- /dev/null +++ b/packages/agent-core/src/agent/hooks/types.ts @@ -0,0 +1,67 @@ +import type { ContentPart } from '@moonshot-ai/kosong'; + +export const HOOK_EVENT_TYPES = [ + 'PreToolUse', + 'PostToolUse', + 'PostToolUseFailure', + 'UserPromptSubmit', + 'Stop', + 'StopFailure', + 'SessionStart', + 'SessionEnd', + 'SubagentStart', + 'SubagentStop', + 'PreCompact', + 'PostCompact', + 'Notification', +] as const; + +export type HookEventType = (typeof HOOK_EVENT_TYPES)[number]; + +export interface HookDef { + readonly event: HookEventType; + readonly matcher?: string; + readonly command: string; + readonly timeout?: number; +} + +export interface HookResult { + readonly action: 'allow' | 'block'; + readonly message?: string; + readonly reason?: string; + readonly stdout?: string; + readonly stderr?: string; + readonly exitCode?: number; + readonly timedOut?: boolean; + readonly structuredOutput?: boolean; +} + +export interface HookBlockDecision { + readonly block: true; + readonly reason: string; +} + +export type HookMatcherValue = string | readonly ContentPart[]; + +export interface HookEngineTriggerArgs { + readonly matcherValue?: HookMatcherValue; + readonly inputData?: Record; + readonly signal?: AbortSignal; +} + +export type HookTriggeredCallback = (event: string, target: string, count: number) => void; + +export type HookResolvedCallback = ( + event: string, + target: string, + action: string, + reason: string | undefined, + durationMs: number, +) => void; + +export interface HookEngineOptions { + readonly cwd?: string; + readonly sessionId?: string; + readonly onTriggered?: HookTriggeredCallback; + readonly onResolved?: HookResolvedCallback; +} diff --git a/packages/agent-core/src/agent/hooks/user-prompt.ts b/packages/agent-core/src/agent/hooks/user-prompt.ts new file mode 100644 index 000000000..1b81c9f61 --- /dev/null +++ b/packages/agent-core/src/agent/hooks/user-prompt.ts @@ -0,0 +1,66 @@ +import type { HookResult } from './types'; + +export function renderHookResult(event: string, message: string): string { + return `\n${message}\n`; +} + +export interface RenderedHookResult { + readonly event: string; + readonly message: string; + readonly text: string; +} + +export function renderUserPromptHookResult( + results: readonly HookResult[] | undefined, +): RenderedHookResult | undefined { + const messages = + results + ?.filter((result) => result.action !== 'block') + ?.map(userPromptHookMessage) + .filter(isNonEmptyString) ?? + []; + if (messages.length === 0) return undefined; + const displayMessage = messages.join('\n\n'); + return { + event: 'UserPromptSubmit', + message: displayMessage, + text: messages.map((message) => renderHookResult('UserPromptSubmit', message)).join('\n'), + }; +} + +export function renderUserPromptHookBlockResult( + results: readonly HookResult[] | undefined, +): RenderedHookResult | undefined { + const block = results?.find((result) => result.action === 'block'); + if (block === undefined) return undefined; + const message = block.message?.trim(); + if (message !== undefined && message.length > 0) { + return { + event: 'UserPromptSubmit', + message, + text: renderHookResult('UserPromptSubmit', message), + }; + } + const reason = block.reason?.trim(); + const result = + reason === undefined || reason.length === 0 ? 'Blocked by UserPromptSubmit hook' : reason; + return { + event: 'UserPromptSubmit', + message: result, + text: renderHookResult('UserPromptSubmit', result), + }; +} + +function userPromptHookMessage(result: HookResult): string | undefined { + if (result.timedOut === true || (result.exitCode !== undefined && result.exitCode !== 0)) { + return undefined; + } + const message = result.message?.trim(); + if (message !== undefined && message.length > 0) return message; + const stdout = result.stdout?.trim(); + return stdout === undefined || stdout.length === 0 ? undefined : stdout; +} + +function isNonEmptyString(value: string | undefined): value is string { + return value !== undefined && value.length > 0; +} diff --git a/packages/agent-core/src/agent/index.ts b/packages/agent-core/src/agent/index.ts new file mode 100644 index 000000000..d502ae344 --- /dev/null +++ b/packages/agent-core/src/agent/index.ts @@ -0,0 +1,518 @@ +import { createHash } from 'node:crypto'; +import { join } from 'node:path'; + +import { ErrorCodes, KimiError, makeErrorPayload } from '#/errors'; +import { log } from '#/logging/logger'; +import type { Logger } from '#/logging/types'; +import type { AgentAPI, AgentEvent, SDKAgentRPC, UsageStatus } from '#/rpc'; +import { + generate, + type ChatProvider, + type Message, + type Tool, +} from '@moonshot-ai/kosong'; + +import type { McpConnectionManager } from '../mcp'; +import { + resolveSystemPromptCwd, + type PreparedSystemPromptContext, + type ResolvedAgentProfile, +} from '../profile'; +import type { ProviderManager } from '../providers/provider-manager'; +import { withProviderRequestAuth } from '../providers/request-auth'; +import type { RuntimeConfig } from '../runtime-types'; +import type { SessionSubagentHost } from '../session/subagent-host'; +import type { SkillRegistry } from '../skill'; +import { noopTelemetryClient, type TelemetryClient } from '../telemetry'; +import { + estimateTokens, + estimateTokensForMessages, + estimateTokensForTools, +} from '../utils/tokens'; +import type { PromisableMethods } from '../utils/types'; +import { BackgroundManager } from './background'; +import { FullCompaction, type CompactionStrategy } from './compaction'; +import { ConfigState } from './config'; +import { ContextMemory } from './context'; +import { HookEngine } from './hooks'; +import { InjectionManager } from './injection/manager'; +import { PermissionManager, type PermissionManagerOptions } from './permission'; +import { PlanMode } from './plan'; +import { + AgentRecords, + FileSystemAgentRecordPersistence, + restoreAgentRecord, + type AgentRecord, + type AgentRecordPersistence, +} from './records'; +import { ReplayBuilder } from './replay'; +import { SkillManager } from './skill'; +import { ToolManager } from './tool/index'; +import { TurnFlow } from './turn'; +import { + GENERATE_REQUEST_LOG_CONTEXT, + type GenerateOptionsWithRequestLog, +} from './turn/kosong-llm'; +import { UsageRecorder } from './usage'; + +export type { AgentRecord, AgentRecordPersistence } from './records'; +export type { BuiltinTool, ToolInfo, ToolSource, UserToolRegistration } from './tool'; + +export type AgentType = 'main' | 'sub' | 'independent'; + +export interface AgentConfig { + readonly runtime: RuntimeConfig; + readonly homedir?: string; + readonly skills?: SkillRegistry; + readonly rpc: SDKAgentRPC; + readonly persistence?: AgentRecordPersistence; + readonly type?: AgentType; + readonly generate?: typeof generate; + readonly compactionStrategy?: CompactionStrategy; + readonly providerManager?: ProviderManager | undefined; + readonly sessionId?: string; + readonly subagentHost?: SessionSubagentHost | undefined; + readonly mcp?: McpConnectionManager; + readonly hookEngine?: HookEngine; + readonly backgroundMaxRunningTasks?: number; + readonly backgroundSessionDir?: string; + readonly permission?: PermissionManagerOptions | undefined; + readonly onRecord?: ((record: AgentRecord) => void) | undefined; + /** Parent logger; the agent appends its own ctx (agentId already bound by session). */ + readonly log?: Logger; + readonly telemetry?: TelemetryClient | undefined; +} + +export class Agent { + readonly runtime: RuntimeConfig; + readonly homedir?: string; + readonly skills?: SkillManager; + readonly rawGenerate: typeof generate; + readonly rpc: SDKAgentRPC; + readonly telemetry: TelemetryClient; + readonly providerManager: ProviderManager | undefined; + readonly subagentHost: SessionSubagentHost | undefined; + readonly mcp: McpConnectionManager | undefined; + readonly hooks: HookEngine | undefined; + + readonly type: AgentType; + readonly records: AgentRecords; + readonly fullCompaction: FullCompaction; + readonly context: ContextMemory; + readonly config: ConfigState; + readonly turn: TurnFlow; + readonly injection: InjectionManager; + readonly permission: PermissionManager; + readonly planMode: PlanMode; + readonly usage: UsageRecorder; + readonly tools: ToolManager; + readonly background: BackgroundManager; + readonly replayBuilder: ReplayBuilder; + readonly log: Logger; + + private lastLlmConfigLogSignature?: string; + + constructor(config: AgentConfig) { + this.log = config.log ?? log; + this.runtime = config.runtime; + this.homedir = config.homedir; + if (config.skills !== undefined) { + this.skills = new SkillManager(this, config.skills); + } + this.rawGenerate = config.generate ?? generate; + this.providerManager = + config.sessionId === undefined + ? config.providerManager + : config.providerManager?.withPromptCacheKey(config.sessionId); + this.subagentHost = config.subagentHost; + this.mcp = config.mcp; + this.hooks = config.hookEngine; + + this.type = config.type ?? 'main'; + + this.rpc = config.rpc; + this.telemetry = config.telemetry ?? noopTelemetryClient; + this.records = new AgentRecords( + (record) => { + restoreAgentRecord(this, record); + }, + config.persistence ?? + (config.homedir + ? new FileSystemAgentRecordPersistence(join(config.homedir, 'wire.jsonl'), { + onError: (error) => { + this.emitRecordsWriteError(error); + }, + }) + : undefined), + ); + this.records.onRecord = config.onRecord; + this.records.onError = (error, record) => { + this.emitRecordsWriteError(error, record); + }; + this.fullCompaction = new FullCompaction(this, config.compactionStrategy); + this.context = new ContextMemory(this); + this.config = new ConfigState(this); + this.turn = new TurnFlow(this); + this.injection = new InjectionManager(this); + this.permission = new PermissionManager(this, config.permission); + this.planMode = new PlanMode(this); + this.usage = new UsageRecorder(this); + this.tools = new ToolManager(this); + this.background = new BackgroundManager(this, { + maxRunningTasks: config.backgroundMaxRunningTasks, + sessionDir: config.backgroundSessionDir, + }); + this.replayBuilder = new ReplayBuilder(this); + } + + get generate(): typeof generate { + return async (provider, systemPrompt, tools, history, callbacks, options) => { + if (options?.auth !== undefined) { + this.logLlmRequest(provider, systemPrompt, tools, history, options); + return this.rawGenerate(provider, systemPrompt, tools, history, callbacks, options); + } + const modelAlias = this.config.modelAlias; + const resolveAuth = + modelAlias === undefined + ? undefined + : this.providerManager?.createAuthResolverForModel(modelAlias, { + log: this.log, + }); + return withProviderRequestAuth(resolveAuth, (auth) => { + const requestOptions = auth === undefined ? options : { ...options, auth }; + this.logLlmRequest(provider, systemPrompt, tools, history, requestOptions); + return this.rawGenerate(provider, systemPrompt, tools, history, callbacks, requestOptions); + }); + }; + } + + private logLlmRequest( + provider: ChatProvider, + systemPrompt: string, + tools: readonly Tool[], + history: readonly Message[], + options: Parameters[5], + ): void { + const context = buildLlmRequestContext(options); + const configMetadata = buildLlmConfigMetadata( + provider, + this.config.modelAlias, + systemPrompt, + tools, + ); + this.logLlmConfigIfChanged( + context, + configMetadata, + buildLlmConfigSignature(configMetadata, systemPrompt, tools), + ); + this.log.info('llm request', { + ...context, + ...buildLlmRequestMetadata(systemPrompt, tools, history), + }); + } + + private logLlmConfigIfChanged( + context: LlmRequestContextFields, + metadata: LlmConfigMetadata, + signature: string, + ): void { + if (signature === this.lastLlmConfigLogSignature) return; + this.lastLlmConfigLogSignature = signature; + this.log.info('llm config', { + ...context, + ...metadata, + }); + } + + useProfile(profile: ResolvedAgentProfile, context?: PreparedSystemPromptContext): void { + const cwd = context?.cwd ?? resolveSystemPromptCwd(this.runtime.kaos, this.config.cwd); + const systemPrompt = profile.systemPrompt({ + osEnv: this.runtime.osEnv, + cwd, + skills: this.skills?.registry, + cwdListing: context?.cwdListing, + agentsMd: context?.agentsMd, + }); + this.config.update({ profileName: profile.name, systemPrompt }); + this.tools.setActiveTools(profile.tools); + } + + async resume(): Promise { + await this.records.replay(); + await this.background.loadFromDisk(); + await this.background.reconcile(); + this.turn.finishResume(); + } + + get rpcMethods(): PromisableMethods { + return { + prompt: (payload) => { + this.turn.prompt(payload.input); + }, + steer: (payload) => { + this.telemetry.track('input_steer', { parts: payload.input.length }); + this.turn.steer(payload.input); + }, + cancel: (payload) => { + if (this.turn.hasActiveTurn) { + this.telemetry.track('cancel', { from: 'streaming' }); + } + this.turn.cancel(payload.turnId); + }, + setThinking: (payload) => { + const wasEnabled = this.config.thinkingLevel !== 'off'; + this.config.update({ thinkingLevel: payload.level }); + const enabled = this.config.thinkingLevel !== 'off'; + if (enabled !== wasEnabled) { + this.telemetry.track('thinking_toggle', { enabled }); + } + }, + setPermission: (payload) => { + const wasYolo = this.permission.mode === 'yolo'; + const wasAuto = this.permission.mode === 'auto'; + this.permission.setMode(payload.mode); + const enabled = this.permission.mode === 'yolo'; + if (enabled !== wasYolo) { + this.telemetry.track('yolo_toggle', { enabled }); + } + const afkEnabled = this.permission.mode === 'auto'; + if (afkEnabled !== wasAuto) { + this.telemetry.track('afk_toggle', { enabled: afkEnabled }); + } + }, + setModel: async (payload) => { + const previous = this.config.modelAlias; + const resolved = await this.providerManager?.resolveProviderForModel(payload.model); + if (resolved === undefined) { + throw new Error('Runtime provider model cannot be empty'); + } + this.config.update({ + modelAlias: resolved.modelName, + }); + if (previous !== resolved.modelName) { + this.telemetry.track('model_switch', { model: resolved.modelName }); + } + return { + model: resolved.modelName, + providerName: resolved.providerName, + }; + }, + getModel: () => { + return this.config.modelAlias ?? ''; + }, + enterPlan: async () => { + await this.planMode.enter(); + }, + cancelPlan: (payload) => { + this.planMode.cancel(payload.id); + }, + clearPlan: () => this.planMode.clear(), + beginCompaction: (payload) => { + this.fullCompaction.begin({ source: 'manual', instruction: payload.instruction }); + }, + cancelCompaction: () => { + if (this.fullCompaction.isCompacting) { + this.telemetry.track('cancel', { from: 'compacting' }); + } + this.fullCompaction.cancel(); + }, + registerTool: (payload) => { + this.tools.registerUserTool(payload); + }, + unregisterTool: (payload) => { + this.tools.unregisterUserTool(payload.name); + }, + setActiveTools: (payload) => { + this.tools.setActiveTools(payload.names); + }, + stopBackground: (payload) => { + void this.background.stop(payload.taskId, payload.reason); + }, + clearContext: () => { + this.context.clear(); + }, + activateSkill: (payload) => { + if (this.skills === undefined) { + throw new KimiError(ErrorCodes.SKILL_NOT_FOUND, `Skill "${payload.name}" was not found`); + } + this.skills.activate(payload); + }, + getBackgroundOutput: (payload) => this.background.readOutput(payload.taskId, payload.tail), + getBackgroundOutputPath: (payload) => this.background.getOutputPath(payload.taskId), + getContext: () => this.context.data(), + getConfig: () => this.config.data(), + getPermission: () => this.permission.data(), + getPlan: () => this.planMode.data(), + getUsage: () => this.usage.data(), + getTools: () => this.tools.data(), + getBackground: (payload) => this.background.list(payload.activeOnly ?? false, payload.limit), + }; + } + + emitEvent(event: AgentEvent): void { + if (this.records.restoring) return; + void this.rpc.emitEvent(event); + } + + emitStatusUpdated(): void { + if (this.records.restoring) return; + if (!this.config.hasModel) return; + + const contextTokens = this.context.tokenCount; + const maxContextTokens = this.config.modelCapabilities.max_context_tokens; + const contextUsage = + maxContextTokens !== undefined && maxContextTokens > 0 + ? contextTokens / maxContextTokens + : undefined; + const usage: UsageStatus | undefined = this.usage.status(); + const model = this.config.model; + + this.emitEvent({ + type: 'agent.status.updated', + model, + contextTokens, + maxContextTokens, + contextUsage, + planMode: this.planMode.isActive, + permission: this.permission.mode, + usage, + }); + } + + private emitRecordsWriteError(error: unknown, record?: AgentRecord | undefined): void { + const message = error instanceof Error ? error.message : String(error); + this.log.error('wire record persist failed', { + agentHomedir: this.homedir, + recordType: record?.type, + error, + }); + this.emitEvent({ + type: 'error', + ...makeErrorPayload( + ErrorCodes.RECORDS_WRITE_FAILED, + `Failed to write agent records: ${message}`, + { + details: { recordType: record?.type }, + }, + ), + }); + } +} + +interface LlmRequestContextFields { + turnId?: string; + step?: number; + attempt?: number; + maxAttempts?: number; +} + +interface LlmRequestMetadata { + estimatedInputTokens: number; + messageCount: number; + toolCallCount: number; + partialMessageCount?: number; +} + +/** + * Fields that identify an LLM configuration for deduplication. + * Keep this interface simple and avoid dynamic keys — the shape is + * serialized with `JSON.stringify` to produce a stable signature in + * `logLlmConfigIfChanged`. + */ +interface LlmConfigMetadata { + provider: string; + model: string; + modelAlias?: string; + thinkingEffort?: string; + systemPromptChars: number; + toolCount: number; +} + +function buildLlmRequestContext(options: Parameters[5]): LlmRequestContextFields { + const context = requestLogContext(options); + if (context === undefined) return {}; + + const fields: LlmRequestContextFields = { + turnId: context.turnId, + step: context.step, + }; + if ( + context.attempt !== undefined && + context.maxAttempts !== undefined && + context.attempt > 1 + ) { + fields.attempt = context.attempt; + fields.maxAttempts = context.maxAttempts; + } + return fields; +} + +function buildLlmRequestMetadata( + systemPrompt: string, + tools: readonly Tool[], + history: readonly Message[], +): LlmRequestMetadata { + let toolCallCount = 0; + let partialMessageCount = 0; + + for (const message of history) { + if (message.partial === true) partialMessageCount += 1; + toolCallCount += message.toolCalls.length; + } + + const estimatedInputTokens = + estimateTokens(systemPrompt) + + estimateTokensForMessages([...history]) + + estimateTokensForTools(tools); + + const metadata: LlmRequestMetadata = { + estimatedInputTokens, + messageCount: history.length, + toolCallCount, + }; + if (partialMessageCount > 0) { + metadata.partialMessageCount = partialMessageCount; + } + return metadata; +} + +function buildLlmConfigMetadata( + provider: ChatProvider, + modelAlias: string | undefined, + systemPrompt: string, + tools: readonly Tool[], +): LlmConfigMetadata { + return { + provider: provider.name, + model: provider.modelName, + modelAlias, + thinkingEffort: provider.thinkingEffort ?? undefined, + systemPromptChars: systemPrompt.length, + toolCount: tools.length, + }; +} + +function buildLlmConfigSignature( + metadata: LlmConfigMetadata, + systemPrompt: string, + tools: readonly Tool[], +): string { + const toolsForSignature = tools.map(({ name, description, parameters }) => ({ + name, + description, + parameters, + })); + return JSON.stringify({ + ...metadata, + systemPromptHash: fingerprint(systemPrompt), + toolsHash: fingerprint(JSON.stringify(toolsForSignature)), + }); +} + +function fingerprint(content: string): string { + return createHash('sha256').update(content).digest('hex'); +} + +function requestLogContext(options: Parameters[5]) { + return (options as GenerateOptionsWithRequestLog | undefined)?.[GENERATE_REQUEST_LOG_CONTEXT]; +} diff --git a/packages/agent-core/src/agent/injection/injector.ts b/packages/agent-core/src/agent/injection/injector.ts new file mode 100644 index 000000000..1084e32ed --- /dev/null +++ b/packages/agent-core/src/agent/injection/injector.ts @@ -0,0 +1,33 @@ +import type { Agent } from '..'; + +export abstract class DynamicInjector { + protected injectedAt: number | null = null; + + constructor(protected readonly agent: Agent) {} + + onContextClear(): void { + this.injectedAt = null; + } + + onContextCompacted(compactedCount: number): void { + if (this.injectedAt !== null) { + const newInjectedAt = this.injectedAt - compactedCount + 1; + this.injectedAt = newInjectedAt >= 0 ? newInjectedAt : null; + } + } + + async inject(): Promise { + const injection = await this.getInjection(); + if (injection) { + this.injectedAt = this.agent.context.history.length; + this.agent.context.appendSystemReminder(injection, { + kind: 'injection', + variant: this.injectionVariant, + }); + } + } + + protected abstract readonly injectionVariant: string; + + protected abstract getInjection(): string | Promise | undefined; +} diff --git a/packages/agent-core/src/agent/injection/manager.ts b/packages/agent-core/src/agent/injection/manager.ts new file mode 100644 index 000000000..ffc1fcf01 --- /dev/null +++ b/packages/agent-core/src/agent/injection/manager.ts @@ -0,0 +1,34 @@ +import type { Agent } from '..'; +import type { DynamicInjector } from './injector'; +import { PermissionModeInjector } from './permission-mode'; +import { PlanModeInjector } from './plan-mode'; + +export class InjectionManager { + private readonly injectors: DynamicInjector[]; + + constructor(protected readonly agent: Agent) { + this.injectors = [new PlanModeInjector(agent), new PermissionModeInjector(agent)]; + } + + async inject(): Promise { + for (const injector of this.injectors) { + await injector.inject(); + } + } + + onContextClear(): void { + for (const injector of this.injectors) { + injector.onContextClear(); + } + } + + onContextCompacted(compactedCount: number): void { + for (const injector of this.injectors) { + try { + injector.onContextCompacted(compactedCount); + } catch { + continue; + } + } + } +} diff --git a/packages/agent-core/src/agent/injection/permission-mode.ts b/packages/agent-core/src/agent/injection/permission-mode.ts new file mode 100644 index 000000000..638ed6760 --- /dev/null +++ b/packages/agent-core/src/agent/injection/permission-mode.ts @@ -0,0 +1,30 @@ +import type { PermissionMode } from '../permission'; +import { DynamicInjector } from './injector'; + +const AUTO_MODE_ENTER_REMINDER = [ + 'Auto permission mode is active. Tool approvals will be handled automatically while this mode remains enabled.', + ' - Continue normally without pausing for approval prompts.', + ' - Do NOT call AskUserQuestion while auto mode is active. Make a reasonable decision and continue without asking the user.', +].join('\n'); + +const AUTO_MODE_EXIT_REMINDER = [ + 'Auto permission mode is no longer active. Tool approvals and permission checks are back to the current mode.', + ' - Continue normally, but expect approval prompts or denials when a tool requires them.', +].join('\n'); + +export class PermissionModeInjector extends DynamicInjector { + protected override readonly injectionVariant = 'permission_mode'; + private lastMode: PermissionMode | undefined; + + getInjection(): string | undefined { + const mode = this.agent.permission.mode; + const previousMode = this.lastMode; + + if (mode === previousMode) return undefined; + + this.lastMode = mode; + if (mode === 'auto') return AUTO_MODE_ENTER_REMINDER; + if (previousMode === 'auto') return AUTO_MODE_EXIT_REMINDER; + return undefined; + } +} diff --git a/packages/agent-core/src/agent/injection/plan-mode.ts b/packages/agent-core/src/agent/injection/plan-mode.ts new file mode 100644 index 000000000..846d7b582 --- /dev/null +++ b/packages/agent-core/src/agent/injection/plan-mode.ts @@ -0,0 +1,181 @@ +import type { PlanFilePath } from '../plan'; +import { DynamicInjector } from './injector'; + +const PLAN_MODE_DEDUP_MIN_TURNS = 2; +const PLAN_MODE_FULL_REFRESH_TURNS = 5; + +/** + * Plan-mode reminder variants. + * + * `reentry` is used once when a restored planning session already has plan + * content. `full` is used for the first reminder and periodic refreshes. + * `sparse` keeps the read-only invariant visible between full reminders. + */ +export type PlanModeVariant = 'full' | 'sparse' | 'reentry'; + +export class PlanModeInjector extends DynamicInjector { + protected override readonly injectionVariant = 'plan_mode'; + private wasActive = false; + + override onContextClear(): void { + super.onContextClear(); + this.wasActive = this.agent.planMode.isActive; + } + + override async getInjection(): Promise { + const { isActive, planFilePath } = this.agent.planMode; + if (!isActive) { + if (!this.wasActive) { + return undefined; + } + this.wasActive = false; + this.injectedAt = null; + return exitReminder(); + } + if (!this.wasActive) { + this.injectedAt = null; + this.wasActive = true; + if (await this.hasCurrentPlanContent()) { + return reentryReminder(planFilePath); + } + } + const variant = this.getVariant(); + if (variant === null) return undefined; + + return variant === 'full' + ? fullReminder(planFilePath) + : variant === 'sparse' + ? sparseReminder(planFilePath) + : reentryReminder(planFilePath); + } + + protected getVariant(): PlanModeVariant | null { + if (this.injectedAt === null) return 'full'; + const history = this.agent.context.history; + let assistantTurnsSince = 0; + for (let i = this.injectedAt + 1; i < history.length; i++) { + const msg = history[i]; + if (msg === undefined) continue; + if (msg.role === 'assistant') { + assistantTurnsSince += 1; + continue; + } + if (msg.role === 'user') { + return 'full'; + } + } + if (assistantTurnsSince >= PLAN_MODE_FULL_REFRESH_TURNS) return 'full'; + if (assistantTurnsSince >= PLAN_MODE_DEDUP_MIN_TURNS) return 'sparse'; + return null; + } + + private async hasCurrentPlanContent(): Promise { + try { + const data = await this.agent.planMode.data(); + return data !== null && data.content.trim().length > 0; + } catch { + return false; + } + } +} +function withPlanFileFooter(body: string, planFilePath: PlanFilePath): string { + if (planFilePath === null || planFilePath.length === 0) return body; + return `${body}\n\nPlan file: ${planFilePath}`; +} + +function fullReminder(planFilePath: PlanFilePath): string { + if (planFilePath === null || planFilePath.length === 0) { + return inlineFullReminder(); + } + + const body = `Plan mode is active. You MUST NOT make any edits (with the exception of the current plan file) or otherwise make changes to the system unless a tool request is explicitly approved. Prefer read-only tools. Use Bash only when needed; Bash follows the normal permission mode and rules. This supersedes any other instructions you have received. + +Workflow: + 1. Understand — explore the codebase with Glob, Grep, Read. + 2. Design — converge on the best approach; consider trade-offs but aim for a single recommendation. + 3. Review — re-read key files to verify understanding. + 4. Write Plan — modify the plan file with Write or Edit. Use Write if the plan file does not exist yet. + 5. Exit — call ExitPlanMode for user approval. + +## Handling multiple approaches +Keep it focused: at most 2-3 meaningfully different approaches. Do NOT pad with minor variations — if one approach is clearly superior, just propose that one. +When the best approach depends on user preferences, constraints, or context you don't have, use AskUserQuestion to clarify first. This helps you write a better, more targeted plan rather than dumping multiple options for the user to sort through. +When you do include multiple approaches in the plan, you MUST pass them as the \`options\` parameter when calling ExitPlanMode, so the user can select which approach to execute at approval time. +NEVER write multiple approaches in the plan and call ExitPlanMode without the \`options\` parameter — the user will only see the default approval controls with no way to choose a specific approach. + +AskUserQuestion is for clarifying missing requirements or user preferences that affect the plan. +Never ask about plan approval via text or AskUserQuestion. +Your turn must end with either AskUserQuestion (to clarify requirements or preferences) or ExitPlanMode (to request plan approval). Do NOT end your turn any other way. +Do NOT use AskUserQuestion to ask about plan approval or reference "the plan" — the user cannot see the plan until you call ExitPlanMode.`; + return withPlanFileFooter(body, planFilePath); +} + +function sparseReminder(planFilePath: PlanFilePath): string { + if (planFilePath === null || planFilePath.length === 0) { + return inlineSparseReminder(); + } + + const body = `Plan mode still active (see full instructions earlier). Prefer read-only tools except the current plan file. Use Write or Edit to modify the plan file. If it does not exist yet, create it with Write first. Use Bash only when needed; Bash follows the normal permission mode and rules. Use AskUserQuestion to clarify user preferences when it helps you write a better plan. If the plan has multiple approaches, pass options to ExitPlanMode so the user can choose. End turns with AskUserQuestion (for clarifications) or ExitPlanMode (for approval). Never ask about plan approval via text or AskUserQuestion.`; + return withPlanFileFooter(body, planFilePath); +} + +function reentryReminder(planFilePath: PlanFilePath): string { + if (planFilePath === null || planFilePath.length === 0) { + return inlineReentryReminder(); + } + + const body = `Plan mode is active. You MUST NOT make any edits (with the exception of the current plan file) or otherwise make changes to the system unless a tool request is explicitly approved. Prefer read-only tools. Use Bash only when needed; Bash follows the normal permission mode and rules. This supersedes any other instructions you have received. + +## Re-entering Plan Mode +A plan file from a previous planning session already exists. +Before proceeding: + 1. Read the existing plan file to understand what was previously planned. + 2. Evaluate the user's current request against that plan. + 3. If different task: replace the old plan with a fresh one. If same task: update the existing plan. + 4. You may use Write or Edit to modify the plan file. If the file does not exist yet, create it with Write first. + 5. Use AskUserQuestion to clarify missing requirements or user preferences that affect the plan. + 6. Always edit the plan file before calling ExitPlanMode. + +Your turn must end with either AskUserQuestion (to clarify requirements) or ExitPlanMode (to request plan approval).`; + return withPlanFileFooter(body, planFilePath); +} + +function inlineFullReminder(): string { + return `Plan mode is active. You MUST NOT make any edits or otherwise make changes to the system unless a tool request is explicitly approved. Prefer read-only tools. Use Bash only when needed; Bash follows the normal permission mode and rules. This supersedes any other instructions you have received. + +Workflow: + 1. Understand — explore the codebase with Glob, Grep, Read. + 2. Design — converge on the best approach; consider trade-offs but aim for a single recommendation. + 3. Review — re-read key files to verify understanding. + 4. Wait for the host to provide a plan file path, write the plan there, then call ExitPlanMode. + +## Handling multiple approaches +Keep it focused: at most 2-3 meaningfully different approaches. Do NOT pad with minor variations — if one approach is clearly superior, just propose that one. +When the best approach depends on user preferences, constraints, or context you don't have, use AskUserQuestion to clarify first. +When you do include multiple approaches in the plan, you MUST pass them as the \`options\` parameter when calling ExitPlanMode, so the user can select which approach to execute at approval time. + +AskUserQuestion is for clarifying missing requirements or user preferences that affect the plan. +Never ask about plan approval via text or AskUserQuestion. +Your turn must end with either AskUserQuestion (to clarify requirements or preferences) or ExitPlanMode (to request plan approval). Do NOT end your turn any other way.`; +} + +function inlineSparseReminder(): string { + return `Plan mode still active (see full instructions earlier). Read-only; no plan file path is available in this host. Wait for the host to provide a plan file path before calling ExitPlanMode. Use AskUserQuestion to clarify user preferences when it helps you write a better plan. If the plan has multiple approaches, pass options to ExitPlanMode so the user can choose. End turns with AskUserQuestion (for clarifications) or ExitPlanMode (for approval).`; +} + +function inlineReentryReminder(): string { + return `Plan mode is active. You MUST NOT make any edits or otherwise make changes to the system unless a tool request is explicitly approved. Prefer read-only tools. Use Bash only when needed; Bash follows the normal permission mode and rules. This supersedes any other instructions you have received. + +## Re-entering Plan Mode +No plan file path is available in this host. +Before proceeding: + 1. Re-evaluate the user request and any existing conversation context. + 2. Use AskUserQuestion to clarify missing requirements or user preferences that affect the plan. + 3. Wait for the host to provide a plan file path, write the revised plan there, then call ExitPlanMode. + +Your turn must end with either AskUserQuestion (to clarify requirements) or ExitPlanMode (to request plan approval).`; +} + +function exitReminder(): string { + return `Plan mode is no longer active. The read-only and plan-file-only restrictions from plan mode no longer apply. Continue with the approved plan using the normal tool and permission rules.`; +} diff --git a/packages/agent-core/src/agent/permission/action-label.ts b/packages/agent-core/src/agent/permission/action-label.ts new file mode 100644 index 000000000..ad1565de4 --- /dev/null +++ b/packages/agent-core/src/agent/permission/action-label.ts @@ -0,0 +1,142 @@ +/** + * describeApprovalAction — coarse action label for approve_for_session. + * + * The label is the key used by `auto_approve_actions` set (session-level + * approve-for-session cache). It must be **coarse** enough that the user + * pressing "approve for session" on one request also unlocks *semantically + * equivalent* future requests — otherwise approve-for-session degrades + * into approve-once. + * + * Derivation priority: + * 1. `ApprovalDisplay.kind` mapping (the display already carries the + * semantic classification the UI renders): + * command → "run command" + * diff → "edit file" + * file_write → "write file" + * task_stop → "stop background task" + * generic → tool-name fallback + * 2. Hard-coded toolName → action map for tools that emit `generic` + * display. + * 3. Last resort: `call `. + */ + +import type { ToolInputDisplay } from '../../tools/display/schemas'; + +/** + * Hard-coded toolName → action label map. Consulted as a fallback so + * tools that carry `generic` displays still get a sensible label. Keys + * match the tool names currently wired in + * `packages/agent-core/src/tools/`. + */ +const TOOL_NAME_TO_ACTION: Readonly> = { + Bash: 'run command', + Shell: 'run command', + BackgroundRun: 'run background command', + BackgroundStop: 'stop background task', + Write: 'edit file', + Edit: 'edit file', + StrReplace: 'edit file', +}; + +/** Inverse table — action label → the representative tool-name pattern. */ +const ACTION_TO_PATTERN: Readonly> = { + 'run command': 'Bash', + 'run command in plan mode': null, + 'run background command': 'BackgroundRun', + 'stop background task': 'BackgroundStop', + 'edit file': 'Write', + 'edit file outside of working directory': 'Write', + 'write file': 'Write', +}; + +export function describeApprovalAction( + toolName: string, + _args: unknown, + display: ToolInputDisplay, + override?: string, +): string { + // Highest priority: explicit override from a hook. + if (override !== undefined && override.length > 0) { + return override; + } + + // Display-driven derivation: the display kind already captures the + // coarse semantic class the UI renders. + switch (display.kind) { + case 'command': + return 'run command'; + case 'diff': + return 'edit file'; + case 'file_io': + switch (display.operation) { + case 'write': + return 'write file'; + case 'edit': + return 'edit file'; + case 'read': + return 'read file'; + case 'glob': + return 'list files'; + case 'grep': + return 'search files'; + } + break; + case 'task_stop': + return 'stop background task'; + case 'plan_review': + return 'review plan'; + case 'agent_call': + return 'spawn agent'; + case 'skill_call': + return 'invoke skill'; + case 'url_fetch': + return 'fetch URL'; + case 'search': + return 'search'; + case 'todo_list': + return 'update todo list'; + case 'background_task': + return 'run background task'; + case 'generic': + // fall through to tool-name map + break; + } + + const mapped = TOOL_NAME_TO_ACTION[toolName]; + if (mapped !== undefined) return mapped; + + // MCP tool naming: `mcp____` gets a coarse + // `"call MCP tool: :"` label so one approve-for-session + // click unlocks every future call to the same server+tool combo. The + // server name is preserved to prevent cross-server privilege escalation. + if (toolName.startsWith('mcp__')) { + const rest = toolName.slice('mcp__'.length); + const sep = rest.indexOf('__'); + if (sep >= 0) { + const serverName = rest.slice(0, sep); + const innerTool = rest.slice(sep + 2); + if (innerTool.length > 0) { + return `call MCP tool: ${serverName}:${innerTool}`; + } + } + } + + return `call ${toolName}`; +} + +/** + * Inverse mapping from an approve_for_session action label to the + * permission-rule pattern that should gate future same-action calls. + * + * When no entry matches, fall back to the concrete tool name. A `null` + * table entry means the action should be cached by action label only, + * without creating a broad PermissionRule. + */ +export function actionToRulePattern( + action: string, + fallbackToolName: string, +): string | undefined { + const mapped = ACTION_TO_PATTERN[action]; + if (mapped !== undefined) return mapped ?? undefined; + return fallbackToolName; +} diff --git a/packages/agent-core/src/agent/permission/check-rules.ts b/packages/agent-core/src/agent/permission/check-rules.ts new file mode 100644 index 000000000..484cc11eb --- /dev/null +++ b/packages/agent-core/src/agent/permission/check-rules.ts @@ -0,0 +1,59 @@ +import { isDefaultAutoAllowTool } from '../../tools/policies/default-permissions'; +import { matchesRule } from './matches-rule'; +import type { PermissionPathMatchOptions } from './path-glob-match'; +import type { PermissionMode, PermissionRule, PermissionRuleDecision } from './types'; + +export interface CheckRulesResult { + readonly decision: PermissionRuleDecision; + /** Rule that produced `decision`. `undefined` for mode/default auto-allow. */ + readonly matchedRule?: PermissionRule | undefined; +} + +export function checkMatchingRules( + rules: readonly PermissionRule[], + toolName: string, + toolInput: unknown, + mode: PermissionMode, + pathOptions?: PermissionPathMatchOptions, +): CheckRulesResult | undefined { + // Priority 1: deny wins in every mode. + for (const rule of rules) { + if (rule.decision === 'deny' && matchesRule(rule, toolName, toolInput, pathOptions)) { + return { decision: 'deny', matchedRule: rule }; + } + } + + const askRule = firstMatchingRule(rules, 'ask', toolName, toolInput, pathOptions); + const allowRule = firstMatchingRule(rules, 'allow', toolName, toolInput, pathOptions); + if (askRule === undefined && allowRule === undefined) return undefined; + + if (isDefaultAutoAllowTool(toolName)) { + return { decision: 'allow' }; + } + + // Mode overlay: yolo treats everything non-deny as allow. + if (mode === 'yolo' || mode === 'auto') { + return { decision: 'allow' }; + } + + // Priority 2: ask before allow so unresolved ambiguity defers to the user. + if (askRule !== undefined) return { decision: 'ask', matchedRule: askRule }; + + // Priority 3: explicit allow. + return { decision: 'allow', matchedRule: allowRule }; +} + +function firstMatchingRule( + rules: readonly PermissionRule[], + decision: PermissionRuleDecision, + toolName: string, + toolInput: unknown, + pathOptions?: PermissionPathMatchOptions, +): PermissionRule | undefined { + for (const rule of rules) { + if (rule.decision === decision && matchesRule(rule, toolName, toolInput, pathOptions)) { + return rule; + } + } + return undefined; +} diff --git a/packages/agent-core/src/agent/permission/index.ts b/packages/agent-core/src/agent/permission/index.ts new file mode 100644 index 000000000..7628d8405 --- /dev/null +++ b/packages/agent-core/src/agent/permission/index.ts @@ -0,0 +1,340 @@ +import type { Agent } from '..'; +import type { PrepareToolExecutionResult, ToolExecutionHookContext } from '../../loop'; +import type { TelemetryPropertyValue } from '../../telemetry'; +import { isDefaultAutoAllowTool } from '../../tools/policies/default-permissions'; +import type { ToolInputDisplay } from '../../tools/display'; +import { actionToRulePattern, describeApprovalAction } from './action-label'; +import { checkMatchingRules, type CheckRulesResult } from './check-rules'; +import type { PermissionPathMatchOptions } from './path-glob-match'; +import { createBuiltinPermissionPolicies } from './policies'; +import type { PermissionPolicy, PermissionPolicyResult } from './policy'; +import type { + PermissionApprovalResultRecord, + PermissionData, + PermissionMode, + PermissionRule, +} from './types'; +export * from './policy'; +export * from './types'; + +type ApprovalTelemetryMode = 'manual' | 'yolo' | 'afk' | 'auto_session' | 'cancelled'; + +export interface PermissionManagerOptions { + readonly initialRules?: readonly PermissionRule[] | undefined; + readonly policies?: readonly PermissionPolicy[] | undefined; + readonly parent?: PermissionManager | undefined; +} + +export class PermissionManager { + rules: PermissionRule[] = []; + private modeOverride: PermissionMode | undefined; + private readonly parent: PermissionManager | undefined; + private readonly sessionApprovedActions = new Set(); + private readonly policies: readonly PermissionPolicy[]; + + constructor( + protected readonly agent: Agent, + options: PermissionManagerOptions = {}, + ) { + this.rules = [...(options.initialRules ?? [])]; + this.parent = options.parent; + this.policies = options.policies ?? createBuiltinPermissionPolicies(); + } + + get mode(): PermissionMode { + return this.modeOverride ?? this.parent?.mode ?? 'manual'; + } + + set mode(mode: PermissionMode) { + this.modeOverride = mode; + } + + data(): PermissionData { + return { + mode: this.mode, + rules: this.effectiveRules(), + }; + } + + setMode(mode: PermissionMode): void { + this.agent.records.logRecord({ + type: 'permission.set_mode', + mode, + }); + this.agent.replayBuilder.push({ + type: 'permission_updated', + mode, + }); + this.modeOverride = mode; + this.agent.emitStatusUpdated(); + } + + recordApprovalResult(record: PermissionApprovalResultRecord): void { + this.agent.records.logRecord({ + type: 'permission.record_approval_result', + ...record, + }); + this.agent.replayBuilder.push({ + type: 'approval_result', + record, + }); + if (record.result.decision !== 'approved' || record.result.scope !== 'session') { + return; + } + if (this.sessionApprovedActions.has(record.action)) return; + + const pattern = actionToRulePattern(record.action, record.toolName); + this.sessionApprovedActions.add(record.action); + if (pattern === undefined) return; + + const rule: PermissionRule = { + decision: 'allow', + scope: 'session-runtime', + pattern, + reason: `approve_for_session: ${record.action}`, + }; + if (!this.hasRule(rule)) { + this.rules.push(rule); + } + } + + async beforeToolCall( + context: ToolExecutionHookContext, + ): Promise { + const name = context.toolCall.function.name; + const args = context.args; + + const mode = this.mode; + const { decision, matchedRule } = this.checkPermission(name, args, mode); + if (decision === 'deny') { + return { + block: true, + reason: this.formatMessage(name, matchedRule?.reason), + }; + } + + const policyResult = await this.evaluatePolicies(context, matchedRule); + if (policyResult !== undefined) { + return this.permissionPolicyResultToPrepare(policyResult, context); + } + + if (mode === 'auto') { + if (this.wouldAskInManualMode(name, args)) { + this.trackToolApproved(name, 'afk'); + } + return undefined; + } + if (mode === 'yolo') { + if (this.wouldAskInManualMode(name, args)) { + this.trackToolApproved(name, 'yolo'); + } + return undefined; + } + + if (decision === 'allow') { + if (matchedRule?.scope === 'session-runtime') { + this.trackToolApproved(name, 'auto_session', 'session'); + } + return undefined; + } + + // decision === 'ask' → bounce through ApprovalRuntime. + return this.requestToolApproval(context); + } + + private async requestToolApproval( + context: ToolExecutionHookContext, + options: { + readonly action?: string | undefined; + readonly display?: ToolInputDisplay | undefined; + } = {}, + ): Promise { + const { signal } = context; + const id = context.toolCall.id; + const name = context.toolCall.function.name; + const args = context.args; + const display = + options.display ?? ({ + kind: 'generic', + summary: `Approve ${name}`, + detail: args, + } satisfies ToolInputDisplay); + const action = options.action ?? describeApprovalAction(name, args, display); + if (this.sessionApprovedActions.has(action)) { + this.trackToolApproved(name, 'auto_session', 'session'); + return undefined; + } + + const result = await this.agent.rpc.requestApproval( + { + turnId: Number(context.turnId), + toolCallId: id, + toolName: name, + action, + display, + }, + { signal }, + ); + this.recordApprovalResult({ + turnId: Number(context.turnId), + toolCallId: id, + toolName: name, + action, + result, + }); + + if (result.decision === 'approved') { + this.trackToolApproved( + name, + approvalTelemetryMode(this.mode), + result.scope === 'session' ? 'session' : 'once', + ); + return undefined; + } + + this.agent.telemetry.track('tool_rejected', { + tool_name: name, + approval_mode: + result.decision === 'cancelled' ? 'cancelled' : approvalTelemetryMode(this.mode), + decision: result.decision, + has_feedback: result.feedback !== undefined && result.feedback.length > 0, + }); + + return { + block: true, + reason: this.formatApprovalRejectionMessage(name, result), + }; + } + + private async evaluatePolicies( + context: ToolExecutionHookContext, + matchedRule: PermissionRule | undefined, + ): Promise { + for (const policy of this.policies) { + const result = await policy.evaluate({ + agent: this.agent, + mode: this.mode, + toolCallContext: context, + matchedRule, + recordApprovalResult: (record) => { + this.recordApprovalResult(record); + }, + }); + if (result !== undefined) return result; + } + return undefined; + } + + private checkPermission( + toolName: string, + toolInput: unknown, + mode: PermissionMode = this.mode, + ): CheckRulesResult { + const matched = this.checkMatchingPermissionRules(toolName, toolInput, mode); + if (matched !== undefined) return matched; + if (isDefaultAutoAllowTool(toolName)) return { decision: 'allow' }; + if (mode === 'yolo' || mode === 'auto') return { decision: 'allow' }; + return { decision: 'ask' }; + } + + private checkMatchingPermissionRules( + toolName: string, + toolInput: unknown, + mode: PermissionMode, + ): CheckRulesResult | undefined { + return ( + checkMatchingRules(this.rules, toolName, toolInput, mode, this.pathMatchOptions()) ?? + this.parent?.checkMatchingPermissionRules(toolName, toolInput, mode) + ); + } + + private effectiveRules(): PermissionRule[] { + return [...this.rules, ...(this.parent?.effectiveRules() ?? [])]; + } + + private wouldAskInManualMode(toolName: string, toolInput: unknown): boolean { + return this.checkPermission(toolName, toolInput, 'manual').decision === 'ask'; + } + + private permissionPolicyResultToPrepare( + result: PermissionPolicyResult, + context: ToolExecutionHookContext, + ): Promise | PrepareToolExecutionResult | undefined { + switch (result.kind) { + case 'allow': + return result.executionMetadata === undefined + ? undefined + : { executionMetadata: result.executionMetadata }; + case 'ask': + return this.requestToolApproval(context, result); + case 'result': + return result.result; + } + } + + private hasRule(target: PermissionRule): boolean { + return this.rules.some((rule) => { + return ( + rule.decision === target.decision && + rule.scope === target.scope && + rule.pattern === target.pattern && + rule.reason === target.reason + ); + }); + } + + protected formatMessage(toolName: string, reason?: string): string { + const suffix = reason !== undefined && reason.length > 0 ? ` Reason: ${reason}` : ''; + if (this.agent.type === 'sub') { + return `Tool "${toolName}" was denied.${suffix} Try a different approach — don't retry the same call, don't attempt to bypass the restriction.`; + } + return `Tool "${toolName}" was denied by permission rule.${suffix}`; + } + + protected formatApprovalRejectionMessage( + toolName: string, + result: { decision: 'approved' | 'rejected' | 'cancelled'; feedback?: string }, + ): string { + const suffix = + result.feedback !== undefined && result.feedback.length > 0 + ? ` Reason: ${result.feedback}` + : ''; + const prefix = + result.decision === 'cancelled' + ? `Tool "${toolName}" was not run because the approval request was cancelled.` + : `Tool "${toolName}" was not run because the user rejected the approval request.`; + if (this.agent.type === 'sub') { + return `${prefix}${suffix} Try a different approach — don't retry the same call, don't attempt to bypass the restriction.`; + } + return `${prefix}${suffix}`; + } + + private pathMatchOptions(): PermissionPathMatchOptions { + return { + cwd: this.agent.config.cwd, + pathClass: this.agent.runtime.kaos.pathClass(), + homeDir: this.agent.runtime.kaos.gethome(), + }; + } + + private trackToolApproved( + toolName: string, + approvalMode: Exclude, + scope?: 'once' | 'session', + ): void { + const properties: Record = { + tool_name: toolName, + approval_mode: approvalMode, + }; + if (scope !== undefined) { + properties['scope'] = scope; + } + this.agent.telemetry.track('tool_approved', properties); + } +} + +function approvalTelemetryMode( + mode: PermissionMode, +): Extract { + return mode === 'auto' ? 'afk' : mode; +} diff --git a/packages/agent-core/src/agent/permission/matches-rule.ts b/packages/agent-core/src/agent/permission/matches-rule.ts new file mode 100644 index 000000000..546567b7c --- /dev/null +++ b/packages/agent-core/src/agent/permission/matches-rule.ts @@ -0,0 +1,125 @@ +/** + * matchesRule — pure function that decides whether a PermissionRule + * applies to a given tool call. + * + * Contract: + * - No side effects, no `this`, no IO, no exceptions. + * - Deterministic: same `(rule, toolName, args)` → same result. + * - Returns boolean only; decision semantics (deny/ask/allow) are a + * caller concern (see `check-rules.ts`). + */ + +import picomatch from 'picomatch'; + +import { parsePattern } from './parse-pattern'; +import { globMatch, pathGlobMatch, type PermissionPathMatchOptions } from './path-glob-match'; +import type { PermissionRule } from './types'; + +type ArgFieldKind = 'generic' | 'path'; + +interface ArgField { + readonly value: string; + readonly kind: ArgFieldKind; +} + +/** + * Tool-specific argument field convention. When a rule uses an arg + * pattern (`Read(./src/**)`), we extract the listed field from the tool + * call args and match the glob against its value. + * + * Unknown tools fall back to `undefined`, which means "arg pattern + * cannot match" — rules with an arg pattern on an unknown tool will + * never fire. Rules without an arg pattern (`UnknownTool`) still match + * on name alone. + */ +function extractArgField(toolName: string, args: unknown): ArgField | undefined { + if (args === null || typeof args !== 'object') return undefined; + const rec = args as Record; + + switch (toolName) { + case 'Bash': + case 'Shell': + case 'Background': + return typeof rec['command'] === 'string' + ? { value: rec['command'], kind: 'generic' } + : undefined; + case 'Read': + case 'Write': + case 'Edit': + case 'ReadMediaFile': + return typeof rec['path'] === 'string' ? { value: rec['path'], kind: 'path' } : undefined; + case 'Grep': + case 'Glob': + return typeof rec['pattern'] === 'string' + ? { value: rec['pattern'], kind: 'generic' } + : undefined; + case 'Task': + case 'Agent': + return typeof rec['subagent_type'] === 'string' + ? { value: rec['subagent_type'], kind: 'generic' } + : undefined; + default: + return undefined; + } +} + +/** + * Decide whether a single rule matches a specific tool call. + * + * Algorithm: + * 1. Parse `rule.pattern` into `{toolName, argPattern?}`. + * 2. If parsed toolName is `*`, skip name check; otherwise compare with + * glob semantics so `mcp__github__*` matches `mcp__github__list`. + * 3. If the rule has no argPattern, name match → rule fires. + * 4. Otherwise extract the tool-specific field value and match against + * the glob. Handle the leading `!` negation prefix by flipping the + * final boolean. + */ +export function matchesRule( + rule: PermissionRule, + toolName: string, + args: unknown, + pathOptions?: PermissionPathMatchOptions, +): boolean { + let parsed; + try { + parsed = parsePattern(rule.pattern); + } catch { + // Malformed patterns never match. The loader is responsible for + // surfacing DSL errors at load time; matcher stays total. + return false; + } + + // 1. Tool-name match (support `*` wildcard + glob-style tool names) + const nameGlob = parsed.toolName; + if (nameGlob !== '*' && !picomatch.isMatch(toolName, nameGlob)) return false; + + // 2. No arg pattern → name match is enough + if (parsed.argPattern === undefined) return true; + + // 3. Arg pattern — resolve negation and glob-match the field + const rawPattern = parsed.argPattern; + const negated = rawPattern.startsWith('!'); + const positivePattern = negated ? rawPattern.slice(1) : rawPattern; + + const fieldValue = extractArgField(toolName, args); + if (fieldValue === undefined) { + // No extractable field → positive pattern cannot match; negation + // semantics here mean "the field is not in the disallowed set", + // which by missing-field convention we treat as a non-match to + // avoid accidentally firing deny rules on malformed args. + return false; + } + + // If the field is a path, use `pathGlobMatch` which handles normalization. + const hit = + fieldValue.kind === 'path' + ? pathGlobMatch(fieldValue.value, positivePattern, { + pathOptions, + conservativeCaseFold: rule.decision === 'deny' && !negated, + }) + : globMatch(fieldValue.value, positivePattern); + return negated ? !hit : hit; +} + +export type { PermissionPathMatchOptions } from './path-glob-match'; diff --git a/packages/agent-core/src/agent/permission/parse-pattern.ts b/packages/agent-core/src/agent/permission/parse-pattern.ts new file mode 100644 index 000000000..9b76ebd28 --- /dev/null +++ b/packages/agent-core/src/agent/permission/parse-pattern.ts @@ -0,0 +1,50 @@ +/** + * DSL parser for PermissionRule `pattern` strings. + * + * Grammar: + * pattern := toolName ( "(" argPattern ")" )? + * toolName := identifier characters (e.g. `Bash`, `mcp__github__*`) + * argPattern := any string (may start with `!` for negation) + * + * Examples: + * "Write" → { toolName: "Write" } + * "Read(/etc/**)" → { toolName: "Read", argPattern: "/etc/**" } + * "Bash(!rm *)" → { toolName: "Bash", argPattern: "!rm *" } + * "mcp__github__*" → { toolName: "mcp__github__*" } + */ + +export interface ParsedPattern { + readonly toolName: string; + readonly argPattern?: string | undefined; +} + +/** + * Parse a DSL pattern. Throws on malformed input (missing closing paren, + * empty tool name). The parser is the single source of truth for DSL + * syntax and is exercised by table-driven tests. + */ +export function parsePattern(pattern: string): ParsedPattern { + const trimmed = pattern.trim(); + if (trimmed.length === 0) { + throw new Error('permission pattern: empty string'); + } + + const openIdx = trimmed.indexOf('('); + if (openIdx === -1) { + return { toolName: trimmed }; + } + + if (!trimmed.endsWith(')')) { + throw new Error(`permission pattern: missing closing paren in "${pattern}"`); + } + + const toolName = trimmed.slice(0, openIdx); + const argPattern = trimmed.slice(openIdx + 1, -1); + if (toolName.length === 0) { + throw new Error(`permission pattern: empty tool name in "${pattern}"`); + } + // Empty arg pattern (`Read()`) is treated as "toolName only" — it + // matches every call to that tool. This aligns with the intuition + // that writing `Read()` is an odd but non-fatal way of saying `Read`. + return { toolName, argPattern: argPattern.length > 0 ? argPattern : undefined }; +} diff --git a/packages/agent-core/src/agent/permission/path-glob-match.ts b/packages/agent-core/src/agent/permission/path-glob-match.ts new file mode 100644 index 000000000..5cecc179e --- /dev/null +++ b/packages/agent-core/src/agent/permission/path-glob-match.ts @@ -0,0 +1,156 @@ +import * as posixPath from 'node:path/posix'; +import * as win32Path from 'node:path/win32'; + +import picomatch from 'picomatch'; + +import { canonicalizePath, type PathClass } from '../../tools/policies/path-access'; + +export interface PermissionPathMatchOptions { + readonly cwd?: string; + readonly pathClass?: PathClass; + readonly homeDir?: string; + readonly caseInsensitivePaths?: boolean; +} + +interface PathMatchSemantics { + readonly pathClass: PathClass; + readonly path: typeof posixPath; +} + +/** + * Match ordinary string fields, like command text or search patterns. + * `*` and `**` work as wildcards, but the value is not treated as a file path. + */ +export function globMatch(value: string, pattern: string, options?: { nocase?: boolean }): boolean { + if (picomatch.isMatch(value, pattern, options)) return true; + + const normalizedValue = stripLeadingDotSlash(value); + const normalizedPattern = stripLeadingDotSlash(pattern); + if (normalizedValue === value && normalizedPattern === pattern) return false; + return picomatch.isMatch(normalizedValue, normalizedPattern, options); +} + +function stripLeadingDotSlash(value: string): string { + return value.startsWith('./') ? value.slice(2) : value; +} + +/** + * Match file path fields, like Read/Write/Edit `path`. + * Also compares normalized forms, so `./a`, `dir/../a`, and Windows + * separator or case variants can match the same rule. + */ +export function pathGlobMatch( + value: string, + pattern: string, + options: { + readonly pathOptions?: PermissionPathMatchOptions; + readonly conservativeCaseFold: boolean; + }, +): boolean { + const semantics = pathMatchSemantics(value, pattern, options.pathOptions); + const nocase = + options.pathOptions?.caseInsensitivePaths ?? + (semantics.pathClass === 'win32' || options.conservativeCaseFold); + + if (globMatch(value, pattern, { nocase })) return true; + + for (const valueVariant of pathVariants(value, semantics, options.pathOptions)) { + for (const patternVariant of pathVariants(pattern, semantics, options.pathOptions)) { + if (globMatch(valueVariant, patternVariant, { nocase })) return true; + } + } + return false; +} + +/** + * Build equivalent spellings for one path string before glob matching: + * the original text, a leading `./` or `.\` form without that prefix, + * the canonical absolute path when possible, and slash-form Windows paths. + * + * Example: with cwd `/repo`, `./src/../secret.txt` adds both + * `src/../secret.txt` and `/repo/secret.txt`. On Windows, + * `C:\repo\secret.txt` also adds `C:/repo/secret.txt`. + */ +function pathVariants( + value: string, + semantics: PathMatchSemantics, + pathOptions: PermissionPathMatchOptions | undefined, +): string[] { + const variants = new Set(); + addPathVariant(variants, value, semantics.pathClass); + addPathVariant(variants, stripLeadingDotPath(value, semantics.pathClass), semantics.pathClass); + + const canonical = canonicalizePathPattern(value, semantics, pathOptions); + if (canonical !== undefined) addPathVariant(variants, canonical, semantics.pathClass); + return Array.from(variants); +} + +function canonicalizePathPattern( + value: string, + semantics: PathMatchSemantics, + pathOptions: PermissionPathMatchOptions | undefined, +): string | undefined { + const expanded = expandUserPath(value, semantics, pathOptions?.homeDir); + const cwd = pathOptions?.cwd ?? defaultCwdForPath(expanded, semantics); + if (cwd === undefined) return undefined; + try { + return canonicalizePath(expanded, cwd, semantics.pathClass); + } catch { + return undefined; + } +} + +function expandUserPath( + value: string, + semantics: PathMatchSemantics, + homeDir: string | undefined, +): string { + if (homeDir === undefined) return value; + if (value === '~') return homeDir; + if (value.startsWith('~/') || (semantics.pathClass === 'win32' && value.startsWith('~\\'))) { + return semantics.path.join(homeDir, value.slice(2)); + } + return value; +} + +function defaultCwdForPath(value: string, semantics: PathMatchSemantics): string | undefined { + if (!semantics.path.isAbsolute(value)) return undefined; + return semantics.path.parse(value).root; +} + +function pathMatchSemantics( + value: string, + pattern: string, + pathOptions: PermissionPathMatchOptions | undefined, +): PathMatchSemantics { + // Production callers pass the active Kaos path class. The fallback keeps + // the pure matcher useful for tests and direct helper calls. + const pathClass = + pathOptions?.pathClass ?? + ([value, pattern].some((candidate) => { + return ( + /^[A-Za-z]:(?:[\\/]|$)/.test(candidate) || + candidate.startsWith('\\\\') || + candidate.includes('\\') + ); + }) + ? 'win32' + : 'posix'); + return { + pathClass, + path: pathClass === 'win32' ? win32Path : posixPath, + }; +} + +function addPathVariant(variants: Set, value: string, pathClass: PathClass): void { + variants.add(value); + // Picomatch treats backslashes as escape syntax in some cases; add a + // slash-separated Win32 variant so nocase and globs behave predictably. + if (pathClass === 'win32') variants.add(value.replaceAll('\\', '/')); +} + +function stripLeadingDotPath(value: string, pathClass: PathClass): string { + if (value.startsWith('./')) return value.slice(2); + if (pathClass === 'win32' && value.startsWith('.\\')) return value.slice(2); + return value; +} diff --git a/packages/agent-core/src/agent/permission/policies/ask-user-question.ts b/packages/agent-core/src/agent/permission/policies/ask-user-question.ts new file mode 100644 index 000000000..a2edcc461 --- /dev/null +++ b/packages/agent-core/src/agent/permission/policies/ask-user-question.ts @@ -0,0 +1,17 @@ +import type { PermissionPolicy } from '../policy'; + +export const AskUserQuestionAutoPermissionPolicy: PermissionPolicy = { + name: 'auto.ask-user-question', + evaluate({ mode, toolCallContext }) { + if (mode !== 'auto') return undefined; + if (toolCallContext.toolCall.function.name !== 'AskUserQuestion') return undefined; + return { + kind: 'result', + result: { + block: true, + reason: + 'AskUserQuestion is disabled while auto permission mode is active. Make a reasonable decision and continue without asking the user.', + }, + }; + }, +}; diff --git a/packages/agent-core/src/agent/permission/policies/default-git-cwd-write.ts b/packages/agent-core/src/agent/permission/policies/default-git-cwd-write.ts new file mode 100644 index 000000000..656f2089b --- /dev/null +++ b/packages/agent-core/src/agent/permission/policies/default-git-cwd-write.ts @@ -0,0 +1,127 @@ +import * as posixPath from 'node:path/posix'; + +import type { Kaos } from '@moonshot-ai/kaos'; + +import { + DEFAULT_WORKSPACE_ACCESS_POLICY, + isWithinDirectory, + resolvePathAccess, +} from '../../../tools/policies/path-access'; +import { isSensitiveFile } from '../../../tools/policies/sensitive'; +import { + findGitWorkTreeMarker, + type GitWorkTreeMarker, +} from '../../../tools/support/git-worktree'; +import type { PermissionPolicy } from '../policy'; + +const AUTO_REASON = 'default_git_cwd_write'; +const S_IFMT = 0o170000; +const S_IFLNK = 0o120000; + +export function createDefaultGitCwdWritePolicy(): PermissionPolicy { + // Cache positive marker lookups only. A session that starts in a non-git + // directory and later `git init`s should pick up the new work tree on the + // next call; negative results pay one extra stat per call, which is + // acceptable. + const cache = new Map(); + + return { + name: 'default.git-cwd-write', + async evaluate({ agent, mode, matchedRule, toolCallContext }) { + if (mode !== 'manual') return undefined; + if (matchedRule !== undefined) return undefined; + + const toolName = toolCallContext.toolCall.function.name; + if (toolName !== 'Write' && toolName !== 'Edit') return undefined; + + const kaos = agent.runtime.kaos; + const pathClass = kaos.pathClass(); + if (pathClass !== 'posix') return undefined; + + const cwd = agent.config.cwd; + if (cwd.length === 0) return undefined; + + const path = readStringField(toolCallContext.args, 'path'); + if (path === undefined) return undefined; + + let access; + try { + access = resolvePathAccess( + path, + cwd, + { workspaceDir: cwd, additionalDirs: [] }, + { + operation: 'write', + pathClass, + homeDir: kaos.gethome(), + policy: DEFAULT_WORKSPACE_ACCESS_POLICY, + }, + ); + } catch { + return undefined; + } + if (access.outsideWorkspace) return undefined; + + const marker = cache.get(cwd) ?? (await findGitWorkTreeMarker(kaos, cwd)); + if (marker === null) return undefined; + cache.set(cwd, marker); + + if (isGitControlPath(access.path, cwd, marker)) return undefined; + if (isSensitiveFile(access.path.toLowerCase(), 'posix')) return undefined; + if (await hasSymlinkInPath(kaos, cwd, access.path)) return undefined; + + agent.telemetry.track('tool_approved', { + tool_name: toolName, + approval_mode: 'manual', + auto_reason: AUTO_REASON, + }); + return { kind: 'allow' }; + }, + }; +} + +function readStringField(args: unknown, key: string): string | undefined { + if (args === null || typeof args !== 'object') return undefined; + const value = (args as Record)[key]; + return typeof value === 'string' ? value : undefined; +} + +function isGitControlPath(targetPath: string, cwd: string, marker: GitWorkTreeMarker): boolean { + const foldedTarget = targetPath.toLowerCase(); + return ( + posixPath.relative(cwd.toLowerCase(), foldedTarget).split(posixPath.sep).includes('.git') || + isWithinDirectory(foldedTarget, marker.dotGitPath.toLowerCase(), 'posix') || + isWithinDirectory(foldedTarget, marker.controlDirPath.toLowerCase(), 'posix') + ); +} + +async function hasSymlinkInPath(kaos: Kaos, cwd: string, targetPath: string): Promise { + const relative = posixPath.relative(cwd, targetPath); + const parts = [cwd]; + + let current = cwd; + for (const part of relative.split(posixPath.sep)) { + if (part.length === 0 || part === '.') continue; + current = posixPath.join(current, part); + parts.push(current); + } + + for (let index = 0; index < parts.length; index += 1) { + try { + const stat = await kaos.stat(parts[index]!, { followSymlinks: false }); + if ((stat.stMode & S_IFMT) === S_IFLNK) return true; + } catch (error) { + return !(index === parts.length - 1 && isFileNotFoundError(error)); + } + } + return false; +} + +function isFileNotFoundError(error: unknown): boolean { + if (error === null || typeof error !== 'object') return false; + if ((error as { name?: unknown }).name === 'KaosFileNotFoundError') return true; + const code = (error as { code?: unknown }).code; + if (code === 'ENOENT' || code === 'ENOTDIR' || code === 2) return true; + const message = error instanceof Error ? error.message : ''; + return message.includes('ENOENT') || message.includes('ENOTDIR'); +} diff --git a/packages/agent-core/src/agent/permission/policies/index.ts b/packages/agent-core/src/agent/permission/policies/index.ts new file mode 100644 index 000000000..07459feb2 --- /dev/null +++ b/packages/agent-core/src/agent/permission/policies/index.ts @@ -0,0 +1,19 @@ +import type { PermissionPolicy } from '../policy'; +import { AskUserQuestionAutoPermissionPolicy } from './ask-user-question'; +import { createDefaultGitCwdWritePolicy } from './default-git-cwd-write'; +import { createPlanPermissionPolicies } from './plan'; +import { YoloOutsideWorkspacePermissionPolicy } from './yolo-workspace-access'; + +export function createBuiltinPermissionPolicies(): readonly PermissionPolicy[] { + return [ + ...createPlanPermissionPolicies(), + YoloOutsideWorkspacePermissionPolicy, + createDefaultGitCwdWritePolicy(), + AskUserQuestionAutoPermissionPolicy, + ]; +} + +export { AskUserQuestionAutoPermissionPolicy } from './ask-user-question'; +export { createDefaultGitCwdWritePolicy } from './default-git-cwd-write'; +export { createPlanPermissionPolicies } from './plan'; +export { YoloOutsideWorkspacePermissionPolicy } from './yolo-workspace-access'; diff --git a/packages/agent-core/src/agent/permission/policies/plan.ts b/packages/agent-core/src/agent/permission/policies/plan.ts new file mode 100644 index 000000000..774799227 --- /dev/null +++ b/packages/agent-core/src/agent/permission/policies/plan.ts @@ -0,0 +1,324 @@ +import type { ExecutableToolResult } from '../../../loop'; +import type { PermissionPolicy, PermissionPolicyContext, PermissionPolicyResult } from '../policy'; +import type { ApprovalResponse } from '../types'; + +interface ExitPlanModeOption { + readonly label: string; + readonly description: string; +} + +interface ExitPlanModeExecutionMetadata { + readonly selectedOption?: ExitPlanModeOption | undefined; + readonly planTelemetrySubmitted: true; + readonly planTelemetryResolved: true; +} + +export const EnterPlanModePermissionPolicy: PermissionPolicy = { + name: 'plan.enter-plan-mode', + evaluate({ toolCallContext }) { + if (toolCallContext.toolCall.function.name !== 'EnterPlanMode') return undefined; + return { kind: 'allow' }; + }, +}; + +export const ExitPlanModePermissionPolicy: PermissionPolicy = { + name: 'plan.exit-plan-mode', + async evaluate(context) { + if (context.toolCallContext.toolCall.function.name !== 'ExitPlanMode') return undefined; + if (context.mode === 'auto') return { kind: 'allow' }; + + const review = await resolveExitPlanModeReview(context); + if (review === null) return { kind: 'allow' }; + + const action = exitPlanModeAction(review.options); + context.agent.telemetry.track('plan_submitted', { + has_options: review.options !== undefined, + }); + let result: ApprovalResponse; + try { + result = await context.agent.rpc.requestApproval( + { + turnId: Number(context.toolCallContext.turnId), + toolCallId: context.toolCallContext.toolCall.id, + toolName: 'ExitPlanMode', + action, + display: { + kind: 'plan_review', + plan: review.plan, + path: review.path, + options: review.options, + }, + }, + { signal: context.toolCallContext.signal }, + ); + } catch (error) { + const message = error instanceof Error ? error.message : 'Plan approval failed.'; + return { + kind: 'result', + result: { + syntheticResult: { + isError: true, + output: `Plan approval failed: ${message}`, + }, + }, + }; + } + + context.recordApprovalResult({ + turnId: Number(context.toolCallContext.turnId), + toolCallId: context.toolCallContext.toolCall.id, + toolName: 'ExitPlanMode', + action, + result, + }); + + trackExitPlanModeResolution(context, result); + return exitPlanModeApprovalResult(context, result, review.options); + }, +}; + +export const PlanModeGuardPermissionPolicy: PermissionPolicy = { + name: 'plan.mode-guard', + evaluate({ agent, toolCallContext }) { + if (!agent.planMode.isActive) return undefined; + + const name = toolCallContext.toolCall.function.name; + const args = toolCallContext.args; + + if (name === 'Write' || name === 'Edit') { + const path = readStringField(args, 'path'); + if (path === undefined) return undefined; + const planFilePath = agent.planMode.planFilePath; + if (planFilePath !== null && path === planFilePath) return { kind: 'allow' }; + return { + kind: 'result', + result: { + block: true, + reason: + `Plan mode is active. You may only write to the current plan file: ${planFilePath ?? '(no plan file selected yet)'}. ` + + 'Call ExitPlanMode to exit plan mode before editing other files.', + }, + }; + } + + if (name === 'TaskStop') { + return { + kind: 'result', + result: { + block: true, + reason: + 'TaskStop is not available in plan mode. ' + + 'Call ExitPlanMode to exit plan mode before stopping a background task.', + }, + }; + } + + return undefined; + }, +}; + +export function createPlanPermissionPolicies(): readonly PermissionPolicy[] { + return [ + EnterPlanModePermissionPolicy, + ExitPlanModePermissionPolicy, + PlanModeGuardPermissionPolicy, + ]; +} + +async function resolveExitPlanModeReview(context: PermissionPolicyContext): Promise<{ + readonly plan: string; + readonly path?: string | undefined; + readonly options?: readonly ExitPlanModeOption[] | undefined; +} | null> { + if (!context.agent.planMode.isActive) return null; + + let data: Awaited>; + try { + data = await context.agent.planMode.data(); + } catch { + return null; + } + if (data === null || data.content.trim().length === 0) return null; + + return { + plan: data.content, + path: data.path, + options: exitPlanModeOptions(context.toolCallContext.args), + }; +} + +function exitPlanModeApprovalResult( + context: PermissionPolicyContext, + result: ApprovalResponse, + options: readonly ExitPlanModeOption[] | undefined, +): PermissionPolicyResult { + if (result.decision === 'approved') { + const selected = selectedExitPlanModeOption(options, result.selectedLabel); + return { + kind: 'allow', + executionMetadata: exitPlanModeExecutionMetadata(selected), + }; + } + + if (result.decision === 'cancelled') { + return { + kind: 'result', + result: { + syntheticResult: { + isError: false, + output: 'Plan approval dismissed. Plan mode remains active.', + }, + }, + }; + } + + if (result.selectedLabel === 'Reject and Exit') { + const failed = exitPlanModeForRejectedPlan(context); + return { + kind: 'result', + result: { + syntheticResult: + failed ?? { + isError: true, + stopTurn: true, + output: 'Plan rejected by user. Plan mode deactivated.', + }, + }, + }; + } + + const feedback = result.feedback ?? ''; + if (result.selectedLabel === 'Revise' || feedback.length > 0) { + return { + kind: 'result', + result: { + syntheticResult: { + isError: false, + output: + feedback.length > 0 + ? `User rejected the plan. Feedback:\n\n${feedback}` + : 'User requested revisions. Plan mode remains active.', + }, + }, + }; + } + + return { + kind: 'result', + result: { + syntheticResult: { + isError: true, + stopTurn: true, + output: 'Plan rejected by user. Plan mode remains active.', + }, + }, + }; +} + +function exitPlanModeExecutionMetadata( + selectedOption: ExitPlanModeOption | undefined, +): ExitPlanModeExecutionMetadata { + return { + selectedOption, + planTelemetrySubmitted: true, + planTelemetryResolved: true, + }; +} + +function trackExitPlanModeResolution( + context: PermissionPolicyContext, + result: ApprovalResponse, +): void { + const selectedLabel = result.selectedLabel ?? ''; + const normalizedSelectedLabel = normalizeOptionLabel(selectedLabel); + const feedback = result.feedback ?? ''; + const hasFeedback = feedback.length > 0; + + if (result.decision === 'cancelled') { + context.agent.telemetry.track('plan_resolved', { outcome: 'dismissed' }); + return; + } + + if (result.decision === 'approved') { + if (selectedLabel.length > 0) { + context.agent.telemetry.track('plan_resolved', { + outcome: 'approved', + chosen_option: selectedLabel, + }); + return; + } + context.agent.telemetry.track('plan_resolved', { outcome: 'approved' }); + return; + } + + if (normalizedSelectedLabel === 'reject and exit') { + context.agent.telemetry.track('plan_resolved', { outcome: 'rejected_and_exited' }); + return; + } + + if (normalizedSelectedLabel === 'revise' || hasFeedback) { + context.agent.telemetry.track('plan_resolved', { + outcome: 'revise', + has_feedback: hasFeedback, + }); + return; + } + + context.agent.telemetry.track('plan_resolved', { outcome: 'rejected' }); +} + +function exitPlanModeForRejectedPlan( + context: PermissionPolicyContext, +): ExecutableToolResult | undefined { + try { + context.agent.planMode.exit(); + } catch (error) { + const message = error instanceof Error ? error.message : 'Failed to exit plan mode.'; + return { + isError: true, + output: `Failed to exit plan mode: ${message}`, + }; + } +} + +function exitPlanModeOptions(args: unknown): readonly ExitPlanModeOption[] | undefined { + if (args === null || typeof args !== 'object') return undefined; + const options = (args as { readonly options?: unknown }).options; + if (!Array.isArray(options) || options.length < 2) return undefined; + const parsed: ExitPlanModeOption[] = []; + for (const option of options) { + if (option === null || typeof option !== 'object') return undefined; + const label = (option as { readonly label?: unknown }).label; + if (typeof label !== 'string') return undefined; + // `description` is optional in the ExitPlanMode schema (defaults to ''), + // so an option that omits it is still valid. + const description = (option as { readonly description?: unknown }).description; + if (description !== undefined && typeof description !== 'string') return undefined; + parsed.push({ label, description: description ?? '' }); + } + return parsed; +} + +function selectedExitPlanModeOption( + options: readonly ExitPlanModeOption[] | undefined, + label: string | undefined, +): ExitPlanModeOption | undefined { + if (options === undefined || label === undefined) return undefined; + return options.find((option) => option.label === label); +} + +function exitPlanModeAction(options: readonly ExitPlanModeOption[] | undefined): string { + return options !== undefined && options.length >= 2 + ? 'Review plan and choose an option' + : 'Review plan'; +} + +function normalizeOptionLabel(label: string): string { + return label.trim().toLowerCase(); +} + +function readStringField(args: unknown, key: string): string | undefined { + if (args === null || typeof args !== 'object') return undefined; + const value = (args as Record)[key]; + return typeof value === 'string' ? value : undefined; +} diff --git a/packages/agent-core/src/agent/permission/policies/yolo-workspace-access.ts b/packages/agent-core/src/agent/permission/policies/yolo-workspace-access.ts new file mode 100644 index 000000000..b95b9542c --- /dev/null +++ b/packages/agent-core/src/agent/permission/policies/yolo-workspace-access.ts @@ -0,0 +1,74 @@ +import type { ToolInputDisplay } from '../../../tools/display'; +import { + DEFAULT_WORKSPACE_ACCESS_POLICY, + resolvePathAccess, + type PathAccessOperation, +} from '../../../tools/policies/path-access'; +import type { PermissionPolicy } from '../policy'; + +type FileInputDisplayOperation = Extract['operation']; + +const FILE_ACCESS_TOOLS: Readonly< + Record +> = { + Read: ['read', 'read'], + ReadMediaFile: ['read', 'read'], + Write: ['write', 'write'], + Edit: ['write', 'edit'], + Grep: ['search', 'grep'], +}; + +export const YoloOutsideWorkspacePermissionPolicy: PermissionPolicy = { + name: 'yolo.outside-workspace', + evaluate({ agent, mode, toolCallContext }) { + if (mode !== 'yolo') return undefined; + + const toolName = toolCallContext.toolCall.function.name; + const toolAccess = FILE_ACCESS_TOOLS[toolName]; + if (toolAccess === undefined) return undefined; + const [operation, displayOperation] = toolAccess; + + const rawPath = readStringField(toolCallContext.args, 'path'); + if (rawPath === undefined) return undefined; + + let access; + try { + access = resolvePathAccess( + rawPath, + agent.config.cwd, + { + workspaceDir: agent.config.cwd, + additionalDirs: agent.skills?.registry.getSkillRoots() ?? [], + }, + { + operation, + pathClass: agent.runtime.kaos.pathClass(), + homeDir: agent.runtime.kaos.gethome(), + policy: { + ...DEFAULT_WORKSPACE_ACCESS_POLICY, + checkSensitive: toolName !== 'Grep', + }, + }, + ); + } catch { + return undefined; + } + + if (!access.outsideWorkspace) return undefined; + return { + kind: 'ask', + display: { + kind: 'file_io', + operation: displayOperation, + path: access.path, + detail: `Outside workspace: ${agent.config.cwd}`, + }, + }; + }, +}; + +function readStringField(args: unknown, key: string): string | undefined { + if (args === null || typeof args !== 'object') return undefined; + const value = (args as Record)[key]; + return typeof value === 'string' ? value : undefined; +} diff --git a/packages/agent-core/src/agent/permission/policy.ts b/packages/agent-core/src/agent/permission/policy.ts new file mode 100644 index 000000000..5baf7776e --- /dev/null +++ b/packages/agent-core/src/agent/permission/policy.ts @@ -0,0 +1,42 @@ +import type { Agent } from '..'; +import type { PrepareToolExecutionResult, ToolExecutionHookContext } from '../../loop'; +import type { ToolInputDisplay } from '../../tools/display'; +import type { PermissionApprovalResultRecord, PermissionMode, PermissionRule } from './types'; + +export interface PermissionPolicyContext { + readonly agent: Agent; + readonly mode: PermissionMode; + readonly toolCallContext: ToolExecutionHookContext; + /** + * The rule matched by `checkPermission()`, if any. + * + * Policies that want to defer to user-defined rules (e.g. a default-allow + * policy that should not override an explicit `ask`/`deny` rule) inspect + * this to decide whether to fire. `undefined` means the decision came + * from the built-in default permission table rather than a user rule. + */ + readonly matchedRule: PermissionRule | undefined; + readonly recordApprovalResult: (record: PermissionApprovalResultRecord) => void; +} + +export type PermissionPolicyResult = + | { + readonly kind: 'allow'; + readonly executionMetadata?: unknown; + } + | { + readonly kind: 'result'; + readonly result: PrepareToolExecutionResult; + } + | { + readonly kind: 'ask'; + readonly action?: string | undefined; + readonly display?: ToolInputDisplay | undefined; + }; + +export interface PermissionPolicy { + readonly name: string; + evaluate( + context: PermissionPolicyContext, + ): PermissionPolicyResult | undefined | Promise; +} diff --git a/packages/agent-core/src/agent/permission/types.ts b/packages/agent-core/src/agent/permission/types.ts new file mode 100644 index 000000000..df51e55fc --- /dev/null +++ b/packages/agent-core/src/agent/permission/types.ts @@ -0,0 +1,60 @@ +import type { ToolInputDisplay } from '../../tools/display'; + +export type PermissionRuleDecision = 'allow' | 'deny' | 'ask'; + +/** + * Rule provenance. `session-runtime` is the value used by the runtime + * "approve for session" path; `turn-override`, `project`, and `user` + * are reserved for static-loaded rules surfaced by external callers. + */ +export type PermissionRuleScope = 'turn-override' | 'session-runtime' | 'project' | 'user'; + +/** + * Top-level user-facing permission posture. Controls how non-deny rules + * are treated when the closure is constructed. Independent of rule + * merging: deny rules always fire regardless of mode. + * + * - `manual` — rule set drives decision; unmatched tool calls ask + * - `yolo` — only deny rules can block; everything else allows + * - `auto` — caller may bypass rule checks entirely + */ +export type PermissionMode = 'manual' | 'yolo' | 'auto'; + +/** + * A single permission rule. `pattern` is the DSL form (`Read(/etc/**)`, + * `Bash(rm *)`, or bare `Write`). See `parse-pattern.ts` for the parser + * and `matches-rule.ts` for the matcher. + */ +export interface PermissionRule { + readonly decision: PermissionRuleDecision; + readonly scope: PermissionRuleScope; + readonly pattern: string; + readonly reason?: string | undefined; +} + +export interface ApprovalRequest { + toolCallId: string; + toolName: string; + action: string; + display: ToolInputDisplay; +} + +export interface ApprovalResponse { + decision: 'approved' | 'rejected' | 'cancelled'; + scope?: 'session'; + feedback?: string; + selectedLabel?: string; +} + +export interface PermissionApprovalResultRecord { + readonly turnId: number; + readonly toolCallId: string; + readonly toolName: string; + readonly action: string; + readonly result: ApprovalResponse; +} + +export interface PermissionData { + mode: PermissionMode; + rules: PermissionRule[]; +} diff --git a/packages/agent-core/src/agent/plan/index.ts b/packages/agent-core/src/agent/plan/index.ts new file mode 100644 index 000000000..e2508e8af --- /dev/null +++ b/packages/agent-core/src/agent/plan/index.ts @@ -0,0 +1,146 @@ +import { randomUUID } from 'node:crypto'; +import { dirname, join } from 'node:path'; + +import type { Agent } from '..'; +import { generateHeroSlug } from '../../utils/hero-slug'; + +export type PlanData = null | { + id: string; + content: string; + path: string; +}; +export type PlanFilePath = string | null; + +export class PlanMode { + protected _isActive = false; + protected _planId: null | string = null; + protected _planFilePath: PlanFilePath = null; + + constructor(protected readonly agent: Agent) {} + + createPlanId(): string { + return generateHeroSlug(randomUUID(), new Set()); + } + + async enter(id = this.createPlanId(), createFile = false, emitStatus = true): Promise { + if (this._isActive) { + throw new Error('Already in plan mode'); + } + + this._isActive = true; + this._planId = id; + this._planFilePath = null; + + let enterRecorded = false; + try { + const planFilePath = this.planFilePathFor(id); + this._planFilePath = planFilePath; + await this.ensurePlanDirectory(planFilePath); + this.agent.records.logRecord({ type: 'plan_mode.enter', id }); + enterRecorded = true; + if (createFile) { + await this.writeEmptyPlanFile(planFilePath); + } + } catch (error) { + if (enterRecorded) { + this.cancel(id); + } else { + this._isActive = false; + this._planId = null; + this._planFilePath = null; + } + throw error; + } + + if (emitStatus) this.agent.emitStatusUpdated(); + } + + restoreEnter({ id }: { readonly id: string }): void { + this.agent.replayBuilder.push({ + type: 'plan_updated', + enabled: true, + }); + + this._isActive = true; + this._planId = id; + this._planFilePath = this.planFilePathFor(id); + } + + cancel(id?: string): void { + this.agent.records.logRecord({ type: 'plan_mode.cancel', id }); + this.agent.replayBuilder.push({ + type: 'plan_updated', + enabled: false, + }); + this._isActive = false; + this._planId = null; + this._planFilePath = null; + this.agent.emitStatusUpdated(); + } + + async clear(): Promise { + if (!this._planFilePath) return; + await this.writeEmptyPlanFile(this._planFilePath); + } + + exit(id?: string): void { + this.agent.records.logRecord({ type: 'plan_mode.exit', id }); + this.agent.replayBuilder.push({ + type: 'plan_updated', + enabled: false, + }); + this._isActive = false; + this._planId = null; + this._planFilePath = null; + this.agent.emitStatusUpdated(); + } + + get isActive() { + return this._isActive; + } + + get planFilePath(): PlanFilePath { + return this._planFilePath; + } + + async data(): Promise { + if (!this._planId || !this._planFilePath) return null; + let content = ''; + try { + content = await this.agent.runtime.kaos.readText(this._planFilePath); + } catch (error) { + if (!isMissingFileError(error)) throw error; + } + return { + id: this._planId, + content, + path: this._planFilePath, + }; + } + + private async writeEmptyPlanFile(path: string): Promise { + await this.ensurePlanDirectory(path); + await this.agent.runtime.kaos.writeText(path, ''); + } + + private async ensurePlanDirectory(path: string): Promise { + await this.agent.runtime.kaos.mkdir(dirname(path), { + parents: true, + existOk: true, + }); + } + + private planFilePathFor(id: string): string { + const plansDir = + this.agent.homedir === undefined + ? join(this.agent.config.cwd || this.agent.runtime.kaos.getcwd(), 'plan') + : join(this.agent.homedir, 'plans'); + return join(plansDir, `${id}.md`); + } +} + +function isMissingFileError(error: unknown): boolean { + if (error === null || typeof error !== 'object') return false; + const code = (error as { readonly code?: unknown }).code; + return code === 'ENOENT'; +} diff --git a/packages/agent-core/src/agent/records/index.ts b/packages/agent-core/src/agent/records/index.ts new file mode 100644 index 000000000..29de0cfa3 --- /dev/null +++ b/packages/agent-core/src/agent/records/index.ts @@ -0,0 +1,139 @@ +import type { Agent } from '..'; +import type { AgentRecord, AgentRecordPersistence } from './types'; + +export * from './types'; +export { FileSystemAgentRecordPersistence } from './wire-file'; +export type { FileSystemAgentRecordPersistenceOptions } from './wire-file'; + +// Contract: restore MUST NOT emit UI events, call the LLM, execute tools, or +// touch the filesystem in a way that triggers external side effects. Each case +// should reproduce the in-memory state the live handler left behind, nothing more. +export function restoreAgentRecord(agent: Agent, input: AgentRecord): void { + switch (input.type) { + case 'turn.prompt': + agent.turn.restorePrompt(); + return; + case 'turn.steer': + agent.turn.restoreSteer(input.input, input.origin); + return; + case 'turn.cancel': + agent.turn.cancel(input.turnId); + return; + case 'background.stop': + return; + case 'config.update': + agent.config.update(input); + return; + case 'permission.set_mode': + agent.permission.setMode(input.mode); + return; + case 'permission.record_approval_result': + agent.permission.recordApprovalResult(input); + return; + case 'usage.record': + agent.usage.record(input.model, input.usage, 'session'); + return; + case 'full_compaction.begin': + agent.fullCompaction.begin(input); + return; + case 'full_compaction.cancel': + agent.fullCompaction.cancel(); + return; + case 'full_compaction.complete': + agent.fullCompaction.complete(input); + return; + case 'plan_mode.enter': + agent.planMode.restoreEnter(input); + return; + case 'plan_mode.cancel': + agent.planMode.cancel(input.id); + return; + case 'plan_mode.exit': + agent.planMode.exit(input.id); + return; + case 'context.append_message': + agent.context.appendMessage(input.message); + return; + case 'context.mark_last_user_prompt_blocked': + agent.context.markLastUserPromptBlocked(input.hookEvent); + return; + case 'context.append_loop_event': + agent.context.appendLoopEvent(input.event); + return; + case 'context.clear': + agent.context.clear(); + return; + case 'context.apply_compaction': + agent.context.applyCompaction(input); + return; + case 'tools.register_user_tool': + agent.tools.registerUserTool(input); + return; + case 'tools.unregister_user_tool': + agent.tools.unregisterUserTool(input.name); + return; + case 'tools.set_active_tools': + agent.tools.setActiveTools(input.names); + return; + case 'tools.update_store': + agent.tools.updateStore(input.key, input.value); + return; + } +} + +export class AgentRecords { + private readonly records: AgentRecord[] = []; + private _restoring = false; + onRecord?: (record: AgentRecord) => void; + onError?: (error: unknown, record: AgentRecord) => void; + + constructor( + private readonly restoreRecord: (record: AgentRecord) => void, + private readonly persistence?: AgentRecordPersistence, + ) {} + + get restoring() { + return this._restoring; + } + + logRecord(record: AgentRecord): void { + if (this._restoring) return; + const stamped: AgentRecord = + record.time !== undefined ? record : { ...record, time: Date.now() }; + this.records.push(stamped); + this.onRecord?.(stamped); + void this.persistence?.append(stamped).catch((error) => { + this.onError?.(error, stamped); + }); + } + + restore(record: AgentRecord): void { + this._restoring = true; + try { + this.restoreRecord(record); + } finally { + this._restoring = false; + } + } + + async replay(): Promise { + if (!this.persistence) throw new Error('No persistence provided for AgentRecords'); + for await (const record of this.persistence.read()) { + this.records.push(record); + this._restoring = true; + try { + this.restoreRecord(record); + } finally { + this._restoring = false; + } + } + } + + snapshot(): readonly AgentRecord[] { + return [...this.records]; + } + + async flush(): Promise { + await this.persistence?.flush(); + } +} diff --git a/packages/agent-core/src/agent/records/types.ts b/packages/agent-core/src/agent/records/types.ts new file mode 100644 index 000000000..83442af43 --- /dev/null +++ b/packages/agent-core/src/agent/records/types.ts @@ -0,0 +1,91 @@ +import type { ContentPart, TokenUsage } from '@moonshot-ai/kosong'; + +import type { LoopRecordedEvent } from '../../loop'; +import type { ToolStoreUpdate } from '../../tools/store'; +import type { CompactionBeginData, CompactionResult } from '../compaction'; +import type { AgentConfigUpdateData } from '../config'; +import type { ContextMessage, PromptOrigin } from '../context'; +import type { PermissionApprovalResultRecord, PermissionMode } from '../permission'; +import type { UserToolRegistration } from '../tool'; +import type { UsageRecordScope } from '../usage'; + +export interface AgentRecordEvents { + 'turn.prompt': { + input: readonly ContentPart[]; + origin: PromptOrigin; + }; + 'turn.steer': { + input: readonly ContentPart[]; + origin: PromptOrigin; + }; + 'turn.cancel': { turnId?: number }; + + 'config.update': AgentConfigUpdateData; + + 'permission.set_mode': { + mode: PermissionMode; + }; + 'permission.record_approval_result': PermissionApprovalResultRecord; + + 'full_compaction.begin': CompactionBeginData; + + 'plan_mode.enter': { + id: string; + }; + 'plan_mode.cancel': { + id?: string; + }; + 'plan_mode.exit': { + id?: string; + }; + + 'tools.register_user_tool': UserToolRegistration; + 'tools.unregister_user_tool': { + name: string; + }; + 'tools.set_active_tools': { + names: readonly string[]; + }; + + 'background.stop': { + taskId: string; + }; + + 'usage.record': { + model: string; + usage: TokenUsage; + usageScope?: UsageRecordScope | undefined; + }; + + 'full_compaction.cancel': {}; + 'full_compaction.complete': CompactionResult; + + 'context.append_message': { message: ContextMessage }; + 'context.mark_last_user_prompt_blocked': { hookEvent: string }; + 'context.append_loop_event': { event: LoopRecordedEvent }; + 'context.clear': {}; + 'context.apply_compaction': CompactionResult; + + 'tools.update_store': ToolStoreUpdate; +} + +export type AgentRecord = { + [K in keyof AgentRecordEvents]: Readonly & { + readonly type: K; + readonly time?: number; + }; +}[keyof AgentRecordEvents]; + +export type AgentRecordOf = Extract< + AgentRecord, + { readonly type: K } +>; + +export const AGENT_WIRE_PROTOCOL_VERSION = '1.0'; + +export interface AgentRecordPersistence { + read(): AsyncIterable; + append(input: AgentRecord): Promise; + flush(): Promise; + close(): Promise; +} diff --git a/packages/agent-core/src/agent/records/wire-file.ts b/packages/agent-core/src/agent/records/wire-file.ts new file mode 100644 index 000000000..d16fe6216 --- /dev/null +++ b/packages/agent-core/src/agent/records/wire-file.ts @@ -0,0 +1,193 @@ +import { mkdir, open, readFile, stat } from 'node:fs/promises'; +import { dirname } from 'node:path'; + +import { syncDir } from '../../utils/fs'; +import { + AGENT_WIRE_PROTOCOL_VERSION, + type AgentRecord, + type AgentRecordPersistence, +} from './types'; + +interface AgentWireMetadata { + readonly type: 'metadata'; + readonly protocol_version: string; + readonly created_at: number; +} + +type WireFileRecord = AgentRecord | AgentWireMetadata; + +export interface FileSystemAgentRecordPersistenceOptions { + readonly onError?: ((error: unknown) => void) | undefined; +} + +class AsyncSerialQueue { + private tail: Promise = Promise.resolve(); + + run(task: () => Promise): Promise { + const next = this.tail.then(task, task); + this.tail = next.catch(() => { + /* swallow so a rejected task does not poison the chain */ + }); + return next; + } +} + +// Single-writer per file: the "is file empty?" check before emitting the +// header is racy across processes, but multi-writer wire.jsonl is unsupported. +export class FileSystemAgentRecordPersistence implements AgentRecordPersistence { + private readonly queue = new AsyncSerialQueue(); + private readonly pending: WireFileRecord[] = []; + private closed = false; + private closing = false; + private directorySynced = false; + private drainScheduled = false; + private lastBackgroundError: Error | undefined; + private headerPromise: Promise | undefined; + + constructor( + private readonly filePath: string, + private readonly options: FileSystemAgentRecordPersistenceOptions = {}, + ) {} + + async *read(): AsyncIterable { + await this.flush(); + + let text: string; + try { + text = await readFile(this.filePath, 'utf8'); + } catch (error) { + const code = (error as NodeJS.ErrnoException).code; + if (code === 'ENOENT') return; + throw error; + } + + const lines = text.split('\n'); + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + if (line === undefined || line.length === 0) continue; + + let parsed: WireFileRecord; + try { + parsed = JSON.parse(line) as WireFileRecord; + } catch (parseError) { + // Tolerate a truncated trailing line — last write may have crashed + // mid-flush; everything before is still well-formed. + if (i === lines.length - 1) continue; + throw new Error( + `wire.jsonl: corrupted line ${i + 1} in ${this.filePath}: ${String(parseError)}`, + { cause: parseError }, + ); + } + if (parsed.type === 'metadata') continue; + yield parsed; + } + } + + async append(input: AgentRecord): Promise { + if (this.closed || this.closing) { + throw new Error('FileSystemAgentRecordPersistence: append on closed persistence'); + } + await this.ensureHeader(); + if (this.closed || this.closing) { + throw new Error('FileSystemAgentRecordPersistence: append on closed persistence'); + } + this.pending.push(input); + this.scheduleDrain(); + } + + async flush(): Promise { + await this.headerPromise; + try { + await this.queue.run(async () => { + while (this.pending.length > 0 && !this.closed) { + await this.drainBatch(); + } + }); + } catch (error) { + this.options.onError?.(error); + throw error; + } + + if (this.lastBackgroundError !== undefined) { + const error = this.lastBackgroundError; + this.lastBackgroundError = undefined; + throw error; + } + } + + async close(): Promise { + if (this.closed) return; + this.closing = true; + try { + await this.flush(); + this.closed = true; + } catch (error) { + this.closing = false; + throw error; + } + } + + private ensureHeader(): Promise { + this.headerPromise ??= this.writeHeaderIfNeeded(); + return this.headerPromise; + } + + private async writeHeaderIfNeeded(): Promise { + let isEmpty = true; + try { + const stats = await stat(this.filePath); + isEmpty = stats.size === 0; + } catch (error) { + const code = (error as NodeJS.ErrnoException).code; + if (code !== 'ENOENT') throw error; + } + if (!isEmpty) return; + + this.pending.unshift({ + type: 'metadata', + protocol_version: AGENT_WIRE_PROTOCOL_VERSION, + created_at: Date.now(), + }); + } + + private scheduleDrain(): void { + if (this.drainScheduled || this.closed) return; + this.drainScheduled = true; + queueMicrotask(() => { + this.drainScheduled = false; + if (this.closed || this.pending.length === 0) return; + this.queue + .run(async () => { + while (this.pending.length > 0 && !this.closed) { + await this.drainBatch(); + } + }) + .catch((error) => { + this.lastBackgroundError = error as Error; + this.options.onError?.(error); + }); + }); + } + + private async drainBatch(): Promise { + if (this.pending.length === 0) return; + + const batch = this.pending.splice(0); + const lines = batch.map((e) => JSON.stringify(e) + '\n'); + + await mkdir(dirname(this.filePath), { recursive: true }); + + const fh = await open(this.filePath, 'a'); + try { + await fh.appendFile(lines.join(''), 'utf8'); + await fh.sync(); + } finally { + await fh.close(); + } + + if (!this.directorySynced) { + await syncDir(dirname(this.filePath)); + this.directorySynced = true; + } + } +} diff --git a/packages/agent-core/src/agent/replay/index.ts b/packages/agent-core/src/agent/replay/index.ts new file mode 100644 index 000000000..0196e6225 --- /dev/null +++ b/packages/agent-core/src/agent/replay/index.ts @@ -0,0 +1,18 @@ +import type { Agent } from '..'; +import type { AgentReplayRecord } from '../..'; + +export class ReplayBuilder { + protected readonly records: AgentReplayRecord[] = []; + + constructor(public readonly agent: Agent) {} + + push(record: AgentReplayRecord): void { + if (this.agent.records.restoring) { + this.records.push(record); + } + } + + buildResult(): readonly AgentReplayRecord[] { + return this.records; + } +} diff --git a/packages/agent-core/src/agent/skill/index.ts b/packages/agent-core/src/agent/skill/index.ts new file mode 100644 index 000000000..7d698b7eb --- /dev/null +++ b/packages/agent-core/src/agent/skill/index.ts @@ -0,0 +1,72 @@ +import { randomUUID } from 'node:crypto'; + +import type { ActivateSkillPayload } from '#/rpc'; +import type { ContentPart } from '@moonshot-ai/kosong'; + +import type { Agent } from '..'; +import { ErrorCodes, KimiError } from '#/errors'; +import { isUserActivatableSkillType, type SkillRegistry } from '../../skill'; +import type { SkillActivationOrigin } from '../context'; + +export class SkillManager { + constructor( + protected readonly agent: Agent, + public readonly registry: SkillRegistry, + ) {} + + activate(input: ActivateSkillPayload): void { + const skill = this.registry.getSkill(input.name); + if (skill === undefined) { + throw new KimiError(ErrorCodes.SKILL_NOT_FOUND, `Skill "${input.name}" was not found`); + } + if (!isUserActivatableSkillType(skill.metadata.type)) { + throw new KimiError(ErrorCodes.SKILL_TYPE_UNSUPPORTED, `Skill "${skill.name}" cannot be activated by the user`); + } + + this.recordActivation( + { + kind: 'skill_activation', + activationId: randomUUID(), + skillName: skill.name, + trigger: 'user-slash', + skillType: skill.metadata.type, + skillPath: skill.path, + skillSource: skill.source, + skillArgs: input.args, + }, + [ + { + type: 'text', + text: this.registry.renderSkillPrompt(skill, input.args ?? ''), + }, + ], + ); + } + + recordActivation( + origin: SkillActivationOrigin, + input?: readonly ContentPart[] | undefined, + ): void { + this.agent.emitEvent({ + type: 'skill.activated', + activationId: origin.activationId, + skillName: origin.skillName, + trigger: origin.trigger, + skillArgs: origin.skillArgs, + skillPath: origin.skillPath, + skillSource: origin.skillSource, + }); + this.agent.telemetry.track('skill_invoked', { + skill_name: origin.skillName, + trigger: origin.trigger, + }); + if (origin.skillType === 'flow') { + this.agent.telemetry.track('flow_invoked', { + flow_name: origin.skillName, + }); + } + if (input !== undefined) { + this.agent.turn.prompt(input, origin); + } + } +} diff --git a/packages/agent-core/src/agent/tool/index.ts b/packages/agent-core/src/agent/tool/index.ts new file mode 100644 index 000000000..614b8ff25 --- /dev/null +++ b/packages/agent-core/src/agent/tool/index.ts @@ -0,0 +1,421 @@ +import { uniq } from '@antfu/utils'; +import type { ChatProvider, Tool } from '@moonshot-ai/kosong'; + +import type { Agent } from '..'; +import { globMatch } from '../permission/path-glob-match'; +import { makeErrorPayload } from '../../errors'; +import type { ExecutableTool } from '../../loop'; +import { createMcpAuthTool } from '../../mcp/auth-tool'; +import type { McpConnectionManager, McpServerEntry } from '../../mcp'; +import { mcpResultToExecutableOutput } from '../../mcp/output'; +import { isMcpToolName, qualifyMcpToolName } from '../../mcp/tool-naming'; +import type { MCPClient } from '../../mcp/types'; +import { DEFAULT_AGENT_PROFILES } from '../../profile'; +import { withProviderRequestAuth } from '../../providers/request-auth'; +import { extendWorkspaceWithSkillRoots } from '../../skill'; +import * as b from '../../tools/builtin'; +import type { ToolStore, ToolStoreData, ToolStoreKey } from '../../tools/store'; +import type { + BuiltinTool, + McpServerRegistrationResult, + McpToolCollision, + ToolInfo, + UserToolRegistration, +} from './types'; + +export * from './types'; + +interface McpToolEntry { + readonly tool: ExecutableTool; + readonly serverName: string; +} + +export class ToolManager { + protected builtinTools: Map = new Map(); + protected readonly userTools: Map = new Map(); + protected readonly mcpTools: Map = new Map(); + /** server name → list of qualified tool names registered for that server. */ + protected readonly mcpToolsByServer: Map = new Map(); + protected enabledTools: Set = new Set(); + /** Glob patterns (e.g. `mcp__*`, `mcp__github__*`) gating which MCP tools the profile exposes. */ + private mcpAccessPatterns: string[] = []; + protected readonly store: Partial = {}; + private mcpToolStatusUnsubscribe: (() => void) | undefined; + + constructor(protected readonly agent: Agent) { + this.attachMcpTools(); + } + + protected get toolStore(): ToolStore { + return { + get: (key) => this.store[key], + set: (key, value) => { + this.updateStore(key, value); + }, + }; + } + + attachMcpTools(): void { + const mcp = this.agent.mcp; + if (mcp === undefined) return; + if (this.mcpToolStatusUnsubscribe !== undefined) return; + for (const entry of mcp.list()) { + if (entry.status === 'connected') { + this.registerConnectedMcpServer(mcp, entry); + } else if (entry.status === 'needs-auth') { + this.registerNeedsAuthMcpServer(mcp, entry); + } + } + this.mcpToolStatusUnsubscribe = mcp.onStatusChange((entry) => { + this.handleMcpServerStatusChange(mcp, entry); + }); + } + + updateStore(key: K, value: ToolStoreData[K]): void { + this.agent.records.logRecord({ + type: 'tools.update_store', + key, + value, + }); + this.store[key] = value; + } + + registerUserTool(input: UserToolRegistration): void { + this.agent.records.logRecord({ + type: 'tools.register_user_tool', + ...input, + }); + const { name, description, parameters } = input; + const tool: ExecutableTool = { + name, + description, + parameters, + resolveExecution: (args) => { + return { + execute: async (context) => { + return this.agent.rpc.toolCall( + { + turnId: Number(context.turnId), + toolCallId: context.toolCallId, + args, + }, + { signal: context.signal }, + ); + }, + }; + }, + }; + this.userTools.set(name, tool); + this.enabledTools.add(name); + } + + unregisterUserTool(name: string): void { + this.agent.records.logRecord({ + type: 'tools.unregister_user_tool', + name, + }); + this.userTools.delete(name); + this.enabledTools.delete(name); + } + + registerMcpServer( + serverName: string, + client: MCPClient, + tools: readonly Tool[], + enabledTools?: ReadonlySet, + ): McpServerRegistrationResult { + this.unregisterMcpServer(serverName); + const qualifiedNames: string[] = []; + const collisions: McpToolCollision[] = []; + const seenInThisCall = new Map(); + for (const tool of tools) { + if (enabledTools !== undefined && !enabledTools.has(tool.name)) continue; + const qualified = qualifyMcpToolName(serverName, tool.name); + const firstInThisCall = seenInThisCall.get(qualified); + if (firstInThisCall !== undefined) { + collisions.push({ + qualified, + toolName: tool.name, + collidesWith: { kind: 'same_server', toolName: firstInThisCall }, + }); + continue; + } + const existingEntry = this.mcpTools.get(qualified); + if (existingEntry !== undefined) { + collisions.push({ + qualified, + toolName: tool.name, + collidesWith: { kind: 'other_server', serverName: existingEntry.serverName }, + }); + continue; + } + seenInThisCall.set(qualified, tool.name); + const wrapped: ExecutableTool = { + name: qualified, + description: tool.description, + parameters: tool.parameters, + resolveExecution: (args) => { + return { + execute: async (context) => { + // `args` has already been JSON-parsed and schema-validated by + // the loop's preflight (`loop/tool-call.ts`), so the MCP + // client gets a plain object directly. + const result = await client.callTool( + tool.name, + (args ?? {}) as Record, + context.signal, + ); + return mcpResultToExecutableOutput(result, qualified); + }, + }; + }, + }; + this.mcpTools.set(qualified, { tool: wrapped, serverName }); + qualifiedNames.push(qualified); + } + this.mcpToolsByServer.set(serverName, qualifiedNames); + return { registered: qualifiedNames, collisions }; + } + + unregisterMcpServer(serverName: string): boolean { + const existing = this.mcpToolsByServer.get(serverName); + if (existing === undefined) return false; + for (const qualified of existing) { + this.mcpTools.delete(qualified); + } + this.mcpToolsByServer.delete(serverName); + return true; + } + + private handleMcpServerStatusChange(mcp: McpConnectionManager, entry: McpServerEntry): void { + if (entry.status === 'connected') { + this.registerConnectedMcpServer(mcp, entry); + return; + } + if (entry.status === 'needs-auth') { + this.registerNeedsAuthMcpServer(mcp, entry); + return; + } + if (entry.status === 'failed') { + this.unregisterMcpServer(entry.name); + this.agent.emitEvent({ + type: 'tool.list.updated', + reason: 'mcp.failed', + serverName: entry.name, + }); + return; + } + if (entry.status === 'disabled' || entry.status === 'pending') { + const removed = this.unregisterMcpServer(entry.name); + if (removed) { + this.agent.emitEvent({ + type: 'tool.list.updated', + reason: 'mcp.disconnected', + serverName: entry.name, + }); + } + } + } + + private registerNeedsAuthMcpServer(mcp: McpConnectionManager, entry: McpServerEntry): void { + // Replace whatever tools (real or synthetic) were registered before; a + // server flipping to needs-auth means previous tokens were invalidated. + this.unregisterMcpServer(entry.name); + const oauthService = mcp.oauthService; + const serverUrl = mcp.getHttpServerUrl(entry.name); + if (oauthService === undefined || serverUrl === undefined) { + // Misconfiguration: a server reached needs-auth without the manager + // owning an OAuth service or being HTTP. Treat it as a no-op so the + // existing failure error message keeps the user informed. + return; + } + const tool = createMcpAuthTool({ + serverName: entry.name, + serverUrl, + oauthService, + reconnect: async () => { + await mcp.reconnect(entry.name); + }, + }); + this.mcpTools.set(tool.name, { tool, serverName: entry.name }); + this.mcpToolsByServer.set(entry.name, [tool.name]); + // The synthetic auth tool is now in the tool list; surface it the same way + // a real toolset would show up so the model picks it up. + this.agent.emitEvent({ + type: 'tool.list.updated', + reason: 'mcp.connected', + serverName: entry.name, + }); + } + + private registerConnectedMcpServer(mcp: McpConnectionManager, entry: McpServerEntry): void { + const resolved = mcp.resolved(entry.name); + if (resolved === undefined) return; + const result = this.registerMcpServer( + entry.name, + resolved.client, + resolved.tools, + resolved.enabledNames, + ); + this.emitMcpToolCollisions(entry.name, result.collisions); + this.agent.emitEvent({ + type: 'tool.list.updated', + reason: 'mcp.connected', + serverName: entry.name, + }); + } + + private emitMcpToolCollisions(serverName: string, collisions: readonly McpToolCollision[]): void { + if (collisions.length === 0) return; + const summary = collisions + .map((c) => + c.collidesWith.kind === 'same_server' + ? `"${c.toolName}" -> ${c.qualified} (collides with "${c.collidesWith.toolName}" from the same server)` + : `"${c.toolName}" -> ${c.qualified} (collides with server "${c.collidesWith.serverName}")`, + ) + .join('; '); + this.agent.emitEvent({ + type: 'error', + ...makeErrorPayload( + 'mcp.tool_name_collision', + `MCP server "${serverName}" registered ${collisions.length} tool name` + + `${collisions.length === 1 ? '' : 's'} ` + + `that collide with existing qualified names; the losing tools were dropped: ${summary}`, + { details: { serverName, collisions: collisions as readonly unknown[] } }, + ), + }); + } + + setActiveTools(names: readonly string[]): void { + this.agent.records.logRecord({ + type: 'tools.set_active_tools', + names, + }); + // MCP entries are glob patterns gated separately; the rest are exact + // builtin/user tool names. The split keeps every caller on one string[]. + this.enabledTools = new Set(names.filter((name) => !isMcpToolName(name))); + this.mcpAccessPatterns = names.filter((name) => isMcpToolName(name)); + } + + private isMcpToolEnabled(name: string): boolean { + return this.mcpAccessPatterns.some((pattern) => globMatch(name, pattern)); + } + + *toolInfos(): Iterable { + for (const tool of this.builtinTools.values()) { + yield { + name: tool.name, + description: tool.description, + active: this.enabledTools.has(tool.name), + source: 'builtin', + }; + } + for (const tool of this.userTools.values()) { + yield { + name: tool.name, + description: tool.description, + active: this.enabledTools.has(tool.name), + source: 'user', + }; + } + for (const entry of this.mcpTools.values()) { + yield { + name: entry.tool.name, + description: entry.tool.description, + active: this.isMcpToolEnabled(entry.tool.name), + source: 'mcp', + }; + } + } + + data(): readonly ToolInfo[] { + return Array.from(this.toolInfos()); + } + + storeData(): Readonly> { + return { ...this.store }; + } + + initializeBuiltinTools(): void { + const { + runtime: { kaos, osEnv, urlFetcher, webSearcher }, + config: { cwd, provider, modelCapabilities }, + background, + } = this.agent; + const videoUploader = this.createVideoUploader(provider); + const workspace = extendWorkspaceWithSkillRoots( + { + workspaceDir: cwd, + additionalDirs: [], + }, + this.agent.skills?.registry.getSkillRoots() ?? [], + ); + const allowBackground = + this.enabledTools.has('TaskList') && + this.enabledTools.has('TaskOutput') && + this.enabledTools.has('TaskStop'); + this.builtinTools = new Map( + [ + new b.ReadTool(kaos, workspace), + new b.WriteTool(kaos, workspace), + new b.EditTool(kaos, workspace), + new b.GrepTool(kaos, workspace), + new b.GlobTool(kaos, workspace), + new b.BashTool(kaos, cwd, osEnv, background, { + allowBackground, + }), + (modelCapabilities.image_in || modelCapabilities.video_in) && + new b.ReadMediaFileTool(kaos, workspace, modelCapabilities, videoUploader), + new b.EnterPlanModeTool(this.agent), + new b.ExitPlanModeTool(this.agent), + new b.AskUserQuestionTool(this.agent), + new b.TodoListTool(this.toolStore), + new b.TaskListTool(background), + new b.TaskOutputTool(background), + new b.TaskStopTool(background), + this.agent.skills !== undefined && + this.agent.skills.registry.listInvocableSkills().length > 0 && + new b.SkillTool(this.agent), + this.agent.subagentHost && + new b.AgentTool( + this.agent.subagentHost, + background, + DEFAULT_AGENT_PROFILES['agent']?.subagents, + { + allowBackground, + log: this.agent.log, + }, + ), + webSearcher && new b.WebSearchTool(webSearcher), + urlFetcher && new b.FetchURLTool(urlFetcher), + ] + .filter((tool) => !!tool) + .map((tool) => [tool.name, tool] as const), + ); + } + + private createVideoUploader(provider: ChatProvider): b.VideoUploader | undefined { + const uploadVideo = provider.uploadVideo?.bind(provider); + if (uploadVideo === undefined) return undefined; + + const modelAlias = this.agent.config.modelAlias; + const resolveAuth = + modelAlias === undefined + ? undefined + : this.agent.providerManager?.createAuthResolverForModel(modelAlias, { + log: this.agent.log, + }); + return (input) => withProviderRequestAuth(resolveAuth, (auth) => uploadVideo(input, { auth })); + } + + get loopTools(): readonly ExecutableTool[] { + const mcpNames = [...this.mcpTools.keys()].filter((name) => this.isMcpToolEnabled(name)); + return uniq([...this.enabledTools, ...mcpNames]) + .toSorted((a, b) => a.localeCompare(b)) + .map( + (name) => + this.userTools.get(name) ?? this.mcpTools.get(name)?.tool ?? this.builtinTools.get(name), + ) + .filter((tool) => !!tool); + } +} diff --git a/packages/agent-core/src/agent/tool/types.ts b/packages/agent-core/src/agent/tool/types.ts new file mode 100644 index 000000000..08a822d85 --- /dev/null +++ b/packages/agent-core/src/agent/tool/types.ts @@ -0,0 +1,31 @@ +import type { ExecutableTool } from '../../loop'; + +export type ToolSource = 'builtin' | 'user' | 'mcp'; + +export type BuiltinTool = ExecutableTool; + +export interface UserToolRegistration { + readonly name: string; + readonly description: string; + readonly parameters: Record; +} + +export interface ToolInfo { + readonly name: string; + readonly description: string; + readonly active: boolean; + readonly source: ToolSource; +} + +export interface McpToolCollision { + readonly qualified: string; + readonly toolName: string; + readonly collidesWith: + | { readonly kind: 'same_server'; readonly toolName: string } + | { readonly kind: 'other_server'; readonly serverName: string }; +} + +export interface McpServerRegistrationResult { + readonly registered: readonly string[]; + readonly collisions: readonly McpToolCollision[]; +} diff --git a/packages/agent-core/src/agent/turn/canonical-args.ts b/packages/agent-core/src/agent/turn/canonical-args.ts new file mode 100644 index 000000000..2753af2f0 --- /dev/null +++ b/packages/agent-core/src/agent/turn/canonical-args.ts @@ -0,0 +1,28 @@ +/** + * JSON canonicalization used by tool-call telemetry and dedup. + * Recursively sorts object keys so semantically-equal args produce identical keys. + */ +export function canonicalTelemetryArgs(args: unknown): string { + const json = JSON.stringify(sortJsonValue(args)); + return json ?? String(args); +} + +function sortJsonValue(value: unknown): unknown { + if (Array.isArray(value)) { + return value.map(sortJsonValue); + } + if (!isPlainRecord(value)) { + return value; + } + const out: Record = {}; + for (const key of Object.keys(value).toSorted()) { + out[key] = sortJsonValue(value[key]); + } + return out; +} + +export function isPlainRecord(value: unknown): value is Record { + if (value === null || typeof value !== 'object') return false; + const proto = Object.getPrototypeOf(value); + return proto === Object.prototype || proto === null; +} diff --git a/packages/agent-core/src/agent/turn/index.ts b/packages/agent-core/src/agent/turn/index.ts new file mode 100644 index 000000000..1b14b2a3f --- /dev/null +++ b/packages/agent-core/src/agent/turn/index.ts @@ -0,0 +1,858 @@ +import { createHash } from 'node:crypto'; + +import { + APIConnectionError, + APIContextOverflowError, + APIEmptyResponseError, + APIStatusError, + APITimeoutError, + inputTotal, + type ContentPart, + type TokenUsage, +} from '@moonshot-ai/kosong'; + +import type { Agent } from '..'; +import { + ErrorCodes, + type KimiErrorPayload, + isKimiError, + makeErrorPayload, + toKimiErrorPayload, +} from '#/errors'; +import { isAbortError, isMaxStepsExceededError } from '../../loop/errors'; +import { + createLoopEventDispatcher, + runTurn, + type ExecutableToolResult, + type LoopEvent, + type LoopRecordedEvent, + type LoopTurnInterruptedEvent, + type LoopTurnStopReason, +} from '../../loop/index'; +import type { AgentEvent, TurnEndedEvent } from '../../rpc'; +import type { TelemetryPropertyValue } from '../../telemetry'; +import { abortable } from '../../utils/abort'; +import { resolveCompletionBudget } from '../../utils/completion-budget'; +import { USER_PROMPT_ORIGIN, type PromptOrigin } from '../context'; +import { renderUserPromptHookBlockResult, renderUserPromptHookResult } from '../hooks'; +import { canonicalTelemetryArgs, isPlainRecord } from './canonical-args'; +import { KosongLLM } from './kosong-llm'; +import { ToolCallDeduplicator } from './tool-dedup'; + +interface ActiveTurn { + controller: AbortController; + promise: Promise; +} + +interface BufferedSteer { + readonly input: readonly ContentPart[]; + readonly origin: PromptOrigin; +} + +export interface TurnEndResult { + readonly event: TurnEndedEvent; + readonly stopReason?: LoopTurnStopReason; +} + +const LLM_NOT_SET_MESSAGE = 'LLM not set, send "/login" to login'; + +export class TurnFlow { + private steerBuffer: BufferedSteer[] = []; + private turnId = -1; + private activeTurn: 'resuming' | ActiveTurn | null = null; + private readonly toolCallStartedAt = new Map(); + private readonly toolCallDupType = new Map(); + private readonly stepToolCallKeys = new Map>(); + private readonly telemetryModeByTurn = new Map(); + private readonly currentStepByTurn = new Map(); + private readonly interruptedTelemetryTurnIds = new Set(); + private readonly stepFailureByTurn = new Map(); + private currentStep = 0; + + constructor(protected readonly agent: Agent) {} + + // Returns the new turnId, or null if the turn was marked as resuming. + prompt(input: readonly ContentPart[], origin: PromptOrigin = USER_PROMPT_ORIGIN): number | null { + this.agent.records.logRecord({ + type: 'turn.prompt', + input, + origin, + }); + return this.launch(input, origin); + } + + // Returns the new turnId, or null if the input was buffered as a steer + // message or the turn was marked as resuming. + steer(input: readonly ContentPart[], origin: PromptOrigin = USER_PROMPT_ORIGIN): number | null { + this.agent.records.logRecord({ + type: 'turn.steer', + input, + origin, + }); + if (this.activeTurn) { + this.steerBuffer.push({ input, origin }); + return null; + } + return this.launch(input, origin); + } + + private launch(input: readonly ContentPart[], origin: PromptOrigin): number | null { + if (this.activeTurn) { + this.agent.emitEvent({ + type: 'error', + ...makeErrorPayload( + 'turn.agent_busy', + `Cannot launch a new turn while another turn (ID ${this.turnId}) is active`, + { details: { turnId: this.turnId } }, + ), + }); + return null; + } + + this.turnId += 1; + this.currentStep = 0; + this.stepToolCallKeys.clear(); + this.toolCallDupType.clear(); + const telemetryMode = this.telemetryMode(); + this.telemetryModeByTurn.set(this.turnId, telemetryMode); + this.currentStepByTurn.set(this.turnId, 0); + this.agent.telemetry.track('turn_started', { mode: telemetryMode }); + this.agent.fullCompaction.resetForTurn(); + this.agent.usage.beginTurn(); + this.agent.emitEvent({ + type: 'turn.started', + turnId: this.turnId, + origin, + }); + this.agent.context.appendUserMessage(input, origin); + const controller = new AbortController(); + const promise = this.turnWorker(this.turnId, input, origin, controller.signal); + this.activeTurn = { controller, promise }; + return this.turnId; + } + + restorePrompt(): void { + if (this.activeTurn) { + return; + } + this.turnId += 1; + this.activeTurn = 'resuming'; + } + + restoreSteer(input: readonly ContentPart[], origin: PromptOrigin): void { + if (this.activeTurn) { + this.steerBuffer.push({ input, origin }); + return; + } + this.turnId += 1; + this.activeTurn = 'resuming'; + } + + cancel(turnId?: number): void { + this.agent.records.logRecord({ type: 'turn.cancel', turnId }); + if (turnId !== undefined && turnId !== this.currentId) { + return; // Ignore cancel for non-active turn + } + this.abortTurn(); + this.agent.subagentHost?.cancelAll(); + } + + get currentId() { + return this.turnId; + } + + get hasActiveTurn(): boolean { + return this.activeTurn !== null && this.activeTurn !== 'resuming'; + } + + waitForCurrentTurn(signal?: AbortSignal | undefined): Promise { + const active = this.activeTurn; + if (active === null || active === 'resuming') { + return Promise.reject(new Error('No active turn')); + } + signal?.throwIfAborted(); + if (signal === undefined) return active.promise; + + const turnId = this.currentId; + const onAbort = (): void => { + this.agent.turn.cancel(turnId); + }; + signal.addEventListener('abort', onAbort, { once: true }); + + return abortable(active.promise, signal).finally(() => { + signal.removeEventListener('abort', onAbort); + }); + } + + private abortTurn() { + if (this.activeTurn !== 'resuming') { + this.activeTurn?.controller.abort(); + } + this.activeTurn = null; + } + + private flushSteerBuffer(): boolean { + const steers = this.steerBuffer; + if (steers.length === 0) return false; + for (const steer of steers) { + this.agent.context.appendUserMessage(steer.input, steer.origin); + } + steers.length = 0; + return true; + } + + finishResume(): void { + if (this.activeTurn === 'resuming') { + this.activeTurn = null; + } + this.steerBuffer.length = 0; + } + + private async turnWorker( + turnId: number, + input: readonly ContentPart[], + origin: PromptOrigin, + signal: AbortSignal, + ): Promise { + const startedAt = Date.now(); + let ended: TurnEndedEvent; + let completedStopReason: LoopTurnStopReason | undefined; + try { + const promptHookEnded = await this.applyUserPromptHook( + turnId, + input, + origin, + signal, + ); + if (promptHookEnded !== undefined) { + ended = promptHookEnded; + } else { + const stopReason = await this.runTurn(turnId, signal); + completedStopReason = stopReason; + ended = { + type: 'turn.ended', + turnId, + reason: stopReason === 'aborted' ? 'cancelled' : 'completed', + }; + this.agent.emitEvent(ended); + } + } catch (error) { + if (isAbortError(error)) { + ended = { + type: 'turn.ended', + turnId, + reason: 'cancelled', + }; + this.agent.emitEvent(ended); + } else { + const summary = summarizeTurnError(error, turnId); + void this.agent.hooks?.fireAndForgetTrigger('StopFailure', { + matcherValue: summary.name, + inputData: { + errorType: summary.name, + errorMessage: summary.message, + }, + }); + ended = { + type: 'turn.ended', + turnId, + reason: 'failed', + error: summary, + }; + this.agent.emitEvent(ended); + this.agent.emitEvent({ + type: 'error', + ...summary, + }); + if (this.shouldTrackApiError(turnId)) { + const classification = classifyApiError(error, summary); + const properties: Record = { + error_type: classification.errorType, + model: this.agent.config.model, + retryable: summary.retryable, + duration_ms: Date.now() - startedAt, + }; + if (classification.statusCode !== undefined) { + properties['status_code'] = classification.statusCode; + } + const inputTokens = currentTurnInputTokens(this.agent.usage.data().currentTurn); + if (inputTokens !== undefined) { + properties['input_tokens'] = inputTokens; + } + this.agent.telemetry.track('api_error', properties); + } + } + } finally { + // The turn may have been aborted and a new turn may have started + if (this.currentId === turnId) { + this.agent.usage.endTurn(); + this.activeTurn = null; + } + } + if (ended.reason !== 'completed') { + this.trackTurnInterrupted(turnId, this.currentStepByTurn.get(turnId) ?? this.currentStep); + } + this.telemetryModeByTurn.delete(turnId); + this.currentStepByTurn.delete(turnId); + this.interruptedTelemetryTurnIds.delete(turnId); + this.stepFailureByTurn.delete(turnId); + return { + event: ended, + stopReason: completedStopReason, + }; + } + + private async applyUserPromptHook( + turnId: number, + input: readonly ContentPart[], + origin: PromptOrigin, + signal: AbortSignal, + ): Promise { + if (origin.kind !== 'user') return undefined; + signal.throwIfAborted(); + const promptHookResults = await this.agent.hooks?.trigger('UserPromptSubmit', { + matcherValue: input, + signal, + inputData: { prompt: input }, + }); + signal.throwIfAborted(); + const blockResult = renderUserPromptHookBlockResult(promptHookResults); + if (blockResult !== undefined) { + this.agent.context.markLastUserPromptBlocked('UserPromptSubmit'); + this.agent.context.appendMessage({ + role: 'assistant', + content: [{ type: 'text', text: blockResult.text }], + toolCalls: [], + origin: { kind: 'hook_result', event: 'UserPromptSubmit', blocked: true }, + }); + this.agent.emitEvent({ + type: 'hook.result', + turnId, + hookEvent: blockResult.event, + content: blockResult.message, + blocked: true, + }); + const ended: TurnEndedEvent = { + type: 'turn.ended', + turnId, + reason: 'completed', + }; + this.agent.emitEvent(ended); + return ended; + } + + const hookResult = renderUserPromptHookResult(promptHookResults); + if (hookResult === undefined) return undefined; + + this.agent.context.appendUserMessage([{ type: 'text', text: hookResult.text }], { + kind: 'hook_result', + event: 'UserPromptSubmit', + }); + this.agent.emitEvent({ + type: 'hook.result', + turnId, + hookEvent: hookResult.event, + content: hookResult.message, + }); + return undefined; + } + + private async runTurn(turnId: number, signal: AbortSignal): Promise { + let stopHookContinuationUsed = false; + const deduper = new ToolCallDeduplicator(); + await this.agent.mcp?.waitForInitialLoad(signal); + while (true) { + signal.throwIfAborted(); + const model = this.agent.config.model; + const provider = this.agent.config.provider.withThinking(this.agent.config.thinkingLevel); + const loopControl = this.agent.providerManager?.config.loopControl; + const completionBudget = resolveCompletionBudget({ + reservedContextSize: loopControl?.reservedContextSize, + }); + + try { + const result = await runTurn({ + turnId: String(turnId), + signal, + llm: new KosongLLM({ + provider, + modelName: model, + systemPrompt: this.agent.config.systemPrompt, + capability: this.agent.config.modelCapabilities, + generate: this.agent.generate, + completionBudget, + }), + buildMessages: () => this.agent.context.messages, + dispatchEvent: this.buildDispatchEvent(turnId), + tools: this.agent.tools.loopTools, + log: this.agent.log, + maxSteps: loopControl?.maxStepsPerTurn, + maxRetryAttempts: loopControl?.maxRetriesPerStep, + hooks: { + beforeStep: async ({ signal: stepSignal }) => { + this.flushSteerBuffer(); + await this.agent.fullCompaction.beforeStep(stepSignal); + await this.agent.injection.inject(); + deduper.beginStep(); + return; + }, + afterStep: async ({ usage }) => { + this.agent.usage.record(model, usage, 'turn'); + await this.agent.fullCompaction.afterStep(); + deduper.endStep(); + }, + // oxlint-disable-next-line no-loop-func -- stop hook continuation state is scoped to this turn. + shouldContinueAfterStop: async ({ signal }) => { + if (this.flushSteerBuffer()) return { continue: true }; + signal.throwIfAborted(); + + // Stop hooks get one continuation; otherwise a hook that always blocks would loop forever. + if (stopHookContinuationUsed) return { continue: false }; + const stopBlock = await this.agent.hooks?.triggerBlock('Stop', { + signal, + inputData: { stopHookActive: stopHookContinuationUsed }, + }); + signal.throwIfAborted(); + if (stopBlock !== undefined) { + stopHookContinuationUsed = true; + this.agent.context.appendUserMessage( + [{ type: 'text', text: stopBlock.reason }], + { + kind: 'system_trigger', + name: 'stop_hook', + }, + ); + return { continue: true }; + } + return { continue: false }; + }, + prepareToolExecution: async (ctx) => { + const cached = deduper.checkSameStep( + ctx.toolCall.id, + ctx.toolCall.function.name, + ctx.args, + ); + if (cached !== null) return { syntheticResult: cached }; + const hookResult = await this.agent.hooks?.triggerBlock('PreToolUse', { + matcherValue: ctx.toolCall.function.name, + signal: ctx.signal, + inputData: { + toolName: ctx.toolCall.function.name, + toolInput: toolInputRecord(ctx.args), + toolCallId: ctx.toolCall.id, + }, + }); + ctx.signal.throwIfAborted(); + if (hookResult) { + this.agent.telemetry.track('hook_triggered', { + event_type: 'PreToolUse', + action: hookResult.block ? 'block' : 'allow', + }); + return hookResult; + } + const permissionResult = await this.agent.permission.beforeToolCall(ctx); + this.agent.telemetry.track('hook_triggered', { + event_type: 'PreToolUse', + action: permissionResult?.block === true ? 'block' : 'allow', + }); + return permissionResult; + }, + finalizeToolResult: async (ctx) => { + // Resolve dedup BEFORE firing the PostToolUse hook so same-step + // dups (whose ctx.result is the dedup placeholder) report the + // original's real outcome, not an empty success. + const finalResult = await deduper.finalizeResult( + ctx.toolCall.id, + ctx.toolCall.function.name, + ctx.args, + ctx.result, + ); + const { isError, output } = finalResult; + const event = isError === true ? 'PostToolUseFailure' : 'PostToolUse'; + void this.agent.hooks?.fireAndForgetTrigger(event, { + matcherValue: ctx.toolCall.function.name, + inputData: { + toolName: ctx.toolCall.function.name, + toolInput: toolInputRecord(ctx.args), + toolCallId: ctx.toolCall.id, + error: isError === true ? toKimiErrorPayload(toolOutputText(output)) : undefined, + toolOutput: isError === true ? undefined : toolOutputText(output).slice(0, 2000), + }, + }); + return finalResult; + }, + }, + }); + + return result.stopReason; + } catch (error) { + if ( + error instanceof APIContextOverflowError || + (isKimiError(error) && error.code === ErrorCodes.CONTEXT_OVERFLOW) + ) { + await this.agent.fullCompaction.handleOverflowError(signal, error); + continue; // Retry with compacted context + } + if (isMaxStepsExceededError(error)) { + this.agent.log.warn('turn hit max steps', { + turnId, + steps: this.currentStepByTurn.get(turnId) ?? this.currentStep, + limit: isKimiError(error) ? error.details?.['maxSteps'] : undefined, + }); + } else { + this.agent.log.error('turn failed', { turnId, error }); + } + throw error; + } + } + } + + private buildDispatchEvent(turnId: number) { + return createLoopEventDispatcher({ + appendTranscriptRecord: async (event: LoopRecordedEvent) => { + this.agent.context.appendLoopEvent(event); + }, + emitLiveEvent: (event: LoopEvent) => { + this.trackLoopTelemetry(event, turnId); + const mapped = mapLoopEvent(event, turnId); + if (mapped !== undefined) this.agent.emitEvent(mapped); + }, + }); + } + + private trackLoopTelemetry(event: LoopEvent, turnId: number): void { + if (event.type === 'step.begin') { + this.beginTrackedStep(turnId, event.step); + return; + } + if (event.type === 'turn.interrupted') { + if (event.reason === 'error' && event.activeStep !== undefined) { + this.stepFailureByTurn.set(turnId, event); + } + this.trackTurnInterrupted(turnId, interruptedStep(event)); + return; + } + this.trackToolLifecycle(event, turnId); + } + + private beginTrackedStep(turnId: number, step: number): void { + this.currentStepByTurn.set(turnId, step); + this.currentStep = step; + if (!this.stepToolCallKeys.has(step)) { + this.stepToolCallKeys.set(step, new Set()); + } + } + + private trackToolLifecycle(event: LoopEvent, turnId: number): void { + if (event.type === 'tool.call') { + const dupType = this.trackDuplicateToolCall(turnId, event.step, event.name, event.args); + this.toolCallDupType.set( + event.toolCallId, + dupType === 'cross_step' ? 'cross_step' : 'normal', + ); + this.toolCallStartedAt.set(event.toolCallId, { + name: event.name, + startedAt: Date.now(), + }); + return; + } + if (event.type === 'tool.result') { + const started = this.toolCallStartedAt.get(event.toolCallId); + if (started === undefined) return; + this.toolCallStartedAt.delete(event.toolCallId); + const dupType = this.toolCallDupType.get(event.toolCallId) ?? 'normal'; + this.toolCallDupType.delete(event.toolCallId); + const outcome = telemetryToolOutcome(event.result); + const properties: Record = { + tool_name: started.name, + outcome, + duration_ms: Date.now() - started.startedAt, + dup_type: dupType, + }; + const errorType = outcome === 'error' ? telemetryToolErrorType(event.result) : undefined; + if (errorType !== undefined) { + properties['error_type'] = errorType; + } + this.agent.telemetry.track('tool_call', properties); + } + } + + private trackDuplicateToolCall( + turnId: number, + step: number, + toolName: string, + args: unknown, + ): 'normal' | 'same_step' | 'cross_step' { + const argsText = canonicalTelemetryArgs(args); + const key = `${toolName}\u0000${argsText}`; + const stepKeys = this.stepToolCallKeys.get(step) ?? new Set(); + this.stepToolCallKeys.set(step, stepKeys); + + let dupType: 'same_step' | 'cross_step' | undefined; + if (stepKeys.has(key)) { + dupType = 'same_step'; + } else if (this.hasPriorStepToolCallKey(step, key)) { + dupType = 'cross_step'; + } + + stepKeys.add(key); + if (dupType === undefined) return 'normal'; + + this.agent.telemetry.track('tool_call_dedup_detected', { + turn_id: turnId, + step_no: step, + tool_name: toolName, + dup_type: dupType, + args_hash: createHash('sha256').update(argsText).digest('hex').slice(0, 8), + }); + return dupType; + } + + private hasPriorStepToolCallKey(step: number, key: string): boolean { + for (const [seenStep, keys] of this.stepToolCallKeys) { + if (seenStep !== step && keys.has(key)) return true; + } + return false; + } + + private trackTurnInterrupted(turnId: number, atStep: number): void { + if (this.interruptedTelemetryTurnIds.has(turnId)) return; + this.interruptedTelemetryTurnIds.add(turnId); + this.agent.telemetry.track('turn_interrupted', { + mode: this.telemetryModeByTurn.get(turnId) ?? this.telemetryMode(), + at_step: atStep, + }); + } + + private telemetryMode(): 'agent' | 'plan' { + return this.agent.planMode.isActive ? 'plan' : 'agent'; + } + + private shouldTrackApiError(turnId: number): boolean { + const failure = this.stepFailureByTurn.get(turnId); + return failure?.reason === 'error' && failure.activeStep !== undefined; + } +} + +function mapLoopEvent(event: LoopEvent, turnId: number): AgentEvent | undefined { + switch (event.type) { + case 'step.begin': + return { + type: 'turn.step.started', + turnId, + step: event.step, + stepId: event.uuid, + }; + case 'step.end': + return { + type: 'turn.step.completed', + turnId, + step: event.step, + stepId: event.uuid, + usage: event.usage, + finishReason: event.finishReason, + providerFinishReason: event.providerFinishReason, + rawFinishReason: event.rawFinishReason, + }; + case 'step.retrying': + return { + type: 'turn.step.retrying', + turnId, + step: event.step, + stepId: event.stepUuid, + failedAttempt: event.failedAttempt, + nextAttempt: event.nextAttempt, + maxAttempts: event.maxAttempts, + delayMs: event.delayMs, + errorName: event.errorName, + errorMessage: event.errorMessage, + statusCode: event.statusCode, + }; + case 'content.part': + return undefined; + case 'tool.call': + return { + type: 'tool.call.started', + turnId, + toolCallId: event.toolCallId, + name: event.name, + args: event.args, + description: event.description, + display: event.display, + }; + case 'tool.result': + return { + type: 'tool.result', + turnId, + toolCallId: event.toolCallId, + output: event.result.output, + isError: event.result.isError, + }; + case 'turn.interrupted': + if (event.activeStep === undefined) return undefined; + return { + type: 'turn.step.interrupted', + turnId, + step: event.activeStep, + reason: event.reason, + message: event.message, + }; + case 'text.delta': + return { + type: 'assistant.delta', + turnId, + delta: event.delta, + }; + case 'thinking.delta': + return { + type: 'thinking.delta', + turnId, + delta: event.delta, + }; + case 'tool.call.delta': + return { + type: 'tool.call.delta', + turnId, + toolCallId: event.toolCallId, + name: event.name, + argumentsPart: event.argumentsPart, + }; + case 'tool.progress': + return { + type: 'tool.progress', + turnId, + toolCallId: event.toolCallId, + update: event.update, + }; + } +} + +function summarizeTurnError(error: unknown, turnId: number): KimiErrorPayload { + const payload = toKimiErrorPayload(error); + const details = { ...payload.details, turnId }; + + // Substitute a friendlier TUI-aware message for model-not-configured. + // Raw kosong text ("Model not set" / "Provider not set") is not + // actionable; this string points the user at the login flow. + if (payload.code === 'model.not_configured') { + return { ...payload, message: LLM_NOT_SET_MESSAGE, details }; + } + + return { ...payload, details }; +} + +function toolInputRecord(args: unknown): Record { + return isPlainRecord(args) ? args : {}; +} + +function toolOutputText(output: ExecutableToolResult['output']): string { + if (typeof output === 'string') return output; + return output + .filter((part): part is Extract<(typeof output)[number], { type: 'text' }> => { + return typeof part === 'object' && part !== null && part.type === 'text'; + }) + .map((part) => part.text) + .join(''); +} + +function interruptedStep(event: LoopTurnInterruptedEvent): number { + return event.activeStep ?? event.attemptedSteps; +} + +interface ApiErrorClassification { + readonly errorType: string; + readonly statusCode?: number; +} + +function classifyApiError(error: unknown, summary: KimiErrorPayload): ApiErrorClassification { + const statusCode = apiStatusCode(error) ?? summaryStatusCode(summary); + if (statusCode !== undefined) { + if (statusCode === 429) return { errorType: 'rate_limit', statusCode }; + if (statusCode === 401 || statusCode === 403) return { errorType: 'auth', statusCode }; + if (statusCode >= 500) return { errorType: '5xx_server', statusCode }; + if (isContextOverflowMessage(summary.message)) { + return { errorType: 'context_overflow', statusCode }; + } + if (statusCode >= 400) return { errorType: '4xx_client', statusCode }; + return { errorType: 'api', statusCode }; + } + + if (summary.code === ErrorCodes.PROVIDER_RATE_LIMIT) return { errorType: 'rate_limit' }; + if (summary.code === ErrorCodes.PROVIDER_AUTH_ERROR) return { errorType: 'auth' }; + if (summary.code === ErrorCodes.CONTEXT_OVERFLOW) return { errorType: 'context_overflow' }; + if (isApiConnectionError(error, summary)) return { errorType: 'network' }; + if (isApiTimeoutError(error, summary)) return { errorType: 'timeout' }; + if (isApiEmptyResponseError(error, summary)) return { errorType: 'empty_response' }; + return { errorType: 'other' }; +} + +function apiStatusCode(error: unknown): number | undefined { + if (error instanceof APIStatusError) { + const statusCode = (error as { readonly statusCode?: unknown }).statusCode; + return typeof statusCode === 'number' ? statusCode : undefined; + } + if (typeof error !== 'object' || error === null) return undefined; + const statusCode = (error as { readonly statusCode?: unknown }).statusCode; + if (typeof statusCode === 'number') return statusCode; + const status = (error as { readonly status?: unknown }).status; + return typeof status === 'number' ? status : undefined; +} + +function summaryStatusCode(summary: KimiErrorPayload): number | undefined { + const statusCode = summary.details?.['statusCode']; + return typeof statusCode === 'number' ? statusCode : undefined; +} + +function isApiConnectionError(error: unknown, summary: KimiErrorPayload): boolean { + return error instanceof APIConnectionError || summary.name === 'APIConnectionError'; +} + +function isApiTimeoutError(error: unknown, summary: KimiErrorPayload): boolean { + return ( + error instanceof APITimeoutError || + summary.name === 'APITimeoutError' || + summary.name === 'TimeoutError' + ); +} + +function isApiEmptyResponseError(error: unknown, summary: KimiErrorPayload): boolean { + return error instanceof APIEmptyResponseError || summary.name === 'APIEmptyResponseError'; +} + +function isContextOverflowMessage(message: string): boolean { + const lower = message.toLowerCase(); + return ( + lower.includes('context length') || + lower.includes('context_length') || + lower.includes('max tokens') || + lower.includes('maximum context') || + lower.includes('too many tokens') + ); +} + +function currentTurnInputTokens(usage: TokenUsage | undefined): number | undefined { + if (usage === undefined) return undefined; + return inputTotal(usage); +} + +type ToolTelemetryResult = Extract['result']; + +function telemetryToolOutcome(result: ToolTelemetryResult): 'success' | 'error' | 'cancelled' { + if (result.isError !== true) return 'success'; + const text = toolResultText(result).toLowerCase(); + return text.includes('aborted') || text.includes('cancelled') ? 'cancelled' : 'error'; +} + +function telemetryToolErrorType(result: ToolTelemetryResult): string { + const text = toolResultText(result); + if (text.startsWith('Tool "') && text.includes('" not found')) return 'ToolNotFound'; + if (text.startsWith('Invalid args for tool "')) return 'ToolInputError'; + if (text.includes('prepareToolExecution hook failed')) return 'HookError'; + if (text.includes('finalizeToolResult hook failed')) return 'HookError'; + if (text.includes('blocked')) return 'ToolBlocked'; + return 'ToolError'; +} + +function toolResultText(result: ToolTelemetryResult): string { + return toolOutputText(result.output); +} diff --git a/packages/agent-core/src/agent/turn/kosong-llm.ts b/packages/agent-core/src/agent/turn/kosong-llm.ts new file mode 100644 index 000000000..834fa4712 --- /dev/null +++ b/packages/agent-core/src/agent/turn/kosong-llm.ts @@ -0,0 +1,254 @@ +/** + * Kosong-backed implementation of the loop `LLM` interface. + * + * Bridges the new `loop/llm.ts` contract onto + * the kosong `generate()` streaming API: + * + * - kosong's per-part `onMessagePart` is forwarded to loop per-delta + * callbacks (`onTextDelta`, `onThinkDelta`, `onToolCallDelta`). + * - loop per-block callbacks (`onTextPart`, `onThinkPart`) only fire + * after the kosong stream drains, iterating over the merged + * `result.message.content`. Completed + * blocks land on the WAL seam, raw deltas never do. + * - kosong's finish reasons are preserved as provider diagnostics. The loop + * derives loop control from the normalized response shape, not from the + * provider's finish-reason spelling. + */ + +import { + APIConnectionError, + APIEmptyResponseError, + APIStatusError, + APITimeoutError, + emptyUsage, + generate as kosongGenerate, + type ChatProvider, + type GenerateCallbacks, + type Message, + type ModelCapability, + type StreamedMessagePart, +} from '@moonshot-ai/kosong'; + +import type { LLM, LLMChatParams, LLMChatResponse, LLMRequestLogContext } from '../../loop'; +import { + applyCompletionBudget, + type CompletionBudget, +} from '../../utils/completion-budget'; + +export const GENERATE_REQUEST_LOG_CONTEXT = '__kimiRequestLogContext'; + +export type GenerateOptionsWithRequestLog = { + readonly signal?: AbortSignal; + readonly [GENERATE_REQUEST_LOG_CONTEXT]?: LLMRequestLogContext; +}; + +export type GenerateFn = typeof kosongGenerate; + +export interface KosongLLMConfig { + readonly provider: ChatProvider; + readonly modelName: string; + readonly systemPrompt: string; + readonly capability?: ModelCapability | undefined; + /** + * Optional override for the kosong `generate()` entry point. Lets the + * agent host (and its test harness) inject a scripted generator without + * having to substitute the entire LLM implementation. + */ + readonly generate?: GenerateFn | undefined; + /** + * Per-request completion-token budget. When set, each `chat()` call + * clones the configured provider with a clamped `max_completion_tokens` + * derived from the current input size and model context window. The + * clone is local to the call and never replaces `this.provider`. + */ + readonly completionBudget?: CompletionBudget | undefined; +} + +export class KosongLLM implements LLM { + readonly systemPrompt: string; + readonly modelName: string; + readonly capability?: ModelCapability | undefined; + + private readonly provider: ChatProvider; + private readonly generate: GenerateFn; + private readonly completionBudget: CompletionBudget | undefined; + + constructor(config: KosongLLMConfig) { + this.provider = config.provider; + this.modelName = config.modelName; + this.systemPrompt = config.systemPrompt; + this.capability = config.capability; + this.generate = config.generate ?? kosongGenerate; + this.completionBudget = config.completionBudget; + } + + async chat(params: LLMChatParams): Promise { + return this.chatOnce(params); + } + + private async chatOnce(params: LLMChatParams): Promise { + const callbacks = buildKosongCallbacks(params); + + // Compute and apply the per-request completion budget against a + // throwaway shallow clone. `effectiveProvider` is local to this call + // and never written back to `this.provider`, so retries (handled at + // a higher layer) keep using the same long-lived provider/client. + // The clamp must see every input the provider will serialize on the + // wire — system prompt and tool schemas included — or a near-full + // context can still slip past the limit. + const effectiveProvider = applyCompletionBudget({ + provider: this.provider, + budget: this.completionBudget, + capability: this.capability, + messages: params.messages, + systemPrompt: this.systemPrompt, + tools: params.tools, + }); + + const result = await this.generate( + effectiveProvider, + this.systemPrompt, + [...params.tools], + [...params.messages], + callbacks, + generateOptions(params), + ); + + // Replay merged content parts onto loop per-block callbacks after the + // stream drained. This preserves WAL append order and stops partial + // parts from landing if the upstream stream aborts mid-message. + if (params.onTextPart !== undefined || params.onThinkPart !== undefined) { + for (const part of result.message.content) { + if (part.type === 'text' && params.onTextPart !== undefined) { + await params.onTextPart(part); + } else if (part.type === 'think' && params.onThinkPart !== undefined) { + await params.onThinkPart(part); + } + } + } + + const response: LLMChatResponse = { + toolCalls: [...result.message.toolCalls], + ...(result.finishReason !== null ? { providerFinishReason: result.finishReason } : {}), + ...(result.rawFinishReason !== null ? { rawFinishReason: result.rawFinishReason } : {}), + usage: result.usage ?? emptyUsage(), + }; + + return response; + } + + isRetryableError(error: unknown): boolean { + if (error instanceof APIConnectionError || error instanceof APITimeoutError) { + return true; + } + if (error instanceof APIEmptyResponseError) { + return true; + } + return error instanceof APIStatusError && [429, 500, 502, 503, 504].includes(error.statusCode); + } +} + +function generateOptions(params: LLMChatParams): GenerateOptionsWithRequestLog { + const options: GenerateOptionsWithRequestLog = { + signal: params.signal, + }; + if (params.requestLogContext !== undefined) { + return { + ...options, + [GENERATE_REQUEST_LOG_CONTEXT]: params.requestLogContext, + }; + } + return options; +} + +function buildKosongCallbacks(params: LLMChatParams): GenerateCallbacks { + type ToolCallIdentity = { readonly toolCallId: string; readonly name: string }; + type BufferedToolCallDelta = { readonly argumentsPart?: string | undefined }; + + const toolCallIdentities = new Map(); + const pendingIndexedToolCallDeltas = new Map(); + let lastToolCallIdentity: ToolCallIdentity | undefined; + + const emitToolCallDelta = (delta: { + toolCallId: string; + name: string; + argumentsPart?: string; + }): void => { + if (params.onToolCallDelta === undefined) return; + params.onToolCallDelta(delta); + }; + + return { + onMessagePart: (part: StreamedMessagePart) => { + if (part.type === 'text') { + if (params.onTextDelta === undefined) return; + params.onTextDelta(part.text); + return; + } + if (part.type === 'think') { + if (params.onThinkDelta === undefined) return; + params.onThinkDelta(part.think); + return; + } + if (part.type === 'function') { + const identity = { toolCallId: part.id, name: part.function.name }; + lastToolCallIdentity = identity; + if (part._streamIndex !== undefined) { + toolCallIdentities.set(part._streamIndex, identity); + } + emitToolCallDelta({ + toolCallId: part.id, + name: part.function.name, + ...(part.function.arguments !== null ? { argumentsPart: part.function.arguments } : {}), + }); + if (part._streamIndex !== undefined) { + const pendingDeltas = pendingIndexedToolCallDeltas.get(part._streamIndex); + if (pendingDeltas !== undefined) { + pendingIndexedToolCallDeltas.delete(part._streamIndex); + for (const delta of pendingDeltas) { + emitToolCallDelta({ + toolCallId: identity.toolCallId, + name: identity.name, + ...delta, + }); + } + } + } + return; + } + if (part.type === 'tool_call_part') { + const argumentsPart = part.argumentsPart; + const delta = argumentsPart !== null ? { argumentsPart } : {}; + if (part.index !== undefined) { + const identity = toolCallIdentities.get(part.index); + if (identity === undefined) { + const pendingDeltas = pendingIndexedToolCallDeltas.get(part.index) ?? []; + pendingDeltas.push(delta); + pendingIndexedToolCallDeltas.set(part.index, pendingDeltas); + return; + } + emitToolCallDelta({ + toolCallId: identity.toolCallId, + name: identity.name, + ...delta, + }); + return; + } + const identity = lastToolCallIdentity; + if (identity === undefined) return; + emitToolCallDelta({ + toolCallId: identity.toolCallId, + name: identity.name, + ...delta, + }); + } + }, + }; +} + +export function buildMessagesWithSystem(systemPrompt: string, history: Message[]): Message[] { + return [ + { role: 'system', content: [{ type: 'text', text: systemPrompt }], toolCalls: [] }, + ...history, + ]; +} diff --git a/packages/agent-core/src/agent/turn/tool-dedup.ts b/packages/agent-core/src/agent/turn/tool-dedup.ts new file mode 100644 index 000000000..c360589aa --- /dev/null +++ b/packages/agent-core/src/agent/turn/tool-dedup.ts @@ -0,0 +1,195 @@ +import type { ContentPart } from '@moonshot-ai/kosong'; + +import type { ExecutableToolResult } from '../../loop/types'; + +import { canonicalTelemetryArgs } from './canonical-args'; + +const CROSS_STEP_DEDUP_TRIGGER_COUNT = 7; + +const REMINDER_TEXT = + '\n\n\n' + + 'You are repeating the exact same tool call with identical parameters.' + + ' Please carefully analyze the previous result. If the task is not yet complete,' + + ' try a different method or parameters instead of repeating the same call.' + + '\n'; + +interface Deferred { + readonly promise: Promise; + resolve(value: T): void; +} + +function makeDeferred(): Deferred { + let resolve!: (value: T) => void; + const promise = new Promise((r) => { + resolve = r; + }); + return { promise, resolve }; +} + +function makeKey(toolName: string, args: unknown): string { + return `${toolName} ${canonicalTelemetryArgs(args)}`; +} + +function appendReminder(result: ExecutableToolResult): ExecutableToolResult { + const output = result.output; + let newOutput: string | ContentPart[]; + if (typeof output === 'string') { + newOutput = output + REMINDER_TEXT; + } else { + const arr: ContentPart[] = [...output]; + const last = arr.at(-1); + if (last !== undefined && last.type === 'text') { + arr[arr.length - 1] = { type: 'text', text: last.text + REMINDER_TEXT }; + } else { + arr.push({ type: 'text', text: REMINDER_TEXT }); + } + newOutput = arr; + } + return result.isError === true + ? { ...result, output: newOutput, isError: true } + : { ...result, output: newOutput }; +} + +/** + * Placeholder result returned from `checkSameStep` for a duplicate call. Never + * reaches the model — it is replaced in `finalizeResult` by awaiting the + * original's deferred result. The loop dispatches `tool.result` events using + * the finalized value, so this content is purely internal bookkeeping. + * + * It must be a non-error result so `toolResultStopsTurn` in tool-call.ts does + * not short-circuit the batch on the dup's behalf. + */ +const DEDUP_PLACEHOLDER_RESULT: ExecutableToolResult = { output: '' }; + +/** + * Detects and suppresses repetitive tool calls within a single turn. + * + * Two behaviours are layered: + * - Same-step dedup: a duplicate `(toolName, args)` issued in the same LLM step + * reuses the original call's result instead of executing the tool twice. + * - Cross-step dedup: when the exact same call is repeated for + * `CROSS_STEP_DEDUP_TRIGGER_COUNT` consecutive occurrences (counting across + * steps), the result returned to the model is suffixed with a system reminder + * nudging it to try a different approach. + */ +export class ToolCallDeduplicator { + private stepDeferreds = new Map>(); + private stepCalls: string[] = []; + private originalCallIndex = new Map(); + private syntheticCallIds = new Set(); + /** + * Records the dedup key used at `checkSameStep` time, keyed by `toolCallId`. + * The loop is allowed to rewrite args between `prepareToolExecution` and + * `finalizeToolResult` via `PrepareToolExecutionResult.updatedArgs`, so the + * `(toolName, args)` pair available at finalize may differ from what was + * registered. We pin the key at registration time and look it up by call id + * during finalize. + */ + private callKeyByCallId = new Map(); + private consecutiveKey: string | null = null; + private consecutiveCount = 0; + + beginStep(): void { + for (const deferred of this.stepDeferreds.values()) { + deferred.resolve({ + output: 'Tool call deduplicated but original result was lost', + isError: true, + }); + } + this.stepDeferreds.clear(); + this.stepCalls = []; + this.originalCallIndex.clear(); + this.syntheticCallIds.clear(); + this.callKeyByCallId.clear(); + } + + endStep(): void { + for (const key of this.stepCalls) { + if (key === this.consecutiveKey) { + this.consecutiveCount += 1; + } else { + this.consecutiveKey = key; + this.consecutiveCount = 1; + } + } + } + + /** + * Called from `prepareToolExecution`. If this `(toolName, args)` was already + * seen in the current step, returns a placeholder result so the loop can + * skip executing the tool again; the real result is patched in during + * `finalizeResult`. Returns `null` for the first occurrence so the normal + * execution path proceeds. + * + * This method is intentionally synchronous to avoid deadlocking the prepare + * loop on a deferred that only resolves in the finalize phase. + */ + checkSameStep(toolCallId: string, toolName: string, args: unknown): ExecutableToolResult | null { + const key = makeKey(toolName, args); + const index = this.stepCalls.length; + this.stepCalls.push(key); + this.callKeyByCallId.set(toolCallId, key); + + const existing = this.stepDeferreds.get(key); + if (existing !== undefined) { + this.syntheticCallIds.add(toolCallId); + return DEDUP_PLACEHOLDER_RESULT; + } + this.stepDeferreds.set(key, makeDeferred()); + this.originalCallIndex.set(toolCallId, index); + return null; + } + + /** + * Called from `finalizeToolResult`, in provider order. For first-occurrence + * calls, projects the consecutive streak ending at this call and, if the + * threshold is reached, appends the system reminder, then resolves the + * deferred so subsequent same-step dups can fetch the real result. For + * synthetic duplicates, awaits the original's deferred and returns its + * value, discarding the placeholder. + */ + async finalizeResult( + toolCallId: string, + _toolName: string, + _args: unknown, + result: ExecutableToolResult, + ): Promise { + // Use the key recorded at registration time, NOT a fresh key from the args + // passed here — the loop may have rewritten args via updatedArgs. + const key = this.callKeyByCallId.get(toolCallId); + if (key === undefined) return result; + this.callKeyByCallId.delete(toolCallId); + + if (this.syntheticCallIds.delete(toolCallId)) { + const deferred = this.stepDeferreds.get(key); + if (deferred === undefined) return result; + return deferred.promise; + } + const index = this.originalCallIndex.get(toolCallId); + if (index === undefined) return result; + this.originalCallIndex.delete(toolCallId); + + let lastKey = this.consecutiveKey; + let streak = this.consecutiveCount; + for (let i = 0; i <= index; i += 1) { + const k = this.stepCalls[i]!; + if (k === lastKey) { + streak += 1; + } else { + lastKey = k; + streak = 1; + } + } + + const finalResult = + streak >= CROSS_STEP_DEDUP_TRIGGER_COUNT ? appendReminder(result) : result; + + this.stepDeferreds.get(key)?.resolve(finalResult); + return finalResult; + } +} + +export const __testing = { + CROSS_STEP_DEDUP_TRIGGER_COUNT, + REMINDER_TEXT, +}; diff --git a/packages/agent-core/src/agent/usage/index.ts b/packages/agent-core/src/agent/usage/index.ts new file mode 100644 index 000000000..e978e7d56 --- /dev/null +++ b/packages/agent-core/src/agent/usage/index.ts @@ -0,0 +1,79 @@ +import type { UsageStatus } from '#/rpc'; +import { addUsage, type TokenUsage } from '@moonshot-ai/kosong'; + +import type { Agent } from '..'; + +export type UsageRecordScope = 'session' | 'turn'; + +function copyUsage(usage: TokenUsage): TokenUsage { + return { ...usage }; +} + +export class UsageRecorder { + private readonly byModel: Record = {}; + private currentTurn: TokenUsage | undefined; + + constructor(protected readonly agent?: Agent) {} + + beginTurn(): void { + this.currentTurn = undefined; + } + + endTurn(): void { + this.currentTurn = undefined; + } + + record(model: string, usage: TokenUsage, scope: UsageRecordScope = 'session'): void { + this.agent?.records.logRecord({ + type: 'usage.record', + model, + usage, + usageScope: scope, + }); + const current = this.byModel[model]; + this.byModel[model] = current === undefined ? copyUsage(usage) : addUsage(current, usage); + + if (scope === 'turn') { + this.currentTurn = + this.currentTurn === undefined ? copyUsage(usage) : addUsage(this.currentTurn, usage); + } + this.agent?.emitStatusUpdated(); + } + + data(): UsageStatus { + const byModel = this.byModelSnapshot(); + const hasByModel = Object.keys(byModel).length > 0; + const currentTurn = this.currentTurn; + return { + byModel: hasByModel ? byModel : undefined, + total: hasByModel ? totalUsage(byModel) : undefined, + currentTurn: currentTurn === undefined ? undefined : copyUsage(currentTurn), + }; + } + + status(): UsageStatus | undefined { + const status = this.data(); + if ( + status.byModel === undefined && + status.total === undefined && + status.currentTurn === undefined + ) { + return undefined; + } + return status; + } + + private byModelSnapshot(): Record { + return Object.fromEntries( + Object.entries(this.byModel).map(([model, usage]) => [model, copyUsage(usage)]), + ); + } +} + +function totalUsage(byModel: Record): TokenUsage | undefined { + let total: TokenUsage | undefined; + for (const usage of Object.values(byModel)) { + total = total === undefined ? copyUsage(usage) : addUsage(total, usage); + } + return total; +} diff --git a/packages/agent-core/src/config/index.ts b/packages/agent-core/src/config/index.ts new file mode 100644 index 000000000..e9b03330f --- /dev/null +++ b/packages/agent-core/src/config/index.ts @@ -0,0 +1,5 @@ +export * from './merge'; +export * from './path'; +export * from './resolve'; +export * from './schema'; +export * from './toml'; diff --git a/packages/agent-core/src/config/merge.ts b/packages/agent-core/src/config/merge.ts new file mode 100644 index 000000000..ce4880b96 --- /dev/null +++ b/packages/agent-core/src/config/merge.ts @@ -0,0 +1,62 @@ +import { ErrorCodes, KimiError } from '#/errors'; +import { + KimiConfigPatchSchema, + formatConfigValidationError, + type KimiConfig, + type KimiConfigPatch, + validateConfig, +} from '#/config/schema'; + +export function mergeConfigPatch(config: KimiConfig, patch: KimiConfigPatch): KimiConfig { + const base = validateConfig(config); + const parsedPatch = parsePatch(patch); + const merged = deepMerge(base, parsedPatch); + return validateConfig(merged); +} + +function parsePatch(patch: KimiConfigPatch): KimiConfigPatch { + try { + return stripUndefinedDeep(KimiConfigPatchSchema.parse(patch)) as KimiConfigPatch; + } catch (error) { + throw new KimiError(ErrorCodes.CONFIG_INVALID, `Invalid configuration patch: ${formatConfigValidationError(error)}`, { + cause: error, + }); + } +} + +function deepMerge( + target: Record, + source: Record, +): Record { + const result = { ...target }; + for (const [key, sourceValue] of Object.entries(source)) { + if (sourceValue === undefined) continue; + const targetValue = result[key]; + if (isPlainObject(targetValue) && isPlainObject(sourceValue)) { + result[key] = deepMerge(targetValue, sourceValue); + } else { + result[key] = sourceValue; + } + } + return result; +} + +function stripUndefinedDeep(value: unknown): unknown { + if (Array.isArray(value)) { + return value.map(stripUndefinedDeep); + } + if (!isPlainObject(value)) { + return value; + } + const out: Record = {}; + for (const [key, entryValue] of Object.entries(value)) { + if (entryValue !== undefined) { + out[key] = stripUndefinedDeep(entryValue); + } + } + return out; +} + +function isPlainObject(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} diff --git a/packages/agent-core/src/config/path.ts b/packages/agent-core/src/config/path.ts new file mode 100644 index 000000000..d8d80f45b --- /dev/null +++ b/packages/agent-core/src/config/path.ts @@ -0,0 +1,18 @@ +import { mkdirSync } from 'node:fs'; +import { homedir } from 'node:os'; +import { join } from 'node:path'; + +export function resolveKimiHome(homeDir?: string | undefined): string { + return homeDir ?? process.env['KIMI_CODE_HOME'] ?? join(homedir(), '.kimi-code'); +} + +export function resolveConfigPath(input: { + readonly homeDir?: string | undefined; + readonly configPath?: string | undefined; +}): string { + return input.configPath ?? join(resolveKimiHome(input.homeDir), 'config.toml'); +} + +export function ensureKimiHome(homeDir: string): void { + mkdirSync(homeDir, { recursive: true, mode: 0o700 }); +} diff --git a/packages/agent-core/src/config/resolve.ts b/packages/agent-core/src/config/resolve.ts new file mode 100644 index 000000000..dbe1f4635 --- /dev/null +++ b/packages/agent-core/src/config/resolve.ts @@ -0,0 +1,26 @@ +const TRUE_BOOLEAN_ENV_VALUES = new Set(['1', 'true', 'yes', 'on']); +const FALSE_BOOLEAN_ENV_VALUES = new Set(['0', 'false', 'no', 'off']); + +export interface ResolveConfigValueInput { + readonly env?: Readonly>; + readonly envKey: string; + readonly configValue?: T; + readonly defaultValue: T; + readonly parseEnv: (value: string | undefined) => T | undefined; +} + +export function resolveConfigValue(input: ResolveConfigValueInput): T { + return ( + input.parseEnv(input.env?.[input.envKey]) ?? + input.configValue ?? + input.defaultValue + ); +} + +export function parseBooleanEnv(value: string | undefined): boolean | undefined { + const normalized = value?.trim().toLowerCase(); + if (normalized === undefined || normalized.length === 0) return undefined; + if (TRUE_BOOLEAN_ENV_VALUES.has(normalized)) return true; + if (FALSE_BOOLEAN_ENV_VALUES.has(normalized)) return false; + return undefined; +} diff --git a/packages/agent-core/src/config/schema.ts b/packages/agent-core/src/config/schema.ts new file mode 100644 index 000000000..5c334a564 --- /dev/null +++ b/packages/agent-core/src/config/schema.ts @@ -0,0 +1,279 @@ +import { HOOK_EVENT_TYPES } from '#/agent/hooks/types'; +import { parsePattern } from '#/agent/permission/parse-pattern'; +import { ErrorCodes, KimiError } from '#/errors'; +import { z } from 'zod'; + +export const ProviderTypeSchema = z.enum([ + 'anthropic', + 'openai', + 'kimi', + 'google-genai', + 'openai_responses', + 'vertexai', +]); + +export type ProviderType = z.infer; + +export const OAuthRefSchema = z.object({ + storage: z.enum(['file', 'keyring']), + key: z.string().min(1), +}); + +export type OAuthRef = z.infer; + +const StringRecordSchema = z.record(z.string(), z.string()); + +export const ProviderConfigSchema = z.object({ + type: ProviderTypeSchema, + apiKey: z.string().optional(), + baseUrl: z.string().optional(), + defaultModel: z.string().optional(), + oauth: OAuthRefSchema.optional(), + env: StringRecordSchema.optional(), + customHeaders: StringRecordSchema.optional(), +}); + +export type ProviderConfig = z.infer; + +export const ModelAliasSchema = z.object({ + provider: z.string(), + model: z.string(), + maxContextSize: z.number().int().min(1), + maxOutputSize: z.number().int().min(1).optional(), + capabilities: z.array(z.string()).optional(), + displayName: z.string().optional(), +}); + +export type ModelAlias = z.infer; + +export const ThinkingConfigSchema = z.object({ + mode: z.enum(['auto', 'on', 'off']).optional(), + effort: z.string().optional(), +}); + +export type ThinkingConfig = z.infer; + +export const PermissionModeSchema = z.enum(['yolo', 'manual', 'auto']); + +export const PermissionRuleDecisionSchema = z.enum(['allow', 'deny', 'ask']); +export const PermissionRuleScopeSchema = z.enum([ + 'turn-override', + 'session-runtime', + 'project', + 'user', +]); + +export const PermissionRuleSchema = z.object({ + decision: PermissionRuleDecisionSchema, + scope: PermissionRuleScopeSchema.default('user'), + pattern: z.string().min(1).refine(isValidPermissionPattern, { + message: 'Invalid permission rule pattern', + }), + reason: z.string().optional(), +}); + +export const PermissionConfigSchema = z.object({ + rules: z.array(PermissionRuleSchema).optional(), +}); + +export type PermissionConfig = z.infer; + +export const LoopControlSchema = z.object({ + maxStepsPerTurn: z.number().int().min(1).optional(), + maxRetriesPerStep: z.number().int().min(0).optional(), + maxRalphIterations: z.number().int().min(-1).optional(), + reservedContextSize: z.number().int().min(0).optional(), + compactionTriggerRatio: z.number().min(0.5).max(0.99).optional(), +}); + +export type LoopControl = z.infer; + +export const BackgroundConfigSchema = z.object({ + maxRunningTasks: z.number().int().min(1).optional(), + keepAliveOnExit: z.boolean().optional(), + killGracePeriodMs: z.number().int().min(0).optional(), + agentTaskTimeoutS: z.number().int().min(1).optional(), + printWaitCeilingS: z.number().int().min(1).optional(), +}); + +export type BackgroundConfig = z.infer; + +export const HookDefSchema = z + .object({ + event: z.enum(HOOK_EVENT_TYPES), + matcher: z.string().optional(), + command: z.string().min(1), + timeout: z.number().int().min(1).max(600).optional(), + }) + .strict(); + +export type HookDefConfig = z.infer; + +export const MoonshotServiceConfigSchema = z.object({ + baseUrl: z.string().optional(), + apiKey: z.string().optional(), + oauth: OAuthRefSchema.optional(), + customHeaders: StringRecordSchema.optional(), +}); + +export type MoonshotServiceConfig = z.infer; + +export const ServicesConfigSchema = z.object({ + moonshotSearch: MoonshotServiceConfigSchema.optional(), + moonshotFetch: MoonshotServiceConfigSchema.optional(), +}); + +export type ServicesConfig = z.infer; + +const McpServerCommonFields = { + enabled: z.boolean().optional(), + startupTimeoutMs: z.number().int().min(1).optional(), + toolTimeoutMs: z.number().int().min(1).optional(), + enabledTools: z.array(z.string()).optional(), + disabledTools: z.array(z.string()).optional(), +} as const; + +export const McpServerStdioConfigSchema = z.object({ + transport: z.literal('stdio'), + command: z.string().min(1), + args: z.array(z.string()).optional(), + env: StringRecordSchema.optional(), + cwd: z.string().optional(), + // Reserved for future kaos-backed stdio launchers. `undefined` and `'local'` + // both mean direct child_process spawn for now. + executor: z.enum(['local', 'kaos']).optional(), + ...McpServerCommonFields, +}); + +export type McpServerStdioConfig = z.infer; + +export const McpServerHttpConfigSchema = z.object({ + transport: z.literal('http'), + url: z.string().url(), + headers: StringRecordSchema.optional(), + // Indirect secret reference: the bearer token is looked up from + // `process.env[bearerTokenEnvVar]` at connection time, never committed. + bearerTokenEnvVar: z.string().min(1).optional(), + ...McpServerCommonFields, +}); + +export type McpServerHttpConfig = z.infer; + +const McpServerConfigDiscriminatedSchema = z.discriminatedUnion('transport', [ + McpServerStdioConfigSchema, + McpServerHttpConfigSchema, +]); + +export const McpServerConfigSchema = z.preprocess((raw) => { + if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) return raw; + const obj = raw as Record; + if ('transport' in obj) return obj; + if (typeof obj['command'] === 'string') return { ...obj, transport: 'stdio' }; + if (typeof obj['url'] === 'string') return { ...obj, transport: 'http' }; + return obj; +}, McpServerConfigDiscriminatedSchema); + +export type McpServerConfig = z.infer; + +export const KimiConfigSchema = z.object({ + providers: z.record(z.string(), ProviderConfigSchema).default({}), + defaultProvider: z.string().optional(), + defaultModel: z.string().optional(), + models: z.record(z.string(), ModelAliasSchema).optional(), + thinking: ThinkingConfigSchema.optional(), + planMode: z.boolean().optional(), + yolo: z.boolean().optional(), + defaultThinking: z.boolean().optional(), + defaultPermissionMode: PermissionModeSchema.optional(), + defaultPlanMode: z.boolean().optional(), + permission: PermissionConfigSchema.optional(), + hooks: z.array(HookDefSchema).optional(), + services: ServicesConfigSchema.optional(), + mergeAllAvailableSkills: z.boolean().optional(), + extraSkillDirs: z.array(z.string()).optional(), + loopControl: LoopControlSchema.optional(), + background: BackgroundConfigSchema.optional(), + telemetry: z.boolean().optional(), + raw: z.record(z.string(), z.unknown()).optional(), +}); + +export type KimiConfig = z.infer; + +const ProviderConfigPatchSchema = ProviderConfigSchema.partial(); +const ModelAliasPatchSchema = ModelAliasSchema.partial(); +const ThinkingConfigPatchSchema = ThinkingConfigSchema.partial(); +const PermissionConfigPatchSchema = PermissionConfigSchema.partial(); +const LoopControlPatchSchema = LoopControlSchema.partial(); +const BackgroundConfigPatchSchema = BackgroundConfigSchema.partial(); +const MoonshotServiceConfigPatchSchema = MoonshotServiceConfigSchema.partial(); +const ServicesConfigPatchSchema = z.object({ + moonshotSearch: MoonshotServiceConfigPatchSchema.optional(), + moonshotFetch: MoonshotServiceConfigPatchSchema.optional(), +}); + +export const KimiConfigPatchSchema = z + .object({ + providers: z.record(z.string(), ProviderConfigPatchSchema).optional(), + defaultProvider: z.string().optional(), + defaultModel: z.string().optional(), + models: z.record(z.string(), ModelAliasPatchSchema).optional(), + thinking: ThinkingConfigPatchSchema.optional(), + planMode: z.boolean().optional(), + yolo: z.boolean().optional(), + defaultThinking: z.boolean().optional(), + defaultPermissionMode: PermissionModeSchema.optional(), + defaultPlanMode: z.boolean().optional(), + permission: PermissionConfigPatchSchema.optional(), + hooks: z.array(HookDefSchema).optional(), + services: ServicesConfigPatchSchema.optional(), + mergeAllAvailableSkills: z.boolean().optional(), + extraSkillDirs: z.array(z.string()).optional(), + loopControl: LoopControlPatchSchema.optional(), + background: BackgroundConfigPatchSchema.optional(), + telemetry: z.boolean().optional(), + }) + .strict(); + +export type KimiConfigPatch = z.infer; + +export function getDefaultConfig(): KimiConfig { + return { + providers: {}, + }; +} + +export function validateConfig(config: unknown): KimiConfig { + try { + return KimiConfigSchema.parse(config); + } catch (error) { + throw new KimiError(ErrorCodes.CONFIG_INVALID, `Invalid configuration: ${formatConfigValidationError(error)}`, { + cause: error, + }); + } +} + +export function formatConfigValidationError(error: unknown): string { + const missingModelContextSize = missingModelContextSizeMessage(error); + if (missingModelContextSize !== undefined) return missingModelContextSize; + return error instanceof Error ? error.message : String(error); +} + +function missingModelContextSizeMessage(error: unknown): string | undefined { + if (!(error instanceof z.ZodError)) return undefined; + for (const issue of error.issues) { + const [section, modelName, field] = issue.path; + if (section === 'models' && typeof modelName === 'string' && field === 'maxContextSize') { + return `Model "${modelName}" must define a positive max_context_size in config.toml.`; + } + } + return undefined; +} + +function isValidPermissionPattern(pattern: string): boolean { + try { + parsePattern(pattern); + return true; + } catch { + return false; + } +} diff --git a/packages/agent-core/src/config/toml.ts b/packages/agent-core/src/config/toml.ts new file mode 100644 index 000000000..a049c1de1 --- /dev/null +++ b/packages/agent-core/src/config/toml.ts @@ -0,0 +1,508 @@ +import { existsSync, readFileSync } from 'node:fs'; +import { mkdir, open } from 'node:fs/promises'; +import { dirname } from 'node:path'; + +import { ErrorCodes, KimiError } from '#/errors'; +import { + KimiConfigSchema, + formatConfigValidationError, + getDefaultConfig, + type BackgroundConfig, + type HookDefConfig, + type KimiConfig, + type LoopControl, + type ModelAlias, + type MoonshotServiceConfig, + type OAuthRef, + type PermissionConfig, + type ProviderConfig, + type ServicesConfig, + type ThinkingConfig, + validateConfig, +} from '#/config/schema'; +import { atomicWrite } from '#/utils/fs'; +import { parse as parseToml, stringify as stringifyToml } from 'smol-toml'; + +/* ------------------------------------------------------------------ */ +/* Key helpers – reuse generic snake / camel conversion instead of */ +/* maintaining per-section *_KEY_MAP tables. */ +/* ------------------------------------------------------------------ */ + +function snakeToCamel(str: string): string { + return str.replaceAll(/_([a-z])/g, (_, ch: string) => ch.toUpperCase()); +} + +function camelToSnake(str: string): string { + return str.replaceAll(/[A-Z]/g, (ch: string) => `_${ch.toLowerCase()}`); +} + +/* ------------------------------------------------------------------ */ +/* Read / parse */ +/* ------------------------------------------------------------------ */ + +const DEFAULT_CONFIG_FILE_TEXT = `# ~/.kimi-code/config.toml +# Runtime settings for Kimi Code. +# This file starts empty so built-in defaults can apply. +# Login will populate managed Kimi provider and model entries. +`; + +export async function ensureConfigFile(filePath: string): Promise { + await mkdir(dirname(filePath), { recursive: true, mode: 0o700 }); + let handle: Awaited> | undefined; + try { + handle = await open(filePath, 'wx', 0o600); + await handle.writeFile(DEFAULT_CONFIG_FILE_TEXT, 'utf-8'); + } catch (error) { + if (isFileExistsError(error)) return; + throw error; + } finally { + await handle?.close(); + } +} + +export function readConfigFile(filePath: string): KimiConfig { + if (!existsSync(filePath)) { + return getDefaultConfig(); + } + const text = readFileSync(filePath, 'utf-8'); + return parseConfigString(text, filePath); +} + +export function parseConfigString(tomlText: string, filePath = 'config.toml'): KimiConfig { + if (tomlText.trim().length === 0) { + return getDefaultConfig(); + } + + let data: Record; + try { + data = parseToml(tomlText) as Record; + } catch (error) { + throw new KimiError(ErrorCodes.CONFIG_INVALID, `Invalid TOML in ${filePath}: ${error instanceof Error ? error.message : String(error)}`, { + cause: error, + }); + } + + return parseConfigData(data, filePath); +} + +function parseConfigData(data: Record, filePath: string): KimiConfig { + const raw = cloneRecord(data); + const transformed = transformTomlData(data); + transformed['raw'] = raw; + + try { + return KimiConfigSchema.parse(transformed); + } catch (error) { + throw new KimiError(ErrorCodes.CONFIG_INVALID, `Invalid configuration in ${filePath}: ${formatConfigValidationError(error)}`, { + cause: error, + }); + } +} + +export function transformTomlData(data: Record): Record { + const result: Record = {}; + for (const [key, value] of Object.entries(data)) { + const targetKey = snakeToCamel(key); + + if (targetKey === 'providers' && isPlainObject(value)) { + result[targetKey] = transformRecord(value, transformProviderData); + } else if (targetKey === 'models' && isPlainObject(value)) { + result[targetKey] = transformRecord(value, transformModelData); + } else if (targetKey === 'thinking' && isPlainObject(value)) { + result[targetKey] = transformPlainObject(value); + } else if (targetKey === 'permission' && isPlainObject(value)) { + result[targetKey] = transformPermissionData(value); + } else if (targetKey === 'services' && isPlainObject(value)) { + result[targetKey] = transformRecord(value, transformServiceData, snakeToCamel); + } else if (targetKey === 'loopControl' && isPlainObject(value)) { + result[targetKey] = transformLoopControlData(value); + } else if (targetKey === 'background' && isPlainObject(value)) { + result[targetKey] = transformPlainObject(value); + } else if (!isPlainObject(value)) { + result[targetKey] = value; + } + } + return result; +} + +function transformRecord( + value: Record, + transformEntry: (entry: Record) => Record, + transformName: (name: string) => string = (name) => name, +): Record { + const record: Record = {}; + for (const [entryName, entryConfig] of Object.entries(value)) { + record[transformName(entryName)] = isPlainObject(entryConfig) + ? transformEntry(entryConfig) + : entryConfig; + } + return record; +} + +function transformPlainObject(data: Record): Record { + const out: Record = {}; + for (const [key, value] of Object.entries(data)) { + out[snakeToCamel(key)] = value; + } + return out; +} + +function transformProviderData(data: Record): Record { + const out: Record = {}; + for (const [key, value] of Object.entries(data)) { + const targetKey = snakeToCamel(key); + if (targetKey === 'oauth') { + out[targetKey] = isPlainObject(value) ? transformPlainObject(value) : value; + } else if (targetKey === 'env' || targetKey === 'customHeaders') { + out[targetKey] = cloneObjectValue(value); + } else { + out[targetKey] = value; + } + } + return out; +} + +function transformModelData(data: Record): Record { + return transformPlainObject(data); +} + +function transformPermissionData(data: Record): Record { + const raw = transformPlainObject(data); + const out: Record = {}; + + const rules: unknown[] = []; + appendPermissionRules(rules, raw['rules']); + appendPermissionRules(rules, raw['deny'], 'deny'); + appendPermissionRules(rules, raw['allow'], 'allow'); + appendPermissionRules(rules, raw['ask'], 'ask'); + if (rules.length > 0) { + out['rules'] = rules; + } + return out; +} + +function appendPermissionRules( + target: unknown[], + value: unknown, + decision?: 'allow' | 'deny' | 'ask', +): void { + if (value === undefined) return; + const entries = Array.isArray(value) ? value : [value]; + for (const entry of entries) { + target.push(transformPermissionRule(entry, decision)); + } +} + +function transformPermissionRule(value: unknown, decision?: 'allow' | 'deny' | 'ask'): unknown { + if (!isPlainObject(value)) return value; + + const rule = transformPlainObject(value); + const tool = rule['tool']; + const match = rule['match']; + const pattern = rule['pattern']; + const out: Record = {}; + + if (decision !== undefined) { + out['decision'] = decision; + } else { + out['decision'] = rule['decision']; + } + out['scope'] = rule['scope']; + out['reason'] = rule['reason']; + + if (typeof tool === 'string') { + const argPattern = typeof match === 'string' ? match : pattern; + out['pattern'] = typeof argPattern === 'string' ? `${tool}(${argPattern})` : tool; + } else { + out['pattern'] = pattern; + } + + return out; +} + +function transformServiceData(data: Record): Record { + const out: Record = {}; + for (const [key, value] of Object.entries(data)) { + const targetKey = snakeToCamel(key); + if (targetKey === 'oauth') { + out[targetKey] = isPlainObject(value) ? transformPlainObject(value) : value; + } else if (targetKey === 'customHeaders') { + out[targetKey] = cloneObjectValue(value); + } else { + out[targetKey] = value; + } + } + return out; +} + +function transformLoopControlData(data: Record): Record { + const out = transformPlainObject(data); + if (out['maxStepsPerTurn'] === undefined && out['maxStepsPerRun'] !== undefined) { + out['maxStepsPerTurn'] = out['maxStepsPerRun']; + } + delete out['maxStepsPerRun']; + return out; +} + +/* ------------------------------------------------------------------ */ +/* Write / stringify */ +/* ------------------------------------------------------------------ */ + +export async function writeConfigFile(filePath: string, config: KimiConfig): Promise { + const validated = validateConfig(config); + await mkdir(dirname(filePath), { recursive: true, mode: 0o700 }); + await atomicWrite(filePath, `${stringifyToml(configToTomlData(validated))}\n`); +} + +export function configToTomlData(config: KimiConfig): Record { + const out = cloneRecord(config.raw); + + // Strip deprecated fields + delete out['default_yolo']; + delete out['defaultYolo']; + delete out['defaultPermissionMode']; + + // Top-level scalar fields + const scalarFields: (keyof KimiConfig)[] = [ + 'defaultProvider', + 'defaultModel', + 'planMode', + 'yolo', + 'defaultThinking', + 'defaultPermissionMode', + 'defaultPlanMode', + 'mergeAllAvailableSkills', + 'extraSkillDirs', + 'telemetry', + ]; + for (const key of scalarFields) { + setDefined(out, camelToSnake(key), config[key]); + } + + setRecordSection(out, 'providers', config.providers, providerToToml); + setRecordSection(out, 'models', config.models, modelToToml); + setSection(out, 'thinking', config.thinking, thinkingToToml); + setSection(out, 'services', config.services, servicesToToml); + setSection(out, 'loop_control', config.loopControl, loopControlToToml); + setSection(out, 'background', config.background, backgroundToToml); + setSection(out, 'permission', config.permission, permissionToToml); + setHooks(out, config.hooks); + + return out; +} + +function setRecordSection( + out: Record, + snakeKey: string, + value: Record | undefined, + toToml: (v: T, raw: unknown) => Record, +): void { + if (value === undefined) { + delete out[snakeKey]; + return; + } + + const rawSub = cloneRecord(out[snakeKey]); + const converted: Record = {}; + for (const [entryName, entryConfig] of Object.entries(value)) { + converted[entryName] = toToml(entryConfig, rawSub[entryName]); + } + + if (Object.keys(converted).length > 0) { + out[snakeKey] = converted; + } else { + delete out[snakeKey]; + } +} + +function setSection( + out: Record, + snakeKey: string, + value: T | undefined, + toToml: (v: T, raw: unknown) => Record, +): void { + if (value === undefined) { + delete out[snakeKey]; + return; + } + const rawSub = cloneRecord(out[snakeKey]); + const converted = toToml(value, rawSub); + if (Object.keys(converted).length > 0) { + out[snakeKey] = converted; + } else { + delete out[snakeKey]; + } +} + +function providerToToml(provider: ProviderConfig, rawProvider: unknown): Record { + const out = cloneRecord(rawProvider); + for (const [key, value] of Object.entries(provider)) { + if (key === 'oauth' && value !== undefined) { + out[camelToSnake(key)] = oauthToToml(value as OAuthRef); + } else if ((key === 'env' || key === 'customHeaders') && value !== undefined) { + out[camelToSnake(key)] = cloneUnknown(value); + } else { + setDefined(out, camelToSnake(key), value); + } + } + return out; +} + +function modelToToml(model: ModelAlias, rawModel: unknown): Record { + const out = cloneRecord(rawModel); + for (const [key, value] of Object.entries(model)) { + if (key === 'capabilities' && Array.isArray(value)) { + out[camelToSnake(key)] = [...value]; + } else { + setDefined(out, camelToSnake(key), value); + } + } + return out; +} + +function thinkingToToml(thinking: ThinkingConfig, rawThinking: unknown): Record { + const out = cloneRecord(rawThinking); + for (const [key, value] of Object.entries(thinking)) { + setDefined(out, camelToSnake(key), value); + } + return out; +} + +function permissionToToml( + permission: PermissionConfig, + rawPermission: unknown, +): Record { + const out = cloneRecord(rawPermission); + delete out['deny']; + delete out['allow']; + delete out['ask']; + + if (permission.rules !== undefined) { + out['rules'] = permission.rules.map(permissionRuleToToml); + } else { + delete out['rules']; + } + return out; +} + +function permissionRuleToToml( + rule: NonNullable[number], +): Record { + const out: Record = {}; + for (const [key, value] of Object.entries(rule)) { + setDefined(out, camelToSnake(key), value); + } + return out; +} + +function servicesToToml(services: ServicesConfig, rawServices: unknown): Record { + const out = cloneRecord(rawServices); + if (services.moonshotSearch !== undefined) { + out['moonshot_search'] = serviceToToml(services.moonshotSearch); + } else { + delete out['moonshot_search']; + } + if (services.moonshotFetch !== undefined) { + out['moonshot_fetch'] = serviceToToml(services.moonshotFetch); + } else { + delete out['moonshot_fetch']; + } + return out; +} + +function serviceToToml(service: MoonshotServiceConfig): Record { + const out: Record = {}; + for (const [key, value] of Object.entries(service)) { + if (key === 'oauth' && value !== undefined) { + out[camelToSnake(key)] = oauthToToml(value as OAuthRef); + } else if (key === 'customHeaders' && value !== undefined) { + out[camelToSnake(key)] = cloneUnknown(value); + } else { + setDefined(out, camelToSnake(key), value); + } + } + return out; +} + +function loopControlToToml( + loopControl: LoopControl, + rawLoopControl: unknown, +): Record { + const out = cloneRecord(rawLoopControl); + for (const [key, value] of Object.entries(loopControl)) { + setDefined(out, camelToSnake(key), value); + } + return out; +} + +function backgroundToToml( + background: BackgroundConfig, + rawBackground: unknown, +): Record { + const out = cloneRecord(rawBackground); + for (const [key, value] of Object.entries(background)) { + setDefined(out, camelToSnake(key), value); + } + return out; +} + +function setHooks(out: Record, hooks: readonly HookDefConfig[] | undefined): void { + if (hooks === undefined) { + delete out['hooks']; + return; + } + out['hooks'] = hooks.map(hookToToml); +} + +function hookToToml(hook: HookDefConfig): Record { + const out: Record = {}; + for (const [key, value] of Object.entries(hook)) { + setDefined(out, camelToSnake(key), value); + } + return out; +} + +function oauthToToml(oauth: OAuthRef): Record { + const out: Record = {}; + for (const [key, value] of Object.entries(oauth)) { + out[camelToSnake(key)] = value; + } + return out; +} + +/* ------------------------------------------------------------------ */ +/* Utilities */ +/* ------------------------------------------------------------------ */ + +function isPlainObject(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + +function cloneRecord(value: unknown): Record { + if (!isPlainObject(value)) return {}; + return cloneUnknown(value); +} + +function cloneUnknown(value: T): T { + return JSON.parse(JSON.stringify(value)) as T; +} + +function cloneObjectValue(value: unknown): unknown { + return isPlainObject(value) ? cloneUnknown(value) : value; +} + +function setDefined(target: Record, key: string, value: unknown): void { + if (value !== undefined) { + target[key] = value; + } else { + delete target[key]; + } +} + +function isFileExistsError(error: unknown): boolean { + return ( + typeof error === 'object' && + error !== null && + (error as { code?: unknown }).code === 'EEXIST' + ); +} diff --git a/packages/agent-core/src/errors.ts b/packages/agent-core/src/errors.ts new file mode 100644 index 000000000..5a9fb663a --- /dev/null +++ b/packages/agent-core/src/errors.ts @@ -0,0 +1,5 @@ +// Barrel re-export so #/errors resolves to a single .ts file (the first +// entry in the package imports map). vitest does not resolve cleanly through +// the directory fallback; this thin barrel keeps the alias working uniformly +// across node, tsc, and vitest. Real module lives under ./errors. +export * from './errors/index'; diff --git a/packages/agent-core/src/errors/classes.ts b/packages/agent-core/src/errors/classes.ts new file mode 100644 index 000000000..8a080ea08 --- /dev/null +++ b/packages/agent-core/src/errors/classes.ts @@ -0,0 +1,28 @@ +import type { KimiErrorCode } from './codes'; + +export interface KimiErrorOptions { + /** JSON-serializable structured details. */ + readonly details?: Record; + /** Original error or value. Local-only; never serialized to the wire. */ + readonly cause?: unknown; +} + +/** + * The single Kimi error class. + * + * Discrimination is always by `code`. Cross-process consumers receive + * `KimiErrorPayload` and must branch on `code` rather than class identity. + */ +export class KimiError extends Error { + readonly code: KimiErrorCode; + readonly details?: Record; + override readonly cause?: unknown; + + constructor(code: KimiErrorCode, message: string, options: KimiErrorOptions = {}) { + super(message); + this.name = 'KimiError'; + this.code = code; + this.details = options.details; + this.cause = options.cause; + } +} diff --git a/packages/agent-core/src/errors/codes.ts b/packages/agent-core/src/errors/codes.ts new file mode 100644 index 000000000..2583a8faa --- /dev/null +++ b/packages/agent-core/src/errors/codes.ts @@ -0,0 +1,377 @@ +/** + * Error codes for Kimi Core's public error protocol. + * + * `ErrorCodes` is the source of truth for every code Kimi Core may emit. + * Downstream consumers (SDK, RPC clients, telemetry, agent-facing docs) + * should depend on these string values rather than on class identity. + * + * Codes follow `domain.reason`. Adding a code is a minor change; renaming + * or removing one is a major change. + */ +export const ErrorCodes = { + CONFIG_INVALID: 'config.invalid', + + SESSION_NOT_FOUND: 'session.not_found', + SESSION_ALREADY_EXISTS: 'session.already_exists', + SESSION_ID_INVALID: 'session.id_invalid', + SESSION_ID_REQUIRED: 'session.id_required', + SESSION_ID_EMPTY: 'session.id_empty', + SESSION_TITLE_EMPTY: 'session.title_empty', + SESSION_STATE_NOT_FOUND: 'session.state_not_found', + SESSION_STATE_INVALID: 'session.state_invalid', + SESSION_FORK_ACTIVE_TURN: 'session.fork_active_turn', + SESSION_EXPORT_NOT_FOUND: 'session.export_not_found', + SESSION_EXPORT_MISSING_VERSION: 'session.export_missing_version', + SESSION_CLOSED: 'session.closed', + SESSION_PERMISSION_MODE_INVALID: 'session.permission_mode_invalid', + SESSION_THINKING_EMPTY: 'session.thinking_empty', + SESSION_MODEL_EMPTY: 'session.model_empty', + SESSION_PLAN_MODE_INVALID: 'session.plan_mode_invalid', + SESSION_APPROVAL_HANDLER_ERROR: 'session.approval_handler_error', + SESSION_QUESTION_HANDLER_ERROR: 'session.question_handler_error', + SESSION_INIT_FAILED: 'session.init_failed', + + AGENT_NOT_FOUND: 'agent.not_found', + TURN_AGENT_BUSY: 'turn.agent_busy', + + MODEL_NOT_CONFIGURED: 'model.not_configured', + MODEL_CONFIG_INVALID: 'model.config_invalid', + AUTH_LOGIN_REQUIRED: 'auth.login_required', + + CONTEXT_OVERFLOW: 'context.overflow', + LOOP_MAX_STEPS_EXCEEDED: 'loop.max_steps_exceeded', + PROVIDER_API_ERROR: 'provider.api_error', + PROVIDER_RATE_LIMIT: 'provider.rate_limit', + PROVIDER_AUTH_ERROR: 'provider.auth_error', + PROVIDER_CONNECTION_ERROR: 'provider.connection_error', + + SKILL_NOT_FOUND: 'skill.not_found', + SKILL_TYPE_UNSUPPORTED: 'skill.type_unsupported', + SKILL_NAME_EMPTY: 'skill.name_empty', + + RECORDS_WRITE_FAILED: 'records.write_failed', + COMPACTION_FAILED: 'compaction.failed', + + BACKGROUND_TASK_ID_EMPTY: 'background.task_id_empty', + MCP_SERVER_NOT_FOUND: 'mcp.server_not_found', + MCP_SERVER_DISABLED: 'mcp.server_disabled', + MCP_STARTUP_FAILED: 'mcp.startup_failed', + MCP_TOOL_NAME_COLLISION: 'mcp.tool_name_collision', + + REQUEST_INVALID: 'request.invalid', + REQUEST_WORK_DIR_REQUIRED: 'request.work_dir_required', + REQUEST_PROMPT_INPUT_EMPTY: 'request.prompt_input_empty', + + SHELL_GIT_BASH_NOT_FOUND: 'shell.git_bash_not_found', + + NOT_IMPLEMENTED: 'not_implemented', + INTERNAL: 'internal', +} as const; + +export type KimiErrorCode = (typeof ErrorCodes)[keyof typeof ErrorCodes]; + +export interface KimiErrorInfo { + readonly title: string; + readonly retryable: boolean; + /** + * Whether the code is a stable public contract. `false` reserves the + * right to rename or remove without a major version bump. + */ + readonly public: boolean; + readonly action?: string; +} + +export const KIMI_ERROR_INFO = { + 'config.invalid': { + title: 'Invalid configuration', + retryable: false, + public: true, + action: 'Check config.toml and provider/model settings.', + }, + + 'session.not_found': { + title: 'Session not found', + retryable: false, + public: true, + action: 'Check the session id or list available sessions.', + }, + 'session.already_exists': { + title: 'Session already exists', + retryable: false, + public: true, + action: 'Use a different session id or remove the existing session first.', + }, + 'session.id_invalid': { + title: 'Invalid session id', + retryable: false, + public: true, + action: 'Use a session id without path-traversal characters.', + }, + 'session.id_required': { + title: 'Session id required', + retryable: false, + public: true, + action: 'Provide a session id when calling this method.', + }, + 'session.id_empty': { + title: 'Session id is empty', + retryable: false, + public: true, + action: 'Provide a non-empty session id.', + }, + 'session.title_empty': { + title: 'Session title is empty', + retryable: false, + public: true, + action: 'Provide a non-empty session title.', + }, + 'session.state_not_found': { + title: 'Session state missing', + retryable: false, + public: true, + action: 'The session directory is corrupted or missing state.json.', + }, + 'session.state_invalid': { + title: 'Session state invalid', + retryable: false, + public: true, + action: 'The session state.json is corrupted; remove the session or repair the file.', + }, + 'session.fork_active_turn': { + title: 'Cannot fork session during active turn', + retryable: true, + public: true, + action: 'Wait for the active turn to complete before forking.', + }, + 'session.export_not_found': { + title: 'Session export directory missing', + retryable: false, + public: true, + action: 'The session has not been persisted to disk yet.', + }, + 'session.export_missing_version': { + title: 'Export version is missing', + retryable: false, + public: true, + action: 'Provide a version when exporting the session.', + }, + 'session.closed': { + title: 'Session is closed', + retryable: false, + public: true, + action: 'Create a new session.', + }, + 'session.permission_mode_invalid': { + title: 'Invalid permission mode', + retryable: false, + public: true, + action: 'Use one of: yolo / manual / auto.', + }, + 'session.thinking_empty': { + title: 'Thinking value is empty', + retryable: false, + public: true, + action: 'Provide a non-empty thinking option.', + }, + 'session.model_empty': { + title: 'Model is empty', + retryable: false, + public: true, + action: 'Provide a non-empty model identifier.', + }, + 'session.plan_mode_invalid': { + title: 'Invalid plan mode', + retryable: false, + public: true, + action: 'Provide a boolean plan mode.', + }, + 'session.approval_handler_error': { + title: 'Approval handler threw', + retryable: false, + public: true, + action: 'Inspect the SDK approval handler for an unhandled exception.', + }, + 'session.question_handler_error': { + title: 'Question handler threw', + retryable: false, + public: true, + action: 'Inspect the SDK question handler for an unhandled exception.', + }, + 'session.init_failed': { + title: 'Session init failed', + retryable: false, + public: false, + action: 'Review the init failure details and try again.', + }, + + 'agent.not_found': { + title: 'Agent not found', + retryable: false, + public: true, + action: 'Check the agent id or list available agents.', + }, + 'turn.agent_busy': { + title: 'Agent is busy', + retryable: true, + public: true, + action: 'Wait for the current turn to finish or steer it.', + }, + + 'model.not_configured': { + title: 'No model configured', + retryable: false, + public: true, + action: 'Set a default model in config.toml or via setModel.', + }, + 'model.config_invalid': { + title: 'Invalid model configuration', + retryable: false, + public: true, + action: 'Check the model and provider entries in config.toml.', + }, + 'auth.login_required': { + title: 'Login required', + retryable: false, + public: true, + action: 'Run the login flow for the provider before retrying.', + }, + + 'context.overflow': { + title: 'Context window overflow', + retryable: true, + public: true, + action: 'Compact the conversation or start a new session.', + }, + 'loop.max_steps_exceeded': { + title: 'Turn exceeded max steps', + retryable: false, + public: true, + action: 'Increase loop_control.max_steps_per_turn or split the task.', + }, + 'provider.api_error': { + title: 'Provider API error', + retryable: false, + public: true, + action: 'Inspect details.statusCode / details.requestId; check provider status.', + }, + 'provider.rate_limit': { + title: 'Provider rate limit', + retryable: true, + public: true, + action: 'Retry after a delay or reduce request frequency.', + }, + 'provider.auth_error': { + title: 'Provider authentication error', + retryable: false, + public: true, + action: 'Re-authenticate with the provider.', + }, + 'provider.connection_error': { + title: 'Provider connection error', + retryable: true, + public: true, + action: 'Check network connectivity and retry.', + }, + + 'skill.not_found': { + title: 'Skill not found', + retryable: false, + public: true, + action: 'List available skills via the skill registry.', + }, + 'skill.type_unsupported': { + title: 'Skill type not supported', + retryable: false, + public: true, + action: 'Only inline skills can be activated by the user.', + }, + 'skill.name_empty': { + title: 'Skill name is empty', + retryable: false, + public: true, + action: 'Provide a non-empty skill name.', + }, + + 'records.write_failed': { + title: 'Failed to write records', + retryable: true, + public: true, + action: 'Check disk space and permissions on the session directory.', + }, + 'compaction.failed': { + title: 'Compaction failed', + retryable: false, + public: true, + action: 'Inspect logs and consider increasing compaction limits.', + }, + + 'background.task_id_empty': { + title: 'Background task id is empty', + retryable: false, + public: true, + action: 'Provide a non-empty task id.', + }, + 'mcp.server_not_found': { + title: 'MCP server not found', + retryable: false, + public: true, + action: 'List configured MCP servers and check the requested name.', + }, + 'mcp.server_disabled': { + title: 'MCP server is disabled', + retryable: false, + public: true, + action: 'Enable the MCP server entry in config before reconnecting.', + }, + 'mcp.startup_failed': { + title: 'MCP server startup failed', + retryable: true, + public: true, + action: 'Inspect the MCP server log or call reconnect once the server is healthy.', + }, + 'mcp.tool_name_collision': { + title: 'MCP tool name collision', + retryable: false, + public: true, + action: 'Rename one of the colliding MCP tools or servers so their qualified names are unique.', + }, + + 'request.invalid': { + title: 'Invalid request', + retryable: false, + public: true, + action: 'Check the input shape matches the API contract.', + }, + 'request.work_dir_required': { + title: 'workDir is required', + retryable: false, + public: true, + action: 'Provide workDir in the request payload.', + }, + 'request.prompt_input_empty': { + title: 'Prompt input is empty', + retryable: false, + public: true, + action: 'Provide non-empty prompt input.', + }, + + 'shell.git_bash_not_found': { + title: 'Git Bash not found', + retryable: false, + public: true, + action: 'Install Git for Windows from https://gitforwindows.org/ or set KIMI_SHELL_PATH to a bash.exe.', + }, + + not_implemented: { + title: 'Not implemented', + retryable: false, + public: true, + action: 'This feature is not implemented yet.', + }, + internal: { + title: 'Internal error', + retryable: false, + public: true, + action: 'Inspect logs or report the issue with diagnostics.', + }, +} as const satisfies Record; diff --git a/packages/agent-core/src/errors/index.ts b/packages/agent-core/src/errors/index.ts new file mode 100644 index 000000000..317c7f3c5 --- /dev/null +++ b/packages/agent-core/src/errors/index.ts @@ -0,0 +1,17 @@ +export { + ErrorCodes, + KIMI_ERROR_INFO, + type KimiErrorCode, + type KimiErrorInfo, +} from './codes'; +export { + KimiError, + type KimiErrorOptions, +} from './classes'; +export { + fromKimiErrorPayload, + isKimiError, + makeErrorPayload, + toKimiErrorPayload, + type KimiErrorPayload, +} from './serialize'; diff --git a/packages/agent-core/src/errors/serialize.ts b/packages/agent-core/src/errors/serialize.ts new file mode 100644 index 000000000..efbbbb453 --- /dev/null +++ b/packages/agent-core/src/errors/serialize.ts @@ -0,0 +1,149 @@ +import { + APIConnectionError, + APIStatusError, + APITimeoutError, + ChatProviderError, +} from '@moonshot-ai/kosong'; + +import { KimiError } from './classes'; +import { ErrorCodes, KIMI_ERROR_INFO, type KimiErrorCode } from './codes'; + +/** + * Wire-safe payload of a Kimi error. + * + * The structure passed across process / language boundaries (RPC, events, + * telemetry, SDK wrappers). Class identity does not survive the boundary; + * downstream code must branch on `code` rather than `instanceof`. + * + * `details` is JSON-serialized. `cause` is intentionally absent -- it is + * local-only diagnostic state and must not cross the boundary. + */ +export interface KimiErrorPayload { + readonly code: KimiErrorCode; + readonly message: string; + readonly name?: string; + readonly details?: Record; + readonly retryable: boolean; +} + +/** Type guard for KimiError. */ +export function isKimiError(error: unknown): error is KimiError { + return error instanceof KimiError; +} + +/** + * Build a KimiErrorPayload directly from a code + message (no Error instance + * needed). Use this for synthetic error events that are signaled, not thrown + * -- e.g. "turn busy" or "compaction failed". `retryable` is filled from + * KIMI_ERROR_INFO so callers cannot drift out of sync with the registry. + */ +export function makeErrorPayload( + code: KimiErrorCode, + message: string, + options?: { readonly details?: Record; readonly name?: string }, +): KimiErrorPayload { + return { + code, + message, + name: options?.name, + details: options?.details, + retryable: KIMI_ERROR_INFO[code].retryable, + }; +} + +/** + * Normalize any value into a KimiErrorPayload. + * + * Recognized errors: + * - `KimiError`: passthrough. + * - `APIStatusError`: 429 -> rate_limit, 401 -> auth_error, otherwise -> api_error. + * - `APIConnectionError` / `APITimeoutError`: connection_error. + * - `ChatProviderError`: api_error. + * - Heuristic "Model not set" / "Provider not set" messages: model.not_configured. + * + * Anything else collapses to `internal`. We never echo `cause` or stack on + * the wire. + */ +export function toKimiErrorPayload(error: unknown): KimiErrorPayload { + if (isKimiError(error)) { + return { + code: error.code, + message: error.message, + name: error.name, + details: error.details, + retryable: KIMI_ERROR_INFO[error.code].retryable, + }; + } + + if (error instanceof APIStatusError) { + const code: KimiErrorCode = + error.statusCode === 429 + ? ErrorCodes.PROVIDER_RATE_LIMIT + : error.statusCode === 401 + ? ErrorCodes.PROVIDER_AUTH_ERROR + : ErrorCodes.PROVIDER_API_ERROR; + return { + code, + message: error.message, + name: error.name, + details: { + statusCode: error.statusCode, + requestId: error.requestId, + }, + retryable: KIMI_ERROR_INFO[code].retryable, + }; + } + + if (error instanceof APIConnectionError || error instanceof APITimeoutError) { + return { + code: ErrorCodes.PROVIDER_CONNECTION_ERROR, + message: error.message, + name: error.name, + retryable: KIMI_ERROR_INFO[ErrorCodes.PROVIDER_CONNECTION_ERROR].retryable, + }; + } + + if (error instanceof ChatProviderError) { + return { + code: ErrorCodes.PROVIDER_API_ERROR, + message: error.message, + name: error.name, + retryable: KIMI_ERROR_INFO[ErrorCodes.PROVIDER_API_ERROR].retryable, + }; + } + + if (error instanceof Error) { + if (error.message === 'Model not set' || error.message === 'Provider not set') { + return { + code: ErrorCodes.MODEL_NOT_CONFIGURED, + message: error.message, + name: error.name, + retryable: KIMI_ERROR_INFO[ErrorCodes.MODEL_NOT_CONFIGURED].retryable, + }; + } + + return { + code: ErrorCodes.INTERNAL, + message: error.message, + name: error.name, + retryable: KIMI_ERROR_INFO[ErrorCodes.INTERNAL].retryable, + }; + } + + return { + code: ErrorCodes.INTERNAL, + message: String(error), + retryable: KIMI_ERROR_INFO[ErrorCodes.INTERNAL].retryable, + }; +} + +/** + * Rehydrate a KimiErrorPayload into a KimiError. Used by SDK boundary code + * receiving errors over RPC to re-surface them with a real class so + * in-process consumers can still use `instanceof`. + */ +export function fromKimiErrorPayload(payload: KimiErrorPayload): KimiError { + return new KimiError(payload.code, payload.message, { + details: payload.details, + }); +} diff --git a/packages/agent-core/src/index.ts b/packages/agent-core/src/index.ts new file mode 100644 index 000000000..5e972d975 --- /dev/null +++ b/packages/agent-core/src/index.ts @@ -0,0 +1,45 @@ +export * from './agent'; +export * from './session'; +export * from './rpc'; +export * from './config'; +export * from './session/export'; +export * from './telemetry'; +export * from './errors'; +export { + flushDiagnosticLogs, + getRootLogger, + log, + redact, + resolveGlobalLogPath, +} from './logging/logger'; +export { resolveLoggingConfig } from './logging/resolve-config'; +export type { ResolveLoggingInput } from './logging/resolve-config'; +export type { + LogContext, + LogEntry, + LogLevel, + LogPayload, + Logger, + LoggingConfig, + RootLogger, + SessionAttachInput, + SessionLogHandle, +} from './logging/types'; +export { USER_PROMPT_ORIGIN } from './agent/context'; +export type { + AgentContextData, + ContextMessage, + PromptOrigin, + UserPromptOrigin, +} from './agent/context'; +export type { + BackgroundLifecycleEvent, + BackgroundTaskInfo, + BackgroundTaskKind, + BackgroundTaskStatus, +} from './tools/background/manager'; +export type { RuntimeConfig } from './runtime-types'; +export type { + BearerTokenProvider, + OAuthTokenProviderResolver, +} from './providers/runtime-provider'; diff --git a/packages/agent-core/src/logging/formatter.ts b/packages/agent-core/src/logging/formatter.ts new file mode 100644 index 000000000..df37591a5 --- /dev/null +++ b/packages/agent-core/src/logging/formatter.ts @@ -0,0 +1,200 @@ +import type { LogContext, LogEntry } from './types'; + +export const MSG_MAX_CHARS = 200; +export const CTX_VALUE_MAX_CHARS = 2048; +export const STACK_MAX_BYTES = 2048; +export const ENTRY_MAX_BYTES = 4096; +export const REDACT_MAX_DEPTH = 10; + +const REDACTED_KEYS: ReadonlySet = new Set([ + 'authorization', + 'apikey', + 'token', + 'refreshtoken', + 'accesstoken', + 'idtoken', + 'password', + 'secret', + 'clientsecret', + 'apisecret', + 'cookie', + 'setcookie', + 'bearer', +]); + +const SAFE_KEY_RE = /^[\w.-]+$/; +const ELLIPSIS = '…'; +const TRUNCATED_TAIL = ` …truncated`; +const REDACTED = '[REDACTED]'; +const RAW_SECRET_PATTERNS: readonly RegExp[] = [ + /\b(authorization\s*[:=]\s*bearer\s+)[^\s"'`]+/gi, + /\b((?:api[_-]?key|access[_-]?token|refresh[_-]?token|id[_-]?token|token|password|secret)\s*[:=]\s*)[^\s"'`]+/gi, + /\b(cookie\s*[:=]\s*)[^\r\n]+/gi, +]; + +const LEVEL_LABEL: Record, string> = { + error: 'ERROR', + warn: 'WARN ', + info: 'INFO ', + debug: 'DEBUG', +}; + +const ANSI_LEVEL: Record, string> = { + error: '\u001B[31m', + warn: '\u001B[33m', + info: '\u001B[36m', + debug: '\u001B[90m', +}; +const ANSI_RESET = '\u001B[0m'; + +function normalizeKey(key: string): string { + return key.toLowerCase().replaceAll(/[_\-.]/g, ''); +} + +export function redactCtx(ctx: LogContext): LogContext { + const seen = new WeakSet(); + const walk = (value: unknown, depth: number): unknown => { + if (depth > REDACT_MAX_DEPTH) return '[REDACTED:depth]'; + if (value === null || typeof value !== 'object') return value; + if (seen.has(value)) return '[REDACTED:cycle]'; + seen.add(value); + if (Array.isArray(value)) { + return value.map((item) => walk(item, depth + 1)); + } + const out: Record = {}; + for (const [key, raw] of Object.entries(value as Record)) { + out[key] = REDACTED_KEYS.has(normalizeKey(key)) + ? REDACTED + : walk(raw, depth + 1); + } + return out; + }; + return walk(ctx, 0) as LogContext; +} + +export interface FormatOptions { + readonly ansi?: boolean | undefined; + readonly omitContextKeys?: readonly string[]; +} + +export interface FormattedEntry { + readonly text: string; + readonly dropped: boolean; +} + +function truncate(value: string, max: number): string { + return value.length <= max ? value : value.slice(0, max - 1) + ELLIPSIS; +} + +function serializeValue(raw: unknown): string { + if (typeof raw === 'string') return redactString(raw); + if (raw === undefined) return 'undefined'; + if (raw === null) return 'null'; + if ( + typeof raw === 'number' || + typeof raw === 'boolean' || + typeof raw === 'bigint' || + typeof raw === 'symbol' + ) { + return String(raw); + } + try { + const json = JSON.stringify(raw); + if (json !== undefined) return json; + } catch { + // fall through to a stable non-contentful fallback + } + if (typeof raw === 'function') return raw.name === '' ? '[Function]' : `[Function: ${raw.name}]`; + return Object.prototype.toString.call(raw); +} + +function redactString(value: string): string { + let out = value; + for (const pattern of RAW_SECRET_PATTERNS) { + out = out.replace(pattern, `$1${REDACTED}`); + } + return out; +} + +function quote(value: string): string { + return `"${value.replaceAll('\\', '\\\\').replaceAll('"', '\\"').replaceAll('\n', '\\n')}"`; +} + +function formatPair(key: string, raw: unknown): string { + const limited = truncate(serializeValue(raw), CTX_VALUE_MAX_CHARS); + const renderedKey = SAFE_KEY_RE.test(key) ? key : quote(key); + const renderedVal = /[\s="\\]/.test(limited) || limited.length === 0 ? quote(limited) : limited; + return `${renderedKey}=${renderedVal}`; +} + +function clipBytes(text: string, maxBytes: number): string { + if (Buffer.byteLength(text, 'utf-8') <= maxBytes) return text; + // Binary-search the longest prefix that fits. + let lo = 0; + let hi = text.length; + while (lo < hi) { + const mid = (lo + hi + 1) >> 1; + if ( + Buffer.byteLength(text.slice(0, mid), 'utf-8') <= + maxBytes - Buffer.byteLength(TRUNCATED_TAIL, 'utf-8') + ) { + lo = mid; + } else { + hi = mid - 1; + } + } + return text.slice(0, lo) + TRUNCATED_TAIL; +} + +function clipStack(stack: string): string { + if (Buffer.byteLength(stack, 'utf-8') <= STACK_MAX_BYTES) return stack; + return clipBytes(stack, STACK_MAX_BYTES); +} + +function indentStack(stack: string): string { + return stack + .split('\n') + .map((line, i) => (i === 0 ? ` ${line}` : ` ${line.trimStart()}`)) + .join('\n'); +} + +export function formatEntry(entry: LogEntry, options: FormatOptions = {}): FormattedEntry { + const ctx = entry.ctx ? redactCtx(entry.ctx) : undefined; + const omitContextKeys = new Set(options.omitContextKeys ?? []); + const msg = truncate(entry.msg, MSG_MAX_CHARS); + const pairs: string[] = []; + if (ctx) { + for (const [k, v] of Object.entries(ctx)) { + if (omitContextKeys.has(k)) continue; + if (v !== undefined) pairs.push(formatPair(k, v)); + } + } + + const time = new Date(entry.t).toISOString(); + const label = LEVEL_LABEL[entry.level]; + const rendered = pairs.length === 0 + ? `${time} ${label} ${msg}` + : `${time} ${label} ${msg} ${pairs.join(' ')}`; + + let head = Buffer.byteLength(rendered, 'utf-8') > ENTRY_MAX_BYTES + ? clipBytes(rendered, ENTRY_MAX_BYTES) + : rendered; + + if (options.ansi === true) { + head = `${ANSI_LEVEL[entry.level]}${head}${ANSI_RESET}`; + } + + if (entry.error?.stack) { + head = `${head}\n${indentStack(clipStack(redactString(entry.error.stack)))}`; + } else if (entry.error?.message) { + head = `${head}\n Error: ${redactString(entry.error.message)}`; + } + + return { text: head, dropped: false }; +} + +export function extractError(value: Error): { message: string; stack?: string } { + return typeof value.stack === 'string' + ? { message: value.message, stack: value.stack } + : { message: value.message }; +} diff --git a/packages/agent-core/src/logging/index.ts b/packages/agent-core/src/logging/index.ts new file mode 100644 index 000000000..51be3b8a6 --- /dev/null +++ b/packages/agent-core/src/logging/index.ts @@ -0,0 +1,32 @@ +export type { + LogContext, + LogEntry, + LogLevel, + LogPayload, + Logger, + LoggingConfig, + RootLogger, + SessionAttachInput, + SessionLogHandle, +} from './types'; +export { LOG_LEVEL_RANK, levelEnabled } from './types'; + +export { + __resetRootLoggerForTest, + flushDiagnosticLogs, + getRootLogger, + log, + redact, + resolveGlobalLogPath, +} from './logger'; + +export { + CTX_VALUE_MAX_CHARS, + ENTRY_MAX_BYTES, + MSG_MAX_CHARS, + REDACT_MAX_DEPTH, + STACK_MAX_BYTES, + extractError, + formatEntry, + redactCtx, +} from './formatter'; diff --git a/packages/agent-core/src/logging/logger.ts b/packages/agent-core/src/logging/logger.ts new file mode 100644 index 000000000..7112de2ca --- /dev/null +++ b/packages/agent-core/src/logging/logger.ts @@ -0,0 +1,437 @@ +import { join } from 'node:path'; + +import { extractError, formatEntry, redactCtx } from './formatter'; +import { RotatingFileSink } from './sinks'; +import { + type LogContext, + type LogEntry, + type LogLevel, + type LogPayload, + type Logger, + type LoggingConfig, + type RootLogger, + type SessionAttachInput, + type SessionLogHandle, + levelEnabled, +} from './types'; + +const ROOT_SYMBOL = Symbol.for('kimi.logger.root'); +const SESSION_LOG_ID = Symbol('kimi.logger.sessionLogId'); +const LLM_REQUEST_SESSION_LOG_OMITTED_CONTEXT_KEYS = ['sessionId']; +const MAIN_LLM_REQUEST_SESSION_LOG_OMITTED_CONTEXT_KEYS = ['sessionId', 'agentId']; +let nextSessionLogId = 0; + +interface SessionEntry { + readonly logId: string; + readonly sessionId: string; + readonly sessionDir: string; + readonly sink: RotatingFileSink; + state: 'open' | 'closing'; + closePromise: Promise | undefined; + refCount: number; +} + +class RootLoggerImpl implements RootLogger { + private config: LoggingConfig | undefined; + private globalSink: RotatingFileSink | undefined; + private readonly sessions = new Map(); + private readonly sessionsById = new Map>(); + + isConfigured(): boolean { + return this.config !== undefined; + } + + getConfig(): LoggingConfig | undefined { + return this.config; + } + + configure(config: LoggingConfig): Promise { + if (this.config !== undefined && sameLoggingConfig(this.config, config)) { + return Promise.resolve(); + } + const oldGlobalSink = this.globalSink; + this.config = config; + this.globalSink = makeGlobalSink(config); + return oldGlobalSink?.close() ?? Promise.resolve(); + } + + attachSession(input: SessionAttachInput): SessionLogHandle { + const existing = this.findOpenSession(input.sessionId, input.sessionDir); + if (existing !== undefined) { + existing.refCount += 1; + return makeHandle(existing); + } + const config = this.config; + if (config === undefined || config.level === 'off') { + return makeNoopHandle(input.sessionId); + } + const sink = new RotatingFileSink({ + path: join(input.sessionDir, 'logs', 'kimi-code.log'), + maxBytes: config.sessionMaxBytes, + files: config.sessionFiles, + }); + const entry: SessionEntry = { + logId: `session-log-${String(++nextSessionLogId)}`, + sessionId: input.sessionId, + sessionDir: input.sessionDir, + sink, + state: 'open', + closePromise: undefined, + refCount: 1, + }; + this.sessions.set(entry.logId, entry); + this.trackSessionId(entry); + return makeHandle(entry); + } + + async flush(): Promise { + const tasks: Promise[] = []; + if (this.globalSink !== undefined) tasks.push(this.globalSink.flush()); + for (const entry of this.sessions.values()) { + tasks.push(this.flushEntry(entry)); + } + if (tasks.length === 0) return true; + const results = await Promise.all(tasks); + return results.every(Boolean); + } + + async flushGlobal(): Promise { + if (this.globalSink === undefined) return true; + return this.globalSink.flush(); + } + + async flushSession(sessionId: string): Promise { + const entries = this.getEntriesForSessionId(sessionId); + if (entries.length === 0) return true; + const results = await Promise.all(entries.map((entry) => this.flushEntry(entry))); + return results.every(Boolean); + } + + flushSync(): void { + const deadline = Date.now() + 200; + this.globalSink?.flushSync(); + for (const entry of this.sessions.values()) { + if (entry.state !== 'open') continue; + if (Date.now() > deadline) break; + entry.sink.flushSync(); + } + } + + emit(entry: LogEntry): void { + const config = this.config; + if (config === undefined || config.level === 'off') return; + if (!levelEnabled(config.level, entry.level)) return; + + const formatted = formatEntry(entry); + if (formatted.dropped) return; + + this.globalSink?.enqueue(formatted.text + '\n'); + + const session = this.resolveSessionEntry(entry); + if (session !== undefined) { + const omitContextKeys = + entry.msg === 'llm request' ? llmRequestSessionLogOmittedKeys(entry) : undefined; + const sessionFormatted = formatEntry(entry, { + omitContextKeys, + }); + if (!sessionFormatted.dropped) { + session.sink.enqueue(sessionFormatted.text + '\n'); + } + } + } + + detachSession(logId: string): Promise { + const entry = this.sessions.get(logId); + if (entry === undefined) return Promise.resolve(); + if (entry.state === 'closing') return entry.closePromise ?? Promise.resolve(); + entry.refCount -= 1; + if (entry.refCount > 0) return Promise.resolve(); + entry.state = 'closing'; + entry.closePromise = entry.sink.close().finally(() => { + if (this.sessions.get(logId) === entry) { + this.sessions.delete(logId); + this.untrackSessionId(entry); + } + }); + return entry.closePromise; + } + + /** @internal — vitest only. */ + async __shutdownForTest(): Promise { + const closes: Promise[] = []; + if (this.globalSink !== undefined) closes.push(this.globalSink.close()); + for (const entry of this.sessions.values()) { + if (entry.state === 'closing') { + if (entry.closePromise !== undefined) closes.push(entry.closePromise); + } else { + entry.state = 'closing'; + entry.closePromise = entry.sink.close(); + closes.push(entry.closePromise); + } + } + this.sessions.clear(); + this.sessionsById.clear(); + this.globalSink = undefined; + this.config = undefined; + await Promise.allSettled(closes); + } + + private findOpenSession(sessionId: string, sessionDir: string): SessionEntry | undefined { + for (const entry of this.getEntriesForSessionId(sessionId)) { + if (entry.sessionDir === sessionDir && entry.state === 'open') return entry; + } + return undefined; + } + + private trackSessionId(entry: SessionEntry): void { + const ids = this.sessionsById.get(entry.sessionId) ?? new Set(); + ids.add(entry.logId); + this.sessionsById.set(entry.sessionId, ids); + } + + private untrackSessionId(entry: SessionEntry): void { + const ids = this.sessionsById.get(entry.sessionId); + if (ids === undefined) return; + ids.delete(entry.logId); + if (ids.size === 0) this.sessionsById.delete(entry.sessionId); + } + + private getEntriesForSessionId(sessionId: string): SessionEntry[] { + const ids = this.sessionsById.get(sessionId); + if (ids === undefined) return []; + return [...ids] + .map((id) => this.sessions.get(id)) + .filter((entry): entry is SessionEntry => entry !== undefined); + } + + private resolveSessionEntry(entry: LogEntry): SessionEntry | undefined { + if (entry.sessionLogId !== undefined) { + const session = this.sessions.get(entry.sessionLogId); + return session?.state === 'open' ? session : undefined; + } + if (entry.sessionId === undefined) return undefined; + const openEntries = this.getEntriesForSessionId(entry.sessionId).filter( + (item) => item.state === 'open', + ); + return openEntries.length === 1 ? openEntries[0] : undefined; + } + + private async flushEntry(entry: SessionEntry): Promise { + if (entry.state === 'closing') { + await entry.closePromise; + return true; + } + return entry.sink.flush(); + } +} + +function llmRequestSessionLogOmittedKeys(entry: LogEntry): readonly string[] { + return entry.ctx?.['agentId'] === 'main' + ? MAIN_LLM_REQUEST_SESSION_LOG_OMITTED_CONTEXT_KEYS + : LLM_REQUEST_SESSION_LOG_OMITTED_CONTEXT_KEYS; +} + +function makeHandle(entry: SessionEntry): SessionLogHandle { + let closed = false; + return { + logger: log.createChild({ + sessionId: entry.sessionId, + [SESSION_LOG_ID]: entry.logId, + } as LogContext), + async flush() { + if (entry.state === 'closing') { + await entry.closePromise; + } else { + await entry.sink.flush(); + } + }, + async close() { + if (closed) return; + closed = true; + await getRootInternal().detachSession(entry.logId); + }, + }; +} + +function makeNoopHandle(sessionId: string): SessionLogHandle { + return { + logger: log.createChild({ sessionId }), + async flush() {}, + async close() {}, + }; +} + +function getRootInternal(): RootLoggerImpl { + const globalAny = globalThis as Record; + const existing = globalAny[ROOT_SYMBOL]; + if (existing instanceof RootLoggerImpl) return existing; + const fresh = new RootLoggerImpl(); + globalAny[ROOT_SYMBOL] = fresh; + return fresh; +} + +export function getRootLogger(): RootLogger { + return getRootInternal(); +} + +export function flushDiagnosticLogs(): Promise { + return getRootInternal().flush(); +} + +class LoggerImpl implements Logger { + constructor(private readonly boundCtx: LogContext) {} + + error(message: string, payload?: LogPayload): void { + this.emitAt('error', message, payload); + } + warn(message: string, payload?: LogPayload): void { + this.emitAt('warn', message, payload); + } + info(message: string, payload?: LogPayload): void { + this.emitAt('info', message, payload); + } + debug(message: string, payload?: LogPayload): void { + this.emitAt('debug', message, payload); + } + + createChild(ctx: LogContext): Logger { + return new LoggerImpl({ ...this.boundCtx, ...ctx }); + } + + private emitAt( + level: Exclude, + message: string, + payload: LogPayload, + ): void { + const root = getRootInternal(); + if (!root.isConfigured()) return; + try { + const { ctx: payloadCtx, error } = resolvePayload(payload); + // Bound ctx wins so call-site can't overwrite ownership fields. + const ctx = mergeCtx(payloadCtx, this.boundCtx); + const sessionId = ctx?.['sessionId']; + const sessionLogId = (ctx as InternalLogContext | undefined)?.[SESSION_LOG_ID]; + root.emit({ + t: Date.now(), + level, + msg: message, + ctx: stripInternalCtx(ctx), + error, + sessionId: typeof sessionId === 'string' ? sessionId : undefined, + sessionLogId: typeof sessionLogId === 'string' ? sessionLogId : undefined, + }); + } catch { + // Diagnostic logging is best-effort and must never affect main control flow. + } + } +} + +type InternalLogContext = LogContext & { [SESSION_LOG_ID]?: unknown }; + +function stripInternalCtx(ctx: LogContext | undefined): LogContext | undefined { + if (ctx === undefined) return undefined; + if (!(SESSION_LOG_ID in ctx)) return ctx; + const { [SESSION_LOG_ID]: _internal, ...visible } = ctx as InternalLogContext; + return visible; +} + +function makeGlobalSink(config: LoggingConfig): RotatingFileSink | undefined { + if (config.level === 'off') return undefined; + return new RotatingFileSink({ + path: config.globalLogPath, + maxBytes: config.globalMaxBytes, + files: config.globalFiles, + }); +} + +function sameLoggingConfig(a: LoggingConfig, b: LoggingConfig): boolean { + return ( + a.level === b.level && + a.globalLogPath === b.globalLogPath && + a.globalMaxBytes === b.globalMaxBytes && + a.globalFiles === b.globalFiles && + a.sessionMaxBytes === b.sessionMaxBytes && + a.sessionFiles === b.sessionFiles + ); +} + +function resolvePayload( + payload: LogPayload, +): { ctx: LogContext | undefined; error: LogEntry['error'] } { + if (payload === undefined || payload === null) { + return { ctx: undefined, error: undefined }; + } + if (payload instanceof Error) { + return { ctx: undefined, error: extractError(payload) }; + } + if (typeof payload === 'object') { + // bunyan-style: a `{ error: Error }` field is hoisted out, stack extracted. + const obj = payload as Record; + if (obj['error'] instanceof Error) { + const { error: errValue, ...rest } = obj; + return { ctx: rest as LogContext, error: extractError(errValue) }; + } + return { ctx: obj as LogContext, error: undefined }; + } + if ( + typeof payload === 'string' || + typeof payload === 'number' || + typeof payload === 'boolean' || + typeof payload === 'bigint' || + typeof payload === 'symbol' + ) { + return { ctx: { reason: String(payload) }, error: undefined }; + } + if (typeof payload === 'function') { + const reason = payload.name === '' ? '[Function]' : `[Function: ${payload.name}]`; + return { ctx: { reason }, error: undefined }; + } + return { ctx: { reason: Object.prototype.toString.call(payload) }, error: undefined }; +} + +function mergeCtx( + payloadCtx: LogContext | undefined, + boundCtx: LogContext, +): LogContext | undefined { + const boundHasKeys = Object.keys(boundCtx).length > 0; + if (!boundHasKeys) return payloadCtx; + if (payloadCtx === undefined) return { ...boundCtx }; + return { ...payloadCtx, ...boundCtx }; +} + +/** + * Root logger. Import and use directly for events that don't belong to any + * session (CLI startup, harness construction, etc.): + * + * import { log } from 'kimi-code-sdk'; + * log.info('kimi-code starting', { version }); + * + * For events scoped to a session or agent, use the parent's `log` field: + * + * session.log.error('mcp initial load failed', error); + * agent.log.error('turn failed', { turnId, error }); + * + * Late-binding: methods look up the current `RootLogger` on every call, so + * importing `log` at module load (before `KimiHarness` configures the root) + * is safe — calls during the pre-configure window are silent noops. + */ +export const log: Logger = new LoggerImpl({}); + +export function redact(value: T): T { + if (value === null || typeof value !== 'object') return value; + return redactCtx({ value: value as unknown })['value'] as T; +} + +/** @internal — vitest only. */ +export async function __resetRootLoggerForTest(): Promise { + const globalAny = globalThis as Record; + const existing = globalAny[ROOT_SYMBOL]; + if (existing instanceof RootLoggerImpl) { + await existing.__shutdownForTest(); + } + globalAny[ROOT_SYMBOL] = undefined; +} + +export function resolveGlobalLogPath(homeDir: string): string { + return join(homeDir, 'logs', 'kimi-code.log'); +} diff --git a/packages/agent-core/src/logging/resolve-config.ts b/packages/agent-core/src/logging/resolve-config.ts new file mode 100644 index 000000000..6bf66cde0 --- /dev/null +++ b/packages/agent-core/src/logging/resolve-config.ts @@ -0,0 +1,51 @@ +import { resolveGlobalLogPath } from './logger'; +import type { LogLevel, LoggingConfig } from './types'; + +export const DEFAULT_LOG_LEVEL: LogLevel = 'info'; +export const DEFAULT_GLOBAL_MAX_BYTES = 6 * 1024 * 1024; // 6 MB +export const DEFAULT_GLOBAL_FILES = 5; // 6 MB x 5 = 30 MB +export const DEFAULT_SESSION_MAX_BYTES = 5 * 1024 * 1024; // 5 MB +export const DEFAULT_SESSION_FILES = 3; // 5 MB x 3 = 15 MB + +export interface ResolveLoggingInput { + readonly homeDir: string; + readonly env?: NodeJS.ProcessEnv | undefined; +} + +/** + * Build the runtime `LoggingConfig` from env vars + defaults. + * + * v1 deliberately does not read `config.toml [logging]` — the schema is in + * flux and reading it adds a startup-time failure surface. Users who need to + * override the defaults set env vars: + * + * KIMI_LOG_LEVEL=debug + * KIMI_LOG_GLOBAL_MAX_BYTES=... KIMI_LOG_GLOBAL_FILES=... + * KIMI_LOG_SESSION_MAX_BYTES=... KIMI_LOG_SESSION_FILES=... + */ +export function resolveLoggingConfig(input: ResolveLoggingInput): LoggingConfig { + const env = input.env ?? process.env; + return { + level: parseLevel(env['KIMI_LOG_LEVEL']) ?? DEFAULT_LOG_LEVEL, + globalLogPath: resolveGlobalLogPath(input.homeDir), + globalMaxBytes: parsePositiveInt(env['KIMI_LOG_GLOBAL_MAX_BYTES']) ?? DEFAULT_GLOBAL_MAX_BYTES, + globalFiles: parsePositiveInt(env['KIMI_LOG_GLOBAL_FILES']) ?? DEFAULT_GLOBAL_FILES, + sessionMaxBytes: + parsePositiveInt(env['KIMI_LOG_SESSION_MAX_BYTES']) ?? DEFAULT_SESSION_MAX_BYTES, + sessionFiles: parsePositiveInt(env['KIMI_LOG_SESSION_FILES']) ?? DEFAULT_SESSION_FILES, + }; +} + +function parseLevel(value: string | undefined): LogLevel | undefined { + if (value === undefined) return undefined; + const v = value.toLowerCase().trim(); + if (v === 'off' || v === 'error' || v === 'warn' || v === 'info' || v === 'debug') return v; + return undefined; +} + +function parsePositiveInt(value: string | undefined): number | undefined { + if (value === undefined) return undefined; + const n = Number.parseInt(value, 10); + if (!Number.isFinite(n) || n <= 0) return undefined; + return n; +} diff --git a/packages/agent-core/src/logging/sinks.ts b/packages/agent-core/src/logging/sinks.ts new file mode 100644 index 000000000..ed7c07016 --- /dev/null +++ b/packages/agent-core/src/logging/sinks.ts @@ -0,0 +1,224 @@ +import { mkdir, open, rename, stat, unlink } from 'node:fs/promises'; +import { appendFileSync, mkdirSync } from 'node:fs'; +import { dirname } from 'node:path'; + +import { syncDir } from '#/utils/fs'; + +export const PENDING_MAX = 1000; +const STDERR_NOTICE_INTERVAL_MS = 30_000; + +class AsyncSerialQueue { + private tail: Promise = Promise.resolve(); + + run(task: () => Promise): Promise { + const next = this.tail.then(task, task); + this.tail = next.catch(() => {}); + return next; + } +} +export interface Sink { + enqueue(line: string): void; + /** Resolves to false when the pending batch could not be written. */ + flush(): Promise; + close(): Promise; + flushSync(): void; +} + +interface RotatingFileSinkOptions { + readonly path: string; + readonly maxBytes: number; + readonly files: number; +} + +export class RotatingFileSink implements Sink { + private readonly queue = new AsyncSerialQueue(); + private pending: string[] = []; + private dropped = 0; + private closed = false; + private failed = false; + private lastStderrNotice = 0; + private currentBytes = -1; + private directorySynced = false; + + constructor(private readonly options: RotatingFileSinkOptions) {} + + enqueue(line: string): void { + if (this.closed) return; + if (this.pending.length >= PENDING_MAX) { + this.pending.shift(); + this.dropped++; + } + this.pending.push(line); + this.scheduleDrain(); + } + + async flush(): Promise { + return this.queue.run(() => this.drain()); + } + + async close(): Promise { + if (this.closed) return; + this.closed = true; + try { + await this.flush(); + } catch { + // swallow — close must not throw + } + } + + flushSync(): void { + if (this.closed || this.pending.length === 0) return; + try { + mkdirSync(dirname(this.options.path), { recursive: true }); + const body = this.pending.join('') + this.takeDroppedNotice(); + this.pending = []; + appendFileSync(this.options.path, body); + } catch (error) { + this.noteFailure(error); + } + } + + private scheduleDrain(): void { + if (this.closed) return; + queueMicrotask(() => { + if (this.closed || this.pending.length === 0) return; + this.queue.run(() => this.drain()).catch(() => {}); + }); + } + + private async drain(): Promise { + if (this.pending.length === 0) return true; + const droppedLine = this.takeDroppedNotice(); + const lines = droppedLine === '' ? [...this.pending] : [...this.pending, droppedLine]; + this.pending = []; + try { + await mkdir(dirname(this.options.path), { recursive: true }); + if (this.currentBytes < 0) { + this.currentBytes = await this.statSize(this.options.path); + } + await this.appendLines(lines); + + if (!this.directorySynced) { + await syncDir(dirname(this.options.path)); + this.directorySynced = true; + } + + this.failed = false; + return true; + } catch (error) { + this.noteFailure(error); + this.restorePending(lines); + return false; + } + } + + private restorePending(lines: readonly string[]): void { + const restored = [...lines, ...this.pending]; + const overflow = restored.length - PENDING_MAX; + if (overflow <= 0) { + this.pending = restored; + return; + } + this.dropped += overflow; + this.pending = restored.slice(overflow); + } + + private async appendLines(lines: readonly string[]): Promise { + let chunk = ''; + let chunkBytes = 0; + for (const line of lines) { + const lineBytes = Buffer.byteLength(line, 'utf-8'); + if ( + chunkBytes > 0 && + (chunkBytes + lineBytes > this.options.maxBytes || + this.currentBytes + chunkBytes + lineBytes > this.options.maxBytes) + ) { + await this.appendChunk(chunk); + chunk = ''; + chunkBytes = 0; + } + + if ( + chunkBytes === 0 && + this.currentBytes > 0 && + this.currentBytes + lineBytes > this.options.maxBytes + ) { + await this.rotate(); + } + + chunk += line; + chunkBytes += lineBytes; + } + if (chunkBytes > 0) { + await this.appendChunk(chunk); + } + } + + private async appendChunk(chunk: string): Promise { + const fh = await open(this.options.path, 'a'); + try { + await fh.appendFile(chunk, 'utf-8'); + await fh.sync(); + } finally { + await fh.close(); + } + this.currentBytes += Buffer.byteLength(chunk, 'utf-8'); + if (this.currentBytes >= this.options.maxBytes) { + await this.rotate(); + } + } + + private async rotate(): Promise { + const { path, files } = this.options; + for (let i = files - 2; i >= 1; i--) { + const from = `${path}.${i}`; + const to = `${path}.${i + 1}`; + try { + await rename(from, to); + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; + } + } + try { + await rename(path, `${path}.1`); + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; + } + // last archive may be evicted; ensure we don't keep > files + try { + await unlink(`${path}.${files}`); + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; + } + this.currentBytes = 0; + this.directorySynced = false; + } + + private async statSize(p: string): Promise { + try { + const s = await stat(p); + return s.size; + } catch (error) { + if ((error as NodeJS.ErrnoException).code === 'ENOENT') return 0; + throw error; + } + } + + private takeDroppedNotice(): string { + if (this.dropped === 0) return ''; + const line = `... dropped ${this.dropped} entries ...\n`; + this.dropped = 0; + return line; + } + + private noteFailure(error: unknown): void { + this.failed = true; + const now = Date.now(); + if (now - this.lastStderrNotice < STDERR_NOTICE_INTERVAL_MS) return; + this.lastStderrNotice = now; + const code = (error as NodeJS.ErrnoException)?.code ?? 'UNKNOWN'; + try { + process.stderr.write(`[logger] write failed: ${code}\n`); + } catch {} + } +} diff --git a/packages/agent-core/src/logging/types.ts b/packages/agent-core/src/logging/types.ts new file mode 100644 index 000000000..0a97b3959 --- /dev/null +++ b/packages/agent-core/src/logging/types.ts @@ -0,0 +1,91 @@ +export type LogLevel = 'off' | 'error' | 'warn' | 'info' | 'debug'; + +export type LogContext = Record; + +/** + * Second argument to `log.error / warn / info / debug`. + * + * Three usage shapes, detected at runtime: + * - `Error` → stack is extracted onto the entry + * - `LogContext` (object) → merged into entry context; if it contains + * `{ error: Error }`, that field is pulled out + * and its stack extracted (bunyan-style) + * - `unknown` → typically a `catch` binding; treated as an Error if + * it's an Error instance, otherwise stringified into a + * `reason` field + */ +export type LogPayload = unknown; + +export interface Logger { + error(message: string, payload?: LogPayload): void; + warn(message: string, payload?: LogPayload): void; + info(message: string, payload?: LogPayload): void; + debug(message: string, payload?: LogPayload): void; + /** + * Returns a new logger that adds `ctx` to every entry it emits. The bound + * context wins over per-call payload context, so callers can't accidentally + * overwrite ownership fields like `sessionId` / `agentId`: + * + * finalCtx = { ...payloadCtx, ...boundCtx } + * + * Children chain — `parent.createChild({a: 1}).createChild({b: 2})` binds + * both. + */ + createChild(ctx: LogContext): Logger; +} + +export interface LogEntry { + readonly t: number; + readonly level: Exclude; + readonly msg: string; + readonly ctx?: LogContext | undefined; + readonly error?: { readonly message: string; readonly stack?: string } | undefined; + readonly sessionId?: string | undefined; + readonly sessionLogId?: string | undefined; +} + +export interface LoggingConfig { + readonly level: LogLevel; + readonly globalLogPath: string; + readonly globalMaxBytes: number; + readonly globalFiles: number; + readonly sessionMaxBytes: number; + readonly sessionFiles: number; +} + +export interface SessionLogHandle { + readonly logger: Logger; + flush(): Promise; + close(): Promise; +} + +export interface SessionAttachInput { + readonly sessionId: string; + readonly sessionDir: string; +} + +export interface RootLogger { + configure(config: LoggingConfig): Promise; + attachSession(input: SessionAttachInput): SessionLogHandle; + /** False if any sink could not flush its pending batch. */ + flush(): Promise; + /** False if the global sink could not flush; true when there is no global sink. */ + flushGlobal(): Promise; + /** False if the session sink could not flush; true when there is no active sink. */ + flushSession(sessionId: string): Promise; + flushSync(): void; + isConfigured(): boolean; + getConfig(): LoggingConfig | undefined; +} + +export const LOG_LEVEL_RANK: Record = { + off: 0, + error: 1, + warn: 2, + info: 3, + debug: 4, +}; + +export function levelEnabled(threshold: LogLevel, level: Exclude): boolean { + return LOG_LEVEL_RANK[threshold] >= LOG_LEVEL_RANK[level]; +} diff --git a/packages/agent-core/src/loop/README.md b/packages/agent-core/src/loop/README.md new file mode 100644 index 000000000..0b2daf030 --- /dev/null +++ b/packages/agent-core/src/loop/README.md @@ -0,0 +1,57 @@ +# Loop + +`loop` is the stateless agent loop. It does not own sessions, wire +transport, compaction execution, permissions UI, or durable protocol +bridging. Those are host-layer responsibilities. + +## Internal Owners + +- `run-turn.ts` owns turn-level convergence: abort and compaction safe + points, max-step enforcement, usage aggregation, optional continuation after + non-tool stops, and final `TurnResult` mapping. +- `turn-step.ts` owns one provider step: pre/post step hooks, message + construction, the atomic step envelope, LLM call, streaming callback + wiring, and the handoff to the tool-call lifecycle. +- `tool-call.ts` owns the tool-call batch lifecycle. Classification is + pure; preparation dispatches recorded `tool.call` events in provider + order; terminal `tool.result` events are recorded in provider order before + `step.end` seals the step. +- `tool-scheduler.ts` owns stateful tool execution scheduling: + tasks with non-conflicting resource accesses may overlap, while conflicting + tasks are serialized at provider-order boundaries. +- `llm.ts`, `events.ts`, and `types.ts` define the narrow model, + event/transcript, message, and tool surfaces that hosts provide to the loop. + +## Contracts + +- The core loop must not import from host-layer implementations. +- `LLM` is the only source of model metadata, optional capability metadata, + and system prompt. +- `buildMessages` builds the latest model-visible messages per model step. +- `dispatchEvent` is the only event path the loop writes to. The dispatcher + records `LoopRecordedEvent`s, publishes `LoopLiveOnlyEvent`s, and routes + shared events such as `step.begin`, `step.end`, `tool.call`, and + `tool.result` to both transcript and live listeners. +- Live event listener failures are contained by `LoopEventDispatcher` and + must not affect the agent loop. +- Provider usage is recorded immediately after `LLM.chat` returns, not + after tool execution completes. Aborted tool execution must still report + spent LLM usage. +- The transcript step envelope is intentionally partial on provider abort: + `step.begin` may exist without `step.end`. +- Every dispatched `tool.call` must be followed by a matching `tool.result` + unless the step is interrupted before the result dispatch point. + +## Test Boundaries + +The main regression guards live in `test/loop`: + +- `turn-lifecycle.e2e.test.ts` covers turn convergence and usage + aggregation. +- `transcript.e2e.test.ts` covers step ordering and durable transcript + linkage. +- `tool-call.e2e.test.ts` and `hooks.e2e.test.ts` cover tool preparation, + description, result finalization, and hook safety points. +- `abort.e2e.test.ts`, `error-paths.e2e.test.ts`, and `events.e2e.test.ts` + cover abort convergence, error propagation, and live event containment. +- `streaming.e2e.test.ts` covers provider streaming callback wiring. diff --git a/packages/agent-core/src/loop/errors.ts b/packages/agent-core/src/loop/errors.ts new file mode 100644 index 000000000..43f7199d7 --- /dev/null +++ b/packages/agent-core/src/loop/errors.ts @@ -0,0 +1,27 @@ +/** + * Loop-local error helpers. + */ + +import { ErrorCodes, KimiError, isKimiError } from '#/errors'; + +export function createMaxStepsExceededError(maxSteps: number, message?: string): KimiError { + return new KimiError(ErrorCodes.LOOP_MAX_STEPS_EXCEEDED, message ?? `Turn exceeded maxSteps=${maxSteps}`, { + details: { maxSteps }, + }); +} + +export function isMaxStepsExceededError(error: unknown): boolean { + return isKimiError(error) && error.code === ErrorCodes.LOOP_MAX_STEPS_EXCEEDED; +} + +export function isAbortError(err: unknown): boolean { + if (err instanceof Error) { + return err.name === 'AbortError'; + } + return false; +} + +export function errorMessage(err: unknown): string { + if (err instanceof Error) return err.message; + return String(err); +} diff --git a/packages/agent-core/src/loop/events.ts b/packages/agent-core/src/loop/events.ts new file mode 100644 index 000000000..640ee7af4 --- /dev/null +++ b/packages/agent-core/src/loop/events.ts @@ -0,0 +1,182 @@ +import type { FinishReason, TextPart, ThinkPart, TokenUsage } from '@moonshot-ai/kosong'; + +import type { ToolInputDisplay } from '../tools/display'; +import type { ExecutableToolResult, LoopStepStopReason, ToolUpdate } from './types'; + +export type LoopInterruptReason = 'aborted' | 'max_steps' | 'error'; + +export interface LoopStepBeginEvent { + readonly type: 'step.begin'; + readonly uuid: string; + readonly turnId: string; + readonly step: number; +} + +export interface LoopStepEndEvent { + readonly type: 'step.end'; + readonly uuid: string; + readonly turnId: string; + readonly step: number; + readonly usage?: TokenUsage | undefined; + readonly finishReason?: LoopStepStopReason | undefined; + /** + * Provider diagnostics are optional and must not drive loop control. + * Use `finishReason` for normalized behavior. + */ + readonly providerFinishReason?: FinishReason | undefined; + readonly rawFinishReason?: string | undefined; +} + +export interface LoopStepRetryingEvent { + readonly type: 'step.retrying'; + readonly turnId: string; + readonly step: number; + readonly stepUuid: string; + readonly failedAttempt: number; + readonly nextAttempt: number; + readonly maxAttempts: number; + readonly delayMs: number; + readonly errorName: string; + readonly errorMessage: string; + readonly statusCode?: number; +} + +export interface LoopContentPartEvent { + readonly type: 'content.part'; + readonly uuid: string; + readonly turnId: string; + readonly step: number; + readonly stepUuid: string; + readonly part: TextPart | ThinkPart; +} + +export interface LoopToolCallEvent { + readonly type: 'tool.call'; + readonly uuid: string; + readonly turnId: string; + readonly step: number; + readonly stepUuid: string; + readonly toolCallId: string; + readonly name: string; + readonly args: unknown; + readonly description?: string | undefined; + readonly display?: ToolInputDisplay | undefined; +} + +export interface LoopToolResultEvent { + readonly type: 'tool.result'; + readonly parentUuid: string; + readonly toolCallId: string; + readonly result: ExecutableToolResult; +} + +export interface LoopTurnInterruptedEvent { + readonly type: 'turn.interrupted'; + readonly reason: LoopInterruptReason; + readonly attemptedSteps: number; + readonly activeStep?: number | undefined; + readonly message?: string | undefined; +} + +export interface LoopTextDeltaEvent { + readonly type: 'text.delta'; + readonly delta: string; +} + +export interface LoopThinkingDeltaEvent { + readonly type: 'thinking.delta'; + readonly delta: string; +} + +export interface LoopToolCallDeltaEvent { + readonly type: 'tool.call.delta'; + readonly toolCallId: string; + readonly name?: string | undefined; + readonly argumentsPart?: string | undefined; +} + +export interface LoopToolProgressEvent { + readonly type: 'tool.progress'; + readonly toolCallId: string; + readonly update: ToolUpdate; +} + +export type LoopRecordedEvent = + | LoopStepBeginEvent + | LoopStepEndEvent + | LoopContentPartEvent + | LoopToolCallEvent + | LoopToolResultEvent; + +export type LoopLiveOnlyEvent = + | LoopTurnInterruptedEvent + | LoopStepRetryingEvent + | LoopTextDeltaEvent + | LoopThinkingDeltaEvent + | LoopToolCallDeltaEvent + | LoopToolProgressEvent; + +export type LoopEvent = LoopRecordedEvent | LoopLiveOnlyEvent; +export type LoopLiveEventEmitter = (event: LoopEvent) => void; + +export type LoopEventDispatcher = { + (event: LoopRecordedEvent): Promise; + (event: LoopLiveOnlyEvent): void; +}; + +export interface CreateLoopEventDispatcherInput { + readonly appendTranscriptRecord: (record: LoopRecordedEvent) => Promise; + readonly emitLiveEvent?: LoopLiveEventEmitter | undefined; +} + +export function createLoopEventDispatcher( + input: CreateLoopEventDispatcherInput, +): LoopEventDispatcher { + function dispatchEvent(event: LoopRecordedEvent): Promise; + function dispatchEvent(event: LoopLiveOnlyEvent): void; + function dispatchEvent(event: LoopEvent): Promise | void { + if (isRecordedEvent(event)) { + return recordEvent(input, event); + } + safeEmitLive(input.emitLiveEvent, event); + } + return dispatchEvent; +} + +function isRecordedEvent(event: LoopEvent): event is LoopRecordedEvent { + return ( + event.type === 'step.begin' || + event.type === 'step.end' || + event.type === 'content.part' || + event.type === 'tool.call' || + event.type === 'tool.result' + ); +} + +async function recordEvent( + input: CreateLoopEventDispatcherInput, + event: LoopRecordedEvent, +): Promise { + await input.appendTranscriptRecord(event); + safeEmitLive(input.emitLiveEvent, event); +} + +function safeEmitLive(emit: LoopLiveEventEmitter | undefined, event: LoopEvent): void { + if (emit === undefined) return; + let maybePromise: unknown; + try { + maybePromise = (emit as (event: LoopEvent) => unknown)(event); + } catch { + return; + } + if ( + maybePromise !== undefined && + maybePromise !== null && + typeof (maybePromise as { then?: unknown }).then === 'function' && + typeof (maybePromise as { catch?: unknown }).catch === 'function' + ) { + (maybePromise as Promise).catch(() => { + // Live listeners are best-effort; their failures must not affect the turn. + }); + } +} diff --git a/packages/agent-core/src/loop/index.ts b/packages/agent-core/src/loop/index.ts new file mode 100644 index 000000000..c0071df54 --- /dev/null +++ b/packages/agent-core/src/loop/index.ts @@ -0,0 +1,70 @@ +/** + * Public entry point for the stateless agent loop. + * + * Higher-level orchestration may import from this module; this module must not + * import from host-layer implementations. + */ + +export type { + AfterStepHook, + BeforeStepResult, + BeforeStepHook, + LoopHooks, + LoopAfterStepContext, + LoopStepHookContext, + LoopStepStopReason, + LoopStoppedStepContext, + LoopTerminalStepStopReason, + LoopTurnStopReason, + StopReason, + ShouldContinueAfterStopHook, + ShouldContinueAfterStopResult, + LoopMessageBuilder, + ExecutableTool, + ToolExecution, + ToolCall, + ExecutableToolContext, + ToolExecutionHookContext, + PrepareToolExecutionHook, + PrepareToolExecutionResult, + ExecutableToolResult, + FinalizeToolResultContext, + FinalizeToolResultHook, + ToolUpdate, + TurnResult, +} from './types'; + +export { ToolAccesses } from './tool-access'; + +export type { + CreateLoopEventDispatcherInput, + LoopContentPartEvent, + LoopRecordedEvent, + LoopStepBeginEvent, + LoopStepEndEvent, + LoopStepRetryingEvent, + LoopLiveOnlyEvent, + LoopEvent, + LoopInterruptReason, + LoopLiveEventEmitter, + LoopEventDispatcher, + LoopTextDeltaEvent, + LoopThinkingDeltaEvent, + LoopToolCallDeltaEvent, + LoopToolCallEvent, + LoopToolProgressEvent, + LoopToolResultEvent, + LoopTurnInterruptedEvent, +} from './events'; +export { createLoopEventDispatcher } from './events'; + +export type { + LLM, + LLMChatParams, + LLMChatResponse, + LLMRequestLogContext, + ToolCallDelta, +} from './llm'; + +export { runTurn } from './run-turn'; +export type { RunTurnInput } from './run-turn'; diff --git a/packages/agent-core/src/loop/llm.ts b/packages/agent-core/src/loop/llm.ts new file mode 100644 index 000000000..df8314593 --- /dev/null +++ b/packages/agent-core/src/loop/llm.ts @@ -0,0 +1,71 @@ +/** + * LLM contract for the model capability used by the stateless loop. + * + * The immutable `LLM` object owns provider/model metadata, capability metadata, + * and the system prompt. Other host concerns are injected through separate + * surfaces. + */ + +import type { + FinishReason, + Message, + ModelCapability, + TextPart, + ThinkPart, + TokenUsage, + Tool, + ToolCall, +} from '@moonshot-ai/kosong'; + +export interface ToolCallDelta { + readonly toolCallId: string; + readonly name?: string | undefined; + readonly argumentsPart?: string | undefined; +} + +export interface LLMRequestLogContext { + readonly turnId?: string; + readonly step?: number; + readonly stepUuid?: string; + readonly attempt?: number; + readonly maxAttempts?: number; +} + +export interface LLMChatParams { + messages: Message[]; + tools: readonly Tool[]; + signal: AbortSignal; + requestLogContext?: LLMRequestLogContext; + onTextDelta?: ((delta: string) => void) | undefined; + onThinkDelta?: ((delta: string) => void) | undefined; + onToolCallDelta?: ((delta: ToolCallDelta) => void) | undefined; + /** + * Fires once per completed text block. Additive relative to + * `onTextDelta` — deltas still fire chunk-by-chunk for UI streaming. + * Returned promises are awaited by the adapter to preserve transcript append + * order. Durable transcript writes receive completed blocks only. + */ + onTextPart?: ((part: TextPart) => Promise | void) | undefined; + /** + * Fires once per completed thinking block. Additive relative to + * `onThinkDelta` — deltas still fire chunk-by-chunk for UI streaming. + * Returned promises are awaited by the adapter to preserve transcript append + * order. Durable transcript writes receive completed blocks only. + */ + onThinkPart?: ((part: ThinkPart) => Promise | void) | undefined; +} + +export interface LLMChatResponse { + toolCalls: ToolCall[]; + providerFinishReason?: FinishReason | undefined; + rawFinishReason?: string | undefined; + usage: TokenUsage; +} + +export interface LLM { + readonly systemPrompt: string; + readonly modelName: string; + readonly capability?: ModelCapability | undefined; + isRetryableError?(error: unknown): boolean; + chat(params: LLMChatParams): Promise; +} diff --git a/packages/agent-core/src/loop/retry.ts b/packages/agent-core/src/loop/retry.ts new file mode 100644 index 000000000..eba9247bd --- /dev/null +++ b/packages/agent-core/src/loop/retry.ts @@ -0,0 +1,137 @@ +import { sleep } from '@antfu/utils'; +import * as retry from 'retry'; + +import type { Logger } from '#/logging/types'; + +import { abortable } from '../utils/abort'; +import type { LoopEventDispatcher } from './events'; +import { isAbortError } from './errors'; +import type { LLM, LLMChatParams, LLMChatResponse } from './llm'; + +export const DEFAULT_MAX_RETRY_ATTEMPTS = 3; + +const RETRY_MIN_TIMEOUT_MS = 300; +const RETRY_MAX_TIMEOUT_MS = 5000; +const RETRY_FACTOR = 2; + +export interface ChatWithRetryInput { + readonly llm: LLM; + readonly params: LLMChatParams; + readonly dispatchEvent: LoopEventDispatcher; + readonly turnId: string; + readonly currentStep: number; + readonly stepUuid: string; + readonly maxAttempts?: number; + readonly log?: Logger | undefined; +} + +export async function chatWithRetry(input: ChatWithRetryInput): Promise { + const maxAttempts = input.maxAttempts ?? DEFAULT_MAX_RETRY_ATTEMPTS; + + if (input.llm.isRetryableError === undefined || maxAttempts <= 1) { + const effectiveMaxAttempts = Math.max(maxAttempts, 1); + try { + return await input.llm.chat(paramsForAttempt(input, 1, effectiveMaxAttempts)); + } catch (error) { + logRequestFailure(input, error, 1, effectiveMaxAttempts); + throw error; + } + } + + const delays = retryBackoffDelays(maxAttempts); + + for (let attempt = 1; ; attempt += 1) { + try { + return await input.llm.chat(paramsForAttempt(input, attempt, maxAttempts)); + } catch (error) { + if (attempt >= maxAttempts || !input.llm.isRetryableError(error)) { + logRequestFailure(input, error, attempt, maxAttempts); + throw error; + } + + const delayMs = delays[attempt - 1] ?? 0; + input.params.signal.throwIfAborted(); + input.dispatchEvent({ + type: 'step.retrying', + turnId: input.turnId, + step: input.currentStep, + stepUuid: input.stepUuid, + failedAttempt: attempt, + nextAttempt: attempt + 1, + maxAttempts, + delayMs, + ...retryErrorFields(error), + }); + await sleepForRetry(delayMs, input.params.signal); + } + } +} + +function logRequestFailure( + input: ChatWithRetryInput, + error: unknown, + attempt: number, + maxAttempts: number, +): void { + if (isAbortError(error) || input.params.signal.aborted) return; + input.log?.warn('llm request failed', { + turnId: input.turnId, + step: input.currentStep, + attempt, + maxAttempts, + model: input.llm.modelName, + ...retryErrorFields(error), + }); +} + +function paramsForAttempt( + input: ChatWithRetryInput, + attempt: number, + maxAttempts: number, +): LLMChatParams { + return { + ...input.params, + requestLogContext: { + turnId: input.turnId, + step: input.currentStep, + stepUuid: input.stepUuid, + attempt, + maxAttempts, + }, + }; +} + +export function retryBackoffDelays(maxAttempts: number): number[] { + return retry.timeouts({ + retries: Math.max(maxAttempts - 1, 0), + minTimeout: RETRY_MIN_TIMEOUT_MS, + maxTimeout: RETRY_MAX_TIMEOUT_MS, + factor: RETRY_FACTOR, + randomize: true, + }); +} + +export async function sleepForRetry(delayMs: number, signal: AbortSignal): Promise { + signal.throwIfAborted(); + await abortable(sleep(delayMs), signal); +} + +interface RetryErrorFields { + readonly errorName: string; + readonly errorMessage: string; + readonly statusCode?: number; +} + +function retryErrorFields(error: unknown): RetryErrorFields { + return { + errorName: error instanceof Error ? error.name : typeof error, + errorMessage: error instanceof Error ? error.message : String(error), + statusCode: maybeStatusCode(error), + }; +} + +function maybeStatusCode(error: unknown): number | undefined { + if (typeof error !== 'object' || error === null) return undefined; + const statusCode = (error as { statusCode?: unknown }).statusCode; + return typeof statusCode === 'number' ? statusCode : undefined; +} diff --git a/packages/agent-core/src/loop/run-turn.ts b/packages/agent-core/src/loop/run-turn.ts new file mode 100644 index 000000000..92e9e6f32 --- /dev/null +++ b/packages/agent-core/src/loop/run-turn.ts @@ -0,0 +1,141 @@ +/** + * Turn-level loop for a stateless agent run. + * + * Owns convergence across steps: abort checks at loop boundaries, max-step + * enforcement, usage aggregation, optional continuation after non-tool stops, + * and final `TurnResult` mapping. One-step execution lives in `turn-step.ts`. + */ + +import { addUsage, emptyUsage, type TokenUsage } from '@moonshot-ai/kosong'; + +import type { Logger } from '#/logging/types'; + +import { + createMaxStepsExceededError, + errorMessage, + isAbortError, + isMaxStepsExceededError, +} from './errors'; +import type { LoopInterruptReason, LoopEventDispatcher, LoopTurnInterruptedEvent } from './events'; +import type { LLM } from './llm'; +import { executeLoopStep } from './turn-step'; +import type { + ExecutableTool, + LoopHooks, + LoopMessageBuilder, + LoopTerminalStepStopReason, + LoopTurnStopReason, + TurnResult, +} from './types'; + +const DEFAULT_MAX_STEPS = 1000; + +export interface RunTurnInput { + readonly turnId: string; + readonly signal: AbortSignal; + readonly llm: LLM; + readonly buildMessages: LoopMessageBuilder; + readonly dispatchEvent: LoopEventDispatcher; + readonly tools?: readonly ExecutableTool[] | undefined; + readonly hooks?: LoopHooks | undefined; + readonly log?: Logger | undefined; + readonly maxSteps?: number | undefined; + readonly maxRetryAttempts?: number; +} + +export async function runTurn(input: RunTurnInput): Promise { + const { + turnId, + signal, + llm, + buildMessages, + dispatchEvent, + tools, + hooks, + log, + maxSteps = DEFAULT_MAX_STEPS, + maxRetryAttempts, + } = input; + let usage: TokenUsage = emptyUsage(); + let steps = 0; + // Normal exits overwrite this with the completed step's stop reason. + let stopReason: LoopTurnStopReason = 'end_turn'; + let activeStep: number | undefined; + const recordStepUsage = (stepUsage: TokenUsage): void => { + usage = addUsage(usage, stepUsage); + }; + + try { + while (true) { + signal.throwIfAborted(); + + if (steps >= maxSteps) { + throw createMaxStepsExceededError(maxSteps); + } + + steps += 1; + activeStep = steps; + const stepResult = await executeLoopStep({ + turnId, + signal, + buildMessages, + dispatchEvent, + llm, + tools, + hooks, + log, + currentStep: steps, + maxRetryAttempts, + recordUsage: recordStepUsage, + }); + activeStep = undefined; + + if (stepResult.stopReason === 'tool_use') { + continue; + } + + const terminalStopReason: LoopTerminalStepStopReason = stepResult.stopReason; + stopReason = terminalStopReason; + + if ( + !( + await hooks?.shouldContinueAfterStop?.({ + turnId, + stepNumber: steps, + usage: stepResult.usage, + stopReason: terminalStopReason, + signal, + llm, + }) + )?.continue + ) { + break; + } + } + } catch (error) { + if (isAbortError(error) || signal.aborted) { + dispatchEvent(makeInterruptedEvent('aborted', steps, activeStep)); + return { stopReason: 'aborted', steps, usage }; + } + const reason: LoopInterruptReason = isMaxStepsExceededError(error) ? 'max_steps' : 'error'; + dispatchEvent(makeInterruptedEvent(reason, steps, activeStep, errorMessage(error))); + throw error; + } + + return { stopReason, steps, usage }; +} + +function makeInterruptedEvent( + reason: LoopInterruptReason, + attemptedSteps: number, + activeStep: number | undefined, + message?: string | undefined, +): LoopTurnInterruptedEvent { + return { + type: 'turn.interrupted', + reason, + attemptedSteps, + ...(activeStep !== undefined ? { activeStep } : {}), + ...(message !== undefined ? { message } : {}), + }; +} diff --git a/packages/agent-core/src/loop/tool-access.ts b/packages/agent-core/src/loop/tool-access.ts new file mode 100644 index 000000000..f14eecb3e --- /dev/null +++ b/packages/agent-core/src/loop/tool-access.ts @@ -0,0 +1,150 @@ +export type ToolFileAccessOperation = 'read' | 'write' | 'readwrite' | 'search'; + +export type ToolResourceAccess = + | { + readonly kind: 'file'; + readonly operation: ToolFileAccessOperation; + /** + * Canonical file path when the access can be narrowed. Omitted means the + * operation may touch any file in the backend file namespace. + */ + readonly path?: string; + readonly recursive?: boolean; + } + | { + /** + * Arbitrary side effects or resources that cannot be represented as a + * file access. This is intentionally operation-less and globally + * exclusive for concurrency. + */ + readonly kind: 'all'; + }; + +export type ToolAccesses = readonly ToolResourceAccess[]; + +export const ToolAccesses = { + none(): ToolAccesses { + return []; + }, + + all(): ToolAccesses { + return [{ kind: 'all' }]; + }, + + file( + operation: ToolFileAccessOperation, + path?: string, + options: { readonly recursive?: boolean } = {}, + ): ToolAccesses { + const access: { + kind: 'file'; + operation: ToolFileAccessOperation; + path?: string; + recursive?: boolean; + } = { kind: 'file', operation }; + if (path !== undefined) access.path = path; + if (options.recursive !== undefined) access.recursive = options.recursive; + return [access]; + }, + + readFile(path: string): ToolAccesses { + return ToolAccesses.file('read', path); + }, + + readTree(path: string): ToolAccesses { + return ToolAccesses.file('read', path, { recursive: true }); + }, + + readAnyFile(): ToolAccesses { + return ToolAccesses.file('read'); + }, + + writeFile(path: string): ToolAccesses { + return ToolAccesses.file('write', path); + }, + + writeTree(path: string): ToolAccesses { + return ToolAccesses.file('write', path, { recursive: true }); + }, + + writeAnyFile(): ToolAccesses { + return ToolAccesses.file('write'); + }, + + readWriteFile(path: string): ToolAccesses { + return ToolAccesses.file('readwrite', path); + }, + + readWriteTree(path: string): ToolAccesses { + return ToolAccesses.file('readwrite', path, { recursive: true }); + }, + + readWriteAnyFile(): ToolAccesses { + return ToolAccesses.file('readwrite'); + }, + + searchTree(path: string): ToolAccesses { + return ToolAccesses.file('search', path, { recursive: true }); + }, + + searchAnyFile(): ToolAccesses { + return ToolAccesses.file('search'); + }, + + conflict(left: ToolAccesses, right: ToolAccesses): boolean { + return left.some((leftAccess) => + right.some((rightAccess) => resourceAccessesConflict(leftAccess, rightAccess)), + ); + }, +}; + +function resourceAccessesConflict(left: ToolResourceAccess, right: ToolResourceAccess): boolean { + if (left.kind === 'all' || right.kind === 'all') return true; + if (!fileOperationsConflict(left.operation, right.operation)) return false; + return fileAccessesOverlap(left, right); +} + +function fileOperationsConflict( + left: ToolFileAccessOperation, + right: ToolFileAccessOperation, +): boolean { + return fileOperationWrites(left) || fileOperationWrites(right); +} + +function fileOperationWrites(operation: ToolFileAccessOperation): boolean { + switch (operation) { + case 'read': + case 'search': + return false; + case 'write': + case 'readwrite': + return true; + } +} + +function fileAccessesOverlap( + left: Extract, + right: Extract, +): boolean { + if (left.path === undefined || right.path === undefined) return true; + + const leftPath = normalizePath(left.path); + const rightPath = normalizePath(right.path); + if (leftPath === rightPath) return true; + + const leftPrefix = leftPath.endsWith('/') ? leftPath : `${leftPath}/`; + const rightPrefix = rightPath.endsWith('/') ? rightPath : `${rightPath}/`; + return ( + (left.recursive === true && rightPath.startsWith(leftPrefix)) || + (right.recursive === true && leftPath.startsWith(rightPrefix)) + ); +} + +function normalizePath(path: string): string { + const normalized = path.replaceAll('\\', '/').replaceAll(/\/+/g, '/'); + const folded = normalized.toLowerCase(); + if (folded.length > 1 && folded.endsWith('/')) { + return folded.slice(0, -1); + } + return folded; +} diff --git a/packages/agent-core/src/loop/tool-call.ts b/packages/agent-core/src/loop/tool-call.ts new file mode 100644 index 000000000..6981ebb51 --- /dev/null +++ b/packages/agent-core/src/loop/tool-call.ts @@ -0,0 +1,626 @@ +/** + * Tool-call lifecycle for one completed provider response. + * + * This module keeps the provider-order invariant in one place: + * - validate every provider tool call before hooks or events + * - run preparation hooks and compute tool-call display fields in provider order + * - dispatch `tool.call` before execution starts + * - execute tools with non-conflicting resource accesses concurrently + * - serialize tools whose resource accesses conflict + * - dispatch terminal `tool.result` events in provider order + * + * These phases are coupled by transcript ordering and abort handling, so they + * should be reviewed together. + */ + +import type { ContentPart } from '@moonshot-ai/kosong'; + +import type { Logger } from '#/logging/types'; +import { + compileToolArgsValidator, + validateToolArgs, + type JsonType, + type ToolArgsValidator, +} from '../tools/args-validator'; +import { PathSecurityError } from '../tools/policies/path-access'; + +import { errorMessage, isAbortError } from './errors'; +import type { LoopEventDispatcher, LoopToolCallEvent } from './events'; +import type { LLM, LLMChatResponse } from './llm'; +import { ToolAccesses } from './tool-access'; +import { ToolScheduler, type ToolCallTask } from './tool-scheduler'; +import type { + ExecutableTool, + LoopHooks, + ToolCall, + PrepareToolExecutionResult, + ExecutableToolResult, + RunnableToolExecution, + ToolExecution, +} from './types'; + +const GRACE_TIMEOUT_MS = 2_000; +const TOOL_OUTPUT_EMPTY = 'Tool output is empty.'; +const TOOL_OUTPUT_NON_TEXT = 'Tool returned non-text content.'; + +const validators = new WeakMap(); + +export interface ToolCallStepContext { + readonly tools?: readonly ExecutableTool[] | undefined; + readonly hooks?: LoopHooks | undefined; + readonly log?: Logger | undefined; + readonly dispatchEvent: LoopEventDispatcher; + readonly llm: LLM; + readonly signal: AbortSignal; + readonly turnId: string; + readonly currentStep: number; + readonly stepUuid: string; +} + +type PreflightedToolCall = RunnableToolCall | RejectedToolCall; + +interface RunnableToolCall { + readonly kind: 'runnable'; + readonly toolCall: ToolCall; + readonly toolName: string; + readonly tool: ExecutableTool; + readonly args: unknown; +} + +interface RejectedToolCall { + readonly kind: 'rejected'; + readonly toolCall: ToolCall; + readonly toolName: string; + readonly args: unknown; + readonly output: string; +} + +type PrepareToolExecutionDecision = + | { readonly kind: 'allowed'; readonly args: unknown; readonly metadata?: unknown } + | { readonly kind: 'synthetic'; readonly args: unknown; readonly result: ExecutableToolResult } + | { readonly kind: 'blocked'; readonly args: unknown; readonly output: string } + | { readonly kind: 'hookFailed'; readonly args: unknown; readonly output: string }; + +interface PendingToolResult { + readonly toolCall: ToolCall; + readonly toolName: string; + readonly args: unknown; + readonly result: ExecutableToolResult; + readonly stopTurn?: boolean | undefined; +} + +interface PreparedToolCallTask { + readonly task: ToolCallTask; + readonly stopBatchAfterThis?: boolean | undefined; +} + +type ToolCallDisplayFields = Pick; + +export interface ToolCallBatchResult { + readonly stopTurn: boolean; +} + +export async function runToolCallBatch( + step: ToolCallStepContext, + response: LLMChatResponse, +): Promise { + if (response.toolCalls.length === 0) return { stopTurn: false }; + const calls = response.toolCalls.map((toolCall) => preflightToolCall(step.tools, toolCall)); + const scheduler = new ToolScheduler(); + const pendingResults: Array> = []; + let stopTurn = false; + + try { + for (let index = 0; index < calls.length; index += 1) { + const call = calls[index]!; + const prepared = await prepareToolCall(step, call); + pendingResults.push(scheduler.add(prepared.task)); + + if (prepared.stopBatchAfterThis === true) { + stopTurn = true; + for (const skippedCall of calls.slice(index + 1)) { + const skippedTask = await prepareSkippedToolCall(step, skippedCall); + pendingResults.push(scheduler.add(skippedTask)); + } + break; + } + } + + // Tool tasks may finish out of order; terminal results are still emitted in + // provider order. Await all tasks so each recorded `tool.call` gets a + // paired `tool.result`; the caller checks abort before writing `step.end`. + for (const pendingResult of pendingResults) { + const result = await finalizePendingToolResult(step, await pendingResult); + if (result.stopTurn === true) stopTurn = true; + await step.dispatchEvent({ + type: 'tool.result', + parentUuid: result.toolCall.id, + toolCallId: result.toolCall.id, + result: result.result, + }); + } + } finally { + // Preparation or result dispatch can throw after execution has started. + // Always settle spawned tasks before the caller continues so rejected + // execute promises cannot surface as detached unhandled rejections. + await Promise.allSettled(pendingResults); + } + return { stopTurn }; +} + +/** + * Provider-order validation pass. It does not run hooks, spawn tools, or write + * events. Validator compilation may populate the local cache. + */ +function preflightToolCall( + tools: readonly ExecutableTool[] | undefined, + toolCall: ToolCall, +): PreflightedToolCall { + const toolName = toolCall.function.name; + const parsedArgs = parseToolCallArguments(toolCall.function.arguments); + const args = parsedArgs.success ? parsedArgs.data : {}; + const tool = tools?.find((candidate) => candidate.name === toolName); + if (tool === undefined) { + return { + kind: 'rejected', + toolCall, + toolName, + args, + output: `Tool "${toolName}" not found`, + }; + } + if (!parsedArgs.success) { + return { + kind: 'rejected', + toolCall, + toolName, + args, + output: `Invalid args for tool "${toolName}": malformed JSON in arguments: ${parsedArgs.error}`, + }; + } + const validationError = validateExecutableToolArgs(tool, parsedArgs.data); + if (validationError !== null) { + return { + kind: 'rejected', + toolCall, + toolName, + args: parsedArgs.data, + output: `Invalid args for tool "${toolName}": ${validationError}`, + }; + } + return { kind: 'runnable', toolCall, toolName, tool, args: parsedArgs.data }; +} + +function parseToolCallArguments( + raw: string | null, +): + | { readonly success: true; readonly data: unknown } + | { readonly success: false; readonly error: string } { + if (raw === null || raw.length === 0) { + return { success: true, data: {} }; + } + try { + return { success: true, data: JSON.parse(raw) as unknown }; + } catch (error) { + return { success: false, error: errorMessage(error) }; + } +} + +function validateExecutableToolArgs(tool: ExecutableTool, args: unknown): string | null { + let validator = validators.get(tool); + if (validator === undefined) { + try { + validator = compileToolArgsValidator(tool.parameters); + validators.set(tool, validator); + } catch (error) { + return error instanceof Error ? error.message : String(error); + } + } + return validateToolArgs(validator, args as JsonType); +} + +async function prepareToolCall( + step: ToolCallStepContext, + call: PreflightedToolCall, +): Promise { + if (call.kind === 'rejected') { + await dispatchToolCall(step, call, call.args); + return { task: makeResolvedToolCallTask(makeErrorToolResult(call, call.args, call.output)) }; + } + + const decision = await runPrepareToolExecutionHook(step, call); + if (decision.kind === 'blocked') { + await dispatchToolCall(step, call, decision.args); + return { + task: makeResolvedToolCallTask(makeErrorToolResult(call, decision.args, decision.output)), + }; + } + + if (decision.kind === 'hookFailed') { + await dispatchToolCall(step, call, decision.args); + return { + task: makeResolvedToolCallTask(makeErrorToolResult(call, decision.args, decision.output)), + }; + } + + if (decision.kind === 'synthetic') { + await dispatchToolCall(step, call, decision.args); + return { + task: makeResolvedToolCallTask(makeToolResult(call, decision.args, decision.result)), + stopBatchAfterThis: toolResultStopsTurn(decision.result), + }; + } + + const validationError = validateExecutableToolArgs(call.tool, decision.args); + if (validationError !== null) { + await dispatchToolCall(step, call, decision.args); + const output = `Invalid args for tool "${call.toolName}" after prepareToolExecution hook: ${validationError}`; + return { task: makeResolvedToolCallTask(makeErrorToolResult(call, decision.args, output)) }; + } + + const effectiveArgs = decision.args; + let execution: ToolExecution; + try { + execution = call.tool.resolveExecution(effectiveArgs); + } catch (error) { + if (!(error instanceof PathSecurityError)) { + step.log?.warn('tool execution setup failed', { + toolName: call.toolName, + toolCallId: call.toolCall.id, + error, + }); + } + const output = + error instanceof PathSecurityError + ? error.message + : `Tool "${call.toolName}" failed to resolve execution: ${errorMessage(error)}`; + await dispatchToolCall(step, call, effectiveArgs); + return { + task: makeResolvedToolCallTask(makeErrorToolResult(call, effectiveArgs, output)), + }; + } + + const displayFields = toolCallDisplayFieldsFromExecution(execution); + await dispatchToolCall(step, call, effectiveArgs, displayFields); + + if (step.signal.aborted) { + return { + task: makeResolvedToolCallTask( + makeErrorToolResult(call, effectiveArgs, `Tool "${call.toolName}" was aborted`), + ), + }; + } + + if (execution.isError === true) { + return { + task: makeResolvedToolCallTask(makeToolResult(call, effectiveArgs, execution)), + stopBatchAfterThis: execution.stopTurn, + }; + } + + return { + task: { + accesses: execution.accesses ?? ToolAccesses.all(), + start: async () => ({ + result: runRunnableToolCall(step, call, effectiveArgs, decision.metadata, execution), + }), + }, + }; +} + +async function prepareSkippedToolCall( + step: ToolCallStepContext, + call: PreflightedToolCall, +): Promise> { + const output = 'Tool skipped because a previous tool call stopped the turn.'; + await dispatchToolCall(step, call, call.args); + return makeResolvedToolCallTask(makeErrorToolResult(call, call.args, output)); +} + +function makeResolvedToolCallTask(result: PendingToolResult): ToolCallTask { + return { + accesses: ToolAccesses.none(), + start: async () => ({ result: Promise.resolve(result) }), + }; +} + +/** + * Run `prepareToolExecution` in provider order before recording `tool.call`. + * Hook decisions can block a call or replace args before execution starts. + */ +async function runPrepareToolExecutionHook( + step: ToolCallStepContext, + call: RunnableToolCall, +): Promise { + const { hooks, signal, turnId, currentStep, llm } = step; + const { toolCall, args } = call; + + if (hooks?.prepareToolExecution === undefined) { + return { kind: 'allowed', args }; + } + + let hookResult: PrepareToolExecutionResult | undefined; + try { + hookResult = await hooks.prepareToolExecution({ + toolCall, + tool: call.tool, + args, + turnId, + stepNumber: currentStep, + signal, + llm, + }); + } catch (error) { + // If the turn is cancelled while an abort-aware hook is awaited, report the + // call as aborted instead of treating it as a hook failure. + if (isAbortError(error) || signal.aborted) { + return { + kind: 'hookFailed', + args, + output: `Tool "${call.toolName}" was aborted during prepareToolExecution hook`, + }; + } + return { + kind: 'hookFailed', + args, + output: `prepareToolExecution hook failed for "${call.toolName}": ${errorMessage(error)}`, + }; + } + + const effectiveArgs = hookResult?.updatedArgs ?? args; + if (hookResult?.block === true) { + return { + kind: 'blocked', + args: effectiveArgs, + output: hookResult.reason ?? `Tool call "${call.toolName}" was blocked`, + }; + } + + if (hookResult?.syntheticResult !== undefined) { + return { kind: 'synthetic', args: effectiveArgs, result: hookResult.syntheticResult }; + } + + return { kind: 'allowed', args: effectiveArgs, metadata: hookResult?.executionMetadata }; +} + +function toolCallDisplayFieldsFromExecution( + execution: ToolExecution, +): ToolCallDisplayFields | undefined { + if (execution.isError === true) return undefined; + const description = execution.description; + const display = execution.display; + return { + description: description !== undefined && description.length > 0 ? description : undefined, + display, + }; +} + +async function runRunnableToolCall( + step: ToolCallStepContext, + call: RunnableToolCall, + effectiveArgs: unknown, + metadata: unknown, + execution: RunnableToolExecution, +): Promise { + const { signal } = step; + const { toolCall, toolName } = call; + + if (signal.aborted) { + return makeErrorToolResult(call, effectiveArgs, `Tool "${toolName}" was aborted`); + } + + let toolResult: ExecutableToolResult; + try { + toolResult = await executeTool(step, execution, toolCall, toolName, metadata); + } catch (error) { + const aborted = isAbortError(error) || signal.aborted; + if (!aborted) { + step.log?.warn('tool execution failed', { + toolName, + toolCallId: toolCall.id, + error, + }); + } + const output = aborted + ? `Tool "${toolName}" was aborted` + : `Tool "${toolName}" failed: ${errorMessage(error)}`; + return makeErrorToolResult(call, effectiveArgs, output); + } + + return makeToolResult(call, effectiveArgs, toolResult); +} + +async function finalizePendingToolResult( + step: ToolCallStepContext, + pendingResult: PendingToolResult, +): Promise { + const { hooks, signal, turnId, currentStep, llm } = step; + if (hooks?.finalizeToolResult === undefined) { + return { ...pendingResult, result: normalizeToolResult(pendingResult.result) }; + } + + try { + const finalizedResult = await hooks.finalizeToolResult({ + toolCall: pendingResult.toolCall, + args: pendingResult.args, + result: pendingResult.result, + turnId, + stepNumber: currentStep, + signal, + llm, + }); + const effectiveResult = finalizedResult ?? pendingResult.result; + return { + ...pendingResult, + stopTurn: pendingResult.stopTurn === true || toolResultStopsTurn(effectiveResult), + result: normalizeToolResult(effectiveResult), + }; + } catch (error) { + // This is the redaction/truncation boundary. If it fails, do not persist + // the raw tool output; write an error result instead. + const aborted = isAbortError(error) || signal.aborted; + if (!aborted) { + step.log?.warn('finalizeToolResult hook failed', { + toolName: pendingResult.toolName, + toolCallId: pendingResult.toolCall.id, + error, + }); + } + const output = aborted + ? `Tool "${pendingResult.toolName}" aborted during finalizeToolResult hook.` + : `finalizeToolResult hook failed for "${pendingResult.toolName}": ${errorMessage(error)}`; + return { + ...pendingResult, + stopTurn: pendingResult.stopTurn, + result: { output, isError: true }, + }; + } +} + +async function executeTool( + step: ToolCallStepContext, + execution: RunnableToolExecution, + toolCall: ToolCall, + toolName: string, + metadata: unknown, +): Promise { + const { dispatchEvent, signal, turnId } = step; + + signal.throwIfAborted(); + + const executePromise = execution.execute({ + turnId, + toolCallId: toolCall.id, + metadata, + signal, + onUpdate: (update) => { + if (signal.aborted) return; + dispatchEvent({ + type: 'tool.progress', + toolCallId: toolCall.id, + update, + }); + }, + }); + return raceExecuteWithGraceTimeout(executePromise, signal, toolName); +} + +async function raceExecuteWithGraceTimeout( + executePromise: Promise, + signal: AbortSignal, + toolName: string, +): Promise { + let graceTimer: ReturnType | undefined; + let onAbort: (() => void) | undefined; + + const graceSentinel: Promise = new Promise((resolve) => { + const armTimer = (): void => { + graceTimer = setTimeout(() => { + resolve({ + output: `Tool "${toolName}" aborted by grace timeout (${String(GRACE_TIMEOUT_MS)}ms)`, + isError: true, + }); + }, GRACE_TIMEOUT_MS); + }; + if (signal.aborted) { + armTimer(); + } else { + onAbort = armTimer; + signal.addEventListener('abort', onAbort, { once: true }); + } + }); + + try { + // Tools that ignore AbortSignal may never settle. After abort, the grace + // branch lets the turn finish with a synthetic error result. + return await Promise.race([executePromise, graceSentinel]); + } finally { + if (graceTimer !== undefined) clearTimeout(graceTimer); + if (onAbort !== undefined) { + try { + signal.removeEventListener('abort', onAbort); + } catch { + // Some AbortSignal polyfills do not implement removeEventListener. + } + } + } +} + +function isMediaContentPart(part: ContentPart): boolean { + return part.type === 'image_url' || part.type === 'audio_url' || part.type === 'video_url'; +} + +function normalizeToolResult(r: ExecutableToolResult): ExecutableToolResult { + let output: ExecutableToolResult['output']; + if (typeof r.output === 'string') { + output = r.output.length > 0 ? r.output : TOOL_OUTPUT_EMPTY; + } else if (r.output.length === 0) { + output = TOOL_OUTPUT_EMPTY; + } else { + const hasMediaBlock = r.output.some(isMediaContentPart); + if (hasMediaBlock) { + const hasNonEmptyText = r.output.some((c) => c.type === 'text' && c.text.length > 0); + output = hasNonEmptyText + ? r.output + : [{ type: 'text', text: TOOL_OUTPUT_NON_TEXT }, ...r.output]; + } else { + const textJoined = r.output + .filter((c): c is Extract => c.type === 'text') + .map((c) => c.text) + .join(''); + output = textJoined.length > 0 ? textJoined : TOOL_OUTPUT_EMPTY; + } + } + return r.isError === true ? { output, isError: true } : { output }; +} + +function makeToolResult( + call: PreflightedToolCall, + args: unknown, + result: ExecutableToolResult, +): PendingToolResult { + return { + toolCall: call.toolCall, + toolName: call.toolName, + args, + result, + stopTurn: toolResultStopsTurn(result), + }; +} + +function toolResultStopsTurn(result: ExecutableToolResult): boolean { + return result.isError === true && result.stopTurn === true; +} + +function makeErrorToolResult( + call: PreflightedToolCall, + args: unknown, + output: string, +): PendingToolResult { + return makeToolResult(call, args, { output, isError: true }); +} + +/** + * Record `tool.call` in provider order. Reusing the provider/API tool-call id + * keeps transcript linkage on one canonical identity. + */ +async function dispatchToolCall( + step: ToolCallStepContext, + call: PreflightedToolCall, + args: unknown, + displayFields?: ToolCallDisplayFields | undefined, +): Promise { + const { toolCall, toolName } = call; + await step.dispatchEvent({ + type: 'tool.call', + uuid: toolCall.id, + turnId: step.turnId, + step: step.currentStep, + stepUuid: step.stepUuid, + toolCallId: toolCall.id, + name: toolName, + args, + description: displayFields?.description, + display: displayFields?.display, + }); +} diff --git a/packages/agent-core/src/loop/tool-scheduler.ts b/packages/agent-core/src/loop/tool-scheduler.ts new file mode 100644 index 000000000..0976d3fed --- /dev/null +++ b/packages/agent-core/src/loop/tool-scheduler.ts @@ -0,0 +1,100 @@ +/** + * Stateful execution scheduler for tool calls in one model step. + * + * The scheduler owns only execution ordering: + * - tasks with non-conflicting resource accesses may overlap + * - tasks with conflicting resource accesses wait for the conflicting active tasks + * - drained results are handed back in provider order + * + * Validation, hooks, event construction, and result finalization stay in + * `tool-call.ts`. + */ + +import { createControlledPromise, type ControlledPromise } from '@antfu/utils'; + +import { ToolAccesses } from './tool-access'; + +// Scheduler + +export interface ToolCallTask { + readonly accesses: ToolAccesses; + readonly start: () => Promise<{ readonly result: Promise }>; +} + +interface ScheduledToolCallTask extends ToolCallTask { + readonly result: ControlledPromise; +} + +export class ToolScheduler { + private readonly activeTasks: Array> = []; + private queuedTasks: Array> = []; + + add(task: ToolCallTask): Promise { + const result = createControlledPromise(); + void result.catch(() => undefined); + + const scheduledTask: ScheduledToolCallTask = { ...task, result }; + if (this.isBlocked(task, this.queuedTasks)) { + this.queuedTasks.push(scheduledTask); + } else { + this.start(scheduledTask); + } + + return result; + } + + private isBlocked( + task: ToolCallTask, + queuedBefore: readonly ToolCallTask[], + ): boolean { + return ( + this.conflictsWithAny(task, this.activeTasks) || this.conflictsWithAny(task, queuedBefore) + ); + } + + private conflictsWithAny( + task: ToolCallTask, + candidates: readonly ToolCallTask[], + ): boolean { + return candidates.some((candidate) => + ToolAccesses.conflict(task.accesses, candidate.accesses), + ); + } + + private start(task: ScheduledToolCallTask): void { + this.activeTasks.push(task); + let started: Promise<{ readonly result: Promise }>; + try { + started = task.start(); + } catch (error) { + task.result.reject(error); + this.finish(task); + return; + } + + void started + .then(({ result }) => result) + .then(task.result.resolve, task.result.reject) + .finally(() => { + this.finish(task); + }); + } + + private finish(task: ScheduledToolCallTask): void { + const index = this.activeTasks.indexOf(task); + if (index >= 0) this.activeTasks.splice(index, 1); + this.startQueuedTasks(); + } + + private startQueuedTasks(): void { + const stillQueued: Array> = []; + for (const task of this.queuedTasks) { + if (this.isBlocked(task, stillQueued)) { + stillQueued.push(task); + } else { + this.start(task); + } + } + this.queuedTasks = stillQueued; + } +} diff --git a/packages/agent-core/src/loop/turn-step.ts b/packages/agent-core/src/loop/turn-step.ts new file mode 100644 index 000000000..ceceb346a --- /dev/null +++ b/packages/agent-core/src/loop/turn-step.ts @@ -0,0 +1,249 @@ +/** + * Executes one provider step. + * + * A step owns the provider call, atomic transcript envelope, streaming callback + * wiring, tool-call lifecycle, and post-step hooks. Provider usage is recorded + * immediately after `llm.chat` returns so a later abort during tool execution + * does not lose model usage that was already spent. + */ + +import { randomUUID } from 'node:crypto'; + +import type { TokenUsage } from '@moonshot-ai/kosong'; +import type { Logger } from '#/logging/types'; + +import type { LoopEventDispatcher } from './events'; +import type { LLM, LLMChatParams, LLMChatResponse } from './llm'; +import { chatWithRetry } from './retry'; +import { runToolCallBatch, type ToolCallStepContext } from './tool-call'; +import type { ExecutableTool, LoopHooks, LoopMessageBuilder, LoopStepStopReason } from './types'; + +type ChatStreamingCallbacks = Pick< + LLMChatParams, + 'onTextDelta' | 'onThinkDelta' | 'onToolCallDelta' | 'onTextPart' | 'onThinkPart' +>; + +export interface ExecuteLoopStepDeps { + readonly turnId: string; + readonly signal: AbortSignal; + readonly buildMessages: LoopMessageBuilder; + readonly dispatchEvent: LoopEventDispatcher; + readonly llm: LLM; + readonly tools?: readonly ExecutableTool[] | undefined; + readonly hooks?: LoopHooks | undefined; + readonly log?: Logger | undefined; + readonly currentStep: number; + readonly maxRetryAttempts?: number; + readonly recordUsage: (usage: TokenUsage) => void; +} + +export async function executeLoopStep(deps: ExecuteLoopStepDeps): Promise<{ + readonly usage: TokenUsage; + readonly stopReason: LoopStepStopReason; +}> { + const { + turnId, + signal, + buildMessages, + dispatchEvent, + llm, + tools, + hooks, + log, + currentStep, + maxRetryAttempts, + recordUsage, + } = deps; + + if (hooks?.beforeStep !== undefined) { + const beforeStep = await hooks.beforeStep({ + turnId, + stepNumber: currentStep, + signal, + llm, + }); + if (beforeStep?.block === true) { + throw new Error(beforeStep.reason ?? `Step ${String(currentStep)} was blocked`); + } + } + + signal.throwIfAborted(); + + const messages = await buildMessages(); + signal.throwIfAborted(); + + const stepUuid = randomUUID(); + + const step: ToolCallStepContext = { + tools, + hooks, + log, + dispatchEvent, + llm, + signal, + turnId, + currentStep, + stepUuid, + }; + + await dispatchEvent({ + type: 'step.begin', + uuid: stepUuid, + turnId, + step: currentStep, + }); + + const chatParams: LLMChatParams = { + messages, + tools: tools ?? [], + signal, + ...createChatStreamingCallbacks({ + dispatchEvent, + turnId, + currentStep, + stepUuid, + }), + }; + const response: LLMChatResponse = await chatWithRetry({ + llm, + params: chatParams, + dispatchEvent, + turnId, + currentStep, + stepUuid, + maxAttempts: maxRetryAttempts, + log, + }); + const usage = response.usage; + recordUsage(usage); + const stopReason = deriveStepStopReason(response); + + // Execute tools only when the normalized response shape represents a tool + // step. Provider terminal diagnostics such as filtering or truncation must + // not trigger side-effecting tool execution even if a malformed response also + // contains tool calls. + let effectiveStopReason = stopReason; + if (stopReason === 'tool_use') { + const toolBatch = await runToolCallBatch(step, response); + if (toolBatch.stopTurn) effectiveStopReason = 'end_turn'; + } + + // When a tool batch runs, it drains paired `tool.result` events even when + // cancellation is requested. Check the signal here before sealing the step. + signal.throwIfAborted(); + + await dispatchEvent({ + type: 'step.end', + uuid: stepUuid, + turnId, + step: currentStep, + usage, + finishReason: effectiveStopReason, + ...stepEndProviderDiagnostics(response, effectiveStopReason), + }); + + if (hooks?.afterStep !== undefined) { + try { + await hooks.afterStep({ + turnId, + stepNumber: currentStep, + usage, + stopReason: effectiveStopReason, + signal, + llm, + }); + } catch { + // The step is already sealed; observer hooks cannot change the result. + } + } + + return { usage, stopReason: effectiveStopReason }; +} + +function deriveStepStopReason(response: LLMChatResponse): LoopStepStopReason { + switch (response.providerFinishReason) { + case 'truncated': + return 'max_tokens'; + case 'filtered': + return 'filtered'; + case 'paused': + return 'paused'; + case 'other': + return 'unknown'; + case 'completed': + case undefined: + return response.toolCalls.length > 0 ? 'tool_use' : 'end_turn'; + case 'tool_calls': + return response.toolCalls.length > 0 ? 'tool_use' : 'unknown'; + default: { + const _exhaustive: never = response.providerFinishReason; + return _exhaustive; + } + } +} + +function stepEndProviderDiagnostics( + response: LLMChatResponse, + stopReason: LoopStepStopReason, +): Pick { + const providerFinishReason = response.providerFinishReason; + if ( + (providerFinishReason === 'completed' && stopReason === 'end_turn') || + (providerFinishReason === 'tool_calls' && stopReason === 'tool_use') + ) { + return {}; + } + + return { + ...(providerFinishReason !== undefined ? { providerFinishReason } : {}), + ...(response.rawFinishReason !== undefined + ? { rawFinishReason: response.rawFinishReason } + : {}), + }; +} + +function createChatStreamingCallbacks(deps: { + readonly dispatchEvent: LoopEventDispatcher; + readonly turnId: string; + readonly currentStep: number; + readonly stepUuid: string; +}): ChatStreamingCallbacks { + const { dispatchEvent, turnId, currentStep, stepUuid } = deps; + + return { + onTextDelta: (delta) => { + dispatchEvent({ type: 'text.delta', delta }); + }, + onThinkDelta: (delta) => { + dispatchEvent({ type: 'thinking.delta', delta }); + }, + onToolCallDelta: (delta) => { + dispatchEvent({ + type: 'tool.call.delta', + toolCallId: delta.toolCallId, + name: delta.name, + argumentsPart: delta.argumentsPart, + }); + }, + onTextPart: async (part) => { + await dispatchEvent({ + type: 'content.part', + uuid: randomUUID(), + turnId, + step: currentStep, + stepUuid, + part, + }); + }, + onThinkPart: async (part) => { + await dispatchEvent({ + type: 'content.part', + uuid: randomUUID(), + turnId, + step: currentStep, + stepUuid, + part, + }); + }, + }; +} diff --git a/packages/agent-core/src/loop/types.ts b/packages/agent-core/src/loop/types.ts new file mode 100644 index 000000000..433435c52 --- /dev/null +++ b/packages/agent-core/src/loop/types.ts @@ -0,0 +1,208 @@ +/** + * Public contracts for the stateless agent loop. + * + * This file defines the narrow surfaces that connect a Kosong conversation to + * tool execution, phase hooks, and turn results. Host-layer metadata, policy, + * archival limits, and UI concerns stay outside these contracts. + * + * Field naming is camelCase unless a reused Kosong type says otherwise. + * Optional fields use `?: T | undefined` intentionally under + * `exactOptionalPropertyTypes: true`. + */ + +import type { ContentPart, Message, TokenUsage, Tool, ToolCall } from '@moonshot-ai/kosong'; + +import type { ToolInputDisplay } from '../tools/display'; +import type { ToolAccesses } from './tool-access'; +import type { LLM } from './llm'; + +export type { ToolCall }; + +export type LoopMessageBuilder = () => Message[] | Promise; + +/** + * Stop reason for one completed model step. + * + * `tool_use` is a loop-control signal: the loop executes the requested tools and + * continues with another step. The other values are terminal for the current + * turn unless a host hook explicitly asks the loop to continue. + */ +export type LoopStepStopReason = + | 'end_turn' + | 'max_tokens' + | 'tool_use' + | 'filtered' + | 'paused' + | 'unknown'; + +export type LoopTerminalStepStopReason = Exclude; + +/** + * Stop reasons that can be returned in a normal `TurnResult`. + * + * `tool_use` is intentionally absent because it cannot be the final result of a + * completed turn. Errors and max-step exhaustion are represented by thrown + * errors, not by this union. Compaction is a host-level retry concern rather + * than a stop reason. + */ +export type LoopTurnStopReason = LoopTerminalStepStopReason | 'aborted'; + +/** + * @deprecated Legacy umbrella union. Use `LoopStepStopReason` for per-step + * model responses and `LoopTurnStopReason` for `TurnResult`. + */ +export type StopReason = LoopStepStopReason | 'aborted'; + +export interface TurnResult { + stopReason: LoopTurnStopReason; + steps: number; + usage: TokenUsage; +} + +export type ExecutableToolOutput = string | ContentPart[]; + +export interface ExecutableToolSuccessResult { + readonly output: ExecutableToolOutput; + readonly isError?: false | undefined; + /** + * Optional human-readable side channel for tool-result metadata that + * should not contaminate the data stream the model sees (e.g. a + * "Task snapshot retrieved." brief for TaskOutput). Distinct from + * `output`: callers rendering tool results decide whether to surface + * this to the user. + */ + readonly message?: string | undefined; +} + +export interface ExecutableToolErrorResult { + readonly output: ExecutableToolOutput; + readonly isError: true; + /** See {@link ExecutableToolSuccessResult.message}. */ + readonly message?: string | undefined; + /** + * Internal loop-control hint. Tool result events strip this field before + * persistence; it only tells the current turn whether another model step is + * allowed after this tool batch. + */ + readonly stopTurn?: boolean | undefined; +} + +export type ExecutableToolResult = ExecutableToolSuccessResult | ExecutableToolErrorResult; + +export interface ToolUpdate { + kind: 'stdout' | 'stderr' | 'progress' | 'status' | 'custom'; + text?: string | undefined; + percent?: number | undefined; + /** Vendor-defined event identifier when `kind === 'custom'`. */ + customKind?: string | undefined; + /** Opaque payload paired with `customKind`. */ + customData?: unknown; +} + +/** + * Per-call context passed to tool implementations. + */ +export interface ExecutableToolContext { + readonly turnId: string; + readonly toolCallId: string; + readonly metadata?: unknown; + readonly signal: AbortSignal; + readonly onUpdate?: ((update: ToolUpdate) => void) | undefined; +} + +export interface RunnableToolExecution { + readonly isError?: false | undefined; + readonly accesses?: ToolAccesses | undefined; + readonly display?: ToolInputDisplay | undefined; + readonly description?: string; + readonly execute: (ctx: ExecutableToolContext) => Promise; +} + +export type ToolExecution = RunnableToolExecution | ExecutableToolErrorResult; + +export interface ExecutableTool extends Tool { + resolveExecution(input: Input): ToolExecution; +} + +/** + * Step hooks are aligned to recorded phase boundaries: `beforeStep` runs before + * `step.begin`, while `afterStep` runs after `step.end`. + */ + +export interface LoopStepHookContext { + readonly turnId: string; + readonly stepNumber: number; + readonly signal: AbortSignal; + readonly llm: LLM; +} + +export interface ToolExecutionHookContext extends LoopStepHookContext { + readonly toolCall: ToolCall; + readonly tool?: ExecutableTool | undefined; + readonly args: unknown; +} + +export interface PrepareToolExecutionResult { + readonly block?: boolean | undefined; + readonly reason?: string | undefined; + readonly updatedArgs?: unknown; + readonly syntheticResult?: ExecutableToolResult | undefined; + readonly executionMetadata?: unknown; +} + +export interface FinalizeToolResultContext extends ToolExecutionHookContext { + readonly result: ExecutableToolResult; +} + +export interface LoopAfterStepContext extends LoopStepHookContext { + readonly usage: TokenUsage; + readonly stopReason: LoopStepStopReason; +} + +export interface LoopStoppedStepContext extends LoopStepHookContext { + readonly usage: TokenUsage; + readonly stopReason: LoopTerminalStepStopReason; +} + +export interface BeforeStepResult { + readonly block?: boolean | undefined; + readonly reason?: string | undefined; +} + +export interface ShouldContinueAfterStopResult { + readonly continue: boolean; +} + +export type BeforeStepHook = (ctx: LoopStepHookContext) => Promise; + +export type AfterStepHook = (ctx: LoopAfterStepContext) => Promise; + +export type PrepareToolExecutionHook = ( + ctx: ToolExecutionHookContext, +) => Promise; + +export type FinalizeToolResultHook = ( + ctx: FinalizeToolResultContext, +) => Promise; + +export type ShouldContinueAfterStopHook = ( + ctx: LoopStoppedStepContext, +) => Promise; + +/** + * Groups every awaited phase hook. + * + * Hooks can affect control flow at deterministic transcript points. Event + * listeners observe output and cannot change turn behavior. + * + * Tool hooks run serially in provider tool-call order before the matching + * durable event is recorded, so preparation and finalization decisions are + * resolved at stable transcript points. + */ +export interface LoopHooks { + beforeStep?: BeforeStepHook | undefined; + afterStep?: AfterStepHook | undefined; + prepareToolExecution?: PrepareToolExecutionHook | undefined; + finalizeToolResult?: FinalizeToolResultHook | undefined; + shouldContinueAfterStop?: ShouldContinueAfterStopHook | undefined; +} diff --git a/packages/agent-core/src/mcp/auth-tool.ts b/packages/agent-core/src/mcp/auth-tool.ts new file mode 100644 index 000000000..37bd73652 --- /dev/null +++ b/packages/agent-core/src/mcp/auth-tool.ts @@ -0,0 +1,181 @@ +/** + * Synthetic `mcp____authenticate` tool. + * + * When an MCP HTTP server lands in the `needs-auth` state — i.e. its + * initial connection failed with a 401 / `UnauthorizedError` and no static + * bearer token is configured — the {@link ToolManager} swaps the real MCP + * tool list for this single tool. Calling it: + * + * 1. Asks {@link McpOAuthService} to perform RFC 9728 / RFC 8414 / RFC 7591 + * discovery and produce an authorization URL. + * 2. Streams that URL back to the model via `onUpdate({kind:'status'})` + * and returns it in the tool output so the model can hand it to the + * human user. + * 3. Blocks (up to {@link DEFAULT_AUTH_TIMEOUT_MS}) on the one-shot + * localhost callback listener owned by the OAuth service. + * 4. Drives a manager-level `reconnect(name)` once tokens have been + * persisted, which flips the entry to `connected` and lets + * `ToolManager` swap the synthetic tool out for the real MCP tools. + * + * The blocking shape (option 1 in the plan) keeps the implementation + * simple at the cost of holding one tool call open for the duration of + * the human's browser flow. If the model ends up re-invoking the tool + * mid-flow we just start a fresh flow; the new callback server supersedes + * the old one. + */ + +import { z } from 'zod'; + +import { + type ExecutableTool, + type ExecutableToolContext, + type ExecutableToolResult, +} from '../loop'; +import { toInputJsonSchema } from '../tools/support/input-schema'; +import { + MCP_OAUTH_AUTHORIZATION_URL_TOOL_UPDATE, + type McpOAuthAuthorizationUrlUpdateData, +} from '../rpc/events'; +import { AlreadyAuthorizedError, type McpOAuthService } from './oauth'; +import { qualifyMcpToolName } from './tool-naming'; + +const DEFAULT_AUTH_TIMEOUT_MS = 15 * 60 * 1000; // 15 minutes + +const AUTH_TOOL_TOOL_NAME = 'authenticate'; + +const DESCRIPTION_TEMPLATE = (serverName: string): string => + `Authenticate with MCP server "${serverName}" via OAuth. + +This server requires an OAuth login that has not yet been completed. ` + + `Calling this tool starts the authorization flow: + + 1. The tool prints an authorization URL. + 2. **You must show that URL to the user verbatim** and ask them to open it + in a browser, sign in, and approve the kimi-code client. + 3. The tool blocks (up to 15 minutes) until the browser redirects back to + the local callback listener. + 4. On success, kimi-code reconnects the MCP server and the real tools + replace this synthetic tool. + +Take no arguments. Treat the URL as sensitive — do not modify it or strip +query parameters.`; + +export interface CreateMcpAuthToolOptions { + /** Friendly MCP server name as configured in `mcp.json`. */ + readonly serverName: string; + /** Base URL of the MCP server (used for OAuth resource metadata discovery). */ + readonly serverUrl: string; + /** OAuth orchestrator, typically `Session`-scoped. */ + readonly oauthService: McpOAuthService; + /** + * Triggers a manager-level reconnect once tokens land on disk. Implemented + * by the {@link McpConnectionManager} and bound in the {@link ToolManager} + * `needs-auth` branch. + */ + readonly reconnect: (signal?: AbortSignal) => Promise; + /** + * Overrides the per-call OAuth wait timeout. Tests set this to a small + * number; production callers should accept the default. + */ + readonly timeoutMs?: number; +} + +export function createMcpAuthTool(options: CreateMcpAuthToolOptions): ExecutableTool { + const { serverName, serverUrl, oauthService, reconnect, timeoutMs } = options; + const name = qualifyMcpToolName(serverName, AUTH_TOOL_TOOL_NAME); + const description = DESCRIPTION_TEMPLATE(serverName); + // No arguments; an empty object schema keeps providers happy across SDKs. + const parameters = toInputJsonSchema(z.object({})); + const execute = async (ctx: ExecutableToolContext): Promise => { + const { signal, onUpdate } = ctx; + signal.throwIfAborted(); + + onUpdate?.({ kind: 'status', text: `Discovering OAuth metadata for ${serverName}…` }); + + let flow: Awaited>; + try { + flow = await oauthService.beginAuthorization(serverName, serverUrl); + } catch (error) { + if (error instanceof AlreadyAuthorizedError) { + onUpdate?.({ kind: 'status', text: `Already authorized; reconnecting ${serverName}…` }); + try { + await reconnect(signal); + } catch (reconnectError) { + return errorResult(serverName, reconnectError); + } + return { + output: + `MCP server "${serverName}" already had valid OAuth credentials. ` + + `Reconnected; real tools are available now.`, + }; + } + return errorResult(serverName, error); + } + + const urlText = flow.authorizationUrl.toString(); + const customData: McpOAuthAuthorizationUrlUpdateData = { + serverName, + authorizationUrl: urlText, + }; + onUpdate?.({ + kind: 'custom', + customKind: MCP_OAUTH_AUTHORIZATION_URL_TOOL_UPDATE, + customData, + }); + onUpdate?.({ + kind: 'status', + text: + `Open this URL in your browser to authorize "${serverName}":\n` + + `\n${urlText}\n\n` + + `Waiting for the OAuth callback (timeout 15 min). ` + + `If you cancel, call this tool again to restart the flow.`, + }); + + try { + await flow.complete({ signal, timeoutMs: timeoutMs ?? DEFAULT_AUTH_TIMEOUT_MS }); + } catch (error) { + return errorResult(serverName, error, urlText); + } + + onUpdate?.({ kind: 'status', text: `Authorized — reconnecting ${serverName}…` }); + try { + await reconnect(signal); + } catch (error) { + return errorResult(serverName, error); + } + + return { + output: + `MCP server "${serverName}" authenticated successfully. ` + + `The real MCP tools have replaced this synthetic authenticate tool.`, + }; + }; + + return { + name, + description, + parameters, + resolveExecution: () => { + return { + description: `Authenticating ${serverName}`, + execute, + }; + }, + }; +} + +function errorResult( + serverName: string, + error: unknown, + authorizationUrl?: string, +): ExecutableToolResult { + const message = error instanceof Error ? error.message : String(error); + const suffix = + authorizationUrl !== undefined + ? `\n\nAuthorization URL (still valid if the listener has not timed out): ${authorizationUrl}` + : ''; + return { + isError: true, + output: `OAuth flow for MCP server "${serverName}" did not complete: ${message}${suffix}`, + }; +} diff --git a/packages/agent-core/src/mcp/client-http.ts b/packages/agent-core/src/mcp/client-http.ts new file mode 100644 index 000000000..626c5d2bd --- /dev/null +++ b/packages/agent-core/src/mcp/client-http.ts @@ -0,0 +1,231 @@ +import { ErrorCodes, KimiError } from '#/errors'; +import type { McpServerHttpConfig } from '#/config/schema'; +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import type { OAuthClientProvider } from '@modelcontextprotocol/sdk/client/auth.js'; +import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; + +import { + buildRequestOptions, + KIMI_MCP_CLIENT_NAME, + KIMI_MCP_CLIENT_VERSION, + toMcpToolDefinition, + toMcpToolResult, + type UnexpectedCloseListener, + type UnexpectedCloseReason, +} from './client-shared'; +import type { MCPClient, MCPToolDefinition, MCPToolResult } from './types'; + +export interface HttpMcpClientOptions { + readonly clientName?: string; + readonly clientVersion?: string; + readonly toolCallTimeoutMs?: number; + /** + * Reads `process.env[name]` by default. Tests can inject a deterministic + * lookup function so they do not have to mutate global env. + */ + readonly envLookup?: (name: string) => string | undefined; + /** + * Lets tests inject a fake `fetch` for the underlying transport. + */ + readonly fetch?: typeof fetch; + /** + * OAuth client provider attached to the transport. Set only when the server + * has no static token configuration; the SDK uses this to handle 401s with + * RFC 9728 / RFC 8414 / DCR discovery and PKCE. The connection manager wires + * this in and surfaces `UnauthorizedError` as a `needs-auth` status. + */ + readonly oauthProvider?: OAuthClientProvider; +} + +/** + * Wraps the SDK streamable-HTTP transport as a kosong {@link MCPClient}. + * Static bearer tokens are looked up from `process.env[bearerTokenEnvVar]`. + * OAuth providers are attached separately by the connection manager. + */ +export class HttpMcpClient implements MCPClient { + private readonly client: Client; + private readonly transport: StreamableHTTPClientTransport; + private readonly toolCallTimeoutMs?: number; + private started = false; + private closed = false; + // See StdioMcpClient.ready — distinguishes handshake-phase failures (caller + // sees them via `connect()` throwing, no unexpectedClose) from post-ready + // disconnects (the case `onUnexpectedClose` is designed to surface). + private ready = false; + private hooksInstalled = false; + private unexpectedCloseListener: UnexpectedCloseListener | undefined; + private lastTransportError: Error | undefined; + // See StdioMcpClient — buffered when the listener has not been installed + // yet so an early close is replayed instead of dropped. + private pendingUnexpectedClose: UnexpectedCloseReason | undefined; + // Latch so `onerror` and a (theoretical) `onclose` for the same transport + // failure do not double-fire. Once we have decided the connection is dead, + // additional SDK notifications are noise. + private unexpectedCloseFired = false; + + constructor(config: McpServerHttpConfig, options: HttpMcpClientOptions = {}) { + const envLookup = options.envLookup ?? ((name) => process.env[name]); + const headers = buildMcpHttpHeaders(config, envLookup); + + this.transport = new StreamableHTTPClientTransport(new URL(config.url), { + requestInit: headers !== undefined ? { headers } : undefined, + fetch: options.fetch, + authProvider: options.oauthProvider, + }); + this.client = new Client({ + name: options.clientName ?? KIMI_MCP_CLIENT_NAME, + version: options.clientVersion ?? KIMI_MCP_CLIENT_VERSION, + }); + this.toolCallTimeoutMs = options.toolCallTimeoutMs; + } + + async connect(): Promise { + if (this.closed) { + throw new Error('MCP HTTP client is closed'); + } + if (this.started) return; + this.started = true; + // Install hooks BEFORE the SDK handshake; see StdioMcpClient.connect. + this.installTransportHooks(); + try { + await this.client.connect(this.transport); + } catch (error) { + await this.closeStartedClient(); + throw error; + } + if (this.closed) { + await this.closeStartedClient(); + throw new Error('MCP HTTP client was closed during startup'); + } + this.ready = true; + } + + async close(): Promise { + if (this.closed) return; + this.closed = true; + await this.closeStartedClient(); + } + + /** + * Register a listener for unsolicited transport drops. See + * `StdioMcpClient.onUnexpectedClose` for semantics. If the transport + * already signalled a terminal failure, the buffered reason is replayed + * synchronously. + */ + onUnexpectedClose(listener: UnexpectedCloseListener): void { + this.unexpectedCloseListener = listener; + const pending = this.pendingUnexpectedClose; + if (pending !== undefined) { + this.pendingUnexpectedClose = undefined; + listener(pending); + } + } + + async listTools(): Promise { + const result = await this.client.listTools(); + return result.tools.map(toMcpToolDefinition); + } + + async callTool( + name: string, + args: Record, + signal?: AbortSignal, + ): Promise { + const requestOptions = buildRequestOptions(this.toolCallTimeoutMs, signal); + const result = await this.client.callTool({ name, arguments: args }, undefined, requestOptions); + return toMcpToolResult(result); + } + + private async closeStartedClient(): Promise { + if (!this.started) return; + this.started = false; + await this.client.close(); + } + + private installTransportHooks(): void { + // Idempotent — see StdioMcpClient.installTransportHooks. + if (this.hooksInstalled) return; + this.hooksInstalled = true; + this.client.onclose = () => { + if (this.closed) return; + // Handshake-phase close surfaces via `client.connect()` throwing. + if (!this.ready) return; + this.fireUnexpectedClose({ error: this.lastTransportError }); + }; + // streamable-http's transport only calls `onclose` on its own `close()` + // path, so 99% of remote disconnects (SSE flap → reconnect exhaustion, + // POST send failure on a dead session) arrive as `onerror` instead. Mirror + // the way the SDK exposes a "the transport is gone" signal there by + // mapping the known-terminal error messages back to an unexpected close; + // everything else is treated as transient and only cached for diagnostics. + this.client.onerror = (error) => { + this.lastTransportError = error; + if (this.closed) return; + // During the handshake, terminal errors (Unauthorized, reconnect + // exhaustion) propagate through `client.connect()` and the manager's + // `shouldMarkNeedsAuth` / `formatStartupError`. Firing here would + // double-report. + if (!this.ready) return; + if (isTerminalTransportError(error)) { + this.fireUnexpectedClose({ error }); + } + }; + } + + private fireUnexpectedClose(reason: UnexpectedCloseReason): void { + if (this.unexpectedCloseFired) return; + this.unexpectedCloseFired = true; + const listener = this.unexpectedCloseListener; + if (listener !== undefined) { + listener(reason); + } else { + this.pendingUnexpectedClose = reason; + } + } +} + +/** + * Returns true when an error reported via `Client.onerror` indicates the + * underlying HTTP transport is dead. The streamable-http SDK does not call + * `onclose` for remote disconnects; instead it surfaces them through + * `onerror`, but only a few specific messages mean "give up" rather than + * "we will retry": + * + * - `UnauthorizedError` — RFC 9728/8414 auth flow gave up; the SDK won't + * retry without a fresh provider call. + * - "Maximum reconnection attempts ... exceeded." — emitted from + * `_scheduleReconnection` after the SSE reconnect budget is gone + * (`streamableHttp.js`, `_scheduleReconnection`). + * + * Transient signals (per-request fetch failures, single SSE flaps that the + * SDK is about to reconnect from) MUST NOT match; otherwise a brief network + * blip would tear down every HTTP MCP entry. + */ +export function isTerminalTransportError(error: Error): boolean { + if (error.name === 'UnauthorizedError') return true; + if (/Maximum reconnection attempts/i.test(error.message)) return true; + return false; +} + +export function buildMcpHttpHeaders( + config: McpServerHttpConfig, + envLookup: (name: string) => string | undefined, +): Record | undefined { + const headers: Record = { ...config.headers }; + if (config.bearerTokenEnvVar !== undefined) { + const token = envLookup(config.bearerTokenEnvVar); + if (token === undefined || token.length === 0) { + throw new KimiError(ErrorCodes.CONFIG_INVALID, `MCP HTTP bearer token env var "${config.bearerTokenEnvVar}" is not set or is empty`); + } + // Strip any case-variant 'authorization' static header before injecting the + // bearer; Fetch Headers folds duplicate keys into a comma-joined value, + // which produces an invalid auth header rather than letting the bearer win. + for (const key of Object.keys(headers)) { + if (key.toLowerCase() === 'authorization') { + delete headers[key]; + } + } + headers['Authorization'] = `Bearer ${token}`; + } + return Object.keys(headers).length > 0 ? headers : undefined; +} diff --git a/packages/agent-core/src/mcp/client-shared.ts b/packages/agent-core/src/mcp/client-shared.ts new file mode 100644 index 000000000..b36d5fad8 --- /dev/null +++ b/packages/agent-core/src/mcp/client-shared.ts @@ -0,0 +1,91 @@ +import { getCoreVersion } from '#/version'; + +import type { MCPToolDefinition, MCPToolResult } from './types'; + +export const KIMI_MCP_CLIENT_NAME = 'kimi-code'; +// Resolved from agent-core's package.json so MCP servers see the real version +// in `initialize` (used for compatibility checks, telemetry, debugging). +// `getCoreVersion()` falls back to '0.0.0' if the package.json read fails. +export const KIMI_MCP_CLIENT_VERSION = getCoreVersion(); + +/** + * Why-context attached when a runtime client notices its underlying transport + * has gone away on its own — i.e. {@link RuntimeMcpClient.close} was NOT + * called. The connection manager turns this into a `failed` status so the + * UI/SDK do not keep advertising tools backed by a dead transport. + * + * - `error` is the last error reported via the SDK's `onerror` channel, if + * any. Useful for HTTP where there is no stderr. + * - `stderr` is the tail of bytes captured from the child process's stderr; + * populated only for the stdio transport. + */ +export interface UnexpectedCloseReason { + readonly error?: Error; + readonly stderr?: string; +} + +export type UnexpectedCloseListener = (reason: UnexpectedCloseReason) => void; + +export interface McpRequestOptions { + readonly timeout?: number; + readonly signal?: AbortSignal; +} + +/** + * Build the `RequestOptions` object accepted by the MCP SDK's `callTool`, + * including either the configured tool-call timeout, an in-flight abort + * signal, both, or neither. Returns `undefined` when nothing needs to be + * passed so the SDK falls back to its defaults. + */ +export function buildRequestOptions( + toolCallTimeoutMs: number | undefined, + signal: AbortSignal | undefined, +): McpRequestOptions | undefined { + if (toolCallTimeoutMs === undefined && signal === undefined) return undefined; + return { timeout: toolCallTimeoutMs, signal }; +} + +interface SdkListedTool { + readonly name: string; + readonly description?: string; + readonly inputSchema: Record; +} + +export function toMcpToolDefinition(tool: SdkListedTool): MCPToolDefinition { + return { + name: tool.name, + description: tool.description ?? '', + inputSchema: tool.inputSchema, + }; +} + +/** + * Normalise the SDK's `callTool` return into kosong's {@link MCPToolResult}. + * The SDK can return either the modern `{ content, isError }` shape or a + * legacy `{ toolResult }` shape; we collapse the legacy shape to a single + * text content block. + */ +export function toMcpToolResult(result: unknown): MCPToolResult { + if (typeof result === 'object' && result !== null && 'content' in result) { + const typed = result as { content: unknown; isError?: unknown }; + if (Array.isArray(typed.content)) { + return { + content: typed.content as MCPToolResult['content'], + isError: typed.isError === true, + }; + } + } + if (typeof result === 'object' && result !== null && 'toolResult' in result) { + const legacy = (result as { toolResult: unknown }).toolResult; + return { + content: [ + { + type: 'text', + text: typeof legacy === 'string' ? legacy : JSON.stringify(legacy), + }, + ], + isError: false, + }; + } + return { content: [], isError: false }; +} diff --git a/packages/agent-core/src/mcp/client-stdio.ts b/packages/agent-core/src/mcp/client-stdio.ts new file mode 100644 index 000000000..55c31aeb1 --- /dev/null +++ b/packages/agent-core/src/mcp/client-stdio.ts @@ -0,0 +1,228 @@ +import { ErrorCodes, KimiError } from '#/errors'; +import type { McpServerStdioConfig } from '#/config/schema'; +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; + +import { + buildRequestOptions, + KIMI_MCP_CLIENT_NAME, + KIMI_MCP_CLIENT_VERSION, + toMcpToolDefinition, + toMcpToolResult, + type UnexpectedCloseListener, + type UnexpectedCloseReason, +} from './client-shared'; +import type { MCPClient, MCPToolDefinition, MCPToolResult } from './types'; + +export interface StdioMcpClientOptions { + readonly clientName?: string; + readonly clientVersion?: string; + readonly toolCallTimeoutMs?: number; +} + +const STDERR_BUFFER_CAPACITY = 4 * 1024; + +/** + * Wraps the `@modelcontextprotocol/sdk` stdio client and exposes the small + * surface required by kosong's {@link MCPClient}. Lifecycle is explicit: + * the caller must `connect()` before use and `close()` to terminate the + * child process. + */ +export class StdioMcpClient implements MCPClient { + private readonly client: Client; + private readonly transport: StdioClientTransport; + private readonly toolCallTimeoutMs?: number; + private readonly stderrBuffer = new BoundedTail(STDERR_BUFFER_CAPACITY); + private started = false; + private closed = false; + // Flips to true only after `client.connect()` resolves AND the caller has + // not torn things down mid-startup. The `onclose` hook uses this to + // distinguish "transport died after the handshake" (→ unexpected close) + // from "transport died during the handshake" (→ `connect()` throws; the + // manager surfaces the failure via `formatStartupError`). + private ready = false; + private hooksInstalled = false; + private unexpectedCloseListener: UnexpectedCloseListener | undefined; + private lastTransportError: Error | undefined; + // Buffered when the transport closes before a listener is installed (e.g. + // a server that exits seconds after answering `tools/list`). Replayed when + // `onUnexpectedClose` registers so the close is never silently dropped. + private pendingUnexpectedClose: UnexpectedCloseReason | undefined; + + /** Capacity (in characters) of the stderr tail captured for diagnostics. */ + static readonly stderrBufferCapacity = STDERR_BUFFER_CAPACITY; + + constructor(config: McpServerStdioConfig, options: StdioMcpClientOptions = {}) { + if (config.executor !== undefined && config.executor !== 'local') { + throw new KimiError(ErrorCodes.NOT_IMPLEMENTED, `MCP stdio executor '${config.executor}' is not yet implemented`); + } + this.transport = new StdioClientTransport({ + command: config.command, + args: config.args, + env: mergeStdioEnv(config.env), + cwd: config.cwd, + stderr: 'pipe', + }); + // `stderr: 'pipe'` means we MUST drain the stream — otherwise the child + // can block on a full pipe. We also keep the last few KB around so the + // connection manager can attach it to user-facing failure messages + // (`Timed out after 30000ms` on its own tells the user nothing). + this.transport.stderr?.on('data', (chunk: Buffer | string) => { + this.stderrBuffer.push(typeof chunk === 'string' ? chunk : chunk.toString('utf8')); + }); + this.client = new Client({ + name: options.clientName ?? KIMI_MCP_CLIENT_NAME, + version: options.clientVersion ?? KIMI_MCP_CLIENT_VERSION, + }); + this.toolCallTimeoutMs = options.toolCallTimeoutMs; + } + + async connect(): Promise { + if (this.closed) { + throw new Error('MCP stdio client is closed'); + } + if (this.started) return; + this.started = true; + // Install transport hooks BEFORE the SDK handshake so we never lose an + // onclose that fires between handshake completion and our wiring. The + // hooks themselves gate on `this.ready`, so a close that happens DURING + // the handshake still flows through `client.connect()` rejecting. + this.installTransportHooks(); + try { + await this.client.connect(this.transport); + } catch (error) { + await this.closeStartedClient(); + throw error; + } + if (this.closed) { + await this.closeStartedClient(); + throw new Error('MCP stdio client was closed during startup'); + } + this.ready = true; + } + + async close(): Promise { + if (this.closed) return; + this.closed = true; + await this.closeStartedClient(); + } + + /** + * Register a listener that fires when the underlying transport closes on + * its own — i.e. the caller has not yet invoked {@link close}. At most one + * listener can be installed; later registrations replace earlier ones. + * Intentional closes never invoke the listener. + * + * If the transport already closed before this method was called, the + * buffered reason is replayed synchronously so the close is never dropped. + */ + onUnexpectedClose(listener: UnexpectedCloseListener): void { + this.unexpectedCloseListener = listener; + const pending = this.pendingUnexpectedClose; + if (pending !== undefined) { + this.pendingUnexpectedClose = undefined; + listener(pending); + } + } + + /** + * Returns the tail of bytes captured from the child's stderr since spawn. + * Bounded by {@link StdioMcpClient.stderrBufferCapacity} so a noisy server + * cannot exhaust memory. + */ + stderrSnapshot(): string { + return this.stderrBuffer.snapshot(); + } + + async listTools(): Promise { + const result = await this.client.listTools(); + return result.tools.map(toMcpToolDefinition); + } + + async callTool( + name: string, + args: Record, + signal?: AbortSignal, + ): Promise { + const requestOptions = buildRequestOptions(this.toolCallTimeoutMs, signal); + const result = await this.client.callTool({ name, arguments: args }, undefined, requestOptions); + return toMcpToolResult(result); + } + + private async closeStartedClient(): Promise { + if (!this.started) return; + this.started = false; + await this.client.close(); + } + + private installTransportHooks(): void { + // Idempotent: `connect()` is the only caller and is itself guarded by + // `started`, but defending here lets future refactors call this freely. + if (this.hooksInstalled) return; + this.hooksInstalled = true; + // `Client.onclose` fires for THREE situations: + // 1. The intentional `close()` path → gated by `this.closed`. + // 2. Transport dying during the SDK handshake → gated by `!this.ready`; + // the failure already surfaces via `client.connect()` rejecting, and + // `formatStartupError` attaches stderr at the manager layer. + // 3. Transport dying after the handshake succeeded → the case we care + // about: fire or buffer for the manager's watch listener. + this.client.onclose = () => { + if (this.closed) return; + if (!this.ready) return; + const stderr = this.stderrBuffer.snapshot(); + const reason: UnexpectedCloseReason = { + error: this.lastTransportError, + stderr: stderr.length > 0 ? stderr : undefined, + }; + const listener = this.unexpectedCloseListener; + if (listener !== undefined) { + listener(reason); + } else { + // Buffer so a listener registered moments later still sees the close. + this.pendingUnexpectedClose = reason; + } + }; + this.client.onerror = (error) => { + // Errors are informational on their own — `_onclose` is what tells us + // the transport is gone — so just remember the latest one and let the + // close handler decide whether to surface it. During startup the thrown + // error from `client.connect()` already carries the message, so this + // capture is only load-bearing post-`ready`. + this.lastTransportError = error; + }; + } +} + +/** + * A bounded "tail" buffer: appends characters and drops the oldest when the + * total exceeds `capacity`. Used to keep the last few KB of child-process + * stderr around without unbounded growth. + */ +class BoundedTail { + private buffer = ''; + constructor(private readonly capacity: number) {} + + push(chunk: string): void { + this.buffer += chunk; + if (this.buffer.length > this.capacity) { + this.buffer = this.buffer.slice(this.buffer.length - this.capacity); + } + } + + snapshot(): string { + return this.buffer; + } +} + +// Inherit the parent's env so PATH/HOME/etc. survive — otherwise `npx`/`uvx` +// style stdio servers fail to launch even with a valid config. Explicit +// `config.env` entries still override on conflict. +function mergeStdioEnv(configEnv?: Record): Record { + const merged: Record = {}; + for (const [key, value] of Object.entries(process.env)) { + if (value !== undefined) merged[key] = value; + } + if (configEnv !== undefined) Object.assign(merged, configEnv); + return merged; +} diff --git a/packages/agent-core/src/mcp/config-loader.ts b/packages/agent-core/src/mcp/config-loader.ts new file mode 100644 index 000000000..0c29abffb --- /dev/null +++ b/packages/agent-core/src/mcp/config-loader.ts @@ -0,0 +1,95 @@ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { resolveKimiHome } from '#/config/path'; +import { McpServerConfigSchema, type McpServerConfig } from '#/config/schema'; +import { ErrorCodes, KimiError } from '#/errors'; +import { z } from 'zod'; + +const McpJsonFileSchema = z.object({ + mcpServers: z.record(z.string(), McpServerConfigSchema).default({}), +}); + +export interface McpJsonPaths { + readonly user: string; + readonly project: string; +} + +export interface ResolveMcpJsonPathsInput { + readonly cwd: string; + readonly homeDir?: string; +} + +export function resolveMcpJsonPaths(input: ResolveMcpJsonPathsInput): McpJsonPaths { + return { + user: join(resolveKimiHome(input.homeDir), 'mcp.json'), + project: join(input.cwd, '.kimi-code', 'mcp.json'), + }; +} + +export interface LoadMcpServersInput { + readonly cwd: string; + readonly homeDir?: string; +} + +/** + * Load MCP server declarations from the user-global `~/.kimi-code/mcp.json` + * and the project-local `/.kimi-code/mcp.json`. Entries in the project + * file override user-global entries with the same key, so a repo can specialise + * or replace a shared definition. + * + * Note: project-local entries may spawn stdio commands at session start, so + * opening a session inside an untrusted checkout will execute whatever its + * `mcp.json` declares. Only enable this in repos you trust. + */ +export async function loadMcpServers( + input: LoadMcpServersInput, +): Promise> { + const paths = resolveMcpJsonPaths({ cwd: input.cwd, homeDir: input.homeDir }); + const [user, project] = await Promise.all([readMcpJson(paths.user), readMcpJson(paths.project)]); + return { ...user, ...project }; +} + +async function readMcpJson(filePath: string): Promise> { + let text: string; + try { + text = await readFile(filePath, 'utf-8'); + } catch (error: unknown) { + if (isFileNotFound(error)) return {}; + throw new KimiError(ErrorCodes.CONFIG_INVALID, `Failed to read ${filePath}: ${describeError(error)}`, { + cause: error, + }); + } + + if (text.trim().length === 0) return {}; + + let data: unknown; + try { + data = JSON.parse(text); + } catch (error: unknown) { + throw new KimiError(ErrorCodes.CONFIG_INVALID, `Invalid JSON in ${filePath}: ${describeError(error)}`, { + cause: error, + }); + } + + try { + return McpJsonFileSchema.parse(data).mcpServers; + } catch (error: unknown) { + throw new KimiError(ErrorCodes.CONFIG_INVALID, `Invalid MCP server config in ${filePath}: ${describeError(error)}`, { + cause: error, + }); + } +} + +function isFileNotFound(error: unknown): boolean { + return ( + typeof error === 'object' && + error !== null && + 'code' in error && + (error as { code: unknown }).code === 'ENOENT' + ); +} + +function describeError(error: unknown): string { + return error instanceof Error ? error.message : String(error); +} diff --git a/packages/agent-core/src/mcp/connection-manager.ts b/packages/agent-core/src/mcp/connection-manager.ts new file mode 100644 index 000000000..fb3b861f2 --- /dev/null +++ b/packages/agent-core/src/mcp/connection-manager.ts @@ -0,0 +1,462 @@ +import { ErrorCodes, KimiError } from '#/errors'; +import type { McpServerConfig } from '#/config/schema'; +import { log as defaultLog } from '#/logging/logger'; +import type { Logger } from '#/logging/types'; +import type { Tool } from '@moonshot-ai/kosong'; + +import { abortable } from '../utils/abort'; +import { HttpMcpClient } from './client-http'; +import type { UnexpectedCloseReason } from './client-shared'; +import { StdioMcpClient } from './client-stdio'; +import type { McpOAuthService } from './oauth'; +import { assertMcpInputSchema, type MCPClient } from './types'; + +export type McpServerStatus = 'pending' | 'connected' | 'failed' | 'disabled' | 'needs-auth'; + +export interface McpServerEntry { + readonly name: string; + readonly transport: 'stdio' | 'http'; + readonly status: McpServerStatus; + readonly toolCount: number; + readonly error?: string; +} + +interface InternalEntry { + readonly name: string; + readonly config: McpServerConfig; + attemptId: number; + status: McpServerStatus; + tools?: readonly Tool[]; + enabledNames?: ReadonlySet; + error?: string; + client?: RuntimeMcpClient; +} + +export type McpStatusListener = (entry: McpServerEntry) => void; + +const DEFAULT_STARTUP_TIMEOUT_MS = 30_000; + +type RuntimeMcpClient = StdioMcpClient | HttpMcpClient; + +export interface McpConnectionManagerOptions { + readonly envLookup?: (name: string) => string | undefined; + /** + * Optional OAuth orchestrator. When provided, HTTP servers without a + * static bearer token participate in the OAuth-via-synthetic-tool flow: + * - If `oauthService.hasTokens(name, url)` is true, the provider is + * attached to the transport so the SDK can refresh tokens on 401. + * - Connection failures that look like 401 / `UnauthorizedError` flip + * the entry into `needs-auth` instead of `failed`; `/mcp-config` + * drives the browser flow through the synthetic auth tool. + */ + readonly oauthService?: McpOAuthService; + /** + * Parent logger. Defaults to the global `log`; Session passes its own + * `session.log` so MCP events land in the session log too. + */ + readonly log?: Logger; +} + +/** + * Owns the lifecycle of every configured MCP server for a Session. + * + * Servers are connected in parallel; per-server failures are isolated so a + * crashed or misconfigured entry never blocks Session startup. State + * transitions are surfaced through {@link onStatusChange} so callers (the + * Session) can react — registering tools onto the main agent, emitting + * wire events, or updating the TUI. + */ +export class McpConnectionManager { + private readonly entries = new Map(); + private readonly listeners = new Set(); + private initialLoad: Promise = Promise.resolve(); + private initialLoadAttemptId = 0; + private initialLoadStartedAt: number | undefined; + private initialLoadFinishedAt: number | undefined; + + /** + * OAuth orchestrator injected at construction time. Consumed by the + * {@link ToolManager} `needs-auth` branch to build the synthetic + * `authenticate` tool. + */ + readonly oauthService: McpOAuthService | undefined; + private readonly log: Logger; + + constructor(private readonly options: McpConnectionManagerOptions = {}) { + this.oauthService = options.oauthService; + this.log = options.log ?? defaultLog; + } + + /** + * Returns the URL of an HTTP MCP server by name, or `undefined` for + * unknown / non-HTTP / disabled entries. Used by the synthetic auth tool + * to drive OAuth discovery against the right base URL. + */ + getHttpServerUrl(name: string): string | undefined { + const entry = this.entries.get(name); + if (entry === undefined) return undefined; + if (entry.config.transport !== 'http') return undefined; + return entry.config.url; + } + + onStatusChange(listener: McpStatusListener): () => void { + this.listeners.add(listener); + return () => { + this.listeners.delete(listener); + }; + } + + list(): readonly McpServerEntry[] { + return Array.from(this.entries.values(), toPublicEntry); + } + + get(name: string): McpServerEntry | undefined { + const entry = this.entries.get(name); + return entry !== undefined ? toPublicEntry(entry) : undefined; + } + + /** + * Returns the MCP client, the discovered tools, and the allow-list of tool + * names for a given connected server, or `undefined` if the server is not + * currently connected. The allow-list combines the server's `enabledTools` + * and `disabledTools` filters; callers should only register names in the + * set. + */ + resolved( + name: string, + ): + | { client: MCPClient; tools: readonly Tool[]; enabledNames: ReadonlySet } + | undefined { + const entry = this.entries.get(name); + if ( + entry?.status !== 'connected' || + entry.tools === undefined || + entry.client === undefined + ) { + return undefined; + } + return { + client: entry.client, + tools: entry.tools, + enabledNames: entry.enabledNames ?? new Set(entry.tools.map((t) => t.name)), + }; + } + + connectAll(configs: Record): Promise { + const attemptId = ++this.initialLoadAttemptId; + this.initialLoadStartedAt = Date.now(); + this.initialLoadFinishedAt = undefined; + const initialLoad = this.connectAllNow(configs).finally(() => { + if (this.initialLoadAttemptId === attemptId) { + this.initialLoadFinishedAt = Date.now(); + } + }); + this.initialLoad = initialLoad; + return initialLoad; + } + + waitForInitialLoad(signal?: AbortSignal): Promise { + signal?.throwIfAborted(); + if (signal === undefined) return this.initialLoad; + return abortable(this.initialLoad, signal); + } + + initialLoadDurationMs(): number { + if (this.initialLoadStartedAt === undefined) return 0; + const endedAt = this.initialLoadFinishedAt ?? Date.now(); + return Math.max(0, endedAt - this.initialLoadStartedAt); + } + + private async connectAllNow(configs: Record): Promise { + const tasks: Promise[] = []; + for (const [name, config] of Object.entries(configs)) { + const disabled = config.enabled === false; + const entry: InternalEntry = { + name, + config, + attemptId: 0, + status: disabled ? 'disabled' : 'pending', + }; + this.entries.set(name, entry); + this.emit(entry); + if (!disabled) { + tasks.push(this.connectOne(entry, this.beginConnectAttempt(entry))); + } + } + await Promise.allSettled(tasks); + } + + async reconnect(name: string): Promise { + const entry = this.entries.get(name); + if (entry === undefined) { + throw new KimiError(ErrorCodes.MCP_SERVER_NOT_FOUND, `Unknown MCP server: ${name}`); + } + if (entry.config.enabled === false) { + throw new KimiError(ErrorCodes.MCP_SERVER_DISABLED, `MCP server is disabled: ${name}`); + } + const attemptId = this.beginConnectAttempt(entry); + await this.closeClient(entry); + if (!this.isCurrent(entry, attemptId)) return; + entry.status = 'pending'; + entry.tools = undefined; + entry.enabledNames = undefined; + entry.error = undefined; + this.emit(entry); + await this.connectOne(entry, attemptId); + } + + async shutdown(): Promise { + const entries = Array.from(this.entries.values()); + this.entries.clear(); + const tasks = entries.map((entry) => this.closeClient(entry)); + await Promise.allSettled(tasks); + } + + private async connectOne(entry: InternalEntry, attemptId: number): Promise { + const timeoutMs = entry.config.startupTimeoutMs ?? DEFAULT_STARTUP_TIMEOUT_MS; + + let client: RuntimeMcpClient | undefined; + try { + const startupClient = this.createClient(entry.config, entry.name); + client = startupClient; + entry.client = startupClient; + const tools = await withTimeout( + this.connectAndDiscoverTools(startupClient), + timeoutMs, + () => { + // Best-effort cleanup if the startup promise is still racing. + void this.closeRuntimeClient(startupClient); + }, + ); + if (!this.isCurrent(entry, attemptId)) { + await this.closeRuntimeClient(startupClient); + return; + } + entry.tools = tools; + entry.enabledNames = computeEnabledNames(entry.config, tools); + entry.status = 'connected'; + this.watchForUnexpectedClose(entry, startupClient, attemptId); + } catch (error) { + if (!this.isCurrent(entry, attemptId)) { + if (client !== undefined) { + await this.closeRuntimeClient(client); + } + return; + } + if (this.shouldMarkNeedsAuth(entry, error)) { + entry.status = 'needs-auth'; + entry.error = `${entry.name} requires OAuth — run /mcp-config login ${entry.name}`; + } else { + entry.status = 'failed'; + entry.error = formatStartupError(error, client); + } + entry.tools = undefined; + entry.enabledNames = undefined; + // Drop the client reference so a later reconnect builds a fresh one. + await this.closeClient(entry); + } + if (!this.isCurrent(entry, attemptId)) return; + this.emit(entry); + } + + private watchForUnexpectedClose( + entry: InternalEntry, + client: RuntimeMcpClient, + attemptId: number, + ): void { + client.onUnexpectedClose((reason) => { + // The client may have outlived its entry (shutdown / reconnect already + // moved on). Drop the event if so — the new attempt owns the state. + if (!this.isCurrent(entry, attemptId)) return; + if (entry.client !== client) return; + entry.status = 'failed'; + entry.error = formatUnexpectedCloseError(entry.name, reason); + entry.tools = undefined; + entry.enabledNames = undefined; + entry.client = undefined; + // Best-effort close; the transport is already gone, but this lets the + // SDK release timers and pending request handlers. + void this.closeRuntimeClient(client); + this.emit(entry); + }); + } + + private beginConnectAttempt(entry: InternalEntry): number { + entry.attemptId += 1; + return entry.attemptId; + } + + private createClient(config: McpServerConfig, name: string): RuntimeMcpClient { + const toolCallTimeoutMs = config.toolTimeoutMs; + if (config.transport === 'stdio') { + return new StdioMcpClient(config, { toolCallTimeoutMs }); + } + return new HttpMcpClient(config, { + toolCallTimeoutMs, + envLookup: this.options.envLookup, + oauthProvider: this.resolveOAuthProvider(config, name), + }); + } + + private resolveOAuthProvider( + config: McpServerConfig, + name: string, + ): ReturnType | undefined { + const oauthService = this.oauthService; + if (oauthService === undefined) return undefined; + if (config.transport !== 'http') return undefined; + if (config.bearerTokenEnvVar !== undefined) return undefined; + // Only attach the provider once tokens have been minted; before that, + // the transport should propagate a clean 401 so we can flip the entry + // into `needs-auth` rather than getting tangled in the SDK's auth() + // flow (which would try DCR before we have an active redirect URL). + if (!oauthService.hasTokens(name, config.url)) return undefined; + return oauthService.getProvider(name, config.url); + } + + private shouldMarkNeedsAuth(entry: InternalEntry, error: unknown): boolean { + if (this.oauthService === undefined) return false; + if (entry.config.transport !== 'http') return false; + if (entry.config.bearerTokenEnvVar !== undefined) return false; + // If the user pinned a static `headers` block, treat 401s as a bad header + // rather than hijacking them into the OAuth flow — the real error is more + // actionable than "run /mcp-config login" for a server that doesn't speak + // OAuth. + if (entry.config.headers !== undefined) return false; + return isUnauthorizedLikeError(error); + } + + private async connectAndDiscoverTools(client: RuntimeMcpClient): Promise { + await client.connect(); + const mcpTools = await client.listTools(); + return mcpTools.map((mcpTool) => ({ + name: mcpTool.name, + description: mcpTool.description, + parameters: assertMcpInputSchema(mcpTool.name, mcpTool.inputSchema), + })); + } + + private async closeClient(entry: InternalEntry): Promise { + if (entry.client === undefined) return; + const client = entry.client; + entry.client = undefined; + await this.closeRuntimeClient(client); + } + + private async closeRuntimeClient(client: RuntimeMcpClient): Promise { + try { + await client.close(); + } catch { + // Suppress close errors — the server is going away regardless and we + // don't want them masking the original startup failure. + } + } + + private isCurrent(entry: InternalEntry, attemptId: number): boolean { + return this.entries.get(entry.name) === entry && entry.attemptId === attemptId; + } + + private emit(entry: InternalEntry): void { + const view = toPublicEntry(entry); + if (view.status === 'failed' || view.status === 'needs-auth') { + this.log.error('mcp server unavailable', { + server: view.name, + transport: view.transport, + status: view.status, + reason: view.error, + }); + } + for (const listener of this.listeners) { + try { + listener(view); + } catch { + // Listener faults must not break the connection manager. + } + } + } +} + +function toPublicEntry(entry: InternalEntry): McpServerEntry { + return { + name: entry.name, + transport: entry.config.transport, + status: entry.status, + toolCount: + entry.status === 'connected' && entry.enabledNames !== undefined + ? entry.enabledNames.size + : 0, + error: entry.error, + }; +} + +function computeEnabledNames(config: McpServerConfig, tools: readonly Tool[]): Set { + const all = tools.map((t) => t.name); + const enabledFilter = + config.enabledTools !== undefined ? new Set(config.enabledTools) : undefined; + const disabledFilter = + config.disabledTools !== undefined ? new Set(config.disabledTools) : undefined; + const allowed = new Set(); + for (const name of all) { + if (enabledFilter !== undefined && !enabledFilter.has(name)) continue; + if (disabledFilter !== undefined && disabledFilter.has(name)) continue; + allowed.add(name); + } + return allowed; +} + +function isUnauthorizedLikeError(error: unknown): boolean { + if (!(error instanceof Error)) return false; + if (error.name === 'UnauthorizedError') return true; + // SDK transport errors typically expose the HTTP status as `.code`. + const code = (error as { code?: unknown }).code; + if (typeof code === 'number' && code === 401) return true; + if (typeof code === 'string' && code === '401') return true; + // Fall back to a message sniff so server-specific error shapes still flip + // us into needs-auth instead of failed. + return /\b401\b/.test(error.message) || /unauthorized/i.test(error.message); +} + +function formatStartupError(error: unknown, client: RuntimeMcpClient | undefined): string { + const base = error instanceof Error ? error.message : String(error); + const tail = stderrTail(client); + if (tail === undefined) return base; + return `${base}\nstderr: ${tail}`; +} + +function formatUnexpectedCloseError(name: string, reason: UnexpectedCloseReason): string { + const parts = [`MCP server "${name}" closed unexpectedly`]; + if (reason.error !== undefined) { + parts.push(reason.error.message); + } + if (reason.stderr !== undefined && reason.stderr.length > 0) { + parts.push(`stderr: ${reason.stderr.trimEnd()}`); + } + return parts.join('\n'); +} + +function stderrTail(client: RuntimeMcpClient | undefined): string | undefined { + if (client === undefined) return undefined; + if (!(client instanceof StdioMcpClient)) return undefined; + const snapshot = client.stderrSnapshot(); + if (snapshot.length === 0) return undefined; + return snapshot.trimEnd(); +} + +async function withTimeout( + promise: Promise, + timeoutMs: number, + onTimeout?: () => void, +): Promise { + let timer: NodeJS.Timeout | undefined; + try { + return await new Promise((resolve, reject) => { + timer = setTimeout(() => { + onTimeout?.(); + reject(new Error(`Timed out after ${timeoutMs}ms`)); + }, timeoutMs); + promise.then(resolve, reject); + }); + } finally { + if (timer !== undefined) clearTimeout(timer); + } +} diff --git a/packages/agent-core/src/mcp/index.ts b/packages/agent-core/src/mcp/index.ts new file mode 100644 index 000000000..d0fef23f6 --- /dev/null +++ b/packages/agent-core/src/mcp/index.ts @@ -0,0 +1,5 @@ +export * from './connection-manager'; +export * from './oauth'; +export * from './session-config'; +export * from './tool-naming'; +export * from './types'; diff --git a/packages/agent-core/src/mcp/oauth/callback-server.ts b/packages/agent-core/src/mcp/oauth/callback-server.ts new file mode 100644 index 000000000..a1c902e28 --- /dev/null +++ b/packages/agent-core/src/mcp/oauth/callback-server.ts @@ -0,0 +1,164 @@ +/** + * One-shot localhost OAuth callback listener. + * + * `startCallbackServer()` binds 127.0.0.1 on a random free port and returns a + * handle exposing the resulting `redirect_uri` and an awaitable + * `waitForCode()` that resolves with `{ code, state }` from the first + * `/callback` request. Any subsequent requests get a generic 404 and a + * non-callback path is ignored. The server is closed automatically once a + * code has been delivered (or `close()` is called explicitly). + */ + +import { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http'; +import type { AddressInfo } from 'node:net'; + +export interface CallbackResult { + readonly code: string; + readonly state: string | undefined; +} + +export interface CallbackServer { + readonly redirectUri: string; + /** + * Resolves with the OAuth callback payload, or rejects when: + * - `signal` aborts → AbortError + * - `timeoutMs` elapses → Error('OAuth callback timed out') + * - the user's authorization server returns an error → Error('OAuth error: ') + */ + waitForCode(opts: { signal?: AbortSignal; timeoutMs?: number }): Promise; + close(): Promise; +} + +const SUCCESS_HTML = + 'Authorized' + + '' + + '

    Sign-in complete

    ' + + '

    You can close this tab and return to kimi-code.

    ' + + ''; + +const ERROR_HTML = + 'OAuth error' + + '' + + '

    Sign-in failed

    ' + + '

    The authorization server reported an error. Return to kimi-code for details.

    ' + + ''; + +export async function startCallbackServer(): Promise { + let resolveCode: ((value: CallbackResult) => void) | undefined; + let rejectCode: ((reason: Error) => void) | undefined; + let settled = false; + + const settle = (fn: () => void) => { + if (settled) return; + settled = true; + fn(); + }; + + const server: Server = createServer((req, res) => { + handle(req, res); + }); + + function handle(req: IncomingMessage, res: ServerResponse): void { + if (req.method !== 'GET' || req.url === undefined) { + res.writeHead(404).end(); + return; + } + let url: URL; + try { + url = new URL(req.url, 'http://localhost'); + } catch { + res.writeHead(404).end(); + return; + } + if (url.pathname !== '/callback') { + res.writeHead(404).end(); + return; + } + const errorParam = url.searchParams.get('error'); + if (errorParam !== null) { + const description = url.searchParams.get('error_description') ?? ''; + res.writeHead(400, { 'content-type': 'text/html; charset=utf-8' }).end(ERROR_HTML); + settle(() => { + rejectCode?.( + new Error(`OAuth error: ${errorParam}${description ? ` — ${description}` : ''}`), + ); + }); + return; + } + const code = url.searchParams.get('code'); + if (code === null || code.length === 0) { + res.writeHead(400, { 'content-type': 'text/html; charset=utf-8' }).end(ERROR_HTML); + settle(() => { + rejectCode?.(new Error('OAuth callback missing authorization code')); + }); + return; + } + const state = url.searchParams.get('state') ?? undefined; + res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' }).end(SUCCESS_HTML); + settle(() => { + resolveCode?.({ code, state }); + }); + } + + await new Promise((resolve, reject) => { + server.once('error', reject); + server.listen(0, '127.0.0.1', () => { + server.off('error', reject); + resolve(); + }); + }); + const port = (server.address() as AddressInfo).port; + const redirectUri = `http://127.0.0.1:${port}/callback`; + + let closed = false; + const close = async () => { + if (closed) return; + closed = true; + await new Promise((resolve) => { + server.close(() => { + resolve(); + }); + }); + }; + + const waitForCode: CallbackServer['waitForCode'] = ({ signal, timeoutMs } = {}) => { + return new Promise((resolve, reject) => { + let timer: NodeJS.Timeout | undefined; + const onAbort = () => { + settle(() => + rejectCode?.( + signal?.reason instanceof Error ? signal.reason : new Error('OAuth flow aborted'), + ), + ); + }; + const cleanup = () => { + if (timer !== undefined) clearTimeout(timer); + signal?.removeEventListener('abort', onAbort); + }; + resolveCode = (value) => { + cleanup(); + void close(); + resolve(value); + }; + rejectCode = (reason) => { + cleanup(); + void close(); + reject(reason); + }; + if (timeoutMs !== undefined) { + timer = setTimeout(() => { + settle(() => rejectCode?.(new Error('OAuth callback timed out'))); + }, timeoutMs); + } + if (signal !== undefined) { + if (signal.aborted) { + onAbort(); + return; + } + signal.addEventListener('abort', onAbort, { once: true }); + } + }); + }; + + return { redirectUri, waitForCode, close }; +} diff --git a/packages/agent-core/src/mcp/oauth/index.ts b/packages/agent-core/src/mcp/oauth/index.ts new file mode 100644 index 000000000..87c98552a --- /dev/null +++ b/packages/agent-core/src/mcp/oauth/index.ts @@ -0,0 +1,4 @@ +export * from './callback-server'; +export * from './provider'; +export * from './service'; +export * from './store'; diff --git a/packages/agent-core/src/mcp/oauth/provider.ts b/packages/agent-core/src/mcp/oauth/provider.ts new file mode 100644 index 000000000..be90113e7 --- /dev/null +++ b/packages/agent-core/src/mcp/oauth/provider.ts @@ -0,0 +1,188 @@ +/** + * `OAuthClientProvider` implementation backed by per-MCP-server JSON files. + * + * One provider instance per server/resource identity. The provider: + * - Persists OAuth tokens, the registered DCR client info, and discovery + * state under `/credentials/mcp/-*.json` + * (mode 0600; default home is `~/.kimi-code`). + * - Captures the authorization URL when the SDK calls + * `redirectToAuthorization` — the {@link McpOAuthService} reads that field + * after the first `auth()` call returns `'REDIRECT'`. + * - Keeps the PKCE verifier and OAuth `state` in-memory (one flow per + * provider at a time; callers serialize via the service). + * + * The provider does **not** open browsers or run servers. The service is the + * orchestrator; the provider is the persistence + flow-state shim. + */ + +import { randomBytes } from 'node:crypto'; + +import type { + OAuthClientProvider, + OAuthDiscoveryState, +} from '@modelcontextprotocol/sdk/client/auth.js'; +import type { + OAuthClientInformationFull, + OAuthClientInformationMixed, + OAuthClientMetadata, + OAuthTokens, +} from '@modelcontextprotocol/sdk/shared/auth.js'; + +import { JsonFileStore, canonicalMcpOAuthResource, mcpOAuthStoreKey } from './store'; + +const TOKENS_SUFFIX = '-tokens.json'; +const CLIENT_SUFFIX = '-client.json'; +const DISCOVERY_SUFFIX = '-discovery.json'; +// Used only when the SDK probes auth during normal transport startup and no +// callback listener is active. Interactive login overrides it with a real URL. +const PASSIVE_REDIRECT_URI = 'http://127.0.0.1:3118/callback'; + +export interface McpOAuthProviderOptions { + /** Friendly name of the MCP server; used in DCR `client_name`. */ + readonly serverName: string; + /** Canonical resource identity used to isolate credentials for this server entry. */ + readonly serverUrl: string | URL; + /** JSON store used for persistence. Tests inject an in-memory dir. */ + readonly store: JsonFileStore; + /** Identifier embedded in DCR `client_name` ("kimi-code (server)"). */ + readonly clientLabel?: string; +} + +export class McpOAuthClientProvider implements OAuthClientProvider { + readonly storeKey: string; + readonly serverUrl: string; + private readonly store: JsonFileStore; + private readonly clientLabel: string; + private _redirectUrl: URL | undefined; + private _codeVerifier: string | undefined; + private _state: string | undefined; + private _lastAuthorizationUrl: URL | undefined; + + constructor(options: McpOAuthProviderOptions) { + this.serverUrl = canonicalMcpOAuthResource(options.serverUrl); + this.storeKey = mcpOAuthStoreKey(options.serverName, this.serverUrl); + this.store = options.store; + this.clientLabel = options.clientLabel ?? `kimi-code (${options.serverName})`; + } + + // ── flow-scoped state, set by McpOAuthService before invoking auth() ──── + + setRedirectUrl(url: URL): void { + this._redirectUrl = url; + } + + /** URL captured from the most recent `redirectToAuthorization` call. */ + takeAuthorizationUrl(): URL | undefined { + const url = this._lastAuthorizationUrl; + this._lastAuthorizationUrl = undefined; + return url; + } + + /** OAuth `state` value generated for the most recent flow, for callback verification. */ + expectedState(): string | undefined { + return this._state; + } + + resetFlow(): void { + this._redirectUrl = undefined; + this._codeVerifier = undefined; + this._state = undefined; + this._lastAuthorizationUrl = undefined; + } + + // ── OAuthClientProvider ───────────────────────────────────────────────── + + get redirectUrl(): string | URL { + return this.effectiveRedirectUri(); + } + + get clientMetadata(): OAuthClientMetadata { + return { + redirect_uris: [this.effectiveRedirectUri()], + token_endpoint_auth_method: 'none', + grant_types: ['authorization_code', 'refresh_token'], + response_types: ['code'], + client_name: this.clientLabel, + }; + } + + state(): string { + this._state ??= randomBytes(16).toString('hex'); + return this._state; + } + + clientInformation(): OAuthClientInformationMixed | undefined { + return this.store.read(`${this.storeKey}${CLIENT_SUFFIX}`); + } + + saveClientInformation(info: OAuthClientInformationMixed): void { + this.store.write(`${this.storeKey}${CLIENT_SUFFIX}`, info); + } + + tokens(): OAuthTokens | undefined { + return this.store.read(`${this.storeKey}${TOKENS_SUFFIX}`); + } + + saveTokens(tokens: OAuthTokens): void { + this.store.write(`${this.storeKey}${TOKENS_SUFFIX}`, tokens); + } + + redirectToAuthorization(url: URL): void { + // Capture the URL for the orchestrator instead of actually opening a + // browser. The synthetic authenticate tool surfaces it to the model so + // the user can complete the flow on their own schedule. + this._lastAuthorizationUrl = url; + } + + saveCodeVerifier(codeVerifier: string): void { + this._codeVerifier = codeVerifier; + } + + codeVerifier(): string { + if (this._codeVerifier === undefined) { + throw new Error('McpOAuthClientProvider: PKCE code verifier not initialized'); + } + return this._codeVerifier; + } + + saveDiscoveryState(state: OAuthDiscoveryState): void { + this.store.write(`${this.storeKey}${DISCOVERY_SUFFIX}`, state); + } + + discoveryState(): OAuthDiscoveryState | undefined { + return this.store.read(`${this.storeKey}${DISCOVERY_SUFFIX}`); + } + + invalidateCredentials(scope: 'all' | 'client' | 'tokens' | 'verifier' | 'discovery'): void { + if (scope === 'verifier') { + this._codeVerifier = undefined; + return; + } + if (scope === 'tokens' || scope === 'all') { + this.store.remove(`${this.storeKey}${TOKENS_SUFFIX}`); + } + if (scope === 'client' || scope === 'all') { + this.store.remove(`${this.storeKey}${CLIENT_SUFFIX}`); + } + if (scope === 'discovery' || scope === 'all') { + this.store.remove(`${this.storeKey}${DISCOVERY_SUFFIX}`); + } + if (scope === 'all') { + this._codeVerifier = undefined; + } + } + + private effectiveRedirectUri(): string { + if (this._redirectUrl !== undefined) { + return this._redirectUrl.toString(); + } + const registered = registeredRedirectUri(this.clientInformation()); + return registered ?? PASSIVE_REDIRECT_URI; + } +} + +function registeredRedirectUri(info: OAuthClientInformationMixed | undefined): string | undefined { + if (info === undefined || !('redirect_uris' in info)) return undefined; + const [redirectUri] = info.redirect_uris; + return redirectUri; +} diff --git a/packages/agent-core/src/mcp/oauth/service.ts b/packages/agent-core/src/mcp/oauth/service.ts new file mode 100644 index 000000000..9b2f807de --- /dev/null +++ b/packages/agent-core/src/mcp/oauth/service.ts @@ -0,0 +1,218 @@ +/** + * Per-process OAuth orchestrator for MCP HTTP servers. + * + * The service owns one {@link McpOAuthClientProvider} per server/resource and + * mediates the synthetic `mcp____authenticate` tool flow: + * + * 1. `getProvider(serverName, serverUrl)` returns the cached provider. + * `HttpMcpClient` hands this to `StreamableHTTPClientTransport.authProvider` + * only when the server has no static bearer token configured **and** the + * provider has stored tokens for that same server URL — first-time + * connections that lack tokens skip the provider entirely so a 401 surfaces + * as `UnauthorizedError` from the transport instead of being swallowed by an + * in-flight `auth()` attempt. + * 2. `beginAuthorization(serverName, serverUrl)` spins up a one-shot + * localhost callback listener, sets the redirect URL on the provider, + * and drives the SDK `auth()` orchestrator forward until it surfaces an + * authorization URL. It returns that URL plus a `complete()` callback + * that finishes the code exchange once the user finishes the browser + * flow. + * 3. After `complete()` resolves successfully the provider has tokens on + * disk; the caller (the synthetic tool) drives a manager-level + * `reconnect` to swap the synthetic tool out for the real MCP tools. + */ + +import { auth, type OAuthClientProvider } from '@modelcontextprotocol/sdk/client/auth.js'; + +import { startCallbackServer, type CallbackServer } from './callback-server'; +import { McpOAuthClientProvider } from './provider'; +import { JsonFileStore, mcpCredentialsDir, mcpOAuthStoreKey } from './store'; + +export interface McpOAuthServiceOptions { + /** Storage backend; overrides `kimiHomeDir` when supplied. */ + readonly store?: JsonFileStore; + /** Resolved Kimi home; credentials default to `/credentials/mcp/`. */ + readonly kimiHomeDir?: string; + /** Override for the label embedded in DCR `client_name`. */ + readonly clientLabel?: string; +} + +export interface BeginAuthorizationOptions { + /** Override the `client_name` embedded in the DCR registration request. */ + readonly clientLabel?: string; +} + +export interface BeginAuthorizationResult { + /** The authorization URL the user must open in their browser. */ + readonly authorizationUrl: URL; + /** + * Awaits the OAuth callback, validates `state`, exchanges the code for + * tokens, and persists them via the provider. Resolves on success; + * rejects on abort, timeout, or auth-server error. + */ + complete(opts?: { signal?: AbortSignal; timeoutMs?: number }): Promise; + /** + * Tears down the callback listener without finishing the flow. Safe to + * call repeatedly; called automatically by `complete()`. + */ + cancel(): Promise; +} + +export class McpOAuthService { + private readonly store: JsonFileStore; + private readonly clientLabel: string | undefined; + private readonly providers = new Map(); + + constructor(options: McpOAuthServiceOptions = {}) { + this.store = + options.store ?? + new JsonFileStore( + options.kimiHomeDir === undefined ? undefined : mcpCredentialsDir(options.kimiHomeDir), + ); + this.clientLabel = options.clientLabel; + } + + /** Returns the cached provider for `serverName` + `serverUrl`, constructing it on first use. */ + getProvider(serverName: string, serverUrl: string | URL): McpOAuthClientProvider { + const storeKey = mcpOAuthStoreKey(serverName, serverUrl); + let provider = this.providers.get(storeKey); + if (provider === undefined) { + provider = new McpOAuthClientProvider({ + serverName, + serverUrl, + store: this.store, + clientLabel: this.clientLabel, + }); + this.providers.set(provider.storeKey, provider); + } + return provider; + } + + /** True once the provider has persisted tokens for this server/resource identity. */ + hasTokens(serverName: string, serverUrl: string | URL): boolean { + return this.getProvider(serverName, serverUrl).tokens() !== undefined; + } + + /** + * Drive the SDK `auth()` orchestrator far enough to surface an + * authorization URL. The caller is responsible for displaying the URL + * (typically via the synthetic authenticate tool) and then awaiting + * `complete()` to finish the code exchange. + */ + async beginAuthorization( + serverName: string, + serverUrl: string | URL, + options: BeginAuthorizationOptions = {}, + ): Promise { + const provider = options.clientLabel === undefined + ? this.getProvider(serverName, serverUrl) + : new McpOAuthClientProvider({ + serverName, + serverUrl, + store: this.store, + clientLabel: options.clientLabel, + }); + if (options.clientLabel !== undefined) { + this.providers.set(provider.storeKey, provider); + } + + provider.resetFlow(); + + let callbackServer: CallbackServer; + try { + callbackServer = await startCallbackServer(); + } catch (error) { + throw wrapAuthError('failed to start OAuth callback listener', error); + } + + provider.setRedirectUrl(new URL(callbackServer.redirectUri)); + + let authorizationUrl: URL | undefined; + try { + const result = await auth(provider as OAuthClientProvider, { serverUrl }); + if (result !== 'REDIRECT') { + // Tokens already valid (e.g. unexpired refresh). Nothing to do. + await callbackServer.close(); + throw new AlreadyAuthorizedError(serverName); + } + authorizationUrl = provider.takeAuthorizationUrl(); + if (authorizationUrl === undefined) { + throw new Error('OAuth provider did not capture an authorization URL'); + } + } catch (error) { + await callbackServer.close().catch(() => undefined); + provider.resetFlow(); + if (error instanceof AlreadyAuthorizedError) throw error; + throw wrapAuthError(`failed to start OAuth flow for "${serverName}"`, error); + } + + let settled = false; + const cancel = async (): Promise => { + if (settled) return; + settled = true; + await callbackServer.close().catch(() => undefined); + provider.resetFlow(); + }; + + const complete: BeginAuthorizationResult['complete'] = async (opts = {}) => { + if (settled) { + throw new Error('OAuth flow already completed or cancelled'); + } + try { + const { code, state } = await callbackServer.waitForCode({ + signal: opts.signal, + timeoutMs: opts.timeoutMs, + }); + const expectedState = provider.expectedState(); + if (expectedState !== undefined && state !== expectedState) { + throw new Error('OAuth state mismatch — possible CSRF; refusing token exchange'); + } + const finalResult = await auth(provider as OAuthClientProvider, { + serverUrl, + authorizationCode: code, + }); + if (finalResult !== 'AUTHORIZED') { + throw new Error(`OAuth code exchange returned "${finalResult}" instead of AUTHORIZED`); + } + } catch (error) { + await cancel(); + throw wrapAuthError(`OAuth flow for "${serverName}" failed`, error); + } + settled = true; + await callbackServer.close().catch(() => undefined); + provider.resetFlow(); + }; + + return { authorizationUrl, complete, cancel }; + } + + /** + * Clear stored credentials for a server. Use `'all'` after the user + * explicitly signs out; use `'tokens'` to force a re-auth while keeping + * the registered DCR client. + */ + invalidate( + serverName: string, + serverUrl: string | URL, + scope: 'all' | 'client' | 'tokens' | 'discovery' = 'all', + ): void { + this.getProvider(serverName, serverUrl).invalidateCredentials(scope); + } +} + +/** Thrown by `beginAuthorization` when stored tokens already satisfy the server. */ +export class AlreadyAuthorizedError extends Error { + constructor(serverName: string) { + super(`"${serverName}" is already authorized; no browser flow needed`); + this.name = 'AlreadyAuthorizedError'; + } +} + +function wrapAuthError(prefix: string, error: unknown): Error { + if (error instanceof Error) { + const wrapped = new Error(`${prefix}: ${error.message}`); + wrapped.cause = error; + return wrapped; + } + return new Error(`${prefix}: ${String(error)}`); +} diff --git a/packages/agent-core/src/mcp/oauth/store.ts b/packages/agent-core/src/mcp/oauth/store.ts new file mode 100644 index 000000000..5c5e07204 --- /dev/null +++ b/packages/agent-core/src/mcp/oauth/store.ts @@ -0,0 +1,130 @@ +/** + * Small atomic JSON file store used by the MCP OAuth provider to persist + * tokens, registered client info, and discovery state under + * `/credentials/mcp/` (default + * `~/.kimi-code/credentials/mcp/`). + * + * Write semantics: write to `.tmp..` → fsync → rename. + * Atomic on POSIX; best-effort on Windows. Files land at mode 0600 (parent + * dir 0700) so other local users cannot read tokens. + * + * Read semantics: missing file → undefined. Corrupt JSON / wrong shape → + * undefined (never throws). The provider treats undefined as "not stored". + */ + +import { createHash, randomBytes } from 'node:crypto'; +import { + chmodSync, + closeSync, + fsyncSync, + mkdirSync, + openSync, + readFileSync, + renameSync, + unlinkSync, + writeSync, +} from 'node:fs'; +import { homedir } from 'node:os'; +import { basename, join } from 'node:path'; + +export function mcpCredentialsDir(kimiHomeDir: string): string { + return join(kimiHomeDir, 'credentials', 'mcp'); +} + +export function defaultMcpCredentialsDir(): string { + return mcpCredentialsDir(join(homedir(), '.kimi-code')); +} + +export function sanitizeStoreKey(name: string): string { + // Strip path-traversal segments. Tokens land under `-.json`, + // so the sanitized value must also be a single filename component. + const safe = basename(name).replaceAll(/[^a-zA-Z0-9_-]/g, '_').replaceAll(/_+/g, '_'); + if (safe.length === 0 || safe.startsWith('.')) { + throw new Error(`Invalid MCP OAuth store key: "${name}"`); + } + return safe; +} + +export function canonicalMcpOAuthResource(serverUrl: string | URL): string { + const url = new URL(serverUrl); + url.hash = ''; + return url.toString(); +} + +export function mcpOAuthStoreKey(serverName: string, serverUrl: string | URL): string { + const safeName = sanitizeStoreKey(serverName); + const resource = canonicalMcpOAuthResource(serverUrl); + const digest = createHash('sha256') + .update(serverName) + .update('\0') + .update(resource) + .digest('hex') + .slice(0, 24); + return `${safeName}-${digest}`; +} + +export class JsonFileStore { + private readonly dir: string; + + constructor(dir: string = defaultMcpCredentialsDir()) { + this.dir = dir; + } + + read(file: string): T | undefined { + const path = join(this.dir, file); + let raw: string; + try { + raw = readFileSync(path, 'utf-8'); + } catch { + return undefined; + } + try { + return JSON.parse(raw) as T; + } catch { + return undefined; + } + } + + write(file: string, data: unknown): void { + mkdirSync(this.dir, { recursive: true, mode: 0o700 }); + try { + chmodSync(this.dir, 0o700); + } catch { + // best-effort; Windows / read-only FS may refuse + } + const target = join(this.dir, file); + const tmp = `${target}.tmp.${process.pid}.${randomBytes(4).toString('hex')}`; + const buf = Buffer.from(`${JSON.stringify(data, null, 2)}\n`, 'utf-8'); + const fd = openSync(tmp, 'w', 0o600); + try { + let written = 0; + while (written < buf.length) { + written += writeSync(fd, buf, written, buf.length - written); + } + fsyncSync(fd); + } finally { + closeSync(fd); + } + try { + chmodSync(tmp, 0o600); + renameSync(tmp, target); + } catch (error) { + try { + unlinkSync(tmp); + } catch { + /* ignore */ + } + throw error; + } + } + + remove(file: string): void { + try { + unlinkSync(join(this.dir, file)); + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== 'ENOENT') { + throw error; + } + } + } +} diff --git a/packages/agent-core/src/mcp/output.ts b/packages/agent-core/src/mcp/output.ts new file mode 100644 index 000000000..9832b04d2 --- /dev/null +++ b/packages/agent-core/src/mcp/output.ts @@ -0,0 +1,258 @@ +/** + * MCP tool-call result → ExecutableTool output pipeline. + * + * Owns the full path from "MCP protocol content blocks" to "what the agent + * loop feeds back to the model": + * 1. Convert each {@link MCPContentBlock} to a kosong `ContentPart` + * (dropping unsupported shapes). + * 2. Wrap media-only outputs in `` tags so the + * model can attribute binary output when several tools return media. + * Mirrors the in-tree `ReadMediaFile` convention. + * 3. Apply size limits: text/think share a 100K character budget; binary + * parts (image/audio/video URLs) each carry an independent 10 MB cap and + * collapse to a notice when oversize, so a single screenshot cannot + * evict every text part. + * 4. Collapse a single-text-part result to a plain string output; otherwise + * emit the `ContentPart[]` as-is. + * + * `mcpResultToExecutableOutput` is the single entry point; the per-step + * helpers stay private so callers cannot bypass the limits. + */ + +import type { ContentPart } from '@moonshot-ai/kosong'; + +import type { MCPContentBlock, MCPToolResult } from './types'; + +// MCP servers can produce arbitrarily large outputs; cap what we feed back to +// the model so a single chatty server does not blow up the context window. The +// notice text is fed to the model verbatim so it can react (e.g. paginate), +// which is why the limits live in the agent layer rather than in kosong. +export const MCP_MAX_OUTPUT_CHARS = 100_000; +const MCP_OUTPUT_TRUNCATED_TEXT = `\n\n[Output truncated: exceeded ${String( + MCP_MAX_OUTPUT_CHARS, +)} character limit. Use pagination or more specific queries to get remaining content.]`; + +// Binary parts (image_url / audio_url / video_url) have an independent per-part +// byte cap and do NOT share the text character budget. base64 length is not a +// useful proxy for multimodal model cost, and a single screenshot is enough to +// evict every text part if both compete for the same 100k budget. +export const MCP_MAX_BINARY_PART_BYTES = 10 * 1024 * 1024; +const MCP_MAX_BINARY_PART_CHARS = Math.ceil((MCP_MAX_BINARY_PART_BYTES * 4) / 3); + +function binaryPartTooLargeNotice(kind: 'image' | 'audio' | 'video', urlLength: number): string { + const approxMb = ((urlLength * 3) / 4 / (1024 * 1024)).toFixed(1); + const capMb = String(MCP_MAX_BINARY_PART_BYTES / (1024 * 1024)); + return `[${kind}_url dropped: ~${approxMb} MB exceeds ${capMb} MB per-part limit. Try a smaller resource.]`; +} + +/** + * Convert a single MCP content block into a kosong {@link ContentPart}. + * + * Returns `null` for block types that cannot be represented (e.g. unknown + * resource shapes) so the caller can drop them. + */ +export function convertMCPContentBlock(block: MCPContentBlock): ContentPart | null { + if (block.type === 'text' && typeof block.text === 'string') { + return { type: 'text', text: block.text }; + } + + if (block.type === 'image' && typeof block.data === 'string') { + const mimeType = block.mimeType ?? 'image/png'; + return { + type: 'image_url', + imageUrl: { url: `data:${mimeType};base64,${block.data}` }, + }; + } + + if (block.type === 'audio' && typeof block.data === 'string') { + const mimeType = block.mimeType ?? 'audio/mpeg'; + return { + type: 'audio_url', + audioUrl: { url: `data:${mimeType};base64,${block.data}` }, + }; + } + + // EmbeddedResource: payload is nested under `resource`, as + // TextResourceContents (`text`) or BlobResourceContents (`blob`). + if (block.type === 'resource' && typeof block.resource === 'object' && block.resource !== null) { + const res = block.resource; + if (typeof res.text === 'string') { + return { type: 'text', text: res.text }; + } + if (typeof res.blob === 'string') { + const mimeType = res.mimeType ?? 'application/octet-stream'; + if (mimeType.startsWith('image/')) { + return { + type: 'image_url', + imageUrl: { url: `data:${mimeType};base64,${res.blob}` }, + }; + } + if (mimeType.startsWith('audio/')) { + return { + type: 'audio_url', + audioUrl: { url: `data:${mimeType};base64,${res.blob}` }, + }; + } + if (mimeType.startsWith('video/')) { + return { + type: 'video_url', + videoUrl: { url: `data:${mimeType};base64,${res.blob}` }, + }; + } + return null; + } + return null; + } + + // ResourceLink: URL reference, not an inline blob. + if (block.type === 'resource_link' && typeof block.uri === 'string') { + const mimeType = block.mimeType ?? 'application/octet-stream'; + if (mimeType.startsWith('image/')) { + return { type: 'image_url', imageUrl: { url: block.uri } }; + } + if (mimeType.startsWith('audio/')) { + return { type: 'audio_url', audioUrl: { url: block.uri } }; + } + if (mimeType.startsWith('video/')) { + return { type: 'video_url', videoUrl: { url: block.uri } }; + } + return null; + } + + return null; +} + +/** + * Convert an `MCPToolResult` into the success-shape `ExecutableToolResult` + * output the agent loop expects. + * + * `qualifiedToolName` is the agent-side qualified name (e.g. + * `mcp__github__create_pr`) — embedded into the `` + * wrap when the result is media-only, so the model can attribute binary parts. + */ +export function mcpResultToExecutableOutput( + result: MCPToolResult, + qualifiedToolName: string, +): { output: string | ContentPart[]; isError: boolean } { + const converted: ContentPart[] = []; + for (const block of result.content) { + const part = convertMCPContentBlock(block); + if (part !== null) { + converted.push(part); + } + } + + const wrapped = wrapMediaOnly(converted, qualifiedToolName); + const limited = applyOutputLimits(wrapped); + const output = collapseSingleText(limited); + return { output, isError: result.isError }; +} + +/** + * If `parts` contains media but no non-empty text, surround it with + * `` text tags so the model can attribute the + * binary content. Returns the input untouched otherwise. + */ +function wrapMediaOnly(parts: readonly ContentPart[], qualifiedToolName: string): ContentPart[] { + const hasMedia = parts.some( + (p) => p.type === 'image_url' || p.type === 'audio_url' || p.type === 'video_url', + ); + const hasNonEmptyText = parts.some((p) => p.type === 'text' && p.text.length > 0); + if (!hasMedia || hasNonEmptyText) return [...parts]; + return [ + { type: 'text', text: `` }, + ...parts, + { type: 'text', text: '' }, + ]; +} + +/** + * Apply the 100K text/think budget and the per-part 10 MB binary cap. + * + * When text/think parts get truncated, the truncation notice is appended to + * the last surviving text part — this keeps the single-text-part collapse + * working when the entire (oversized) input is a single text block. + */ +function applyOutputLimits(parts: readonly ContentPart[]): ContentPart[] { + let remaining = MCP_MAX_OUTPUT_CHARS; + let textTruncated = false; + const out: ContentPart[] = []; + + for (const part of parts) { + if (part.type === 'text') { + if (remaining <= 0) { + textTruncated = true; + continue; + } + if (part.text.length > remaining) { + out.push({ type: 'text', text: part.text.slice(0, remaining) }); + remaining = 0; + textTruncated = true; + } else { + out.push(part); + remaining -= part.text.length; + } + continue; + } + + if (part.type === 'think') { + const size = part.think.length + (part.encrypted?.length ?? 0); + if (remaining <= 0) { + textTruncated = true; + continue; + } + if (size > remaining) { + out.push({ type: 'think', think: part.think.slice(0, remaining) }); + remaining = 0; + textTruncated = true; + } else { + out.push(part); + remaining -= size; + } + continue; + } + + // image_url / audio_url / video_url: per-part byte cap, independent of the + // text character budget. Oversized parts collapse into a per-part notice so + // the model can pick a smaller resource instead of silently losing the blob. + const url = + part.type === 'image_url' + ? part.imageUrl.url + : part.type === 'audio_url' + ? part.audioUrl.url + : part.videoUrl.url; + if (url.length > MCP_MAX_BINARY_PART_CHARS) { + const kind = + part.type === 'image_url' ? 'image' : part.type === 'audio_url' ? 'audio' : 'video'; + out.push({ type: 'text', text: binaryPartTooLargeNotice(kind, url.length) }); + continue; + } + out.push(part); + } + + if (textTruncated) { + appendTruncationNotice(out); + } + return out; +} + +function appendTruncationNotice(out: ContentPart[]): void { + // Merge the notice into the last text part so the very common + // "single oversized text" case still collapses to a plain string. Falls + // back to a standalone notice part if there is no text part to merge with. + for (let i = out.length - 1; i >= 0; i--) { + const candidate = out[i]; + if (candidate?.type === 'text') { + out[i] = { type: 'text', text: candidate.text + MCP_OUTPUT_TRUNCATED_TEXT }; + return; + } + } + out.push({ type: 'text', text: MCP_OUTPUT_TRUNCATED_TEXT }); +} + +function collapseSingleText(parts: readonly ContentPart[]): string | ContentPart[] { + if (parts.length === 1 && parts[0]?.type === 'text') { + return parts[0].text; + } + return [...parts]; +} diff --git a/packages/agent-core/src/mcp/session-config.ts b/packages/agent-core/src/mcp/session-config.ts new file mode 100644 index 000000000..874de1ea1 --- /dev/null +++ b/packages/agent-core/src/mcp/session-config.ts @@ -0,0 +1,23 @@ +import type { McpServerConfig } from '#/config/schema'; + +import { loadMcpServers } from './config-loader'; + +export interface SessionMcpConfig { + readonly servers: Record; +} + +export interface ResolveSessionMcpConfigInput { + readonly cwd: string; + readonly homeDir?: string; +} + +export async function resolveSessionMcpConfig( + input: ResolveSessionMcpConfigInput, +): Promise { + const servers = await loadMcpServers({ + cwd: input.cwd, + homeDir: input.homeDir, + }); + if (Object.keys(servers).length === 0) return undefined; + return { servers }; +} diff --git a/packages/agent-core/src/mcp/tool-naming.ts b/packages/agent-core/src/mcp/tool-naming.ts new file mode 100644 index 000000000..87dca29a5 --- /dev/null +++ b/packages/agent-core/src/mcp/tool-naming.ts @@ -0,0 +1,49 @@ +const MCP_NAME_PREFIX = 'mcp__'; +const MCP_NAME_SEPARATOR = '__'; +/** + * Most LLM providers cap tool names around 64 characters. Leave headroom + * for the prefix and a separator and truncate longer names with a stable + * hash suffix so collisions remain extremely unlikely. + */ +const MAX_QUALIFIED_LENGTH = 64; + +/** + * Replace any character outside the safe ASCII set with `_`, then collapse + * any run of `_` into a single underscore. The collapse step guarantees neither the sanitized server + * nor tool name contains the `__` separator used by {@link qualifyMcpToolName}, + * which lets {@link isMcpToolName}-aware decoders split unambiguously on the + * first `__` after the prefix. + */ +export function sanitizeMcpNamePart(part: string): string { + return part.replaceAll(/[^a-zA-Z0-9_-]/g, '_').replaceAll(/_+/g, '_'); +} + +export function isMcpToolName(name: string): boolean { + return name.startsWith(MCP_NAME_PREFIX); +} + +/** + * Produce the qualified MCP tool name used inside the agent and on the wire. + * If the result would exceed {@link MAX_QUALIFIED_LENGTH}, a deterministic + * 8-char hash suffix replaces the tail so the prefix structure stays intact. + */ +export function qualifyMcpToolName(serverName: string, toolName: string): string { + const full = `${MCP_NAME_PREFIX}${sanitizeMcpNamePart(serverName)}${MCP_NAME_SEPARATOR}${sanitizeMcpNamePart(toolName)}`; + if (full.length <= MAX_QUALIFIED_LENGTH) return full; + + const hash = stableHash8(full); + const head = full.slice(0, MAX_QUALIFIED_LENGTH - hash.length - 1); + return `${head}_${hash}`; +} + +function stableHash8(input: string): string { + // 32-bit FNV-1a — enough to disambiguate truncated tool names within a + // single server's tool list. Not cryptographic; only used for collision + // resistance among a handful of strings. + let hash = 0x811c9dc5; + for (let i = 0; i < input.length; i++) { + hash ^= input.codePointAt(i)!; + hash = Math.trunc(Math.imul(hash, 0x01000193)); + } + return hash.toString(16).padStart(8, '0'); +} diff --git a/packages/agent-core/src/mcp/types.ts b/packages/agent-core/src/mcp/types.ts new file mode 100644 index 000000000..dee8cf4eb --- /dev/null +++ b/packages/agent-core/src/mcp/types.ts @@ -0,0 +1,105 @@ +/** + * MCP protocol types and the minimal client contract `ToolManager` consumes. + * + * Lives in its own file (rather than `toolset.ts`) because the agent-side + * tool-runtime layer is `ExecutableTool`, not the legacy `Toolset` interface. + * What remains here is the wire-level surface: tool definitions returned by + * `tools/list`, the `tools/call` result shape, and the small interface that + * lets tests inject a fake transport without pulling in the MCP SDK type graph. + */ + +/** + * Inline resource contents nested under an EmbeddedResource block. + * Exactly one of `text` or `blob` is populated, per the MCP schema's + * `TextResourceContents | BlobResourceContents` union. + */ +export interface MCPEmbeddedResourceContents { + uri: string; + mimeType?: string; + text?: string; + blob?: string; + [key: string]: unknown; +} + +/** + * A content block as returned by an MCP tool call (`tools/call`). + * + * This is a structural subset of the MCP protocol `ContentBlock` union, + * covering the shapes that {@link convertMCPContentBlock} knows how to convert + * into kosong `ContentPart`s. Additional fields are ignored. + */ +export interface MCPContentBlock { + // Known values: 'text' | 'image' | 'audio' | 'resource' | 'resource_link'. + // Declared as `string` to also accept future MCP content types without a + // type assertion. + type: string; + text?: string; + data?: string; + mimeType?: string; + uri?: string; + // EmbeddedResource carries its payload nested under `resource`, per the + // MCP spec — never as top-level `data`/`mimeType`. + resource?: MCPEmbeddedResourceContents; + [key: string]: unknown; +} + +/** + * Result of a single MCP tool invocation. + * + * Matches the shape returned by the MCP protocol's `tools/call` method. + */ +export interface MCPToolResult { + content: MCPContentBlock[]; + isError: boolean; +} + +/** + * An MCP tool definition as returned by an MCP server's `tools/list` method. + */ +export interface MCPToolDefinition { + name: string; + description: string; + inputSchema: unknown; +} + +/** + * Minimal MCP client interface consumed by {@link McpConnectionManager} and + * {@link ToolManager}. + * + * This is a transport-agnostic seam: implementations can wrap + * `@modelcontextprotocol/sdk`, a bespoke stdio client, an HTTP SSE client, + * or a mock for testing. Keeping the surface small lets tests inject fakes + * without pulling in the full SDK type graph. + */ +export interface MCPClient { + /** List the tools advertised by the MCP server. */ + listTools(): Promise; + /** + * Invoke a tool by name with the given JSON arguments. + * + * `signal`, when provided, is forwarded to the underlying transport so an + * abort from the loop (e.g. user cancellation) propagates all the way to + * the server instead of leaving the request running in the background. + */ + callTool( + name: string, + args: Record, + signal?: AbortSignal, + ): Promise; +} + +/** + * Validate the `inputSchema` field of an MCP tool definition. MCP advertises + * input schemas as JSON Schema objects; reject anything that is not a plain + * object so the validator compiler downstream never sees `null` or a + * primitive. + */ +export function assertMcpInputSchema( + toolName: string, + inputSchema: unknown, +): Record { + if (typeof inputSchema === 'object' && inputSchema !== null && !Array.isArray(inputSchema)) { + return inputSchema as Record; + } + throw new Error(`Invalid inputSchema for MCP tool "${toolName}": schema must be a JSON object`); +} diff --git a/packages/agent-core/src/profile/context.ts b/packages/agent-core/src/profile/context.ts new file mode 100644 index 000000000..ba22806fc --- /dev/null +++ b/packages/agent-core/src/profile/context.ts @@ -0,0 +1,192 @@ +import * as posixPath from 'node:path/posix'; +import * as win32Path from 'node:path/win32'; + +import type { Kaos } from '@moonshot-ai/kaos'; + +import { listDirectory } from '../tools/support/list-directory'; +import type { SystemPromptContext } from './types'; + +const AGENTS_MD_MAX_BYTES = 32 * 1024; +const S_IFMT = 0o170000; +const S_IFREG = 0o100000; + +export type PreparedSystemPromptContext = Pick< + SystemPromptContext, + 'cwd' | 'cwdListing' | 'agentsMd' +>; + +export function resolveSystemPromptCwd(kaos: Kaos, cwd: string): string { + return cwd === '' ? kaos.getcwd() : cwd; +} + +export async function prepareSystemPromptContext( + kaos: Kaos, + cwd: string, +): Promise { + const resolvedCwd = resolveSystemPromptCwd(kaos, cwd); + const [cwdListing, agentsMd] = await Promise.all([ + listDirectory(kaos, resolvedCwd), + loadAgentsMd(kaos, resolvedCwd), + ]); + + return { + cwd: resolvedCwd, + cwdListing, + agentsMd, + }; +} + +export async function loadAgentsMd(kaos: Kaos, workDir: string): Promise { + const projectRoot = await findProjectRoot(kaos, workDir); + const dirs = dirsRootToLeaf(kaos, workDir, projectRoot); + const discovered: AgentFile[] = []; + const seen = new Set(); + + const collect = async (path: string): Promise => { + const file = await readAgentFile(kaos, path); + if (file === undefined) return false; + const key = kaos.normpath(file.path); + if (seen.has(key)) return false; + seen.add(key); + discovered.push(file); + return true; + }; + + // User-level files come first so any project-level AGENTS.md overrides them. + const home = kaos.gethome(); + await collect(joinPath(kaos, home, '.kimi-code', 'AGENTS.md')); + + // Generic user-level dir (.agents) matches skill discovery. + const genericDirs = [joinPath(kaos, home, '.agents')]; + const genericFiles = genericDirs.flatMap((dir) => + ['AGENTS.md', 'agents.md'].map((name) => joinPath(kaos, dir, name)), + ); + for (const file of genericFiles) { + if (await collect(file)) break; + } + + for (const dir of dirs) { + await collect(joinPath(kaos, dir, '.kimi-code', 'AGENTS.md')); + for (const fileName of ['AGENTS.md', 'agents.md']) { + if (await collect(joinPath(kaos, dir, fileName))) break; + } + } + + return renderAgentFiles(discovered); +} + +async function findProjectRoot(kaos: Kaos, workDir: string): Promise { + const path = pathMod(kaos); + const initial = kaos.normpath(workDir); + let current = initial; + + while (true) { + if (await pathExists(kaos, path.join(current, '.git'))) return current; + const parent = path.dirname(current); + if (parent === current) return initial; + current = parent; + } +} + +function dirsRootToLeaf(kaos: Kaos, workDir: string, projectRoot: string): string[] { + const path = pathMod(kaos); + const dirs: string[] = []; + let current = kaos.normpath(workDir); + + while (true) { + dirs.push(current); + if (current === projectRoot) break; + const parent = path.dirname(current); + if (parent === current) break; + current = parent; + } + + return dirs.toReversed(); +} + +interface AgentFile { + readonly path: string; + readonly content: string; +} + +async function readAgentFile(kaos: Kaos, path: string): Promise { + if (!(await isFile(kaos, path))) return undefined; + const content = (await kaos.readText(path, { errors: 'ignore' })).trim(); + if (content.length === 0) return undefined; + return { path, content }; +} + +async function pathExists(kaos: Kaos, path: string): Promise { + try { + await kaos.stat(path); + return true; + } catch { + return false; + } +} + +async function isFile(kaos: Kaos, path: string): Promise { + try { + const stat = await kaos.stat(path); + return (stat.stMode & S_IFMT) === S_IFREG; + } catch { + return false; + } +} + +function renderAgentFiles(files: readonly AgentFile[]): string { + if (files.length === 0) return ''; + + let remaining = AGENTS_MD_MAX_BYTES; + const budgeted: Array = Array.from({ length: files.length }); + + for (let i = files.length - 1; i >= 0; i--) { + const file = files[i]; + if (file === undefined) continue; + + const annotation = annotationFor(file.path); + const separator = i < files.length - 1 ? '\n\n' : ''; + remaining -= byteLength(annotation) + byteLength(separator); + if (remaining <= 0) { + budgeted[i] = { path: file.path, content: '' }; + remaining = 0; + continue; + } + + let content = file.content; + if (byteLength(content) > remaining) { + content = truncateUtf8(content, remaining).trim(); + } + remaining -= byteLength(content); + budgeted[i] = { path: file.path, content }; + } + + return budgeted + .filter((file): file is AgentFile => file !== undefined && file.content.length > 0) + .map((file) => `${annotationFor(file.path)}${file.content}`) + .join('\n\n'); +} + +function truncateUtf8(text: string, maxBytes: number): string { + let result = text; + while (byteLength(result) > maxBytes) { + result = result.slice(0, -1); + } + return result; +} + +function byteLength(text: string): number { + return Buffer.byteLength(text, 'utf8'); +} + +function annotationFor(path: string): string { + return `\n`; +} + +function joinPath(kaos: Kaos, ...parts: string[]): string { + return pathMod(kaos).join(...parts); +} + +function pathMod(kaos: Kaos): typeof posixPath { + return kaos.pathClass() === 'win32' ? win32Path : posixPath; +} diff --git a/packages/agent-core/src/profile/default.ts b/packages/agent-core/src/profile/default.ts new file mode 100644 index 000000000..d3ce92304 --- /dev/null +++ b/packages/agent-core/src/profile/default.ts @@ -0,0 +1,26 @@ +import agentYaml from './default/agent.yaml'; +import coderYaml from './default/coder.yaml'; +import exploreYaml from './default/explore.yaml'; +import initMd from './default/init.md'; +import planYaml from './default/plan.yaml'; +import systemMd from './default/system.md'; +import { loadAgentProfilesFromSources } from './load'; + +// Keyed by the source path the profile loader expects: profile YAML files +// plus any file referenced through `systemPromptPath`. +const PROFILE_SOURCES: Record = { + 'profile/default/agent.yaml': agentYaml, + 'profile/default/coder.yaml': coderYaml, + 'profile/default/explore.yaml': exploreYaml, + 'profile/default/plan.yaml': planYaml, + 'profile/default/system.md': systemMd, +}; + +export const DEFAULT_INIT_PROMPT = initMd; + +export const DEFAULT_AGENT_PROFILES = loadAgentProfilesFromSources( + ['agent.yaml', 'coder.yaml', 'explore.yaml', 'plan.yaml'].map( + (file) => `profile/default/${file}`, + ), + PROFILE_SOURCES, +); diff --git a/packages/agent-core/src/profile/default/agent.yaml b/packages/agent-core/src/profile/default/agent.yaml new file mode 100644 index 000000000..0c0490bec --- /dev/null +++ b/packages/agent-core/src/profile/default/agent.yaml @@ -0,0 +1,35 @@ +name: agent +description: Default Kimi Code agent + +systemPromptPath: ./system.md +promptVars: + roleAdditional: '' + +tools: + - Read + - Write + - Edit + - Grep + - Glob + - Bash + - TaskList + - TaskOutput + - TaskStop + - ReadMediaFile + - TodoList + - Skill + - WebSearch + - Agent + - FetchURL + - AskUserQuestion + - EnterPlanMode + - ExitPlanMode + - mcp__* + +subagents: + coder: + description: Good at general software engineering tasks. + explore: + description: Fast codebase exploration with prompt-enforced read-only behavior. + plan: + description: Read-only implementation planning and architecture design. diff --git a/packages/agent-core/src/profile/default/coder.yaml b/packages/agent-core/src/profile/default/coder.yaml new file mode 100644 index 000000000..22780c65c --- /dev/null +++ b/packages/agent-core/src/profile/default/coder.yaml @@ -0,0 +1,18 @@ +extends: agent +name: coder +promptVars: + roleAdditional: | + You are now running as a subagent. All the `user` messages are sent by the main agent. The main agent cannot see your context, it can only see your last message when you finish the task. You must treat the parent agent as your caller. Do not directly ask the end user questions. If something is unclear, explain the ambiguity in your final summary to the parent agent. +whenToUse: | + Use this agent for non-trivial software engineering work that may require reading files, editing code, running commands, and returning a compact but technically complete summary to the parent agent. +tools: + - Bash + - Read + - ReadMediaFile + - Glob + - Grep + - Write + - Edit + - WebSearch + - FetchURL + - mcp__* diff --git a/packages/agent-core/src/profile/default/explore.yaml b/packages/agent-core/src/profile/default/explore.yaml new file mode 100644 index 000000000..84d024e58 --- /dev/null +++ b/packages/agent-core/src/profile/default/explore.yaml @@ -0,0 +1,36 @@ +extends: agent +name: explore +promptVars: + roleAdditional: | + You are now running as a subagent. All the `user` messages are sent by the main agent. The main agent cannot see your context, it can only see your last message when you finish the task. You must treat the parent agent as your caller. Do not directly ask the end user questions. If something is unclear, explain the ambiguity in your final summary to the parent agent. + + You are a codebase exploration specialist. Your role is EXCLUSIVELY to search, read, and analyze existing code and resources. You do NOT have access to file editing tools. + + Your strengths: + - Rapidly finding files using glob patterns + - Searching code and text with powerful regex patterns + - Reading and analyzing file contents + - Running read-only shell commands (git log, git diff, ls, find, etc.) + + Guidelines: + - Use Glob for broad file pattern matching. Patterns MUST contain a literal anchor (extension or subdirectory); pure wildcards like `*` or `**/*` are rejected by the tool. + - Use Grep for searching file contents with regex + - Use Read when you know the specific file path + - Use Bash ONLY for read-only operations (ls, git status, git log, git diff, find) + - NEVER use Bash for any file creation or modification commands + - Adapt your search depth based on the thoroughness level specified by the caller + - Wherever possible, spawn multiple parallel tool calls for grepping and reading files to maximize speed + + If the prompt includes a block, use it to orient yourself about the repository state before starting your investigation. + + You are meant to be a fast agent. Complete the search request efficiently and report your findings clearly in a structured format. +whenToUse: | + Fast agent specialized for exploring codebases. Use this when you need to quickly find files by patterns (e.g. "src/**/*.yaml"), search code for keywords (e.g. "database connection"), or answer questions about the codebase (e.g. "how does the auth module work?"). When calling this agent, specify the desired thoroughness level: "quick" for basic searches, "medium" for moderate exploration, or "thorough" for comprehensive analysis across multiple locations and naming conventions. Use this agent for any read-only exploration that will clearly require more than 3 search queries. Prefer launching multiple explore agents concurrently when investigating independent questions. +tools: + - Bash + - Read + - ReadMediaFile + - Glob + - Grep + - WebSearch + - FetchURL diff --git a/packages/agent-core/src/profile/default/init.md b/packages/agent-core/src/profile/default/init.md new file mode 100644 index 000000000..25d6c561a --- /dev/null +++ b/packages/agent-core/src/profile/default/init.md @@ -0,0 +1,21 @@ +You are a software engineering expert with many years of programming experience. Please explore the current project directory to understand the project's architecture and main details. + +Task requirements: +1. Analyze the project structure and identify key configuration files (such as pyproject.toml, package.json, Cargo.toml, etc.). +2. Understand the project's technology stack, build process and runtime architecture. +3. Identify how the code is organized and main module divisions. +4. Discover project-specific development conventions, testing strategies, and deployment processes. + +After the exploration, you should do a thorough summary of your findings and overwrite it into `AGENTS.md` file in the project root. You need to refer to what is already in the file when you do so. + +For your information, `AGENTS.md` is a file intended to be read by AI coding agents. Expect the reader of this file know nothing about the project. + +You should compose this file according to the actual project content. Do not make any assumptions or generalizations. Ensure the information is accurate and useful. You must use the natural language that is mainly used in the project's comments and documentation. + +Popular sections that people usually write in `AGENTS.md` are: + +- Project overview +- Build and test commands +- Code style guidelines +- Testing instructions +- Security considerations diff --git a/packages/agent-core/src/profile/default/plan.yaml b/packages/agent-core/src/profile/default/plan.yaml new file mode 100644 index 000000000..beb3e1c2d --- /dev/null +++ b/packages/agent-core/src/profile/default/plan.yaml @@ -0,0 +1,19 @@ +extends: agent +name: plan +promptVars: + roleAdditional: | + You are now running as a subagent. All the `user` messages are sent by the main agent. The main agent cannot see your context, it can only see your last message when you finish the task. You must treat the parent agent as your caller. Do not directly ask the end user questions. If something is unclear, explain the ambiguity in your final summary to the parent agent. + + Before designing your implementation plan, consider whether you fully understand the codebase areas relevant to the task. If not, recommend the parent agent to use the explore agent (subagent_type="explore") to investigate key questions first. In your response, clearly state: + 1. What you already know from the information provided + 2. What questions remain unanswered that would benefit from explore agent investigation + 3. Your implementation plan (either preliminary if questions remain, or final if sufficient context exists) +whenToUse: | + Use this agent when the parent agent needs a step-by-step implementation plan, key file identification, and architectural trade-off analysis before code changes are made. +tools: + - Read + - ReadMediaFile + - Glob + - Grep + - WebSearch + - FetchURL diff --git a/packages/agent-core/src/profile/default/system.md b/packages/agent-core/src/profile/default/system.md new file mode 100644 index 000000000..0436f944b --- /dev/null +++ b/packages/agent-core/src/profile/default/system.md @@ -0,0 +1,165 @@ +You are Kimi Code CLI, an interactive general AI agent running on a user's computer. + +Your primary goal is to help users with software engineering tasks by taking action — use the tools available to you to make real changes on the user's system. You should also answer questions when asked. Always adhere strictly to the following system instructions and the user's requirements. + +{{ ROLE_ADDITIONAL }} + +# Prompt and Tool Use + +The user's messages may contain questions and/or task descriptions in natural language, code snippets, logs, file paths, or other forms of information. Read them, understand them and do what the user requested. For simple questions/greetings that do not involve any information in the working directory or on the internet, you may simply reply directly. For anything else, default to taking action with tools. When the request could be interpreted as either a question to answer or a task to complete, treat it as a task. + +When handling the user's request, if it involves creating, modifying, or running code or files, you MUST use the appropriate tools (e.g., `Write`, `Bash`) to make actual changes — do not just describe the solution in text. For questions that only need an explanation, you may reply in text directly. When calling tools, do not provide explanations because the tool calls themselves should be self-explanatory. You MUST follow the description of each tool and its parameters when calling tools. + +If the `Agent` tool is available, you can use it to delegate a focused subtask to a subagent instance. The tool can either start a new instance or resume an existing one by its agent id. Subagent instances are persistent session objects with their own context history. When delegating, provide a complete prompt with all necessary context — a new subagent instance does not see your current context. If an existing subagent already has useful context or the task clearly continues its prior work, prefer resuming it over creating a new instance. Default to foreground subagents; use `run_in_background=true` only when there is a clear benefit to letting the conversation continue before the subagent finishes and you do not need the result immediately. + +You have the capability to output any number of tool calls in a single response. If you anticipate making multiple non-interfering tool calls, you are HIGHLY RECOMMENDED to make them in parallel to significantly improve efficiency. This is very important to your performance. + +The results of the tool calls will be returned to you in a tool message. You must determine your next action based on the tool call results, which could be one of the following: 1. Continue working on the task, 2. Inform the user that the task is completed or has failed, or 3. Ask the user for more information. + +The system may insert information wrapped in `` tags within user or tool messages. This information provides supplementary context relevant to the current task — take it into consideration when determining your next action. + +Tool results and user messages may also include `` tags. Unlike `` tags, these are **authoritative system directives** that you MUST follow. They bear no direct relation to the specific tool results or user messages in which they appear. Always read them carefully and comply with their instructions — they may override or constrain your normal behavior (e.g., restricting you to read-only actions during plan mode). + +If the `Bash`, `TaskList`, `TaskOutput`, and `TaskStop` tools are available and you are the root agent, you can use background `Bash` for long-running shell commands. Launch it via `Bash` with `run_in_background=true` and a short `description`. The system will notify you when the background task reaches a terminal state. Use `TaskList` to re-enumerate active tasks when needed, especially after context compaction. Use `TaskOutput` for non-blocking status/output snapshots; only set `block=true` when you intentionally want to wait for completion. After starting a background task, default to returning control to the user instead of immediately waiting on it. Use `TaskStop` only when you need to cancel the task. For human users in the interactive shell, the only task-management slash command is `/tasks`. Do not tell users to run `/task`, `/tasks list`, `/tasks output`, `/tasks stop`, or any other invented slash subcommands. If you are a subagent or these tools are not available, do not assume you can create or control background tasks. + +If a foreground tool call or a background agent requests approval, the approval is coordinated through the unified approval runtime and surfaced through the root UI channel. Do not assume approvals are local to a single subagent turn. + +When responding to the user, you MUST use the SAME language as the user, unless explicitly instructed to do otherwise. + +# General Guidelines for Coding + +When building something from scratch, you should: + +- Understand the user's requirements. +- Ask the user for clarification if there is anything unclear. +- Design the architecture and make a plan for the implementation. +- Write the code in a modular and maintainable way. + +Always use tools to implement your code changes: + +- Use `Write` to create or overwrite source files. Code that only appears in your text response is NOT saved to the file system and will not take effect. +- Use `Bash` to run and test your code after writing it. +- Iterate: if tests fail, read the error, fix the code with `Write` or `Edit`, and re-test with `Bash`. + +When working on an existing codebase, you should: + +- Understand the codebase by reading it with tools (`Read`, `Glob`, `Grep`) before making changes. Identify the ultimate goal and the most important criteria to achieve the goal. +- When using `Glob`, include a literal anchor (file extension or subdirectory) in the pattern. Pure wildcards like `*` or `**/*` are rejected by the tool. +- For a bug fix, you typically need to check error logs or failed tests, scan over the codebase to find the root cause, and figure out a fix. If user mentioned any failed tests, you should make sure they pass after the changes. +- For a feature, you typically need to design the architecture, and write the code in a modular and maintainable way, with minimal intrusions to existing code. Add new tests if the project already has tests. +- For a code refactoring, you typically need to update all the places that call the code you are refactoring if the interface changes. DO NOT change any existing logic especially in tests, focus only on fixing any errors caused by the interface changes. +- Make MINIMAL changes to achieve the goal. This is very important to your performance. +- Follow the coding style of existing code in the project. +- For broader codebase exploration and deep research, use `Agent` with `subagent_type="explore"` — a fast, read-only agent specialized for searching and understanding codebases. Reach for it when your task will clearly require more than 3 search queries, or when you need to investigate multiple files and patterns. Launch multiple explore agents concurrently when investigating independent questions. + +DO NOT run `git commit`, `git push`, `git reset`, `git rebase` and/or do any other git mutations unless explicitly asked to do so. Ask for confirmation each time when you need to do git mutations, even if the user has confirmed in earlier conversations. + +# General Guidelines for Research and Data Processing + +The user may ask you to research on certain topics, process or generate certain multimedia files. When doing such tasks, you must: + +- Understand the user's requirements thoroughly, ask for clarification before you start if needed. +- Make plans before doing deep or wide research, to ensure you are always on track. +- Search on the Internet if possible, with carefully-designed search queries to improve efficiency and accuracy. +- Use proper tools or shell commands or Python packages to process or generate images, videos, PDFs, docs, spreadsheets, presentations, or other multimedia files. Detect if there are already such tools in the environment. If you have to install third-party tools/packages, you MUST ensure that they are installed in a virtual/isolated environment. +- Once you generate or edit any images, videos or other media files, try to read it again before proceed, to ensure that the content is as expected. +- Avoid installing or deleting anything to/from outside of the current working directory. If you have to do so, ask the user for confirmation. + +# Working Environment + +## Operating System + +You are running on **{{ KIMI_OS }}**. The Bash tool executes commands using **{{ KIMI_SHELL }}**. +{% if KIMI_OS == "Windows" %} + +IMPORTANT: You are on Windows. The Bash tool runs through Git Bash, so use Unix shell syntax inside Bash commands — `/dev/null` not `NUL`, and forward slashes in paths. For file operations, always prefer the built-in tools (Read, Write, Edit, Glob, Grep) over Bash commands — they work reliably across all platforms. +{% endif %} + +The operating environment is not in a sandbox. Any actions you do will immediately affect the user's system. So you MUST be extremely cautious. Unless being explicitly instructed to do so, you should never access (read/write/execute) files outside of the working directory. + +## Date and Time + +The current date and time in ISO format is `{{ KIMI_NOW }}`. This is only a reference for you when searching the web, or checking file modification time, etc. If you need the exact time, use Bash tool with proper command. + +## Working Directory + +The current working directory is `{{ KIMI_WORK_DIR }}`. This should be considered as the project root if you are instructed to perform tasks on the project. Every file system operation will be relative to the working directory if you do not explicitly specify the absolute path. Tools may require absolute paths for some parameters, IF SO, YOU MUST use absolute paths for these parameters. + +The directory listing of current working directory is: + +``` +{{ KIMI_WORK_DIR_LS }} +``` + +Use this as your basic understanding of the project structure. The tree only shows the first two levels; entries marked "... and N more" indicate additional contents — use Glob or Bash to explore further. +{% if KIMI_ADDITIONAL_DIRS_INFO %} + +## Additional Directories + +The following directories have been added to the workspace. You can read, write, search, and glob files in these directories as part of your workspace scope. + +{{ KIMI_ADDITIONAL_DIRS_INFO }} +{% endif %} + +# Project Information + +Markdown files named `AGENTS.md` usually contain the background, structure, coding styles, user preferences and other relevant information about the project. You should use this information to understand the project and the user's preferences. `AGENTS.md` files may exist at different locations in the project, but typically there is one in the project root. + +> Why `AGENTS.md`? +> +> `README.md` files are for humans: quick starts, project descriptions, and contribution guidelines. `AGENTS.md` complements this by containing the extra, sometimes detailed context coding agents need: build steps, tests, and conventions that might clutter a README or aren’t relevant to human contributors. +> +> We intentionally kept it separate to: +> +> - Give agents a clear, predictable place for instructions. +> - Keep `README`s concise and focused on human contributors. +> - Provide precise, agent-focused guidance that complements existing `README` and docs. + +The `AGENTS.md` instructions (merged from all applicable directories): + +````````` +{{ KIMI_AGENTS_MD }} +````````` + +`AGENTS.md` files can appear at any level of the project directory tree, including inside `.kimi-code/` directories. Each file governs the directory it resides in and all subdirectories beneath it. When multiple `AGENTS.md` files apply to a file you are modifying, instructions in deeper directories take precedence over those in parent directories. User instructions given directly in the conversation always take the highest precedence. + +When working on files in subdirectories, always check whether those directories contain their own `AGENTS.md` with more specific guidance that supplements or overrides the instructions above. You may also check `README`/`README.md` files for more information about the project. + +If you modified any files/styles/structures/configurations/workflows/... mentioned in `AGENTS.md` files, you MUST update the corresponding `AGENTS.md` files to keep them up-to-date. + +# Skills + +Skills are reusable, composable capabilities that enhance your abilities. Each skill is either a self-contained directory with a `SKILL.md` file or a standalone `.md` file that contains instructions, examples, and/or reference material. + +## What are skills? + +Skills are modular extensions that provide: + +- Specialized knowledge: Domain-specific expertise (e.g., PDF processing, data analysis) +- Workflow patterns: Best practices for common tasks +- Tool integrations: Pre-configured tool chains for specific operations +- Reference material: Documentation, templates, and examples + +## Available skills + +Skills are grouped by scope (`Project`, `User`, `Extra`, `Built-in`) so you can tell where each came from. When the user refers to "the skill in this project" or "the user-scope skill", use the scope heading to disambiguate. When multiple scopes define a skill with the same name, the more specific scope takes precedence: **Project overrides User overrides Extra overrides Built-in**. + +{{ KIMI_SKILLS }} + +## How to use skills + +Identify the skills that are likely to be useful for the tasks you are currently working on, read the skill file for detailed instructions, guidelines, scripts and more. + +Only read skill details when needed to conserve the context window. + +# Ultimate Reminders + +At any time, you should be HELPFUL, CONCISE, and ACCURATE. Be thorough in your actions — test what you build, verify what you change — not in your explanations. + +- Never diverge from the requirements and the goals of the task you work on. Stay on track. +- Never give the user more than what they want. +- Try your best to avoid any hallucination. Do fact checking before providing any factual information. +- Think about the best approach, then take action decisively. +- Do not give up too early. +- ALWAYS, keep it stupidly simple. Do not overcomplicate things. +- When the task requires creating or modifying files, always use tools to do so. Never treat displaying code in your response as a substitute for actually writing it to the file system. diff --git a/packages/agent-core/src/profile/index.ts b/packages/agent-core/src/profile/index.ts new file mode 100644 index 000000000..80e303814 --- /dev/null +++ b/packages/agent-core/src/profile/index.ts @@ -0,0 +1,5 @@ +export * from './types'; +export * from './context'; +export * from './load'; +export * from './resolve'; +export * from './default'; diff --git a/packages/agent-core/src/profile/load.ts b/packages/agent-core/src/profile/load.ts new file mode 100644 index 000000000..613edcf63 --- /dev/null +++ b/packages/agent-core/src/profile/load.ts @@ -0,0 +1,125 @@ +import { readFile } from 'node:fs/promises'; +import { dirname, join, posix as posixPath } from 'node:path'; + +import { load as loadYaml } from 'js-yaml'; + +import { resolveAgentProfiles } from './resolve'; +import { RawAgentProfileSchema, type RawAgentProfile, type ResolvedAgentProfile } from './types'; + +export async function loadAgentProfilesFromDir( + paths: readonly string[], +): Promise> { + const rawProfiles = await loadRawAgentProfiles(paths); + return resolveAgentProfiles(rawProfiles); +} + +export function loadAgentProfilesFromSources( + paths: readonly string[], + sources: Readonly>, +): Record { + const rawProfiles = paths.map((profilePath) => + finalizeRawAgentProfileSource(readRequiredSource(sources, profilePath), profilePath, sources), + ); + return resolveAgentProfiles(rawProfiles); +} + +async function loadRawAgentProfiles(paths: readonly string[]): Promise { + const profiles: RawAgentProfile[] = []; + + for (const profilePath of paths) { + let content: string; + try { + content = await readFile(profilePath, 'utf-8'); + } catch (error) { + if (isFileNotFound(error)) continue; + throw readError('agent profile', profilePath, error); + } + profiles.push(await finalizeRawAgentProfile(content, profilePath)); + } + + return profiles; +} + +async function finalizeRawAgentProfile( + content: string, + profilePath: string, +): Promise { + const raw = parseAgentProfileYaml(content, profilePath); + if (raw.systemPromptPath === undefined) return raw; + const templatePath = join(dirname(profilePath), raw.systemPromptPath); + try { + return { ...raw, systemPromptTemplate: await readFile(templatePath, 'utf-8') }; + } catch (error) { + throw new Error( + `Failed to read system prompt template for "${raw.name}" at ${templatePath}: ${ + error instanceof Error ? error.message : String(error) + }`, + { cause: error }, + ); + } +} + +function finalizeRawAgentProfileSource( + content: string, + profilePath: string, + sources: Readonly>, +): RawAgentProfile { + const raw = parseAgentProfileYaml(content, profilePath); + if (raw.systemPromptPath === undefined) return raw; + const templatePath = resolveProfileSourcePath(profilePath, raw.systemPromptPath); + return { ...raw, systemPromptTemplate: readRequiredSource(sources, templatePath) }; +} + +function parseAgentProfileYaml(content: string, profilePath: string): RawAgentProfile { + let parsed: unknown; + try { + parsed = loadYaml(content); + } catch (error) { + throw new Error( + `Invalid agent profile YAML at ${profilePath}: ${ + error instanceof Error ? error.message : String(error) + }`, + { cause: error }, + ); + } + const result = RawAgentProfileSchema.safeParse(parsed); + if (!result.success) { + throw new Error(`Invalid agent profile at ${profilePath}`); + } + return result.data; +} + +function resolveProfileSourcePath(profilePath: string, relativePath: string): string { + return normalizeSourcePath( + posixPath.join(posixPath.dirname(normalizeSourcePath(profilePath)), relativePath), + ); +} + +function readRequiredSource(sources: Readonly>, path: string): string { + const normalized = normalizeSourcePath(path); + const content = sources[normalized]; + if (content === undefined) { + throw new Error(`Embedded agent profile source missing: ${normalized}`); + } + return content; +} + +function normalizeSourcePath(path: string): string { + return posixPath.normalize(path.replaceAll('\\', '/')).replace(/^\.\//, ''); +} + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + +function isFileNotFound(error: unknown): boolean { + return isRecord(error) && error['code'] === 'ENOENT'; +} + +function readError(label: string, filePath: string, error: unknown): Error { + return new Error( + `Failed to read ${label} at ${filePath}: ${ + error instanceof Error ? error.message : String(error) + }`, + ); +} diff --git a/packages/agent-core/src/profile/resolve.ts b/packages/agent-core/src/profile/resolve.ts new file mode 100644 index 000000000..001f7d19f --- /dev/null +++ b/packages/agent-core/src/profile/resolve.ts @@ -0,0 +1,222 @@ +import { renderPrompt } from '../utils/render-prompt'; +import type { + RawAgentProfile, + RawSubagentProfile, + ResolvedAgentProfile, + SystemPromptContext, + SystemPromptRenderer, +} from './types'; + +interface MergedAgentProfile { + readonly name: string; + readonly description?: string | undefined; + readonly systemPromptTemplate: string; + readonly promptVars: Record; + readonly tools: string[]; + readonly whenToUse?: string | undefined; + readonly subagents?: Record | undefined; +} + +/** + * Resolve agent profiles with extends inheritance. + * + * Each resolved profile exposes its `systemPrompt` as a renderer that + * closes over the merged template and prompt vars. The renderer is + * invoked later with a {@link SystemPromptContext} to produce the + * concrete prompt — this lets context that only exists at runtime + * (cwd listing, AGENTS.md, skills) flow through without re-loading + * profiles. + */ +export function resolveAgentProfiles( + raw: readonly RawAgentProfile[], +): Record { + const profileMap = new Map(); + const mergedCache = new Map(); + const resolvedCache = new Map(); + + for (const profile of raw) { + if (profileMap.has(profile.name)) { + throw new Error(`Duplicate agent profile name: "${profile.name}"`); + } + profileMap.set(profile.name, profile); + } + + for (const profile of raw) { + const merged = resolveMergedProfile(profile.name, profileMap, mergedCache, []); + resolvedCache.set(profile.name, toResolvedProfile(merged)); + } + + applySubagentDescriptions(mergedCache, resolvedCache); + linkResolvedSubagents(mergedCache, resolvedCache); + + const result: Record = {}; + for (const [name, profile] of resolvedCache) { + result[name] = profile; + } + + return result; +} + +function resolveMergedProfile( + name: string, + profileMap: Map, + cache: Map, + stack: string[], +): MergedAgentProfile { + const cached = cache.get(name); + if (cached !== undefined) { + return cached; + } + + const cycleIndex = stack.indexOf(name); + if (cycleIndex !== -1) { + const cycle = [...stack.slice(cycleIndex), name].join(' -> '); + throw new Error(`Agent profile extends cycle detected: ${cycle}`); + } + + const profile = profileMap.get(name); + if (profile === undefined) { + throw new Error(`Agent profile "${name}" not found`); + } + + let parent: MergedAgentProfile | undefined; + if (profile.extends !== undefined) { + if (!profileMap.has(profile.extends)) { + throw new Error( + `Agent profile "${profile.name}" extends "${profile.extends}" but parent profile was not found`, + ); + } + parent = resolveMergedProfile(profile.extends, profileMap, cache, [...stack, name]); + } + + const merged: MergedAgentProfile = { + name: profile.name, + description: profile.description, + systemPromptTemplate: profile.systemPromptTemplate ?? parent?.systemPromptTemplate ?? '', + promptVars: { + ...parent?.promptVars, + ...profile.promptVars, + }, + tools: profile.tools !== undefined ? [...profile.tools] : [...(parent?.tools ?? [])], + whenToUse: profile.whenToUse ?? parent?.whenToUse, + subagents: cloneSubagents(profile.subagents), + }; + + cache.set(profile.name, merged); + return merged; +} + +function toResolvedProfile(merged: MergedAgentProfile): ResolvedAgentProfile { + return { + name: merged.name, + description: merged.description, + systemPrompt: createSystemPromptRenderer(merged), + tools: [...merged.tools], + whenToUse: merged.whenToUse, + }; +} + +/** + * Build a renderer that captures the merged template and prompt vars. + * The runtime SystemPromptContext is mapped to the template variables + * (KIMI_OS, KIMI_AGENTS_MD, ...) at render time. + */ +function createSystemPromptRenderer(merged: MergedAgentProfile): SystemPromptRenderer { + return (context: SystemPromptContext): string => { + const vars = buildTemplateVars(context, merged.promptVars); + try { + return renderPrompt(merged.systemPromptTemplate, vars); + } catch (error) { + throw new Error( + `Failed to render system prompt for agent profile "${merged.name}": ${ + error instanceof Error ? error.message : String(error) + }`, + { cause: error }, + ); + } + }; +} + +function buildTemplateVars( + context: SystemPromptContext, + promptVars: Record, +): Record { + const skills = + typeof context.skills === 'string' + ? context.skills + : (context.skills?.getModelSkillListing() ?? ''); + const now = + context.now instanceof Date + ? context.now.toISOString() + : (context.now ?? new Date().toISOString()); + + return { + ...promptVars, + KIMI_OS: context.osEnv.osKind, + KIMI_SHELL: `${context.osEnv.shellName} (\`${context.osEnv.shellPath}\`)`, + KIMI_NOW: now, + KIMI_WORK_DIR: context.cwd, + KIMI_WORK_DIR_LS: context.cwdListing ?? '', + KIMI_AGENTS_MD: context.agentsMd ?? '', + KIMI_SKILLS: skills, + KIMI_ADDITIONAL_DIRS_INFO: context.additionalDirsInfo ?? '', + ROLE_ADDITIONAL: + context.roleAdditional ?? promptVars['ROLE_ADDITIONAL'] ?? promptVars['roleAdditional'] ?? '', + }; +} + +function applySubagentDescriptions( + mergedProfiles: Map, + resolvedProfiles: Map, +): void { + for (const [ownerName, owner] of mergedProfiles) { + if (owner.subagents === undefined) continue; + for (const [subagentName, subagent] of Object.entries(owner.subagents)) { + const target = resolvedProfiles.get(subagentName); + if (target === undefined) { + throwMissingSubagent(ownerName, subagentName); + } + if (target.description === undefined && subagent.description !== undefined) { + target.description = subagent.description; + } + } + } +} + +function linkResolvedSubagents( + mergedProfiles: Map, + resolvedProfiles: Map, +): void { + for (const [ownerName, owner] of mergedProfiles) { + if (owner.subagents === undefined) continue; + + const subagents: Record = {}; + for (const subagentName of Object.keys(owner.subagents)) { + const target = resolvedProfiles.get(subagentName); + if (target === undefined) { + throwMissingSubagent(ownerName, subagentName); + } + subagents[subagentName] = target; + } + + const resolved = resolvedProfiles.get(ownerName); + if (resolved !== undefined) { + resolved.subagents = subagents; + } + } +} + +function throwMissingSubagent(ownerName: string, subagentName: string): never { + throw new Error( + `Agent profile "${ownerName}" declares subagent "${subagentName}" but that agent profile was not found`, + ); +} + +function cloneSubagents( + subagents: Record | undefined, +): Record | undefined { + if (subagents === undefined) return undefined; + return Object.fromEntries( + Object.entries(subagents).map(([name, subagent]) => [name, { ...subagent }]), + ); +} diff --git a/packages/agent-core/src/profile/types.ts b/packages/agent-core/src/profile/types.ts new file mode 100644 index 000000000..5e9ef5c68 --- /dev/null +++ b/packages/agent-core/src/profile/types.ts @@ -0,0 +1,56 @@ +import { z } from 'zod'; + +import type { SkillRegistry } from '../skill'; +import type { Environment } from '../utils/environment'; + +export const RawSubagentProfileSchema = z.object({ + description: z.string().optional(), +}); + +export type RawSubagentProfile = z.infer; + +export const RawAgentProfileSchema = z.object({ + extends: z.string().optional(), + name: z.string().min(1), + description: z.string().optional(), + systemPromptPath: z.string().optional(), + systemPromptTemplate: z.string().optional(), + promptVars: z.record(z.string(), z.string()).optional(), + // Exact builtin/user tool names, plus optional MCP glob patterns + // (`mcp__*`, `mcp__github__*`) that gate which MCP tools the profile sees. + tools: z.array(z.string()).optional(), + whenToUse: z.string().optional(), + subagents: z.record(z.string(), RawSubagentProfileSchema).optional(), +}); + +export type RawAgentProfile = z.infer; + +/** + * Runtime context supplied to a system prompt renderer. + * + * Captures everything determined at render time rather than at profile-load + * time: the OS/shell, working directory, AGENTS.md instructions, available + * skills, and so on. Loaders return renderers; callers invoke them with + * the live context whenever a concrete prompt is needed. + */ +export interface SystemPromptContext { + readonly osEnv: Environment; + readonly cwd: string; + readonly now?: string | Date; + readonly cwdListing?: string; + readonly agentsMd?: string; + readonly skills?: SkillRegistry | string; + readonly additionalDirsInfo?: string; + readonly roleAdditional?: string; +} + +export type SystemPromptRenderer = (context: SystemPromptContext) => string; + +export interface ResolvedAgentProfile { + name: string; + description?: string; + systemPrompt: SystemPromptRenderer; + tools: string[]; + whenToUse?: string; + subagents?: Record; +} diff --git a/packages/agent-core/src/prompt-modules.d.ts b/packages/agent-core/src/prompt-modules.d.ts new file mode 100644 index 000000000..345a703a0 --- /dev/null +++ b/packages/agent-core/src/prompt-modules.d.ts @@ -0,0 +1,12 @@ +// Raw-string imports for prompt sources. The `raw-text-plugin` (used by both +// tsdown and vitest) loads `.md` / `.yaml` files as their string content. + +declare module '*.md' { + const content: string; + export default content; +} + +declare module '*.yaml' { + const content: string; + export default content; +} diff --git a/packages/agent-core/src/providers/index.ts b/packages/agent-core/src/providers/index.ts new file mode 100644 index 000000000..85cca4b1c --- /dev/null +++ b/packages/agent-core/src/providers/index.ts @@ -0,0 +1,2 @@ +export * from './provider-manager'; +export * from './runtime-provider'; diff --git a/packages/agent-core/src/providers/provider-manager.ts b/packages/agent-core/src/providers/provider-manager.ts new file mode 100644 index 000000000..7c2bc14f1 --- /dev/null +++ b/packages/agent-core/src/providers/provider-manager.ts @@ -0,0 +1,139 @@ +import type { KimiConfig } from '../config'; +import { ErrorCodes, KimiError } from '#/errors'; +import type { Logger } from '#/logging/types'; +import { resolveThinkingEffort, type ThinkingEffort } from '../agent/config/thinking'; +import { + createRuntimeProviderAuthResolver, + resolveRuntimeProvider, + resolveRuntimeProviderWithOAuth, + type OAuthTokenProviderResolver, + type ProviderRequestAuthResolver, + type ResolvedRuntimeProvider, +} from './runtime-provider'; + +export interface ProviderManagerOptions { + readonly config: KimiConfig; + readonly kimiRequestHeaders?: Record | undefined; + readonly resolveOAuthTokenProvider?: OAuthTokenProviderResolver | undefined; + readonly promptCacheKey?: string; +} + +export class ProviderManager { + private readonly state: ProviderManagerState; + + constructor( + private readonly options: ProviderManagerOptions, + state?: ProviderManagerState, + ) { + this.state = state ?? { config: options.config }; + } + + get config(): KimiConfig { + return this.state.config; + } + + get providers(): KimiConfig['providers'] { + return this.state.config.providers; + } + + get models(): NonNullable { + return this.state.config.models ?? {}; + } + + updateConfig(config: KimiConfig): void { + this.state.config = config; + } + + withPromptCacheKey(promptCacheKey?: string): ProviderManager { + return new ProviderManager( + { + ...this.options, + config: this.state.config, + promptCacheKey, + }, + this.state, + ); + } + + resolveProviderConfigForModel( + model: string | undefined, + options?: { readonly promptCacheKey?: string }, + ): ResolvedRuntimeProvider | undefined { + const selectedModel = this.resolveSelectedModel(model); + if (selectedModel === undefined) return undefined; + + return resolveRuntimeProvider({ + config: this.state.config, + model: selectedModel, + kimiRequestHeaders: this.options.kimiRequestHeaders, + promptCacheKey: options?.promptCacheKey ?? this.options.promptCacheKey, + validateCredentials: false, + }); + } + + async resolveProviderForModel( + model: string | undefined, + ): Promise { + const selectedModel = this.resolveSelectedModel(model); + if (selectedModel === undefined) return undefined; + + return resolveRuntimeProviderWithOAuth({ + config: this.state.config, + model: selectedModel, + kimiRequestHeaders: this.options.kimiRequestHeaders, + promptCacheKey: this.options.promptCacheKey, + resolveOAuthTokenProvider: this.options.resolveOAuthTokenProvider, + }); + } + + createAuthResolverForModel( + model: string | undefined, + options?: { readonly log?: Logger }, + ): ProviderRequestAuthResolver | undefined { + const selectedModel = this.resolveSelectedModel(model); + if (selectedModel === undefined) return undefined; + + const resolved = resolveRuntimeProvider({ + config: this.state.config, + model: selectedModel, + kimiRequestHeaders: this.options.kimiRequestHeaders, + promptCacheKey: this.options.promptCacheKey, + }); + return createRuntimeProviderAuthResolver( + { + config: this.state.config, + model: selectedModel, + kimiRequestHeaders: this.options.kimiRequestHeaders, + promptCacheKey: this.options.promptCacheKey, + resolveOAuthTokenProvider: this.options.resolveOAuthTokenProvider, + log: options?.log, + }, + resolved, + ); + } + + resolveThinkingLevel(requestedThinking?: string): ThinkingEffort { + return resolveThinkingEffort(requestedThinking, this.state.config.thinking); + } + + resolveSelectedModel(requestedModel: string | undefined): string | undefined { + if (requestedModel !== undefined) { + const normalized = normalizeString(requestedModel); + if (normalized === undefined) { + throw new KimiError(ErrorCodes.MODEL_CONFIG_INVALID, 'Runtime provider model cannot be empty'); + } + return normalized; + } + return normalizeString(this.state.config.defaultModel); + } +} + +interface ProviderManagerState { + config: KimiConfig; +} + +function normalizeString(value: string | undefined): string | undefined { + if (value === undefined) return undefined; + const trimmed = value.trim(); + return trimmed.length > 0 ? trimmed : undefined; +} diff --git a/packages/agent-core/src/providers/request-auth.ts b/packages/agent-core/src/providers/request-auth.ts new file mode 100644 index 000000000..31a032177 --- /dev/null +++ b/packages/agent-core/src/providers/request-auth.ts @@ -0,0 +1,46 @@ +import { APIStatusError, type ProviderRequestAuth } from '@moonshot-ai/kosong'; + +import { ErrorCodes, KimiError } from '#/errors'; + +export interface ProviderRequestAuthOptions { + readonly forceRefresh?: boolean; +} + +export type ProviderRequestAuthResolver = ( + options?: ProviderRequestAuthOptions, +) => Promise; + +export async function withProviderRequestAuth( + resolveAuth: ProviderRequestAuthResolver | undefined, + request: (auth: ProviderRequestAuth | undefined) => Promise, +): Promise { + let auth = await resolveAuth?.(); + for (let refreshed = false; ; refreshed = true) { + try { + return await request(auth); + } catch (error) { + if ( + auth === undefined || + !(error instanceof APIStatusError) || + error.statusCode !== 401 + ) { + throw error; + } + if (!refreshed && resolveAuth !== undefined) { + auth = await resolveAuth({ forceRefresh: true }); + continue; + } + throw new KimiError( + ErrorCodes.AUTH_LOGIN_REQUIRED, + 'OAuth provider credentials were rejected. Send /login to login.', + { + cause: error, + details: { + statusCode: error.statusCode, + requestId: error.requestId, + }, + }, + ); + } + } +} diff --git a/packages/agent-core/src/providers/runtime-provider.ts b/packages/agent-core/src/providers/runtime-provider.ts new file mode 100644 index 000000000..01abe39fe --- /dev/null +++ b/packages/agent-core/src/providers/runtime-provider.ts @@ -0,0 +1,409 @@ +import type { KimiConfig, ModelAlias, OAuthRef, ProviderConfig } from '#/config'; +import { ErrorCodes, KimiError, isKimiError } from '#/errors'; +import { log as defaultLog } from '#/logging/logger'; +import type { Logger } from '#/logging/types'; +import { + createProvider, + UNKNOWN_CAPABILITY, + type ModelCapability, + type ProviderConfig as KosongProviderConfig, +} from '@moonshot-ai/kosong'; + +import type { ProviderRequestAuthResolver } from './request-auth'; + +export type { ProviderRequestAuthResolver }; + +export interface ResolveRuntimeProviderInput { + readonly config: KimiConfig; + readonly model?: string | undefined; + readonly kimiRequestHeaders?: Record | undefined; + readonly promptCacheKey?: string; + readonly validateCredentials?: boolean; +} + +export interface BearerTokenProvider { + getAccessToken(options?: { readonly force?: boolean | undefined }): Promise; +} + +export type OAuthTokenProviderResolver = ( + providerName: string, + oauthRef?: OAuthRef | undefined, +) => BearerTokenProvider | undefined; + +export interface ResolveRuntimeProviderWithOAuthInput extends ResolveRuntimeProviderInput { + readonly resolveOAuthTokenProvider?: OAuthTokenProviderResolver | undefined; + /** + * Caller-scoped logger (typically `agent.log`). Used to report OAuth token + * fetch failures so they land in the session log alongside the surrounding + * turn / tool call. Defaults to the global `log` when absent. + */ + readonly log?: Logger; +} + +export interface ResolvedRuntimeProvider { + readonly modelName: string; + readonly providerName?: string | undefined; + readonly provider: KosongProviderConfig; + readonly modelCapabilities: ModelCapability; + readonly resolveAuth?: ProviderRequestAuthResolver; +} + +export function resolveRuntimeProvider( + input: ResolveRuntimeProviderInput, +): ResolvedRuntimeProvider { + const modelName = input.model ?? input.config.defaultModel; + if (modelName === undefined) { + throw new KimiError( + ErrorCodes.CONFIG_INVALID, + 'No model is selected. Set default_model in config.toml or pass a configured model alias.', + ); + } + + const alias = input.config.models?.[modelName]; + if (alias === undefined) { + throw new KimiError( + ErrorCodes.CONFIG_INVALID, + `Model "${modelName}" is not configured in config.toml. Add a [models."${modelName}"] entry with max_context_size.`, + ); + } + + const resolvedModel = alias.model; + const providerName = alias.provider ?? input.config.defaultProvider; + const providerConfig = + providerName === undefined ? undefined : input.config.providers[providerName]; + + if (providerName === undefined) { + throw new KimiError( + ErrorCodes.CONFIG_INVALID, + `Model "${modelName}" must define a provider in config.toml.`, + ); + } + + if (providerConfig === undefined) { + throw new KimiError( + ErrorCodes.CONFIG_INVALID, + `Provider "${providerName}" for model "${modelName}" is not configured.`, + ); + } + + if (!Number.isInteger(alias.maxContextSize) || alias.maxContextSize <= 0) { + throw new KimiError( + ErrorCodes.CONFIG_INVALID, + `Model "${modelName}" must define a positive max_context_size in config.toml.`, + ); + } + + // Fail-fast when an operation explicitly selects/uses a runtime provider. + // Session creation only records the selected model name and does not need + // credential validation. + if ( + input.validateCredentials !== false && + providerConfig.type !== 'vertexai' && + providerConfig.oauth === undefined && + providerApiKey(providerConfig) === undefined + ) { + throw new KimiError( + ErrorCodes.CONFIG_INVALID, + `Provider "${providerName}" has no credentials configured. Set apiKey, oauth, or a provider env API key in config.toml.`, + ); + } + + const provider = toKosongProviderConfig( + providerConfig, + resolvedModel, + input.kimiRequestHeaders, + alias.maxOutputSize, + input.promptCacheKey, + ); + const modelCapabilities = resolveModelCapabilities(alias, provider); + + return { + modelName, + providerName, + modelCapabilities, + provider, + }; +} + +export async function resolveRuntimeProviderWithOAuth( + input: ResolveRuntimeProviderWithOAuthInput, +): Promise { + const resolved = resolveRuntimeProvider(input); + const resolveAuth = createRuntimeProviderAuthResolver(input, resolved); + if (resolveAuth === undefined) return resolved; + + // Validate login eagerly, preserving existing setModel + // behavior, but do not store the short-lived token in provider config. + await resolveAuth(); + return { + ...resolved, + resolveAuth, + }; +} + +export function createRuntimeProviderAuthResolver( + input: ResolveRuntimeProviderWithOAuthInput, + resolved: ResolvedRuntimeProvider = resolveRuntimeProvider(input), +): ProviderRequestAuthResolver | undefined { + const providerName = resolved.providerName; + if (providerName === undefined) return undefined; + + const providerConfig = input.config.providers[providerName]; + if (providerConfig?.oauth === undefined) return undefined; + if (providerApiKey(providerConfig) !== undefined) { + // oauth + apiKey on the same provider makes request auth ambiguous: + // provider construction would prefer apiKey while runtime auth resolves + // OAuth. Reject it so misconfiguration surfaces at model resolution. + throw new KimiError( + ErrorCodes.CONFIG_INVALID, + `Provider "${providerName}" has both apiKey and oauth set in config.toml — they are mutually exclusive. Remove one.`, + ); + } + + const tokenProvider = input.resolveOAuthTokenProvider?.(providerName, providerConfig.oauth); + if (tokenProvider === undefined) { + return async () => { + throw new KimiError( + ErrorCodes.AUTH_LOGIN_REQUIRED, + `OAuth provider "${providerName}" requires login before it can be used.`, + ); + }; + } + + return async (options) => { + let apiKey: string; + try { + apiKey = await tokenProvider.getAccessToken( + options?.forceRefresh === true ? { force: true } : undefined, + ); + } catch (error) { + if (!isAuthLoginRequired(error)) { + (input.log ?? defaultLog).warn('oauth token fetch failed', { providerName, error }); + } + throw new KimiError( + ErrorCodes.AUTH_LOGIN_REQUIRED, + `OAuth provider "${providerName}" requires login before it can be used.`, + { + cause: error, + }, + ); + } + if (apiKey.trim().length === 0) { + throw new KimiError( + ErrorCodes.AUTH_LOGIN_REQUIRED, + `OAuth provider "${providerName}" requires login before it can be used.`, + ); + } + return { apiKey }; + }; +} + +function isAuthLoginRequired(error: unknown): boolean { + return isKimiError(error) && error.code === ErrorCodes.AUTH_LOGIN_REQUIRED; +} + +function resolveModelCapabilities( + alias: ModelAlias & { maxContextSize: number }, + provider: KosongProviderConfig, +): ModelCapability { + const capabilities = new Set( + (alias.capabilities ?? []).map((capability) => capability.trim().toLowerCase()), + ); + const has = (capability: string): boolean => capabilities.has(capability); + const capabilityProvider = createProvider(providerForCapabilityProbe(provider)); + const providerCapability = + capabilityProvider.getCapability?.(provider.model) ?? UNKNOWN_CAPABILITY; + + return { + image_in: has('image_in') || providerCapability.image_in, + video_in: has('video_in') || providerCapability.video_in, + audio_in: has('audio_in') || providerCapability.audio_in, + thinking: has('thinking') || has('always_thinking') || providerCapability.thinking, + tool_use: has('tool_use') || providerCapability.tool_use, + max_context_tokens: alias.maxContextSize, + }; +} + +function toKosongProviderConfig( + provider: ProviderConfig, + model: string, + kimiRequestHeaders?: Record | undefined, + maxOutputSize?: number | undefined, + promptCacheKey?: string, +): KosongProviderConfig { + switch (provider.type) { + case 'anthropic': + return { + type: 'anthropic', + model, + baseUrl: providerValue(provider.baseUrl, provider.env, 'ANTHROPIC_BASE_URL'), + apiKey: providerApiKey(provider), + ...(maxOutputSize !== undefined ? { defaultMaxTokens: maxOutputSize } : {}), + ...defaultHeadersField(provider.customHeaders), + }; + case 'openai': + return { + type: 'openai', + model, + baseUrl: providerValue(provider.baseUrl, provider.env, 'OPENAI_BASE_URL'), + apiKey: providerApiKey(provider), + ...defaultHeadersField(provider.customHeaders), + }; + case 'kimi': { + const defaultHeaders = { + ...kimiRequestHeaders, + ...provider.customHeaders, + }; + if (Object.keys(defaultHeaders).length === 0) { + return { + type: 'kimi', + model, + baseUrl: providerValue(provider.baseUrl, provider.env, 'KIMI_BASE_URL'), + generationKwargs: { + prompt_cache_key: promptCacheKey, + }, + apiKey: providerApiKey(provider), + }; + } + return { + type: 'kimi', + model, + baseUrl: providerValue(provider.baseUrl, provider.env, 'KIMI_BASE_URL'), + generationKwargs: { + prompt_cache_key: promptCacheKey, + }, + defaultHeaders, + apiKey: providerApiKey(provider), + }; + } + case 'google-genai': + return { + type: 'google-genai', + model, + apiKey: providerApiKey(provider), + }; + case 'openai_responses': + return { + type: 'openai_responses', + model, + baseUrl: providerValue(provider.baseUrl, provider.env, 'OPENAI_BASE_URL'), + apiKey: providerApiKey(provider), + ...defaultHeadersField(provider.customHeaders), + }; + case 'vertexai': + return { + type: 'vertexai', + model, + vertexai: hasVertexAIServiceEnv(provider), + apiKey: hasVertexAIServiceEnv(provider) ? undefined : providerApiKey(provider), + project: vertexAIProject(provider), + location: vertexAILocation(provider), + }; + default: { + const exhaustive: never = provider.type; + throw new KimiError( + ErrorCodes.MODEL_CONFIG_INVALID, + `Unsupported provider type: ${String(exhaustive)}`, + ); + } + } +} + +// Spread-ready `defaultHeaders` field for a kosong provider config. Returns a +// fresh copy so resolved provider instances never share a header object, and +// omits the key entirely when there are no headers (callers and tests rely on +// `'defaultHeaders' in provider`). +function defaultHeadersField( + headers: Record | undefined, +): { defaultHeaders?: Record } { + if (headers === undefined || Object.keys(headers).length === 0) return {}; + return { defaultHeaders: { ...headers } }; +} + +function providerForCapabilityProbe(provider: KosongProviderConfig): KosongProviderConfig { + if (provider.type === 'vertexai') { + return { + ...provider, + vertexai: false, + project: undefined, + location: undefined, + apiKey: + provider.apiKey === undefined || provider.apiKey.length === 0 + ? 'capability-probe' + : provider.apiKey, + }; + } + if (provider.apiKey !== undefined && provider.apiKey.length > 0) return provider; + return { ...provider, apiKey: 'capability-probe' }; +} + +function providerApiKey(provider: ProviderConfig): string | undefined { + switch (provider.type) { + case 'anthropic': + return providerValue(provider.apiKey, provider.env, 'ANTHROPIC_API_KEY'); + case 'openai': + case 'openai_responses': + return providerValue(provider.apiKey, provider.env, 'OPENAI_API_KEY'); + case 'kimi': + return providerValue(provider.apiKey, provider.env, 'KIMI_API_KEY'); + case 'google-genai': + return providerValue(provider.apiKey, provider.env, 'GOOGLE_API_KEY'); + case 'vertexai': + return ( + nonEmptyString(provider.apiKey) ?? + envValue(provider.env, 'VERTEXAI_API_KEY') ?? + envValue(provider.env, 'GOOGLE_API_KEY') + ); + default: { + const exhaustive: never = provider.type; + throw new KimiError( + ErrorCodes.MODEL_CONFIG_INVALID, + `Unsupported provider type: ${String(exhaustive)}`, + ); + } + } +} + +function hasVertexAIServiceEnv(provider: ProviderConfig): boolean { + return vertexAIProject(provider) !== undefined && vertexAILocation(provider) !== undefined; +} + +function vertexAIProject(provider: ProviderConfig): string | undefined { + return envValue(provider.env, 'GOOGLE_CLOUD_PROJECT'); +} + +function vertexAILocation(provider: ProviderConfig): string | undefined { + return ( + envValue(provider.env, 'GOOGLE_CLOUD_LOCATION') ?? + locationFromVertexAIBaseUrl(provider.baseUrl) + ); +} + +function providerValue( + configured: string | undefined, + env: Record | undefined, + envKey: string, +): string | undefined { + return nonEmptyString(configured) ?? envValue(env, envKey); +} + +function envValue(env: Record | undefined, key: string): string | undefined { + return nonEmptyString(env?.[key]); +} + +function nonEmptyString(value: string | undefined): string | undefined { + const trimmed = value?.trim(); + return trimmed === undefined || trimmed.length === 0 ? undefined : trimmed; +} + +function locationFromVertexAIBaseUrl(baseUrl: string | undefined): string | undefined { + const url = nonEmptyString(baseUrl); + if (url === undefined) return undefined; + try { + const host = new URL(url).hostname; + const suffix = '-aiplatform.googleapis.com'; + return host.endsWith(suffix) ? nonEmptyString(host.slice(0, -suffix.length)) : undefined; + } catch { + return undefined; + } +} diff --git a/packages/agent-core/src/rpc/client.ts b/packages/agent-core/src/rpc/client.ts new file mode 100644 index 000000000..d50cb5f3f --- /dev/null +++ b/packages/agent-core/src/rpc/client.ts @@ -0,0 +1,108 @@ +import type { PromisableMethods, Promisify } from '#/utils/types'; +import { createControlledPromise, objectMap } from '@antfu/utils'; + +import { + fromKimiErrorPayload, + type KimiErrorPayload, + toKimiErrorPayload, +} from '../errors'; +import { abortable } from '../utils/abort'; +import type { CoreAPI } from './core-api'; +import type { SDKAPI } from './sdk-api'; + +export interface RPCCallOptions { + signal?: AbortSignal; +} + +type RpcResponse = + | { readonly ok: true; readonly value: unknown } + | { readonly ok: false; readonly error: KimiErrorPayload }; + +export type RPCMethods = { + [K in keyof T]: T[K] extends (payload: infer Payload) => infer Return + ? (payload: Payload, options?: RPCCallOptions) => Promisify + : never; +}; + +export type RPCClient, Other extends Record> = ( + self: PromisableMethods, +) => Promise>; + +export function createRPC, Right extends Record>(): [ + RPCClient, + RPCClient, +] { + const left = createControlledPromise>(); + const right = createControlledPromise>(); + + function simulateNetwork(data: T): Promise { + return new Promise((resolve) => { + setTimeout(() => { + const serialized = JSON.stringify(data); + resolve(serialized === undefined ? (undefined as T) : JSON.parse(serialized)); + }, 0); + }); + } + + function abortableRpc(promise: Promise, signal?: AbortSignal): Promise { + return signal === undefined ? promise : abortable(promise, signal); + } + + function mapRpcFunction(fn: Function): Function { + return async (payload: any, options?: RPCCallOptions) => { + const signal = options?.signal; + const rpcPayload = await simulateNetwork(payload); + signal?.throwIfAborted(); + let response: RpcResponse; + try { + const value = await abortableRpc(Promise.resolve(fn(rpcPayload)), signal); + response = { ok: true, value }; + } catch (error) { + signal?.throwIfAborted(); + response = { ok: false, error: toKimiErrorPayload(error) }; + } + const remoteResponse = await simulateNetwork(response); + if (remoteResponse.ok) return remoteResponse.value; + throw fromKimiErrorPayload(remoteResponse.error); + }; + } + + function bindAllFunctions>(obj: T): T { + const bound: Record = {}; + let current: object | null = obj; + + while (current !== null && current !== Object.prototype) { + for (const key of Object.getOwnPropertyNames(current)) { + if (key === 'constructor' || Object.hasOwn(bound, key)) { + continue; + } + + const descriptor = Object.getOwnPropertyDescriptor(current, key); + if (typeof descriptor?.value === 'function') { + bound[key] = descriptor.value.bind(obj); + } + } + + current = Object.getPrototypeOf(current); + } + + return bound as T; + } + + async function leftClient(self: PromisableMethods): Promise> { + left.resolve(bindAllFunctions(self)); + return objectMap(await right, (key, fn) => [key, mapRpcFunction(fn)]) as RPCMethods; + } + + async function rightClient(self: PromisableMethods): Promise> { + right.resolve(bindAllFunctions(self)); + return objectMap(await left, (key, fn) => [key, mapRpcFunction(fn)]) as RPCMethods; + } + + return [leftClient, rightClient]; +} + +export type CoreRPCClient = RPCClient; +export type SDKRPCClient = RPCClient; + +export type CoreRPC = RPCMethods; diff --git a/packages/agent-core/src/rpc/core-api.ts b/packages/agent-core/src/rpc/core-api.ts new file mode 100644 index 000000000..8b80597d7 --- /dev/null +++ b/packages/agent-core/src/rpc/core-api.ts @@ -0,0 +1,273 @@ +import type { AgentConfigData } from '#/agent/config'; +import type { AgentContextData } from '#/agent/context'; +import type { PermissionData, PermissionMode } from '#/agent/permission'; +import type { PlanData } from '#/agent/plan'; +import type { ToolInfo } from '#/agent/tool'; +import type { KimiConfig, KimiConfigPatch } from '#/config'; +import type { ResumeSessionResult } from '#/rpc/resumed'; +import type { SessionMeta } from '#/session'; +import type { BackgroundTaskInfo } from '#/tools/builtin'; +import type { ContentPart } from '@moonshot-ai/kosong'; + +import type { UsageStatus } from './events'; +import type { WithAgentId, WithSessionId } from './types'; + +export type JsonPrimitive = string | number | boolean | null; +export type JsonValue = JsonPrimitive | JsonValue[] | { readonly [key: string]: JsonValue }; +export type JsonObject = { readonly [key: string]: JsonValue }; + +export type Unsubscribe = () => void; + +export type { KimiConfig, KimiConfigPatch }; + +export type TextPromptPart = Extract; +export type PromptPart = Extract; + +export type PromptInput = readonly PromptPart[]; + +export type EmptyPayload = {}; +export type SessionMetadataPatch = Partial>; + +export interface CreateSessionPayload { + readonly id?: string | undefined; + readonly workDir: string; + readonly model?: string | undefined; + readonly thinking?: string | undefined; + readonly permission?: PermissionMode | undefined; + readonly metadata?: JsonObject | undefined; +} + +export interface CloseSessionPayload { + readonly sessionId: string; +} + +export interface ResumeSessionPayload { + readonly sessionId: string; +} + +export interface ForkSessionPayload { + readonly sessionId: string; + readonly id?: string; + readonly title?: string; + readonly metadata?: JsonObject; +} + +export interface ExportSessionPayload { + readonly sessionId: string; + readonly outputPath?: string | undefined; + /** + * When true, the active global diagnostic log (`$KIMI_CODE_HOME/logs/kimi-code.log`) + * is copied into the zip at `logs/global/kimi-code.log`. Off by default to + * avoid bundling events from concurrent sessions / other projects. + */ + readonly includeGlobalLog?: boolean | undefined; + /** Host version to record in the export manifest. */ + readonly version: string; +} + +export interface ExportSessionManifest { + readonly sessionId: string; + readonly exportedAt: string; + readonly kimiCodeVersion: string; + readonly wireProtocolVersion: string; + readonly os: string; + readonly nodejsVersion: string; + readonly sessionFirstActivity?: string | undefined; + readonly sessionLastActivity?: string | undefined; + readonly title?: string | undefined; + readonly workspaceDir?: string | undefined; + /** zip-relative path to the session diagnostic log when present. */ + readonly sessionLogPath?: string | undefined; + /** zip-relative path to the bundled global diagnostic log (only when --include-global-log). */ + readonly globalLogPath?: string | undefined; +} + +export interface ExportSessionResult { + readonly zipPath: string; + readonly entries: readonly string[]; + readonly sessionDir: string; + readonly manifest: ExportSessionManifest; +} + +export interface ListSessionsPayload { + readonly workDir: string; +} + +export interface CoreInfo { + readonly version: string; +} + +export interface SessionSummary { + readonly id: string; + readonly title?: string | undefined; + readonly lastPrompt?: string; + readonly workDir: string; + readonly sessionDir: string; + readonly createdAt: number; + readonly updatedAt: number; + readonly archived?: boolean | undefined; + readonly metadata?: JsonObject | undefined; +} + +export interface PromptPayload { + readonly input: readonly ContentPart[]; +} +export interface SteerPayload { + readonly input: readonly ContentPart[]; +} +export interface CancelPayload { + readonly turnId?: number; +} +export interface SetThinkingPayload { + readonly level: string; +} +export interface SetPermissionPayload { + readonly mode: PermissionMode; +} +export interface SetModelPayload { + readonly model: string; +} +export interface SetModelResult { + readonly model: string; + readonly providerName?: string | undefined; +} +export interface CancelPlanPayload { + readonly id?: string; +} +export interface BeginCompactionPayload { + readonly instruction?: string; +} +export interface RegisterToolPayload { + readonly name: string; + readonly description: string; + readonly parameters: Record; +} +export interface UnregisterToolPayload { + readonly name: string; +} +export interface SetActiveToolsPayload { + readonly names: readonly string[]; +} +export interface StopBackgroundPayload { + readonly taskId: string; + /** Free-form human-readable reason persisted with the task record. */ + readonly reason?: string; +} +export interface GetBackgroundOutputPayload { + readonly taskId: string; + readonly tail?: number; +} +export interface GetBackgroundOutputPathPayload { + readonly taskId: string; +} +export interface GetBackgroundPayload { + /** + * When omitted, returns all tasks (including terminal/lost). Pass + * `true` to filter down to active-only — useful for model-facing + * surfaces. UI/TUI consumers should leave it undefined. + */ + readonly activeOnly?: boolean; + /** Caps the number of tasks returned. When omitted, returns all matching tasks. */ + readonly limit?: number; +} +export interface SkillSummary { + readonly name: string; + readonly description: string; + readonly path: string; + readonly source: 'builtin' | 'user' | 'extra' | 'project'; + readonly type?: string | undefined; + readonly disableModelInvocation?: boolean | undefined; +} + +export interface ActivateSkillPayload { + readonly name: string; + readonly args?: string | undefined; +} + +export interface McpServerInfo { + readonly name: string; + readonly transport: 'stdio' | 'http'; + readonly status: 'pending' | 'connected' | 'failed' | 'disabled' | 'needs-auth'; + readonly toolCount: number; + readonly error?: string; +} + +export interface McpStartupMetrics { + readonly durationMs: number; +} + +export interface ReconnectMcpServerPayload { + readonly name: string; +} + +export interface RenameSessionPayload { + readonly title: string; +} + +export interface UpdateSessionMetadataPayload { + readonly metadata: SessionMetadataPatch; +} + +export type SetKimiConfigPayload = KimiConfigPatch; + +export interface RemoveKimiProviderPayload { + readonly providerId: string; +} + +export interface AgentAPI { + prompt: (payload: PromptPayload) => void; + steer: (payload: SteerPayload) => void; + cancel: (payload: CancelPayload) => void; + setThinking: (payload: SetThinkingPayload) => void; + setPermission: (payload: SetPermissionPayload) => void; + setModel: (payload: SetModelPayload) => SetModelResult; + getModel: (payload: EmptyPayload) => string; + enterPlan: (payload: EmptyPayload) => void; + cancelPlan: (payload: CancelPlanPayload) => void; + clearPlan: (payload: EmptyPayload) => void; + beginCompaction: (payload: BeginCompactionPayload) => void; + cancelCompaction: (payload: EmptyPayload) => void; + registerTool: (payload: RegisterToolPayload) => void; + unregisterTool: (payload: UnregisterToolPayload) => void; + setActiveTools: (payload: SetActiveToolsPayload) => void; + stopBackground: (payload: StopBackgroundPayload) => void; + clearContext: (payload: EmptyPayload) => void; + activateSkill: (payload: ActivateSkillPayload) => void; + getBackgroundOutput: (payload: GetBackgroundOutputPayload) => string; + getBackgroundOutputPath: (payload: GetBackgroundOutputPathPayload) => string | undefined; + getContext: (payload: EmptyPayload) => AgentContextData; + getConfig: (payload: EmptyPayload) => AgentConfigData; + getPermission: (payload: EmptyPayload) => PermissionData; + getPlan: (payload: EmptyPayload) => PlanData; + getUsage: (payload: EmptyPayload) => UsageStatus; + getTools: (payload: EmptyPayload) => readonly ToolInfo[]; + getBackground: (payload: GetBackgroundPayload) => readonly BackgroundTaskInfo[]; +} + +type AgentAPIWithId = WithAgentId; + +export interface SessionAPI extends AgentAPIWithId { + renameSession: (payload: RenameSessionPayload) => void; + updateSessionMetadata: (payload: UpdateSessionMetadataPayload) => void; + getSessionMetadata: (payload: EmptyPayload) => SessionMeta; + listSkills: (payload: EmptyPayload) => readonly SkillSummary[]; + listMcpServers: (payload: EmptyPayload) => readonly McpServerInfo[]; + getMcpStartupMetrics: (payload: EmptyPayload) => McpStartupMetrics; + reconnectMcpServer: (payload: ReconnectMcpServerPayload) => void; + generateAgentsMd: (payload: EmptyPayload) => void; +} + +type SessionAPIWithId = WithSessionId; + +export interface CoreAPI extends SessionAPIWithId { + getCoreInfo: (payload: EmptyPayload) => CoreInfo; + getKimiConfig: (payload: EmptyPayload) => KimiConfig; + setKimiConfig: (payload: SetKimiConfigPayload) => KimiConfig; + removeKimiProvider: (payload: RemoveKimiProviderPayload) => KimiConfig; + createSession: (payload: CreateSessionPayload) => SessionSummary; + closeSession: (payload: CloseSessionPayload) => void; + resumeSession: (payload: ResumeSessionPayload) => ResumeSessionResult; + forkSession: (payload: ForkSessionPayload) => ResumeSessionResult; + listSessions: (payload: ListSessionsPayload) => readonly SessionSummary[]; + exportSession: (payload: ExportSessionPayload) => ExportSessionResult; +} diff --git a/packages/agent-core/src/rpc/core-impl.ts b/packages/agent-core/src/rpc/core-impl.ts new file mode 100644 index 000000000..189ae276d --- /dev/null +++ b/packages/agent-core/src/rpc/core-impl.ts @@ -0,0 +1,739 @@ +import { randomUUID } from 'node:crypto'; +import { homedir } from 'node:os'; + +import { localKaos } from '@moonshot-ai/kaos'; +import { ErrorCodes, KimiError } from '#/errors'; +import { getRootLogger, log } from '#/logging/logger'; +import { LocalFetchURLProvider } from '#/tools/providers/local-fetch-url'; +import { MoonshotFetchURLProvider } from '#/tools/providers/moonshot-fetch-url'; +import { MoonshotWebSearchProvider } from '#/tools/providers/moonshot-web-search'; +import { detectEnvironmentFromNode } from '#/utils/environment'; +import { getCoreVersion } from '#/version'; +import type { + ActivateSkillPayload, + BeginCompactionPayload, + CancelPayload, + CancelPlanPayload, + CloseSessionPayload, + CoreAPI, + CoreInfo, + CreateSessionPayload, + EmptyPayload, + ExportSessionPayload, + ExportSessionResult, + ForkSessionPayload, + GetBackgroundOutputPathPayload, + GetBackgroundOutputPayload, + GetBackgroundPayload, + ListSessionsPayload, + McpServerInfo, + McpStartupMetrics, + PromptPayload, + ReconnectMcpServerPayload, + RemoveKimiProviderPayload, + RenameSessionPayload, + ResumeSessionPayload, + RegisterToolPayload, + SetKimiConfigPayload, + SetActiveToolsPayload, + SetModelPayload, + SetModelResult, + SetPermissionPayload, + SetThinkingPayload, + SkillSummary, + SteerPayload, + StopBackgroundPayload, + SessionSummary, + UnregisterToolPayload, + UpdateSessionMetadataPayload, +} from './core-api'; +import type { CoreRPCClient } from './client'; +import type { ResumedAgentState, ResumeSessionResult } from './resumed'; +import type { SDKRPC } from './sdk-api'; +import { proxyWithExtraPayload } from './types'; +import type { PromisableMethods } from '#/utils/types'; + +import { resolveSessionMcpConfig } from '../mcp'; +import { Session, type SessionMeta, type SessionSkillConfig } from '../session'; +import { SessionAPIImpl } from '../session/rpc'; +import { + ensureKimiHome, + mergeConfigPatch, + readConfigFile, + resolveConfigPath, + resolveKimiHome, + writeConfigFile, + type KimiConfig, + type MoonshotServiceConfig, +} from '../config'; +import { exportSessionDirectory } from '../session/export'; +import { ProviderManager } from '../providers/provider-manager'; +import { + type BearerTokenProvider, + type OAuthTokenProviderResolver, +} from '../providers/runtime-provider'; +import type { Logger } from '../logging/types'; +import type { RuntimeConfig } from '../runtime-types'; +import { normalizeWorkDir, SessionStore } from '../session/store'; +import { noopTelemetryClient, withTelemetryContext, type TelemetryClient } from '../telemetry'; + +const KIMI_CODE_PROVIDER_NAME = 'managed:kimi-code'; + +type AgentScopedPayload = T & { readonly agentId: string }; +type SessionScopedPayload = T & { readonly sessionId: string }; +type SessionAgentPayload = SessionScopedPayload>; +type RenameSessionRequest = SessionScopedPayload; +type UpdateSessionMetadataRequest = SessionScopedPayload; + +export interface KimiCoreOptions { + readonly homeDir?: string | undefined; + readonly configPath?: string | undefined; + readonly runtime?: RuntimeConfig | undefined; + readonly kimiRequestHeaders?: Record | undefined; + readonly resolveOAuthTokenProvider?: OAuthTokenProviderResolver | undefined; + readonly skillDirs?: readonly string[]; + readonly telemetry?: TelemetryClient | undefined; +} + +export class KimiCore implements PromisableMethods { + readonly sdk: Promise; + readonly homeDir: string; + readonly configPath: string; + readonly sessions = new Map(); + readonly telemetry: TelemetryClient; + + private runtime: RuntimeConfig | undefined; + private readonly userHomeDir: string; + private readonly kimiRequestHeaders: Record | undefined; + private readonly resolveOAuthTokenProvider: OAuthTokenProviderResolver | undefined; + private readonly skillDirs: readonly string[]; + private readonly providerManager: ProviderManager; + private readonly sessionStore: SessionStore; + + constructor( + protected readonly rpcClient: CoreRPCClient, + options: KimiCoreOptions = {}, + ) { + const explicitHomeDir = options.homeDir ?? process.env['KIMI_CODE_HOME']; + this.homeDir = resolveKimiHome(options.homeDir); + this.userHomeDir = explicitHomeDir ?? homedir(); + this.configPath = resolveConfigPath({ + homeDir: this.homeDir, + configPath: options.configPath, + }); + this.runtime = options.runtime; + this.kimiRequestHeaders = options.kimiRequestHeaders; + this.resolveOAuthTokenProvider = options.resolveOAuthTokenProvider; + this.skillDirs = options.skillDirs ?? []; + this.telemetry = options.telemetry ?? noopTelemetryClient; + ensureKimiHome(this.homeDir); + this.providerManager = new ProviderManager({ + config: readConfigFile(this.configPath), + kimiRequestHeaders: this.kimiRequestHeaders, + resolveOAuthTokenProvider: this.resolveOAuthTokenProvider, + }); + this.sessionStore = new SessionStore(this.homeDir); + + this.sdk = rpcClient(this); + } + + async createSession(input: CreateSessionPayload): Promise { + const options = input; + const workDir = requiredWorkDir('createSession', options.workDir); + const config = this.reloadProviderManager(); + const id = options.id ?? createSessionId(); + const modelName = this.providerManager.resolveSelectedModel(options.model); + const thinkingLevel = this.providerManager.resolveThinkingLevel(options.thinking); + const permissionMode = options.permission ?? config.defaultPermissionMode; + const mcpConfig = await resolveSessionMcpConfig({ + cwd: workDir, + homeDir: this.homeDir, + }); + const summary = await this.sessionStore.create({ + id, + workDir, + }); + const result: SessionSummary = { + ...summary, + metadata: options.metadata, + }; + + // Session ctor attaches its own log sink. If anything in the setup-after- + // ctor block throws, `session.close()` releases the sink (and mcp). + const session = new Session({ + runtime: await this.resolveRuntime(config), + id, + homedir: summary.sessionDir, + kimiHomeDir: this.homeDir, + rpc: proxyWithExtraPayload(await this.sdk, { sessionId: summary.id }), + cwd: workDir, + providerManager: this.providerManager, + background: config.background, + hooks: config.hooks, + permissionRules: config.permission?.rules, + skills: this.resolveSessionSkillConfig(config), + mcpConfig, + telemetry: withTelemetryContext(this.telemetry, { sessionId: summary.id }), + }); + try { + session.metadata = { + ...session.metadata, + createdAt: new Date(summary.createdAt).toISOString(), + updatedAt: new Date(summary.updatedAt).toISOString(), + ...(summary.title !== undefined + ? { + title: summary.title, + isCustomTitle: true, + } + : {}), + custom: options.metadata === undefined ? {} : { ...options.metadata }, + }; + const mainAgent = await session.createMain(); + mainAgent.config.update({ + cwd: workDir, + modelAlias: modelName, + thinkingLevel, + }); + if (permissionMode !== undefined) { + mainAgent.permission.setMode(permissionMode); + } + // Honor config.defaultPlanMode for fresh sessions. Resumed sessions + // restore their own plan state from records and never re-apply this. + if (config.defaultPlanMode === true) { + await mainAgent.planMode.enter(); + } + await session.writeMetadata(); + await session.flushMetadata(); + } catch (error) { + await session.close().catch(() => {}); + throw error; + } + this.sessions.set(id, session); + return result; + } + + getCoreInfo(): CoreInfo { + return { version: getCoreVersion() }; + } + + async closeSession({ sessionId }: CloseSessionPayload): Promise { + const session = this.sessions.get(sessionId); + if (session) { + await session.close(); + this.sessions.delete(sessionId); + } + } + + async resumeSession(input: ResumeSessionPayload): Promise { + const summary = await this.sessionStore.get(input.sessionId); + const active = this.sessions.get(summary.id); + if (active !== undefined) { + return resumeSessionResult(summary, active); + } + + const config = this.reloadProviderManager(); + const mcpConfig = await resolveSessionMcpConfig({ + cwd: summary.workDir, + homeDir: this.homeDir, + }); + const session = new Session({ + runtime: await this.resolveRuntime(config), + id: summary.id, + homedir: summary.sessionDir, + kimiHomeDir: this.homeDir, + rpc: proxyWithExtraPayload(await this.sdk, { sessionId: summary.id }), + cwd: summary.workDir, + providerManager: this.providerManager, + background: config.background, + hooks: config.hooks, + permissionRules: config.permission?.rules, + skills: this.resolveSessionSkillConfig(config), + mcpConfig, + telemetry: withTelemetryContext(this.telemetry, { sessionId: summary.id }), + initializeMainAgent: false, + }); + try { + await session.resume(); + await this.refreshSessionRuntimeConfig(session, config); + } catch (error) { + await session.close().catch(() => {}); + withTelemetryContext(this.telemetry, { sessionId: summary.id }).track('session_load_failed', { + reason: telemetryErrorReason(error), + }); + throw error; + } + this.sessions.set(summary.id, session); + return resumeSessionResult(summary, session); + } + + async forkSession(input: ForkSessionPayload): Promise { + const source = await this.sessionStore.get(input.sessionId); + const active = this.sessions.get(source.id); + if (active !== undefined) { + await active.flushMetadata(); + } + + const id = input.id ?? createSessionId(); + await this.sessionStore.fork({ + sourceId: source.id, + targetId: id, + title: input.title, + metadata: input.metadata, + }); + return this.resumeSession({ sessionId: id }); + } + + async listSessions(input: ListSessionsPayload): Promise { + const options = input; + return this.sessionStore.list({ + ...options, + workDir: requiredWorkDir('listSessions', options.workDir), + }); + } + + async renameSession({ sessionId, ...payload }: RenameSessionRequest): Promise { + const session = this.sessions.get(sessionId); + if (session !== undefined) { + await new SessionAPIImpl(session).renameSession(payload); + return; + } + await this.sessionStore.rename(sessionId, payload.title); + } + + async exportSession(input: ExportSessionPayload): Promise { + const summary = await this.sessionStore.get(input.sessionId); + const active = this.sessions.get(input.sessionId); + // Closed sessions have no `Session.log`; create an ad-hoc child bound to + // their id so the entries still route to the session log file. + const exportLog = + active?.log ?? log.createChild({ sessionId: input.sessionId }); + if (active !== undefined) { + try { + await active.flushMetadata(); + } catch (error) { + exportLog.warn('flushMetadata failed before export', { error }); + } + } + await warnIfLogFlushFails(exportLog, 'export session log flush failed', () => + getRootLogger().flushSession(input.sessionId), + ); + if (input.includeGlobalLog === true) { + await warnIfLogFlushFails(exportLog, 'export global log flush failed', () => + getRootLogger().flushGlobal(), + ); + } + const result = await exportSessionDirectory({ + request: input, + summary, + homeDir: this.homeDir, + globalLogPath: getRootLogger().getConfig()?.globalLogPath, + }); + return result; + } + + async getKimiConfig(input: EmptyPayload = {}): Promise { + void input; + return readConfigFile(this.configPath); + } + + async setKimiConfig(input: SetKimiConfigPayload): Promise { + const config = mergeConfigPatch(readConfigFile(this.configPath), input); + await writeConfigFile(this.configPath, config); + const updated = readConfigFile(this.configPath); + this.providerManager.updateConfig(updated); + return updated; + } + + async removeKimiProvider(input: RemoveKimiProviderPayload): Promise { + const config = readConfigFile(this.configPath); + delete config.providers[input.providerId]; + + let removedDefault = false; + const existingModels = config.models ?? {}; + for (const [key, model] of Object.entries(existingModels)) { + if ( + typeof model === 'object' && + model !== null && + !Array.isArray(model) && + model['provider'] === input.providerId + ) { + delete existingModels[key]; + if (config.defaultModel === key) removedDefault = true; + } + } + config.models = existingModels; + + if (removedDefault) { + config.defaultModel = undefined; + } + + if (config.defaultProvider === input.providerId) { + config.defaultProvider = undefined; + } + + await writeConfigFile(this.configPath, config); + const updated = readConfigFile(this.configPath); + this.providerManager.updateConfig(updated); + return updated; + } + + prompt({ sessionId, ...payload }: SessionAgentPayload) { + return this.sessionApi(sessionId).prompt(payload); + } + + steer({ sessionId, ...payload }: SessionAgentPayload) { + return this.sessionApi(sessionId).steer(payload); + } + + cancel({ sessionId, ...payload }: SessionAgentPayload) { + return this.sessionApi(sessionId).cancel(payload); + } + + async setModel({ + sessionId, + ...payload + }: SessionAgentPayload): Promise { + this.reloadProviderManager(); + return this.sessionApi(sessionId).setModel(payload); + } + + setThinking({ sessionId, ...payload }: SessionAgentPayload) { + return this.sessionApi(sessionId).setThinking(payload); + } + + setPermission({ sessionId, ...payload }: SessionAgentPayload) { + return this.sessionApi(sessionId).setPermission(payload); + } + + getModel({ sessionId, ...payload }: SessionAgentPayload) { + return this.sessionApi(sessionId).getModel(payload); + } + + enterPlan({ sessionId, ...payload }: SessionAgentPayload) { + return this.sessionApi(sessionId).enterPlan(payload); + } + + cancelPlan({ sessionId, ...payload }: SessionAgentPayload) { + return this.sessionApi(sessionId).cancelPlan(payload); + } + + clearPlan({ sessionId, ...payload }: SessionAgentPayload) { + return this.sessionApi(sessionId).clearPlan(payload); + } + + beginCompaction({ sessionId, ...payload }: SessionAgentPayload) { + return this.sessionApi(sessionId).beginCompaction(payload); + } + + cancelCompaction({ sessionId, ...payload }: SessionAgentPayload) { + return this.sessionApi(sessionId).cancelCompaction(payload); + } + + registerTool({ sessionId, ...payload }: SessionAgentPayload) { + return this.sessionApi(sessionId).registerTool(payload); + } + + unregisterTool({ sessionId, ...payload }: SessionAgentPayload) { + return this.sessionApi(sessionId).unregisterTool(payload); + } + + setActiveTools({ sessionId, ...payload }: SessionAgentPayload) { + return this.sessionApi(sessionId).setActiveTools(payload); + } + + stopBackground({ sessionId, ...payload }: SessionAgentPayload) { + return this.sessionApi(sessionId).stopBackground(payload); + } + + clearContext({ sessionId, ...payload }: SessionAgentPayload) { + return this.sessionApi(sessionId).clearContext(payload); + } + + activateSkill({ + sessionId, + ...payload + }: SessionAgentPayload): Promise { + return this.sessionApi(sessionId).activateSkill(payload); + } + + getBackgroundOutput({ sessionId, ...payload }: SessionAgentPayload) { + return this.sessionApi(sessionId).getBackgroundOutput(payload); + } + + getBackgroundOutputPath({ + sessionId, + ...payload + }: SessionAgentPayload) { + return this.sessionApi(sessionId).getBackgroundOutputPath(payload); + } + + getContext({ sessionId, ...payload }: SessionAgentPayload) { + return this.sessionApi(sessionId).getContext(payload); + } + + getConfig({ sessionId, ...payload }: SessionAgentPayload) { + return this.sessionApi(sessionId).getConfig(payload); + } + + getPermission({ sessionId, ...payload }: SessionAgentPayload) { + return this.sessionApi(sessionId).getPermission(payload); + } + + getPlan({ sessionId, ...payload }: SessionAgentPayload) { + return this.sessionApi(sessionId).getPlan(payload); + } + + getUsage({ sessionId, ...payload }: SessionAgentPayload) { + return this.sessionApi(sessionId).getUsage(payload); + } + + getTools({ sessionId, ...payload }: SessionAgentPayload) { + return this.sessionApi(sessionId).getTools(payload); + } + + getBackground({ sessionId, ...payload }: SessionAgentPayload) { + return this.sessionApi(sessionId).getBackground(payload); + } + + updateSessionMetadata({ sessionId, ...payload }: UpdateSessionMetadataRequest): Promise { + return this.sessionApi(sessionId).updateSessionMetadata(payload); + } + + getSessionMetadata({ sessionId, ...payload }: SessionScopedPayload): SessionMeta { + return this.sessionApi(sessionId).getSessionMetadata(payload); + } + + listSkills({ + sessionId, + ...payload + }: SessionScopedPayload): Promise { + return this.sessionApi(sessionId).listSkills(payload); + } + + listMcpServers({ + sessionId, + ...payload + }: SessionScopedPayload): readonly McpServerInfo[] { + return this.sessionApi(sessionId).listMcpServers(payload); + } + + getMcpStartupMetrics({ + sessionId, + ...payload + }: SessionScopedPayload): Promise { + return this.sessionApi(sessionId).getMcpStartupMetrics(payload); + } + + reconnectMcpServer({ + sessionId, + ...payload + }: SessionScopedPayload): Promise { + return this.sessionApi(sessionId).reconnectMcpServer(payload); + } + + generateAgentsMd({ sessionId, ...payload }: SessionScopedPayload): Promise { + return this.sessionApi(sessionId).generateAgentsMd(payload); + } + + private async resolveRuntime(config: KimiConfig): Promise { + if (this.runtime !== undefined) return this.runtime; + const runtime = await createRuntimeConfig({ + config, + kimiRequestHeaders: this.kimiRequestHeaders, + resolveOAuthTokenProvider: this.resolveOAuthTokenProvider, + }); + this.runtime = runtime; + return runtime; + } + + private resolveSessionSkillConfig(config: KimiConfig): SessionSkillConfig { + const explicitDirs = this.skillDirs.length > 0 ? this.skillDirs : undefined; + return { + userHomeDir: this.userHomeDir, + explicitDirs, + extraDirs: config.extraSkillDirs, + mergeAllAvailableSkills: config.mergeAllAvailableSkills, + }; + } + + private sessionApi(sessionId: string): SessionAPIImpl { + const session = this.sessions.get(sessionId); + if (session === undefined) { + throw new KimiError(ErrorCodes.SESSION_NOT_FOUND, `Session "${sessionId}" was not found`, { + details: { sessionId }, + }); + } + return new SessionAPIImpl(session); + } + + private reloadProviderManager(): KimiConfig { + const config = readConfigFile(this.configPath); + this.providerManager.updateConfig(config); + return config; + } + + private async refreshSessionRuntimeConfig( + session: Session, + config: KimiConfig, + ): Promise { + const api = new SessionAPIImpl(session); + // A session migrated from an external tool carries no model, and any + // session may reference a model alias that no longer exists in config.toml. + // Try the session's own model first, then fall back to the configured + // default, so resume degrades gracefully instead of hard-failing. + const requested = (await api.getModel({ agentId: 'main' })).trim(); + const fallback = config.defaultModel?.trim() ?? ''; + const candidates = [...new Set([requested, fallback].filter((model) => model.length > 0))]; + for (const model of candidates) { + try { + await api.setModel({ agentId: 'main', model }); + await session.flushMetadata(); + return; + } catch (error) { + // Skip a candidate only when the alias is genuinely absent from + // config (a stale or migrated model) — that is the graceful-degrade + // case. A *configured* alias that fails to resolve (missing provider, + // no credentials, bad max_context_size) is an actionable config error + // the user must see; surface it instead of silently swapping models. + const aliasMissing = config.models?.[model] === undefined; + if ( + aliasMissing && + error instanceof KimiError && + error.code === ErrorCodes.CONFIG_INVALID + ) { + continue; + } + throw error; + } + } + // No candidate resolved (the replayed alias and the configured default are + // both invalid/unset). Clear the stale alias so the session is honestly + // model-less — the TUI then prompts for a model instead of showing a + // selection whose next prompt fails with a config error. Not persisted: + // `refreshSessionRuntimeConfig` re-derives this on every resume. + if (requested.length > 0) { + session.agents.get('main')?.config.update({ modelAlias: undefined }); + } + } +} + +async function createRuntimeConfig(input: { + readonly config: KimiConfig; + readonly kimiRequestHeaders?: Record | undefined; + readonly resolveOAuthTokenProvider?: OAuthTokenProviderResolver | undefined; +}): Promise { + const localFetcher = new LocalFetchURLProvider(); + const searchService = input.config.services?.moonshotSearch; + const fetchService = input.config.services?.moonshotFetch; + + return { + kaos: localKaos, + osEnv: await detectEnvironmentFromNode(), + urlFetcher: + fetchService?.baseUrl === undefined + ? localFetcher + : new MoonshotFetchURLProvider({ + baseUrl: fetchService.baseUrl, + localFallback: localFetcher, + defaultHeaders: input.kimiRequestHeaders, + ...serviceCredentials(fetchService, input.resolveOAuthTokenProvider), + }), + webSearcher: + searchService?.baseUrl === undefined + ? undefined + : new MoonshotWebSearchProvider({ + baseUrl: searchService.baseUrl, + defaultHeaders: input.kimiRequestHeaders, + ...serviceCredentials(searchService, input.resolveOAuthTokenProvider), + }), + }; +} + +function serviceCredentials( + service: MoonshotServiceConfig, + resolveOAuthTokenProvider: OAuthTokenProviderResolver | undefined, +): { + readonly apiKey?: string | undefined; + readonly tokenProvider?: BearerTokenProvider | undefined; + readonly customHeaders?: Record | undefined; +} { + const apiKey = nonEmptyString(service.apiKey); + return { + apiKey, + tokenProvider: + service.oauth !== undefined + ? resolveOAuthTokenProvider?.(KIMI_CODE_PROVIDER_NAME, service.oauth) + : undefined, + customHeaders: service.customHeaders, + }; +} + +function nonEmptyString(value: string | undefined): string | undefined { + const trimmed = value?.trim(); + return trimmed === undefined || trimmed.length === 0 ? undefined : trimmed; +} + +function requiredWorkDir(operation: string, value: string): string { + if (typeof value !== 'string' || value.trim() === '') { + throw new KimiError(ErrorCodes.REQUEST_WORK_DIR_REQUIRED, `${operation} requires workDir`); + } + return normalizeWorkDir(value); +} + +function createSessionId(): string { + return `session_${randomUUID()}`; +} + +function telemetryErrorReason(error: unknown): string { + if (error instanceof KimiError) return error.code; + if (error instanceof Error && error.name.length > 0) return error.name; + return typeof error; +} + +async function resumeSessionResult( + summary: SessionSummary, + session: Session, +): Promise { + const api = new SessionAPIImpl(session); + const agents: Record = {}; + for (const [agentId, agent] of session.agents) { + const config = await api.getConfig({ agentId }); + const context = await api.getContext({ agentId }); + const permission = await api.getPermission({ agentId }); + const plan = await api.getPlan({ agentId }); + const usage = await api.getUsage({ agentId }); + agents[agentId] = { + type: agent.type, + config, + context, + replay: agent.replayBuilder.buildResult(), + permission, + plan, + usage, + tools: await api.getTools({ agentId }), + toolStore: agent.tools.storeData(), + background: agent.background.list(false), + }; + } + return { + ...summary, + sessionMetadata: api.getSessionMetadata({}), + agents, + }; +} + +async function warnIfLogFlushFails( + exportLog: Logger, + message: string, + flush: () => Promise, +): Promise { + try { + if (await flush()) return; + exportLog.warn(message); + } catch (error) { + exportLog.warn(message, { error }); + } + try { + await flush(); + } catch {} +} diff --git a/packages/agent-core/src/rpc/events.ts b/packages/agent-core/src/rpc/events.ts new file mode 100644 index 000000000..d387c49ae --- /dev/null +++ b/packages/agent-core/src/rpc/events.ts @@ -0,0 +1,297 @@ +import type { FinishReason, TokenUsage } from '@moonshot-ai/kosong'; + +import type { PromptOrigin } from '../agent/context'; +import type { KimiErrorPayload } from '../errors'; +import type { PermissionMode } from '../agent/permission'; +import type { SkillSource } from '../skill'; +import type { BackgroundTaskInfo } from '../tools/background/manager'; +import type { ToolInputDisplay } from '../tools/display'; + +export type { ToolInputDisplay } from '../tools/display'; +export type { KimiErrorPayload } from '../errors'; + +export interface UsageStatus { + readonly byModel?: Record | undefined; + readonly currentTurn?: TokenUsage | undefined; + readonly total?: TokenUsage | undefined; +} + +export interface ToolUpdate { + readonly kind: 'stdout' | 'stderr' | 'progress' | 'status' | 'custom'; + readonly text?: string | undefined; + readonly percent?: number | undefined; + readonly customKind?: string | undefined; + readonly customData?: unknown; +} + +export const MCP_OAUTH_AUTHORIZATION_URL_TOOL_UPDATE = 'mcp.oauth.authorization_url'; + +export interface McpOAuthAuthorizationUrlUpdateData { + readonly serverName: string; + readonly authorizationUrl: string; +} + +export interface CompactionResult { + readonly summary: string; + readonly compactedCount: number; + readonly tokensBefore: number; + readonly tokensAfter: number; +} + +export type TurnEndReason = 'completed' | 'cancelled' | 'failed'; + +export interface AgentStatusUpdatedEvent { + readonly type: 'agent.status.updated'; + readonly model?: string | undefined; + readonly contextTokens?: number | undefined; + readonly maxContextTokens?: number | undefined; + readonly contextUsage?: number | undefined; + readonly planMode?: boolean | undefined; + readonly permission?: PermissionMode | undefined; + readonly usage?: UsageStatus | undefined; +} + +export interface SessionMetaUpdatedEvent { + readonly type: 'session.meta.updated'; + readonly title?: string | undefined; + readonly patch?: Record | undefined; +} + +export interface SkillActivatedEvent { + readonly type: 'skill.activated'; + readonly activationId: string; + readonly skillName: string; + readonly skillArgs?: string | undefined; + readonly trigger: 'user-slash' | 'model-tool' | 'nested-skill'; + readonly skillPath?: string | undefined; + readonly skillSource?: SkillSource | undefined; +} + +export interface ErrorEvent extends KimiErrorPayload { + readonly type: 'error'; + readonly sessionId?: string | undefined; + readonly agentId?: string | undefined; +} + +export interface TurnStartedEvent { + readonly type: 'turn.started'; + readonly turnId: number; + readonly origin: PromptOrigin; +} + +export interface TurnEndedEvent { + readonly type: 'turn.ended'; + readonly turnId: number; + readonly reason: TurnEndReason; + readonly error?: KimiErrorPayload | undefined; +} + +export interface TurnStepStartedEvent { + readonly type: 'turn.step.started'; + readonly turnId: number; + readonly step: number; + readonly stepId?: string | undefined; +} + +export interface TurnStepCompletedEvent { + readonly type: 'turn.step.completed'; + readonly turnId: number; + readonly step: number; + readonly stepId?: string | undefined; + readonly usage?: TokenUsage | undefined; + readonly finishReason?: string | undefined; + readonly providerFinishReason?: FinishReason | undefined; + readonly rawFinishReason?: string | undefined; +} + +export interface TurnStepRetryingEvent { + readonly type: 'turn.step.retrying'; + readonly turnId: number; + readonly step: number; + readonly stepId?: string; + readonly failedAttempt: number; + readonly nextAttempt: number; + readonly maxAttempts: number; + readonly delayMs: number; + readonly errorName: string; + readonly errorMessage: string; + readonly statusCode?: number; +} + +export interface TurnStepInterruptedEvent { + readonly type: 'turn.step.interrupted'; + readonly turnId: number; + readonly step: number; + readonly stepId?: string | undefined; + readonly reason: string; + readonly message?: string | undefined; +} + +export interface AssistantDeltaEvent { + readonly type: 'assistant.delta'; + readonly turnId: number; + readonly delta: string; +} + +export interface HookResultEvent { + readonly type: 'hook.result'; + readonly turnId: number; + readonly hookEvent: string; + readonly content: string; + readonly blocked?: boolean | undefined; +} + +export interface ThinkingDeltaEvent { + readonly type: 'thinking.delta'; + readonly turnId: number; + readonly delta: string; +} + +export interface ToolCallDeltaEvent { + readonly type: 'tool.call.delta'; + readonly turnId: number; + readonly toolCallId: string; + readonly name?: string | undefined; + readonly argumentsPart?: string | undefined; +} + +export interface ToolCallStartedEvent { + readonly type: 'tool.call.started'; + readonly turnId: number; + readonly toolCallId: string; + readonly name: string; + readonly args: unknown; + readonly description?: string | undefined; + readonly display?: ToolInputDisplay | undefined; +} + +export interface ToolProgressEvent { + readonly type: 'tool.progress'; + readonly turnId: number; + readonly toolCallId: string; + readonly update: ToolUpdate; +} + +export interface ToolResultEvent { + readonly type: 'tool.result'; + readonly turnId: number; + readonly toolCallId: string; + readonly output: unknown; + readonly isError?: boolean | undefined; + readonly synthetic?: boolean | undefined; +} + +export interface SubagentSpawnedEvent { + readonly type: 'subagent.spawned'; + readonly subagentId: string; + readonly subagentName: string; + readonly parentToolCallId: string; + readonly parentToolCallUuid?: string | undefined; + readonly parentAgentId?: string | undefined; + readonly description?: string | undefined; + readonly runInBackground: boolean; +} + +export interface SubagentCompletedEvent { + readonly type: 'subagent.completed'; + readonly subagentId: string; + readonly parentToolCallId: string; + readonly resultSummary: string; + readonly usage?: TokenUsage | undefined; +} + +export interface SubagentFailedEvent { + readonly type: 'subagent.failed'; + readonly subagentId: string; + readonly parentToolCallId: string; + readonly error: string; +} + +export interface CompactionStartedEvent { + readonly type: 'compaction.started'; + readonly trigger: 'manual' | 'auto'; + readonly instruction?: string | undefined; +} + +export interface CompactionBlockedEvent { + readonly type: 'compaction.blocked'; + readonly turnId?: number | undefined; +} + +export interface CompactionCancelledEvent { + readonly type: 'compaction.cancelled'; +} + +export interface CompactionCompletedEvent { + readonly type: 'compaction.completed'; + readonly result: CompactionResult; +} + +export interface BackgroundTaskStartedEvent { + readonly type: 'background.task.started'; + readonly info: BackgroundTaskInfo; +} + +export interface BackgroundTaskUpdatedEvent { + readonly type: 'background.task.updated'; + readonly info: BackgroundTaskInfo; +} + +export interface BackgroundTaskTerminatedEvent { + readonly type: 'background.task.terminated'; + readonly info: BackgroundTaskInfo; +} + +export type ToolListUpdatedReason = 'mcp.connected' | 'mcp.disconnected' | 'mcp.failed'; + +export interface ToolListUpdatedEvent { + readonly type: 'tool.list.updated'; + readonly reason: ToolListUpdatedReason; + readonly serverName: string; +} + +export interface McpServerStatusEvent { + readonly type: 'mcp.server.status'; + readonly server: McpServerStatusPayload; +} + +export interface McpServerStatusPayload { + readonly name: string; + readonly transport: 'stdio' | 'http'; + readonly status: 'pending' | 'connected' | 'failed' | 'disabled' | 'needs-auth'; + readonly toolCount: number; + readonly error?: string | undefined; +} + +export type AgentEvent = + | ErrorEvent + | AgentStatusUpdatedEvent + | SessionMetaUpdatedEvent + | SkillActivatedEvent + | TurnStartedEvent + | TurnEndedEvent + | TurnStepStartedEvent + | TurnStepCompletedEvent + | TurnStepRetryingEvent + | TurnStepInterruptedEvent + | AssistantDeltaEvent + | HookResultEvent + | ThinkingDeltaEvent + | ToolCallDeltaEvent + | ToolCallStartedEvent + | ToolProgressEvent + | ToolResultEvent + | ToolListUpdatedEvent + | McpServerStatusEvent + | SubagentSpawnedEvent + | SubagentCompletedEvent + | SubagentFailedEvent + | CompactionStartedEvent + | CompactionBlockedEvent + | CompactionCancelledEvent + | CompactionCompletedEvent + | BackgroundTaskStartedEvent + | BackgroundTaskUpdatedEvent + | BackgroundTaskTerminatedEvent; + +export type Event = AgentEvent & { agentId: string; sessionId: string }; diff --git a/packages/agent-core/src/rpc/index.ts b/packages/agent-core/src/rpc/index.ts new file mode 100644 index 000000000..1bc8f0b05 --- /dev/null +++ b/packages/agent-core/src/rpc/index.ts @@ -0,0 +1,7 @@ +export * from './client'; +export * from './core-api'; +export * from './core-impl'; +export * from './resumed'; +export * from './sdk-api'; +export * from './events'; +export * from './types'; diff --git a/packages/agent-core/src/rpc/resumed.ts b/packages/agent-core/src/rpc/resumed.ts new file mode 100644 index 000000000..61eb1dd67 --- /dev/null +++ b/packages/agent-core/src/rpc/resumed.ts @@ -0,0 +1,39 @@ +import type { AgentType } from '#/agent'; +import type { AgentConfigData, AgentConfigUpdateData } from '#/agent/config'; +import type { AgentContextData, ContextMessage } from '#/agent/context'; +import type { + PermissionApprovalResultRecord, + PermissionData, + PermissionMode, +} from '#/agent/permission'; +import type { PlanData } from '#/agent/plan'; +import type { ToolInfo } from '#/agent/tool'; +import type { SessionSummary } from '#/rpc/core-api'; +import type { UsageStatus } from '#/rpc/events'; +import type { SessionMeta } from '#/session'; +import type { BackgroundTaskInfo } from '#/tools/builtin'; + +export type AgentReplayRecord = + | { type: 'message'; message: ContextMessage } + | { type: 'plan_updated'; enabled: boolean } + | { type: 'config_updated'; config: AgentConfigUpdateData } + | { type: 'permission_updated'; mode: PermissionMode } + | { type: 'approval_result'; record: PermissionApprovalResultRecord }; + +export interface ResumedAgentState { + readonly type: AgentType; + readonly config: AgentConfigData; + readonly context: AgentContextData; + readonly replay: readonly AgentReplayRecord[]; + readonly permission: PermissionData; + readonly plan: PlanData; + readonly usage: UsageStatus; + readonly tools: readonly ToolInfo[]; + readonly toolStore?: Readonly>; + readonly background: readonly BackgroundTaskInfo[]; +} + +export interface ResumeSessionResult extends SessionSummary { + readonly sessionMetadata: SessionMeta; + readonly agents: Readonly>; +} diff --git a/packages/agent-core/src/rpc/sdk-api.ts b/packages/agent-core/src/rpc/sdk-api.ts new file mode 100644 index 000000000..3e58672be --- /dev/null +++ b/packages/agent-core/src/rpc/sdk-api.ts @@ -0,0 +1,79 @@ +import type { ContentPart } from '@moonshot-ai/kosong'; + +import type { RPCMethods } from './client'; +import type { AgentEvent, ToolInputDisplay } from './events'; +import type { WithAgentId, WithSessionId } from './types'; + +export type ApprovalDecision = 'approved' | 'rejected' | 'cancelled'; +export type ApprovalScope = 'session'; + +export interface ApprovalResponse { + readonly decision: ApprovalDecision; + readonly scope?: ApprovalScope | undefined; + readonly feedback?: string | undefined; + readonly selectedLabel?: string | undefined; +} + +export interface ApprovalRequest { + readonly turnId?: number | undefined; + readonly toolCallId: string; + readonly toolName: string; + readonly action: string; + readonly display: ToolInputDisplay; +} + +export interface QuestionOption { + readonly label: string; + readonly description?: string; +} + +export interface QuestionItem { + readonly question: string; + readonly header?: string; + readonly body?: string; + readonly options: readonly QuestionOption[]; + readonly multiSelect?: boolean; + readonly otherLabel?: string; + readonly otherDescription?: string; +} + +export type QuestionAnswerMethod = 'enter' | 'space' | 'number_key'; +export type QuestionAnswers = Record; + +export interface QuestionResponse { + readonly answers: QuestionAnswers; + readonly method?: QuestionAnswerMethod | undefined; +} + +export type QuestionResult = null | QuestionAnswers | QuestionResponse; + +export interface QuestionRequest { + readonly turnId?: number; + readonly toolCallId?: string; + readonly questions: readonly QuestionItem[]; +} + +export interface ToolCallRequest { + readonly turnId?: number | undefined; + readonly toolCallId: string; + readonly args: unknown; +} + +export interface ToolCallResponse { + readonly output: string | ContentPart[]; + readonly isError?: boolean | undefined; +} + +export interface SDKAgentAPI { + emitEvent: (event: AgentEvent) => void; + requestApproval: (request: ApprovalRequest) => Promise; + requestQuestion: (request: QuestionRequest) => Promise; + toolCall: (request: ToolCallRequest) => Promise; +} +export type SDKAgentRPC = RPCMethods; + +export type SDKSessionAPI = WithAgentId; +export type SDKSessionRPC = RPCMethods; + +export type SDKAPI = WithSessionId; +export type SDKRPC = RPCMethods; diff --git a/packages/agent-core/src/rpc/types.ts b/packages/agent-core/src/rpc/types.ts new file mode 100644 index 000000000..29a5bb609 --- /dev/null +++ b/packages/agent-core/src/rpc/types.ts @@ -0,0 +1,29 @@ +import type { RPCMethods } from './client'; + +type Prettify = { + [K in keyof T]: T[K]; +} & {}; + +type WithExtraPayload = { + [K in keyof T]: T[K] extends (payload: infer P) => infer R + ? (payload: Prettify

    ) => R + : never; +}; + +export type WithAgentId = WithExtraPayload; +export type WithSessionId = WithExtraPayload; + +export function proxyWithExtraPayload( + methods: RPCMethods>, + extraPayload: U, +): RPCMethods { + return new Proxy(methods as any, { + get(target, prop) { + const origMethod = target[prop as keyof typeof target]; + if (typeof origMethod !== 'function') { + return origMethod; + } + return (payload: any, ...args: any) => origMethod({ ...payload, ...extraPayload }, ...args); + }, + }); +} diff --git a/packages/agent-core/src/runtime-types.ts b/packages/agent-core/src/runtime-types.ts new file mode 100644 index 000000000..3ab4fcbb8 --- /dev/null +++ b/packages/agent-core/src/runtime-types.ts @@ -0,0 +1,11 @@ +import type { Kaos } from '@moonshot-ai/kaos'; + +import type { UrlFetcher, WebSearchProvider } from './tools/builtin'; +import type { Environment } from './utils/environment'; + +export interface RuntimeConfig { + readonly kaos: Kaos; + readonly osEnv: Environment; + readonly urlFetcher?: UrlFetcher | undefined; + readonly webSearcher?: WebSearchProvider | undefined; +} diff --git a/packages/agent-core/src/session/export/index.ts b/packages/agent-core/src/session/export/index.ts new file mode 100644 index 000000000..baaa03182 --- /dev/null +++ b/packages/agent-core/src/session/export/index.ts @@ -0,0 +1,4 @@ +export * from '#/session/export/manifest'; +export * from '#/session/export/session-export'; +export * from '#/session/export/wire-scan'; +export * from '#/session/export/zip'; diff --git a/packages/agent-core/src/session/export/manifest.ts b/packages/agent-core/src/session/export/manifest.ts new file mode 100644 index 000000000..d5334606b --- /dev/null +++ b/packages/agent-core/src/session/export/manifest.ts @@ -0,0 +1,36 @@ +import { AGENT_WIRE_PROTOCOL_VERSION } from '../../agent/records/types'; +import type { SessionWireScan } from '#/session/export/wire-scan'; +import type { ExportSessionManifest, SessionSummary } from '#/rpc/core-api'; + +export const WIRE_PROTOCOL_VERSION = AGENT_WIRE_PROTOCOL_VERSION; + +export function buildExportManifest(args: { + readonly summary: SessionSummary; + readonly now: Date; + readonly version: string; + readonly wireProtocolVersion?: string | undefined; + readonly sessionScan: SessionWireScan; + readonly sessionLogPath?: string | undefined; + readonly globalLogPath?: string | undefined; +}): ExportSessionManifest { + return { + sessionId: args.summary.id, + exportedAt: args.now.toISOString(), + kimiCodeVersion: args.version, + wireProtocolVersion: args.wireProtocolVersion ?? WIRE_PROTOCOL_VERSION, + os: `${process.platform} ${process.arch}`, + nodejsVersion: process.version.replace(/^v/, ''), + sessionFirstActivity: + args.sessionScan.firstActivityMs === undefined + ? undefined + : new Date(args.sessionScan.firstActivityMs).toISOString(), + sessionLastActivity: + args.sessionScan.lastActivityMs === undefined + ? undefined + : new Date(args.sessionScan.lastActivityMs).toISOString(), + title: args.summary.title, + workspaceDir: args.summary.workDir, + sessionLogPath: args.sessionLogPath, + globalLogPath: args.globalLogPath, + }; +} diff --git a/packages/agent-core/src/session/export/session-export.ts b/packages/agent-core/src/session/export/session-export.ts new file mode 100644 index 000000000..dc71b929a --- /dev/null +++ b/packages/agent-core/src/session/export/session-export.ts @@ -0,0 +1,86 @@ +import { readFile } from 'node:fs/promises'; +import { resolve } from 'node:path'; + +import { ErrorCodes, KimiError } from '#/errors'; +import { resolveGlobalLogPath } from '#/logging/logger'; +import { buildExportManifest } from '#/session/export/manifest'; +import { scanSessionWire } from '#/session/export/wire-scan'; +import { + type ExtraZipEntry, + collectFilesRecursive, + writeExportZip, +} from '#/session/export/zip'; +import type { ExportSessionPayload, ExportSessionResult, SessionSummary } from '#/rpc/core-api'; + +const SESSION_LOG_REL = 'logs/kimi-code.log'; +const GLOBAL_LOG_REL = 'logs/global/kimi-code.log'; + +export async function exportSessionDirectory(input: { + readonly request: ExportSessionPayload; + readonly summary: SessionSummary; + readonly homeDir?: string | undefined; + readonly globalLogPath?: string | undefined; +}): Promise { + const sessionDir = input.summary.sessionDir; + const sessionFiles = await collectFilesRecursive(sessionDir); + if (sessionFiles.length === 0) { + throw new KimiError(ErrorCodes.SESSION_EXPORT_NOT_FOUND, `Session "${input.summary.id}" has no exportable directory at "${sessionDir}"`, { + details: { sessionId: input.summary.id, sessionDir }, + }); + } + + const sessionScan = await scanSessionWire(sessionDir); + const hasSessionLog = sessionFiles.some((f) => + f.endsWith(`/${SESSION_LOG_REL}`) || f.endsWith(`\\${SESSION_LOG_REL.replaceAll('/', '\\')}`), + ); + + const extras: ExtraZipEntry[] = []; + let bundledGlobal = false; + const globalPath = + input.globalLogPath ?? + (input.homeDir === undefined ? undefined : resolveGlobalLogPath(input.homeDir)); + if (input.request.includeGlobalLog === true && globalPath !== undefined) { + const data = await readOptionalFile(globalPath); + if (data !== undefined) { + extras.push({ data, target: GLOBAL_LOG_REL }); + bundledGlobal = true; + } + } + + const manifest = buildExportManifest({ + summary: input.summary, + now: new Date(), + version: input.request.version, + sessionScan, + sessionLogPath: hasSessionLog ? SESSION_LOG_REL : undefined, + globalLogPath: bundledGlobal ? GLOBAL_LOG_REL : undefined, + }); + + const outputPath = + input.request.outputPath !== undefined + ? resolve(input.request.outputPath) + : resolve(`${input.summary.id}.zip`); + + const entries = await writeExportZip({ + outputPath, + manifest, + sessionDir, + sessionFiles, + extraEntries: extras, + }); + + return { + zipPath: outputPath, + entries, + sessionDir, + manifest, + }; +} + +async function readOptionalFile(path: string): Promise { + try { + return await readFile(path); + } catch { + return undefined; + } +} diff --git a/packages/agent-core/src/session/export/wire-scan.ts b/packages/agent-core/src/session/export/wire-scan.ts new file mode 100644 index 000000000..2d5aa2e2e --- /dev/null +++ b/packages/agent-core/src/session/export/wire-scan.ts @@ -0,0 +1,69 @@ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +export interface SessionWireScan { + readonly firstActivityMs?: number | undefined; + readonly lastActivityMs?: number | undefined; + readonly lastUserMessageMs?: number | undefined; + readonly firstUserInput?: string | undefined; +} + +export async function scanSessionWire(sessionDir: string): Promise { + let raw: string; + try { + raw = await readFile(join(sessionDir, 'wire.jsonl'), 'utf-8'); + } catch { + return {}; + } + + let firstActivityMs: number | undefined; + let lastActivityMs: number | undefined; + let lastUserMessageMs: number | undefined; + let firstUserInput: string | undefined; + + for (const line of raw.split('\n')) { + const trimmed = line.trim(); + if (trimmed.length === 0) continue; + let parsed: unknown; + try { + parsed = JSON.parse(trimmed) as unknown; + } catch { + continue; + } + if (typeof parsed !== 'object' || parsed === null) continue; + const record = parsed as { + type?: unknown; + time?: unknown; + userInput?: unknown; + }; + const timeMs = typeof record.time === 'number' ? normalizeTimestampMs(record.time) : undefined; + if (timeMs !== undefined) { + firstActivityMs ??= timeMs; + lastActivityMs = timeMs; + } + if (record.type === 'turn_begin') { + if (timeMs !== undefined) { + lastUserMessageMs = timeMs; + } + if ( + firstUserInput === undefined && + typeof record.userInput === 'string' && + record.userInput.trim().length > 0 + ) { + firstUserInput = record.userInput; + } + } + } + + return { + firstActivityMs, + lastActivityMs, + lastUserMessageMs, + firstUserInput, + }; +} + +export function normalizeTimestampMs(value: number): number | undefined { + if (!Number.isFinite(value) || value <= 0) return undefined; + return value > 1e12 ? Math.floor(value) : Math.floor(value * 1000); +} diff --git a/packages/agent-core/src/session/export/zip.ts b/packages/agent-core/src/session/export/zip.ts new file mode 100644 index 000000000..295ac4a31 --- /dev/null +++ b/packages/agent-core/src/session/export/zip.ts @@ -0,0 +1,69 @@ +import { createWriteStream } from 'node:fs'; +import { mkdir, readdir, readFile } from 'node:fs/promises'; +import { dirname, join, relative } from 'node:path'; +import type { Readable } from 'node:stream'; +import { pipeline } from 'node:stream/promises'; + +import type { ExportSessionManifest } from '#/rpc/core-api'; +import { ZipFile } from 'yazl'; + +export async function collectFilesRecursive(root: string): Promise { + try { + const entries = await readdir(root, { recursive: true, withFileTypes: true }); + return entries + .filter((entry) => entry.isFile()) + .map((entry) => join(entry.parentPath, entry.name)) + .toSorted((a, b) => a.localeCompare(b)); + } catch { + return []; + } +} + +export type ExtraZipEntry = + | { + /** Absolute path on disk. */ + readonly source: string; + /** zip-relative target path. */ + readonly target: string; + } + | { + readonly data: Buffer; + /** zip-relative target path. */ + readonly target: string; + }; + +export async function writeExportZip(args: { + readonly outputPath: string; + readonly manifest: ExportSessionManifest; + readonly sessionDir: string; + readonly sessionFiles: readonly string[]; + readonly extraEntries?: readonly ExtraZipEntry[]; +}): Promise { + await mkdir(dirname(args.outputPath), { recursive: true }); + + const entries: string[] = ['manifest.json']; + const zip = new ZipFile(); + zip.addBuffer(Buffer.from(JSON.stringify(args.manifest, null, 2), 'utf-8'), 'manifest.json'); + + for (const abs of args.sessionFiles) { + const rel = relative(args.sessionDir, abs).split(/[\\/]/).join('/'); + const data = await readFile(abs); + zip.addBuffer(data, rel); + entries.push(rel); + } + + for (const extra of args.extraEntries ?? []) { + try { + const data = 'data' in extra ? extra.data : await readFile(extra.source); + zip.addBuffer(data, extra.target); + entries.push(extra.target); + } catch { + // missing source is not fatal — caller decided it should be opt-in; + // do not abort the whole export. + } + } + + zip.end(); + await pipeline(zip.outputStream as unknown as Readable, createWriteStream(args.outputPath)); + return entries; +} diff --git a/packages/agent-core/src/session/git-context.ts b/packages/agent-core/src/session/git-context.ts new file mode 100644 index 000000000..f30edb479 --- /dev/null +++ b/packages/agent-core/src/session/git-context.ts @@ -0,0 +1,197 @@ +/** + * Git repository context for explore subagents. + * + * `collectGitContext` produces a `` block that is prepended to a + * fresh explore subagent's prompt so it can orient itself in the repository + * before searching. Every git command is individually guarded — a single + * failure never aborts the whole collection — and remote URLs are sanitized + * so internal infrastructure is not surfaced to the model. + */ + +import type { Readable } from 'node:stream'; + +import type { Kaos } from '@moonshot-ai/kaos'; + +const GIT_TIMEOUT_MS = 5_000; +const MAX_DIRTY_FILES = 20; +const MAX_COMMIT_LINE_LENGTH = 200; + +// Well-known public hosts whose remote URLs are safe to surface. Self-hosted +// or unrecognized hosts are excluded to avoid leaking internal infrastructure. +const ALLOWED_HOSTS = [ + 'github.com', + 'gitlab.com', + 'gitee.com', + 'bitbucket.org', + 'codeberg.org', + 'git.sr.ht', +] as const; + +/** + * Collect git context for the explore agent. + * + * Returns a formatted `` block, or an empty string if the + * directory is not a git repository or no useful information was collected. + */ +export async function collectGitContext(kaos: Kaos, cwd: string): Promise { + // Quick check: is this a git repo? + if ((await runGit(kaos, cwd, ['rev-parse', '--is-inside-work-tree'])) === null) { + return ''; + } + + const [remoteUrl, branch, dirtyRaw, logRaw] = await Promise.all([ + runGit(kaos, cwd, ['remote', 'get-url', 'origin']), + runGit(kaos, cwd, ['branch', '--show-current']), + runGit(kaos, cwd, ['status', '--porcelain']), + runGit(kaos, cwd, ['log', '-3', '--format=%h %s']), + ]); + + const sections: string[] = [`Working directory: ${cwd}`]; + + if (remoteUrl) { + const safeUrl = sanitizeRemoteUrl(remoteUrl); + if (safeUrl) { + sections.push(`Remote: ${safeUrl}`); + // Derive the project slug only from an allowed remote — deriving it from + // a rejected host would leak an internal owner/repo into the prompt. + const project = parseProjectName(safeUrl); + if (project) sections.push(`Project: ${project}`); + } + } + + if (branch) sections.push(`Branch: ${branch}`); + + if (dirtyRaw !== null) { + const dirtyLines = dirtyRaw.split('\n').filter((line) => line.trim().length > 0); + if (dirtyLines.length > 0) { + const total = dirtyLines.length; + const shown = dirtyLines.slice(0, MAX_DIRTY_FILES); + let body = shown.map((line) => ` ${line}`).join('\n'); + if (total > MAX_DIRTY_FILES) { + body += `\n ... and ${String(total - MAX_DIRTY_FILES)} more`; + } + sections.push(`Dirty files (${String(total)}):\n${body}`); + } + } + + if (logRaw) { + const logLines = logRaw.split('\n').filter((line) => line.trim().length > 0); + if (logLines.length > 0) { + const body = logLines.map((line) => ` ${line.slice(0, MAX_COMMIT_LINE_LENGTH)}`).join('\n'); + sections.push(`Recent commits:\n${body}`); + } + } + + if (sections.length <= 1) { + // Only the working directory line — nothing useful collected. + return ''; + } + + return `\n${sections.join('\n')}\n`; +} + +/** + * Return the remote URL if it points to a well-known public host, stripping + * credentials from HTTPS URLs. Returns `null` for unrecognized hosts. + */ +export function sanitizeRemoteUrl(remoteUrl: string): string | null { + // SSH format: git@host:owner/repo.git — no credentials possible. + for (const host of ALLOWED_HOSTS) { + if (remoteUrl.startsWith(`git@${host}:`)) return remoteUrl; + } + + // HTTPS format: parse the hostname exactly and drop any userinfo. + let parsed: URL; + try { + parsed = new URL(remoteUrl); + } catch { + return null; + } + if ((ALLOWED_HOSTS as readonly string[]).includes(parsed.hostname)) { + const port = parsed.port ? `:${parsed.port}` : ''; + return `https://${parsed.hostname}${port}${parsed.pathname}`; + } + + return null; +} + +/** + * Extract the project path from a git remote URL — `owner/repo`, or the full + * `group/subgroup/repo` for nested namespaces (e.g. GitLab subgroups). + * Supports scp-like SSH (`git@host:path`) and URL forms (`https://`, `ssh://`). + */ +export function parseProjectName(remoteUrl: string): string | null { + // scp-like SSH (`git@host:owner/.../repo.git`) is not a valid URL — match it + // directly; everything else goes through URL parsing. The whole path is kept + // so nested namespaces survive. + const scp = /^[^/]+@[^/:]+:(.+)$/.exec(remoteUrl); + const rawPath = scp?.[1] ?? tryUrlPath(remoteUrl); + if (rawPath === null) return null; + const project = rawPath.replace(/^\/+/, '').replace(/\/+$/, '').replace(/\.git$/, ''); + return project.length > 0 ? project : null; +} + +function tryUrlPath(remoteUrl: string): string | null { + try { + return new URL(remoteUrl).pathname; + } catch { + return null; + } +} + +/** + * Run a single `git -C ` command and return its trimmed stdout, + * or `null` on any failure (spawn error, non-zero exit, or timeout). The + * `git -C` form runs in the target directory regardless of the Kaos backend. + */ +async function runGit(kaos: Kaos, cwd: string, args: readonly string[]): Promise { + let proc; + try { + proc = await kaos.exec('git', '-C', cwd, ...args); + } catch { + return null; + } + + try { + proc.stdin.end(); + } catch { + /* stdin already closed */ + } + + const work = Promise.all([collectStream(proc.stdout), proc.wait()]); + // Attach a rejection handler up front: if `work` rejects during the + // timeout-handling window (before the catch block re-awaits it), Node must + // not flag it as an unhandled rejection. + work.catch(() => {}); + let timer: ReturnType | undefined; + try { + const timeout = new Promise((_resolve, reject) => { + timer = setTimeout(() => { + reject(new Error(`git ${args.join(' ')} timed out`)); + }, GIT_TIMEOUT_MS); + }); + const [stdout, exitCode] = await Promise.race([work, timeout]); + if (exitCode !== 0) return null; + return stdout.trim(); + } catch { + try { + await proc.kill('SIGKILL'); + } catch { + /* process already gone */ + } + // Let the stdout drain settle so the process resources are released, + // even though the timed-out output is discarded. + await work.catch(() => {}); + return null; + } finally { + if (timer !== undefined) clearTimeout(timer); + } +} + +async function collectStream(stream: Readable): Promise { + const chunks: Buffer[] = []; + for await (const chunk of stream) { + chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk as string)); + } + return Buffer.concat(chunks).toString('utf-8'); +} diff --git a/packages/agent-core/src/session/index.ts b/packages/agent-core/src/session/index.ts new file mode 100644 index 000000000..b4c0adba5 --- /dev/null +++ b/packages/agent-core/src/session/index.ts @@ -0,0 +1,511 @@ +import { homedir } from 'node:os'; +import { join } from 'node:path'; + +import { ErrorCodes, KimiError } from '#/errors'; +import { getRootLogger, log } from '#/logging/logger'; +import type { Logger, SessionLogHandle } from '#/logging/types'; +import type { SDKSessionRPC } from '#/rpc'; +import { proxyWithExtraPayload } from '#/rpc/types'; + +import { Agent, type AgentConfig, type AgentType } from '../agent'; +import { HookEngine, type HookDef } from '../agent/hooks'; +import type { PermissionManagerOptions, PermissionRule } from '../agent/permission'; +import { parseBooleanEnv, resolveConfigValue, type BackgroundConfig } from '../config'; +import { makeErrorPayload } from '../errors'; +import { + McpConnectionManager, + McpOAuthService, + type McpServerEntry, + type SessionMcpConfig, +} from '../mcp'; +import { + DEFAULT_AGENT_PROFILES, + DEFAULT_INIT_PROMPT, + loadAgentsMd, + prepareSystemPromptContext, + type ResolvedAgentProfile, +} from '../profile'; +import type { ProviderManager } from '../providers/provider-manager'; +import type { RuntimeConfig } from '../runtime-types'; +import { + registerBuiltinSkills, + resolveSkillRoots, + SkillRegistry, + summarizeSkill, + type SkillSummary, +} from '../skill'; +import { noopTelemetryClient, type TelemetryClient } from '../telemetry'; +import { SessionSubagentHost } from './subagent-host'; + +export interface SessionConfig { + readonly runtime: RuntimeConfig; + readonly id?: string | undefined; + readonly homedir: string; + readonly kimiHomeDir?: string; + readonly rpc: SDKSessionRPC; + readonly cwd?: string; + readonly initializeMainAgent?: boolean | undefined; + readonly providerManager?: ProviderManager | undefined; + readonly background?: BackgroundConfig | undefined; + readonly hooks?: readonly HookDef[]; + readonly permissionRules?: readonly PermissionRule[]; + readonly skills?: SessionSkillConfig; + readonly mcpConfig?: SessionMcpConfig; + readonly telemetry?: TelemetryClient | undefined; +} + +export interface SessionSkillConfig { + readonly userHomeDir?: string; + readonly explicitDirs?: readonly string[]; + readonly extraDirs?: readonly string[]; + readonly mergeAllAvailableSkills?: boolean; + readonly builtinDir?: string; +} + +export interface AgentMeta { + readonly homedir: string; + readonly type: AgentType; + readonly parentAgentId: string | null; +} + +export interface SessionMeta { + createdAt: string; + updatedAt: string; + title: string; + isCustomTitle: boolean; + lastPrompt?: string; + forkedFrom?: string; + agents: Record; + custom: Record; +} + +const BACKGROUND_KEEP_ALIVE_ON_EXIT_ENV = 'KIMI_CODE_BACKGROUND_KEEP_ALIVE_ON_EXIT'; + +export class Session { + readonly rpc: SDKSessionRPC; + readonly telemetry: TelemetryClient; + readonly skills: SkillRegistry; + readonly agents: Map = new Map(); + readonly mcp: McpConnectionManager; + readonly log: Logger; + private readonly logHandle: SessionLogHandle | undefined; + readonly hookEngine: HookEngine; + private agentIdCounter = 0; + private readonly skillsReady: Promise; + metadata: SessionMeta = { + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + title: 'New Session', + isCustomTitle: false, + agents: {}, + custom: {}, + }; + private writeMetadataPromise = Promise.resolve(); + + constructor(public readonly config: SessionConfig) { + // Attach the per-session log sink up front so the constructor's + // fire-and-forget `loadSkills` / `loadMcpServers` failures (and + // anything else that races) land in the session log, not just global. + this.logHandle = + config.id === undefined + ? undefined + : getRootLogger().attachSession({ + sessionId: config.id, + sessionDir: config.homedir, + }); + this.log = + this.logHandle?.logger ?? + (config.id === undefined ? log : log.createChild({ sessionId: config.id })); + this.rpc = config.rpc; + this.hookEngine = new HookEngine(config.hooks, { + cwd: config.cwd, + sessionId: config.id, + }); + this.telemetry = config.telemetry ?? noopTelemetryClient; + this.skills = new SkillRegistry({ sessionId: config.id }); + this.mcp = new McpConnectionManager({ + oauthService: new McpOAuthService({ kimiHomeDir: config.kimiHomeDir }), + log: this.log, + }); + this.mcp.onStatusChange((entry) => { + this.onMcpServerStatusChange(entry); + }); + this.skillsReady = this.loadSkills() + .catch((error: unknown) => { + this.log.error('skills load failed', error); + }) + .then(() => { + this.refreshAgentBuiltinTools(); + }); + void this.loadMcpServers().catch((error: unknown) => { + this.emitInitialMcpLoadError(error); + }); + } + + async createMain() { + const { agent } = await this.createAgent({ type: 'main' }, DEFAULT_AGENT_PROFILES['agent']); + await this.triggerSessionStart('startup'); + return agent; + } + + async resume() { + await this.skillsReady; + const { agents } = await this.readMetadata(); + this.agents.clear(); + const resumeTasks = Object.keys(agents).map(async (id) => { + const agent = this.ensureResumeAgentInstantiated(id, agents); + await agent.resume(); + }); + await Promise.all(resumeTasks); + // A session migrated from an external tool ships a wire without the + // `config.update` bootstrap events a natively-created agent writes, so the + // main agent comes back with an empty system prompt and no tools. Apply the + // default profile so the resumed session is usable. Native sessions always + // replay a non-empty system prompt and never enter this branch. + const main = this.agents.get('main'); + const profile = DEFAULT_AGENT_PROFILES['agent']; + if (main !== undefined && profile !== undefined && main.config.systemPrompt === '') { + await this.bootstrapAgentProfile(main, profile); + } + await this.triggerSessionStart('resume'); + } + + async close(): Promise { + try { + await this.stopBackgroundTasksOnExit(); + await this.flushMetadata(); + await this.triggerSessionEnd('exit'); + } finally { + try { + await this.mcp.shutdown(); + } finally { + await this.logHandle?.close(); + } + } + } + + private async stopBackgroundTasksOnExit(): Promise { + const keepAliveOnExit = resolveConfigValue({ + env: process.env, + envKey: BACKGROUND_KEEP_ALIVE_ON_EXIT_ENV, + configValue: this.config.background?.keepAliveOnExit, + defaultValue: true, + parseEnv: parseBooleanEnv, + }); + if (keepAliveOnExit) return; + await Promise.all( + Array.from(this.agents.values(), (agent) => + agent.background.stopAll('Session closed'), + ), + ); + } + + async createAgent( + config: Partial, + profile?: ResolvedAgentProfile, + parentAgentId?: string | undefined, + ): Promise<{ readonly id: string; readonly agent: Agent }> { + await this.skillsReady; + const type = config.type ?? 'main'; + const id = type === 'main' ? 'main' : this.nextGeneratedAgentId(); + const homedir = config.homedir ?? join(this.config.homedir, 'agents', id); + const agent = this.instantiateAgent(id, homedir, type, config, parentAgentId ?? null); + if (profile) { + await this.bootstrapAgentProfile(agent, profile); + } else if (this.config.cwd !== undefined) { + agent.config.update({ cwd: this.config.cwd }); + } + + this.agents.set(id, agent); + this.metadata.agents[id] = { + homedir, + type, + parentAgentId: parentAgentId ?? null, + }; + void this.writeMetadata(); + + return { id, agent }; + } + + /** + * Applies a profile's derived config — cwd, system prompt, active tools — to + * an agent. Fresh creation and resume-of-an-incomplete-wire both route + * through here so the two paths cannot drift apart. + */ + private async bootstrapAgentProfile( + agent: Agent, + profile: ResolvedAgentProfile, + ): Promise { + if (this.config.cwd !== undefined) { + agent.config.update({ cwd: this.config.cwd }); + } + const context = await prepareSystemPromptContext(this.config.runtime.kaos, agent.config.cwd); + agent.useProfile(profile, context); + } + + async generateAgentsMd(): Promise { + await this.skillsReady; + const mainAgent = this.requireMainAgent(); + + try { + const handle = await mainAgent.subagentHost!.spawn('coder', { + parentToolCallId: 'generate-agents-md', + prompt: DEFAULT_INIT_PROMPT, + description: 'Initialize AGENTS.md', + runInBackground: false, + origin: { kind: 'system_trigger', name: 'init' }, + signal: new AbortController().signal, + }); + await handle.completion; + + const agentsMd = await loadAgentsMd(this.config.runtime.kaos, mainAgent.config.cwd); + mainAgent.context.appendSystemReminder(initCompletionReminder(agentsMd), { + kind: 'injection', + variant: 'init', + }); + await mainAgent.records.flush(); + } catch (error) { + throw new KimiError( + ErrorCodes.SESSION_INIT_FAILED, + error instanceof Error ? error.message : 'Init failed', + { cause: error }, + ); + } + } + + get hasActiveTurn(): boolean { + for (const agent of this.agents.values()) { + if (agent.turn.hasActiveTurn) return true; + } + return false; + } + + protected get metadataPath() { + return join(this.config.homedir, 'state.json'); + } + + writeMetadata() { + const text = JSON.stringify(this.metadata, null, 2); + const write = async () => { + await this.config.runtime.kaos.mkdir(this.config.homedir, { parents: true, existOk: true }); + await this.config.runtime.kaos.writeText(this.metadataPath, text); + }; + this.writeMetadataPromise = this.writeMetadataPromise.then(write, write); + return this.writeMetadataPromise; + } + + async readMetadata() { + const text = await this.config.runtime.kaos.readText(this.metadataPath); + this.metadata = JSON.parse(text); + return this.metadata; + } + + async flushMetadata() { + await this.skillsReady; + await this.writeMetadataPromise; + await Promise.all(Array.from(this.agents.values()).map((agent) => agent.records.flush())); + } + + async listSkills(): Promise { + await this.skillsReady; + return this.skills.listSkills().map(summarizeSkill); + } + + private async loadSkills(): Promise { + const roots = await resolveSkillRoots({ + paths: { + userHomeDir: this.config.skills?.userHomeDir ?? homedir(), + workDir: this.config.cwd ?? process.cwd(), + }, + explicitDirs: this.config.skills?.explicitDirs, + extraDirs: this.config.skills?.extraDirs, + mergeAllAvailableSkills: this.config.skills?.mergeAllAvailableSkills, + builtinDir: this.config.skills?.builtinDir, + }); + await this.skills.loadRoots(roots); + registerBuiltinSkills(this.skills); + } + + private async loadMcpServers(): Promise { + const servers = this.config.mcpConfig?.servers; + if (servers === undefined || Object.keys(servers).length === 0) return; + await this.mcp.connectAll(servers); + const entries = this.mcp.list().filter((entry) => entry.status !== 'disabled'); + const totalCount = entries.length; + if (totalCount === 0) return; + + const connectedCount = entries.filter((entry) => entry.status === 'connected').length; + if (connectedCount > 0) { + this.telemetry.track('mcp_connected', { + server_count: connectedCount, + total_count: totalCount, + }); + } + + const failedCount = entries.filter((entry) => entry.status === 'failed').length; + if (failedCount > 0) { + this.telemetry.track('mcp_failed', { + failed_count: failedCount, + total_count: totalCount, + }); + } + } + + private emitInitialMcpLoadError(error: unknown): void { + const message = error instanceof Error ? error.message : String(error); + this.log.error('mcp initial load failed', error); + void this.rpc.emitEvent({ + type: 'error', + agentId: 'main', + ...makeErrorPayload(ErrorCodes.MCP_STARTUP_FAILED, message), + }); + } + + private onMcpServerStatusChange(entry: McpServerEntry): void { + // Always surface server-level status changes to clients so the TUI/SDK + // can keep its dashboard in sync, even before the main agent exists. + void this.rpc.emitEvent({ + type: 'mcp.server.status', + agentId: 'main', + server: { + name: entry.name, + transport: entry.transport, + status: entry.status, + toolCount: entry.toolCount, + error: entry.error, + }, + }); + } + + private backgroundTaskTimeoutMs(): number | undefined { + const timeoutS = this.config.background?.agentTaskTimeoutS; + return timeoutS === undefined ? undefined : timeoutS * 1000; + } + + private refreshAgentBuiltinTools(): void { + for (const agent of this.agents.values()) { + if (!agent.config.hasProvider) continue; + agent.tools.initializeBuiltinTools(); + } + } + + private instantiateAgent( + id: string, + homedir: string, + type: AgentType, + config: Partial = {}, + parentAgentId: string | null = null, + ): Agent { + return new Agent({ + ...config, + type, + runtime: this.config.runtime, + homedir, + skills: this.skills, + rpc: proxyWithExtraPayload(this.rpc, { agentId: id }), + providerManager: this.config.providerManager, + sessionId: this.config.id, + hookEngine: config.hookEngine ?? this.hookEngine, + subagentHost: + config.subagentHost ?? new SessionSubagentHost(this, id, this.backgroundTaskTimeoutMs()), + mcp: this.mcp, + backgroundMaxRunningTasks: this.config.background?.maxRunningTasks, + backgroundSessionDir: homedir, + permission: this.permissionOptions(parentAgentId, config.permission), + telemetry: this.telemetry, + log: this.log.createChild({ agentId: id }), + }); + } + + private permissionOptions( + parentAgentId: string | null, + input?: PermissionManagerOptions | undefined, + ): PermissionManagerOptions { + if (parentAgentId === null) { + return { + ...input, + initialRules: input?.initialRules ?? this.config.permissionRules, + }; + } + return { + ...input, + parent: input?.parent ?? this.agents.get(parentAgentId)?.permission, + }; + } + + private ensureResumeAgentInstantiated( + id: string, + agents: Record, + stack: readonly string[] = [], + ): Agent { + const existing = this.agents.get(id); + if (existing !== undefined) return existing; + if (stack.includes(id)) { + throw new KimiError( + ErrorCodes.SESSION_STATE_INVALID, + `Session agent parent chain contains a cycle: ${[...stack, id].join(' -> ')}`, + ); + } + + const meta = agents[id]; + if (meta === undefined) { + throw new KimiError(ErrorCodes.SESSION_STATE_INVALID, `Session agent "${id}" is missing`); + } + + const parentAgentId = meta.parentAgentId ?? null; + if (parentAgentId !== null) { + this.ensureResumeAgentInstantiated(parentAgentId, agents, [...stack, id]); + } + + const agent = this.instantiateAgent(id, meta.homedir, meta.type, {}, parentAgentId); + this.agents.set(id, agent); + return agent; + } + + private nextGeneratedAgentId(): string { + while (true) { + const id = `agent-${this.agentIdCounter++}`; + if (this.agents.has(id)) continue; + if (this.metadata.agents[id] !== undefined) continue; + return id; + } + } + + private requireMainAgent(): Agent { + const agent = this.agents.get('main'); + if (agent === undefined) { + throw new KimiError(ErrorCodes.AGENT_NOT_FOUND, 'Main agent was not found'); + } + return agent; + } + + private async triggerSessionStart(source: 'startup' | 'resume'): Promise { + await this.hookEngine.trigger('SessionStart', { + matcherValue: source, + inputData: { source }, + }); + } + + private async triggerSessionEnd(reason: 'exit'): Promise { + await this.hookEngine.trigger('SessionEnd', { + matcherValue: reason, + inputData: { reason }, + }); + } +} + +export * from './subagent-host'; + +function initCompletionReminder(agentsMd: string): string { + const latest = + agentsMd.trim().length === 0 + ? 'No AGENTS.md content was found after `/init` completed.' + : agentsMd; + return [ + 'The user just ran `/init` slash command.', + 'The system has analyzed the codebase and generated an `AGENTS.md` file.', + '', + 'Latest AGENTS.md file content:', + latest, + ].join('\n'); +} diff --git a/packages/agent-core/src/session/prompt-metadata.ts b/packages/agent-core/src/session/prompt-metadata.ts new file mode 100644 index 000000000..94b417346 --- /dev/null +++ b/packages/agent-core/src/session/prompt-metadata.ts @@ -0,0 +1,62 @@ +import type { ActivateSkillPayload, PromptPayload } from '#/rpc'; +import type { ContentPart } from '@moonshot-ai/kosong'; + +const MAX_TITLE_LENGTH = 200; +const MAX_LAST_PROMPT_LENGTH = 4000; + +export function titleFromPromptMetadataText(text: string): string { + return text.slice(0, MAX_TITLE_LENGTH); +} + +export function promptMetadataTextFromPayload(payload: PromptPayload): string | undefined { + const parts: string[] = []; + for (const part of payload.input) { + const text = promptPartText(part); + if (text !== undefined) parts.push(text); + } + return sanitizeAndTruncatePromptText(parts.join('\n'), MAX_LAST_PROMPT_LENGTH); +} + +export function promptMetadataTextFromSkill(payload: ActivateSkillPayload): string | undefined { + const args = payload.args?.trim(); + return sanitizeAndTruncatePromptText( + args === undefined || args.length === 0 ? `/${payload.name}` : `/${payload.name} ${args}`, + MAX_LAST_PROMPT_LENGTH, + ); +} + +function promptPartText(part: ContentPart): string | undefined { + switch (part.type) { + case 'text': + return part.text; + case 'image_url': + return '[image]'; + case 'audio_url': + return '[audio]'; + case 'video_url': + return '[video]'; + case 'think': + return undefined; + } +} + +function sanitizeAndTruncatePromptText(text: string, maxLength: number): string | undefined { + const sanitized = text + .replaceAll( + /-----BEGIN [^-]*PRIVATE KEY-----[\s\S]*?-----END [^-]*PRIVATE KEY-----/gi, + '[redacted]', + ) + .replaceAll(/\b(authorization)\s*:\s*bearer\s+\S+/gi, '$1: Bearer [redacted]') + .replaceAll( + /\b(api[_-]?key|token|secret|password|passwd|pwd)\b\s*[:=]\s*(?:"[^"]*"|'[^']*'|\S+)/gi, + '$1=[redacted]', + ) + .replaceAll(/\bsk-[A-Za-z0-9_-]{12,}\b/g, '[redacted]') + .replaceAll(/\b[A-Za-z0-9][A-Za-z0-9+/=_-]{39,}\b/g, '[redacted]') + .replaceAll(/\p{Cc}+/gu, ' ') + .replaceAll(/\s+/g, ' ') + .trim(); + + if (sanitized.length === 0) return undefined; + return sanitized.slice(0, maxLength); +} diff --git a/packages/agent-core/src/session/rpc.ts b/packages/agent-core/src/session/rpc.ts new file mode 100644 index 000000000..be5eac82b --- /dev/null +++ b/packages/agent-core/src/session/rpc.ts @@ -0,0 +1,261 @@ +import { ErrorCodes, KimiError } from '#/errors'; +import type { + ActivateSkillPayload, + AgentAPI, + BeginCompactionPayload, + CancelPayload, + CancelPlanPayload, + EmptyPayload, + GetBackgroundOutputPathPayload, + GetBackgroundOutputPayload, + GetBackgroundPayload, + McpServerInfo, + McpStartupMetrics, + PromptPayload, + ReconnectMcpServerPayload, + RenameSessionPayload, + RegisterToolPayload, + SessionAPI, + SetActiveToolsPayload, + SetModelPayload, + SetPermissionPayload, + SetThinkingPayload, + SkillSummary, + SteerPayload, + StopBackgroundPayload, + UnregisterToolPayload, + UpdateSessionMetadataPayload, +} from '#/rpc'; +import type { PromisableMethods } from '#/utils/types'; + +import type { Session, SessionMeta } from '.'; +import { + promptMetadataTextFromPayload, + promptMetadataTextFromSkill, + titleFromPromptMetadataText, +} from './prompt-metadata'; + +type AgentScopedPayload = T & { agentId: string }; + +export class SessionAPIImpl implements PromisableMethods { + constructor(protected readonly session: Session) {} + + async renameSession(payload: RenameSessionPayload): Promise { + const title = payload.title.trim(); + if (title.length === 0) { + throw new KimiError(ErrorCodes.SESSION_TITLE_EMPTY, 'Session title cannot be empty'); + } + this.session.metadata = { + ...this.session.metadata, + title, + isCustomTitle: true, + updatedAt: new Date().toISOString(), + }; + await this.session.writeMetadata(); + } + + async updateSessionMetadata(payload: UpdateSessionMetadataPayload): Promise { + this.session.metadata = { + ...this.session.metadata, + ...payload.metadata, + agents: this.session.metadata.agents, + }; + await this.session.writeMetadata(); + } + + getSessionMetadata(_payload: EmptyPayload): SessionMeta { + return this.session.metadata; + } + + listSkills(_payload: EmptyPayload): Promise { + return this.session.listSkills(); + } + + listMcpServers(_payload: EmptyPayload): readonly McpServerInfo[] { + return this.session.mcp.list(); + } + + async getMcpStartupMetrics(_payload: EmptyPayload): Promise { + await this.session.mcp.waitForInitialLoad(); + return { durationMs: this.session.mcp.initialLoadDurationMs() }; + } + + async reconnectMcpServer(payload: ReconnectMcpServerPayload): Promise { + await this.session.mcp.reconnect(payload.name); + } + + generateAgentsMd(_payload: EmptyPayload): Promise { + return this.session.generateAgentsMd(); + } + + async prompt({ agentId, ...payload }: AgentScopedPayload) { + if (agentId === 'main') { + await this.updatePromptMetadata(promptMetadataTextFromPayload(payload)); + } + return this.getAgent(agentId).prompt(payload); + } + + steer({ agentId, ...payload }: AgentScopedPayload) { + return this.getAgent(agentId).steer(payload); + } + + cancel({ agentId, ...payload }: AgentScopedPayload) { + return this.getAgent(agentId).cancel(payload); + } + + setModel({ agentId, ...payload }: AgentScopedPayload) { + return this.getAgent(agentId).setModel(payload); + } + + setThinking({ agentId, ...payload }: AgentScopedPayload) { + return this.getAgent(agentId).setThinking(payload); + } + + setPermission({ agentId, ...payload }: AgentScopedPayload) { + return this.getAgent(agentId).setPermission(payload); + } + + getModel({ agentId, ...payload }: AgentScopedPayload) { + return this.getAgent(agentId).getModel(payload); + } + + enterPlan({ agentId, ...payload }: AgentScopedPayload) { + return this.getAgent(agentId).enterPlan(payload); + } + + cancelPlan({ agentId, ...payload }: AgentScopedPayload) { + return this.getAgent(agentId).cancelPlan(payload); + } + + clearPlan({ agentId, ...payload }: AgentScopedPayload) { + return this.getAgent(agentId).clearPlan(payload); + } + + beginCompaction({ agentId, ...payload }: AgentScopedPayload) { + return this.getAgent(agentId).beginCompaction(payload); + } + + cancelCompaction({ agentId, ...payload }: AgentScopedPayload) { + return this.getAgent(agentId).cancelCompaction(payload); + } + + registerTool({ agentId, ...payload }: AgentScopedPayload) { + return this.getAgent(agentId).registerTool(payload); + } + + unregisterTool({ agentId, ...payload }: AgentScopedPayload) { + return this.getAgent(agentId).unregisterTool(payload); + } + + setActiveTools({ agentId, ...payload }: AgentScopedPayload) { + return this.getAgent(agentId).setActiveTools(payload); + } + + stopBackground({ agentId, ...payload }: AgentScopedPayload) { + return this.getAgent(agentId).stopBackground(payload); + } + + clearContext({ agentId, ...payload }: AgentScopedPayload) { + return this.getAgent(agentId).clearContext(payload); + } + + async activateSkill({ agentId, ...payload }: AgentScopedPayload) { + await this.getAgent(agentId).activateSkill(payload); + if (agentId === 'main') { + await this.updatePromptMetadata(promptMetadataTextFromSkill(payload)); + } + } + + getBackgroundOutput({ agentId, ...payload }: AgentScopedPayload) { + return this.getAgent(agentId).getBackgroundOutput(payload); + } + + getBackgroundOutputPath({ + agentId, + ...payload + }: AgentScopedPayload) { + return this.getAgent(agentId).getBackgroundOutputPath(payload); + } + + getContext({ agentId, ...payload }: AgentScopedPayload) { + return this.getAgent(agentId).getContext(payload); + } + + getConfig({ agentId, ...payload }: AgentScopedPayload) { + return this.getAgent(agentId).getConfig(payload); + } + + getPermission({ agentId, ...payload }: AgentScopedPayload) { + return this.getAgent(agentId).getPermission(payload); + } + + getPlan({ agentId, ...payload }: AgentScopedPayload) { + return this.getAgent(agentId).getPlan(payload); + } + + getUsage({ agentId, ...payload }: AgentScopedPayload) { + return this.getAgent(agentId).getUsage(payload); + } + + getTools({ agentId, ...payload }: AgentScopedPayload) { + return this.getAgent(agentId).getTools(payload); + } + + getBackground({ agentId, ...payload }: AgentScopedPayload) { + return this.getAgent(agentId).getBackground(payload); + } + + private getAgent(agentId: string): PromisableMethods { + const agent = this.session.agents.get(agentId); + if (agent === undefined) { + throw new KimiError(ErrorCodes.AGENT_NOT_FOUND, `Agent "${agentId}" was not found`); + } + return agent.rpcMethods; + } + + private needUpdateEasyTitle(metadata: SessionMeta): boolean { + if (hasCustomTitle(metadata)) return false; + if (!isUntitled(metadata.title)) return false; + return true; + } + + private async updatePromptMetadata(lastPrompt: string | undefined): Promise { + if (lastPrompt === undefined) return; + + const title = this.needUpdateEasyTitle(this.session.metadata) + ? titleFromPromptMetadataText(lastPrompt) + : undefined; + const now = new Date().toISOString(); + const nextMetadata = { + ...this.session.metadata, + lastPrompt, + updatedAt: now, + }; + if (title !== undefined) { + nextMetadata.title = title; + nextMetadata.isCustomTitle = false; + } + + this.session.metadata = nextMetadata; + await this.session.writeMetadata(); + await this.session.rpc.emitEvent({ + type: 'session.meta.updated', + agentId: 'main', + title, + patch: { + title, + isCustomTitle: title === undefined ? undefined : false, + lastPrompt, + }, + }); + } +} + +function isUntitled(title: unknown): boolean { + return typeof title !== 'string' || title.trim().length === 0 || title === 'New Session'; +} + +function hasCustomTitle(metadata: SessionMeta): boolean { + if (metadata.isCustomTitle) return true; + return typeof (metadata as SessionMeta & { customTitle?: unknown }).customTitle === 'string'; +} diff --git a/packages/agent-core/src/session/store/index.ts b/packages/agent-core/src/session/store/index.ts new file mode 100644 index 000000000..54bba0465 --- /dev/null +++ b/packages/agent-core/src/session/store/index.ts @@ -0,0 +1,8 @@ +export { SessionStore } from '#/session/store/session-store'; +export type { + CreateSessionRecordInput, + ForkSessionRecordInput, + SessionStoreOptions, +} from '#/session/store/session-store'; +export { sessionIndexPath } from '#/session/store/session-index'; +export { encodeWorkDirKey, normalizeWorkDir } from '#/session/store/workdir-key'; diff --git a/packages/agent-core/src/session/store/session-index.ts b/packages/agent-core/src/session/store/session-index.ts new file mode 100644 index 000000000..6b77ed007 --- /dev/null +++ b/packages/agent-core/src/session/store/session-index.ts @@ -0,0 +1,79 @@ +import { appendFile, mkdir, readFile } from 'node:fs/promises'; +import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path'; + +export interface SessionIndexEntry { + readonly sessionId: string; + readonly sessionDir: string; + readonly workDir: string; +} + +export function sessionIndexPath(homeDir: string): string { + return join(homeDir, 'session_index.jsonl'); +} + +export async function appendSessionIndexEntry( + homeDir: string, + entry: SessionIndexEntry, +): Promise { + const indexPath = sessionIndexPath(homeDir); + await mkdir(dirname(indexPath), { recursive: true, mode: 0o700 }); + await appendFile(indexPath, `${JSON.stringify(entry)}\n`, 'utf-8'); +} + +export async function readSessionIndex( + homeDir: string, + sessionsDir: string, +): Promise> { + let raw: string; + try { + raw = await readFile(sessionIndexPath(homeDir), 'utf-8'); + } catch { + return new Map(); + } + + const result = new Map(); + for (const line of raw.split(/\r?\n/)) { + const trimmed = line.trim(); + if (trimmed === '') continue; + const entry = parseIndexLine(trimmed); + if (entry === undefined) continue; + const sessionDir = resolve(entry.sessionDir); + if (!isAbsolute(entry.sessionDir)) continue; + if (!isAbsolute(entry.workDir)) continue; + if (!isPathInside(sessionsDir, sessionDir)) continue; + if (basename(sessionDir) !== entry.sessionId) continue; + result.set(entry.sessionId, { + sessionId: entry.sessionId, + sessionDir, + workDir: resolve(entry.workDir), + }); + } + return result; +} + +function parseIndexLine(line: string): SessionIndexEntry | undefined { + try { + const parsed = JSON.parse(line) as unknown; + if (typeof parsed !== 'object' || parsed === null) return undefined; + const entry = parsed as Partial; + if ( + typeof entry.sessionId !== 'string' || + typeof entry.sessionDir !== 'string' || + typeof entry.workDir !== 'string' + ) { + return undefined; + } + return { + sessionId: entry.sessionId, + sessionDir: entry.sessionDir, + workDir: entry.workDir, + }; + } catch { + return undefined; + } +} + +function isPathInside(parent: string, child: string): boolean { + const rel = relative(resolve(parent), resolve(child)); + return rel !== '' && !rel.startsWith('..') && !isAbsolute(rel); +} diff --git a/packages/agent-core/src/session/store/session-store.ts b/packages/agent-core/src/session/store/session-store.ts new file mode 100644 index 000000000..88a1bfcd0 --- /dev/null +++ b/packages/agent-core/src/session/store/session-store.ts @@ -0,0 +1,366 @@ +import { cp, mkdir, readdir, readFile, rm, stat, writeFile } from 'node:fs/promises'; +import { dirname, isAbsolute, join, relative } from 'node:path'; + +import { z } from 'zod'; + +import { ErrorCodes, KimiError } from '#/errors'; +import type { SessionIndexEntry } from '#/session/store/session-index'; +import { appendSessionIndexEntry, readSessionIndex } from '#/session/store/session-index'; +import { encodeWorkDirKey, normalizeWorkDir } from '#/session/store/workdir-key'; +import type { JsonObject, ListSessionsPayload, SessionSummary } from '#/rpc/core-api'; + +const SessionSummaryStateSchema = z.object({ + customTitle: z.string().optional(), + isCustomTitle: z.boolean().optional(), + lastPrompt: z.string().optional(), + title: z.string().optional(), + custom: z.record(z.string(), z.unknown()).optional(), +}); + +type SessionSummaryState = z.infer; + +export interface CreateSessionRecordInput { + readonly id: string; + readonly workDir: string; +} + +export interface ForkSessionRecordInput { + readonly sourceId: string; + readonly targetId: string; + readonly title?: string; + readonly metadata?: JsonObject; +} + +export type SessionStoreOptions = Record; + +export class SessionStore { + readonly sessionsDir: string; + + constructor( + readonly homeDir: string, + _options: SessionStoreOptions = {}, + ) { + this.sessionsDir = join(homeDir, 'sessions'); + } + + sessionDirFor(input: { readonly id: string; readonly workDir: string }): string { + assertSafeSessionId(input.id); + return join(this.sessionsDir, encodeWorkDirKey(normalizeWorkDir(input.workDir)), input.id); + } + + async create(input: CreateSessionRecordInput): Promise { + assertSafeSessionId(input.id); + const workDir = normalizeWorkDir(input.workDir); + const indexed = await this.findSessionEntry(input.id); + if (indexed !== undefined) { + throw new KimiError(ErrorCodes.SESSION_ALREADY_EXISTS, `Session "${input.id}" already exists`); + } + + const dir = this.sessionDirFor({ id: input.id, workDir }); + if (await isDirectory(dir)) { + throw new KimiError(ErrorCodes.SESSION_ALREADY_EXISTS, `Session "${input.id}" already exists`); + } + + await mkdir(dir, { recursive: true, mode: 0o700 }); + await appendSessionIndexEntry(this.homeDir, { + sessionId: input.id, + sessionDir: dir, + workDir, + }); + return this.summaryFromDir(input.id, dir, workDir); + } + + async fork(input: ForkSessionRecordInput): Promise { + const source = await this.findExistingSessionEntry(input.sourceId); + assertSafeSessionId(input.targetId); + const indexed = await this.findSessionEntry(input.targetId); + if (indexed !== undefined) { + throw new KimiError(ErrorCodes.SESSION_ALREADY_EXISTS, `Session "${input.targetId}" already exists`); + } + + const targetDir = this.sessionDirFor({ id: input.targetId, workDir: source.workDir }); + if (await isDirectory(targetDir)) { + throw new KimiError(ErrorCodes.SESSION_ALREADY_EXISTS, `Session "${input.targetId}" already exists`); + } + + await mkdir(dirname(targetDir), { recursive: true, mode: 0o700 }); + try { + await cp(source.sessionDir, targetDir, { + recursive: true, + force: false, + errorOnExist: true, + }); + await this.writeForkedState(input, source.sessionDir, targetDir); + const summary = await this.summaryFromDir(input.targetId, targetDir, source.workDir); + await appendSessionIndexEntry(this.homeDir, { + sessionId: input.targetId, + sessionDir: targetDir, + workDir: source.workDir, + }); + return summary; + } catch (error) { + await rm(targetDir, { recursive: true, force: true }).catch(() => {}); + throw error; + } + } + + async get(id: string): Promise { + const entry = await this.findExistingSessionEntry(id); + return this.summaryFromDir(id, entry.sessionDir, entry.workDir); + } + + async rename(id: string, title: string): Promise { + const normalized = title.trim(); + if (normalized.length === 0) { + throw new KimiError(ErrorCodes.SESSION_TITLE_EMPTY, 'Session title cannot be empty'); + } + const entry = await this.findExistingSessionEntry(id); + const statePath = join(entry.sessionDir, 'state.json'); + let parsed: unknown; + try { + parsed = JSON.parse(await readFile(statePath, 'utf-8')) as unknown; + } catch (error) { + throw new KimiError(ErrorCodes.SESSION_STATE_NOT_FOUND, `Session "${id}" state.json was not found`, { + cause: error, + }); + } + if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) { + throw new KimiError(ErrorCodes.SESSION_STATE_INVALID, `Session "${id}" state.json is invalid`); + } + const next: Record = { + ...(parsed as Record), + title: normalized, + isCustomTitle: true, + }; + await writeFile(statePath, `${JSON.stringify(next, null, 2)}\n`, 'utf-8'); + } + + async list(options: ListSessionsPayload): Promise { + const workDir = normalizeWorkDir(options.workDir); + const bucketDir = join(this.sessionsDir, encodeWorkDirKey(workDir)); + let entries; + try { + entries = await readdir(bucketDir, { withFileTypes: true }); + } catch { + return []; + } + + const sessions: SessionSummary[] = []; + for (const entry of entries) { + if (!entry.isDirectory()) continue; + const id = entry.name; + if (!isSafeSessionId(id)) continue; + const dir = join(bucketDir, id); + sessions.push(await this.summaryFromDir(id, dir, workDir)); + } + sessions.sort(compareSessionSummary); + return sessions; + } + + async assertDirectory(id: string): Promise { + return (await this.findExistingSessionEntry(id)).sessionDir; + } + + private async findSessionEntry(id: string): Promise { + if (!isSafeSessionId(id)) return undefined; + const index = await readSessionIndex(this.homeDir, this.sessionsDir); + return index.get(id); + } + + private async findExistingSessionEntry(id: string): Promise { + const entry = await this.findSessionEntry(id); + if (entry !== undefined && (await isDirectory(entry.sessionDir))) return entry; + throw new KimiError(ErrorCodes.SESSION_NOT_FOUND, `Session "${id}" was not found`, { + details: { sessionId: id }, + }); + } + + private async writeForkedState( + input: ForkSessionRecordInput, + sourceDir: string, + targetDir: string, + ): Promise { + const statePath = join(targetDir, 'state.json'); + let parsed: unknown; + try { + parsed = JSON.parse(await readFile(statePath, 'utf-8')) as unknown; + } catch (error) { + throw new KimiError( + ErrorCodes.SESSION_STATE_NOT_FOUND, + `Session "${input.sourceId}" state.json was not found`, + { + cause: error, + }, + ); + } + if (!isRecord(parsed)) { + throw new KimiError( + ErrorCodes.SESSION_STATE_INVALID, + `Session "${input.sourceId}" state.json is invalid`, + ); + } + + const title = normalizeForkTitle(input.title, parsed['title']); + const now = new Date().toISOString(); + const next: Record = { + ...parsed, + createdAt: now, + updatedAt: now, + title, + isCustomTitle: input.title === undefined ? parsed['isCustomTitle'] === true : true, + forkedFrom: input.sourceId, + agents: rewriteAgentHomedirs(parsed['agents'], sourceDir, targetDir), + custom: Object.assign({}, isRecord(parsed['custom']) ? parsed['custom'] : {}, input.metadata), + }; + await writeFile(statePath, `${JSON.stringify(next, null, 2)}\n`, 'utf-8'); + } + + private async summaryFromDir( + id: string, + sessionDir: string, + workDir: string, + ): Promise { + const dirStat = await stat(sessionDir); + const state = await readOptionalState(sessionDir); + const [stateInfo, wireInfo, agentsWireMtime] = await Promise.all([ + statIfExists(join(sessionDir, 'state.json')), + statIfExists(join(sessionDir, 'wire.jsonl')), + latestAgentWireMtime(sessionDir), + ]); + return { + id, + workDir, + sessionDir, + createdAt: timestampOrFallback(dirStat.birthtimeMs, dirStat.ctimeMs), + updatedAt: Math.max( + dirStat.mtimeMs, + stateInfo?.mtimeMs ?? 0, + wireInfo?.mtimeMs ?? 0, + agentsWireMtime ?? 0, + ), + title: titleFromState(state), + lastPrompt: state?.lastPrompt, + metadata: metadataFromState(state), + }; + } +} + +function metadataFromState(state: SessionSummaryState | undefined): JsonObject | undefined { + if (state === undefined || state.custom === undefined) return undefined; + return state.custom as JsonObject; +} + +async function latestAgentWireMtime(sessionDir: string): Promise { + const agentsDir = join(sessionDir, 'agents'); + let entries; + try { + entries = await readdir(agentsDir, { withFileTypes: true }); + } catch { + return undefined; + } + + let latest = 0; + for (const entry of entries) { + if (!entry.isDirectory()) continue; + const wireInfo = await statIfExists(join(agentsDir, entry.name, 'wire.jsonl')); + latest = Math.max(latest, wireInfo?.mtimeMs ?? 0); + } + return latest > 0 ? latest : undefined; +} + +function titleFromState(state: SessionSummaryState | undefined): string | undefined { + if (state === undefined) return undefined; + if (typeof state.isCustomTitle === 'boolean' && typeof state.title === 'string') { + return state.title; + } + if (typeof state.customTitle === 'string') return state.customTitle; + return typeof state.title === 'string' ? state.title : undefined; +} + +async function readOptionalState(sessionDir: string): Promise { + try { + const parsed = JSON.parse(await readFile(join(sessionDir, 'state.json'), 'utf-8')) as unknown; + const result = SessionSummaryStateSchema.safeParse(parsed); + return result.success ? result.data : undefined; + } catch { + return undefined; + } +} + +function normalizeForkTitle(title: string | undefined, fallback: unknown): string { + if (title !== undefined) { + const normalized = title.trim(); + if (normalized.length === 0) { + throw new KimiError(ErrorCodes.SESSION_TITLE_EMPTY, 'Session title cannot be empty'); + } + return normalized; + } + return typeof fallback === 'string' && fallback.trim().length > 0 ? fallback : 'New Session'; +} + +function rewriteAgentHomedirs(value: unknown, sourceDir: string, targetDir: string): unknown { + if (!isRecord(value)) return {}; + + const agents: Record = {}; + for (const [agentId, agentMeta] of Object.entries(value)) { + if (!isRecord(agentMeta)) { + agents[agentId] = agentMeta; + continue; + } + const homedir = agentMeta['homedir']; + agents[agentId] = { + ...agentMeta, + homedir: + typeof homedir === 'string' ? remapSessionPath(homedir, sourceDir, targetDir) : homedir, + }; + } + return agents; +} + +function remapSessionPath(value: string, sourceDir: string, targetDir: string): string { + const rel = relative(sourceDir, value); + if (rel === '') return targetDir; + if (rel.startsWith('..') || isAbsolute(rel)) return value; + return join(targetDir, rel); +} + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + +async function statIfExists(path: string): Promise<{ readonly mtimeMs: number } | undefined> { + try { + return await stat(path); + } catch { + return undefined; + } +} + +async function isDirectory(path: string): Promise { + try { + return (await stat(path)).isDirectory(); + } catch { + return false; + } +} + +function timestampOrFallback(value: number, fallback: number): number { + return Number.isFinite(value) && value > 0 ? value : fallback; +} + +function assertSafeSessionId(id: string): void { + if (isSafeSessionId(id)) return; + throw new KimiError(ErrorCodes.SESSION_ID_INVALID, 'Session id contains unsupported path characters'); +} + +function isSafeSessionId(id: string): boolean { + return /^[A-Za-z0-9._-]+$/.test(id) && id !== '.' && id !== '..'; +} + +function compareSessionSummary(a: SessionSummary, b: SessionSummary): number { + if (a.updatedAt !== b.updatedAt) return b.updatedAt - a.updatedAt; + if (a.createdAt !== b.createdAt) return b.createdAt - a.createdAt; + if (a.id < b.id) return -1; + if (a.id > b.id) return 1; + return 0; +} diff --git a/packages/agent-core/src/session/store/workdir-key.ts b/packages/agent-core/src/session/store/workdir-key.ts new file mode 100644 index 000000000..2b0aaafbd --- /dev/null +++ b/packages/agent-core/src/session/store/workdir-key.ts @@ -0,0 +1,18 @@ +import { createHash } from 'node:crypto'; +import { basename, resolve } from 'node:path'; + +import { slugifyWorkDirName } from '#/utils/workdir-slug'; + +const WORKDIR_KEY_PREFIX = 'wd_'; +const HASH_LENGTH = 12; + +export function normalizeWorkDir(workDir: string): string { + return resolve(workDir); +} + +export function encodeWorkDirKey(workDir: string): string { + const normalized = normalizeWorkDir(workDir); + const slug = slugifyWorkDirName(basename(normalized)); + const hash = createHash('sha256').update(normalized).digest('hex').slice(0, HASH_LENGTH); + return `${WORKDIR_KEY_PREFIX}${slug}_${hash}`; +} diff --git a/packages/agent-core/src/session/subagent-host.ts b/packages/agent-core/src/session/subagent-host.ts new file mode 100644 index 000000000..20b744ec2 --- /dev/null +++ b/packages/agent-core/src/session/subagent-host.ts @@ -0,0 +1,345 @@ +import type { TokenUsage } from '@moonshot-ai/kosong'; + +import type { Agent } from '../agent'; +import type { PromptOrigin } from '../agent/context'; +import type { LoopTurnStopReason } from '../loop'; +import { + DEFAULT_AGENT_PROFILES, + prepareSystemPromptContext, + type ResolvedAgentProfile, +} from '../profile'; +import { linkAbortSignal } from '../utils/abort'; +import { collectGitContext } from './git-context'; +import type { Session } from './index'; +import SUMMARY_CONTINUATION_PROMPT from './summary-continuation.md'; + +/** + * A subagent summary shorter than this many characters triggers one + * follow-up turn that asks the subagent to expand it, so the parent + * agent receives a technically complete handoff. + */ +const SUMMARY_MIN_LENGTH = 200; +const SUMMARY_CONTINUATION_ATTEMPTS = 1; +const HOOK_TEXT_PREVIEW_LENGTH = 500; +const SUBAGENT_MAX_TOKENS_ERROR = + 'Subagent turn failed before completing its final summary: reason=max_tokens'; + +type RunSubagentOptions = { + readonly parentToolCallId: string; + readonly parentToolCallUuid?: string | undefined; + readonly prompt: string; + readonly description: string; + readonly runInBackground: boolean; + readonly origin?: PromptOrigin | undefined; + readonly signal: AbortSignal; +}; + +type SubagentCompletion = { + readonly result: string; + readonly usage?: TokenUsage; +}; + +type ActiveChild = { + readonly controller: AbortController; + readonly runInBackground: boolean; +}; + +export type SubagentHandle = { + readonly agentId: string; + readonly profileName: string; + readonly resumed: boolean; + readonly completion: Promise; +}; + +export class SessionSubagentHost { + private readonly activeChildren = new Map(); + + constructor( + private readonly session: Session, + private readonly ownerAgentId: string, + readonly backgroundTaskTimeoutMs?: number | undefined, + ) {} + + async spawn(profileName: string, options: RunSubagentOptions): Promise { + options.signal.throwIfAborted(); + + const parent = this.session.agents.get(this.ownerAgentId); + if (parent === undefined) { + throw new Error(`Parent agent "${this.ownerAgentId}" was not found`); + } + + const profile = this.resolveProfile(parent, profileName); + const { id, agent } = await this.session.createAgent( + { type: 'sub', generate: parent.rawGenerate }, + undefined, + this.ownerAgentId, + ); + const controller = new AbortController(); + const unlinkAbortSignal = linkAbortSignal(options.signal, controller); + this.activeChildren.set(id, { + controller, + runInBackground: options.runInBackground, + }); + + const completion = this.runChild( + parent, + id, + agent, + profile.name, + { + ...options, + signal: controller.signal, + }, + () => this.configureChild(parent, agent, profile), + ).finally(() => { + unlinkAbortSignal(); + this.activeChildren.delete(id); + }); + + return { + agentId: id, + profileName: profile.name, + resumed: false, + completion, + }; + } + + async resume(agentId: string, options: RunSubagentOptions): Promise { + options.signal.throwIfAborted(); + + const parent = this.session.agents.get(this.ownerAgentId); + if (parent === undefined) { + throw new Error(`Parent agent "${this.ownerAgentId}" was not found`); + } + + const child = this.session.agents.get(agentId); + if (child === undefined) { + throw new Error(`Agent instance "${agentId}" was not found`); + } + const metadata = this.session.metadata.agents[agentId]; + if (metadata?.type !== 'sub') { + throw new Error(`Agent instance "${agentId}" is not a subagent`); + } + if (metadata.parentAgentId !== this.ownerAgentId) { + throw new Error(`Agent instance "${agentId}" does not belong to this parent agent`); + } + if (this.activeChildren.has(agentId) || child.turn.hasActiveTurn) { + throw new Error( + `Agent instance "${agentId}" is already running and cannot be resumed concurrently`, + ); + } + + const profileName = child.config.profileName ?? 'subagent'; + + const controller = new AbortController(); + const unlinkAbortSignal = linkAbortSignal(options.signal, controller); + this.activeChildren.set(agentId, { + controller, + runInBackground: options.runInBackground, + }); + + const completion = this.runChild( + parent, + agentId, + child, + profileName, + { + ...options, + signal: controller.signal, + }, + // A resumed subagent is realigned to the parent agent's current model, + // so a parent setModel between the initial spawn and the resume is + // reflected — a subagent always uses the parent agent's model. + () => { + child.config.update({ modelAlias: parent.config.modelAlias }); + return Promise.resolve(); + }, + ).finally(() => { + unlinkAbortSignal(); + this.activeChildren.delete(agentId); + }); + + return { + agentId, + profileName, + resumed: true, + completion, + }; + } + + cancelAll(): void { + const foregroundChildren = Array.from(this.activeChildren).filter( + ([, child]) => !child.runInBackground, + ); + for (const [childId, child] of foregroundChildren) { + this.session.agents.get(childId)?.subagentHost?.cancelAll(); + child.controller.abort(); + } + } + + getProfileName(agentId: string): string | undefined { + const metadata = this.session.metadata.agents[agentId]; + if (metadata?.type !== 'sub' || metadata.parentAgentId !== this.ownerAgentId) { + return undefined; + } + return this.session.agents.get(agentId)?.config.profileName; + } + + private resolveProfile(parent: Agent, profileName: string): ResolvedAgentProfile { + const profile = + DEFAULT_AGENT_PROFILES[parent.config.profileName ?? 'agent']?.subagents?.[profileName] ?? + DEFAULT_AGENT_PROFILES['agent']?.subagents?.[profileName]; + if (profile === undefined) { + throw new Error(`Subagent profile "${profileName}" was not found`); + } + return profile; + } + + private async runChild( + parent: Agent, + childId: string, + child: Agent, + profileName: string, + options: RunSubagentOptions, + prepareChild: () => Promise, + ): Promise { + parent.emitEvent({ + type: 'subagent.spawned', + subagentId: childId, + subagentName: profileName, + parentToolCallId: options.parentToolCallId, + parentToolCallUuid: options.parentToolCallUuid, + parentAgentId: this.ownerAgentId, + description: options.description, + runInBackground: options.runInBackground, + }); + parent.telemetry.track('subagent_created', { + subagent_name: profileName, + run_in_background: options.runInBackground, + }); + + try { + await prepareChild(); + options.signal.throwIfAborted(); + await this.triggerSubagentStart(parent, profileName, options.prompt, options.signal); + options.signal.throwIfAborted(); + + // Explore subagents start cold; a git-context block helps them orient + // in the repository before searching. + let childPrompt = options.prompt; + if (profileName === 'explore') { + const gitContext = await collectGitContext(child.runtime.kaos, child.config.cwd); + if (gitContext) childPrompt = `${gitContext}\n\n${childPrompt}`; + } + const origin: PromptOrigin = options.origin ?? { kind: 'system_trigger', name: 'subagent' }; + child.turn.prompt([{ type: 'text', text: childPrompt }], origin); + await runChildTurnToCompletion(child, options.signal); + + // A subagent that returns an overly terse summary leaves the parent + // agent under-informed. Give it a bounded number of chances to expand + // the handoff; if it is still short after that, accept it as-is rather + // than retrying indefinitely. + let result = lastAssistantText(child); + let remainingContinuations = SUMMARY_CONTINUATION_ATTEMPTS; + while (remainingContinuations > 0 && result.length < SUMMARY_MIN_LENGTH) { + remainingContinuations -= 1; + options.signal.throwIfAborted(); + child.turn.prompt([{ type: 'text', text: SUMMARY_CONTINUATION_PROMPT }], origin); + await runChildTurnToCompletion(child, options.signal); + result = lastAssistantText(child); + } + const usage = child.usage.data().total; + parent.emitEvent({ + type: 'subagent.completed', + subagentId: childId, + parentToolCallId: options.parentToolCallId, + resultSummary: result, + usage, + }); + this.triggerSubagentStop(parent, profileName, result); + return { result, usage }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + parent.emitEvent({ + type: 'subagent.failed', + subagentId: childId, + parentToolCallId: options.parentToolCallId, + error: message, + }); + throw error; + } + } + + private async configureChild( + parent: Agent, + child: Agent, + profile: ResolvedAgentProfile, + ): Promise { + // A subagent always inherits the parent agent's model. + child.config.update({ + cwd: parent.config.cwd, + modelAlias: parent.config.modelAlias, + thinkingLevel: parent.config.thinkingLevel, + }); + + const context = await prepareSystemPromptContext(child.runtime.kaos, child.config.cwd); + child.useProfile(profile, context); + } + + private async triggerSubagentStart( + parent: Agent, + profileName: string, + prompt: string, + signal: AbortSignal, + ): Promise { + await parent.hooks?.trigger('SubagentStart', { + matcherValue: profileName, + signal, + inputData: { + agentName: profileName, + prompt: prompt.slice(0, HOOK_TEXT_PREVIEW_LENGTH), + }, + }); + } + + private triggerSubagentStop(parent: Agent, profileName: string, result: string): void { + void parent.hooks?.fireAndForgetTrigger('SubagentStop', { + matcherValue: profileName, + inputData: { + agentName: profileName, + response: result.slice(0, HOOK_TEXT_PREVIEW_LENGTH), + }, + }); + } +} + +async function runChildTurnToCompletion(child: Agent, signal: AbortSignal): Promise { + const completion = await child.turn.waitForCurrentTurn(signal); + const turnEnded = completion.event; + if (turnEnded.reason !== 'completed') { + throw new Error( + turnEnded.error === undefined + ? `Subagent turn ${turnEnded.reason}` + : `[${turnEnded.error.code}] ${turnEnded.error.message}`, + ); + } + throwIfSubagentStoppedAtMaxTokens(completion.stopReason); +} + +function throwIfSubagentStoppedAtMaxTokens(stopReason: LoopTurnStopReason | undefined): void { + if (stopReason === 'max_tokens') { + throw new Error(`${SUBAGENT_MAX_TOKENS_ERROR}.`); + } +} + +function lastAssistantText(agent: Agent): string { + for (const message of [...agent.context.history].toReversed()) { + if (message.role !== 'assistant') continue; + const text = message.content + .filter((part) => part.type === 'text') + .map((part) => part.text) + .join(''); + if (text.trim().length > 0) return text.trim(); + } + return ''; +} diff --git a/packages/agent-core/src/session/summary-continuation.md b/packages/agent-core/src/session/summary-continuation.md new file mode 100644 index 000000000..8efb589a5 --- /dev/null +++ b/packages/agent-core/src/session/summary-continuation.md @@ -0,0 +1,5 @@ +Your previous response was too brief. Please provide a more comprehensive summary that includes: + +1. Specific technical details and implementations +2. Detailed findings and analysis +3. All important information that the parent agent should know \ No newline at end of file diff --git a/packages/agent-core/src/skill/builtin/index.ts b/packages/agent-core/src/skill/builtin/index.ts new file mode 100644 index 000000000..e921ec1bf --- /dev/null +++ b/packages/agent-core/src/skill/builtin/index.ts @@ -0,0 +1,8 @@ +import type { SkillRegistry } from '../registry'; +import { MCP_CONFIG_SKILL } from './mcp-config'; + +export function registerBuiltinSkills(registry: SkillRegistry): void { + registry.registerBuiltinSkill(MCP_CONFIG_SKILL); +} + +export { MCP_CONFIG_SKILL }; diff --git a/packages/agent-core/src/skill/builtin/mcp-config.md b/packages/agent-core/src/skill/builtin/mcp-config.md new file mode 100644 index 000000000..bf20d847a --- /dev/null +++ b/packages/agent-core/src/skill/builtin/mcp-config.md @@ -0,0 +1,96 @@ +--- +name: mcp-config +description: Configure MCP servers and handle MCP OAuth login. +--- + +# Interactive MCP server configuration + +The user invoked this skill through `/mcp-config` or `/skill:mcp-config`. +Either they want to log into an MCP server that asked for OAuth, or they +want to edit the `mcp.json` that lists MCP servers. The work is small and +local — handle it on this turn yourself, no agents or planning todos. + +Pick the flow from the user's message and your tool list: + +- An `mcp____authenticate` tool is in your list, the user says + "log in" / "auth" / "sign in", they invoke `/mcp-config login + `, or they quote a `needs-auth` status → **Login**. +- Add / edit / remove / list of an `mcp.json` entry → **Config edit**. +- Bare `/mcp-config` with no `authenticate` tool in your list → + **Config edit**. If there were a pending login, the authenticate tool + would be in your list. + +## Login + +Each MCP server in `needs-auth` exposes one `mcp____authenticate` +tool. Call it for the server the user means — its own description owns +the OAuth UX (printing the URL, blocking on the callback, reconnecting on +success). Surface its output verbatim, including the authorization URL +unchanged; the URL contains state and PKCE parameters that break if +edited. + +If the user named a server that has no authenticate tool, say so in one +sentence and stop — do **not** fall into config edit. They're trying to +log in to a server that isn't currently waiting for login; quietly +rewriting `mcp.json` would be the wrong fix. If multiple authenticate +tools exist and the user didn't name one, ask which. + +## Config edit + +Config lives in two files; on key collision the project file overrides +the user-global one: + +- User-global: `~/.kimi-code/mcp.json` (or `$KIMI_CODE_HOME/mcp.json` if + set). Use for servers you want everywhere. +- Project-local: `/.kimi-code/mcp.json`. Mention once that stdio + entries spawn commands at session start, so this should only live in + trusted repos. + +Both files wrap their entries the same way: + +```json +{ "mcpServers": { "": { /* entry */ } } } +``` + +A minimal stdio entry needs `command` (+ optional `args`, `env`, `cwd`). +A minimal http entry needs `url`; add `bearerTokenEnvVar: "ENV_NAME"` for +servers that authenticate with a static bearer token from the +environment. Servers that use OAuth take no token field — the login flow +above handles them. `transport` is inferred from `command` vs `url`, so +omit it. For less common fields (`enabled`, `startupTimeoutMs`, +`toolTimeoutMs`, `enabledTools`, `disabledTools`, `headers`) the source of +truth is `McpServerStdioConfigSchema` / `McpServerHttpConfigSchema` in +`packages/agent-core/src/config/schema.ts`. + +If the user only wants to **see** what's configured, read both files, +show a merged view, and stop — no scope prompt, no write. + +For changes, the flow is: + +1. **Pick a scope.** Infer it from the user's words when you can + (project / repo / this checkout / cwd → project; global / everywhere / + all projects → user-global). When the request is genuinely scope-less, + use one `AskUserQuestion` to ask user-global vs project-local, defaulting + to user-global. Use plain text for every other question — `AskUserQuestion` + is a poor fit for free-form input. If the user dismisses the scope + question, stop; you can't safely guess where they wanted the change. +2. **Read and announce.** Read the target file (a missing or empty file + is fine; you'll create `{ "mcpServers": {} }`). If JSON parsing fails, + surface the error verbatim and stop — silently overwriting a broken + file could destroy work. Then show the user the target path, what's + currently in it, and the entry you're about to write or delete. This + is for transparency, not a confirmation gate — the Edit/Write + permission prompt is the real gate, and your message is what gives + the user context when that prompt appears. In yolo / afk modes there + is no prompt, which is those modes' explicit contract. +3. **Write and tell them how to reload MCP servers.** Preserve unrelated + entries and the `mcpServers` wrapper. MCP servers load at session + start, so tell the user to start a new session (for example `/new`) or + restart `kimi-code` for the change to take effect. + +## Secrets + +Don't store secrets (tokens, keys, passwords) as literals in +`mcp.json` — it's a plain config file on disk. http servers should use +`bearerTokenEnvVar` to reference an env var instead; if a stdio entry +must inline one in `env`, warn the user before writing. diff --git a/packages/agent-core/src/skill/builtin/mcp-config.ts b/packages/agent-core/src/skill/builtin/mcp-config.ts new file mode 100644 index 000000000..36e60c1c7 --- /dev/null +++ b/packages/agent-core/src/skill/builtin/mcp-config.ts @@ -0,0 +1,23 @@ +import { parseSkillText } from '../parser'; +import type { SkillDefinition } from '../types'; +import MCP_CONFIG_BODY from './mcp-config.md'; + +const PSEUDO_PATH = 'builtin://mcp-config'; + +const parsed = parseSkillText({ + skillMdPath: '/builtin/skills/mcp-config.md', + skillDirName: 'mcp-config', + source: 'builtin', + text: MCP_CONFIG_BODY, +}); + +export const MCP_CONFIG_SKILL: SkillDefinition = { + ...parsed, + path: PSEUDO_PATH, + dir: PSEUDO_PATH, + metadata: { + ...parsed.metadata, + type: parsed.metadata.type ?? 'inline', + disableModelInvocation: true, + }, +}; diff --git a/packages/agent-core/src/skill/index.ts b/packages/agent-core/src/skill/index.ts new file mode 100644 index 000000000..924027bfa --- /dev/null +++ b/packages/agent-core/src/skill/index.ts @@ -0,0 +1,5 @@ +export * from './builtin'; +export * from './parser'; +export * from './registry'; +export * from './scanner'; +export * from './types'; diff --git a/packages/agent-core/src/skill/parser.ts b/packages/agent-core/src/skill/parser.ts new file mode 100644 index 000000000..f308112b2 --- /dev/null +++ b/packages/agent-core/src/skill/parser.ts @@ -0,0 +1,295 @@ +import { readFile } from 'node:fs/promises'; +import path from 'node:path'; + +import { load as loadYaml } from 'js-yaml'; +import regexpEscape from 'regexp.escape'; + +import type { SkillDefinition, SkillMetadata, SkillSource } from './types'; +import { isSupportedSkillType } from './types'; + +export class FrontmatterError extends Error { + constructor(message: string, cause?: unknown) { + super(message); + this.name = 'FrontmatterError'; + if (cause !== undefined) { + Object.defineProperty(this, 'cause', { value: cause, configurable: true }); + } + } +} + +export class SkillParseError extends Error { + readonly reason?: unknown; + + constructor(message: string, cause?: unknown) { + super(message); + this.name = 'SkillParseError'; + if (cause !== undefined) this.reason = cause; + } +} + +export class UnsupportedSkillTypeError extends Error { + readonly skillType: string; + + constructor(skillType: string) { + super( + `Skill type "${skillType}" is not supported; only "prompt", "inline", and "flow" are supported.`, + ); + this.name = 'UnsupportedSkillTypeError'; + this.skillType = skillType; + } +} + +export interface ParseSkillOptions { + readonly skillMdPath: string; + readonly skillDirName: string; + readonly source: SkillSource; +} + +export interface ParseSkillTextOptions extends ParseSkillOptions { + readonly text: string; +} + +export interface SkillExpandContext { + readonly skillDir: string; + readonly sessionId?: string; + readonly argumentNames?: readonly string[]; +} + +export interface ParsedFrontmatter { + readonly data: unknown; + readonly body: string; +} + +const FENCE = '---'; +const METADATA_ALIASES: Readonly> = { + 'when-to-use': 'whenToUse', + when_to_use: 'whenToUse', + 'disable-model-invocation': 'disableModelInvocation', + disable_model_invocation: 'disableModelInvocation', +}; + +export async function parseSkillFromFile(options: ParseSkillOptions): Promise { + let text: string; + try { + text = await readFile(options.skillMdPath, 'utf8'); + } catch (error) { + throw new SkillParseError(`Failed to read ${options.skillMdPath}`, error); + } + return parseSkillText({ ...options, text }); +} + +export function parseFrontmatter(text: string): ParsedFrontmatter { + const lines = text.split(/\r?\n/); + if (lines[0]?.trim() !== FENCE) { + return { data: null, body: text }; + } + + const close = lines.findIndex((line, index) => index > 0 && line.trim() === FENCE); + if (close === -1) { + throw new FrontmatterError('Missing closing frontmatter fence'); + } + + const yamlText = lines.slice(1, close).join('\n').trim(); + const body = lines.slice(close + 1).join('\n'); + if (yamlText === '') { + return { data: {}, body }; + } + + try { + return { data: loadYaml(yamlText) ?? {}, body }; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + throw new FrontmatterError(message, error); + } +} + +export function parseSkillText(options: ParseSkillTextOptions): SkillDefinition { + const isDirectorySkill = path.basename(options.skillMdPath) === 'SKILL.md'; + if (isDirectorySkill && options.text.split(/\r?\n/, 1)[0]?.trim() !== FENCE) { + throw new SkillParseError(`Missing frontmatter in ${options.skillMdPath}`); + } + + let parsed; + try { + parsed = parseFrontmatter(options.text); + } catch (error) { + if (error instanceof FrontmatterError) { + throw new SkillParseError( + `Invalid frontmatter in ${options.skillMdPath}: ${error.message}`, + error, + ); + } + throw error; + } + + const frontmatter = parsed.data ?? {}; + if (!isRecord(frontmatter)) { + throw new SkillParseError( + `Frontmatter in ${options.skillMdPath} must be a mapping at the top level`, + ); + } + + const metadata = normalizeMetadata(frontmatter); + if (!isSupportedSkillType(metadata.type)) { + throw new UnsupportedSkillTypeError(metadata.type ?? String(frontmatter['type'])); + } + + const name = nonEmptyString(metadata.name); + const description = nonEmptyString(metadata.description); + if (isDirectorySkill && (name === undefined || description === undefined)) { + const field = name === undefined ? '"name"' : '"description"'; + throw new SkillParseError( + `Missing required frontmatter field ${field} in ${options.skillMdPath}`, + ); + } + + const skillPath = path.resolve(options.skillMdPath); + const content = parsed.body.trim(); + return { + name: name ?? options.skillDirName, + description: description ?? descriptionFromBody(content), + path: skillPath, + dir: path.dirname(skillPath), + content, + metadata, + source: options.source, + mermaid: parseMermaidFlowchart(content), + d2: parseD2Flowchart(content), + }; +} + +export function parseMermaidFlowchart(markdown: string): string | undefined { + return /```mermaid\r?\n([\s\S]*?)\r?\n```/.exec(markdown)?.[1]; +} + +export function parseD2Flowchart(markdown: string): string | undefined { + return /```d2\r?\n([\s\S]*?)\r?\n```/.exec(markdown)?.[1]; +} + +/** + * Expand argument placeholders in a skill body. + * + * Placeholder syntax ($ARGUMENTS, $0, $name, etc.) is modelled after common + * shell/CLI conventions rather than any specific product. + */ +export function expandSkillParameters( + body: string, + rawArgs: string, + context: SkillExpandContext, +): string { + const tokens = tokenizeArgs(rawArgs); + let content = body; + + for (let index = 0; index < (context.argumentNames?.length ?? 0); index++) { + const name = context.argumentNames?.[index]; + if (name === undefined) continue; + const escaped = regexpEscape(name); + content = content.replaceAll( + new RegExp(`\\$${escaped}(?![\\[\\w])`, 'g'), + tokens[index] ?? '', + ); + } + + content = content + .replaceAll(/\$ARGUMENTS\[(\d+)\]/g, (_match, indexText: string) => { + const index = Number.parseInt(indexText, 10); + return tokens[index] ?? ''; + }) + .replaceAll(/\$(\d+)(?!\w)/g, (_match, indexText: string) => { + const index = Number.parseInt(indexText, 10); + return tokens[index] ?? ''; + }) + .replaceAll('$ARGUMENTS', rawArgs); + + const hasArgumentPlaceholder = content !== body; + content = content + .replaceAll('${KIMI_SKILL_DIR}', context.skillDir) + .replaceAll('${KIMI_SESSION_ID}', context.sessionId ?? ''); + + if (!hasArgumentPlaceholder && rawArgs.length > 0) { + return `${content}\n\nARGUMENTS: ${rawArgs}`; + } + return content; +} + +export function skillArgumentNames(metadata: SkillMetadata): readonly string[] { + const value = metadata.arguments; + const isValidName = (name: string): boolean => + name.trim() !== '' && !/^\d+$/.test(name); + if (typeof value === 'string') return value.split(/\s+/).filter(isValidName); + if (!Array.isArray(value)) return []; + return value.filter((item): item is string => typeof item === 'string' && isValidName(item)); +} + +function normalizeMetadata(raw: Record): SkillMetadata { + const out: Record = {}; + for (const [rawKey, value] of Object.entries(raw)) { + const key = METADATA_ALIASES[rawKey] ?? rawKey; + out[key] = value; + } + + const type = nonEmptyString(out['type']); + if (type !== undefined) out['type'] = type; + + const name = nonEmptyString(out['name']); + if (name !== undefined) out['name'] = name; + + const description = nonEmptyString(out['description']); + if (description !== undefined) out['description'] = description; + + return out as SkillMetadata; +} + +function descriptionFromBody(body: string): string { + const firstLine = body + .split(/\r?\n/) + .map((line) => line.trim()) + .find((line) => line.length > 0); + if (firstLine === undefined) return 'No description provided.'; + return firstLine.length > 240 ? `${firstLine.slice(0, 239)}…` : firstLine; +} + +function tokenizeArgs(raw: string): string[] { + const out: string[] = []; + let current = ''; + let quote: '"' | "'" | undefined; + let hasContent = false; + + for (const char of raw) { + if (quote !== undefined) { + if (char === quote) { + quote = undefined; + } else { + current += char; + hasContent = true; + } + continue; + } + if (char === '"' || char === "'") { + quote = char; + hasContent = true; + continue; + } + if (/\s/.test(char)) { + if (hasContent) { + out.push(current); + current = ''; + hasContent = false; + } + continue; + } + current += char; + hasContent = true; + } + + if (hasContent) out.push(current); + return out; +} + +function nonEmptyString(value: unknown): string | undefined { + return typeof value === 'string' && value.trim() !== '' ? value.trim() : undefined; +} + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} diff --git a/packages/agent-core/src/skill/registry.ts b/packages/agent-core/src/skill/registry.ts new file mode 100644 index 000000000..551b5bfc7 --- /dev/null +++ b/packages/agent-core/src/skill/registry.ts @@ -0,0 +1,150 @@ +import { expandSkillParameters, skillArgumentNames } from './parser'; +import { discoverSkills, type DiscoverSkillsOptions } from './scanner'; +import type { SkillDefinition, SkillRoot, SkillSource, SkippedSkill } from './types'; +import { isInlineSkillType, normalizeSkillName } from './types'; + +const LISTING_DESC_MAX = 250; + +export class SkillNotFoundError extends Error { + readonly skillName: string; + + constructor(skillName: string) { + super(`Skill "${skillName}" is not registered`); + this.name = 'SkillNotFoundError'; + this.skillName = skillName; + } +} + +export interface SkillRegistryOptions { + readonly discover?: typeof discoverSkills; + readonly onWarning?: (message: string, cause?: unknown) => void; + readonly sessionId?: string; +} + +export class SkillRegistry { + private readonly byName = new Map(); + private readonly roots: string[] = []; + private readonly skipped: SkippedSkill[] = []; + private readonly discoverImpl: typeof discoverSkills; + private readonly onWarning: (message: string, cause?: unknown) => void; + readonly sessionId?: string; + + constructor(options: SkillRegistryOptions = {}) { + this.discoverImpl = options.discover ?? discoverSkills; + this.onWarning = options.onWarning ?? (() => {}); + this.sessionId = options.sessionId; + } + + async loadRoots(roots: readonly SkillRoot[]): Promise { + for (const root of roots) { + if (!this.roots.includes(root.path)) this.roots.push(root.path); + } + + const skills = await this.discoverImpl({ + roots, + onWarning: this.onWarning, + onSkippedByPolicy: (skill) => this.skipped.push(skill), + } satisfies DiscoverSkillsOptions); + + for (const skill of skills) { + this.byName.set(normalizeSkillName(skill.name), skill); + } + } + + registerBuiltinSkill(skill: SkillDefinition): void { + this.register(skill.source === 'builtin' ? skill : { ...skill, source: 'builtin' }); + } + + register(skill: SkillDefinition, options: { readonly replace?: boolean } = {}): void { + const key = normalizeSkillName(skill.name); + if (options.replace === true || !this.byName.has(key)) { + this.byName.set(key, skill); + } + } + + getSkill(name: string): SkillDefinition | undefined { + return this.byName.get(normalizeSkillName(name)); + } + + renderSkillPrompt(skill: SkillDefinition, rawArgs: string): string { + const argumentNames = skillArgumentNames(skill.metadata); + return expandSkillParameters(skill.content, rawArgs, { + skillDir: skill.dir, + sessionId: this.sessionId, + argumentNames, + }); + } + + listSkills(): readonly SkillDefinition[] { + return [...this.byName.values()].toSorted((a, b) => a.name.localeCompare(b.name)); + } + + listInvocableSkills(): readonly SkillDefinition[] { + return this.listSkills().filter( + (skill) => + skill.metadata.disableModelInvocation !== true && isInlineSkillType(skill.metadata.type), + ); + } + + getSkillRoots(): readonly string[] { + return [...this.roots]; + } + + getSkippedByPolicy(): readonly SkippedSkill[] { + return [...this.skipped]; + } + + getKimiSkillsDescription(): string { + const rendered = renderGroupedSkills(this.listSkills(), formatFullSkill); + return rendered.length === 0 ? 'No skills' : rendered; + } + + getModelSkillListing(): string { + const lines = ['DISREGARD any earlier skill listings. Current available skills:']; + const listing = renderGroupedSkills(this.listInvocableSkills(), formatModelSkill); + if (listing.length > 0) { + lines.push(listing); + } + return lines.length === 1 ? '' : lines.join('\n'); + } +} + +const SOURCE_GROUPS: ReadonlyArray<{ readonly source: SkillSource; readonly label: string }> = [ + { source: 'project', label: 'Project' }, + { source: 'user', label: 'User' }, + { source: 'extra', label: 'Extra' }, + { source: 'builtin', label: 'Built-in' }, +]; + +function renderGroupedSkills( + skills: readonly SkillDefinition[], + format: (skill: SkillDefinition) => readonly string[], +): string { + const lines: string[] = []; + for (const group of SOURCE_GROUPS) { + const groupSkills = skills.filter((skill) => skill.source === group.source); + if (groupSkills.length === 0) continue; + lines.push(`### ${group.label}`); + for (const skill of groupSkills) { + lines.push(...format(skill)); + } + } + return lines.join('\n'); +} + +function formatFullSkill(skill: SkillDefinition): readonly string[] { + return [`- ${skill.name}`, ` - Path: ${skill.path}`, ` - Description: ${skill.description}`]; +} + +function formatModelSkill(skill: SkillDefinition): readonly string[] { + const lines = [`- ${skill.name}: ${truncate(skill.description, LISTING_DESC_MAX)}`]; + if (typeof skill.metadata.whenToUse === 'string' && skill.metadata.whenToUse.length > 0) { + lines.push(` When to use: ${skill.metadata.whenToUse}`); + } + lines.push(` Path: ${skill.path}`); + return lines; +} + +function truncate(value: string, max: number): string { + return value.length > max ? value.slice(0, max) : value; +} diff --git a/packages/agent-core/src/skill/scanner.ts b/packages/agent-core/src/skill/scanner.ts new file mode 100644 index 000000000..ecbe11b6e --- /dev/null +++ b/packages/agent-core/src/skill/scanner.ts @@ -0,0 +1,366 @@ +import { promises as fs } from 'node:fs'; +import path from 'node:path'; + +import { SkillParseError, UnsupportedSkillTypeError, parseSkillFromFile } from './parser'; +import type { SkillDefinition, SkillRoot, SkillSource, SkippedSkill } from './types'; +import { normalizeSkillName } from './types'; + +const USER_BRAND_DIRS = ['.kimi-code/skills'] as const; +const USER_GENERIC_DIRS = ['.agents/skills'] as const; +const PROJECT_BRAND_DIRS = ['.kimi-code/skills'] as const; +const PROJECT_GENERIC_DIRS = ['.agents/skills'] as const; + +// Bounds recursion so a directory symlink cycle inside a skill root cannot +// loop forever. Real skill trees are 1-3 levels deep. +const MAX_SKILL_SCAN_DEPTH = 8; + +export interface SkillPathContext { + readonly userHomeDir: string; + readonly workDir: string; +} + +export interface ResolveSkillRootsOptions { + readonly paths: SkillPathContext; + readonly builtinDir?: string; + readonly explicitDirs?: readonly string[]; + readonly extraDirs?: readonly string[]; + readonly mergeAllAvailableSkills?: boolean; + readonly realpath?: (p: string) => Promise; + readonly isDir?: (p: string) => Promise; +} + +export interface DiscoverSkillsOptions { + readonly roots: readonly SkillRoot[]; + readonly onWarning?: (message: string, cause?: unknown) => void; + readonly onSkippedByPolicy?: (skill: SkippedSkill) => void; + readonly readdir?: (p: string) => Promise; + readonly isFile?: (p: string) => Promise; + readonly isDir?: (p: string) => Promise; + readonly parse?: (input: { + readonly skillMdPath: string; + readonly skillDirName: string; + readonly source: SkillSource; + }) => Promise; +} + +export interface WorkspaceWithAdditionalDirs { + readonly workspaceDir: string; + readonly additionalDirs: readonly string[]; +} + +export async function resolveSkillRoots( + options: ResolveSkillRootsOptions, +): Promise { + const isDir = options.isDir ?? defaultIsDir; + const realpath = options.realpath ?? ((p: string) => fs.realpath(p)); + const roots: SkillRoot[] = []; + const mergeAllAvailableSkills = options.mergeAllAvailableSkills ?? true; + const { userHomeDir, workDir } = options.paths; + const projectRoot = await findProjectRoot(workDir); + + if (options.explicitDirs !== undefined && options.explicitDirs.length > 0) { + await pushConfiguredDirs( + roots, + options.explicitDirs, + projectRoot, + userHomeDir, + 'user', + isDir, + realpath, + ); + } else { + await pushBrandGroup( + roots, + PROJECT_BRAND_DIRS, + projectRoot, + 'project', + mergeAllAvailableSkills, + isDir, + realpath, + ); + await pushFirstExisting(roots, PROJECT_GENERIC_DIRS, projectRoot, 'project', isDir, realpath); + await pushBrandGroup( + roots, + USER_BRAND_DIRS, + userHomeDir, + 'user', + mergeAllAvailableSkills, + isDir, + realpath, + ); + await pushFirstExisting(roots, USER_GENERIC_DIRS, userHomeDir, 'user', isDir, realpath); + } + + if (options.extraDirs !== undefined) { + await pushConfiguredDirs( + roots, + options.extraDirs, + projectRoot, + userHomeDir, + 'extra', + isDir, + realpath, + ); + } + + if (options.builtinDir !== undefined) { + await pushExistingRoot(roots, options.builtinDir, 'builtin', isDir, realpath); + } + + return roots; +} + +export async function discoverSkills( + options: DiscoverSkillsOptions, +): Promise { + const readdir = options.readdir ?? ((p: string) => fs.readdir(p)); + const isFile = options.isFile ?? defaultIsFile; + const isDir = options.isDir ?? defaultIsDir; + const parse = options.parse ?? parseSkillFromFile; + const warn = options.onWarning ?? (() => {}); + const skip = options.onSkippedByPolicy ?? (() => {}); + const byName = new Map(); + + async function walkSkillDir( + dirPath: string, + source: SkillSource, + isTopLevel: boolean, + depth: number, + ): Promise { + if (depth > MAX_SKILL_SCAN_DEPTH) return; + + let entries: readonly string[]; + try { + // Sorted so first-wins collision resolution across sibling directories + // is deterministic rather than dependent on filesystem readdir order. + entries = [...(await readdir(dirPath))].toSorted(); + } catch (error) { + warn(`Failed to read skill directory ${dirPath}`, error); + return; + } + + const directorySkills = new Set(); + const subdirs: string[] = []; + for (const entry of entries) { + // A directory holding SKILL.md is a skill bundle: register it and do not + // descend, so its bundled references/scripts are never scanned. + if (await isFile(path.join(dirPath, entry, 'SKILL.md'))) { + directorySkills.add(entry); + continue; + } + if (entry === 'node_modules' || entry.startsWith('.')) continue; + if (await isDir(path.join(dirPath, entry))) subdirs.push(entry); + } + + for (const entry of directorySkills) { + await parseAndRegister({ + parse, + byName, + skillMdPath: path.join(dirPath, entry, 'SKILL.md'), + skillDirName: entry, + source, + warn, + skip, + }); + } + + // Flat .md skills count only at a root's top level; deeper .md files are + // skill payload (e.g. references/foo.md), not skills. + if (isTopLevel) { + for (const entry of entries) { + if (!entry.endsWith('.md')) continue; + if (entry === 'SKILL.md') continue; + const skillName = entry.slice(0, -'.md'.length); + if (directorySkills.has(skillName)) { + warn( + `Ignoring flat skill ${path.join(dirPath, entry)} because ${path.join(dirPath, skillName, 'SKILL.md')} exists with the same name`, + ); + continue; + } + const skillMdPath = path.join(dirPath, entry); + if (!(await isFile(skillMdPath))) continue; + await parseAndRegister({ + parse, + byName, + skillMdPath, + skillDirName: skillName, + source, + warn, + skip, + }); + } + } + + for (const entry of subdirs) { + await walkSkillDir(path.join(dirPath, entry), source, false, depth + 1); + } + } + + for (const root of options.roots) { + await walkSkillDir(root.path, root.source, true, 0); + } + + return sortSkills([...byName.values()]); +} + +export function extendWorkspaceWithSkillRoots( + workspace: T, + skillRoots: readonly string[], +): T { + const additionalDirs = [...workspace.additionalDirs]; + for (const root of skillRoots) { + if (isWithin(root, workspace.workspaceDir)) continue; + if (additionalDirs.some((dir) => root === dir || isWithin(root, dir))) continue; + additionalDirs.push(root); + } + if (additionalDirs.length === workspace.additionalDirs.length) return workspace; + return { ...workspace, additionalDirs }; +} + +function sortSkills(skills: readonly SkillDefinition[]): readonly SkillDefinition[] { + return [...skills].toSorted((a, b) => a.name.localeCompare(b.name)); +} + +async function pushFirstExisting( + out: SkillRoot[], + dirs: readonly string[], + base: string, + source: SkillSource, + isDir: (p: string) => Promise, + realpath: (p: string) => Promise, +): Promise { + for (const dir of dirs) { + if (await pushExistingRoot(out, path.join(base, dir), source, isDir, realpath)) return; + } +} + +async function pushBrandGroup( + out: SkillRoot[], + dirs: readonly string[], + base: string, + source: SkillSource, + mergeAllAvailableSkills: boolean, + isDir: (p: string) => Promise, + realpath: (p: string) => Promise, +): Promise { + if (!mergeAllAvailableSkills) { + await pushFirstExisting(out, dirs, base, source, isDir, realpath); + return; + } + for (const dir of dirs) { + await pushExistingRoot(out, path.join(base, dir), source, isDir, realpath); + } +} + +async function pushConfiguredDirs( + out: SkillRoot[], + dirs: readonly string[], + projectRoot: string, + userHomeDir: string, + source: SkillSource, + isDir: (p: string) => Promise, + realpath: (p: string) => Promise, +): Promise { + for (const dir of dirs) { + await pushExistingRoot( + out, + resolveConfiguredDir(dir, projectRoot, userHomeDir), + source, + isDir, + realpath, + ); + } +} + +async function pushExistingRoot( + out: SkillRoot[], + dir: string, + source: SkillSource, + isDir: (p: string) => Promise, + realpath: (p: string) => Promise, +): Promise { + if (!(await isDir(dir))) return false; + const resolved = await realpath(dir); + if (!out.some((root) => root.path === resolved)) out.push({ path: resolved, source }); + return true; +} + +async function parseAndRegister(input: { + readonly parse: NonNullable; + readonly byName: Map; + readonly skillMdPath: string; + readonly skillDirName: string; + readonly source: SkillSource; + readonly warn: (message: string, cause?: unknown) => void; + readonly skip: (skill: SkippedSkill) => void; +}): Promise { + try { + const skill = await input.parse({ + skillMdPath: input.skillMdPath, + skillDirName: input.skillDirName, + source: input.source, + }); + const key = normalizeSkillName(skill.name); + if (!input.byName.has(key)) input.byName.set(key, skill); + } catch (error) { + if (error instanceof UnsupportedSkillTypeError) { + input.skip({ + path: input.skillMdPath, + type: error.skillType, + reason: `unsupported skill type "${error.skillType}"`, + }); + } else if (error instanceof SkillParseError) { + input.warn(`Skipping invalid skill at ${input.skillMdPath}: ${error.message}`, error); + } else { + input.warn(`Skipping skill at ${input.skillMdPath} due to unexpected error`, error); + } + } +} + +async function defaultIsDir(p: string): Promise { + try { + return (await fs.stat(p)).isDirectory(); + } catch { + return false; + } +} + +async function defaultIsFile(p: string): Promise { + try { + return (await fs.stat(p)).isFile(); + } catch { + return false; + } +} + +async function findProjectRoot(workDir: string): Promise { + const start = path.resolve(workDir); + let current = start; + while (true) { + if (await exists(path.join(current, '.git'))) return current; + const parent = path.dirname(current); + if (parent === current) return start; + current = parent; + } +} + +async function exists(p: string): Promise { + try { + await fs.stat(p); + return true; + } catch { + return false; + } +} + +function resolveConfiguredDir(dir: string, projectRoot: string, userHomeDir: string): string { + if (dir === '~') return userHomeDir; + if (dir.startsWith('~/')) return path.join(userHomeDir, dir.slice(2)); + if (path.isAbsolute(dir)) return dir; + return path.resolve(projectRoot, dir); +} + +function isWithin(child: string, parent: string): boolean { + const relative = path.relative(parent, child); + return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative)); +} diff --git a/packages/agent-core/src/skill/types.ts b/packages/agent-core/src/skill/types.ts new file mode 100644 index 000000000..8ea89b1dd --- /dev/null +++ b/packages/agent-core/src/skill/types.ts @@ -0,0 +1,77 @@ +export type SkillSource = 'project' | 'user' | 'extra' | 'builtin'; + +export interface SkillMetadata { + readonly name?: string | undefined; + readonly description?: string | undefined; + readonly type?: string | undefined; + readonly whenToUse?: string | undefined; + readonly disableModelInvocation?: boolean | undefined; + readonly safe?: boolean | undefined; + readonly arguments?: readonly unknown[] | string | undefined; + readonly [key: string]: unknown; +} + +export interface SkillDefinition { + readonly name: string; + readonly description: string; + readonly path: string; + readonly dir: string; + readonly content: string; + readonly metadata: SkillMetadata; + readonly source: SkillSource; + readonly mermaid?: string | undefined; + readonly d2?: string; +} + +export interface SkillSummary { + readonly name: string; + readonly description: string; + readonly path: string; + readonly source: SkillSource; + readonly type?: string | undefined; + readonly disableModelInvocation?: boolean | undefined; +} + +export interface SkillRoot { + readonly path: string; + readonly source: SkillSource; +} + +export interface SkippedSkill { + readonly path: string; + readonly type: string; + readonly reason: string; +} + +export interface SkillCatalog { + getSkill(name: string): SkillDefinition | undefined; + listSkills(): readonly SkillDefinition[]; + listInvocableSkills(): readonly SkillDefinition[]; +} + +export function normalizeSkillName(name: string): string { + return name.toLowerCase(); +} + +export function isInlineSkillType(type: string | undefined): boolean { + return type === undefined || type === 'prompt' || type === 'inline'; +} + +export function isUserActivatableSkillType(type: string | undefined): boolean { + return isInlineSkillType(type) || type === 'flow'; +} + +export function isSupportedSkillType(type: string | undefined): boolean { + return isUserActivatableSkillType(type); +} + +export function summarizeSkill(skill: SkillDefinition): SkillSummary { + return { + name: skill.name, + description: skill.description, + path: skill.path, + source: skill.source, + type: skill.metadata.type, + disableModelInvocation: skill.metadata.disableModelInvocation, + }; +} diff --git a/packages/agent-core/src/telemetry.ts b/packages/agent-core/src/telemetry.ts new file mode 100644 index 000000000..e1c9c3ad6 --- /dev/null +++ b/packages/agent-core/src/telemetry.ts @@ -0,0 +1,26 @@ +export type TelemetryPropertyValue = boolean | number | string | null; + +export type TelemetryProperties = Readonly>; + +export interface TelemetryContextPatch { + readonly sessionId?: string | null; +} + +export interface TelemetryClient { + track(event: string, properties?: TelemetryProperties): void; + withContext?(patch: TelemetryContextPatch): TelemetryClient; + setContext?(patch: TelemetryContextPatch): void; +} + +export const noopTelemetryClient: TelemetryClient = { + track: () => {}, + withContext: () => noopTelemetryClient, + setContext: () => {}, +}; + +export function withTelemetryContext( + telemetry: TelemetryClient, + patch: TelemetryContextPatch, +): TelemetryClient { + return telemetry.withContext?.(patch) ?? telemetry; +} diff --git a/packages/agent-core/src/tools/args-validator.ts b/packages/agent-core/src/tools/args-validator.ts new file mode 100644 index 000000000..ce967ccb0 --- /dev/null +++ b/packages/agent-core/src/tools/args-validator.ts @@ -0,0 +1,93 @@ +import Ajv, { type ErrorObject, type ValidateFunction } from 'ajv'; +import Ajv2019 from 'ajv/dist/2019'; +import Ajv2020 from 'ajv/dist/2020'; +import addFormats from 'ajv-formats'; + +const DRAFT_07_AJV = new Ajv({ strict: false, allErrors: true }); +addFormats(DRAFT_07_AJV); + +const DRAFT_2019_AJV = new Ajv2019({ strict: false, allErrors: true }); +addFormats(DRAFT_2019_AJV); + +const DRAFT_2020_AJV = new Ajv2020({ strict: false, allErrors: true }); +addFormats(DRAFT_2020_AJV); + +const DRAFT_2019_KEYWORDS = new Set([ + 'dependentRequired', + 'dependentSchemas', + 'maxContains', + 'minContains', + 'unevaluatedItems', + 'unevaluatedProperties', + '$recursiveAnchor', + '$recursiveRef', +]); + +const DRAFT_2020_KEYWORDS = new Set(['prefixItems', '$dynamicAnchor', '$dynamicRef']); + +// Mixing JSON Schema dialects in a single Ajv instance is unsafe because +// keyword semantics differ, e.g. draft-07 tuple `items` vs 2020-12 `prefixItems`. +function ajvFor(schema: Record): Ajv | Ajv2019 | Ajv2020 { + const $schema = schema['$schema']; + if (typeof $schema === 'string') { + if ($schema.includes('2020-12')) return DRAFT_2020_AJV; + if ($schema.includes('2019-09')) return DRAFT_2019_AJV; + return DRAFT_07_AJV; + } + if (containsSchemaKeyword(schema, DRAFT_2020_KEYWORDS)) return DRAFT_2020_AJV; + if (containsSchemaKeyword(schema, DRAFT_2019_KEYWORDS)) return DRAFT_2019_AJV; + return DRAFT_07_AJV; +} + +function containsSchemaKeyword(value: unknown, keywords: ReadonlySet): boolean { + if (Array.isArray(value)) { + return value.some((item) => containsSchemaKeyword(item, keywords)); + } + if (typeof value !== 'object' || value === null) return false; + for (const [key, child] of Object.entries(value)) { + if (keywords.has(key)) return true; + if (containsSchemaKeyword(child, keywords)) return true; + } + return false; +} + +export type JsonType = null | number | string | boolean | JsonArray | JsonObject; + +/** @internal */ +export interface JsonArray extends Array {} + +/** @internal */ +export interface JsonObject extends Record {} + +export type ToolArgsValidator = ValidateFunction; + +function formatValidationError(error: ErrorObject): string { + if (error.keyword === 'required' && 'missingProperty' in error.params) { + return `must have required property '${String(error.params['missingProperty'])}'`; + } + + if (error.keyword === 'additionalProperties' && 'additionalProperty' in error.params) { + return `must NOT have additional property '${String(error.params['additionalProperty'])}'`; + } + + const path = error.instancePath ? `${error.instancePath} ` : ''; + return `${path}${error.message ?? 'is invalid'}`; +} + +export function compileToolArgsValidator(schema: Record): ToolArgsValidator { + return ajvFor(schema).compile(schema) as ToolArgsValidator; +} + +export function validateToolArgs(validator: ToolArgsValidator, args: JsonType): string | null { + const valid = validator(args); + if (valid) { + return null; + } + + const errors = validator.errors ?? []; + if (errors.length === 0) { + return 'Tool parameter validation failed'; + } + + return errors.map((error) => formatValidationError(error)).join('; '); +} diff --git a/packages/agent-core/src/tools/background/index.ts b/packages/agent-core/src/tools/background/index.ts new file mode 100644 index 000000000..1f924b359 --- /dev/null +++ b/packages/agent-core/src/tools/background/index.ts @@ -0,0 +1,19 @@ +/** + * Background task management tools barrel. + */ + +export { BackgroundProcessManager, generateTaskId } from './manager'; +export type { + BackgroundTaskInfo, + BackgroundTaskKind, + BackgroundTaskOutputSnapshot, + BackgroundTaskStatus, + ReconcileResult, +} from './manager'; +export { VALID_TASK_ID } from './persist'; +export { TaskListTool, TaskListInputSchema } from './task-list'; +export type { TaskListInput } from './task-list'; +export { TaskOutputTool, TaskOutputInputSchema } from './task-output'; +export type { TaskOutputInput } from './task-output'; +export { TaskStopTool, TaskStopInputSchema } from './task-stop'; +export type { TaskStopInput } from './task-stop'; diff --git a/packages/agent-core/src/tools/background/manager.ts b/packages/agent-core/src/tools/background/manager.ts new file mode 100644 index 000000000..97f77fc9e --- /dev/null +++ b/packages/agent-core/src/tools/background/manager.ts @@ -0,0 +1,1205 @@ +/** + * BackgroundProcessManager — manages background shell processes. + * + * Tracks background bash tasks spawned by `BashTool` when + * `run_in_background=true`. + * + * Each task gets a unique ID, captures stdout+stderr to a ring buffer, + * and supports status query / output retrieval / stop operations. + * + * Accepts `KaosProcess` (not `ChildProcess`) so there is no unsafe cast + * at the BashTool call site. Lifecycle detection uses `wait()` instead + * of EventEmitter `on('exit')`. + */ + +import { randomBytes } from 'node:crypto'; + +import type { KaosProcess } from '@moonshot-ai/kaos'; + +import { isAbortError } from '../../loop/errors'; +import { + appendTaskOutput, + listTasks, + readTaskOutput, + readTaskOutputBytes, + removeTask, + taskOutputExists, + taskOutputExistsSync, + taskOutputFile, + taskOutputSizeBytes, + writeTask, + type PersistedTask, +} from './persist'; + +// ── Types ──────────────────────────────────────────────────────────── + +/** + * `'lost'` is a reconcile-only terminal state. Tasks loaded from disk + * that were marked `running` at startup but have no live KaosProcess + * (the previous CLI process died) are reclassified as lost. + * + * `'awaiting_approval'` is a non-terminal state entered when a background + * agent task is paused waiting for tool-call approval from the root + * agent. The BPM state machine is the single source of truth for "is + * this task actively running vs. gated on approval" — UI reads from BPM + * instead of reverse-querying the ApprovalRuntime. The loop boundary is + * preserved because `awaiting_approval` in BPM does not leak permission + * vocabulary into the loop. + */ +export type BackgroundTaskStatus = + | 'running' + | 'awaiting_approval' + | 'completed' + | 'failed' + | 'killed' + | 'lost'; + +/** Terminal states tasks never leave once reached. */ +const TERMINAL_STATUSES: ReadonlySet = new Set([ + 'completed', + 'failed', + 'killed', + 'lost', +]); + +export function isBackgroundTaskTerminal(status: BackgroundTaskStatus): boolean { + return TERMINAL_STATUSES.has(status); +} + +/** Task kinds with distinct id prefixes. */ +export type BackgroundTaskKind = 'bash' | 'agent'; + +/** Lifecycle phases observed by `onLifecycle` subscribers. */ +export type BackgroundLifecycleEvent = 'started' | 'updated' | 'terminated'; + +export interface BackgroundTaskInfo { + readonly taskId: string; + readonly command: string; + readonly description: string; + readonly status: BackgroundTaskStatus; + readonly pid: number; + readonly exitCode: number | null; + readonly startedAt: number; + readonly endedAt: number | null; + /** Populated only while `status === 'awaiting_approval'`. */ + readonly approvalReason?: string | undefined; + /** True when an agent task was aborted by its deadline. */ + readonly timedOut?: boolean | undefined; + /** Reason recorded when a task is explicitly stopped. */ + readonly stopReason?: string | undefined; + /** + * Deadline (ms) supplied to `registerAgentTask`. Surfaced so shutdown + * wait-caps and UI can read the originally-requested timeout without + * round-tripping the call site. `undefined` means no deadline. + */ + readonly timeoutMs?: number | undefined; + /** Identifier of the spawned subagent (agent tasks only). */ + readonly agentId?: string | undefined; + /** Profile name of the spawned subagent (agent tasks only). */ + readonly subagentType?: string | undefined; + /** + * Human-readable reason recorded when a non-terminal task is reclassified + * via reconcile (e.g. a stale heartbeat → lost). + */ + readonly failureReason?: string | undefined; +} + +interface ManagedProcess { + readonly taskId: string; + readonly command: string; + readonly description: string; + readonly proc: KaosProcess; + readonly outputChunks: string[]; + /** Total UTF-8 bytes observed, including chunks dropped from the live ring buffer. */ + outputSizeBytes: number; + status: BackgroundTaskStatus; + exitCode: number | null; + readonly startedAt: number; + endedAt: number | null; + /** Listeners awaiting task completion. */ + readonly waiters: Array<() => void>; + /** True once `fireTerminalCallbacks` has already run. */ + terminalFired: boolean; + /** Reason carried while awaiting approval. */ + approvalReason?: string | undefined; + /** Set when a deadline fires before natural completion. */ + timedOut?: boolean | undefined; + /** Reason recorded when a task is explicitly stopped. */ + stopReason?: string | undefined; + /** Deadline supplied at registration; surfaced via task info. */ + timeoutMs?: number | undefined; + /** Subagent identifier (agent tasks only). */ + agentId?: string | undefined; + /** Subagent profile name (agent tasks only). */ + subagentType?: string | undefined; + /** Non-terminal-reclassification reason (e.g. stale heartbeat). */ + failureReason?: string | undefined; + /** True after stop() has requested cancellation but before terminal status is chosen. */ + stopRequested: boolean; + /** Session dir captured at registration for output.log writes. */ + readonly outputSessionDir?: string | undefined; + lifecyclePromise: Promise; + persistWriteQueue: Promise; + outputWriteQueue: Promise; +} + +/** + * Maximum bytes of combined output kept in the in-memory ring buffer per + * task. When exceeded, the oldest chunks are dropped. + * + * The ring buffer is a lightweight tail intended for the `/tasks` UI and + * terminal notifications only — it deliberately discards old output to + * cap memory. It is NOT the authoritative full output: the complete, + * never-truncated log lives on disk at `/tasks//output.log`. + * Callers that need the full output (e.g. `TaskOutput`) must read the + * disk log via `getOutputSizeBytes` / `readOutputBytesFromDisk`. + */ +const MAX_OUTPUT_BYTES = 1024 * 1024; // 1 MiB + +const SIGTERM_GRACE_MS = 5_000; +const EXIT_SETTLE_GRACE_MS = 10; + +const _ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz'; + +/** + * Generate `{prefix}-{8 base36 chars}`. + * + * `randomBytes(8) % 36` has a modest modulo bias (256 % 36 = 4) but + * over an 8-char suffix yields ~36^8 ≈ 2.8e12 distinct ids which is + * more than enough uniqueness for per-session task ids. + */ +export function generateTaskId(kind: BackgroundTaskKind): string { + const bytes = randomBytes(8); + let suffix = ''; + for (let i = 0; i < 8; i++) { + suffix += _ALPHABET[bytes[i]! % 36]; + } + return `${kind}-${suffix}`; +} + +/** + * Terminal-state info for tasks reconciled as lost on resume. They + * have no live KaosProcess and no captured output (the buffer died + * with the previous process), so list/get returns this minimal record. + */ +export interface ReconcileResult { + /** Task IDs that were marked `lost` because their process is gone. */ + readonly lost: readonly string[]; + /** Snapshot of each lost task's persisted info for terminal notifications. */ + readonly lostInfo: readonly BackgroundTaskInfo[]; +} + +export interface BackgroundProcessManagerOptions { + readonly maxRunningTasks?: number; + readonly sessionDir?: string; +} + +export interface BackgroundTaskReservation { + release(): void; +} + +export interface BackgroundTaskOutputSnapshot { + readonly outputPath?: string; + readonly outputSizeBytes: number; + readonly previewBytes: number; + readonly truncated: boolean; + readonly fullOutputAvailable: boolean; + readonly preview: string; +} + +function emptyOutputSnapshot(): BackgroundTaskOutputSnapshot { + return { + outputSizeBytes: 0, + previewBytes: 0, + truncated: false, + fullOutputAvailable: false, + preview: '', + }; +} + +// ── Manager ────────────────────────────────────────────────────────── + +export class BackgroundProcessManager { + private readonly processes = new Map(); + private reservedTaskSlots = 0; + /** + * Ghosts: tasks loaded from disk during reconcile that have no live + * KaosProcess. They appear in `list()` / `getTask()` with status + * `lost` so users see what was running before the crash/restart. + */ + private readonly ghosts = new Map(); + /** When set, register/lifecycle changes persist to disk. */ + private sessionDir: string | undefined; + + /** + * Registered terminal-state callbacks. Fired once per task when the + * task reaches a terminal state (completed / failed / killed). + */ + private readonly terminalCallbacks: Array<(info: BackgroundTaskInfo) => void | Promise> = + []; + + /** + * Registered lifecycle callbacks. Fired for every observable + * transition (started / updated / terminated). Errors thrown by + * callbacks are silently swallowed so the BPM main flow never breaks + * because of a buggy subscriber. + */ + private readonly lifecycleCallbacks: Array< + (event: BackgroundLifecycleEvent, info: BackgroundTaskInfo) => void + > = []; + + constructor(private readonly options: BackgroundProcessManagerOptions = {}) { + this.sessionDir = options.sessionDir; + } + + /** + * Register a callback that fires when any task reaches a terminal + * state. The callback receives the task's `BackgroundTaskInfo` + * snapshot. Multiple callbacks may be registered; they are invoked in + * registration order. Errors thrown by callbacks are silently swallowed. + */ + onTerminal(callback: (info: BackgroundTaskInfo) => void | Promise): void { + this.terminalCallbacks.push(callback); + } + + /** + * Register a callback that fires on every lifecycle transition: + * - 'started': task just registered (either bash or agent) + * - 'updated': awaiting_approval entered / cleared + * - 'terminated': task reached a terminal state (also triggers + * onTerminal); fires exactly once per task. + * + * Synchronous callback. Errors are swallowed so the BPM lifecycle + * machinery (status updates, persistence, waiters) cannot be blocked + * by a buggy subscriber. Use it for fan-out to RPC events; do not put + * heavy work in it (defer to microtask if needed). + */ + onLifecycle(callback: (event: BackgroundLifecycleEvent, info: BackgroundTaskInfo) => void): void { + this.lifecycleCallbacks.push(callback); + } + + /** Fan out a lifecycle event to subscribers. */ + private fireLifecycle(event: BackgroundLifecycleEvent, info: BackgroundTaskInfo): void { + for (const cb of this.lifecycleCallbacks) { + try { + cb(event, info); + } catch { + /* swallow callback errors */ + } + } + } + + /** + * Subclasses can react to live task completion here. Restored disk + * tasks reconciled as lost do not call this hook. + */ + protected onLiveTaskTerminal(_info: BackgroundTaskInfo): void | Promise {} + + /** + * Fire all registered terminal callbacks for a task. Idempotent: the + * second invocation for the same task is a no-op so `reconcile()` / + * a lagging `wait()` resolver / a race between `stop()` and natural + * exit cannot yield duplicate notifications. This is the manager-side + * half of the dedupe pact with `NotificationManager.dedupe_key`. + */ + private fireTerminalCallbacks(entry: ManagedProcess): void { + if (entry.terminalFired) return; + entry.terminalFired = true; + const info = this.toInfo(entry); + try { + const result = this.onLiveTaskTerminal(info); + if (result && typeof result.catch === 'function') { + result.catch(() => {}); + } + } catch { + /* swallow */ + } + this.fireTerminalSubscribers(info); + } + + private fireTerminalSubscribers(info: BackgroundTaskInfo): void { + for (const cb of this.terminalCallbacks) { + try { + const result = cb(info); + if (result && typeof result.catch === 'function') { + result.catch(() => {}); + } + } catch { + /* swallow callback errors */ + } + } + this.fireLifecycle('terminated', info); + } + + private resolveWaiters(entry: ManagedProcess): void { + const waiters = entry.waiters.splice(0); + for (const resolve of waiters) resolve(); + } + + assertCanRegister(): void { + const maxRunningTasks = this.options.maxRunningTasks; + if (maxRunningTasks === undefined) return; + if (this.activeTaskCount() + this.reservedTaskSlots < maxRunningTasks) return; + throw new Error('Too many background tasks are already running.'); + } + + reserveSlot(): BackgroundTaskReservation { + const maxRunningTasks = this.options.maxRunningTasks; + if (maxRunningTasks === undefined) { + return { release: () => {} }; + } + this.assertCanRegister(); + this.reservedTaskSlots++; + let released = false; + return { + release: () => { + if (released) return; + released = true; + this.reservedTaskSlots--; + }, + }; + } + + private activeTaskCount(): number { + let count = 0; + for (const entry of this.processes.values()) { + if (!TERMINAL_STATUSES.has(entry.status)) count++; + } + return count; + } + + /** + * Register a KaosProcess as a background task. + * Starts capturing stdout/stderr and monitors lifecycle via `wait()`. + * Returns the assigned task ID. + * + * `opts.kind` picks the id prefix. Defaults to `'bash'` because bash + * subprocess registration is the only caller on the process path + * today; agent tasks go through `registerAgentTask` which forces + * `'agent'`. + */ + register( + proc: KaosProcess, + command: string, + description: string, + opts: + | { + kind?: BackgroundTaskKind; + /** + * Optional shell metadata. Carried so the `/task` UI and the + * background persist snapshot can surface which dialect a + * task was launched under. Legacy callers omitting this + * field keep the implicit 'bash' default. + */ + shellInfo?: { + shellName: string; + shellPath: string; + cwd: string; + }; + reservation?: BackgroundTaskReservation; + } + | undefined = undefined, + ): string { + if (opts?.reservation) { + opts.reservation.release(); + } else { + this.assertCanRegister(); + } + const kind = opts?.kind; + const taskId = generateTaskId(kind ?? 'bash'); + const entry: ManagedProcess = { + taskId, + command, + description, + proc, + outputChunks: [], + outputSizeBytes: 0, + status: 'running', + exitCode: null, + startedAt: Date.now(), + endedAt: null, + waiters: [], + terminalFired: false, + stopRequested: false, + outputSessionDir: this.sessionDir, + lifecyclePromise: Promise.resolve(), + persistWriteQueue: Promise.resolve(), + outputWriteQueue: Promise.resolve(), + }; + this.processes.set(taskId, entry); + + // Capture stdout + stderr into the ring buffer. + for (const stream of [proc.stdout, proc.stderr]) { + stream.setEncoding('utf8'); + stream.on('data', (chunk: string) => { + this.appendOutput(entry, chunk); + }); + } + + // Initial persistence (snapshot at start). + void this.persistLive(entry); + this.fireLifecycle('started', this.toInfo(entry)); + + // Monitor lifecycle via wait() — no EventEmitter dependency. + entry.lifecyclePromise = proc + .wait() + .then((exitCode) => this.settleProcessExit(entry, exitCode)) + .catch(async (_err: unknown) => { + // When `proc.wait()` rejects (launch failed / stream error), + // still drive the task through the same terminal finalizer. + await this.finalizeTerminal(entry, entry.stopRequested ? 'killed' : 'failed', null); + }); + void entry.lifecyclePromise; + + return taskId; + } + + /** Get info about a specific task. Falls back to reconcile ghosts. */ + getTask(taskId: string): BackgroundTaskInfo | undefined { + const entry = this.processes.get(taskId); + if (entry !== undefined) { + return this.toInfo(entry); + } + return this.ghosts.get(taskId); + } + + /** + * Give just-ended processes a short grace period to settle their `wait()` + * promise, then return with whatever lifecycle state has been finalized. + */ + async settlePendingExits(): Promise { + const pendingCompletions = this.observedExitCompletions(); + if (pendingCompletions.length === 0) return; + await Promise.race([ + Promise.allSettled(pendingCompletions).then(() => {}), + new Promise((resolve) => { + setTimeout(resolve, EXIT_SETTLE_GRACE_MS); + }), + ]); + } + + /** + * List tasks, optionally filtering to active-only. + * + * When `activeOnly=false`, includes reconcile ghosts (lost tasks + * from a prior CLI process) so the user sees what survived the + * restart. Active-only mode never shows ghosts (they're terminal). + */ + list(activeOnly = true, limit?: number): BackgroundTaskInfo[] { + const result: BackgroundTaskInfo[] = []; + for (const entry of this.processes.values()) { + // An awaiting_approval task is non-terminal and therefore counts + // as active in listings (UI needs to show it alongside plain + // running tasks). + if (activeOnly && TERMINAL_STATUSES.has(entry.status)) continue; + result.push(this.toInfo(entry)); + if (limit !== undefined && result.length >= limit) return result; + } + if (!activeOnly) { + for (const ghost of this.ghosts.values()) { + result.push(ghost); + if (limit !== undefined && result.length >= limit) return result; + } + } + return result; + } + + /** + * Await all pending `output.log` appends for a task to settle. + * + * Output chunks are persisted to disk on an async queue, so a task can + * reach a terminal state before its final chunks have landed on disk. + * Callers that read the on-disk log (`getOutputSizeBytes` / + * `readOutputBytesFromDisk`) should `await flushOutput()` first so they + * observe the complete log. No-op for unknown/ghost tasks. + */ + async flushOutput(taskId: string): Promise { + const entry = this.processes.get(taskId); + if (entry === undefined) return; + await entry.outputWriteQueue; + } + + /** + * Total byte size of a task's full output as stored on disk. + * + * Reads `/tasks//output.log`, which is the complete, + * never-truncated log — unlike the in-memory ring buffer it never drops + * old chunks. Returns 0 when the manager is detached, the task is + * unknown, or the task has produced no output yet. + */ + async getOutputSizeBytes(taskId: string): Promise { + const outputSessionDir = this.outputSessionDirFor(taskId); + if (outputSessionDir === undefined) return 0; + return taskOutputSizeBytes(outputSessionDir, taskId); + } + + /** + * Read a byte range of a task's full output from the on-disk log. + * + * Reads up to `maxBytes` bytes starting at `offset` of `output.log`, + * straight from disk so it never loses the head of a large task the way + * the in-memory ring buffer would. Callers derive `offset` and `maxBytes` + * from a single `getOutputSizeBytes` snapshot, so the bytes returned stay + * consistent with the size used for metadata even when a still-running + * task keeps growing its log. Returns an empty string when the manager + * is detached, the task is unknown, or the log is absent. + */ + async readOutputBytesFromDisk( + taskId: string, + offset: number, + maxBytes: number, + ): Promise { + const outputSessionDir = this.outputSessionDirFor(taskId); + if (outputSessionDir === undefined) return ''; + return readTaskOutputBytes(outputSessionDir, taskId, offset, maxBytes); + } + + /** + * Return the output snapshot used by TaskOutput. + * + * Persisted logs are preferred when the task was registered with an + * output session directory and `output.log` has actually been created, + * because they are the complete, never-truncated source. Detached managers, + * tasks registered before a session dir was attached, and silent tasks with + * no persisted log fall back to the live ring buffer. + */ + async getOutputSnapshot( + taskId: string, + maxPreviewBytes: number, + ): Promise { + if (this.getTask(taskId) === undefined) return emptyOutputSnapshot(); + + await this.flushOutput(taskId); + + const previewLimit = Math.max(0, Math.trunc(maxPreviewBytes)); + const outputSessionDir = this.outputSessionDirFor(taskId); + if (outputSessionDir !== undefined && (await taskOutputExists(outputSessionDir, taskId))) { + const outputSizeBytes = await taskOutputSizeBytes(outputSessionDir, taskId); + const previewOffset = Math.max(0, outputSizeBytes - previewLimit); + const previewBytes = outputSizeBytes - previewOffset; + const preview = await readTaskOutputBytes( + outputSessionDir, + taskId, + previewOffset, + previewBytes, + ); + return { + outputPath: taskOutputFile(outputSessionDir, taskId), + outputSizeBytes, + previewBytes, + truncated: previewOffset > 0, + fullOutputAvailable: true, + preview, + }; + } + + const entry = this.processes.get(taskId); + if (entry === undefined) return emptyOutputSnapshot(); + + const available = Buffer.from(entry.outputChunks.join(''), 'utf-8'); + const previewBytes = Math.min(previewLimit, available.byteLength, entry.outputSizeBytes); + const previewOffset = available.byteLength - previewBytes; + return { + outputSizeBytes: entry.outputSizeBytes, + previewBytes, + truncated: entry.outputSizeBytes > previewBytes, + fullOutputAvailable: false, + preview: available.subarray(previewOffset).toString('utf-8'), + }; + } + + /** Get the combined output of a task (tail of the ring buffer). */ + getOutput(taskId: string, tail?: number): string { + const entry = this.processes.get(taskId); + if (!entry) return ''; + const full = entry.outputChunks.join(''); + if (tail !== undefined && tail < full.length) { + return full.slice(-tail); + } + return full; + } + + async readOutput(taskId: string, tail?: number): Promise { + const entry = this.processes.get(taskId); + const outputSessionDir = this.outputSessionDirFor(taskId); + if (outputSessionDir !== undefined) { + await entry?.outputWriteQueue; + const persisted = await readTaskOutput(outputSessionDir, taskId); + if (persisted.length > 0) { + if (tail !== undefined && tail < persisted.length) { + return persisted.slice(-tail); + } + return persisted; + } + } + return this.getOutput(taskId, tail); + } + + getOutputPath(taskId: string): string | undefined { + const outputSessionDir = this.outputSessionDirFor(taskId); + if (outputSessionDir === undefined) return undefined; + if (!taskOutputExistsSync(outputSessionDir, taskId)) return undefined; + return taskOutputFile(outputSessionDir, taskId); + } + + /** Stop a running task. SIGTERM → 5s grace → SIGKILL. */ + async stop(taskId: string, reason?: string): Promise { + const entry = this.processes.get(taskId); + if (!entry) return undefined; + // Normalize at this shared boundary: every public stop path (the TaskStop + // tool, SDK/RPC) funnels through here, so a blank or whitespace-only + // reason must never be recorded as an empty stopReason. + const trimmedReason = reason?.trim(); + const stopReason = + trimmedReason === undefined || trimmedReason.length === 0 ? undefined : trimmedReason; + // Terminal tasks short-circuit. awaiting_approval tasks can still + // be stopped (the approval gate is lifted when we transition to + // 'killed'). + if (TERMINAL_STATUSES.has(entry.status)) { + await entry.persistWriteQueue; + return this.toInfo(entry); + } + + entry.approvalReason = undefined; + entry.stopRequested = true; + entry.stopReason = stopReason; + + try { + await entry.proc.kill('SIGTERM'); + } catch { + /* process already gone */ + } + + // Wait up to 5s for the lifecycle path to settle, then SIGKILL. + // Waiting on lifecyclePromise, rather than proc.wait() directly, lets a + // natural completion win the race instead of being overwritten here. + let graceTimer: ReturnType | undefined; + const graceful = await Promise.race([ + entry.lifecyclePromise.then( + () => true, + () => true, + ), + new Promise((resolve) => { + graceTimer = setTimeout(() => { + resolve(false); + }, SIGTERM_GRACE_MS); + }), + ]); + if (graceTimer !== undefined) clearTimeout(graceTimer); + + if (TERMINAL_STATUSES.has(entry.status)) { + await entry.persistWriteQueue; + return this.toInfo(entry); + } + + if (!graceful && entry.proc.exitCode === null) { + try { + await entry.proc.kill('SIGKILL'); + } catch { + /* ignore */ + } + } + + if (TERMINAL_STATUSES.has(entry.status)) { + await entry.persistWriteQueue; + return this.toInfo(entry); + } + + // Agent tasks whose completion promise never settles (no timeoutMs, + // or a truly hung coroutine) need an explicit terminal finalize here. + await this.finalizeTerminal(entry, 'killed', null, { stopReason }); + + return this.toInfo(entry); + } + + async stopAll(reason?: string): Promise { + const taskIds = Array.from(this.processes.values()) + .filter((entry) => !TERMINAL_STATUSES.has(entry.status)) + .map((entry) => entry.taskId); + const results = await Promise.all(taskIds.map((taskId) => this.stop(taskId, reason))); + return results.filter((info): info is BackgroundTaskInfo => info !== undefined); + } + + /** + * Wait for a task to reach a terminal state. + * Returns immediately if already terminal. Times out after `timeoutMs`. + */ + async wait(taskId: string, timeoutMs = 30_000): Promise { + const entry = this.processes.get(taskId); + if (!entry) return undefined; + if (TERMINAL_STATUSES.has(entry.status)) { + await entry.persistWriteQueue; + return this.toInfo(entry); + } + + let terminalWaiter: (() => void) | undefined; + let timeout: ReturnType | undefined; + try { + await Promise.race([ + new Promise((resolve) => { + terminalWaiter = resolve; + entry.waiters.push(resolve); + }), + new Promise((resolve) => { + timeout = setTimeout(resolve, timeoutMs); + }), + ]); + } finally { + if (timeout !== undefined) clearTimeout(timeout); + if (terminalWaiter !== undefined) { + const index = entry.waiters.indexOf(terminalWaiter); + if (index !== -1) entry.waiters.splice(index, 1); + } + } + + if (TERMINAL_STATUSES.has(entry.status)) { + await entry.persistWriteQueue; + } + return this.toInfo(entry); + } + + /** + * Register a Promise-based agent task (no KaosProcess). Used by + * AgentTool for background subagent dispatch. Agent tasks appear in + * `list()` / `getTask()` but have pid=0 and empty output. + * + * `opts.timeoutMs` wraps the completion in an external deadline. On + * deadline fire, the task is marked `failed` with `timedOut=true` + * (distinct from a caller-driven `stop()` which uses `killed`, and + * distinct from an internal `TimeoutError` rejection which is a + * generic `failed` with `timedOut` left unset). + */ + registerAgentTask( + completion: Promise<{ result: string }>, + description: string, + opts: { + timeoutMs?: number; + abort?: () => void; + reservation?: BackgroundTaskReservation; + /** Subagent identifier; surfaced on task info. */ + agentId?: string; + /** Subagent profile name; surfaced on task info. */ + subagentType?: string; + } = {}, + ): string { + if (opts.reservation) { + opts.reservation.release(); + } else { + this.assertCanRegister(); + } + const taskId = generateTaskId('agent'); + const entry: ManagedProcess = { + taskId, + command: `[agent] ${description}`, + description, + timeoutMs: opts.timeoutMs, + // Fall back to defaults that satisfy callers reading these fields + // without forcing every call site to supply them. The dedicated + // dispatch path in AgentTool passes the real handle.agentId / + // handle.profileName. + agentId: opts.agentId ?? taskId, + subagentType: opts.subagentType ?? 'agent', + // Dummy KaosProcess — agent tasks are Promise-based, not process-based + proc: { + stdin: { write: () => false, end: () => {} } as never, + stdout: { setEncoding: () => {}, on: () => {} } as never, + stderr: { setEncoding: () => {}, on: () => {} } as never, + pid: 0, + exitCode: null, + wait: () => completion.then(() => 0), + kill: async () => { + opts.abort?.(); + }, + } as unknown as KaosProcess, + outputChunks: [], + outputSizeBytes: 0, + status: 'running', + exitCode: null, + startedAt: Date.now(), + endedAt: null, + waiters: [], + terminalFired: false, + stopRequested: false, + outputSessionDir: this.sessionDir, + lifecyclePromise: Promise.resolve(), + persistWriteQueue: Promise.resolve(), + outputWriteQueue: Promise.resolve(), + }; + this.processes.set(taskId, entry); + void this.persistLive(entry); + this.fireLifecycle('started', this.toInfo(entry)); + + // Deadline symbol distinguishes "external timeout fired" from "the + // agent promise itself rejected with TimeoutError" (which must + // remain a generic failure, not a deadline timeout). + const deadlineTimeout = Symbol('deadline-timeout'); + let deadlineTimer: ReturnType | undefined; + + const raceInputs: Array> = [completion]; + if (opts.timeoutMs !== undefined && opts.timeoutMs > 0) { + raceInputs.push( + new Promise((resolve) => { + deadlineTimer = setTimeout(() => { + resolve(deadlineTimeout); + }, opts.timeoutMs); + }), + ); + } + + const settleLifecycle = Promise.race(raceInputs) + .then(async (outcome) => { + if (outcome === deadlineTimeout) { + // External deadline fired before the agent resolved. + if (TERMINAL_STATUSES.has(entry.status)) return; + opts.abort?.(); + await this.finalizeTerminal(entry, 'failed', 1, { timedOut: true }); + return; + } + // `completion` resolved before deadline. + const r = outcome as { result: string }; + if (TERMINAL_STATUSES.has(entry.status)) return; + this.appendOutput(entry, r.result); + await this.finalizeTerminal(entry, 'completed', 0); + }) + .catch(async (error: unknown) => { + // Caller-driven stop() that ran to completion through our own + // abort callback: the rejection is an AbortError-shaped object. + // Treat as `killed` so user-initiated cancellation is recorded + // as a cancellation, not a failure. The shape check is + // load-bearing — if a non-AbortError rejection arrives while + // `stopRequested` is set, it means a real failure (e.g. a + // model error) won the race against the in-flight stop, and we + // must record that failure rather than hide it behind the + // user's cancellation. + if (entry.stopRequested && isAbortError(error)) { + await this.finalizeTerminal(entry, 'killed', null); + return; + } + // Runner-initiated cancellation: the background agent runner + // raises `RunCancelled` to signal "abort this run" (e.g. on a + // Ctrl+C path with no BPM-side stop()). Map to `killed` + // because cancellation is not a failure. + if (error instanceof Error && error.name === 'RunCancelled') { + await this.finalizeTerminal(entry, 'killed', null); + return; + } + // Internal rejection (including TimeoutError, model errors, + // and stopRequested cases where a non-abort failure won the + // race): generic failure. `timedOut` stays unset so consumers + // can distinguish this from a true external deadline. + await this.finalizeTerminal(entry, 'failed', 1); + }) + .finally(() => { + if (deadlineTimer !== undefined) clearTimeout(deadlineTimer); + }); + + entry.lifecyclePromise = settleLifecycle; + void entry.lifecyclePromise; + + return taskId; + } + + // ── awaiting_approval state transitions ──────────────────────────── + + /** + * Mark a running task as paused pending approval. The approval reason + * (tool call description) is retained until the task either returns + * to `'running'` via `clearAwaitingApproval()` or reaches a terminal + * state. Calls on terminal or unknown tasks are silently ignored so + * the ApprovalRuntime callback path is race-safe. + */ + markAwaitingApproval(taskId: string, reason: string): void { + const entry = this.processes.get(taskId); + if (!entry) return; + if (TERMINAL_STATUSES.has(entry.status)) return; + entry.status = 'awaiting_approval'; + entry.approvalReason = reason; + void this.persistLive(entry); + this.fireLifecycle('updated', this.toInfo(entry)); + } + + /** + * Drop the approval gate and return to `'running'`. Clears the stored + * reason so stale text cannot leak into a future `awaiting_approval` + * cycle. No-op unless the task is currently in the awaiting_approval + * state. + */ + clearAwaitingApproval(taskId: string): void { + const entry = this.processes.get(taskId); + if (!entry) return; + if (entry.status !== 'awaiting_approval') return; + entry.status = 'running'; + entry.approvalReason = undefined; + void this.persistLive(entry); + this.fireLifecycle('updated', this.toInfo(entry)); + } + + // ── completion event (await lifecycle end) ──────────────────────── + + /** + * Resolve when the task reaches a terminal state. If the task is + * already terminal, resolves synchronously on the next microtask. + * Intended for integration code that wants to `await` a specific + * task's exit without installing a full `onTerminal` subscriber. + * Returns `undefined` for unknown ids (matching `getTask`). Ghost + * (reconciled-lost) entries are considered terminal from the + * manager's perspective. + */ + async waitForTerminal(taskId: string): Promise { + const entry = this.processes.get(taskId); + if (entry === undefined) return this.ghosts.get(taskId); + if (TERMINAL_STATUSES.has(entry.status)) { + await entry.persistWriteQueue; + return this.toInfo(entry); + } + await new Promise((resolve) => { + entry.waiters.push(resolve); + }); + await entry.persistWriteQueue; + return this.toInfo(entry); + } + + /** Reset internal state (for testing). */ + _reset(): void { + this.processes.clear(); + this.ghosts.clear(); + this.sessionDir = undefined; + } + + // ── persistence + reconcile ──────────────────────────────────────── + + /** + * Attach the manager to a session directory for persistence. Tasks + * created via `register()` after this call are written to + * `/tasks/.json` and updated on lifecycle change. + * Tasks created before attach are NOT retroactively persisted. + */ + attachSessionDir(sessionDir: string): void { + this.sessionDir = sessionDir; + } + + /** + * Load persisted task records into the ghost map. Does NOT reconcile + * (call `reconcile()` after `loadFromDisk()`). Idempotent; subsequent + * calls overwrite the ghost map. + * + * Requires `attachSessionDir()` first; no-op otherwise. + */ + async loadFromDisk(): Promise { + if (this.sessionDir === undefined) return; + this.ghosts.clear(); + const persisted = await listTasks(this.sessionDir); + for (const t of persisted) { + // Skip ids that already exist as live processes — live wins. + if (this.processes.has(t.task_id)) continue; + this.ghosts.set(t.task_id, persistedToInfo(t)); + } + } + + /** + * Reconcile loaded ghost tasks. Any ghost with status `running` is + * reclassified as `lost` (its previous CLI process died without + * writing a terminal state). Updates the on-disk record and returns + * the lost task ids so the caller can emit user-facing notifications. + */ + protected async markLoadedTasksLost(): Promise { + const lost: string[] = []; + const lostInfo: BackgroundTaskInfo[] = []; + for (const [id, info] of this.ghosts) { + // Any non-terminal ghost is lost. Includes `awaiting_approval` + // (the approval context died with the previous process so it + // cannot be resumed). + if (TERMINAL_STATUSES.has(info.status)) continue; + const updated: BackgroundTaskInfo = { + ...info, + status: 'lost', + endedAt: info.endedAt ?? Date.now(), + approvalReason: undefined, + failureReason: 'Background worker heartbeat expired', + }; + this.ghosts.set(id, updated); + if (this.sessionDir !== undefined) { + await writeTask(this.sessionDir, infoToPersisted(updated)); + } + lost.push(id); + lostInfo.push(updated); + } + return { lost, lostInfo }; + } + + async reconcile(): Promise { + const result = await this.markLoadedTasksLost(); + // Fire onTerminal for newly-lost ghosts so NotificationManager + // receives a `task.lost` notification. Dedupe on the consumer side + // is by `dedupe_key`; a second reconcile() on the same ghost is a + // no-op because the status flips to `lost` above and we guard on + // TERMINAL_STATUSES on the next pass. + for (const info of result.lostInfo) { + this.fireTerminalSubscribers(info); + } + return result; + } + + /** Drop a persisted task from disk and ghost map. */ + async forgetTask(taskId: string): Promise { + this.ghosts.delete(taskId); + if (this.sessionDir !== undefined) { + await removeTask(this.sessionDir, taskId); + } + } + + /** + * Persist the current state of a live ManagedProcess. Called from + * `register()` and the lifecycle finally block. No-op unless attached. + */ + private persistLive(entry: ManagedProcess): Promise { + if (this.sessionDir === undefined) return Promise.resolve(); + const sessionDir = this.sessionDir; + const task: PersistedTask = { + task_id: entry.taskId, + command: entry.command, + description: entry.description, + pid: entry.proc.pid, + started_at: entry.startedAt, + ended_at: entry.endedAt, + exit_code: entry.exitCode, + status: entry.status, + approval_reason: entry.approvalReason, + timed_out: entry.timedOut, + stop_reason: entry.stopReason, + }; + entry.persistWriteQueue = entry.persistWriteQueue + .then(() => writeTask(sessionDir, task)) + .catch(() => {}); + return entry.persistWriteQueue; + } + + private appendOutput(entry: ManagedProcess, chunk: string): void { + entry.outputSizeBytes += Buffer.byteLength(chunk, 'utf-8'); + entry.outputChunks.push(chunk); + // Enforce output cap: drop oldest chunks when over budget. + let total = entry.outputChunks.reduce((s, c) => s + c.length, 0); + while (total > MAX_OUTPUT_BYTES && entry.outputChunks.length > 1) { + const removed = entry.outputChunks.shift(); + if (removed === undefined) break; + total -= removed.length; + } + + const outputSessionDir = entry.outputSessionDir; + if (outputSessionDir === undefined) return; + entry.outputWriteQueue = entry.outputWriteQueue + .then(() => appendTaskOutput(outputSessionDir, entry.taskId, chunk)) + .catch(() => {}); + } + + private outputSessionDirFor(taskId: string): string | undefined { + const entry = this.processes.get(taskId); + if (entry !== undefined) return entry.outputSessionDir; + if (this.ghosts.has(taskId)) return this.sessionDir; + return undefined; + } + + private async settleProcessExit(entry: ManagedProcess, exitCode: number): Promise { + if (TERMINAL_STATUSES.has(entry.status)) { + if (entry.status === 'killed' && entry.exitCode === null) { + entry.exitCode = exitCode; + entry.endedAt = Date.now(); + await this.persistLive(entry); + this.fireTerminalCallbacks(entry); + this.resolveWaiters(entry); + } + return; + } + const status = entry.stopRequested ? 'killed' : exitCode === 0 ? 'completed' : 'failed'; + await this.finalizeTerminal(entry, status, exitCode); + } + + private observedExitCompletions(): Promise[] { + const completions: Promise[] = []; + for (const entry of this.processes.values()) { + if (!TERMINAL_STATUSES.has(entry.status) && entry.proc.exitCode !== null) { + completions.push(entry.lifecyclePromise); + } + } + return completions; + } + + private toInfo(entry: ManagedProcess): BackgroundTaskInfo { + return { + taskId: entry.taskId, + command: entry.command, + description: entry.description, + status: entry.status, + pid: entry.proc.pid, + exitCode: entry.exitCode, + startedAt: entry.startedAt, + endedAt: entry.endedAt, + approvalReason: entry.approvalReason, + timedOut: entry.timedOut, + stopReason: entry.stopReason, + timeoutMs: entry.timeoutMs, + agentId: entry.agentId, + subagentType: entry.subagentType, + failureReason: entry.failureReason, + }; + } + + private async finalizeTerminal( + entry: ManagedProcess, + status: BackgroundTaskStatus, + exitCode: number | null, + options: { readonly timedOut?: boolean; readonly stopReason?: string } = {}, + ): Promise { + if (TERMINAL_STATUSES.has(entry.status)) return false; + entry.status = status; + entry.exitCode = exitCode; + entry.endedAt = Date.now(); + entry.timedOut = options.timedOut; + entry.stopReason = status === 'killed' ? (options.stopReason ?? entry.stopReason) : undefined; + // A task that ended while still in awaiting_approval (e.g. crashed + // mid-prompt, deadline fired, or got killed) must not leak the + // stale approvalReason onto the terminal record. The awaiting → + // running path (clearAwaitingApproval) already clears it; mirror + // that here for the awaiting → terminal path. + entry.approvalReason = undefined; + entry.stopRequested = false; + await this.persistLive(entry); + this.fireTerminalCallbacks(entry); + this.resolveWaiters(entry); + return true; + } +} + +// ── persistence shape <-> in-memory shape ────────────────────────────── + +function persistedToInfo(t: PersistedTask): BackgroundTaskInfo { + return { + taskId: t.task_id, + command: t.command, + description: t.description, + status: t.status, + pid: t.pid, + exitCode: t.exit_code, + startedAt: t.started_at, + endedAt: t.ended_at, + approvalReason: t.approval_reason, + timedOut: t.timed_out, + stopReason: t.stop_reason, + }; +} + +function infoToPersisted(info: BackgroundTaskInfo): PersistedTask { + return { + task_id: info.taskId, + command: info.command, + description: info.description, + pid: info.pid, + started_at: info.startedAt, + ended_at: info.endedAt, + exit_code: info.exitCode, + status: info.status, + approval_reason: info.approvalReason, + timed_out: info.timedOut, + stop_reason: info.stopReason, + }; +} diff --git a/packages/agent-core/src/tools/background/persist.ts b/packages/agent-core/src/tools/background/persist.ts new file mode 100644 index 000000000..bc4364e29 --- /dev/null +++ b/packages/agent-core/src/tools/background/persist.ts @@ -0,0 +1,269 @@ +/** + * Background task persistence helpers. + * + * Each task lives at `/tasks/.json` so a CLI + * restart can list previously-running tasks (now lost) and emit terminal + * notifications. + * + * Writes use `atomicWrite` (write-tmp-fsync-rename) so a crash mid-write + * never leaves a half-truncated file. + */ + +import { statSync } from 'node:fs'; +import { appendFile, mkdir, open, readFile, readdir, rm, stat, unlink } from 'node:fs/promises'; +import { dirname, join } from 'node:path'; + +import { atomicWrite } from '../../utils/fs'; +import type { BackgroundTaskStatus } from './manager'; + +/** + * Task id format: `{bash|agent}-{8 chars of [0-9a-z]}`. + * + * Strictly enforced by `taskFile()` so neither path-traversal (`../`) + * nor a legacy `bg_` format can escape through the persistence + * layer. + */ +export const VALID_TASK_ID: RegExp = /^(bash|agent)-[0-9a-z]{8}$/; + +/** On-disk task representation (snake_case, Python-friendly). */ +export interface PersistedTask { + readonly task_id: string; + readonly command: string; + readonly description: string; + readonly pid: number; + readonly started_at: number; + readonly ended_at: number | null; + readonly exit_code: number | null; + readonly status: BackgroundTaskStatus; + /** + * Reason supplied when the task is marked `awaiting_approval`. + * Cleared (omitted) when the task leaves that state. + */ + readonly approval_reason?: string | undefined; + /** + * True when an agent task was forcibly terminated by its external + * deadline (`registerAgentTask(..., { timeoutMs })`). An internal + * `TimeoutError` raised by the agent promise itself is a generic + * failure and does NOT set this flag. + */ + readonly timed_out?: boolean | undefined; + /** Reason recorded when a task is explicitly stopped. */ + readonly stop_reason?: string | undefined; + /** + * Shell origin metadata (name / path / cwd) captured when + * `BackgroundProcessManager.register` attached a `shellInfo` option. + * Persisted so restart can reconstruct the spawn environment. + */ + readonly shell_info?: + | { + readonly name: string; + readonly path?: string | undefined; + readonly cwd?: string | undefined; + } + | undefined; +} + +function tasksDirOf(sessionDir: string): string { + return join(sessionDir, 'tasks'); +} + +function taskFile(sessionDir: string, taskId: string): string { + if (!VALID_TASK_ID.test(taskId)) { + throw new Error(`Invalid task id: "${taskId}"`); + } + return join(tasksDirOf(sessionDir), `${taskId}.json`); +} + +function taskOutputDir(sessionDir: string, taskId: string): string { + if (!VALID_TASK_ID.test(taskId)) { + throw new Error(`Invalid task id: "${taskId}"`); + } + return join(tasksDirOf(sessionDir), taskId); +} + +export function taskOutputFile(sessionDir: string, taskId: string): string { + return join(taskOutputDir(sessionDir, taskId), 'output.log'); +} + +/** Atomically write a task's persisted state. Creates dirs as needed. */ +export async function writeTask(sessionDir: string, task: PersistedTask): Promise { + await mkdir(tasksDirOf(sessionDir), { recursive: true, mode: 0o700 }); + const target = taskFile(sessionDir, task.task_id); + await atomicWrite(target, JSON.stringify(task, null, 2)); +} + +/** Read a single task file. Returns undefined when missing/corrupt. */ +export async function readTask( + sessionDir: string, + taskId: string, +): Promise { + // Path-traversal validation runs before the try/catch so callers see + // an explicit error instead of a misleading "missing" return. + const path = taskFile(sessionDir, taskId); + let raw: string; + try { + raw = await readFile(path, 'utf-8'); + } catch { + return undefined; + } + try { + const parsed = JSON.parse(raw) as Record; + if (typeof parsed !== 'object' || parsed === null) return undefined; + return parsed as unknown as PersistedTask; + } catch { + return undefined; + } +} + +export async function appendTaskOutput( + sessionDir: string, + taskId: string, + chunk: string, +): Promise { + const path = taskOutputFile(sessionDir, taskId); + await mkdir(dirname(path), { recursive: true, mode: 0o700 }); + await appendFile(path, chunk, 'utf-8'); +} + +export async function readTaskOutput(sessionDir: string, taskId: string): Promise { + try { + return await readFile(taskOutputFile(sessionDir, taskId), 'utf-8'); + } catch { + return ''; + } +} + +/** + * Total byte size of a task's `output.log`. Returns 0 when the log does + * not exist yet (the task has produced no output, or is unknown). + * + * This is the authoritative full-output size — unlike the in-memory ring + * buffer it is never truncated, so callers can report how much output a + * task has actually produced. + */ +export async function taskOutputSizeBytes(sessionDir: string, taskId: string): Promise { + try { + const st = await stat(taskOutputFile(sessionDir, taskId)); + return st.size; + } catch { + return 0; + } +} + +export async function taskOutputExists(sessionDir: string, taskId: string): Promise { + try { + return (await stat(taskOutputFile(sessionDir, taskId))).isFile(); + } catch { + return false; + } +} + +export function taskOutputExistsSync(sessionDir: string, taskId: string): boolean { + try { + return statSync(taskOutputFile(sessionDir, taskId)).isFile(); + } catch { + return false; + } +} + +/** + * Read a byte window of a task's `output.log`. + * + * Reads at most `maxBytes` bytes starting at byte `offset`. A window that + * runs past EOF is clamped to whatever remains; an `offset` at/after EOF + * yields an empty string. Returns an empty string when the log is absent. + * + * Byte-level (not line-level) paging mirrors how the full log is stored + * on disk, so callers can page arbitrarily large logs without loading the + * whole file into memory. + */ +export async function readTaskOutputBytes( + sessionDir: string, + taskId: string, + offset: number, + maxBytes: number, +): Promise { + const start = Math.max(0, Math.trunc(offset)); + const limit = Math.max(0, Math.trunc(maxBytes)); + if (limit === 0) return ''; + let handle; + try { + handle = await open(taskOutputFile(sessionDir, taskId), 'r'); + } catch { + return ''; + } + try { + const size = (await handle.stat()).size; + if (start >= size) return ''; + const length = Math.min(limit, size - start); + const buffer = Buffer.allocUnsafe(length); + const { bytesRead } = await handle.read(buffer, 0, length, start); + return buffer.toString('utf-8', 0, bytesRead); + } catch { + return ''; + } finally { + await handle.close(); + } +} + +/** Enumerate all persisted tasks for a session. Skips corrupt entries. */ +export async function listTasks(sessionDir: string): Promise { + let entries: string[]; + try { + entries = await readdir(tasksDirOf(sessionDir)); + } catch { + return []; + } + const out: PersistedTask[] = []; + for (const entry of entries) { + if (!entry.endsWith('.json')) continue; + const taskId = entry.slice(0, -'.json'.length); + // Silently drop: filename basename is not a valid task id (stray files, + // legacy bg_* leftovers, etc.). + if (!VALID_TASK_ID.test(taskId)) continue; + const task = await readTask(sessionDir, taskId); + // Silently drop: JSON parse failed or file disappeared between readdir + // and readTask. writeTask uses an atomic temp+rename pattern so a + // genuinely truncated file in production is rare; if it happens we + // accept the loss rather than emit a ghost with no recoverable + // metadata beyond the filename. + if (task === undefined) continue; + // Silently drop: parsed JSON is missing one or more required fields + // for a PersistedTask. Treated the same as a missing file. + if (!isValidPersistedTask(task)) continue; + out.push(task); + } + return out; +} + +/** + * Validate that the parsed JSON actually shapes like a PersistedTask. + * Cheap shape check (not a full zod schema) — rejects the canonical + * "spec with missing fields" failure mode. + */ +function isValidPersistedTask(obj: unknown): obj is PersistedTask { + if (typeof obj !== 'object' || obj === null) return false; + const o = obj as Record; + return ( + typeof o['task_id'] === 'string' && + typeof o['command'] === 'string' && + typeof o['description'] === 'string' && + typeof o['pid'] === 'number' && + typeof o['started_at'] === 'number' && + (o['ended_at'] === null || typeof o['ended_at'] === 'number') && + (o['exit_code'] === null || typeof o['exit_code'] === 'number') && + typeof o['status'] === 'string' + ); +} + +/** Remove a task file (idempotent). */ +export async function removeTask(sessionDir: string, taskId: string): Promise { + // Path-traversal validation outside try/catch. + const path = taskFile(sessionDir, taskId); + try { + await unlink(path); + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error; + } + await rm(taskOutputDir(sessionDir, taskId), { recursive: true, force: true }); +} diff --git a/packages/agent-core/src/tools/background/task-list.md b/packages/agent-core/src/tools/background/task-list.md new file mode 100644 index 000000000..c2826c1ef --- /dev/null +++ b/packages/agent-core/src/tools/background/task-list.md @@ -0,0 +1,24 @@ +List background tasks and their current status. + +Use this tool to discover which background tasks exist and where each one +stands. It is the entry point for inspecting background work: it returns a +task ID, status, command, description, and PID for every task it reports, +plus the exit code and stop reason for tasks that have already finished. + +Guidelines: + +- After a context compaction, or whenever you are unsure which background + tasks are running or what their task IDs are, call this tool to + re-enumerate them instead of guessing a task ID. +- Prefer the default `active_only=true`, which lists only non-terminal tasks. + Pass `active_only=false` only when you specifically need to see tasks that + have already finished. With `active_only=false` the result may also include + `lost` tasks — tasks left over from a previous process that can no longer be + inspected or controlled; treat them as already terminated. +- `limit` caps how many tasks are returned. It accepts a value between 1 and + 100 and defaults to 20 when omitted. +- This tool only lists tasks; it does not return their output. Use it first + to locate the task ID you need, then call `TaskOutput` with that ID to read + the task's output and details. +- This tool is read-only and does not change any state, so it is always safe + to call, including in plan mode. diff --git a/packages/agent-core/src/tools/background/task-list.ts b/packages/agent-core/src/tools/background/task-list.ts new file mode 100644 index 000000000..1a8c5ccd5 --- /dev/null +++ b/packages/agent-core/src/tools/background/task-list.ts @@ -0,0 +1,83 @@ +/** + * TaskListTool — list background tasks. + */ + +import { z } from 'zod'; + +import type { BuiltinTool } from '../../agent/tool'; +import type { ToolExecution } from '../../loop/types'; +import { toInputJsonSchema } from '../support/input-schema'; +import type { BackgroundProcessManager, BackgroundTaskInfo } from './manager'; +import { isBackgroundTaskTerminal } from './manager'; +import TASK_LIST_DESCRIPTION from './task-list.md'; + +// ── Input schema ───────────────────────────────────────────────────── + +export const TaskListInputSchema = z.object({ + active_only: z + .boolean() + .optional() + .default(true) + .describe('Whether to list only non-terminal background tasks.'), + limit: z + .number() + .int() + .min(1) + .max(100) + .default(20) + .describe('Maximum number of tasks to return.') + .optional(), +}); + +export type TaskListInput = z.Infer; + +// ── Implementation ─────────────────────────────────────────────────── + +function formatTask(t: BackgroundTaskInfo): string { + const lines = [ + `task_id: ${t.taskId}`, + `status: ${t.status}`, + `command: ${t.command}`, + `description: ${t.description}`, + `pid: ${String(t.pid ?? 'N/A')}`, + ]; + // Terminal tasks carry an outcome the AI needs to act on: the process + // exit code, and — when the task was ended via TaskStop — the stop reason. + if (isBackgroundTaskTerminal(t.status)) { + if (t.exitCode !== null) lines.push(`exit_code: ${String(t.exitCode)}`); + if (t.stopReason !== undefined) lines.push(`reason: ${t.stopReason}`); + } + return lines.join('\n'); +} + +function formatTaskList(tasks: BackgroundTaskInfo[], activeOnly: boolean): string { + // `active_only=false` mixes in terminal/lost tasks, so the count is no + // longer purely "active" — use a neutral label to avoid mislabeling them. + const label = activeOnly ? 'active_background_tasks' : 'background_tasks'; + const header = `${label}: ${String(tasks.length)}`; + if (tasks.length === 0) return `${header}\nNo background tasks found.`; + return `${header}\n${tasks.map(formatTask).join('\n---\n')}`; +} + +export class TaskListTool implements BuiltinTool { + readonly name = 'TaskList' as const; + readonly description = TASK_LIST_DESCRIPTION; + readonly parameters: Record = toInputJsonSchema(TaskListInputSchema); + + constructor(private readonly manager: BackgroundProcessManager) {} + + resolveExecution(args: TaskListInput): ToolExecution { + return { + description: 'Listing background tasks', + execute: async () => { + await this.manager.settlePendingExits(); + const activeOnly = args.active_only ?? true; + const tasks = this.manager.list(activeOnly, args.limit ?? 20); + return { + output: formatTaskList(tasks, activeOnly), + isError: false, + }; + }, + }; + } +} diff --git a/packages/agent-core/src/tools/background/task-output.md b/packages/agent-core/src/tools/background/task-output.md new file mode 100644 index 000000000..395f25379 --- /dev/null +++ b/packages/agent-core/src/tools/background/task-output.md @@ -0,0 +1,12 @@ +Retrieve output from a running or completed background task. + +Use this after `Bash(run_in_background=true)` when you need to inspect progress or explicitly wait for completion. + +Guidelines: +- Prefer relying on automatic completion notifications. Use this tool only when you need task output before the automatic notification arrives. +- By default this tool is non-blocking and returns a current status/output snapshot. +- Use block=true only when you intentionally want to wait for completion or timeout. +- This tool returns structured task metadata, a fixed-size output preview, and an output_path for the full log. +- For a terminal task, the metadata also explains why it ended: `timed_out` when an agent task was aborted by its deadline, and `stop_reason` when the task was explicitly stopped. `terminal_reason` is a categorical label for the same event — its value is `timed_out` or `stopped` — and is emitted alongside the matching `timed_out` / `stop_reason` field. A task that ended on its own emits none of these three fields. +- The full, never-truncated log is always available at output_path; use the `Read` tool with that path to page through it, whether or not the preview was truncated. +- This tool works with the generic background task system and should remain the primary read path for future task types, not just bash. diff --git a/packages/agent-core/src/tools/background/task-output.ts b/packages/agent-core/src/tools/background/task-output.ts new file mode 100644 index 000000000..f77de21bf --- /dev/null +++ b/packages/agent-core/src/tools/background/task-output.ts @@ -0,0 +1,179 @@ +/** + * TaskOutputTool — read output from a background task. + * + * Returns structured task metadata plus a fixed-size tail preview of the + * task's output. The full, never-truncated output lives on disk at + * `output_path`; the caller is always pointed at the `Read` tool to page + * through the complete log, and the preview also carries a banner when it + * has been truncated to a tail. + * + * For terminal tasks the output also surfaces why the task ended: + * `timed_out` when an agent task was aborted by its deadline, and + * `stop_reason` when the task was explicitly stopped via `TaskStop`. + */ + +import { z } from 'zod'; + +import type { BuiltinTool } from '../../agent/tool'; +import type { ExecutableToolResult, ToolExecution } from '../../loop/types'; +import { + isBackgroundTaskTerminal, + type BackgroundProcessManager, + type BackgroundTaskStatus, +} from './manager'; +import { toInputJsonSchema } from '../support/input-schema'; +import TASK_OUTPUT_DESCRIPTION from './task-output.md'; + +/** + * Maximum bytes of output included inline as a preview. Output larger + * than this is truncated to its tail; the full log is read separately + * via the `Read` tool with the returned `output_path`. + */ +const OUTPUT_PREVIEW_BYTES = 32 * 1024; // 32 KiB + +/** Number of lines the paging hint suggests reading per `Read` call. */ +const PAGING_HINT_LINES = 300; + +// ── Input schema ───────────────────────────────────────────────────── + +export const TaskOutputInputSchema = z.object({ + task_id: z.string().describe('The background task ID to inspect.'), + block: z + .boolean() + .default(false) + .describe('Whether to wait for the task to finish before returning.') + .optional(), + timeout: z + .number() + .int() + .min(0) + .max(3600) + .default(30) + .describe('Maximum number of seconds to wait when block=true.') + .optional(), +}); + +export type TaskOutputInput = z.Infer; + +// ── Implementation ─────────────────────────────────────────────────── + +function retrievalStatus( + status: BackgroundTaskStatus, + block: boolean | undefined, +): 'success' | 'timeout' | 'not_ready' { + if (isBackgroundTaskTerminal(status)) return 'success'; + return block ? 'timeout' : 'not_ready'; +} + +export class TaskOutputTool implements BuiltinTool { + readonly name = 'TaskOutput' as const; + readonly description: string = TASK_OUTPUT_DESCRIPTION; + readonly parameters: Record = toInputJsonSchema(TaskOutputInputSchema); + + constructor(private readonly manager: BackgroundProcessManager) {} + + resolveExecution(args: TaskOutputInput): ToolExecution { + return { + description: `Reading output of task ${args.task_id}`, + execute: () => this.execute(args), + }; + } + + private async execute(args: TaskOutputInput): Promise { + await this.manager.settlePendingExits(); + const info = this.manager.getTask(args.task_id); + if (!info) { + return { isError: true, output: `Task not found: ${args.task_id}` }; + } + + if (args.block && !isBackgroundTaskTerminal(info.status)) { + await this.manager.wait(args.task_id, (args.timeout ?? 30) * 1000); + } + + // Re-fetch after potential wait. + const current = this.manager.getTask(args.task_id); + if (!current) { + return { isError: true, output: `Task not found: ${args.task_id}` }; + } + + // A single manager-owned snapshot drives the tail window and every + // reported metric below. Persisted logs remain authoritative when + // available; detached managers fall back to their live ring buffer. + const output = await this.manager.getOutputSnapshot(args.task_id, OUTPUT_PREVIEW_BYTES); + + const lines = [ + `retrieval_status: ${retrievalStatus(current.status, args.block)}`, + `task_id: ${current.taskId}`, + `status: ${current.status}`, + `description: ${current.description}`, + `command: ${current.command}`, + ]; + if (output.outputPath !== undefined) { + lines.push(`output_path: ${output.outputPath}`); + } + if (current.exitCode !== null) { + lines.push(`exit_code: ${String(current.exitCode)}`); + } + // Surface why a terminal task ended. `terminal_reason` is a categorical + // label; `timed_out` / `stop_reason` carry the concrete detail. + // - timed_out: an agent task aborted by its external deadline. + // - stopped: the task was explicitly cancelled via `TaskStop` + // (`stop_reason` is the reason text supplied there). + // A task that ended on its own (completed / failed / lost) emits none + // of these so the absence is itself meaningful. + if (current.timedOut === true) { + lines.push('timed_out: true', 'terminal_reason: timed_out'); + } else if (current.stopReason !== undefined) { + lines.push(`stop_reason: ${current.stopReason}`, 'terminal_reason: stopped'); + } + lines.push( + `output_size_bytes: ${String(output.outputSizeBytes)}`, + `output_preview_bytes: ${String(output.previewBytes)}`, + `output_truncated: ${String(output.truncated)}`, + ); + // The full, never-truncated log is readable from disk via the `Read` + // tool whenever it was persisted. Surface that guidance unconditionally + // — even when the preview already shows everything — so the model knows + // it can page the complete output. The hint text adapts to whether the + // preview was truncated. When no full log was persisted, say so instead. + if (output.fullOutputAvailable && output.outputPath !== undefined) { + lines.push('full_output_available: true', 'full_output_tool: Read'); + lines.push( + output.truncated + ? `full_output_hint: Only the last ${String(OUTPUT_PREVIEW_BYTES)} bytes are shown ` + + 'above. Use the Read tool with the output_path to page through the full log ' + + `(parameters: path, line_offset, n_lines; read about ${String(PAGING_HINT_LINES)} ` + + 'lines per page).' + : 'full_output_hint: The preview above is the complete output. Use the Read tool ' + + 'with the output_path if you need to re-read the full log later ' + + `(parameters: path, line_offset, n_lines; read about ${String(PAGING_HINT_LINES)} ` + + 'lines per page).', + ); + } else { + lines.push('full_output_available: false'); + } + + // When the preview omits the head of the log, emit an explicit + // banner just before the `[output]` marker so the model knows it is + // looking at a tail, not the full output. + lines.push(''); + if (output.truncated) { + lines.push( + output.fullOutputAvailable && output.outputPath !== undefined + ? `[Truncated. Full output: ${output.outputPath}]` + : '[Truncated. No persisted full log is available for this task.]', + ); + } + lines.push('[output]', output.preview || '[no output available]'); + + // Side-channel brief for the host UI / log readers. Distinct from + // the `output` body which is parsed by the LLM. Kept short so log + // readers can render it as a one-liner. + return { + output: lines.join('\n'), + isError: false, + message: 'Task snapshot retrieved.', + }; + } + +} diff --git a/packages/agent-core/src/tools/background/task-stop.md b/packages/agent-core/src/tools/background/task-stop.md new file mode 100644 index 000000000..c1ff20a01 --- /dev/null +++ b/packages/agent-core/src/tools/background/task-stop.md @@ -0,0 +1,13 @@ +Stop a running background task. + +Only use this when a task must genuinely be cancelled — for a task that is +finishing normally, wait for its completion notification or inspect it with +`TaskOutput` instead of stopping it. + +Guidelines: +- This is a general-purpose stop capability for any background task. It is not + a bash-specific kill. +- Stopping a task is destructive: it may leave partial side effects behind. + Use it with care. +- If the task has already finished, this tool simply returns its current + status. diff --git a/packages/agent-core/src/tools/background/task-stop.ts b/packages/agent-core/src/tools/background/task-stop.ts new file mode 100644 index 000000000..20b93fa72 --- /dev/null +++ b/packages/agent-core/src/tools/background/task-stop.ts @@ -0,0 +1,87 @@ +/** + * TaskStopTool — stop a running background task. + */ + +import { z } from 'zod'; + +import type { BuiltinTool } from '../../agent/tool'; +import type { ToolExecution } from '../../loop/types'; +import { toInputJsonSchema } from '../support/input-schema'; +import { isBackgroundTaskTerminal, type BackgroundProcessManager } from './manager'; +import TASK_STOP_DESCRIPTION from './task-stop.md'; + +// ── Input schema ───────────────────────────────────────────────────── + +export const TaskStopInputSchema = z.object({ + task_id: z.string().describe('The background task ID to stop.'), + reason: z + .string() + .default('Stopped by TaskStop') + .describe('Short reason recorded when the task is stopped.') + .optional(), +}); + +export type TaskStopInput = z.Infer; + +// ── Implementation ─────────────────────────────────────────────────── + +export class TaskStopTool implements BuiltinTool { + readonly name = 'TaskStop' as const; + readonly description = TASK_STOP_DESCRIPTION; + readonly parameters: Record = toInputJsonSchema(TaskStopInputSchema); + + constructor(private readonly manager: BackgroundProcessManager) {} + + resolveExecution(args: TaskStopInput): ToolExecution { + return { + description: `Stopping task ${args.task_id}`, + execute: async () => { + await this.manager.settlePendingExits(); + const info = this.manager.getTask(args.task_id); + if (!info) { + return { isError: true, output: `Task not found: ${args.task_id}` }; + } + + // A blank or whitespace-only reason falls back to the default. `?? default` + // would not cover the empty-string case, so trim and coalesce explicitly. + const trimmedReason = args.reason?.trim(); + const reason = + trimmedReason === undefined || trimmedReason.length === 0 + ? 'Stopped by TaskStop' + : trimmedReason; + + if (isBackgroundTaskTerminal(info.status)) { + // Already-terminal tasks report their current state using the same + // structured multi-line format as the normal stop path below. + return { + output: + `task_id: ${info.taskId}\n` + + `status: ${info.status}\n` + + // A task persisted by an older build may carry a blank stopReason; + // `??` would not coalesce `''`, so trim-and-`||` to the placeholder. + `reason: ${terminalStopReason(info.stopReason)}`, + isError: false, + }; + } + + const result = await this.manager.stop(args.task_id, reason); + if (!result) { + return { isError: true, output: `Failed to stop task: ${args.task_id}` }; + } + + return { + output: + `task_id: ${result.taskId}\n` + + `status: ${result.status}\n` + + `reason: ${result.stopReason ?? reason}`, + isError: false, + }; + }, + }; + } +} + +function terminalStopReason(reason: string | undefined): string { + const trimmed = reason?.trim(); + return trimmed === undefined || trimmed.length === 0 ? 'Task already in terminal state' : trimmed; +} diff --git a/packages/agent-core/src/tools/builtin/collaboration/agent-background-disabled.md b/packages/agent-core/src/tools/builtin/collaboration/agent-background-disabled.md new file mode 100644 index 000000000..d44efcddc --- /dev/null +++ b/packages/agent-core/src/tools/builtin/collaboration/agent-background-disabled.md @@ -0,0 +1 @@ +Background agent execution is disabled for this agent. Do not set `run_in_background=true`. \ No newline at end of file diff --git a/packages/agent-core/src/tools/builtin/collaboration/agent-background-enabled.md b/packages/agent-core/src/tools/builtin/collaboration/agent-background-enabled.md new file mode 100644 index 000000000..0732a8cc3 --- /dev/null +++ b/packages/agent-core/src/tools/builtin/collaboration/agent-background-enabled.md @@ -0,0 +1,3 @@ +When `run_in_background=true`, the subagent runs detached from this turn. The completion arrives in a later turn as a synthetic user-role message containing its result — you do not need to poll, sleep, or check on its progress. Continue with other work or respond to the user. Never fabricate or predict what the result will say. + +For a background task, when `timeout` is omitted it falls back to the operator-configured background timeout, if one is set. If the operator has not configured a background timeout, an omitted `timeout` means the task runs with no time limit. diff --git a/packages/agent-core/src/tools/builtin/collaboration/agent.md b/packages/agent-core/src/tools/builtin/collaboration/agent.md new file mode 100644 index 000000000..0c6bae3bf --- /dev/null +++ b/packages/agent-core/src/tools/builtin/collaboration/agent.md @@ -0,0 +1,15 @@ +Launch a subagent to handle a task. The subagent runs as a same-process loop instance with its own context and wire file. + +Writing the prompt: +- The subagent starts with zero context — it has not seen this conversation. Brief it like a colleague who just walked into the room: state the goal, list what you already know, hand over the specifics. +- Lookups (read this file, run that test): put the exact path or command in the prompt. The subagent should not have to search for things you already know. +- Investigations (figure out X, find why Y): give the question, not prescribed steps — fixed steps become dead weight when the premise is wrong. +- Do not delegate understanding. If the task hinges on a file path or line number, find it yourself first and write it into the prompt. + +Usage notes: +- When the task continues earlier work a subagent already did, prefer resuming that agent (pass its `resume` id) over spawning a fresh instance — the resumed agent keeps its prior context. +- A subagent's result is only visible to you, not to the user. When the user needs to see what a subagent produced, summarize the relevant parts yourself in your own reply. + +When NOT to use Agent: skip delegation for trivial work you can do directly — reading a file whose path you already know, searching a small known set of files, or any task that takes only a step or two. Delegation has a context-handoff cost; it pays off only when the task is substantial enough to outweigh it. + +Once a subagent is running, leave that scope to it: do not redo its searches or reads in parallel, and do not abandon it midway and finish the job manually. Both undo the context savings the delegation was meant to buy. \ No newline at end of file diff --git a/packages/agent-core/src/tools/builtin/collaboration/agent.ts b/packages/agent-core/src/tools/builtin/collaboration/agent.ts new file mode 100644 index 000000000..062a1ff00 --- /dev/null +++ b/packages/agent-core/src/tools/builtin/collaboration/agent.ts @@ -0,0 +1,334 @@ +/** + * AgentTool — collaboration tool for spawning task subagents. + * + * Unlike the built-in tools (Read/Write/Edit/Bash/Grep/Glob), this is a + * "collaboration tool". It uses `SessionSubagentHost` (injected via the + * constructor rather than through the Runtime) to create in-process subagent + * loop instances. + * + * Two modes: + * - **Foreground** (default): blocks the parent turn, `await handle.completion` + * - **Background**: returns the agent id immediately; the result is delivered + * via a notification. + * + * `ToolResult.content` is textual; the structured output exposed by + * `AgentToolOutputSchema` is only used for drift-guard and is not consumed at + * runtime. + */ + +import { z } from 'zod'; + +import type { BuiltinTool } from '../../../agent/tool'; +import type { Logger } from '../../../logging'; +import { ToolAccesses } from '../../../loop/tool-access'; +import type { ExecutableToolContext, ExecutableToolResult, ToolExecution } from '../../../loop/types'; +import type { ResolvedAgentProfile } from '../../../profile'; +import type { SessionSubagentHost, SubagentHandle } from '../../../session/subagent-host'; +import { createDeadlineAbortSignal, type DeadlineAbortSignal } from '../../../utils/abort'; +import type { BackgroundProcessManager } from '../../background/manager'; +import { toInputJsonSchema } from '../../support/input-schema'; +import AGENT_BACKGROUND_DISABLED_DESCRIPTION from './agent-background-disabled.md'; +import AGENT_BACKGROUND_DESCRIPTION from './agent-background-enabled.md'; +import AGENT_DESCRIPTION_BASE from './agent.md'; + +// ── AgentTool input ────────────────────────────────────────────────── + +export const AgentToolInputSchema = z.preprocess( + (input) => { + if (typeof input !== 'object' || input === null || Array.isArray(input)) { + return input; + } + const record = input as Record; + const normalized = { ...record }; + const hasResumeId = + typeof normalized['resume'] === 'string' && normalized['resume'].trim().length > 0; + const hasSubagentType = + typeof normalized['subagent_type'] === 'string' && normalized['subagent_type'].length > 0; + if (!hasSubagentType && !hasResumeId) { + normalized['subagent_type'] = 'coder'; + } else if (!hasSubagentType) { + delete normalized['subagent_type']; + } + return normalized; + }, + z.object({ + prompt: z.string().describe('Full task prompt for the subagent'), + description: z.string().describe('Short task description (3-5 words) for UI display'), + subagent_type: z + .string() + .optional() + .describe( + 'One of the available agent types (see "Available agent types" in this tool description). Defaults to "coder" when omitted.', + ), + resume: z + .string() + .optional() + .describe('Optional agent ID to resume instead of creating a new instance'), + run_in_background: z + .boolean() + .optional() + .describe( + 'If true, return immediately without waiting for completion. Prefer false unless the task can run independently and there is a clear benefit to not waiting.', + ), + timeout: z + .number() + .int() + .min(30) + .max(3600) + .optional() + .describe( + 'Timeout in seconds for the agent task (min 30s, max 3600s / 1hr). When omitted, a foreground task runs until completion with no timeout. The agent is stopped if it exceeds this limit.', + ), + }), +); + +export type AgentToolInput = z.infer; + +// ── AgentTool output ───────────────────────────────────────────────── + +export const AgentToolOutputSchema = z.object({ + result: z.string().describe('Aggregated text output from the subagent'), + usage: z + .object({ + input: z.number().int().nonnegative(), + output: z.number().int().nonnegative(), + cache_read: z.number().int().nonnegative().optional(), + cache_write: z.number().int().nonnegative().optional(), + }) + .describe('Cumulative token usage'), +}); + +export type AgentToolOutput = z.infer; + +const BACKGROUND_AGENT_UNAVAILABLE = + 'Background agent execution is not available for this agent because TaskList, TaskOutput, and TaskStop are not enabled.'; + +// ── AgentTool class ────────────────────────────────────────────────── + +export class AgentTool implements BuiltinTool { + readonly name: string = 'Agent'; + readonly description: string; + readonly parameters: Record = toInputJsonSchema(AgentToolInputSchema); + private readonly allowBackground: boolean; + + constructor( + private readonly subagentHost: SessionSubagentHost, + private readonly backgroundManager?: BackgroundProcessManager | undefined, + subagents?: ResolvedAgentProfile['subagents'] | undefined, + options?: { + allowBackground?: boolean; + log?: Logger; + }, + ) { + this.allowBackground = options?.allowBackground ?? this.backgroundManager !== undefined; + const log = options?.log; + const typeLines = buildSubagentDescriptions(subagents); + const baseDescription = `${AGENT_DESCRIPTION_BASE}\n\n${ + this.allowBackground ? AGENT_BACKGROUND_DESCRIPTION : AGENT_BACKGROUND_DISABLED_DESCRIPTION + }`; + this.description = typeLines + ? `${baseDescription}\n\nAvailable agent types (pass via subagent_type):\n${typeLines}` + : baseDescription; + this.log = log; + } + + private readonly log?: Logger; + + resolveExecution(args: AgentToolInput): ToolExecution { + let profileName = args.subagent_type?.length ? args.subagent_type : 'coder'; + const resumeAgentId = args.resume?.trim(); + if (resumeAgentId !== undefined && resumeAgentId.length > 0) { + profileName = this.subagentHost.getProfileName?.(resumeAgentId) ?? 'subagent'; + } + const prefix = args.run_in_background === true ? 'Launching background' : 'Launching'; + return { + description: `${prefix} ${profileName} agent: ${args.description}`, + accesses: ToolAccesses.none(), + execute: (ctx) => this.execution(args, ctx), + }; + } + + private async execution( + args: AgentToolInput, + { + toolCallId, + signal, + }: ExecutableToolContext, + ): Promise { + let foregroundDeadline: DeadlineAbortSignal | undefined; + try { + signal.throwIfAborted(); + const runInBackground = args.run_in_background === true; + const requestedProfileName = args.subagent_type?.length ? args.subagent_type : undefined; + const resumeAgentId = args.resume?.trim(); + if ( + resumeAgentId !== undefined && + resumeAgentId.length > 0 && + requestedProfileName !== undefined + ) { + return { + output: 'Cannot set subagent_type when resuming an existing agent. Resume by agent id only.', + isError: true, + }; + } + + let reservation: ReturnType | undefined; + let backgroundManager: BackgroundProcessManager | undefined; + if (runInBackground) { + const configuredBackgroundManager = this.backgroundManager; + if (!this.allowBackground || configuredBackgroundManager === undefined) { + return { + output: BACKGROUND_AGENT_UNAVAILABLE, + isError: true, + }; + } + try { + reservation = configuredBackgroundManager.reserveSlot(); + backgroundManager = configuredBackgroundManager; + } catch (error) { + return { + output: error instanceof Error ? error.message : String(error), + isError: true, + }; + } + } + const backgroundController = runInBackground ? new AbortController() : undefined; + const timeoutMs = args.timeout === undefined ? undefined : args.timeout * 1000; + foregroundDeadline = + !runInBackground && timeoutMs !== undefined + ? createDeadlineAbortSignal(signal, timeoutMs) + : undefined; + + const options = { + parentToolCallId: toolCallId, + prompt: args.prompt, + description: args.description, + runInBackground, + signal: backgroundController?.signal ?? foregroundDeadline?.signal ?? signal, + }; + + let handle: SubagentHandle; + const operation = resumeAgentId !== undefined && resumeAgentId.length > 0 ? 'resume' : 'spawn'; + try { + if (resumeAgentId !== undefined && resumeAgentId.length > 0) { + handle = await this.subagentHost.resume(resumeAgentId, options); + } else { + const profileName = requestedProfileName ?? 'coder'; + handle = await this.subagentHost.spawn(profileName, options); + } + } catch (error) { + reservation?.release(); + this.log?.warn('subagent launch failed', { + toolCallId, + runInBackground, + operation, + agentId: resumeAgentId, + subagentType: operation === 'spawn' ? requestedProfileName ?? 'coder' : undefined, + error, + }); + throw error; + } + + if (runInBackground) { + if (backgroundManager === undefined) { + reservation?.release(); + return { + output: BACKGROUND_AGENT_UNAVAILABLE, + isError: true, + }; + } + let taskId: string; + try { + taskId = backgroundManager.registerAgentTask(handle.completion, args.description, { + timeoutMs: timeoutMs ?? this.subagentHost.backgroundTaskTimeoutMs, + reservation, + agentId: handle.agentId, + subagentType: handle.profileName, + abort: () => { + backgroundController?.abort(); + }, + }); + } catch (error) { + reservation?.release(); + backgroundController?.abort(); + void handle.completion.catch(() => {}); + this.log?.warn('background agent task registration failed', { + toolCallId, + agentId: handle.agentId, + subagentType: handle.profileName, + error, + }); + return { + output: error instanceof Error ? error.message : String(error), + isError: true, + }; + } + const lines = [ + `task_id: ${taskId}`, + 'status: running', + `agent_id: ${handle.agentId}`, + `actual_subagent_type: ${handle.profileName}`, + 'automatic_notification: true', + '', + `description: ${args.description}`, + '', + `next_step: The completion arrives automatically in a later turn — no polling needed. To peek at progress without blocking, call TaskOutput(task_id="${taskId}", block=false).`, + `resume_hint: To continue this same subagent instance later, call Agent(resume="${handle.agentId}", prompt="...").`, + ]; + return { output: lines.join('\n') }; + } + + try { + const result = await handle.completion; + const lines = [ + `agent_id: ${handle.agentId}`, + `actual_subagent_type: ${handle.profileName}`, + 'status: completed', + '', + '[summary]', + result.result, + ]; + return { output: lines.join('\n') }; + } catch (error) { + const message = + foregroundDeadline?.timedOut() === true && args.timeout !== undefined + ? `Agent timed out after ${args.timeout}s.` + : error instanceof Error + ? error.message + : String(error); + const lines = [ + `agent_id: ${handle.agentId}`, + `actual_subagent_type: ${handle.profileName}`, + 'status: failed', + '', + `subagent error: ${message}`, + ]; + return { output: lines.join('\n'), isError: true }; + } + } catch (error) { + const message = + foregroundDeadline?.timedOut() === true && args.timeout !== undefined + ? `Agent timed out after ${args.timeout}s.` + : error instanceof Error + ? error.message + : String(error); + return { output: `subagent error: ${message}`, isError: true }; + } finally { + foregroundDeadline?.clear(); + } + } +} + +function buildSubagentDescriptions(subagents: ResolvedAgentProfile['subagents']): string { + if (subagents === undefined) return ''; + return Object.entries(subagents) + .map(([name, subagent]) => { + const details = [subagent.description, subagent.whenToUse].filter( + (part): part is string => part !== undefined && part.length > 0, + ); + const header = details.length === 0 ? `- ${name}` : `- ${name}: ${details.join(' ')}`; + if (subagent.tools.length === 0) return header; + return `${header}\n Tools: ${subagent.tools.join(', ')}`; + }) + .join('\n'); +} diff --git a/packages/agent-core/src/tools/builtin/collaboration/ask-user.md b/packages/agent-core/src/tools/builtin/collaboration/ask-user.md new file mode 100644 index 000000000..597cd8eb9 --- /dev/null +++ b/packages/agent-core/src/tools/builtin/collaboration/ask-user.md @@ -0,0 +1,19 @@ +Use this tool when you need to ask the user questions with structured options during execution. This allows you to: +1. Collect user preferences or requirements before proceeding +2. Resolve ambiguous or underspecified instructions +3. Let the user decide between implementation approaches as you work +4. Present concrete options when multiple valid directions exist + +**When NOT to use:** +- When you can infer the answer from context — be decisive and proceed +- Trivial decisions that don't materially affect the outcome + +Overusing this tool interrupts the user's flow. Only use it when the user's input genuinely changes your next action. + +**Usage notes:** +- Users always have an "Other" option for custom input — don't create one yourself +- Use multi_select to allow multiple answers to be selected for a question +- Keep option labels concise (1-5 words), use descriptions for trade-offs and details +- Each question should have 2-4 meaningful, distinct options +- You can ask 1-4 questions at a time; group related questions to minimize interruptions +- If you recommend a specific option, list it first and append "(Recommended)" to its label \ No newline at end of file diff --git a/packages/agent-core/src/tools/builtin/collaboration/ask-user.ts b/packages/agent-core/src/tools/builtin/collaboration/ask-user.ts new file mode 100644 index 000000000..a1dbf6179 --- /dev/null +++ b/packages/agent-core/src/tools/builtin/collaboration/ask-user.ts @@ -0,0 +1,187 @@ +/** + * AskUserQuestionTool — structured user question tool. + * + * The LLM calls this tool when it needs structured input from the user + * (multiple-choice, preference selection, disambiguation). The tool + * delegates to the SDK reverse-RPC question handler, which owns the + * actual UI interaction. + * + * Permission policy decides whether this tool is available for the + * current mode. Once executed, it dispatches through `requestQuestion` + * and awaits the user's answer. + */ + +import { z } from 'zod'; + +import type { Agent } from '../../../agent'; +import type { BuiltinTool } from '../../../agent/tool'; +import { ErrorCodes, KimiError } from '../../../errors'; +import { isAbortError } from '../../../loop/errors'; +import type { ExecutableToolContext, ExecutableToolResult, ToolExecution } from '../../../loop/types'; +import type { + QuestionAnswers, + QuestionAnswerMethod, + QuestionResponse, + QuestionResult, +} from '../../../rpc'; +import type { TelemetryPropertyValue } from '../../../telemetry'; +import { toInputJsonSchema } from '../../support/input-schema'; +import DESCRIPTION from './ask-user.md'; + +// ── Input schema ───────────────────────────────────────────────────── + +const QuestionOptionSchema = z.object({ + label: z + .string() + .describe("Concise display text (1-5 words). If recommended, append '(Recommended)'."), + description: z.string().default('').describe('Brief explanation of trade-offs or implications.'), +}); + +const QuestionItemSchema = z.object({ + question: z.string().describe("A specific, actionable question. End with '?'."), + header: z + .string() + .default('') + .describe("Short category tag (max 12 chars, e.g. 'Auth', 'Style')."), + options: z + .array(QuestionOptionSchema) + .min(2) + .max(4) + .describe( + "2-4 meaningful, distinct options. Do NOT include an 'Other' option — the system adds one automatically.", + ), + multi_select: z + .boolean() + .default(false) + .describe('Whether the user can select multiple options.'), +}); + +export interface AskUserQuestionInput { + questions: Array<{ + question: string; + header: string; + options: Array<{ label: string; description: string }>; + multi_select: boolean; + }>; +} + +export const AskUserQuestionInputSchema: z.ZodType = z.object({ + questions: z + .array(QuestionItemSchema) + .min(1) + .max(4) + .describe('The questions to ask the user (1-4 questions).'), +}); + +const QUESTION_DISMISSED_MESSAGE = 'User dismissed the question without answering.'; + +const QUESTION_UNSUPPORTED_FAILURE_MESSAGE = + 'The connected client does not support interactive questions. Do NOT call this tool again. Ask the user directly in your text response instead.'; + +// ── Implementation ─────────────────────────────────────────────────── + +export class AskUserQuestionTool implements BuiltinTool { + readonly name = 'AskUserQuestion' as const; + readonly description: string = DESCRIPTION; + readonly parameters: Record = toInputJsonSchema(AskUserQuestionInputSchema); + + constructor(private readonly agent: Agent) {} + + resolveExecution(args: AskUserQuestionInput): ToolExecution { + return { + description: 'Asking user questions', + execute: (ctx) => this.execution(args, ctx), + }; + } + + private async execution( + args: AskUserQuestionInput, + { + toolCallId, + signal, + turnId, + }: ExecutableToolContext, + ): Promise { + try { + const result = await this.agent.rpc.requestQuestion( + { + turnId: numericTurnId(turnId), + toolCallId, + questions: args.questions.map((q) => ({ + question: q.question, + header: q.header, + options: q.options.map((o) => ({ + label: o.label, + description: o.description, + })), + multiSelect: q.multi_select, + })), + }, + { signal }, + ); + + const normalized = normalizeQuestionResult(result); + if (normalized === null || Object.keys(normalized.answers).length === 0) { + this.agent.telemetry.track('question_dismissed'); + return dismissedQuestionResult(); + } + + const properties: Record = { + answered: Object.keys(normalized.answers).length, + }; + if (normalized.method !== undefined) properties['method'] = normalized.method; + this.agent.telemetry.track('question_answered', properties); + return { + isError: false, + output: JSON.stringify({ answers: normalized.answers }), + }; + } catch (error) { + if (isAbortError(error) || signal.aborted) throw error; + + if (error instanceof KimiError && error.code === ErrorCodes.NOT_IMPLEMENTED) { + return { + isError: true, + output: QUESTION_UNSUPPORTED_FAILURE_MESSAGE, + }; + } + + return dismissedQuestionResult(); + } + } +} + +function dismissedQuestionResult(): ExecutableToolResult { + return { + isError: false, + output: JSON.stringify({ + answers: {}, + note: QUESTION_DISMISSED_MESSAGE, + }), + }; +} + +function numericTurnId(turnId: string): number | undefined { + if (turnId.trim().length === 0) return undefined; + const parsed = Number(turnId); + return Number.isFinite(parsed) ? parsed : undefined; +} + +function normalizeQuestionResult( + result: QuestionResult, +): { readonly answers: QuestionAnswers; readonly method?: QuestionAnswerMethod | undefined } | null { + if (result === null) return null; + if (isQuestionResponse(result)) { + return { + answers: result.answers, + method: result.method, + }; + } + return { answers: result }; +} + +function isQuestionResponse(result: Exclude): result is QuestionResponse { + if (typeof result !== 'object' || result === null) return false; + if (!Object.hasOwn(result, 'answers')) return false; + const answers = (result as { readonly answers?: unknown }).answers; + return typeof answers === 'object' && answers !== null && !Array.isArray(answers); +} diff --git a/packages/agent-core/src/tools/builtin/collaboration/skill-tool.md b/packages/agent-core/src/tools/builtin/collaboration/skill-tool.md new file mode 100644 index 000000000..bc67f43f5 --- /dev/null +++ b/packages/agent-core/src/tools/builtin/collaboration/skill-tool.md @@ -0,0 +1 @@ +Invoke a registered skill from the current skill listing. BLOCKING REQUIREMENT: when a skill from the listing matches the user's request, you MUST call this tool (not free-form text). Do NOT call the same skill repeatedly inside one turn — recursive depth is capped at {{ MAX_SKILL_QUERY_DEPTH }}. \ No newline at end of file diff --git a/packages/agent-core/src/tools/builtin/collaboration/skill-tool.ts b/packages/agent-core/src/tools/builtin/collaboration/skill-tool.ts new file mode 100644 index 000000000..1e8ec8d96 --- /dev/null +++ b/packages/agent-core/src/tools/builtin/collaboration/skill-tool.ts @@ -0,0 +1,168 @@ +/** + * SkillTool — invoke a registered skill. + * + * Collaboration tool that lets the LLM proactively invoke an inline + * registered skill. Inline skills record their activation through the + * owning agent; non-inline skill types are intentionally not model-invocable + * in the v1 default runtime. + * + * Anti-loop: `MAX_SKILL_QUERY_DEPTH` caps Skill→Skill recursion so a + * skill that re-invokes itself (or chains into another) cannot recurse + * without bound. + */ + +import { randomUUID } from 'node:crypto'; + +import { z } from 'zod'; + +import type { Agent } from '../../../agent'; +import type { SkillActivationOrigin } from '../../../agent/context'; +import type { BuiltinTool } from '../../../agent/tool'; +import type { ExecutableToolResult, ToolExecution } from '../../../loop/types'; +import { isInlineSkillType, type SkillDefinition } from '../../../skill'; +import { renderPrompt } from '../../../utils/render-prompt'; +import { toInputJsonSchema } from '../../support/input-schema'; +import skillDescriptionTemplate from './skill-tool.md'; + +export const MAX_SKILL_QUERY_DEPTH = 3; + +export class NestedSkillTooDeepError extends Error { + readonly skillName?: string; + readonly depth: number; + + constructor(depth: number, skillName?: string) { + const label = skillName !== undefined ? ` "${skillName}"` : ''; + super( + `Nested skill invocation${label} exceeded the maximum depth of ${String(depth)} — refusing to recurse further.`, + ); + this.name = 'NestedSkillTooDeepError'; + this.depth = depth; + if (skillName !== undefined) this.skillName = skillName; + } +} + +export interface SkillToolInput { + skill: string; + args?: string; +} + +export const SkillToolInputSchema: z.ZodType = z.object({ + skill: z.string(), + args: z.string().optional(), +}); + +export interface SkillToolOptions { + /** + * Current inline skill recursion depth. + */ + readonly queryDepth?: number; + /** + * Alias for `queryDepth`. Kept so older call sites can seed the + * inline recursion depth without knowing the internal field name. + */ + readonly initialQueryDepth?: number; +} + +export class SkillTool implements BuiltinTool { + readonly name = 'Skill'; + readonly description: string = renderPrompt(skillDescriptionTemplate, { + MAX_SKILL_QUERY_DEPTH, + }); + readonly parameters: Record = toInputJsonSchema(SkillToolInputSchema); + + constructor( + private readonly agent: Agent, + private readonly options: SkillToolOptions = {}, + ) {} + + resolveExecution(args: SkillToolInput): ToolExecution { + return { + description: `Invoke skill ${args.skill}`, + execute: () => this.execution(args), + }; + } + + withInitialQueryDepth(initialQueryDepth: number): SkillTool { + return new SkillTool(this.agent, { + ...this.options, + initialQueryDepth, + }); + } + + private async execution(args: SkillToolInput): Promise { + // Recursion hard cap. Once `currentDepth` has reached + // MAX_SKILL_QUERY_DEPTH, firing another Skill call would push the + // child to depth+1 which violates the invariant. Throw a structured + // error (rather than a soft tool-error) so Runtime can distinguish + // "LLM mis-dispatched" from "safety net fired". + const currentDepth = this.options.initialQueryDepth ?? this.options.queryDepth ?? 0; + if (currentDepth >= MAX_SKILL_QUERY_DEPTH) { + throw new NestedSkillTooDeepError(MAX_SKILL_QUERY_DEPTH, args.skill); + } + + const skills = this.agent.skills; + if (skills === undefined) { + return errorResult(`Skill "${args.skill}" not found in the current skill listing.`); + } + const skill = skills.registry.getSkill(args.skill); + if (skill === undefined) { + return errorResult(`Skill "${args.skill}" not found in the current skill listing.`); + } + if (skill.metadata.disableModelInvocation === true) { + // Keep the exact wording "can only be triggered by the user" so + // contract audits and integration tests stay deterministic. + return errorResult( + `Skill "${args.skill}" can only be triggered by the user (model invocation is disabled).`, + ); + } + + const skillArgs = args.args ?? ''; + if (!isInlineSkillType(skill.metadata.type)) { + return errorResult( + `Skill "${skill.name}" is not an inline skill and cannot be invoked by the model in v1.`, + ); + } + + const origin = skillOrigin(skill, skillArgs, currentDepth); + skills.recordActivation(origin); + const skillContent = skills.registry.renderSkillPrompt(skill, skillArgs); + this.agent.context.appendSystemReminder( + `\n` + + `${skillContent}\n` + + ``, + origin, + ); + return { + output: `Skill "${skill.name}" loaded inline. Follow its instructions.`, + }; + } +} + +function errorResult(message: string): ExecutableToolResult { + return { isError: true, output: message }; +} + +function skillOrigin( + skill: SkillDefinition, + skillArgs: string, + currentDepth: number, +): SkillActivationOrigin { + return { + kind: 'skill_activation', + activationId: randomUUID(), + skillName: skill.name, + skillArgs: skillArgs.length > 0 ? skillArgs : undefined, + trigger: currentDepth > 0 ? 'nested-skill' : 'model-tool', + skillType: skill.metadata.type, + skillPath: skill.path, + skillSource: skill.source, + }; +} + +function escapeXml(input: string): string { + return input + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"'); +} diff --git a/packages/agent-core/src/tools/builtin/file/edit.md b/packages/agent-core/src/tools/builtin/file/edit.md new file mode 100644 index 000000000..f5de2c551 --- /dev/null +++ b/packages/agent-core/src/tools/builtin/file/edit.md @@ -0,0 +1,10 @@ +Perform exact string replacements against the text view returned by Read. + +- When copying from Read output, omit the line-number prefix and tab; match only the file content. +- By default, old_string must occur exactly once. If it matches multiple locations, add surrounding context or set replace_all when every occurrence should change. +- Prefer Edit for targeted changes to existing files; use Write only for new files or complete overwrites. +- To modify a file, always use Edit; do not run a Shell `sed` command for edits. +- When making several independent changes, issue multiple Edit calls in parallel within a single response; edits to the same file are serialized automatically by a write lock. +- When several parallel Edit calls target the same file, a write lock serializes them; they apply in the order the calls appear in your response. An edit fails with `old_string not found` if its old_string was taken from text an earlier edit already replaced — base every old_string on the latest Read view and order dependent edits accordingly. +- For pure CRLF files, Read shows LF and Edit.old_string/new_string should use LF; Edit writes the file back with CRLF preserved. +- For mixed line endings or lone carriage returns, Read displays carriage returns as \r; include actual \r escapes in old_string/new_string for those positions. \ No newline at end of file diff --git a/packages/agent-core/src/tools/builtin/file/edit.ts b/packages/agent-core/src/tools/builtin/file/edit.ts new file mode 100644 index 000000000..de14c3456 --- /dev/null +++ b/packages/agent-core/src/tools/builtin/file/edit.ts @@ -0,0 +1,149 @@ +/** + * EditTool — exact string replacement in a file. + * + * Replaces the first occurrence of `old_string` with `new_string` by + * default. When `replace_all` is true, replaces all occurrences. + * Errors when `old_string` is not found or not unique (when + * `replace_all=false`). Path access policy is resolved before any + * Kaos I/O. + */ + +import type { Kaos } from '@moonshot-ai/kaos'; +import { z } from 'zod'; + +import type { BuiltinTool } from '../../../agent/tool'; +import { ToolAccesses } from '../../../loop/tool-access'; +import type { ExecutableToolResult, ToolExecution } from '../../../loop/types'; +import { resolvePathAccessPath } from '../../policies/path-access'; +import { toInputJsonSchema } from '../../support/input-schema'; +import type { WorkspaceConfig } from '../../support/workspace'; +import { materializeModelText, toModelTextView } from './line-endings'; +import EDIT_DESCRIPTION from './edit.md'; + +// `old_string` must be non-empty: the non-replace_all branch walks +// occurrences with `content.indexOf("", pos)`, which would loop forever +// on an empty search string. +export const EditInputSchema = z.object({ + path: z + .string() + .describe( + 'Path to the text file to edit. Relative paths resolve against the working directory; a path outside the working directory must be absolute.', + ), + old_string: z + .string() + .min(1) + .describe( + 'Exact content to replace from the Read output view, without the line-number prefix. Use LF for pure CRLF files; use actual \\r escapes where Read shows \\r.', + ), + new_string: z + .string() + .describe( + 'Replacement text in the same Read output view. LF is written back as CRLF only for pure CRLF files.', + ), + replace_all: z + .boolean() + .optional() + .describe('Set true only when every occurrence of old_string should be replaced.'), +}); + +export type EditInput = z.Infer; + +function replaceOnceLiteral(content: string, oldString: string, newString: string): string { + const index = content.indexOf(oldString); + if (index === -1) return content; + return content.slice(0, index) + newString + content.slice(index + oldString.length); +} + +export class EditTool implements BuiltinTool { + readonly name = 'Edit' as const; + readonly description = EDIT_DESCRIPTION; + readonly parameters: Record = toInputJsonSchema(EditInputSchema); + + constructor( + private readonly kaos: Kaos, + private readonly workspace: WorkspaceConfig, + ) {} + + resolveExecution(args: EditInput): ToolExecution { + const path = resolvePathAccessPath(args.path, { + kaos: this.kaos, + workspace: this.workspace, + operation: 'write', + }); + return { + accesses: ToolAccesses.readWriteFile(path), + description: `Editing ${args.path}`, + execute: () => this.execution(args, path), + }; + } + + private async execution(args: EditInput, safePath: string): Promise { + if (args.old_string === args.new_string) { + return { + isError: true, + output: 'No changes to make: old_string and new_string are exactly the same.', + }; + } + + try { + const raw = await this.kaos.readText(safePath); + const modelView = toModelTextView(raw); + const content = modelView.text; + const replaceAll = args.replace_all ?? false; + + if (!replaceAll) { + let count = 0; + let pos = 0; + while (pos < content.length) { + const idx = content.indexOf(args.old_string, pos); + if (idx === -1) break; + count++; + pos = idx + args.old_string.length; + } + + if (count === 0) { + return { isError: true, output: `old_string not found in ${args.path}, The file contents may be out of date. Please use the Read Tool to reload the content. +` }; + } + if (count > 1) { + return { + isError: true, + output: + `old_string is not unique in ${args.path} (found ${String(count)} occurrences). ` + + 'To replace every occurrence, set replace_all=true. To replace only one occurrence, include more surrounding context in old_string.', + }; + } + + const newContent = replaceOnceLiteral(content, args.old_string, args.new_string); + await this.kaos.writeText( + safePath, + materializeModelText(newContent, modelView.lineEndingStyle), + ); + return { output: `Replaced 1 occurrence in ${args.path}` }; + } + + const parts = content.split(args.old_string); + const replacementCount = parts.length - 1; + if (replacementCount === 0) { + return { isError: true, output: `old_string not found in ${args.path}, The file contents may be out of date. Please use the Read Tool to reload the content. +` }; + } + + const newContent = parts.join(args.new_string); + await this.kaos.writeText( + safePath, + materializeModelText(newContent, modelView.lineEndingStyle), + ); + return { output: `Replaced ${String(replacementCount)} occurrences in ${args.path}` }; + } catch (error) { + const code = (error as { code?: unknown } | null)?.code; + if (code === 'EISDIR') { + return { isError: true, output: `${args.path} is not a file.` }; + } + return { + isError: true, + output: error instanceof Error ? error.message : String(error), + }; + } + } +} diff --git a/packages/agent-core/src/tools/builtin/file/glob.md b/packages/agent-core/src/tools/builtin/file/glob.md new file mode 100644 index 000000000..f26cc8a05 --- /dev/null +++ b/packages/agent-core/src/tools/builtin/file/glob.md @@ -0,0 +1,16 @@ +Find files (and optionally directories) by glob pattern, sorted by modification time (most recent first). + +Good patterns: +- `*.ts` — files in the current directory matching an extension +- `src/**/*.ts` — recursive with a subdirectory anchor and extension +- `test_*.py` — files whose name starts with a literal prefix + +Rejected patterns (no literal anchor — nothing bounds the result set): +- `**`, `**/*`, `*/*` — pure wildcards. Add an extension or subdirectory to give the walk a concrete target. +- Anything that starts with `**/` (e.g. `**/*.md`, `**/main/*.py`). The leading `**/` has no literal anchor in front of it. Anchor it with a top-level subdirectory like `src/**/*.md`. +- `*.{ts,tsx}` — brace expansion is not supported. Issue two calls: `*.ts` and `*.tsx`. + +Large-directory warning — avoid recursing into dependency/build output even with an anchor: +- `node_modules/**/*.js`, `.venv/**/*.py`, `__pycache__/**`, `target/**` all match technically but + typically produce thousands of results that truncate at the match cap and waste the caller context. + Prefer specific subpaths like `node_modules/react/src/**/*.js`. \ No newline at end of file diff --git a/packages/agent-core/src/tools/builtin/file/glob.ts b/packages/agent-core/src/tools/builtin/file/glob.ts new file mode 100644 index 000000000..d58e37c5c --- /dev/null +++ b/packages/agent-core/src/tools/builtin/file/glob.ts @@ -0,0 +1,376 @@ +/** + * GlobTool — file pattern matching. + * + * Finds files matching a glob pattern, returned sorted by modification + * time (most recent first). Uses `kaos.glob`. + * + * Output convention: `content` shown to the LLM is relativized to the + * search base to save tokens; `output.paths` keeps absolute paths so + * downstream Read/Edit can consume them directly. + * + * Safety rails: + * - Pure-wildcard patterns (nothing but `*` / `?` / `/`) are rejected + * because they have no literal anchor — they would enumerate every + * file under the search root and exhaust the caller's context on + * large trees. Examples: `**`, `** / *`, `** / **`, `* / *`. + * Constrained patterns (with any literal anchor such as an extension + * or subdirectory) are allowed — the literal bounds the result set. + * - Patterns using brace expansion (`{a,b,c}`) are rejected up-front + * because the underlying `_globWalk` treats `{` / `}` as literals, + * so such patterns would silently match zero files. + * - `path` is validated by `resolvePathAccess` in strict mode. Explicit + * paths must be absolute and within the workspace roots. + * - match count is capped at `MAX_MATCHES`; a separate `YIELD_SAFETY_CAP` + * (MAX_MATCHES × 2) on the raw yield stream is a secondary belt that + * still terminates the stream if the kaos layer's own symlink-cycle + * detection were ever absent or bypassed. Primary cycle defense lives + * in `packages/kaos/src/local.ts:_globWalk` via a path-local visited + * inode set. + */ + +import type { Kaos } from '@moonshot-ai/kaos'; +import { z } from 'zod'; + +import type { BuiltinTool } from '../../../agent/tool'; +import { ToolAccesses } from '../../../loop/tool-access'; +import type { ExecutableToolResult, ToolExecution } from '../../../loop/types'; +import { resolvePathAccessPath } from '../../policies/path-access'; +import type { PathClass } from '../../policies/path-access'; +import { toInputJsonSchema } from '../../support/input-schema'; +import { listDirectory } from '../../support/list-directory'; +import type { WorkspaceConfig } from '../../support/workspace'; +import GLOB_DESCRIPTION from './glob.md'; + +export const GlobInputSchema = z.object({ + pattern: z.string().describe('Glob pattern to match files/directories.'), + path: z + .string() + .optional() + .describe( + 'Absolute path to the directory to search in. Defaults to the current working directory.', + ), + include_dirs: z + .boolean() + .default(true) + .optional() + .describe( + 'Whether to include directories in results. Defaults to true. Set false to return only files.', + ), +}); + +export type GlobInput = z.Infer; + +export const MAX_MATCHES = 1000; + +/** + * Path-shape hint appended to the tool description only on a Windows + * (`win32` path class) backend. The `path` argument accepts both native + * Windows paths and POSIX-style paths, but matched paths come back in + * Windows backslash form — a command run through Bash must convert them + * to forward slashes first. Injected conditionally so non-Windows + * sessions are not shown a hint that does not apply to them. + */ +export const WINDOWS_PATH_HINT = + '\n\nWindows note: the `path` argument accepts both Windows paths ' + + '(e.g. `C:\\Users\\foo`) and POSIX-style paths (e.g. `/c/Users/foo`). Matched paths are ' + + 'returned in Windows backslash form; convert them to forward slashes before ' + + 'using them in a Bash command.'; + +// POSIX mode bits — same constants used by KaosPath.isDir (packages/kaos/src/path.ts:199). +const S_IFMT = 0o170000; +const S_IFDIR = 0o040000; + +/** + * Tool-level description shown to the LLM at tool declaration time. + * Tells the model — before any round-trip — which patterns are + * accepted, which are rejected, and which directories are too large to + * recurse into. Patterns with a literal anchor before a double-star are + * allowed; pure-wildcard patterns (a bare double-star or a double-star + * followed by `/`) are rejected outright. On a Windows backend + * the description also carries `WINDOWS_PATH_HINT` (path-shape guidance). + */ +export class GlobTool implements BuiltinTool { + readonly name = 'Glob' as const; + readonly description: string; + readonly parameters: Record = toInputJsonSchema(GlobInputSchema); + constructor( + private readonly kaos: Kaos, + private readonly workspace: WorkspaceConfig, + ) { + this.description = + this.kaos.pathClass() === 'win32' + ? GLOB_DESCRIPTION + WINDOWS_PATH_HINT + : GLOB_DESCRIPTION; + } + + resolveExecution(args: GlobInput): ToolExecution { + let path: string | undefined; + if (args.path !== undefined) { + path = resolvePathAccessPath(args.path, { + kaos: this.kaos, + workspace: this.workspace, + operation: 'search', + policy: { guardMode: 'strict', checkSensitive: false }, + }); + } + const searchRoots = [path ?? this.workspace.workspaceDir]; + return { + accesses: ToolAccesses.searchTree(searchRoots[0]!), + description: `Searching ${args.pattern}`, + execute: () => this.execution(args, searchRoots), + }; + } + + private async execution(args: GlobInput, searchRoots: string[]): Promise { + if (startsWithDoubleStarPrefix(args.pattern)) { + let tree: string; + try { + tree = await listDirectory(this.kaos, this.workspace.workspaceDir); + } catch { + tree = '(listing unavailable)'; + } + return { + isError: true, + output: + `Pattern "${args.pattern}" starts with '**' which is not allowed — ` + + `the leading '**/' has no literal anchor in front of it and would ` + + `enumerate every file under the search root, typically exhausting ` + + `the caller's context on large trees. Use more specific patterns ` + + `instead, such as "src/**/*.py" or "test/**/*.py".\n\n` + + `Top of ${this.workspace.workspaceDir}:\n${tree}`, + }; + } + + if (isPureWildcard(args.pattern)) { + const allowedRoots = [this.workspace.workspaceDir, ...this.workspace.additionalDirs]; + const rootList = allowedRoots.map((d) => ` - ${d}`).join('\n'); + let tree: string; + try { + tree = await listDirectory(this.kaos, this.workspace.workspaceDir); + } catch { + tree = '(listing unavailable)'; + } + return { + isError: true, + output: + `Pattern "${args.pattern}" is a pure wildcard (only \`*\`, \`?\`, \`**\`, \`/\`) ` + + `and would enumerate every file under the search root — with no literal ` + + `anchor to bound the result set, this typically exhausts your context on ` + + `large trees. Add an extension ` + + `("${args.pattern === '**' || args.pattern === '**/*' ? '**/*.ts' : '**/*.md'}") ` + + `or a subdirectory ("src/**/*.ts") to constrain the walk.\n\n` + + `Allowed roots for explicit path searches:\n${rootList}\n\n` + + `Top of ${this.workspace.workspaceDir}:\n${tree}`, + }; + } + + if (containsBraceExpansion(args.pattern)) { + return { + isError: true, + output: + `Pattern "${args.pattern}" uses brace expansion (\`{a,b,...}\`), which ` + + `is not supported by this Glob tool. Split it into separate calls, ` + + `one pattern per alternative. For example, instead of "*.{ts,tsx}" ` + + `issue two calls: "*.ts" and "*.tsx".`, + }; + } + + // Default true. When false, directories yielded by kaos are + // filtered out using the same stat that fuels the mtime sort + // (no second stat per path). + const includeDirs = args.include_dirs ?? true; + + // kaos.glob silently returns empty for missing or non-directory roots + // (its _globWalk catches the readdir failure and exits without yielding). + // Without this pre-check, a Glob against a missing path would report + // "No matches found" instead of "does not exist", and the model would + // not realize the search root itself was wrong. iterdir is the right + // signal: pulling one entry triggers the same readdir that kaos.glob + // would do, so ENOENT/ENOTDIR surface here for the realistic backends + // before the walker is invoked. Any other failure (e.g. an unmocked + // test backend that throws "not implemented") falls through silently + // so the existing kaos.glob path still runs. + for (const root of searchRoots) { + try { + const iter = this.kaos.iterdir(root); + await iter.next(); + if (typeof iter.return === 'function') { + await iter.return(undefined); + } + } catch (error) { + if (error !== null && typeof error === 'object' && 'code' in error) { + const code = (error as { code?: string }).code; + if (code === 'ENOENT') { + return { isError: true, output: `${root} does not exist` }; + } + if (code === 'ENOTDIR') { + return { isError: true, output: `${root} is not a directory` }; + } + } + // Unknown failure (including unmocked test backends): fall + // through and let kaos.glob run; it will either yield results + // or its own catch path will surface the error. + } + } + + try { + // Two counters, two jobs: + // - `entries.length` caps the *unique* paths we return, so a + // truncation warning only fires after MAX_MATCHES real hits. + // - `yielded` counts every path the kaos stream emits, including + // duplicates. Secondary safety belt: the kaos `_globWalk` + // itself detects symlink cycles, so a well-formed kaos layer + // never re-yields the same real + // file. `yielded` still terminates the stream if that primary + // defense were ever absent or bypassed (e.g. a future kaos + // backend without inode tracking), so the tool layer doesn't + // depend on the kaos implementation for cycle safety. + const seen = new Set(); + const entries: Array<{ path: string; mtime: number }> = []; + const YIELD_SAFETY_CAP = MAX_MATCHES * 2; + let yielded = 0; + let truncated = false; + + outer: for (const root of searchRoots) { + for await (const filePath of this.kaos.glob(root, args.pattern)) { + yielded++; + if (yielded >= YIELD_SAFETY_CAP) { + truncated = true; + break outer; + } + if (seen.has(filePath)) continue; + if (entries.length >= MAX_MATCHES) { + truncated = true; + break outer; + } + seen.add(filePath); + let mtime = 0; + let isDir = false; + try { + const st = await this.kaos.stat(filePath); + mtime = st.stMtime ?? 0; + isDir = (st.stMode & S_IFMT) === S_IFDIR; + } catch { + // stat failure — use 0 mtime / assume file so it still surfaces + } + // Apply include_dirs *after* marking seen so a filtered dir + // doesn't re-enter via a later duplicate yield, and *before* + // pushing to entries so MAX_MATCHES continues to cap output + // (not pre-filter) size. + if (!includeDirs && isDir) continue; + entries.push({ path: filePath, mtime }); + } + } + + entries.sort((a, b) => b.mtime - a.mtime); + + const paths = entries.map((e) => e.path); + // Content shown to the LLM uses paths relative to the search base + // to save tokens; `output.paths` keeps the absolute form so callers + // can feed them into Read/Edit without further resolution. + const pathClass = this.kaos.pathClass(); + const relBase = searchRoots[0] ?? this.workspace.workspaceDir; + const displayLines = paths.map((p) => relativizeIfUnder(p, relBase, pathClass)); + + if (entries.length === 0 && !truncated) { + return { output: 'No matches found' }; + } + const lines: string[] = []; + if (truncated) { + lines.push(`[Truncated at ${String(MAX_MATCHES)} matches — use a more specific pattern]`); + lines.push(`Only the first ${String(MAX_MATCHES)} matches are returned.`); + } + lines.push(...displayLines); + if (!truncated && entries.length === MAX_MATCHES) { + lines.push(`Found ${String(entries.length)} matches`); + } + return { output: lines.join('\n') }; + } catch (error) { + if (error !== null && typeof error === 'object' && 'code' in error) { + const code = (error as { code?: string }).code; + const path = searchRoots[0] ?? this.workspace.workspaceDir; + if (code === 'ENOENT') { + return { isError: true, output: `${path} does not exist` }; + } + if (code === 'ENOTDIR') { + return { isError: true, output: `${path} is not a directory` }; + } + } + return { isError: true, output: error instanceof Error ? error.message : String(error) }; + } + } + +} + +/** + * If `candidate` is under `base`, return the portion after `base/`. + * Otherwise return `candidate` unchanged (absolute). Both arguments + * should be canonical absolute paths. + */ +function relativizeIfUnder(candidate: string, base: string, pathClass: PathClass): string { + const sep = pathClass === 'win32' ? '\\' : '/'; + const comparableCandidate = pathClass === 'win32' ? candidate.toLowerCase() : candidate; + const comparableBase = pathClass === 'win32' ? base.toLowerCase() : base; + if (comparableCandidate === comparableBase) return '.'; + const prefix = comparableBase.endsWith(sep) ? comparableBase : comparableBase + sep; + if (comparableCandidate.startsWith(prefix)) { + return candidate.slice(prefix.length); + } + return candidate; +} + +// Return true iff `pattern` begins with the literal sequence `**` followed +// by a `/`. Such patterns have no literal anchor in front of the recursive +// wildcard, so the walk has nothing to bound it on the left and would +// descend into every top-level directory of the search root before any +// suffix constraint can filter. Rejected up-front to match the Python Glob +// behavior — callers must anchor with a top-level subdirectory. +function startsWithDoubleStarPrefix(pattern: string): boolean { + return pattern.startsWith('**/'); +} + +/** + * Return true if `pattern` is pure wildcards — only `*`, `?`, `**`, `/`. + * Such patterns have no literal anchor and would enumerate every file + * under the search root. Backslash-escaped characters (`\X`) count as + * literals so `\*` or `\?` still means "pattern has an anchor". + */ +function isPureWildcard(pattern: string): boolean { + if (pattern === '') return false; + for (let i = 0; i < pattern.length; i++) { + const ch = pattern[i]; + if (ch === '\\' && i + 1 < pattern.length) { + // escaped literal — pattern has an anchor + return false; + } + if (ch !== '*' && ch !== '?' && ch !== '/') { + return false; + } + } + return true; +} + +/** Return true iff `pattern` looks like it uses `{a,b,c}` brace expansion. */ +function containsBraceExpansion(pattern: string): boolean { + let inBrace = false; + let sawCommaInsideBrace = false; + for (let i = 0; i < pattern.length; i++) { + const ch = pattern[i]; + if (ch === '\\' && i + 1 < pattern.length) { + i++; + continue; + } + if (ch === '{') { + inBrace = true; + sawCommaInsideBrace = false; + continue; + } + if (ch === '}') { + if (inBrace && sawCommaInsideBrace) return true; + inBrace = false; + continue; + } + if (ch === ',' && inBrace) sawCommaInsideBrace = true; + } + return false; +} diff --git a/packages/agent-core/src/tools/builtin/file/grep.md b/packages/agent-core/src/tools/builtin/file/grep.md new file mode 100644 index 000000000..c0d2776b3 --- /dev/null +++ b/packages/agent-core/src/tools/builtin/file/grep.md @@ -0,0 +1,9 @@ +Search file contents using regular expressions (powered by ripgrep). + +Use Grep when the task is to find unknown content or unknown file locations. Do not use shell `grep` or `rg` directly; this tool applies workspace path policy, output limits, and sensitive-file filtering. +ALWAYS use Grep tool instead of running `grep` or `rg` from a shell — direct shell calls bypass workspace policy, output limits, and sensitive-file filtering. +If you already know a concrete file path and need to inspect its contents, use Read directly instead. + +Write patterns in ripgrep regex syntax, which differs from POSIX `grep` syntax. For example, braces are special, so escape them as `\{` to match a literal `{`. + +Hidden files (dotfiles such as `.gitlab-ci.yml` or `.eslintrc.json`) are searched by default. To also search files excluded by `.gitignore` (such as `node_modules` or build outputs), set `include_ignored` to `true`. Sensitive files (such as `.env`) are always skipped for safety, even when `include_ignored` is `true`. diff --git a/packages/agent-core/src/tools/builtin/file/grep.ts b/packages/agent-core/src/tools/builtin/file/grep.ts new file mode 100644 index 000000000..00dbcf25f --- /dev/null +++ b/packages/agent-core/src/tools/builtin/file/grep.ts @@ -0,0 +1,955 @@ +/** + * GrepTool — content search via ripgrep. + * + * Shells out to `rg` through Kaos. Supports glob/type filtering, context + * lines, output modes, pagination, multiline, and case-insensitive search. + * + * Path safety is enforced before any Kaos I/O. Explicit absolute paths outside + * the workspace are allowed; relative paths that escape the workspace are + * rejected. + * + * Output is bounded and post-processed before it reaches the model: + * - timeout and ambient abort both terminate the rg subprocess; + * - stdout/stderr are capped while streams continue draining; + * - hidden files are searched, but VCS metadata and common sensitive glob + * patterns are prefiltered where possible; + * - parsed path records are filtered again after rg returns, using the active + * backend path class. + */ + +import type { Readable } from 'node:stream'; + +import type { Kaos, KaosProcess } from '@moonshot-ai/kaos'; +import { z } from 'zod'; + +import type { BuiltinTool } from '../../../agent/tool'; +import { isAbortError } from '../../../loop/errors'; +import { ToolAccesses } from '../../../loop/tool-access'; +import type { ExecutableToolResult, ToolExecution } from '../../../loop/types'; +import { resolvePathAccessPath } from '../../policies/path-access'; +import type { PathClass } from '../../policies/path-access'; +import { isSensitiveFile, SENSITIVE_DOT_VARIANT_SUFFIXES } from '../../policies/sensitive'; +import { toInputJsonSchema } from '../../support/input-schema'; +import { ensureRgPath, rgUnavailableMessage } from '../../support/rg-locator'; +import { ToolResultBuilder } from '../../support/result-builder'; +import type { WorkspaceConfig } from '../../support/workspace'; +import GREP_DESCRIPTION from './grep.md'; + +export const GrepInputSchema = z.object({ + pattern: z.string().describe('Regular expression to search for.'), + path: z + .string() + .optional() + .describe( + 'File or directory to search. Accepts an absolute path, or a path relative to the current working directory. Omit to search the current working directory. Use Read instead when you already know a concrete file path and need its contents.', + ), + glob: z.string().optional().describe('Optional glob filter passed to ripgrep.'), + type: z + .string() + .optional() + .describe( + 'Optional ripgrep file type filter, such as ts or py. Prefer this over `glob` when filtering by language or file kind: it is more efficient and less error-prone than an equivalent glob pattern.', + ), + output_mode: z + .enum(['content', 'files_with_matches', 'count_matches']) + .optional() + .describe( + 'Shape of the result. `content` shows matching lines (honors `-A`, `-B`, `-C`, `-n`, and `head_limit`); `files_with_matches` shows only the paths of files that contain a match (honors `head_limit`); `count_matches` shows the total number of matches. Defaults to `files_with_matches`.', + ), + '-i': z.boolean().optional().describe('Perform a case-insensitive search. Defaults to false.'), + '-n': z + .boolean() + .optional() + .describe( + 'Prefix each matching line with its line number. Applies only when `output_mode` is `content`. Defaults to true.', + ), + '-A': z + .number() + .int() + .nonnegative() + .optional() + .describe( + 'Number of lines to show after each match. Applies only when `output_mode` is `content`.', + ), + '-B': z + .number() + .int() + .nonnegative() + .optional() + .describe( + 'Number of lines to show before each match. Applies only when `output_mode` is `content`.', + ), + '-C': z + .number() + .int() + .nonnegative() + .optional() + .describe( + 'Number of lines to show before and after each match. Applies only when `output_mode` is `content`; takes precedence over `-A` and `-B`.', + ), + head_limit: z + .number() + .int() + .nonnegative() + .optional() + .describe( + 'Limit output to the first N lines/entries after offset. Defaults to 250. Pass 0 for unlimited.', + ), + offset: z + .number() + .int() + .nonnegative() + .optional() + .describe( + 'Number of leading lines/entries to skip before applying `head_limit`. Use it together with `head_limit` to page through large result sets. Defaults to 0.', + ), + multiline: z + .boolean() + .optional() + .describe( + 'Enable multiline matching, where the pattern can span line boundaries and `.` also matches newlines. Defaults to false.', + ), + include_ignored: z + .boolean() + .optional() + .describe( + 'Also search files excluded by ignore files such as `.gitignore`, `.ignore`, and `.rgignore` (for example `node_modules` or build outputs). Sensitive files (such as `.env`) remain filtered out for safety. Defaults to false.', + ), +}); + +export const GrepOutputSchema = z.object({ + mode: z.enum(['content', 'files_with_matches', 'count_matches']), + numFiles: z.number().int().nonnegative(), + filenames: z.array(z.string()), + content: z.string().optional(), + numLines: z.number().int().nonnegative().optional(), + numMatches: z.number().int().nonnegative().optional(), + appliedLimit: z.number().int().nonnegative().optional(), +}); + +export type GrepInput = z.Infer; +export type GrepOutput = z.Infer; + +const DEFAULT_TIMEOUT_MS = 20_000; +const SIGTERM_GRACE_MS = 5_000; +const MAX_OUTPUT_BYTES = 10 * 1024 * 1024; +// Column cap applied to non-content output modes only; `content` mode returns +// matching lines in full so the cap is intentionally skipped there. +const RG_MAX_COLUMNS = 500; +const DEFAULT_HEAD_LIMIT = 250; +const MTIME_STAT_CONCURRENCY = 32; +const VCS_DIRECTORIES_TO_EXCLUDE = ['.git', '.svn', '.hg', '.bzr', '.jj', '.sl'] as const; +// This is a conservative prefilter. The authoritative sensitive-file check +// still happens on parsed rg records after execution. +const SENSITIVE_KEY_BASENAMES = ['id_rsa', 'id_ed25519', 'id_ecdsa'] as const; +const SENSITIVE_KEY_GLOBS_TO_EXCLUDE = SENSITIVE_KEY_BASENAMES.flatMap((name) => [ + `**/${name}`, + `**/${name}[-_]*`, + ...SENSITIVE_DOT_VARIANT_SUFFIXES.map((suffix) => `**/${name}${suffix}`), +]); +const SENSITIVE_GLOBS_TO_EXCLUDE = [ + '**/.env', + ...SENSITIVE_KEY_GLOBS_TO_EXCLUDE, + '**/.aws/credentials', + '**/.aws/credentials/**', + '**/.gcp/credentials', + '**/.gcp/credentials/**', +] as const; + +// Line formats produced by ripgrep: +// content match with --null: "file.py10:matched text" +// context line with --null: "file.py9-context text" +// count_matches with --null: "file.py2" +// non-NUL content fallback: "file.py:10:matched text" +// context divider: "--" +// Runtime rg output uses NUL as the path boundary; the regex handles +// line-oriented output without NUL delimiters. +const CONTENT_LINE_RE = /^(.*?)([:-])(\d+)\2/; + +export class GrepTool implements BuiltinTool { + readonly name = 'Grep' as const; + readonly description = GREP_DESCRIPTION; + readonly parameters: Record = toInputJsonSchema(GrepInputSchema); + constructor( + private readonly kaos: Kaos, + private readonly workspace: WorkspaceConfig, + ) {} + + resolveExecution(args: GrepInput): ToolExecution { + let path: string | undefined; + if (args.path !== undefined) { + path = resolvePathAccessPath(args.path, { + kaos: this.kaos, + workspace: this.workspace, + operation: 'search', + policy: { guardMode: 'absolute-outside-allowed', checkSensitive: false }, + }); + } + const searchPaths = [path ?? this.workspace.workspaceDir]; + const searchPath = args.path ?? this.workspace.workspaceDir; + return { + accesses: ToolAccesses.searchTree(searchPaths[0]!), + description: `Searching for '${args.pattern}' in ${searchPath}`, + execute: ({ signal }) => this.execution(args, signal, searchPaths), + }; + } + + private async execution( + args: GrepInput, + signal: AbortSignal, + searchPaths: string[], + ): Promise { + if (signal.aborted) { + return { isError: true, output: 'Aborted before search started' }; + } + + const pathClass = this.kaos.pathClass(); + let rgPath: string; + try { + const resolution = await ensureRgPath({ signal }); + rgPath = resolution.path; + } catch (error) { + if (isAbortError(error)) { + return { isError: true, output: 'Grep aborted' }; + } + return { isError: true, output: rgUnavailableMessage(error) }; + } + + let runResult = await runRipgrepOnce(this.kaos, buildRgArgs(rgPath, args, searchPaths), signal); + if (runResult.kind === 'tool-error') return runResult.result; + if (shouldRetryRipgrepEagain(runResult)) { + runResult = await runRipgrepOnce( + this.kaos, + buildRgArgs(rgPath, args, searchPaths, true), + signal, + ); + if (runResult.kind === 'tool-error') return runResult.result; + } + + const { exitCode, stderrText, bufferTruncated, stderrTruncated, timedOut } = runResult; + let { stdoutText } = runResult; + + // rg exit codes: 0 = matches, 1 = no matches, 2 = error. Timeout kills + // usually surface as a signal exit code; keep any complete partial records. + if (exitCode !== 0 && exitCode !== 1 && !timedOut) { + return { + isError: true, + output: formatRipgrepError(exitCode, stderrText, stderrTruncated), + }; + } + + const mode = args.output_mode ?? 'files_with_matches'; + if (bufferTruncated || timedOut) { + stdoutText = omitIncompleteTrailingRecord(stdoutText, mode); + } + if (timedOut && stdoutText.trim() === '') { + return { + isError: true, + output: `Grep timed out after ${String(DEFAULT_TIMEOUT_MS / 1000)}s. Try a more specific path or pattern.`, + }; + } + if (signal.aborted) { + return { isError: true, output: 'Grep aborted' }; + } + + const rawLines = parseRipgrepOutput(stdoutText, mode); + + const filteredSensitive = new Set(); + const keptLines = filterSensitiveLines(rawLines, mode, filteredSensitive, pathClass); + let orderedLines: ParsedGrepLine[]; + try { + orderedLines = + mode === 'files_with_matches' && !timedOut + ? await sortFilesWithMatchesByMtime(keptLines, this.kaos, signal) + : keptLines; + } catch (error) { + if (error instanceof GrepAbortedError) { + return { isError: true, output: 'Grep aborted' }; + } + throw error; + } + + const offset = args.offset ?? 0; + const headLimit = args.head_limit ?? DEFAULT_HEAD_LIMIT; + const afterOffset = offset > 0 ? orderedLines.slice(offset) : orderedLines; + const limitActive = headLimit > 0; + const limited = limitActive ? afterOffset.slice(0, headLimit) : afterOffset; + const paginationTruncated = limitActive && afterOffset.length > headLimit; + + // Human-readable annotations are appended after visible matches. + // In count mode, the data stream must stay pure `path:count` lines + // — the count summary and pagination notice move to a side channel + // (returned via `result.message`) so they don't contaminate it. + // Other modes keep these notices inline in `output`. + const messages: string[] = []; + const sideChannelMessages: string[] = []; + if (filteredSensitive.size > 0) { + const displayedFilteredPaths = [...filteredSensitive].map((path) => + relativizeIfUnder(path, this.workspace.workspaceDir, pathClass), + ); + messages.push( + `Filtered ${String(filteredSensitive.size)} sensitive file(s): ${displayedFilteredPaths.join(', ')}`, + ); + } + if (mode === 'count_matches' && orderedLines.length > 0) { + sideChannelMessages.push(formatCountSummary(orderedLines, filteredSensitive.size > 0)); + } + if (paginationTruncated) { + const total = afterOffset.length + offset; + const nextOffset = offset + headLimit; + const paginationNotice = `Results truncated to ${String(headLimit)} lines (total: ${String(total)}). Use offset=${String(nextOffset)} to see more.`; + if (mode === 'count_matches') { + sideChannelMessages.push(paginationNotice); + } else { + messages.push(paginationNotice); + } + } + if (bufferTruncated) { + messages.push( + `[stdout truncated at ${String(MAX_OUTPUT_BYTES)} bytes; incomplete trailing line omitted]`, + ); + } + if (timedOut) { + messages.push( + `Grep timed out after ${String(DEFAULT_TIMEOUT_MS / 1000)}s; partial results returned`, + ); + } + + const contentIncludesLineNumbers = mode === 'content' && args['-n'] !== false; + const displayedLines = limited.map((line) => + formatDisplayLine( + line, + mode, + this.workspace.workspaceDir, + pathClass, + contentIncludesLineNumbers, + ), + ); + const contentBody = displayedLines.join('\n'); + const visibleBody = + orderedLines.length === 0 && filteredSensitive.size > 0 + ? 'No non-sensitive matches found' + : contentBody; + const emptyResultMessage = + SENSITIVE_GLOBS_TO_EXCLUDE.length > 0 ? 'No non-sensitive matches found' : 'No matches found'; + const combined = + visibleBody === '' && messages.length === 0 + ? emptyResultMessage + : messages.length > 0 + ? visibleBody === '' + ? messages.join('\n') + : `${visibleBody}\n${messages.join('\n')}` + : visibleBody; + + const builder = new ToolResultBuilder(); + builder.write(combined); + return builder.ok(sideChannelMessages.join('\n')); + } + +} + +interface RipgrepRunResult { + readonly kind: 'result'; + readonly exitCode: number; + readonly stdoutText: string; + readonly stderrText: string; + readonly bufferTruncated: boolean; + readonly stderrTruncated: boolean; + readonly timedOut: boolean; +} + +type RipgrepRunOutcome = + | RipgrepRunResult + | { readonly kind: 'tool-error'; readonly result: ExecutableToolResult }; + +type GrepMode = 'content' | 'files_with_matches' | 'count_matches'; + +type ParsedGrepLine = + | { + readonly kind: 'record'; + readonly filePath: string; + readonly payload: string; + } + | { + readonly kind: 'separator'; + } + | { + readonly kind: 'legacy'; + readonly text: string; + }; + +class GrepAbortedError extends Error { + constructor() { + super('Grep aborted'); + this.name = 'GrepAbortedError'; + } +} + +async function runRipgrepOnce( + kaos: Kaos, + rgArgs: readonly string[], + signal: AbortSignal, +): Promise { + if (signal.aborted) { + return { kind: 'tool-error', result: { isError: true, output: 'Grep aborted' } }; + } + + let proc: KaosProcess; + try { + proc = await kaos.exec(...rgArgs); + } catch (error) { + // Spawn can still fail after path resolution, e.g. permissions or a + // corrupt binary. ENOENT gets the same actionable hint as locator failures. + const isEnoent = + error instanceof Error && + 'code' in error && + (error as NodeJS.ErrnoException).code === 'ENOENT'; + return { + kind: 'tool-error', + result: { + isError: true, + output: isEnoent + ? rgUnavailableMessage(error) + : error instanceof Error + ? error.message + : String(error), + }, + }; + } + + try { + proc.stdin.end(); + } catch { + /* already gone */ + } + + let timedOut = false; + let aborted = false; + let killed = false; + + const killProc = async (): Promise => { + if (killed) return; + killed = true; + try { + await proc.kill('SIGTERM'); + } catch { + /* process already gone */ + } + const exited = proc + .wait() + .then(() => true) + .catch(() => true); + const raced = await Promise.race([ + exited, + new Promise((resolve) => { + setTimeout(() => { + resolve(false); + }, SIGTERM_GRACE_MS); + }), + ]); + if (!raced && proc.exitCode === null) { + try { + await proc.kill('SIGKILL'); + } catch { + /* ignore */ + } + } + }; + + const onAbort = (): void => { + aborted = true; + void killProc(); + }; + signal.addEventListener('abort', onAbort); + // AbortSignal does not replay past abort events; check once after registering + // the listener so already-aborted calls still run the cleanup path. + if (signal.aborted) onAbort(); + + const timeoutHandle = setTimeout(() => { + timedOut = true; + void killProc(); + }, DEFAULT_TIMEOUT_MS); + + let exitCode = 0; + let stdoutText = ''; + let stderrText = ''; + let bufferTruncated = false; + let stderrTruncated = false; + + try { + const [stdoutResult, stderrResult, code] = await Promise.all([ + readStreamWithCap(proc.stdout, MAX_OUTPUT_BYTES), + readStreamWithCap(proc.stderr, MAX_OUTPUT_BYTES), + proc.wait(), + ]); + stdoutText = stdoutResult.text; + stderrText = stderrResult.text; + bufferTruncated = stdoutResult.truncated; + stderrTruncated = stderrResult.truncated; + exitCode = code; + } catch (error) { + return { + kind: 'tool-error', + result: { + isError: true, + output: error instanceof Error ? error.message : String(error), + }, + }; + } finally { + clearTimeout(timeoutHandle); + signal.removeEventListener('abort', onAbort); + } + + if (aborted) { + return { kind: 'tool-error', result: { isError: true, output: 'Grep aborted' } }; + } + + return { + kind: 'result', + exitCode, + stdoutText, + stderrText, + bufferTruncated, + stderrTruncated, + timedOut, + }; +} + +function shouldRetryRipgrepEagain(result: RipgrepRunResult): boolean { + return ( + result.exitCode !== 0 && + result.exitCode !== 1 && + !result.timedOut && + isEagainRipgrepError(result.stderrText) + ); +} + +function isEagainRipgrepError(stderr: string): boolean { + return stderr.includes('os error 11') || stderr.includes('Resource temporarily unavailable'); +} + +async function sortFilesWithMatchesByMtime( + lines: readonly ParsedGrepLine[], + kaos: Kaos, + signal: AbortSignal, +): Promise { + const entries = await mapWithConcurrency( + lines, + MTIME_STAT_CONCURRENCY, + signal, + async (line, index) => { + const path = + line.kind === 'record' ? line.filePath : line.kind === 'legacy' ? line.text : undefined; + let mtime = 0; + if (path !== undefined) { + try { + mtime = (await kaos.stat(path)).stMtime ?? 0; + } catch { + // Keep stat failures visible; use mtime=0 so they sort after known files. + } + } + return { line, mtime, index }; + }, + ); + entries.sort((a, b) => b.mtime - a.mtime || a.index - b.index); + return entries.map((entry) => entry.line); +} + +async function mapWithConcurrency( + items: readonly T[], + concurrency: number, + signal: AbortSignal, + mapper: (item: T, index: number) => Promise, +): Promise { + if (signal.aborted) throw new GrepAbortedError(); + if (items.length === 0) return []; + + const results: U[] = []; + results.length = items.length; + let nextIndex = 0; + const workerCount = Math.min(Math.max(1, concurrency), items.length); + await Promise.all( + Array.from({ length: workerCount }, async () => { + while (true) { + if (signal.aborted) return; + const index = nextIndex; + nextIndex += 1; + if (index >= items.length) return; + results[index] = await mapper(items[index] as T, index); + } + }), + ); + if (signal.aborted) throw new GrepAbortedError(); + return results; +} + +function buildRgArgs( + rgPath: string, + args: GrepInput, + searchPaths: readonly string[], + singleThreaded = false, +): string[] { + const cmd: string[] = [rgPath]; + if (singleThreaded) cmd.push('-j', '1'); + cmd.push('--hidden'); + const mode = args.output_mode ?? 'files_with_matches'; + // `content` mode returns matching lines verbatim. Capping columns here would + // make rg replace any line wider than the cap with a placeholder, silently + // dropping the actual match text. The cap is only useful outside `content` + // mode, where line text is never surfaced. + if (mode !== 'content') { + cmd.push('--max-columns', String(RG_MAX_COLUMNS)); + } + cmd.push('--null'); + for (const dir of VCS_DIRECTORIES_TO_EXCLUDE) { + cmd.push('--glob', `!${dir}`); + } + + if (mode === 'files_with_matches') cmd.push('-l'); + else if (mode === 'count_matches') { + // rg omits the filename when only one file is searched, so pin it on. Without + // this, the per-file line collapses to a bare count and the summary parser + // disagrees with the displayed number. + cmd.push('--count-matches', '--with-filename'); + } + + if (args['-i']) cmd.push('-i'); + if (mode === 'content') { + cmd.push('--with-filename'); + if (args['-n'] !== false) { + cmd.push('-n'); + } else { + cmd.push('--field-context-separator', ':'); + } + if (args['-C'] !== undefined) { + cmd.push('-C', String(args['-C'])); + } else { + if (args['-A'] !== undefined) cmd.push('-A', String(args['-A'])); + if (args['-B'] !== undefined) cmd.push('-B', String(args['-B'])); + } + } + if (args.glob !== undefined) cmd.push('--glob', args.glob); + if (args.type !== undefined) cmd.push('--type', args.type); + if (args.multiline) cmd.push('-U', '--multiline-dotall'); + if (args.include_ignored) cmd.push('--no-ignore'); + for (const glob of SENSITIVE_GLOBS_TO_EXCLUDE) { + // Appended after user globs so a broad include such as `**/.env` cannot + // undo this first-pass exclusion. Explicit file paths are still protected + // by the post-processing filter because rg intentionally searches them. + cmd.push('--glob', `!${glob}`); + } + // Do not forward `head_limit` to `rg --max-count`: omitted means "use the + // tool default", head_limit=0 means "unlimited", while `rg --max-count 0` + // means "zero matches per file". Pagination happens in post-processing. + + cmd.push('--', args.pattern, ...searchPaths); + return cmd; +} + +function splitRgLines(text: string): string[] { + if (text === '') return []; + const lines = text.split('\n'); + // Strip the trailing empty line left by a final newline. + while (lines.length > 0 && lines.at(-1) === '') { + lines.pop(); + } + return lines.map((line) => stripTrailingCarriageReturn(line)); +} + +function parseRipgrepOutput(text: string, mode: GrepMode): ParsedGrepLine[] { + if (text === '') return []; + if (!text.includes('\0')) { + return splitRgLines(text).map((line) => + mode === 'content' && line === '--' ? { kind: 'separator' } : { kind: 'legacy', text: line }, + ); + } + + if (mode === 'files_with_matches') { + return text + .split('\0') + .map((filePath) => stripTrailingCarriageReturn(filePath)) + .filter((filePath) => filePath !== '') + .map((filePath) => ({ kind: 'record', filePath, payload: '' })); + } + + const records: ParsedGrepLine[] = []; + let cursor = 0; + while (cursor < text.length) { + if (text[cursor] === '\n') { + cursor += 1; + continue; + } + if (text.startsWith('--\r\n', cursor)) { + records.push({ kind: 'separator' }); + cursor += 4; + continue; + } + if (text.startsWith('--\n', cursor)) { + records.push({ kind: 'separator' }); + cursor += 3; + continue; + } + + const nulIndex = text.indexOf('\0', cursor); + if (nulIndex < 0) { + const tail = stripTrailingCarriageReturn(text.slice(cursor)); + if (tail !== '') records.push({ kind: 'legacy', text: tail }); + break; + } + + const lineEnd = text.indexOf('\n', nulIndex + 1); + const payloadEnd = lineEnd >= 0 ? lineEnd : text.length; + const filePath = text.slice(cursor, nulIndex); + const payload = stripTrailingCarriageReturn(text.slice(nulIndex + 1, payloadEnd)); + records.push({ kind: 'record', filePath, payload }); + cursor = lineEnd >= 0 ? lineEnd + 1 : text.length; + } + return records; +} + +function formatDisplayLine( + line: ParsedGrepLine, + mode: GrepMode, + workspaceDir: string, + pathClass: PathClass, + contentIncludesLineNumbers: boolean, +): string { + if (line.kind === 'separator') return '--'; + if (line.kind === 'record') { + const displayPath = relativizeIfUnder(line.filePath, workspaceDir, pathClass); + if (mode === 'files_with_matches') return displayPath; + if (mode === 'count_matches') return `${displayPath}:${line.payload}`; + const separator = contentIncludesLineNumbers ? contentPayloadPathSeparator(line.payload) : ':'; + return `${displayPath}${separator}${line.payload}`; + } + + const text = line.text; + if (mode === 'files_with_matches') { + return relativizeIfUnder(text, workspaceDir, pathClass); + } + if (mode === 'count_matches') { + const idx = text.lastIndexOf(':'); + if (idx <= 0) return text; + return relativizeIfUnder(text.slice(0, idx), workspaceDir, pathClass) + text.slice(idx); + } + + const filePath = extractContentFilePath(text, pathClass); + if (filePath !== undefined) { + return relativizeIfUnder(filePath, workspaceDir, pathClass) + text.slice(filePath.length); + } + return text; +} + +/** + * If `candidate` is under `base`, return the portion after `base/`. + * Otherwise return `candidate` unchanged. Both arguments should be + * canonical absolute paths in the active backend path class. + */ +function relativizeIfUnder(candidate: string, base: string, pathClass: PathClass): string { + const sep = pathClass === 'win32' ? '\\' : '/'; + const comparableCandidate = pathClass === 'win32' ? candidate.toLowerCase() : candidate; + const comparableBase = pathClass === 'win32' ? base.toLowerCase() : base; + if (comparableCandidate === comparableBase) return '.'; + const prefix = comparableBase.endsWith(sep) ? comparableBase : comparableBase + sep; + if (comparableCandidate.startsWith(prefix)) { + return candidate.slice(prefix.length); + } + return candidate; +} + +function omitIncompleteTrailingRecord(text: string, mode: GrepMode): string { + if (!text.includes('\0')) return omitIncompleteTrailingLine(text); + if (mode === 'files_with_matches') { + const lastNul = text.lastIndexOf('\0'); + return lastNul >= 0 ? text.slice(0, lastNul + 1) : ''; + } + + let cursor = 0; + let lastCompleteEnd = 0; + while (cursor < text.length) { + if (text[cursor] === '\n') { + cursor += 1; + lastCompleteEnd = cursor; + continue; + } + if (text.startsWith('--\r\n', cursor)) { + cursor += 4; + lastCompleteEnd = cursor; + continue; + } + if (text.startsWith('--\n', cursor)) { + cursor += 3; + lastCompleteEnd = cursor; + continue; + } + + const nulIndex = text.indexOf('\0', cursor); + if (nulIndex < 0) break; + const lineEnd = text.indexOf('\n', nulIndex + 1); + if (lineEnd < 0) break; + cursor = lineEnd + 1; + lastCompleteEnd = cursor; + } + return text.slice(0, lastCompleteEnd); +} + +function omitIncompleteTrailingLine(text: string): string { + const lastNewline = text.lastIndexOf('\n'); + return lastNewline >= 0 ? text.slice(0, lastNewline) : ''; +} + +function formatRipgrepError( + exitCode: number, + stderrText: string, + stderrTruncated: boolean, +): string { + const stderr = stderrText.trim(); + if (stderr.length === 0) { + return `Failed to grep: ripgrep exited with code ${String(exitCode)}`; + } + + const summary = summarizeRipgrepStderr(stderr); + const lines = [`Failed to grep: ${summary}`, '', 'ripgrep stderr:', stderr]; + if (stderrTruncated) { + lines.push(`[stderr truncated at ${String(MAX_OUTPUT_BYTES)} bytes]`); + } + return lines.join('\n'); +} + +function summarizeRipgrepStderr(stderr: string): string { + const lines = splitRgLines(stderr) + .map((line) => line.trim()) + .filter((line) => line.length > 0); + const errorLine = lines.findLast((line) => line.toLowerCase().startsWith('error:')); + return errorLine ?? lines.at(-1) ?? 'ripgrep error'; +} + +function filterSensitiveLines( + lines: readonly ParsedGrepLine[], + mode: GrepMode, + filteredPaths: Set, + pathClass: PathClass, +): ParsedGrepLine[] { + const kept: ParsedGrepLine[] = []; + for (const line of lines) { + if (line.kind === 'separator') { + kept.push(line); + continue; + } + const filePath = parsedFilePath(line, mode, pathClass); + if (filePath !== undefined && isSensitiveFile(filePath, pathClass)) { + filteredPaths.add(filePath); + continue; + } + kept.push(line); + } + return mode === 'content' ? normalizeContextSeparators(kept) : kept; +} + +function normalizeContextSeparators(lines: readonly ParsedGrepLine[]): ParsedGrepLine[] { + const normalized: ParsedGrepLine[] = []; + for (const line of lines) { + if ( + line.kind === 'separator' && + (normalized.length === 0 || normalized.at(-1)?.kind === 'separator') + ) { + continue; + } + normalized.push(line); + } + while (normalized.length > 0 && normalized.at(-1)?.kind === 'separator') { + normalized.pop(); + } + return normalized; +} + +function parsedFilePath( + line: ParsedGrepLine, + mode: GrepMode, + pathClass: PathClass, +): string | undefined { + if (line.kind === 'record') return line.filePath; + if (line.kind === 'separator') return undefined; + const text = line.text; + if (mode === 'files_with_matches') return text; + if (mode === 'count_matches') { + const idx = text.lastIndexOf(':'); + return idx > 0 ? text.slice(0, idx) : text; + } + return extractContentFilePath(text, pathClass); +} + +function extractContentFilePath(line: string, pathClass: PathClass): string | undefined { + const m = CONTENT_LINE_RE.exec(line); + if (m?.[1] !== undefined) return m[1]; + + const separatorIndex = noLineNumberContentSeparatorIndex(line, pathClass); + return separatorIndex > 0 ? line.slice(0, separatorIndex) : undefined; +} + +function noLineNumberContentSeparatorIndex(line: string, pathClass: PathClass): number { + const searchFrom = pathClass === 'win32' && /^[A-Za-z]:/.test(line) ? 2 : 0; + return line.indexOf(':', searchFrom); +} + +function contentPayloadPathSeparator(payload: string): ':' | '-' { + const m = /^(\d+)([:-])/.exec(payload); + return m?.[2] === '-' ? '-' : ':'; +} + +function stripTrailingCarriageReturn(value: string): string { + return value.endsWith('\r') ? value.slice(0, -1) : value; +} + +function formatCountSummary(lines: readonly ParsedGrepLine[], redactedSensitive: boolean): string { + let totalMatches = 0; + let totalFiles = 0; + for (const line of lines) { + const rawCount = + line.kind === 'record' + ? line.payload + : line.kind === 'legacy' + ? countPayloadFromLegacyLine(line.text) + : undefined; + if (rawCount === undefined) continue; + const count = Number(rawCount); + if (!Number.isSafeInteger(count) || count < 0) continue; + totalMatches += count; + totalFiles++; + } + + const occurrenceWord = totalMatches === 1 ? 'occurrence' : 'occurrences'; + const fileWord = totalFiles === 1 ? 'file' : 'files'; + const scope = redactedSensitive ? 'total non-sensitive' : 'total'; + return `Found ${String(totalMatches)} ${scope} ${occurrenceWord} across ${String(totalFiles)} ${fileWord}.`; +} + +function countPayloadFromLegacyLine(line: string): string | undefined { + const idx = line.lastIndexOf(':'); + return idx > 0 ? line.slice(idx + 1) : undefined; +} + +interface CappedStreamResult { + readonly text: string; + readonly truncated: boolean; +} + +async function readStreamWithCap(stream: Readable, maxBytes: number): Promise { + const chunks: Buffer[] = []; + let total = 0; + let truncated = false; + for await (const chunk of stream) { + const buf: Buffer = typeof chunk === 'string' ? Buffer.from(chunk, 'utf8') : (chunk as Buffer); + if (truncated) continue; + if (total + buf.length > maxBytes) { + const remaining = maxBytes - total; + if (remaining > 0) chunks.push(buf.subarray(0, remaining)); + total = maxBytes; + truncated = true; + continue; + } + chunks.push(buf); + total += buf.length; + } + return { text: Buffer.concat(chunks).toString('utf8'), truncated }; +} diff --git a/packages/agent-core/src/tools/builtin/file/line-endings.ts b/packages/agent-core/src/tools/builtin/file/line-endings.ts new file mode 100644 index 000000000..38e117a77 --- /dev/null +++ b/packages/agent-core/src/tools/builtin/file/line-endings.ts @@ -0,0 +1,51 @@ +export type LineEndingStyle = 'lf' | 'crlf' | 'mixed'; + +export interface ModelTextView { + text: string; + lineEndingStyle: LineEndingStyle; +} + +export function detectLineEndingStyle(text: string): LineEndingStyle { + let hasCrLf = false; + let hasLf = false; + let hasLoneCr = false; + + for (let i = 0; i < text.length; i++) { + const code = text.codePointAt(i); + if (code === 13) { + if (text.codePointAt(i + 1) === 10) { + hasCrLf = true; + i++; + } else { + hasLoneCr = true; + } + } else if (code === 10) { + hasLf = true; + } + } + + if (hasLoneCr || (hasCrLf && hasLf)) return 'mixed'; + if (hasCrLf) return 'crlf'; + return 'lf'; +} + +export function toModelTextView(raw: string): ModelTextView { + const lineEndingStyle = detectLineEndingStyle(raw); + if (lineEndingStyle !== 'crlf') { + return { text: raw, lineEndingStyle }; + } + + return { + text: raw.replaceAll('\r\n', '\n'), + lineEndingStyle, + }; +} + +export function materializeModelText(text: string, lineEndingStyle: LineEndingStyle): string { + if (lineEndingStyle !== 'crlf') return text; + return text.replaceAll('\r\n', '\n').replaceAll('\n', '\r\n'); +} + +export function makeCarriageReturnsVisible(text: string): string { + return text.replaceAll('\r', '\\r'); +} diff --git a/packages/agent-core/src/tools/builtin/file/read-media.md b/packages/agent-core/src/tools/builtin/file/read-media.md new file mode 100644 index 000000000..0ff49fb6c --- /dev/null +++ b/packages/agent-core/src/tools/builtin/file/read-media.md @@ -0,0 +1,13 @@ +Read media content from a file. + +**Tips:** +- Make sure you follow the description of each tool parameter. +- A `` tag is given before the file content; it summarizes the mime type, byte size and, for images, the original pixel dimensions. When outputting coordinates, give relative coordinates first and compute absolute coordinates from the original image size. After generating or editing media via commands or scripts, read the result back before continuing. +- The system will notify you when there is anything wrong when reading the file. +- This tool is a tool that you typically want to use in parallel. Always read multiple files in one response when possible. +- This tool can only read image or video files. To read text files, use the Read tool. To list directories, use `ls` via Bash for a known directory, or Glob for pattern search. +- If the file doesn't exist or path is invalid, an error will be returned. +- The maximum size that can be read is {{ MAX_MEDIA_MEGABYTES }}MB. An error will be returned if the file is larger than this limit. +- The media content will be returned in a form that you can directly view and understand. + +**Capabilities** \ No newline at end of file diff --git a/packages/agent-core/src/tools/builtin/file/read-media.ts b/packages/agent-core/src/tools/builtin/file/read-media.ts new file mode 100644 index 000000000..b486d74ff --- /dev/null +++ b/packages/agent-core/src/tools/builtin/file/read-media.ts @@ -0,0 +1,264 @@ +/** + * ReadMediaFileTool — read image/video files as multi-modal content. + * + * Returns a 4-part wrap: + * `[TextPart(''), TextPart(''), + * ImageContent|VideoContent, TextPart('')]` + * and gates on the model's `image_in` / `video_in` capability. + * + * The leading `` block summarizes mime type, byte size and (for + * images) original pixel dimensions, guides the model to derive absolute + * coordinates from that original size, and reminds it to re-read any media + * it generates or edits. + * + * Path safety: goes through the shared path access resolver used by + * Read/Write/Edit. + */ + +import type { Kaos } from '@moonshot-ai/kaos'; +import type { + ContentPart, + ModelCapability, + VideoURLPart, + VideoUploadInput as ProviderVideoUploadInput, +} from '@moonshot-ai/kosong'; +import { z } from 'zod'; + +import type { BuiltinTool } from '../../../agent/tool'; +import { ToolAccesses } from '../../../loop/tool-access'; +import type { ExecutableToolResult, ToolExecution } from '../../../loop/types'; +import { renderPrompt } from '../../../utils/render-prompt'; +import { resolvePathAccessPath } from '../../policies/path-access'; +import { MEDIA_SNIFF_BYTES, detectFileType, sniffImageDimensions } from '../../support/file-type'; +import { toInputJsonSchema } from '../../support/input-schema'; +import type { WorkspaceConfig } from '../../support/workspace'; +import readMediaDescriptionHead from './read-media.md'; + +// ── Constants ──────────────────────────────────────────────────────── + +const MAX_MEDIA_MEGABYTES = 100; +const MAX_MEDIA_BYTES = MAX_MEDIA_MEGABYTES * 1024 * 1024; + +export type VideoUploadInput = ProviderVideoUploadInput; + +export type VideoUploader = (input: VideoUploadInput) => Promise; + +// ── Input schema ───────────────────────────────────────────────────── + +export const ReadMediaFileInputSchema = z.object({ + path: z + .string() + .describe( + 'Path to an image or video file. Relative paths resolve against the working directory; ' + + 'a path outside the working directory must be absolute. ' + + 'Directories and text files are not supported.', + ), +}); + +export type ReadMediaFileInput = z.Infer; + +// ── Tool description (capability-driven) ───────────────────────────── + +function buildDescription(capabilities: ModelCapability): string { + const head = renderPrompt(readMediaDescriptionHead, { MAX_MEDIA_MEGABYTES }); + const lines: string[] = [head]; + const hasImage = capabilities.image_in; + const hasVideo = capabilities.video_in; + if (hasImage && hasVideo) { + lines.push('- This tool supports image and video files for the current model.'); + } else if (hasImage) { + lines.push( + '- This tool supports image files for the current model.', + '- Video files are not supported by the current model.', + ); + } else if (hasVideo) { + lines.push( + '- This tool supports video files for the current model.', + '- Image files are not supported by the current model.', + ); + } else { + lines.push('- The current model does not support image or video input.'); + } + return lines.join('\n'); +} + +// ── System summary ─────────────────────────────────────────────────── + +/** + * Build the `` summary that precedes the media content. + * + * Carries mime type, byte size and (for images) the original pixel + * dimensions. When the dimensions are known it also guides the model to + * derive absolute coordinates from that original size; it always reminds + * the model to re-read any media it generates or edits. + */ +function buildSystemSummary(input: { + readonly kind: 'image' | 'video'; + readonly mimeType: string; + readonly byteSize: number; + readonly dimensions: { readonly width: number; readonly height: number } | null; +}): string { + const parts: string[] = [ + `Read ${input.kind} file.`, + `Mime type: ${input.mimeType}.`, + `Size: ${String(input.byteSize)} bytes.`, + ]; + // Coordinate guidance is only emitted when the original size is actually + // known — sniffing fails for some image formats (TIFF/ICO/HEIC/…), and + // telling the model to use a size that is not in the block would mislead it. + if (input.kind === 'image' && input.dimensions) { + parts.push( + `Original dimensions: ${String(input.dimensions.width)}x${String(input.dimensions.height)} pixels.`, + 'If you need to output coordinates, output relative coordinates first ' + + 'and compute absolute coordinates using the original image size.', + ); + } + parts.push( + 'If you generate or edit images or videos via commands or scripts, ' + + 'read the result back immediately before continuing.', + ); + return `${parts.join(' ')}`; +} + +// ── Implementation ─────────────────────────────────────────────────── + +export class ReadMediaFileTool implements BuiltinTool { + readonly name = 'ReadMediaFile' as const; + readonly description: string; + readonly parameters: Record = toInputJsonSchema(ReadMediaFileInputSchema); + constructor( + private readonly kaos: Kaos, + private readonly workspace: WorkspaceConfig, + private readonly capabilities: ModelCapability, + private readonly videoUploader?: VideoUploader | undefined, + ) { + if (!capabilities.image_in && !capabilities.video_in) { + const skip = new Error('ReadMediaFile requires image_in or video_in capability'); + skip.name = 'SkipThisTool'; + throw skip; + } + this.description = buildDescription(capabilities); + } + + resolveExecution(args: ReadMediaFileInput): ToolExecution { + const path = resolvePathAccessPath(args.path, { + kaos: this.kaos, + workspace: this.workspace, + operation: 'read', + }); + return { + accesses: ToolAccesses.readFile(path), + description: `Reading media: ${args.path}`, + execute: () => this.execution(args, path), + }; + } + + private async execution( + args: ReadMediaFileInput, + safePath: string, + ): Promise { + if (!args.path) { + return { isError: true, output: 'File path cannot be empty.' }; + } + + try { + // Sniff header first — read the first 512 bytes before deciding + // anything about MIME. + const header = await this.kaos.readBytes(safePath, MEDIA_SNIFF_BYTES); + const fileType = detectFileType(safePath, header); + + if (fileType.kind === 'text') { + return { + isError: true, + output: `"${args.path}" is a text file. Use Read to read text files.`, + }; + } + if (fileType.kind === 'unknown') { + return { + isError: true, + output: + `"${args.path}" is not a supported image or video file. ` + + 'Use Read for text files, or Bash or an MCP tool for other binary formats.', + }; + } + + if (fileType.kind === 'image' && !this.capabilities.image_in) { + return { + isError: true, + output: + 'The current model does not support image input. ' + + 'Tell the user to use a model with image input capability.', + }; + } + if (fileType.kind === 'video' && !this.capabilities.video_in) { + return { + isError: true, + output: + 'The current model does not support video input. ' + + 'Tell the user to use a model with video input capability.', + }; + } + + const stat = await this.kaos.stat(safePath); + if (stat.stSize === 0) { + return { isError: true, output: `"${args.path}" is empty.` }; + } + if (stat.stSize > MAX_MEDIA_BYTES) { + return { + isError: true, + output: + `"${args.path}" is ${String(stat.stSize)} bytes, which exceeds the ` + + `maximum ${String(MAX_MEDIA_MEGABYTES)}MB for media files.`, + }; + } + + const data = await this.kaos.readBytes(safePath); + const base64 = data.toString('base64'); + let mediaPart: ContentPart; + if (fileType.kind === 'image') { + mediaPart = { + type: 'image_url', + imageUrl: { url: `data:${fileType.mimeType};base64,${base64}` }, + }; + } else if (this.videoUploader !== undefined) { + mediaPart = await this.videoUploader({ + data, + mimeType: fileType.mimeType, + filename: safePath.split(/[\\/]/).at(-1), + }); + } else { + mediaPart = { + type: 'video_url', + videoUrl: { url: `data:${fileType.mimeType};base64,${base64}` }, + }; + } + + const tag = fileType.kind === 'image' ? 'image' : 'video'; + const openText = `<${tag} path="${safePath}">`; + const closeText = ``; + + const dimensions = + fileType.kind === 'image' ? sniffImageDimensions(data) : null; + const systemText = buildSystemSummary({ + kind: fileType.kind, + mimeType: fileType.mimeType, + byteSize: stat.stSize, + dimensions, + }); + + const output: ContentPart[] = [ + { type: 'text', text: systemText }, + { type: 'text', text: openText }, + mediaPart, + { type: 'text', text: closeText }, + ]; + + return { output, isError: false }; + } catch (error) { + return { + isError: true, + output: `Failed to read ${args.path}: ${error instanceof Error ? error.message : String(error)}`, + }; + } + } +} diff --git a/packages/agent-core/src/tools/builtin/file/read.md b/packages/agent-core/src/tools/builtin/file/read.md new file mode 100644 index 000000000..79bdda810 --- /dev/null +++ b/packages/agent-core/src/tools/builtin/file/read.md @@ -0,0 +1,17 @@ +Read a text file from the local filesystem. + +If the user provides a concrete file path to a text file, call Read directly. Do not `Glob`, `ls`, or otherwise pre-check known text file paths; missing or invalid file paths return errors you can handle. Do not use Read for directories; use `ls` via Bash for a known directory, or Glob when you need files/directories matching a pattern. Use `Grep` only when the task is to search for unknown content or locations. + +When you need several files, prefer to read them in parallel: emit multiple `Read` calls in a single response instead of reading one file per turn. + +- Relative paths resolve against the working directory; a path outside the working directory must be absolute. +- Returns up to {{ MAX_LINES }} lines or {{ MAX_BYTES_KB }} KB per call, whichever comes first; lines longer than {{ MAX_LINE_LENGTH }} chars are truncated mid-line. +- Page larger files with `line_offset` (1-based start line) and `n_lines`. Omit `n_lines` to read up to the {{ MAX_LINES }}-line cap. +- Sensitive files (`.env` files, credential stores, SSH keys, and similar secrets) are refused to protect secrets; do not attempt to read them. +- Only UTF-8 text files can be read. Non-UTF-8 encodings, binary files, and files containing NUL bytes are refused; use `ReadMediaFile` for images or video, and Bash or an MCP tool for other binary formats. +- Negative line_offset reads from the end of the file (for example, -100 reads the last 100 lines); the absolute value cannot exceed {{ MAX_LINES }}. +- Output format: `\t` per line. +- A `...` status block is appended after the file content; it summarizes how much was read (line and byte counts, truncation, line-ending notes) and is not part of the file itself. +- Pure CRLF files are displayed with LF line endings; `Edit` matches this output and preserves CRLF when writing back. +- Mixed or lone carriage-return line endings are shown as `\r` and require exact `Edit.old_string` escapes. +- After a successful `Edit`/`Write`, do not re-read solely to prove the write landed. When the task depends on an exact file, API, or output shape, inspect the final external contract before finishing. diff --git a/packages/agent-core/src/tools/builtin/file/read.ts b/packages/agent-core/src/tools/builtin/file/read.ts new file mode 100644 index 000000000..2e27b06a8 --- /dev/null +++ b/packages/agent-core/src/tools/builtin/file/read.ts @@ -0,0 +1,444 @@ +import type { Kaos, StatResult } from '@moonshot-ai/kaos'; +import { z } from 'zod'; + +import type { BuiltinTool } from '../../../agent/tool'; +import { ToolAccesses } from '../../../loop/tool-access'; +import type { ExecutableToolResult, ToolExecution } from '../../../loop/types'; +import { renderPrompt } from '../../../utils/render-prompt'; +import { resolvePathAccessPath } from '../../policies/path-access'; +import { MEDIA_SNIFF_BYTES, detectFileType } from '../../support/file-type'; +import { toInputJsonSchema } from '../../support/input-schema'; +import type { WorkspaceConfig } from '../../support/workspace'; +import { makeCarriageReturnsVisible, type LineEndingStyle } from './line-endings'; +import readDescriptionTemplate from './read.md'; + +export const MAX_LINES: number = 1000; +export const MAX_LINE_LENGTH: number = 2000; +export const MAX_BYTES: number = 100 * 1024; +const S_IFMT = 0o170000; +const S_IFREG = 0o100000; + +const PositiveLineOffsetSchema = z.number().int().min(1); +const TailLineOffsetSchema = z.number().int().min(-MAX_LINES).max(-1); + +export const ReadInputSchema = z.object({ + path: z + .string() + .describe( + 'Path to a text file. Relative paths resolve against the working directory; a path outside the working directory must be absolute. Directories are not supported; use `ls` via Bash for a known directory, or Glob for pattern search.', + ), + line_offset: z + .union([PositiveLineOffsetSchema, TailLineOffsetSchema]) + .optional() + .describe( + `The line number to start reading from. Omit to start at line 1. Negative values read from the end of the file; the absolute value cannot exceed ${String(MAX_LINES)}.`, + ), + n_lines: z + .number() + .int() + .positive() + .optional() + .describe( + `The number of lines to read; the tool also applies its internal cap. Omit to read up to the internal cap of ${String(MAX_LINES)} lines.`, + ), +}); + +export const ReadOutputSchema = z.object({ + content: z.string(), + lineCount: z.number().int().nonnegative(), +}); + +export type ReadInput = z.Infer; +export type ReadOutput = z.Infer; + +interface LineEndingFlags { + hasCrLf: boolean; + hasLf: boolean; + hasLoneCr: boolean; +} + +interface ReadLineEntry { + readonly lineNo: number; + readonly rawContent: string; +} + +interface RenderedLine { + readonly line: string; + readonly wasTruncated: boolean; +} + +interface FinishReadResultInput { + readonly renderedLines: readonly string[]; + readonly truncatedLineNumbers: readonly number[]; + readonly maxLinesReached: boolean; + readonly maxBytesReached: boolean; + readonly lineEndingStyle: LineEndingStyle; + readonly startLine: number; + readonly totalLines: number; + readonly requestedLines: number; +} + +function truncateLine(line: string, maxLength: number): string { + if (line.length <= maxLength) return line; + const marker = '...'; + const target = Math.max(maxLength, marker.length); + return line.slice(0, target - marker.length) + marker; +} + +function stripTrailingLf(line: string): string { + return line.endsWith('\n') ? line.slice(0, -1) : line; +} + +function updateLineEndingFlags(flags: LineEndingFlags, text: string): void { + for (let i = 0; i < text.length; i += 1) { + const code = text.codePointAt(i); + if (code === 13) { + if (text.codePointAt(i + 1) === 10) { + flags.hasCrLf = true; + i += 1; + } else { + flags.hasLoneCr = true; + } + } else if (code === 10) { + flags.hasLf = true; + } + } +} + +function lineEndingStyleFromFlags(flags: LineEndingFlags): LineEndingStyle { + if (flags.hasLoneCr || (flags.hasCrLf && flags.hasLf)) return 'mixed'; + if (flags.hasCrLf) return 'crlf'; + return 'lf'; +} + +function renderLine(entry: ReadLineEntry, lineEndingStyle: LineEndingStyle): RenderedLine { + const modelContent = + lineEndingStyle === 'crlf' && entry.rawContent.endsWith('\r') + ? entry.rawContent.slice(0, -1) + : entry.rawContent; + const truncated = truncateLine(modelContent, MAX_LINE_LENGTH); + const renderedContent = + lineEndingStyle === 'mixed' ? makeCarriageReturnsVisible(truncated) : truncated; + return { + line: `${String(entry.lineNo)}\t${renderedContent}`, + wasTruncated: truncated !== modelContent, + }; +} + +function renderedLineBytes(renderedLine: string, isFirst: boolean): number { + return (isFirst ? 0 : 1) + Buffer.byteLength(renderedLine, 'utf8'); +} + +function isRegularFileMode(stMode: number): boolean { + return (stMode & S_IFMT) === S_IFREG; +} + +function isFileNotFoundError(error: unknown): boolean { + if (typeof error !== 'object' || error === null) return false; + const code = (error as { code?: unknown })['code']; + return code === 'ENOENT' || code === 'ENOTDIR'; +} + +function isTextDecodeError(error: unknown): boolean { + if (typeof error !== 'object' || error === null) return false; + const code = (error as { code?: unknown })['code']; + if (code === 'ERR_ENCODING_INVALID_ENCODED_DATA') return true; + if (!(error instanceof Error)) return false; + return /encoded data was not valid|invalid.*encoding|invalid.*utf-?8/i.test(error.message); +} + +function containsNulByte(text: string): boolean { + return text.includes('\u0000'); +} + +function notReadableFileOutput(path: string): string { + return ( + `"${path}" is not readable as UTF-8 text. ` + + 'If it is an image or video, use ReadMediaFile. ' + + 'For other binary formats, use Bash or an MCP tool if available.' + ); +} + +const READ_DESCRIPTION = renderPrompt(readDescriptionTemplate, { + MAX_LINES, + MAX_BYTES_KB: MAX_BYTES / 1024, + MAX_LINE_LENGTH, +}); + +export class ReadTool implements BuiltinTool { + readonly name = 'Read' as const; + readonly description = READ_DESCRIPTION; + readonly parameters: Record = toInputJsonSchema(ReadInputSchema); + constructor( + private readonly kaos: Kaos, + private readonly workspace: WorkspaceConfig, + ) {} + + resolveExecution(args: ReadInput): ToolExecution { + const path = resolvePathAccessPath(args.path, { + kaos: this.kaos, + workspace: this.workspace, + operation: 'read', + }); + return { + accesses: ToolAccesses.readFile(path), + description: `Reading ${args.path}`, + execute: () => this.execution(args, path), + }; + } + + private async execution(args: ReadInput, safePath: string): Promise { + try { + let stat: StatResult; + try { + stat = await this.kaos.stat(safePath); + } catch (error) { + if (isFileNotFoundError(error)) { + return { isError: true, output: `"${args.path}" does not exist.` }; + } + throw error; + } + if (!isRegularFileMode(stat.stMode)) { + return { isError: true, output: `"${args.path}" is not a file.` }; + } + + const header = await this.kaos.readBytes(safePath, MEDIA_SNIFF_BYTES); + const fileType = detectFileType(safePath, header); + if (fileType.kind === 'image' || fileType.kind === 'video') { + return { + isError: true, + output: `"${args.path}" is a ${fileType.kind} file. Use ReadMediaFile to read image or video files.`, + }; + } + if (fileType.kind === 'unknown') { + return { + isError: true, + output: notReadableFileOutput(args.path), + }; + } + + const lineOffset = args.line_offset ?? 1; + const requestedLines = args.n_lines ?? MAX_LINES; + const effectiveLimit = Math.min(requestedLines, MAX_LINES); + + if (lineOffset < 0) { + return await this.readTail( + safePath, + args.path, + lineOffset, + effectiveLimit, + requestedLines, + ); + } + return await this.readForward( + safePath, + args.path, + lineOffset, + effectiveLimit, + requestedLines, + ); + } catch (error) { + if (isTextDecodeError(error)) { + return { isError: true, output: notReadableFileOutput(args.path) }; + } + return { + isError: true, + output: error instanceof Error ? error.message : String(error), + }; + } + } + + private async readForward( + safePath: string, + displayPath: string, + lineOffset: number, + effectiveLimit: number, + requestedLines: number, + ): Promise { + const selectedEntries: ReadLineEntry[] = []; + const flags: LineEndingFlags = { hasCrLf: false, hasLf: false, hasLoneCr: false }; + let currentLineNo = 0; + let maxLinesReached = false; + let collectionClosed = false; + + for await (const rawLine of this.kaos.readLines(safePath, { errors: 'strict' })) { + if (containsNulByte(rawLine)) { + return { isError: true, output: notReadableFileOutput(displayPath) }; + } + currentLineNo += 1; + updateLineEndingFlags(flags, rawLine); + if (collectionClosed) { + if (effectiveLimit >= MAX_LINES && currentLineNo >= lineOffset) { + maxLinesReached = true; + } + continue; + } + if (currentLineNo < lineOffset) continue; + if (selectedEntries.length >= effectiveLimit) { + if (effectiveLimit >= MAX_LINES) { + maxLinesReached = true; + } + collectionClosed = true; + continue; + } + selectedEntries.push({ + lineNo: currentLineNo, + rawContent: stripTrailingLf(rawLine), + }); + if (selectedEntries.length >= effectiveLimit) { + collectionClosed = true; + } + } + + const lineEndingStyle = lineEndingStyleFromFlags(flags); + const renderedLines: string[] = []; + const truncatedLineNumbers: number[] = []; + let bytes = 0; + let maxBytesReached = false; + + for (const entry of selectedEntries) { + const rendered = renderLine(entry, lineEndingStyle); + const lineBytes = renderedLineBytes(rendered.line, renderedLines.length === 0); + if (renderedLines.length > 0 && bytes + lineBytes > MAX_BYTES) { + maxBytesReached = true; + break; + } + + if (rendered.wasTruncated) { + truncatedLineNumbers.push(entry.lineNo); + } + renderedLines.push(rendered.line); + bytes += lineBytes; + if (bytes >= MAX_BYTES) { + maxBytesReached = true; + break; + } + } + + return this.finishReadResult({ + renderedLines, + truncatedLineNumbers, + maxLinesReached, + maxBytesReached, + lineEndingStyle, + startLine: renderedLines.length > 0 ? lineOffset : 0, + totalLines: currentLineNo, + requestedLines, + }); + } + + private async readTail( + safePath: string, + displayPath: string, + lineOffset: number, + effectiveLimit: number, + requestedLines: number, + ): Promise { + const tailCount = Math.abs(lineOffset); + const entries: ReadLineEntry[] = []; + const flags: LineEndingFlags = { hasCrLf: false, hasLf: false, hasLoneCr: false }; + let currentLineNo = 0; + + for await (const rawLine of this.kaos.readLines(safePath, { errors: 'strict' })) { + if (containsNulByte(rawLine)) { + return { isError: true, output: notReadableFileOutput(displayPath) }; + } + currentLineNo += 1; + updateLineEndingFlags(flags, rawLine); + entries.push({ + lineNo: currentLineNo, + rawContent: stripTrailingLf(rawLine), + }); + if (entries.length > tailCount) { + entries.shift(); + } + } + + const lineEndingStyle = lineEndingStyleFromFlags(flags); + let renderedCandidates = entries.slice(0, effectiveLimit).map((entry) => { + return { entry, rendered: renderLine(entry, lineEndingStyle) }; + }); + + let totalBytes = 0; + for (const [index, candidate] of renderedCandidates.entries()) { + totalBytes += renderedLineBytes(candidate.rendered.line, index === 0); + } + + let maxBytesReached = false; + if (totalBytes > MAX_BYTES) { + maxBytesReached = true; + const kept: typeof renderedCandidates = []; + let bytes = 0; + for (let i = renderedCandidates.length - 1; i >= 0; i -= 1) { + const candidate = renderedCandidates[i]; + if (candidate === undefined) continue; + const lineBytes = renderedLineBytes(candidate.rendered.line, kept.length === 0); + if (bytes + lineBytes > MAX_BYTES) break; + kept.unshift(candidate); + bytes += lineBytes; + } + renderedCandidates = kept; + } + + const renderedLines: string[] = []; + const truncatedLineNumbers: number[] = []; + for (const candidate of renderedCandidates) { + renderedLines.push(candidate.rendered.line); + if (candidate.rendered.wasTruncated) { + truncatedLineNumbers.push(candidate.entry.lineNo); + } + } + + return this.finishReadResult({ + renderedLines, + truncatedLineNumbers, + maxLinesReached: false, + maxBytesReached, + lineEndingStyle, + startLine: renderedCandidates[0]?.entry.lineNo ?? 0, + totalLines: currentLineNo, + requestedLines, + }); + } + + private finishReadResult(input: FinishReadResultInput): ExecutableToolResult { + return { + output: this.finishOutput(input.renderedLines, this.finishMessage(input)), + }; + } + + private finishOutput(renderedLines: readonly string[], message: string): string { + const rendered = renderedLines.join('\n'); + const status = `${message}`; + return rendered.length > 0 ? `${rendered}\n${status}` : status; + } + + private finishMessage(input: FinishReadResultInput): string { + const lineCount = input.renderedLines.length; + const lineWord = lineCount === 1 ? 'line' : 'lines'; + const parts = + lineCount > 0 + ? [ + `${String(lineCount)} ${lineWord} read from file starting from line ${String(input.startLine)}.`, + ] + : ['No lines read from file.']; + + parts.push(`Total lines in file: ${String(input.totalLines)}.`); + if (input.maxLinesReached) { + parts.push(`Max ${String(MAX_LINES)} lines reached.`); + } else if (input.maxBytesReached) { + parts.push(`Max ${String(MAX_BYTES)} bytes reached.`); + } else if (lineCount < input.requestedLines) { + parts.push('End of file reached.'); + } + if (input.truncatedLineNumbers.length > 0) { + parts.push(`Lines [${input.truncatedLineNumbers.join(', ')}] were truncated.`); + } + if (input.lineEndingStyle === 'mixed') { + parts.push( + 'Mixed or lone carriage-return line endings are shown as \\r. Use exact \\r\\n or \\r escapes in Edit.old_string for those lines.', + ); + } + return parts.join(' '); + } +} diff --git a/packages/agent-core/src/tools/builtin/file/write.md b/packages/agent-core/src/tools/builtin/file/write.md new file mode 100644 index 000000000..5d82cd148 --- /dev/null +++ b/packages/agent-core/src/tools/builtin/file/write.md @@ -0,0 +1 @@ +Overwrite or append to a file with content exactly as provided, creating the file if needed; the parent directory must already exist. Defaults to overwrite; append adds content to the end without adding a newline. Write does not use the Read/Edit model text view and does not preserve or infer the previous line-ending style: \n stays LF, \r\n stays CRLF. Use Edit for targeted changes to existing files. When the content is very large, you can split it across multiple calls: write the first chunk with overwrite, then add the remaining chunks with append. diff --git a/packages/agent-core/src/tools/builtin/file/write.ts b/packages/agent-core/src/tools/builtin/file/write.ts new file mode 100644 index 000000000..593980070 --- /dev/null +++ b/packages/agent-core/src/tools/builtin/file/write.ts @@ -0,0 +1,144 @@ +/** + * WriteTool — overwrite or append to a file. + * + * Creates the file if it does not exist; parent directory must already exist. + * Path access policy is resolved before any Kaos I/O. + */ + +import type { Kaos } from '@moonshot-ai/kaos'; +import * as posixPath from 'node:path/posix'; +import * as win32Path from 'node:path/win32'; +import { z } from 'zod'; + +import type { BuiltinTool } from '../../../agent/tool'; +import { ToolAccesses } from '../../../loop/tool-access'; +import type { ExecutableToolResult, ToolExecution } from '../../../loop/types'; +import { + type PathClass, + resolvePathAccessPath, +} from '../../policies/path-access'; +import { toInputJsonSchema } from '../../support/input-schema'; +import type { WorkspaceConfig } from '../../support/workspace'; +import WRITE_DESCRIPTION from './write.md'; + +/** Mask isolating the file-type bits of a stat mode. */ +const S_IFMT = 0o170000; +/** File-type bits of a directory. */ +const S_IFDIR = 0o040000; + +function pathMod(pathClass: PathClass): typeof posixPath { + return pathClass === 'win32' ? win32Path : posixPath; +} + +export const WriteInputSchema = z.object({ + path: z + .string() + .describe( + 'Path to the file to create, append to, or completely overwrite. Relative paths resolve against the working directory; a path outside the working directory must be absolute. The parent directory must already exist.', + ), + content: z + .string() + .describe( + 'Raw full file content to write exactly as provided. This does not use the Read/Edit text view.', + ), + mode: z + .enum(['overwrite', 'append']) + .optional() + .describe( + 'Write mode. Defaults to overwrite. append adds content to the end exactly as provided and does not add a newline.', + ), +}); + +export const WriteOutputSchema = z.object({ + /** Number of UTF-8 bytes written to disk by this call. */ + bytesWritten: z.number().int().nonnegative(), +}); + +export type WriteInput = z.Infer; +export type WriteOutput = z.Infer; + +export class WriteTool implements BuiltinTool { + readonly name = 'Write' as const; + readonly description = WRITE_DESCRIPTION; + readonly parameters: Record = toInputJsonSchema(WriteInputSchema); + + constructor( + private readonly kaos: Kaos, + private readonly workspace: WorkspaceConfig, + ) {} + + resolveExecution(args: WriteInput): ToolExecution { + const path = resolvePathAccessPath(args.path, { + kaos: this.kaos, + workspace: this.workspace, + operation: 'write', + }); + return { + accesses: ToolAccesses.writeFile(path), + description: `Writing ${args.path}`, + execute: () => this.execution(args, path), + }; + } + + private async execution(args: WriteInput, safePath: string): Promise { + const parentError = await this.checkParentDirectory(safePath); + if (parentError !== undefined) { + return { isError: true, output: parentError }; + } + + try { + const mode = args.mode ?? 'overwrite'; + if (mode === 'append') { + await this.kaos.writeText(safePath, args.content, { mode: 'a' }); + } else { + await this.kaos.writeText(safePath, args.content); + } + // Report the number of UTF-8 bytes this call wrote to disk. The string + // length would only equal the byte count for pure ASCII content, so it + // is not used here. + const bytesWritten = Buffer.byteLength(args.content, 'utf8'); + return { + output: `${mode === 'append' ? 'Appended' : 'Wrote'} ${String(bytesWritten)} bytes to ${args.path}`, + }; + } catch (error) { + const code = (error as { code?: unknown } | null)?.code; + if (code === 'ENOENT') { + return { + isError: true, + output: `Failed to write ${args.path}: parent directory does not exist.`, + }; + } + return { + isError: true, + output: error instanceof Error ? error.message : String(error), + }; + } + } + + /** + * Best-effort check that the parent directory exists and is a directory. + * + * The path schema documents this precondition; probing it up front turns a + * bare `ENOENT` from the underlying write into an actionable message. + * Returns an error string when the precondition is definitively violated, + * or `undefined` otherwise. Any other `stat` failure (permissions, an + * environment without `stat`) is treated as inconclusive: the check is + * skipped and the write proceeds, surfacing the real I/O error if any. + */ + private async checkParentDirectory(safePath: string): Promise { + const parent = pathMod(this.kaos.pathClass()).dirname(safePath); + let stat; + try { + stat = await this.kaos.stat(parent); + } catch (error) { + if ((error as NodeJS.ErrnoException).code === 'ENOENT') { + return `Parent directory does not exist: ${parent}. Create it before writing this file.`; + } + return undefined; + } + if ((stat.stMode & S_IFMT) !== S_IFDIR) { + return `Parent path is not a directory: ${parent}.`; + } + return undefined; + } +} diff --git a/packages/agent-core/src/tools/builtin/index.ts b/packages/agent-core/src/tools/builtin/index.ts new file mode 100644 index 000000000..046d47557 --- /dev/null +++ b/packages/agent-core/src/tools/builtin/index.ts @@ -0,0 +1,19 @@ +export * from '../background/manager'; +export * from '../background/task-list'; +export * from '../background/task-output'; +export * from '../background/task-stop'; +export * from './collaboration/agent'; +export * from './collaboration/ask-user'; +export * from './collaboration/skill-tool'; +export * from './file/edit'; +export * from './file/glob'; +export * from './file/grep'; +export * from './file/read'; +export * from './file/read-media'; +export * from './file/write'; +export * from './planning/enter-plan-mode'; +export * from './planning/exit-plan-mode'; +export * from './shell/bash'; +export * from './state/todo-list'; +export * from './web/fetch-url'; +export * from './web/web-search'; diff --git a/packages/agent-core/src/tools/builtin/planning/enter-plan-mode.md b/packages/agent-core/src/tools/builtin/planning/enter-plan-mode.md new file mode 100644 index 000000000..d792e71b7 --- /dev/null +++ b/packages/agent-core/src/tools/builtin/planning/enter-plan-mode.md @@ -0,0 +1,32 @@ +Use this tool proactively when you're about to start a non-trivial implementation task. +Getting user sign-off on your approach via ExitPlanMode before writing code prevents wasted effort. + +Use it when ANY of these conditions apply: + +1. New Feature Implementation - e.g. "Add a caching layer to the API" +2. Multiple Valid Approaches - e.g. "Optimize database queries" (indexing vs rewrite vs caching) +3. Code Modifications - e.g. "Refactor auth module to support OAuth" +4. Architectural Decisions - e.g. "Add WebSocket support" +5. Multi-File Changes - involves more than 2-3 files +6. Unclear Requirements - need exploration to understand scope +7. User Preferences Matter - if user input would materially change the implementation approach, use EnterPlanMode to structure the decision + +Permission mode notes: +- EnterPlanMode enters plan mode automatically without an approval prompt in all permission modes. +- In yolo and manual modes, ExitPlanMode still presents the plan to the user for approval. +- In auto permission mode, do not use AskUserQuestion; make the best decision from available context. +- In auto permission mode, ExitPlanMode exits plan mode without asking the user. +- Use EnterPlanMode only when planning itself adds value. + +When NOT to use: +- Single-line or few-line fixes (typos, obvious bugs, small tweaks) +- User gave very specific, detailed instructions +- Pure research/exploration tasks + +## What Happens in Plan Mode +In plan mode, you will: +1. Identify 2-3 key questions about the codebase that are critical to your plan. If you are not confident about the codebase structure or relevant code paths, use `Agent(subagent_type="explore")` to investigate these questions first - this is strongly recommended for non-trivial tasks. +2. Explore the codebase using Glob, Grep, Read, and other read-only tools for any remaining quick lookups. Use Bash only when needed; Bash follows the normal permission mode and rules. +3. Design an implementation approach based on your findings +4. Write your plan to the current plan file with Write or Edit +5. Present your plan to the user via ExitPlanMode for approval diff --git a/packages/agent-core/src/tools/builtin/planning/enter-plan-mode.ts b/packages/agent-core/src/tools/builtin/planning/enter-plan-mode.ts new file mode 100644 index 000000000..b15b78c6c --- /dev/null +++ b/packages/agent-core/src/tools/builtin/planning/enter-plan-mode.ts @@ -0,0 +1,82 @@ +/** + * EnterPlanModeTool — plan-mode entry tool. + * + * The LLM calls this tool to enter plan mode directly. Entering plan mode + * does not require approval in any permission mode. + */ + + +import type { Agent } from '#/agent'; +import { z } from 'zod'; + +import type { BuiltinTool } from '../../../agent/tool'; +import type { ToolExecution } from '../../../loop/types'; +import { toInputJsonSchema } from '../../support/input-schema'; +import DESCRIPTION from './enter-plan-mode.md'; + +// ── Input schema ───────────────────────────────────────────────────── + +export const EnterPlanModeInputSchema = z.object({}).strict(); +export type EnterPlanModeInput = z.infer; + +export class EnterPlanModeTool implements BuiltinTool { + readonly name = 'EnterPlanMode' as const; + readonly description: string = DESCRIPTION; + readonly parameters: Record = toInputJsonSchema(EnterPlanModeInputSchema); + + constructor(private readonly agent: Agent) {} + + resolveExecution(_args: EnterPlanModeInput): ToolExecution { + return { + description: 'Requesting to enter plan mode', + execute: async () => { + // Guard: already in plan mode + if (this.agent.planMode.isActive) { + return { + isError: true, + output: 'Plan mode is already active. Use ExitPlanMode when the plan is ready.', + }; + } + + try { + await this.agent.planMode.enter(); + } catch (error) { + const message = error instanceof Error ? error.message : 'Failed to enter plan mode.'; + return { isError: true, output: `Failed to enter plan mode: ${message}` }; + } + + this.agent.telemetry.track('plan_enter_resolved', { outcome: 'auto_approved' }); + return { output: enteredPlanModeMessage(this.agent.planMode.planFilePath) }; + }, + }; + } +} + +function enteredPlanModeMessage(planPath: string | null): string { + if (planPath === null) { + return [ + 'Plan mode is now active. Your workflow:', + '', + '1. Use read-only tools (Read, Grep, Glob) to investigate the codebase. Use Bash only when needed.', + '2. Design a concrete, step-by-step plan.', + '3. Wait for the host to provide a plan file path before calling ExitPlanMode.', + '', + 'Do NOT use Write or Edit while plan mode is active in this host; no plan file path is available.', + 'Use Bash only when needed; Bash follows the normal permission mode and rules.', + ].join('\n'); + } + + return [ + 'Plan mode is now active. Your workflow:', + '', + `Plan file: ${planPath}`, + '', + '1. Use read-only tools (Read, Grep, Glob) to investigate the codebase. Use Bash only when needed.', + '2. Design a concrete, step-by-step plan.', + '3. Write the plan to the plan file with Write or Edit.', + '4. When the plan is ready, call ExitPlanMode for user approval.', + '', + 'Do NOT edit files other than the plan file while plan mode is active.', + 'Use Bash only when needed; Bash follows the normal permission mode and rules.', + ].join('\n'); +} diff --git a/packages/agent-core/src/tools/builtin/planning/exit-plan-mode.md b/packages/agent-core/src/tools/builtin/planning/exit-plan-mode.md new file mode 100644 index 000000000..b83f47f38 --- /dev/null +++ b/packages/agent-core/src/tools/builtin/planning/exit-plan-mode.md @@ -0,0 +1,29 @@ +Use this tool when you are in plan mode and have finished writing your plan to the plan file and are ready for user approval. + +## How This Tool Works +- You should have already written your plan to the plan file specified in the plan mode reminder. +- This tool does NOT take the plan content as a parameter - it reads the plan from the file you wrote. +- The user will see the contents of your plan file when they review it. In auto permission mode, the tool reads the file and exits plan mode without asking the user. + +## When to Use +Only use this tool for tasks that require planning implementation steps. For research tasks (searching files, reading code, understanding the codebase), do NOT use this tool. + +## Multiple Approaches +If your plan contains multiple alternative approaches: +- Pass them via the `options` parameter so the user can choose which approach to execute. +- Each option should have a concise label and a brief description of trade-offs. +- If you recommend one option, append "(Recommended)" to its label. +- In yolo and manual modes, the user will see all options alongside Reject and Revise choices. +- Provide up to 3 options; the host adds the standard rejection and revision controls. When the plan offers a real choice, 2-3 distinct approaches work best. +- Passing a single option is allowed and is equivalent to a plain plan approval (no approach choice is surfaced to the user). +- Do NOT use "Reject", "Reject and Exit", "Revise", or "Approve" as option labels - these are reserved by the system. + +## Before Using +- In auto permission mode, do NOT use AskUserQuestion; make the best decision from available context. +- In auto permission mode, this tool exits plan mode without asking the user. +- In yolo and manual modes, this tool still presents the plan to the user for approval. +- If auto permission mode is not active and you have unresolved questions, use AskUserQuestion first. +- If auto permission mode is not active and you have multiple approaches and haven't narrowed down yet, consider using AskUserQuestion first to let the user choose, then write a plan for the chosen approach only. +- Once your plan is finalized, use THIS tool to request approval. +- Do NOT use AskUserQuestion to ask "Is this plan OK?" or "Should I proceed?" - that is exactly what ExitPlanMode does. +- If rejected, revise based on feedback and call ExitPlanMode again. diff --git a/packages/agent-core/src/tools/builtin/planning/exit-plan-mode.ts b/packages/agent-core/src/tools/builtin/planning/exit-plan-mode.ts new file mode 100644 index 000000000..32a0983bb --- /dev/null +++ b/packages/agent-core/src/tools/builtin/planning/exit-plan-mode.ts @@ -0,0 +1,242 @@ +/** + * ExitPlanModeTool — plan-mode exit tool. + * + * The LLM calls this tool to surface a finalised plan to the user and + * exit plan mode. The plan must already be written to the current plan + * file; this tool reads that file and flips plan mode off. PermissionManager + * handles plan approval before this tool runs and passes any selected option + * through execution metadata. + */ + +import type { Agent } from '#/agent'; +import { z } from 'zod'; + +import type { BuiltinTool } from '../../../agent/tool'; +import type { ExecutableToolContext, ExecutableToolResult, ToolExecution } from '../../../loop/types'; +import { toInputJsonSchema } from '../../support/input-schema'; +import DESCRIPTION from './exit-plan-mode.md'; + +// ── Input schema ───────────────────────────────────────────────────── + +/** + * User-selectable option surfaced at plan approval time. The LLM supplies + * up to 3 of these when the plan contains multiple approaches; the host's + * ApprovalRuntime presents them to the user and returns the chosen `label` + * (or `{kind:'revise', feedback}` when the user asks for revisions). + */ +export interface ExitPlanModeOption { + label: string; + description: string; +} + +export interface ExitPlanModeInput { + options?: readonly ExitPlanModeOption[] | undefined; +} + +const RESERVED_OPTION_LABELS = new Set( + ['Approve', 'Reject', 'Reject and Exit', 'Revise'].map(normalizeOptionLabel), +); + +const ExitPlanModeOptionSchema = z + .object({ + label: z + .string() + .min(1) + .max(80) + .describe( + 'Short name for this option (1-8 words). Append "(Recommended)" if you recommend this option.', + ), + description: z + .string() + .default('') + .describe('Brief summary of this approach and its trade-offs.'), + }) + .strict(); + +export const ExitPlanModeInputSchema: z.ZodType = z + .object({ + options: z + .array(ExitPlanModeOptionSchema) + .min(1) + .max(3) + .refine(hasUniqueOptionLabels, 'Option labels must be unique.') + .refine(hasNoReservedOptionLabels, 'Option labels must not use reserved approval labels.') + .optional() + .describe( + 'When the plan contains multiple alternative approaches, list them here so the user can choose which one to execute. Provide up to 3 options; 2-3 distinct approaches work best when the plan offers a real choice. Passing a single option is allowed and is equivalent to a plain plan approval. Each option represents a distinct approach from the plan. Do not use "Reject", "Revise", "Approve", or "Reject and Exit" as labels.', + ), + }) + .strict(); + +export interface ExitPlanModePlanSource { + plan: string; + path?: string | undefined; +} + +type ResolvePlanResult = + | { readonly ok: true; readonly plan: string; readonly path?: string | undefined } + | { readonly ok: false; readonly error: ExecutableToolResult }; + +// ── Implementation ─────────────────────────────────────────────────── + +export class ExitPlanModeTool implements BuiltinTool { + readonly name = 'ExitPlanMode' as const; + readonly description: string = DESCRIPTION; + readonly parameters: Record = toInputJsonSchema(ExitPlanModeInputSchema); + + constructor(private readonly agent: Agent) {} + + resolveExecution(args: ExitPlanModeInput): ToolExecution { + return { + description: 'Presenting plan and exiting plan mode', + execute: (ctx) => this.execution(args, ctx), + }; + } + + private async execution( + args: ExitPlanModeInput, + { + metadata, + }: ExecutableToolContext, + ): Promise { + if (!this.agent.planMode.isActive) { + return { + isError: true, + output: + 'ExitPlanMode can only be called while plan mode is active. Use EnterPlanMode (or /plan) first.', + }; + } + + const resolvedPlan = await this.resolvePlan(); + if (!resolvedPlan.ok) return resolvedPlan.error; + + if (!planTelemetryWasSubmitted(metadata)) { + this.agent.telemetry.track('plan_submitted', { + has_options: args.options !== undefined && args.options.length >= 2, + }); + } + return this.exitWithPlan( + resolvedPlan.plan, + resolvedPlan.path, + selectedOptionFromMetadata(metadata), + planTelemetryWasResolved(metadata), + ); + } + + private async exitWithPlan( + plan: string, + path: string | undefined, + option: ExitPlanModeOption | undefined = undefined, + telemetryResolved = false, + ): Promise { + const failed = this.exitPlanMode(); + if (failed !== undefined) return failed; + + if (!telemetryResolved) { + this.agent.telemetry.track('plan_resolved', { outcome: 'auto_approved' }); + } + const optionPrefix = + option === undefined + ? '' + : `Selected approach: ${option.label}\nExecute ONLY the selected approach. Do not execute any unselected alternatives.\n\n`; + return { + isError: false, + output: `Exited plan mode. ${optionPrefix}${formatPlanForOutput(plan, path)}`, + }; + } + + private exitPlanMode(): ExecutableToolResult | undefined { + try { + this.agent.planMode.exit(); + } catch (error) { + const message = error instanceof Error ? error.message : 'Failed to exit plan mode.'; + return { + isError: true, + output: `Failed to exit plan mode: ${message}`, + }; + } + } + + private async resolvePlan(): Promise { + let source: ExitPlanModePlanSource | null; + try { + const data = await this.agent.planMode.data(); + source = data === null ? null : { plan: data.content, path: data.path }; + } catch (error) { + const message = error instanceof Error ? error.message : 'Failed to read plan file.'; + return { + ok: false, + error: { isError: true, output: `Failed to read plan file: ${message}` }, + }; + } + + if (source !== null && source.plan.trim().length > 0) { + return { + ok: true, + plan: source.plan, + path: source.path, + }; + } + + const path = source?.path ?? this.agent.planMode.planFilePath; + return { + ok: false, + error: { + isError: true, + output: + path === null + ? 'No plan file found. Write the plan to the current plan file first, then call ExitPlanMode.' + : `No plan file found. Write your plan to ${path} first, then call ExitPlanMode.`, + }, + }; + } +} + +function hasUniqueOptionLabels(options: readonly ExitPlanModeOption[]): boolean { + const labels = new Set(); + for (const option of options) { + const label = normalizeOptionLabel(option.label); + if (labels.has(label)) return false; + labels.add(label); + } + return true; +} + +function hasNoReservedOptionLabels(options: readonly ExitPlanModeOption[]): boolean { + return options.every((option) => !RESERVED_OPTION_LABELS.has(normalizeOptionLabel(option.label))); +} + +function normalizeOptionLabel(label: string): string { + return label.trim().toLowerCase(); +} + +function selectedOptionFromMetadata(metadata: unknown): ExitPlanModeOption | undefined { + if (metadata === null || typeof metadata !== 'object') return undefined; + const selectedOption = (metadata as { readonly selectedOption?: unknown }).selectedOption; + if (selectedOption === null || typeof selectedOption !== 'object') return undefined; + const label = (selectedOption as { readonly label?: unknown }).label; + const description = (selectedOption as { readonly description?: unknown }).description; + if (typeof label !== 'string' || typeof description !== 'string') return undefined; + return { label, description }; +} + +function planTelemetryWasSubmitted(metadata: unknown): boolean { + return ( + metadata !== null && + typeof metadata === 'object' && + (metadata as { readonly planTelemetrySubmitted?: unknown }).planTelemetrySubmitted === true + ); +} + +function planTelemetryWasResolved(metadata: unknown): boolean { + return ( + metadata !== null && + typeof metadata === 'object' && + (metadata as { readonly planTelemetryResolved?: unknown }).planTelemetryResolved === true + ); +} + +function formatPlanForOutput(plan: string, path: string | undefined): string { + const savedTo = path !== undefined ? `Plan saved to: ${path}\n\n` : ''; + return `Plan mode deactivated. All tools are now available.\n${savedTo}## Approved Plan:\n${plan}`; +} diff --git a/packages/agent-core/src/tools/builtin/shell/bash.md b/packages/agent-core/src/tools/builtin/shell/bash.md new file mode 100644 index 000000000..a2afff7b4 --- /dev/null +++ b/packages/agent-core/src/tools/builtin/shell/bash.md @@ -0,0 +1,43 @@ +Execute a `{{ SHELL_NAME }}` command. Use this for shell semantics — pipes, env, processes, git, package managers, build/test runners, anything genuinely interactive or multi-step. + +**Translate these to a dedicated tool instead:** +- `cat` / `head` / `tail` (known path) → `Read` +- `sed` / `awk` (in-place edit) → `Edit` +- `echo > file` / `cat <` is fine for listing a directory) +- `grep` / `rg` (search file contents) → `Grep` +- `echo` / `printf` (talk to the user) → just output text directly + +The dedicated tools render in the per-tool permission UI and keep raw stdout out of the conversation; that is why they are worth reaching for whenever one fits. + +**Output:** +The stdout and stderr will be combined and returned as a string. The output may be truncated if it is too long. If the command failed, the output will end with a `Command failed with exit code: N` line stating the non-zero exit code. + +If `run_in_background=true`, the command will be started as a background task and this tool will return a task ID instead of waiting for command completion. When doing that, you must provide a short `description`. Background commands default to a {{ DEFAULT_BACKGROUND_TIMEOUT_S }}s timeout and `timeout` is capped at {{ MAX_BACKGROUND_TIMEOUT_S }}s; set `disable_timeout=true` only when the task should run without a timeout. You will be automatically notified when the task completes. Use `TaskOutput` for a non-blocking status/output snapshot, and only set `block=true` when you explicitly want to wait for completion. Use `TaskStop` only if the task must be cancelled. If a human user wants to inspect background tasks themselves, point them to the `/tasks` command, which opens an interactive panel; it has no subcommands. + +**Guidelines for safety and security:** +- Each shell tool call will be executed in a fresh shell environment. The shell variables, current working directory changes, and the shell history is not preserved between calls. +- The tool call will return after the command is finished. You shall not use this tool to execute an interactive command or a command that may run forever. For possibly long-running foreground commands, set the `timeout` argument in seconds. Foreground commands default to {{ DEFAULT_TIMEOUT_S }}s and allow up to {{ MAX_TIMEOUT_S }}s. +- Avoid using `..` to access files or directories outside of the working directory. +- Avoid modifying files outside of the working directory unless explicitly instructed to do so. +- Never run commands that require superuser privileges unless explicitly instructed to do so. + +**Guidelines for efficiency:** +- For multiple related commands, use `&&` to chain them in a single call, e.g. `cd /path && ls -la` +- Use `;` to run commands sequentially regardless of success/failure +- Use `||` for conditional execution (run second command only if first fails) +- Use pipe operations (`|`) and redirections (`>`, `>>`) to chain input and output between commands +- Always quote file paths containing spaces with double quotes (e.g., cd "/path with spaces/") +- Compose multi-step logic in a single call with `if` / `case` / `for` / `while` control flows. +- Prefer `run_in_background=true` for long-running builds, tests, watchers, or servers when you need the conversation to continue before the command finishes. + +**Commands available:** +The following common command categories are usually available. Availability still depends on the host, so when in doubt run `which ` first to confirm a command exists before relying on it. +- Navigation and inspection: `ls`, `pwd`, `cd`, `stat`, `file`, `du`, `df`, `tree` +- File and directory management: `cp`, `mv`, `rm`, `mkdir`, `touch`, `ln`, `chmod`, `chown` +- Text and data processing: `wc`, `sort`, `uniq`, `cut`, `tr`, `diff`, `xargs` +- Archives and compression: `tar`, `gzip`, `gunzip`, `zip`, `unzip` +- Networking and transfer: `curl`, `wget`, `ping`, `ssh`, `scp` +- Version control: `git` +- Process and system: `ps`, `kill`, `top`, `env`, `date`, `uname`, `whoami` +- Language and package toolchains: `node`, `npm`, `pnpm`, `yarn`, `python`, `pip` (use whichever the project actually relies on) diff --git a/packages/agent-core/src/tools/builtin/shell/bash.ts b/packages/agent-core/src/tools/builtin/shell/bash.ts new file mode 100644 index 000000000..148b899d0 --- /dev/null +++ b/packages/agent-core/src/tools/builtin/shell/bash.ts @@ -0,0 +1,473 @@ +/** + * BashTool — execute shell commands. + * + * Invokes bash (POSIX) according to an injected `Environment`. On Windows + * the shell is Git Bash; the path is resolved by `detectEnvironment`. + * + * Dependencies injected via constructor: + * - `Kaos` — shell execution abstraction (exec / execWithEnv) + * - `cwd` — default working directory for commands + * - `Environment` — cross-platform probe (shellName / shellPath) + * - `BackgroundProcessManager?` — optional: required iff run_in_background=true + * + * Execution goes through Kaos, never directly via node:child_process. + * + * Hardening: + * - `args.timeout` (seconds) and the ambient `signal` both drive + * `Promise.race`; fire-a-kill on either edge. + * - stdin is closed immediately so interactive commands (`cat`, `read`, + * `python -c 'input()'`) receive EOF instead of hanging. + * - Two-phase kill: SIGTERM → 5s grace → SIGKILL (Kaos honours this + * contract cross-platform). + * - stdout/stderr stream into ToolResultBuilder; excess is replaced with a + * truncation marker so a runaway command cannot OOM the host. + */ + +import type { Readable } from 'node:stream'; +import { StringDecoder } from 'node:string_decoder'; + +import type { Kaos, KaosProcess } from '@moonshot-ai/kaos'; +import { z } from 'zod'; + +import type { BuiltinTool } from '../../../agent/tool'; +import type { ExecutableToolResult, ToolExecution } from '../../../loop/types'; +import type { Environment } from '../../../utils/environment'; +import { renderPrompt } from '../../../utils/render-prompt'; +import type { BackgroundProcessManager } from '../../background/manager'; +import { toInputJsonSchema } from '../../support/input-schema'; +import { ToolResultBuilder } from '../../support/result-builder'; +import bashDescriptionTemplate from './bash.md'; + +const MS_PER_SECOND = 1000; +const DEFAULT_TIMEOUT_S = 60; +const MAX_TIMEOUT_S = 5 * 60; +const DEFAULT_BACKGROUND_TIMEOUT_S = 10 * 60; +const MAX_BACKGROUND_TIMEOUT_S = 24 * 60 * 60; +const SIGTERM_GRACE_MS = 5_000; + +export const BashInputSchema = z + .object({ + command: z.string().min(1, 'Command cannot be empty.').describe('The command to execute.'), + cwd: z + .string() + .optional() + .describe( + "The working directory in which to run the command. When omitted, the command runs in the session's working directory.", + ), + timeout: z + .number() + .int() + .positive() + .default(DEFAULT_TIMEOUT_S) + .describe( + `Optional timeout in seconds for the command to execute. Foreground default ${String(DEFAULT_TIMEOUT_S)}s, max ${String(MAX_TIMEOUT_S)}s. Background default ${String(DEFAULT_BACKGROUND_TIMEOUT_S)}s, max ${String(MAX_BACKGROUND_TIMEOUT_S)}s. Ignored for background commands when disable_timeout=true.`, + ) + .optional(), + description: z + .string() + .optional() + .describe( + 'A short description for the background task. Required when run_in_background is true.', + ), + run_in_background: z + .boolean() + .optional() + .describe('Whether to run the command as a background task.'), + disable_timeout: z + .boolean() + .optional() + .describe( + 'If true, do not apply a timeout to the command. Only applies when run_in_background is true.', + ), + }) + .superRefine((val, ctx) => { + if (val.timeout === undefined) return; + const isBackground = val.run_in_background === true; + if (!isValidTimeoutValue(val.timeout, isBackground)) { + const cap = isBackground ? MAX_BACKGROUND_TIMEOUT_S : MAX_TIMEOUT_S; + ctx.addIssue({ + code: z.ZodIssueCode.custom, + path: ['timeout'], + message: `timeout must be ≤ ${String(cap)}s (${isBackground ? 'background' : 'foreground'})`, + }); + } + }); + +export const BashOutputSchema = z.object({ + exitCode: z.number().int(), + stdout: z.string(), + stderr: z.string(), +}); + +export type BashInput = z.Infer; +export type BashOutput = z.Infer; + +const SHELL_TIMEOUT_VARS = { + DEFAULT_TIMEOUT_S, + DEFAULT_BACKGROUND_TIMEOUT_S, + MAX_TIMEOUT_S, + MAX_BACKGROUND_TIMEOUT_S, +}; + +function timeoutCapS(isBackground: boolean): number { + return isBackground ? MAX_BACKGROUND_TIMEOUT_S : MAX_TIMEOUT_S; +} + +function isValidTimeoutValue(timeout: number, isBackground: boolean): boolean { + return timeout <= timeoutCapS(isBackground); +} + +function normalizeTimeoutMs(timeout: number | undefined, isBackground: boolean): number { + const defaultSeconds = isBackground ? DEFAULT_BACKGROUND_TIMEOUT_S : DEFAULT_TIMEOUT_S; + const value = timeout ?? defaultSeconds; + return Math.min(value, timeoutCapS(isBackground)) * MS_PER_SECOND; +} + +function renderBashDescription(shellName: string): string { + return renderPrompt(bashDescriptionTemplate, { ...SHELL_TIMEOUT_VARS, SHELL_NAME: shellName }); +} + +function withoutBackgroundDescription(description: string): string { + return description + .replace( + /\n\nIf `run_in_background=true`,[\s\S]*?point them to the `\/tasks` command, which opens an interactive panel; it has no subcommands\./, + '\n\nBackground execution is disabled for this agent. Do not set `run_in_background=true`.', + ) + .replace( + ` For possibly long-running foreground commands, set the \`timeout\` argument in seconds. Foreground commands default to ${String(DEFAULT_TIMEOUT_S)}s and allow up to ${String(MAX_TIMEOUT_S)}s.`, + ` For possibly long-running commands, set the \`timeout\` argument in seconds. The default is ${String(DEFAULT_TIMEOUT_S)}s; foreground commands allow up to ${String(MAX_TIMEOUT_S)}s.`, + ) + .replace( + /\n- Prefer `run_in_background=true`[\s\S]*?conversation to continue before the command finishes\./, + '\n- Do not set `run_in_background=true`; background task management tools are not available.', + ); +} + +export class BashTool implements BuiltinTool { + readonly name = 'Bash' as const; + readonly description: string; + readonly parameters: Record = toInputJsonSchema(BashInputSchema); + + private readonly isWindowsBash: boolean; + + private readonly allowBackground: boolean; + + constructor( + private readonly kaos: Kaos, + private readonly cwd: string, + private readonly environment: Environment, + private readonly backgroundManager?: BackgroundProcessManager, + options?: { + allowBackground?: boolean | undefined; + }, + ) { + this.isWindowsBash = this.environment.osKind === 'Windows'; + this.allowBackground = options?.allowBackground ?? this.backgroundManager !== undefined; + const rendered = renderBashDescription(this.environment.shellName); + this.description = this.allowBackground ? rendered : withoutBackgroundDescription(rendered); + } + + resolveExecution(args: BashInput): ToolExecution { + const preview = args.command.length > 50 ? `${args.command.slice(0, 50)}…` : args.command; + return { + description: args.run_in_background + ? `Starting background: ${preview}` + : `Running: ${preview}`, + execute: ({ signal }) => this.execution(args, signal), + }; + } + + private spawn(effectiveCwd: string, command: string): Promise { + const shellCwd = this.isWindowsBash ? windowsPathToPosixPath(effectiveCwd) : effectiveCwd; + const shellArgs = [ + this.environment.shellPath, + '-c', + `cd ${shellQuote(shellCwd)} && ${command}`, + ]; + + const noninteractiveEnv: Record = { + NO_COLOR: '1', + TERM: 'dumb', + // Default to '0' so git fails fast on private remotes if a TTY happens + // to be inherited; honour an explicit ambient value when the user has + // set one. + GIT_TERMINAL_PROMPT: process.env['GIT_TERMINAL_PROMPT'] ?? '0', + SHELL: this.environment.shellPath, + }; + + // Merge ambient env + noninteractive knobs so tools like git / node + // don't open a pager and paints don't colour the stream. + const mergedEnv: Record = { + ...(process.env as Record), + ...noninteractiveEnv, + }; + return this.kaos.execWithEnv(shellArgs, mergedEnv); + } + + private async execution(args: BashInput, signal: AbortSignal): Promise { + if (signal.aborted) { + return { isError: true, output: 'Aborted before command started' }; + } + if (args.command.length === 0) { + return { isError: true, output: 'Command cannot be empty.' }; + } + + if (args.run_in_background) { + if (!this.allowBackground) { + return { + isError: true, + output: + 'Background execution is not available for this agent because TaskOutput and TaskStop are not enabled.', + }; + } + return this.executeInBackground(args); + } + + const timeoutMs = normalizeTimeoutMs(args.timeout, false); + + let proc: KaosProcess; + const command = this.isWindowsBash ? rewriteWindowsNullRedirect(args.command) : args.command; + try { + const effectiveCwd = args.cwd ?? this.cwd; + proc = await this.spawn(effectiveCwd, command); + } catch (error) { + return { + isError: true, + output: error instanceof Error ? error.message : String(error), + }; + } + + try { + proc.stdin.end(); + } catch { + // Closing stdin on a process that has already exited is a no-op on + // some platforms and throws on others — either is safe to ignore. + } + + let timedOut = false; + let aborted = false; + let killed = false; + + const killProc = async (): Promise => { + if (killed) return; + killed = true; + try { + await proc.kill('SIGTERM'); + } catch { + /* process already gone */ + } + const exited = proc + .wait() + .then(() => true) + .catch(() => true); + const raced = await Promise.race([ + exited, + new Promise((resolve) => { + setTimeout(() => { + resolve(false); + }, SIGTERM_GRACE_MS); + }), + ]); + if (!raced && proc.exitCode === null) { + try { + await proc.kill('SIGKILL'); + } catch { + /* ignore */ + } + } + }; + + const onAbort = (): void => { + aborted = true; + void killProc(); + }; + signal.addEventListener('abort', onAbort); + + const timeoutHandle = setTimeout(() => { + timedOut = true; + void killProc(); + }, timeoutMs); + + try { + const builder = new ToolResultBuilder(); + const [, exitCode] = await Promise.all([ + Promise.all([ + readStreamIntoBuilder(proc.stdout, builder), + readStreamIntoBuilder(proc.stderr, builder), + ]), + proc.wait(), + ]); + + if (timedOut) { + const timeoutLabel = + timeoutMs % 1000 === 0 ? `${String(timeoutMs / 1000)}s` : `${String(timeoutMs)}ms`; + return builder.error(`Command killed by timeout (${timeoutLabel})`, { + brief: `Killed by timeout (${timeoutLabel})`, + }); + } + if (aborted) { + return builder.error('Interrupted by user', { brief: 'Interrupted by user' }); + } + + const isError = exitCode !== 0; + if (isError && builder.nChars === 0) { + builder.write(`Process exited with code ${String(exitCode)}`); + } + + if (!isError) { + return builder.ok('Command executed successfully.'); + } + return builder.error(`Command failed with exit code: ${String(exitCode)}.`, { + brief: `Failed with exit code: ${String(exitCode)}`, + }); + } catch (error) { + return { + isError: true, + output: error instanceof Error ? error.message : String(error), + }; + } finally { + clearTimeout(timeoutHandle); + signal.removeEventListener('abort', onAbort); + } + } + + private async executeInBackground(args: BashInput): Promise { + if (!this.backgroundManager) { + return { + isError: true, + output: 'Background execution is not available (no BackgroundProcessManager configured).', + }; + } + const backgroundManager = this.backgroundManager; + + if (!args.description?.trim()) { + return { + isError: true, + output: 'description is required when run_in_background is true.', + }; + } + + let reservation: ReturnType; + try { + reservation = backgroundManager.reserveSlot(); + } catch (error) { + return { + isError: true, + output: error instanceof Error ? error.message : String(error), + }; + } + + const timeoutMs = args.disable_timeout ? undefined : normalizeTimeoutMs(args.timeout, true); + + let proc: KaosProcess; + const command = this.isWindowsBash ? rewriteWindowsNullRedirect(args.command) : args.command; + try { + const effectiveCwd = args.cwd ?? this.cwd; + proc = await this.spawn(effectiveCwd, command); + } catch (error) { + reservation.release(); + return { + isError: true, + output: error instanceof Error ? error.message : String(error), + }; + } + + try { + proc.stdin.end(); + } catch { + /* process already gone */ + } + + let taskId: string; + try { + taskId = backgroundManager.register(proc, command, args.description.trim(), { + reservation, + shellInfo: { + shellName: this.environment.shellName, + shellPath: this.environment.shellPath, + cwd: args.cwd ?? this.cwd, + }, + }); + } catch (error) { + reservation.release(); + try { + await proc.kill('SIGTERM'); + } catch { + /* process already gone */ + } + return { + isError: true, + output: error instanceof Error ? error.message : String(error), + }; + } + + if (timeoutMs !== undefined) { + setTimeout(() => { + void (async (): Promise => { + if (proc.exitCode !== null) { + await backgroundManager.settlePendingExits(); + return; + } + const info = backgroundManager.getTask(taskId); + if (info && info.status === 'running') { + void backgroundManager.stop(taskId); + } + })(); + }, timeoutMs); + } + + // register() synchronously inserts taskId into the manager's Map, so + // this lookup in the same tick cannot return undefined. + const status = backgroundManager.getTask(taskId)!.status; + const builder = new ToolResultBuilder(); + builder.write( + `task_id: ${taskId}\n` + + `pid: ${String(proc.pid)}\n` + + `description: ${args.description.trim()}\n` + + `status: ${status}\n` + + `automatic_notification: true\n` + + 'next_step: You will be automatically notified when it completes.\n' + + 'next_step: Use TaskOutput with this task_id for a non-blocking status/output snapshot.\n' + + 'next_step: Use TaskStop only if the task must be cancelled.\n' + + 'human_shell_hint: Tell the human to run /tasks to open the interactive background-task panel.', + ); + return builder.ok('Background task started', { brief: `Started ${taskId}` }); + } +} + +async function readStreamIntoBuilder( + stream: Readable, + builder: ToolResultBuilder, +): Promise { + const decoder = new StringDecoder('utf8'); + for await (const chunk of stream) { + const buf: Buffer = typeof chunk === 'string' ? Buffer.from(chunk, 'utf8') : (chunk as Buffer); + builder.write(decoder.write(buf)); + } + builder.write(decoder.end()); +} + +function shellQuote(s: string): string { + return `'${s.replaceAll("'", "'\\''")}'`; +} + +function windowsPathToPosixPath(path: string): string { + if (path.startsWith('\\\\')) { + return path.replaceAll('\\', '/'); + } + + const driveMatch = /^([A-Za-z]):(?:[\\/]|$)/.exec(path); + if (driveMatch !== null) { + const drive = driveMatch[1]!.toLowerCase(); + const rest = path.slice(2).replaceAll('\\', '/'); + return `/${drive}${rest.startsWith('/') ? rest : `/${rest}`}`; + } + + return path.replaceAll('\\', '/'); +} + +const WINDOWS_NUL_REDIRECT = /(\d?&?>+\s*)[Nn][Uu][Ll](?=\s|$|[|&;)\n])/g; + +function rewriteWindowsNullRedirect(command: string): string { + return command.replace(WINDOWS_NUL_REDIRECT, '$1/dev/null'); +} diff --git a/packages/agent-core/src/tools/builtin/state/todo-list.md b/packages/agent-core/src/tools/builtin/state/todo-list.md new file mode 100644 index 000000000..2e83f6026 --- /dev/null +++ b/packages/agent-core/src/tools/builtin/state/todo-list.md @@ -0,0 +1,22 @@ +Use this tool to maintain a structured TODO list as you work through a multi-step task. This is especially useful in plan mode and for long-running investigations. + +**When to use:** +- Multi-step tasks that span several tool calls +- Tracking investigation progress across a large codebase search +- Planning a sequence of edits before making them + +**When NOT to use:** +- Single-shot answers that complete in one or two tool calls +- Trivial requests where tracking adds no clarity + +**Avoid churn:** +- Do not re-call this tool when nothing meaningful has changed since the last call — update the list only after real progress. +- When unsure of the current state, call query mode first (omit `todos`) to check the list before deciding what to update. +- If no available tool can move any task forward, tell the user where you are stuck instead of repeatedly re-ordering the same todos. + +**How to use:** +- Call with `todos: [...]` to replace the full list. Statuses: pending / in_progress / done. +- Call with no arguments to retrieve the current list without changing it. +- Call with `todos: []` to clear the list. +- Keep titles short and actionable (e.g. "Read session-control.ts", "Add planMode flag to TurnManager"). +- Update statuses as you make progress — mark one item in_progress at a time. \ No newline at end of file diff --git a/packages/agent-core/src/tools/builtin/state/todo-list.ts b/packages/agent-core/src/tools/builtin/state/todo-list.ts new file mode 100644 index 000000000..22431ff22 --- /dev/null +++ b/packages/agent-core/src/tools/builtin/state/todo-list.ts @@ -0,0 +1,133 @@ +/** + * TodoListTool — structured TODO list management tool. + * + * The LLM uses this tool to maintain a visible plan of sub-tasks during + * plan-mode workflows and multi-step operations. A single tool serves + * both reads and writes: + * + * - `resolveExecution({ todos: [...] })` — replace the full list + * - `resolveExecution({ todos: [] })` — clear the list + * - `resolveExecution({})` — query current list (no mutation) + * + * Storage: todos live in the agent-level tool store. Writes go through + * `tools.update_store`, so the store update is visible on wire replay. + */ + +import { z } from 'zod'; + +import type { BuiltinTool } from '../../../agent/tool'; +import type { ToolExecution } from '../../../loop/types'; +import { toInputJsonSchema } from '../../support/input-schema'; +import type { ToolStore } from '../../store'; +import DESCRIPTION from './todo-list.md'; + +// ── TODO state shape ───────────────────────────────────────────────── + +export type TodoStatus = 'pending' | 'in_progress' | 'done'; + +export interface TodoItem { + readonly title: string; + readonly status: TodoStatus; +} + +declare module '../../store' { + interface ToolStoreData { + todo: readonly TodoItem[]; + } +} + +// ── Schema ─────────────────────────────────────────────────────────── + +const TodoItemSchema = z.object({ + title: z.string().min(1).describe('Short, actionable title for the todo.'), + status: z.enum(['pending', 'in_progress', 'done']).describe('Current status of the todo.'), +}); + +export interface TodoListInput { + todos?: Array<{ title: string; status: TodoStatus }>; +} + +export const TodoListInputSchema: z.ZodType = z.object({ + todos: z + .array(TodoItemSchema) + .optional() + .describe( + 'The updated todo list. Omit to read the current todo list without making changes. Pass an empty array to clear the list.', + ), +}); + +const TODO_STORE_KEY = 'todo'; + +// ── Implementation ─────────────────────────────────────────────────── + +function renderTodoList(todos: readonly TodoItem[]): string { + if (todos.length === 0) { + return 'Todo list is empty.'; + } + const lines = todos.map((t) => { + const marker = statusMarker(t.status); + return ` ${marker} ${t.title}`; + }); + return ['Current todo list:', ...lines].join('\n'); +} + +function statusMarker(status: TodoStatus): string { + switch (status) { + case 'pending': + return '[pending]'; + case 'in_progress': + return '[in_progress]'; + case 'done': + return '[done]'; + default: { + const _exhaustive: never = status; + return _exhaustive; + } + } +} + +export class TodoListTool implements BuiltinTool { + readonly name = 'TodoList' as const; + readonly description: string = DESCRIPTION; + readonly parameters: Record = toInputJsonSchema(TodoListInputSchema); + + constructor(private readonly store: ToolStore) {} + + resolveExecution(args: TodoListInput): ToolExecution { + const description = + args.todos === undefined + ? 'Reading todo list' + : args.todos.length === 0 + ? 'Clearing todo list' + : 'Updating todo list'; + return { + description, + execute: async () => { + // Query mode — return the current list without mutation. + if (args.todos === undefined) { + const current = this.getTodos(); + return { isError: false, output: renderTodoList(current) }; + } + + // Write mode — replace the full list and return the new state. + this.setTodos(args.todos); + const stored = this.getTodos(); + const output = + stored.length === 0 ? 'Todo list cleared.' : `Todo list updated.\n${renderTodoList(stored)}`; + return { isError: false, output }; + }, + }; + } + + private getTodos(): readonly TodoItem[] { + const todos = this.store.get(TODO_STORE_KEY); + return todos ?? []; + } + + private setTodos(todos: readonly TodoItem[]): void { + this.store.set( + TODO_STORE_KEY, + todos.map((todo) => ({ title: todo.title, status: todo.status })), + ); + } +} diff --git a/packages/agent-core/src/tools/builtin/web/fetch-url.md b/packages/agent-core/src/tools/builtin/web/fetch-url.md new file mode 100644 index 000000000..f2356e690 --- /dev/null +++ b/packages/agent-core/src/tools/builtin/web/fetch-url.md @@ -0,0 +1,3 @@ +Fetch content from a URL. Returns the main text content extracted from the page. Use this when you need to read a specific web page. + +Only public `http`/`https` URLs are supported. Requests to private, loopback, or link-local addresses are refused, and responses larger than 10 MiB are rejected. diff --git a/packages/agent-core/src/tools/builtin/web/fetch-url.ts b/packages/agent-core/src/tools/builtin/web/fetch-url.ts new file mode 100644 index 000000000..6c1534651 --- /dev/null +++ b/packages/agent-core/src/tools/builtin/web/fetch-url.ts @@ -0,0 +1,122 @@ +/** + * FetchURLTool — host-injected URL fetcher. + * + * kimi-core defines the interface; the host provides the real fetch + * implementation via `UrlFetcher`. If no fetcher is supplied, the tool + * should not be registered (not exposed to the LLM). + */ + +import { z } from 'zod'; + +import type { BuiltinTool } from '../../../agent/tool'; +import { ToolAccesses } from '../../../loop/tool-access'; +import type { ExecutableToolContext, ExecutableToolResult, ToolExecution } from '../../../loop/types'; +import { toInputJsonSchema } from '../../support/input-schema'; +import { ToolResultBuilder } from '../../support/result-builder'; +import DESCRIPTION from './fetch-url.md'; + +// ── Provider interface (host-injected) ─────────────────────────────── + +/** + * How the returned content relates to the original response body. + * + * - `passthrough` — the body was already plain text / markdown and is + * returned verbatim, in full. + * - `extracted` — the body was an HTML page; only the main article text + * was extracted and returned. + */ +export type UrlFetchKind = 'passthrough' | 'extracted'; + +export interface UrlFetchResult { + /** The text handed to the LLM. */ + content: string; + /** Whether `content` is a verbatim passthrough or extracted main text. */ + kind: UrlFetchKind; +} + +export interface UrlFetcher { + fetch(url: string, options?: { toolCallId?: string }): Promise; +} + +/** + * Thrown by a `UrlFetcher` when the upstream HTTP request completed but + * returned a non-success status. The tool branches on this to surface + * `Status: N` in the error message; non-HTTP failures (DNS, timeout, + * connection reset, …) keep flowing through as plain `Error`. + */ +export class HttpFetchError extends Error { + override readonly name = 'HttpFetchError'; + readonly status: number; + constructor(status: number, message: string) { + super(message); + this.status = status; + } +} + +// ── Input schema ───────────────────────────────────────────────────── + +export const FetchURLInputSchema = z.object({ + url: z.string().describe('The URL to fetch content from.'), +}); + +export type FetchURLInput = z.Infer; + +// ── Implementation ─────────────────────────────────────────────────── + +export class FetchURLTool implements BuiltinTool { + readonly name = 'FetchURL' as const; + readonly description: string = DESCRIPTION; + readonly parameters: Record = toInputJsonSchema(FetchURLInputSchema); + constructor(private readonly fetcher: UrlFetcher) {} + + resolveExecution(args: FetchURLInput): ToolExecution { + const preview = args.url.length > 50 ? `${args.url.slice(0, 50)}…` : args.url; + return { + accesses: ToolAccesses.none(), + description: `Fetching: ${preview}`, + execute: (ctx) => this.execution(args, ctx), + }; + } + + private async execution( + args: FetchURLInput, + { + toolCallId, + }: ExecutableToolContext, + ): Promise { + try { + const { content, kind } = await this.fetcher.fetch(args.url, { toolCallId }); + + if (!content) { + return { + output: 'The response body is empty.', + isError: false, + }; + } + + const builder = new ToolResultBuilder({ maxLineLength: null }); + builder.write(content); + // Tell the LLM whether it received the whole body or only the + // extracted article text, so it can judge how complete the + // content is. + const message = + kind === 'passthrough' + ? 'The returned content is the full response body, returned verbatim.' + : 'The returned content is the main text extracted from the page.'; + return builder.ok(message); + } catch (error) { + const msg = error instanceof Error ? error.message : String(error); + if (error instanceof HttpFetchError) { + return { + isError: true, + output: `Failed to fetch URL. Status: ${String(error.status)}. ${msg}`, + }; + } + return { + isError: true, + output: `Failed to fetch URL due to network error: ${args.url}. ${msg}`, + }; + } + } + +} diff --git a/packages/agent-core/src/tools/builtin/web/web-search.md b/packages/agent-core/src/tools/builtin/web/web-search.md new file mode 100644 index 000000000..fbab5c828 --- /dev/null +++ b/packages/agent-core/src/tools/builtin/web/web-search.md @@ -0,0 +1,3 @@ +Search the web for information. Use this when you need up-to-date information from the internet. + +Each result includes its title, URL, snippet, and—when available—a publication date. When `include_content` is enabled, the full page content—when available—is appended after the snippet. diff --git a/packages/agent-core/src/tools/builtin/web/web-search.ts b/packages/agent-core/src/tools/builtin/web/web-search.ts new file mode 100644 index 000000000..153a14f9d --- /dev/null +++ b/packages/agent-core/src/tools/builtin/web/web-search.ts @@ -0,0 +1,152 @@ +/** + * WebSearchTool — host-injected web search. + * + * kimi-core defines the interface; the host provides the real search + * implementation via `WebSearchProvider`. If no provider is supplied, + * the tool should not be registered (not exposed to the LLM). + */ + +import { z } from 'zod'; + +import type { BuiltinTool } from '../../../agent/tool'; +import { ToolAccesses } from '../../../loop/tool-access'; +import type { ExecutableToolContext, ExecutableToolResult, ToolExecution } from '../../../loop/types'; +import { toInputJsonSchema } from '../../support/input-schema'; +import { ToolResultBuilder } from '../../support/result-builder'; +import DESCRIPTION from './web-search.md'; + +// ── Provider interface (host-injected) ─────────────────────────────── + +export interface WebSearchResult { + title: string; + url: string; + snippet: string; + date?: string | undefined; + content?: string | undefined; +} + +export interface WebSearchProvider { + search( + query: string, + options?: { limit?: number; includeContent?: boolean; toolCallId?: string }, + ): Promise; +} + +// ── Input schema ───────────────────────────────────────────────────── + +export const WebSearchInputSchema = z.object({ + query: z.string().describe('The query text to search for.'), + limit: z + .number() + .int() + .min(1) + .max(20) + .default(5) + .describe( + 'The number of results to return. Typically you do not need to set this value. When the results do not contain what you need, you probably want to give a more concrete query.', + ) + .optional(), + include_content: z + .boolean() + .default(false) + .describe( + 'Whether to include the content of the web pages in the results. It can consume a large amount of tokens when this is set to true. You should avoid enabling this when `limit` is set to a large value.', + ) + .optional(), +}); + +export type WebSearchInput = z.Infer; + +// ── Implementation ─────────────────────────────────────────────────── + +export class WebSearchTool implements BuiltinTool { + readonly name = 'WebSearch' as const; + readonly description: string = DESCRIPTION; + readonly parameters: Record = toInputJsonSchema(WebSearchInputSchema); + constructor(private readonly provider: WebSearchProvider) {} + + resolveExecution(args: WebSearchInput): ToolExecution { + const preview = args.query.length > 40 ? `${args.query.slice(0, 40)}…` : args.query; + return { + accesses: ToolAccesses.none(), + description: `Searching: ${preview}`, + execute: (ctx) => this.execution(args, ctx), + }; + } + + private async execution( + args: WebSearchInput, + { + toolCallId, + }: ExecutableToolContext, + ): Promise { + try { + const opts: { limit?: number; includeContent?: boolean; toolCallId?: string } = { + toolCallId, + }; + if (args.limit !== undefined) opts.limit = args.limit; + if (args.include_content !== undefined) opts.includeContent = args.include_content; + const results = await this.provider.search(args.query, opts); + const builder = new ToolResultBuilder({ maxLineLength: null }); + + if (results.length === 0) { + builder.write('No search results found.'); + return builder.ok(); + } + + let first = true; + for (const result of results) { + if (!first) builder.write('---\n\n'); + first = false; + + builder.write(`Title: ${result.title}\n`); + if (result.date) builder.write(`Date: ${result.date}\n`); + builder.write(`URL: ${result.url}\n`); + builder.write(`Snippet: ${result.snippet}\n\n`); + if (result.content) builder.write(`${result.content}\n\n`); + } + + return builder.ok(); + } catch (error) { + return { + isError: true, + output: classifySearchError(error), + }; + } + } + +} + +// ── Error classification ───────────────────────────────────────────── + +/** + * Maps a thrown search error to a categorised, human-readable message. + * + * The original error text is always preserved so the model can still see the + * underlying detail; the prefix only adds a category so failures are easier to + * reason about (e.g. retry vs. surface to the user). + */ +function classifySearchError(error: unknown): string { + const name = error instanceof Error ? error.name : ''; + const message = error instanceof Error ? error.message : String(error); + const lower = message.toLowerCase(); + + if (name === 'AbortError' || lower.includes('abort')) { + return `Search cancelled: ${message}`; + } + if (name === 'TimeoutError' || lower.includes('timed out') || lower.includes('timeout')) { + return `Search timed out: ${message}`; + } + if (lower.includes('401') || lower.includes('unauthorized') || lower.includes('auth')) { + return `Search failed (authentication): ${message}`; + } + if ( + lower.includes('http ') || + lower.includes('network') || + lower.includes('fetch') || + name === 'TypeError' + ) { + return `Search failed (network): ${message}`; + } + return `Search failed: ${message}`; +} diff --git a/packages/agent-core/src/tools/display/index.ts b/packages/agent-core/src/tools/display/index.ts new file mode 100644 index 000000000..e84d06c37 --- /dev/null +++ b/packages/agent-core/src/tools/display/index.ts @@ -0,0 +1 @@ +export * from './schemas'; diff --git a/packages/agent-core/src/tools/display/schemas.ts b/packages/agent-core/src/tools/display/schemas.ts new file mode 100644 index 000000000..abf180088 --- /dev/null +++ b/packages/agent-core/src/tools/display/schemas.ts @@ -0,0 +1,158 @@ +/** + * Zod schemas for the display unions. + * + * The wire-record layer validates incoming / outgoing + * `input_display` / `result_display` fields against these schemas. + */ + +import { z } from 'zod'; + +export const ToolInputDisplaySchema = z.discriminatedUnion('kind', [ + z.object({ + kind: z.literal('command'), + command: z.string(), + cwd: z.string().optional(), + description: z.string().optional(), + language: z.literal('bash').optional(), + }), + z.object({ + kind: z.literal('file_io'), + operation: z.enum(['read', 'write', 'edit', 'glob', 'grep']), + path: z.string(), + detail: z.string().optional(), + }), + z.object({ + kind: z.literal('diff'), + path: z.string(), + before: z.string(), + after: z.string(), + hunks: z.number().optional(), + }), + z.object({ + kind: z.literal('search'), + query: z.string(), + scope: z.string().optional(), + }), + z.object({ + kind: z.literal('url_fetch'), + url: z.string(), + method: z.string().optional(), + }), + z.object({ + kind: z.literal('agent_call'), + agent_name: z.string(), + prompt: z.string(), + background: z.boolean().optional(), + }), + z.object({ + kind: z.literal('skill_call'), + skill_name: z.string(), + args: z.string().optional(), + }), + z.object({ + kind: z.literal('todo_list'), + items: z.array(z.object({ title: z.string(), status: z.string() })), + }), + z.object({ + kind: z.literal('background_task'), + task_id: z.string(), + status: z.string(), + description: z.string(), + task_kind: z.string().optional(), + }), + z.object({ + kind: z.literal('task_stop'), + task_id: z.string(), + task_description: z.string(), + }), + z.object({ + kind: z.literal('plan_review'), + plan: z.string(), + path: z.string().optional(), + options: z + .array( + z.object({ + label: z.string(), + description: z.string(), + }), + ) + .readonly() + .optional(), + }), + z.object({ + kind: z.literal('generic'), + summary: z.string(), + detail: z.unknown().optional(), + }), +]); + +export const ToolResultDisplaySchema = z.discriminatedUnion('kind', [ + z.object({ + kind: z.literal('command_output'), + exit_code: z.number(), + stdout: z.string().optional(), + stderr: z.string().optional(), + }), + z.object({ + kind: z.literal('file_content'), + path: z.string(), + content: z.string(), + range: z.object({ start: z.number(), end: z.number() }).optional(), + truncated: z.boolean().optional(), + }), + z.object({ + kind: z.literal('diff'), + path: z.string(), + before: z.string(), + after: z.string(), + hunks: z.number().optional(), + }), + z.object({ + kind: z.literal('search_results'), + query: z.string(), + matches: z.array(z.object({ file: z.string(), line: z.number(), text: z.string() })), + }), + z.object({ + kind: z.literal('url_content'), + url: z.string(), + status: z.number(), + preview: z.string().optional(), + content_type: z.string().optional(), + }), + z.object({ + kind: z.literal('agent_summary'), + agent_name: z.string(), + result: z.string().optional(), + steps: z.number().optional(), + }), + z.object({ + kind: z.literal('background_task'), + task_id: z.string(), + status: z.string(), + description: z.string(), + }), + z.object({ + kind: z.literal('todo_list'), + items: z.array(z.object({ title: z.string(), status: z.string() })), + }), + z.object({ kind: z.literal('structured'), data: z.unknown() }), + z.object({ + kind: z.literal('text'), + text: z.string(), + truncated: z.boolean().optional(), + }), + z.object({ + kind: z.literal('error'), + message: z.string(), + code: z.string().optional(), + }), + z.object({ + kind: z.literal('generic'), + summary: z.string(), + detail: z.unknown().optional(), + }), +]); + +// Types inferred from schemas — single source of truth. +export type ToolInputDisplay = z.infer; +export type ToolResultDisplay = z.infer; diff --git a/packages/agent-core/src/tools/policies/default-permissions.ts b/packages/agent-core/src/tools/policies/default-permissions.ts new file mode 100644 index 000000000..01a562e4a --- /dev/null +++ b/packages/agent-core/src/tools/policies/default-permissions.ts @@ -0,0 +1,69 @@ +/** + * Default permission posture for built-in tools. + * + * This table captures the Python-parity distinction between tools that + * call Approval.request() and tools that run without an approval prompt. + * The rule layer still applies explicit deny rules before consulting this + * table. + */ + +export type BuiltinToolDefaultPermission = 'auto_allow' | 'ask'; +export type KnownBuiltinToolName = + | 'Read' + | 'Grep' + | 'Glob' + | 'ReadMediaFile' + | 'Think' + | 'TodoList' + | 'TaskList' + | 'TaskOutput' + | 'WebSearch' + | 'FetchURL' + | 'Agent' + | 'AskUserQuestion' + | 'EnterPlanMode' + | 'ExitPlanMode' + | 'Skill' + | 'Bash' + | 'Write' + | 'Edit' + | 'TaskStop'; + +type BuiltinToolDefaultPermissionTable = Readonly< + Record +>; + +const BUILTIN_TOOL_DEFAULT_PERMISSION_TABLE: BuiltinToolDefaultPermissionTable = { + Read: 'auto_allow', + Grep: 'auto_allow', + Glob: 'auto_allow', + ReadMediaFile: 'auto_allow', + Think: 'auto_allow', + TodoList: 'auto_allow', + TaskList: 'auto_allow', + TaskOutput: 'auto_allow', + WebSearch: 'auto_allow', + FetchURL: 'auto_allow', + Agent: 'auto_allow', + AskUserQuestion: 'auto_allow', + EnterPlanMode: 'auto_allow', + ExitPlanMode: 'auto_allow', + Skill: 'auto_allow', + Bash: 'ask', + Write: 'ask', + Edit: 'ask', + TaskStop: 'ask', +}; + +export const BUILTIN_TOOL_DEFAULT_PERMISSIONS: BuiltinToolDefaultPermissionTable = + BUILTIN_TOOL_DEFAULT_PERMISSION_TABLE; + +export function getBuiltinToolDefaultPermission( + toolName: string, +): BuiltinToolDefaultPermission | undefined { + return BUILTIN_TOOL_DEFAULT_PERMISSIONS[toolName as KnownBuiltinToolName]; +} + +export function isDefaultAutoAllowTool(toolName: string): boolean { + return getBuiltinToolDefaultPermission(toolName) === 'auto_allow'; +} diff --git a/packages/agent-core/src/tools/policies/path-access.ts b/packages/agent-core/src/tools/policies/path-access.ts new file mode 100644 index 000000000..00bcd4f9a --- /dev/null +++ b/packages/agent-core/src/tools/policies/path-access.ts @@ -0,0 +1,334 @@ +/** + * Path safety guards used by Read/Write/Edit/Grep/Glob. + * + * Canonicalization is **lexical** only (no `realpath` / symlink following). + * Mirrors `KaosPath.canonical()` and keeps the guard backend-aware: + * callers should pass the active Kaos path class so SSH paths stay POSIX + * even when the host Node process is running on Windows. + * + * Shared-prefix escapes (a path like `/workspace-evil` passing a naive + * `startswith('/workspace')` check) are blocked by requiring a path + * separator (or exact equality) after the base prefix in + * `isWithinDirectory`. + */ + +import * as nativePath from 'node:path'; +import * as posixPath from 'node:path/posix'; +import * as win32Path from 'node:path/win32'; + +import type { Kaos } from '@moonshot-ai/kaos'; + +import type { WorkspaceConfig } from '../support/workspace'; +import { isSensitiveFile } from './sensitive'; + +export type PathClass = 'posix' | 'win32'; +export type PathSecurityCode = 'PATH_OUTSIDE_WORKSPACE' | 'PATH_SENSITIVE' | 'PATH_INVALID'; +export type PathAccessOperation = 'read' | 'write' | 'search'; +export type WorkspaceGuardMode = 'strict' | 'absolute-outside-allowed' | 'disabled'; + +export interface WorkspaceAccessPolicy { + readonly guardMode: WorkspaceGuardMode; + readonly checkSensitive: boolean; +} + +export const STRICT_WORKSPACE_ACCESS_POLICY: WorkspaceAccessPolicy = { + guardMode: 'strict', + checkSensitive: true, +}; + +export const DEFAULT_WORKSPACE_ACCESS_POLICY: WorkspaceAccessPolicy = { + guardMode: 'absolute-outside-allowed', + checkSensitive: true, +}; + +export interface PathAccess { + readonly path: string; + readonly outsideWorkspace: boolean; +} + +export class PathSecurityError extends Error { + readonly code: PathSecurityCode; + readonly rawPath: string; + readonly canonicalPath: string; + + constructor(code: PathSecurityCode, rawPath: string, canonicalPath: string, message: string) { + super(message); + this.name = 'PathSecurityError'; + this.code = code; + this.rawPath = rawPath; + this.canonicalPath = canonicalPath; + } +} + +const DEFAULT_PATH_CLASS: PathClass = nativePath.sep === '\\' ? 'win32' : 'posix'; + +function pathMod(pathClass: PathClass): typeof posixPath { + return pathClass === 'win32' ? win32Path : posixPath; +} + +function comparablePath(path: string, pathClass: PathClass): string { + return pathClass === 'win32' ? path.toLowerCase().replaceAll('/', '\\') : path; +} + +function isWin32DriveRelative(path: string): boolean { + return /^[A-Za-z]:(?:$|[^\\/])/.test(path); +} + +export function normalizeUserPath(path: string, pathClass: PathClass = DEFAULT_PATH_CLASS): string { + if (pathClass !== 'win32') return path; + + // A bare root slash flips to backslash so downstream win32 string + // operations (join, isAbsolute, drive-letter detection) treat it as a + // native path component. Matches the py helper's behavior. + if (path === '/') return '\\'; + + if (path.startsWith('//')) { + return path.replaceAll('/', '\\'); + } + + const cygdriveMatch = /^\/cygdrive\/([A-Za-z])(?:\/|$)/.exec(path); + if (cygdriveMatch !== null) { + const drive = cygdriveMatch[1]!.toUpperCase(); + const rest = path.slice(`/cygdrive/${cygdriveMatch[1]!}`.length).replaceAll('/', '\\'); + return `${drive}:${rest === '' ? '\\' : rest}`; + } + + const driveMatch = /^\/([A-Za-z])(?:\/|$)/.exec(path); + if (driveMatch !== null) { + const drive = driveMatch[1]!.toUpperCase(); + const rest = path.slice(2).replaceAll('/', '\\'); + return `${drive}:${rest === '' ? '\\' : rest}`; + } + + return path; +} + +function expandUserPath(path: string, homeDir: string | undefined, pathClass: PathClass): string { + if (homeDir === undefined) return path; + if (path === '~') return homeDir; + if (path.startsWith('~/') || (pathClass === 'win32' && path.startsWith('~\\'))) { + return pathMod(pathClass).join(homeDir, path.slice(2)); + } + return path; +} + +/** + * Lexical canonicalization: resolve relative → absolute against `cwd`, + * then normalize `..` / `.` segments. No filesystem I/O. + */ +export function canonicalizePath( + path: string, + cwd: string, + pathClass: PathClass = DEFAULT_PATH_CLASS, +): string { + if (path === '') { + throw new PathSecurityError('PATH_INVALID', path, path, 'Path cannot be empty'); + } + const mod = pathMod(pathClass); + const normalizedPath = normalizeUserPath(path, pathClass); + if (pathClass === 'win32' && isWin32DriveRelative(normalizedPath)) { + throw new PathSecurityError( + 'PATH_INVALID', + path, + normalizedPath, + `"${path}" is a drive-relative Windows path. Use an absolute path like C:\\path or a path relative to the working directory.`, + ); + } + if (!mod.isAbsolute(normalizedPath) && !mod.isAbsolute(cwd)) { + throw new PathSecurityError( + 'PATH_INVALID', + path, + normalizedPath, + `Cannot resolve "${path}" against non-absolute cwd "${cwd}".`, + ); + } + const abs = mod.isAbsolute(normalizedPath) ? normalizedPath : mod.resolve(cwd, normalizedPath); + return mod.normalize(abs); +} + +/** + * True iff `candidate` is `base` itself or a descendant of it, compared + * on path-component boundaries. Both arguments must already be canonical. + */ +export function isWithinDirectory( + candidate: string, + base: string, + pathClass: PathClass = DEFAULT_PATH_CLASS, +): boolean { + const mod = pathMod(pathClass); + const comparableCandidate = comparablePath(candidate, pathClass); + const comparableBase = comparablePath(base, pathClass); + if (comparableCandidate === comparableBase) return true; + const prefix = comparableBase.endsWith(mod.sep) ? comparableBase : comparableBase + mod.sep; + return comparableCandidate.startsWith(prefix); +} + +/** + * True iff `candidate` (already canonical) sits inside any of the workspace + * roots listed in `config` (primary `workspaceDir` or any `additionalDirs`). + */ +export function isWithinWorkspace( + candidate: string, + config: WorkspaceConfig, + pathClass: PathClass = DEFAULT_PATH_CLASS, +): boolean { + if (isWithinDirectory(candidate, config.workspaceDir, pathClass)) return true; + for (const dir of config.additionalDirs) { + if (isWithinDirectory(candidate, dir, pathClass)) return true; + } + return false; +} + +export interface AssertPathOptions { + readonly mode: PathAccessOperation; + /** When true (default), also reject paths matching a sensitive-file pattern. */ + readonly checkSensitive?: boolean | undefined; + readonly pathClass?: PathClass | undefined; +} + +export interface ResolvePathAccessOptions { + readonly operation: PathAccessOperation; + readonly policy?: WorkspaceAccessPolicy | undefined; + readonly pathClass?: PathClass | undefined; + readonly homeDir?: string; +} + +export interface ResolvePathAccessPathOptions { + readonly kaos: Pick; + readonly workspace: WorkspaceConfig; + readonly operation: PathAccessOperation; + readonly policy?: WorkspaceAccessPolicy; + readonly expandHome?: boolean; +} + +function outsideWorkspaceMessage( + path: string, + canonical: string, + config: WorkspaceConfig, + operation: PathAccessOperation, +): string { + const allowed = [config.workspaceDir, ...config.additionalDirs].join(', '); + const verb = operation === 'write' ? 'written' : operation === 'search' ? 'searched' : 'read'; + return ( + `"${path}" (canonical: "${canonical}") is outside the workspace ` + + `and outside the working directory "${config.workspaceDir}". ` + + `Cannot be ${verb}. Allowed roots: ${allowed}` + ); +} + +function relativeOutsideMessage(path: string, operation: PathAccessOperation): string { + const verb = + operation === 'write' + ? 'write or edit a file' + : operation === 'search' + ? 'search' + : 'read a file'; + return ( + `"${path}" is not an absolute path. ` + + `You must provide an absolute path to ${verb} outside the working directory.` + ); +} + +export function resolvePathAccess( + path: string, + cwd: string, + config: WorkspaceConfig, + options: ResolvePathAccessOptions, +): PathAccess { + const pathClass = options.pathClass ?? DEFAULT_PATH_CLASS; + const mod = pathMod(pathClass); + const normalizedPath = normalizeUserPath(path, pathClass); + const expandedPath = expandUserPath(normalizedPath, options.homeDir, pathClass); + const rawIsAbsolute = mod.isAbsolute(expandedPath); + const canonical = canonicalizePath(expandedPath, cwd, pathClass); + const outsideWorkspace = !isWithinWorkspace(canonical, config, pathClass); + const policy = options.policy ?? DEFAULT_WORKSPACE_ACCESS_POLICY; + + if (policy.checkSensitive && isSensitiveFile(canonical, pathClass)) { + throw new PathSecurityError( + 'PATH_SENSITIVE', + path, + canonical, + `"${path}" matches a sensitive-file pattern (env / credential / SSH key). ` + + `Access is blocked to protect secrets.`, + ); + } + + // Strict mode requires the input itself to be absolute, even if it + // would canonicalize to a path inside the workspace. The python Glob + // contract is "directory must be an absolute path"; resolving a + // relative argument against the workspace cwd silently re-targets the + // search and is rejected outright in that contract. + if (policy.guardMode === 'strict' && !rawIsAbsolute) { + throw new PathSecurityError( + 'PATH_OUTSIDE_WORKSPACE', + path, + canonical, + relativeOutsideMessage(path, options.operation), + ); + } + + if (outsideWorkspace) { + switch (policy.guardMode) { + case 'strict': + throw new PathSecurityError( + 'PATH_OUTSIDE_WORKSPACE', + path, + canonical, + outsideWorkspaceMessage(path, canonical, config, options.operation), + ); + case 'absolute-outside-allowed': + if (!rawIsAbsolute) { + throw new PathSecurityError( + 'PATH_OUTSIDE_WORKSPACE', + path, + canonical, + relativeOutsideMessage(path, options.operation), + ); + } + break; + case 'disabled': + break; + } + } + + return { path: canonical, outsideWorkspace }; +} + +export function resolvePathAccessPath( + path: string, + options: ResolvePathAccessPathOptions, +): string { + const { kaos, workspace, operation, policy, expandHome = true } = options; + return resolvePathAccess(path, workspace.workspaceDir, workspace, { + operation, + policy, + pathClass: kaos.pathClass(), + homeDir: expandHome ? kaos.gethome() : undefined, + }).path; +} + +/** + * Throw `PathSecurityError` if `path` is outside the workspace, a known + * sensitive file, or an empty string. Returns the canonical absolute path + * when the check passes. + * + * Note: this is purely lexical. It does NOT protect against symlink + * targets that point outside the workspace — that would require kaos-layer + * realpath support, which is not currently available. + */ +export function assertPathAllowed( + path: string, + cwd: string, + config: WorkspaceConfig, + options: AssertPathOptions, +): string { + return resolvePathAccess(path, cwd, config, { + operation: options.mode, + pathClass: options.pathClass, + policy: { + guardMode: 'strict', + checkSensitive: options.checkSensitive ?? STRICT_WORKSPACE_ACCESS_POLICY.checkSensitive, + }, + }).path; +} diff --git a/packages/agent-core/src/tools/policies/sensitive.ts b/packages/agent-core/src/tools/policies/sensitive.ts new file mode 100644 index 000000000..6557488f5 --- /dev/null +++ b/packages/agent-core/src/tools/policies/sensitive.ts @@ -0,0 +1,93 @@ +/** + * Sensitive-file detection. + * + * The pattern list is intentionally small to avoid false positives; files + * matching any of these patterns are blocked from Read/Write/Edit so + * credentials cannot be exfiltrated through a compromised prompt. Exemptions + * like `.env.example` are explicitly allowed. + */ + +import * as nativePath from 'node:path'; +import * as posixPath from 'node:path/posix'; +import * as win32Path from 'node:path/win32'; + +import type { PathClass } from './path-access'; + +const SENSITIVE_BASENAMES = new Set([ + '.env', + 'id_rsa', + 'id_ed25519', + 'id_ecdsa', + 'credentials', +]); + +const SENSITIVE_PATH_SUFFIXES = [ + ['.aws', 'credentials'], + ['.gcp', 'credentials'], +]; + +const ENV_PREFIX = '.env.'; +const ENV_EXEMPTIONS = new Set(['.env.example', '.env.sample', '.env.template']); + +const SENSITIVE_BASENAME_PREFIXES = ['id_rsa', 'id_ed25519', 'id_ecdsa', 'credentials']; +const PUBLIC_KEY_BASENAMES = new Set(['id_rsa.pub', 'id_ed25519.pub', 'id_ecdsa.pub']); +export const SENSITIVE_DOT_VARIANT_SUFFIXES = [ + '.bak', + '.backup', + '.copy', + '.disabled', + '.key', + '.old', + '.orig', + '.pem', + '.save', + '.tmp', +] as const; +const SENSITIVE_DOT_VARIANT_SUFFIX_SET = new Set(SENSITIVE_DOT_VARIANT_SUFFIXES); + +const DEFAULT_PATH_CLASS: PathClass = nativePath.sep === '\\' ? 'win32' : 'posix'; + +function pathMod(pathClass: PathClass): typeof posixPath { + return pathClass === 'win32' ? win32Path : posixPath; +} + +function comparable(path: string, pathClass: PathClass): string { + return pathClass === 'win32' ? path.toLowerCase() : path; +} + +export function isSensitiveFile(path: string, pathClass: PathClass = DEFAULT_PATH_CLASS): boolean { + const mod = pathMod(pathClass); + const name = mod.basename(path); + const comparableName = comparable(name, pathClass); + const comparablePath = comparable(path, pathClass); + + if (ENV_EXEMPTIONS.has(comparableName)) return false; + if (PUBLIC_KEY_BASENAMES.has(comparableName)) return false; + if (SENSITIVE_BASENAMES.has(comparableName)) return true; + if (comparableName.startsWith(ENV_PREFIX)) return true; + + for (const prefix of SENSITIVE_BASENAME_PREFIXES) { + if (comparableName === prefix) return true; + // Catch rename-shielded variants without flagging unrelated filenames + // like `id_rsafoo` or ordinary JSON files like `credentials.json`. + if (comparableName.length > prefix.length && comparableName.startsWith(prefix)) { + const suffix = comparableName.slice(prefix.length); + const next = suffix[0]; + if (next === '-' || next === '_') return true; + if (next === '.' && SENSITIVE_DOT_VARIANT_SUFFIX_SET.has(suffix)) return true; + } + } + + for (const suffixParts of SENSITIVE_PATH_SUFFIXES) { + const suffix = suffixParts.join(mod.sep); + const comparableSuffix = comparable(suffix, pathClass); + if ( + comparablePath.endsWith(`${mod.sep}${comparableSuffix}`) || + comparablePath.includes(`${mod.sep}${comparableSuffix}${mod.sep}`) + ) { + return true; + } + } + + return false; +} diff --git a/packages/agent-core/src/tools/providers/local-fetch-url.ts b/packages/agent-core/src/tools/providers/local-fetch-url.ts new file mode 100644 index 000000000..af10a8ca3 --- /dev/null +++ b/packages/agent-core/src/tools/providers/local-fetch-url.ts @@ -0,0 +1,229 @@ +/** + * LocalFetchURLProvider — host-side URL fetcher. + * + * Flow: + * 1. GET the URL with a Chrome-like UA. + * 2. Reject HTTP >= 400 with the status code in the message. + * 3. Reject responses larger than `maxBytes` (content-length first, + * then measured body length as a defensive second check). + * 4. `text/plain` / `text/markdown` → passthrough verbatim. + * 5. Otherwise (assumed HTML) → run Readability over a linkedom + * document. Return `# ${title}\n\n${text}` (title omitted when + * absent). If extraction yields no meaningful text, fall back to + * common content containers (`

    ` / `
    ` / ``) + * before throwing a "meaningful content" error. + */ + +import { Readability } from '@mozilla/readability'; +import { parseHTML as rawParseHTML } from 'linkedom'; + +import { HttpFetchError, type UrlFetcher, type UrlFetchResult } from '../builtin'; + +// Readability's .d.ts references the global `Document` type, but this +// package compiles with `lib: ES2023` (no DOM). Extracting the +// constructor parameter type keeps us off the global `Document` name +// while still accepting whatever Readability wants. +type ReadabilityDocument = ConstructorParameters[0]; + +// linkedom's published types depend on DOM libs we don't load. Declare +// the minimal surface we actually use so the rest of the file stays +// type-safe without pulling lib.dom.d.ts into the host build. +interface DomElementLike { + textContent: string | null; + querySelector(selector: string): DomElementLike | null; +} +interface DomParseResult { + document: DomElementLike; +} +const parseHTML = rawParseHTML as unknown as (html: string) => DomParseResult; + +const DEFAULT_USER_AGENT = + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ' + + '(KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'; + +const DEFAULT_MAX_BYTES = 10 * 1024 * 1024; + +export interface LocalFetchURLProviderOptions { + userAgent?: string; + fetchImpl?: typeof fetch; + maxBytes?: number; + /** + * Allow fetching loopback / RFC 1918 / link-local / ULA addresses. + * Defaults to `false` — enabled only for tests and (future) explicit + * opt-in. Keeps an LLM that's been prompt-injected from exfiltrating + * AWS/GCP metadata (169.254.169.254), probing internal services + * (10.x, 192.168.x), or reading local daemons (127.0.0.1:*). + */ + allowPrivateAddresses?: boolean; +} + +/** + * SSRF guard — reject non-http(s) schemes and (by default) any hostname + * that is, or parses as, a private / loopback / link-local / ULA IP + * literal. This is a *static* check against the URL string; it does NOT + * do DNS resolution, so a domain that resolves to a private IP via + * DNS-rebinding is **not** caught here. That attack is a known + * limitation; mitigations (e.g. pinning the resolved IP through to + * fetch) are left for a follow-up. + */ +function assertSafeFetchTarget(url: string, allowPrivate: boolean): void { + let parsed: URL; + try { + parsed = new URL(url); + } catch { + throw new Error(`Invalid URL: "${url}"`); + } + if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') { + throw new Error(`Unsupported URL scheme "${parsed.protocol}" — only http(s) allowed.`); + } + if (allowPrivate) return; + // URL hostname preserves surrounding `[ ]` for IPv6 literals on some + // Node versions (and not others). Strip them for uniform comparison. + const hostRaw = parsed.hostname.toLowerCase(); + const host = hostRaw.startsWith('[') && hostRaw.endsWith(']') ? hostRaw.slice(1, -1) : hostRaw; + // Literal "localhost" / loopback aliases. + if (host === 'localhost' || host.endsWith('.localhost')) { + throw new Error(`Refusing to fetch private host: "${host}"`); + } + // IPv6 loopback / ULA / link-local. Check after bracket strip. + if ( + host === '::1' || + host === '::' || + host.startsWith('fe80:') || + host.startsWith('fc') || + host.startsWith('fd') + ) { + throw new Error(`Refusing to fetch private host: "${host}"`); + } + // IPv4 literal — only check when the hostname is a dotted-quad; normal + // domains will never match. + const v4 = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(host); + if (v4 !== null) { + const octets = [v4[1], v4[2], v4[3], v4[4]].map(Number); + if (octets.some((n) => !Number.isInteger(n) || n < 0 || n > 255)) { + throw new Error(`Invalid IPv4 literal: "${host}"`); + } + const [a, b] = octets as [number, number, number, number]; + // 127.0.0.0/8 loopback, 10.0.0.0/8, 192.168.0.0/16, + // 172.16.0.0/12, 169.254.0.0/16 link-local / AWS metadata, + // 0.0.0.0/8 "this network", 100.64.0.0/10 CGNAT. + const isLoopback = a === 127; + const isPrivate10 = a === 10; + const isPrivate192 = a === 192 && b === 168; + const isPrivate172 = a === 172 && b >= 16 && b <= 31; + const isLinkLocal = a === 169 && b === 254; + const isZero = a === 0; + const isCgnat = a === 100 && b >= 64 && b <= 127; + if ( + isLoopback || + isPrivate10 || + isPrivate192 || + isPrivate172 || + isLinkLocal || + isZero || + isCgnat + ) { + throw new Error(`Refusing to fetch private address: "${host}"`); + } + } +} + +export class LocalFetchURLProvider implements UrlFetcher { + private readonly userAgent: string; + private readonly fetchImpl: typeof fetch; + private readonly maxBytes: number; + private readonly allowPrivateAddresses: boolean; + + constructor(options: LocalFetchURLProviderOptions = {}) { + this.userAgent = options.userAgent ?? DEFAULT_USER_AGENT; + this.fetchImpl = options.fetchImpl ?? globalThis.fetch.bind(globalThis); + this.maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES; + this.allowPrivateAddresses = options.allowPrivateAddresses ?? false; + } + + async fetch(url: string, _options?: { toolCallId?: string }): Promise { + assertSafeFetchTarget(url, this.allowPrivateAddresses); + + const response = await this.fetchImpl(url, { + method: 'GET', + headers: { 'User-Agent': this.userAgent }, + }); + + if (response.status >= 400) { + // Drain the unused body so undici can release the socket back to + // the keep-alive pool instead of leaking it on error paths. + await response.body?.cancel().catch(() => { + /* already closed */ + }); + throw new HttpFetchError( + response.status, + `HTTP ${String(response.status)} ${response.statusText}`, + ); + } + + // Reject oversized responses before buffering the full body. + const contentLengthRaw = response.headers.get('content-length'); + if (contentLengthRaw !== null) { + const cl = Number(contentLengthRaw); + if (Number.isFinite(cl) && cl > this.maxBytes) { + throw new Error( + `Response body too large: ${String(cl)} bytes exceeds maxBytes (${String(this.maxBytes)}).`, + ); + } + } + + const body = await response.text(); + + // Servers may omit content-length — measure again defensively. + const actualBytes = Buffer.byteLength(body, 'utf8'); + if (actualBytes > this.maxBytes) { + throw new Error( + `Response body too large: ${String(actualBytes)} bytes exceeds maxBytes (${String(this.maxBytes)}).`, + ); + } + + const contentType = (response.headers.get('content-type') ?? '').toLowerCase(); + if (contentType.startsWith('text/plain') || contentType.startsWith('text/markdown')) { + return { content: body, kind: 'passthrough' }; + } + + return { content: this.extractMainContent(body), kind: 'extracted' }; + } + + private extractMainContent(html: string): string { + // Readability mutates the DOM it parses, so parse twice — once for + // the primary extractor and once for the fallback path. + const primary = parseHTML(html); + try { + const reader = new Readability(primary.document as unknown as ReadabilityDocument, { + charThreshold: 0, + }); + const article = reader.parse(); + if (article !== null) { + const text = (article.textContent ?? '').trim(); + if (text.length > 0) { + const title = (article.title ?? '').trim(); + return title.length > 0 ? `# ${title}\n\n${text}` : text; + } + } + } catch { + // Fall through to the container-based fallback. + } + + const { document } = parseHTML(html); + const titleText = (document.querySelector('title')?.textContent ?? '').trim(); + const container = + document.querySelector('article') ?? + document.querySelector('main') ?? + document.querySelector('body'); + const fallbackText = (container?.textContent ?? '').trim(); + + if (fallbackText.length === 0) { + throw new Error( + 'Failed to extract meaningful content from the page. The page may require JavaScript to render.', + ); + } + + return titleText.length > 0 ? `# ${titleText}\n\n${fallbackText}` : fallbackText; + } +} diff --git a/packages/agent-core/src/tools/providers/moonshot-fetch-url.ts b/packages/agent-core/src/tools/providers/moonshot-fetch-url.ts new file mode 100644 index 000000000..825781da4 --- /dev/null +++ b/packages/agent-core/src/tools/providers/moonshot-fetch-url.ts @@ -0,0 +1,128 @@ +/** + * MoonshotFetchURLProvider — host-side UrlFetcher. + * + * Flow: + * 1. Try Moonshot coding-fetch service (POST {url}, Bearer token from a + * narrow token provider, Accept: text/markdown, host-provided headers). + * 2. Moonshot 200 → return the body as `extracted` content (the + * service has already extracted the main page text on its side). + * 3. Any Moonshot failure — non-200, network error, or token + * refresh failure — → delegate to `localFallback`, forwarding its + * content kind, so the LLM still gets *something* when the service + * is down. + * 4. If localFallback also throws → propagate that error. + */ + +import { HttpFetchError, type UrlFetcher, type UrlFetchResult } from '../builtin'; + +export interface BearerTokenProvider { + getAccessToken(options?: { readonly force?: boolean | undefined }): Promise; +} + +export interface MoonshotFetchURLProviderOptions { + tokenProvider?: BearerTokenProvider; + apiKey?: string; + baseUrl: string; + defaultHeaders?: Record; + customHeaders?: Record; + localFallback: UrlFetcher; + fetchImpl?: typeof fetch; +} + +export class MoonshotFetchURLProvider implements UrlFetcher { + private readonly tokenProvider: BearerTokenProvider | undefined; + private readonly apiKey: string | undefined; + private readonly baseUrl: string; + private readonly defaultHeaders: Record; + private readonly customHeaders: Record; + private readonly localFallback: UrlFetcher; + private readonly fetchImpl: typeof fetch; + + constructor(options: MoonshotFetchURLProviderOptions) { + this.tokenProvider = options.tokenProvider; + this.apiKey = options.apiKey; + this.baseUrl = options.baseUrl; + this.defaultHeaders = options.defaultHeaders ?? {}; + this.customHeaders = options.customHeaders ?? {}; + this.localFallback = options.localFallback; + this.fetchImpl = options.fetchImpl ?? globalThis.fetch.bind(globalThis); + } + + async fetch(url: string, options?: { toolCallId?: string }): Promise { + try { + const content = await this.fetchViaMoonshot(url, options?.toolCallId); + // The service returns text it has already extracted from the page. + return { content, kind: 'extracted' }; + } catch { + // Forward an explicit options object even when the caller passed + // none, so downstream consumers always see a defined second arg. + return this.localFallback.fetch(url, options ?? {}); + } + } + + private async fetchViaMoonshot( + url: string, + toolCallId: string | undefined, + ): Promise { + const bodyJson = JSON.stringify({ url }); + + const response = await this.post(bodyJson, toolCallId); + + if (response.status !== 200) { + let detail = ''; + try { + detail = await response.text(); + } catch { + // ignore — status code alone is informative enough for the + // fallback path that catches this. + } + throw new HttpFetchError( + response.status, + `Moonshot fetch request failed: HTTP ${String(response.status)}. ${detail}`.trim(), + ); + } + + return response.text(); + } + + private async post(bodyJson: string, toolCallId: string | undefined): Promise { + const accessToken = await this.resolveApiKey(); + return this.fetchImpl(this.baseUrl, { + method: 'POST', + headers: { + ...this.defaultHeaders, + Authorization: `Bearer ${accessToken}`, + Accept: 'text/markdown', + 'Content-Type': 'application/json', + ...(toolCallId !== undefined && toolCallId.length > 0 + ? { 'X-Msh-Tool-Call-Id': toolCallId } + : {}), + ...this.customHeaders, + }, + body: bodyJson, + }); + } + + private async resolveApiKey(): Promise { + if (this.tokenProvider !== undefined) { + try { + const token = await this.tokenProvider.getAccessToken(); + if (token.trim().length > 0) { + return token; + } + if (this.apiKey !== undefined && this.apiKey.length > 0) { + return this.apiKey; + } + } catch (error) { + if (this.apiKey !== undefined && this.apiKey.length > 0) { + return this.apiKey; + } + throw error; + } + } + if (this.apiKey !== undefined && this.apiKey.length > 0) { + return this.apiKey; + } + throw new Error('Moonshot fetch service is not configured: missing API key or token provider.'); + } +} diff --git a/packages/agent-core/src/tools/providers/moonshot-web-search.ts b/packages/agent-core/src/tools/providers/moonshot-web-search.ts new file mode 100644 index 000000000..f1ef6c18b --- /dev/null +++ b/packages/agent-core/src/tools/providers/moonshot-web-search.ts @@ -0,0 +1,143 @@ +/** + * MoonshotWebSearchProvider — host-side `WebSearchProvider`. + * + * Auth uses a narrow bearer token provider per request. Host-specific + * default headers are supplied by runtime and request-level overrides + * come from `customHeaders`. + */ + +import type { WebSearchProvider, WebSearchResult } from '../builtin'; + +export interface BearerTokenProvider { + getAccessToken(options?: { readonly force?: boolean | undefined }): Promise; +} + +export interface MoonshotWebSearchProviderOptions { + tokenProvider?: BearerTokenProvider; + apiKey?: string; + baseUrl: string; + defaultHeaders?: Record; + customHeaders?: Record; + fetchImpl?: typeof fetch; +} + +interface MoonshotSearchResult { + site_name?: string; + title?: string; + url?: string; + snippet?: string; + content?: string; + date?: string; + icon?: string; + mime?: string; +} + +interface MoonshotSearchResponse { + search_results?: MoonshotSearchResult[]; +} + +export class MoonshotWebSearchProvider implements WebSearchProvider { + private readonly tokenProvider: BearerTokenProvider | undefined; + private readonly apiKey: string | undefined; + private readonly baseUrl: string; + private readonly defaultHeaders: Record; + private readonly customHeaders: Record; + private readonly fetchImpl: typeof fetch; + + constructor(options: MoonshotWebSearchProviderOptions) { + this.tokenProvider = options.tokenProvider; + this.apiKey = options.apiKey; + this.baseUrl = options.baseUrl; + this.defaultHeaders = options.defaultHeaders ?? {}; + this.customHeaders = options.customHeaders ?? {}; + this.fetchImpl = options.fetchImpl ?? globalThis.fetch.bind(globalThis); + } + + async search( + query: string, + options?: { limit?: number; includeContent?: boolean; toolCallId?: string }, + ): Promise { + const body = { + text_query: query, + limit: options?.limit ?? 5, + enable_page_crawling: options?.includeContent ?? false, + timeout_seconds: 30, + }; + const bodyJson = JSON.stringify(body); + + const toolCallId = options?.toolCallId; + const response = await this.post(bodyJson, toolCallId); + + if (response.status === 401) { + const detail = await safeReadText(response); + throw new Error( + `Moonshot search request failed: HTTP 401 (auth/unauthorized). ${detail}`.trim(), + ); + } + + if (response.status !== 200) { + const detail = await safeReadText(response); + throw new Error( + `Moonshot search request failed: HTTP ${String(response.status)}. ${detail}`.trim(), + ); + } + + const json = (await response.json()) as MoonshotSearchResponse; + const raw = Array.isArray(json.search_results) ? json.search_results : []; + + return raw.map((r): WebSearchResult => { + const out: WebSearchResult = { + title: r.title ?? '', + url: r.url ?? '', + snippet: r.snippet ?? '', + }; + if (typeof r.date === 'string' && r.date.length > 0) out.date = r.date; + if (typeof r.content === 'string' && r.content.length > 0) out.content = r.content; + return out; + }); + } + + private async post(bodyJson: string, toolCallId: string | undefined): Promise { + const accessToken = await this.resolveApiKey(); + return this.fetchImpl(this.baseUrl, { + method: 'POST', + headers: { + ...this.defaultHeaders, + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + ...(toolCallId !== undefined && toolCallId.length > 0 + ? { 'X-Msh-Tool-Call-Id': toolCallId } + : {}), + ...this.customHeaders, + }, + body: bodyJson, + }); + } + + private async resolveApiKey(): Promise { + if (this.tokenProvider !== undefined) { + try { + return await this.tokenProvider.getAccessToken(); + } catch (error) { + if (this.apiKey !== undefined && this.apiKey.length > 0) { + return this.apiKey; + } + throw error; + } + } + if (this.apiKey !== undefined && this.apiKey.length > 0) { + return this.apiKey; + } + throw new Error( + 'Moonshot search service is not configured: missing API key or token provider.', + ); + } +} + +async function safeReadText(response: Response): Promise { + try { + return await response.text(); + } catch { + return ''; + } +} diff --git a/packages/agent-core/src/tools/store.ts b/packages/agent-core/src/tools/store.ts new file mode 100644 index 000000000..60279b68a --- /dev/null +++ b/packages/agent-core/src/tools/store.ts @@ -0,0 +1,13 @@ +export interface ToolStoreData {} + +export type ToolStoreKey = Extract; + +export interface ToolStore { + get(key: K): ToolStoreData[K] | undefined; + set(key: K, value: ToolStoreData[K]): void; +} + +export interface ToolStoreUpdate { + readonly key: K; + readonly value: ToolStoreData[K]; +} diff --git a/packages/agent-core/src/tools/support/file-type.ts b/packages/agent-core/src/tools/support/file-type.ts new file mode 100644 index 000000000..56e40529d --- /dev/null +++ b/packages/agent-core/src/tools/support/file-type.ts @@ -0,0 +1,383 @@ +/** + * file-type — magic-byte + extension detection. No npm dependency. + */ + +export const MEDIA_SNIFF_BYTES = 512; + +export interface FileType { + readonly kind: 'text' | 'image' | 'video' | 'unknown'; + readonly mimeType: string; +} + +export const IMAGE_MIME_BY_SUFFIX: Readonly> = Object.freeze({ + '.png': 'image/png', + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.gif': 'image/gif', + '.bmp': 'image/bmp', + '.tif': 'image/tiff', + '.tiff': 'image/tiff', + '.webp': 'image/webp', + '.ico': 'image/x-icon', + '.heic': 'image/heic', + '.heif': 'image/heif', + '.avif': 'image/avif', + '.svgz': 'image/svg+xml', +}); + +export const VIDEO_MIME_BY_SUFFIX: Readonly> = Object.freeze({ + '.mp4': 'video/mp4', + '.mpg': 'video/mpeg', + '.mpeg': 'video/mpeg', + '.mkv': 'video/x-matroska', + '.avi': 'video/x-msvideo', + '.mov': 'video/quicktime', + '.ogv': 'video/ogg', + '.wmv': 'video/x-ms-wmv', + '.webm': 'video/webm', + '.m4v': 'video/x-m4v', + '.flv': 'video/x-flv', + '.3gp': 'video/3gpp', + '.3g2': 'video/3gpp2', +}); + +const TEXT_MIME_BY_SUFFIX: Readonly> = Object.freeze({ + '.svg': 'image/svg+xml', +}); + +export const NON_TEXT_SUFFIXES: ReadonlySet = new Set([ + '.icns', + '.psd', + '.ai', + '.eps', + '.pdf', + '.doc', + '.docx', + '.dot', + '.dotx', + '.rtf', + '.odt', + '.xls', + '.xlsx', + '.xlsm', + '.xlt', + '.xltx', + '.xltm', + '.ods', + '.ppt', + '.pptx', + '.pptm', + '.pps', + '.ppsx', + '.odp', + '.pages', + '.numbers', + '.key', + '.zip', + '.rar', + '.7z', + '.tar', + '.gz', + '.tgz', + '.bz2', + '.xz', + '.zst', + '.lz', + '.lz4', + '.br', + '.cab', + '.ar', + '.deb', + '.rpm', + '.mp3', + '.wav', + '.flac', + '.ogg', + '.oga', + '.opus', + '.aac', + '.m4a', + '.wma', + '.ttf', + '.otf', + '.woff', + '.woff2', + '.exe', + '.dll', + '.so', + '.dylib', + '.bin', + '.apk', + '.ipa', + '.jar', + '.class', + '.pyc', + '.pyo', + '.wasm', + '.dmg', + '.iso', + '.img', + '.sqlite', + '.sqlite3', + '.db', + '.db3', +]); + +const ASF_HEADER = Buffer.from([ + 0x30, 0x26, 0xb2, 0x75, 0x8e, 0x66, 0xcf, 0x11, 0xa6, 0xd9, 0x00, 0xaa, 0x00, 0x62, 0xce, 0x6c, +]); + +const FTYP_IMAGE_BRANDS: Readonly> = Object.freeze({ + avif: 'image/avif', + avis: 'image/avif', + heic: 'image/heic', + heif: 'image/heif', + heix: 'image/heif', + hevc: 'image/heic', + mif1: 'image/heif', + msf1: 'image/heif', +}); + +const FTYP_VIDEO_BRANDS: Readonly> = Object.freeze({ + isom: 'video/mp4', + iso2: 'video/mp4', + iso5: 'video/mp4', + mp41: 'video/mp4', + mp42: 'video/mp4', + avc1: 'video/mp4', + mp4v: 'video/mp4', + m4v: 'video/x-m4v', + qt: 'video/quicktime', + '3gp4': 'video/3gpp', + '3gp5': 'video/3gpp', + '3gp6': 'video/3gpp', + '3gp7': 'video/3gpp', + '3g2': 'video/3gpp2', +}); + +function toBuffer(data: Buffer | Uint8Array): Buffer { + return Buffer.isBuffer(data) ? data : Buffer.from(data.buffer, data.byteOffset, data.byteLength); +} + +function startsWith(buf: Buffer, prefix: Buffer | readonly number[]): boolean { + const needle = Buffer.isBuffer(prefix) ? prefix : Buffer.from(prefix); + if (buf.length < needle.length) return false; + for (let i = 0; i < needle.length; i += 1) { + if (buf[i] !== needle[i]) return false; + } + return true; +} + +function sniffFtypBrand(header: Buffer): string | null { + if (header.length < 12) return null; + if (header.subarray(4, 8).toString('latin1') !== 'ftyp') return null; + const raw = header.subarray(8, 12).toString('latin1').toLowerCase(); + // Python `.strip()` removes ASCII whitespace including trailing NULs via + // the `decode(..., errors="ignore")` semantics. We approximate: trim + // spaces and trailing NULs so brands like `qt ` → `qt`. + // oxlint-disable-next-line no-control-regex + return raw.replaceAll(/[\s\u0000]+$/g, '').trim(); +} + +export function sniffMediaFromMagic(data: Buffer | Uint8Array): FileType | null { + const buf = toBuffer(data); + const header = buf.length > MEDIA_SNIFF_BYTES ? buf.subarray(0, MEDIA_SNIFF_BYTES) : buf; + + if (startsWith(header, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])) { + return { kind: 'image', mimeType: 'image/png' }; + } + if (startsWith(header, [0xff, 0xd8, 0xff])) { + return { kind: 'image', mimeType: 'image/jpeg' }; + } + if (startsWith(header, Buffer.from('GIF87a')) || startsWith(header, Buffer.from('GIF89a'))) { + return { kind: 'image', mimeType: 'image/gif' }; + } + if (startsWith(header, Buffer.from('BM'))) { + return { kind: 'image', mimeType: 'image/bmp' }; + } + if ( + startsWith(header, [0x49, 0x49, 0x2a, 0x00]) || + startsWith(header, [0x4d, 0x4d, 0x00, 0x2a]) + ) { + return { kind: 'image', mimeType: 'image/tiff' }; + } + if (startsWith(header, [0x00, 0x00, 0x01, 0x00])) { + return { kind: 'image', mimeType: 'image/x-icon' }; + } + if (startsWith(header, Buffer.from('RIFF')) && header.length >= 12) { + const chunk = header.subarray(8, 12).toString('latin1'); + if (chunk === 'WEBP') return { kind: 'image', mimeType: 'image/webp' }; + if (chunk === 'AVI ') return { kind: 'video', mimeType: 'video/x-msvideo' }; + } + if (startsWith(header, Buffer.from('FLV'))) { + return { kind: 'video', mimeType: 'video/x-flv' }; + } + if (startsWith(header, ASF_HEADER)) { + return { kind: 'video', mimeType: 'video/x-ms-wmv' }; + } + if (startsWith(header, [0x1a, 0x45, 0xdf, 0xa3])) { + const lowered = header.toString('latin1').toLowerCase(); + if (lowered.includes('webm')) return { kind: 'video', mimeType: 'video/webm' }; + if (lowered.includes('matroska')) return { kind: 'video', mimeType: 'video/x-matroska' }; + } + const brand = sniffFtypBrand(header); + if (brand !== null && brand !== '') { + if (brand in FTYP_IMAGE_BRANDS) { + return { kind: 'image', mimeType: FTYP_IMAGE_BRANDS[brand]! }; + } + if (brand in FTYP_VIDEO_BRANDS) { + return { kind: 'video', mimeType: FTYP_VIDEO_BRANDS[brand]! }; + } + } + return null; +} + +export interface ImageDimensions { + readonly width: number; + readonly height: number; +} + +/** + * Best-effort pixel-dimension reader for common raster formats. + * + * Inspects only the fixed region near the start of the file where each + * format records its dimensions (the IHDR/DIB header, the RIFF chunk + * after the `WEBP` tag, or the first JPEG SOFn segment). Returns `null` + * for formats whose dimensions are not locatable from that region, or + * when the supplied buffer is too short to cover it. + */ +export function sniffImageDimensions(data: Buffer | Uint8Array): ImageDimensions | null { + const buf = toBuffer(data); + + // PNG — IHDR is the first chunk; width/height are big-endian uint32 + // at offsets 16 and 20. + if (startsWith(buf, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]) && buf.length >= 24) { + return { width: buf.readUInt32BE(16), height: buf.readUInt32BE(20) }; + } + + // GIF — logical-screen width/height are little-endian uint16 at + // offsets 6 and 8. + if ( + (startsWith(buf, Buffer.from('GIF87a')) || startsWith(buf, Buffer.from('GIF89a'))) && + buf.length >= 10 + ) { + return { width: buf.readUInt16LE(6), height: buf.readUInt16LE(8) }; + } + + // BMP — DIB header width/height are little-endian int32 at offsets 18 + // and 22 (height may be negative for top-down bitmaps). + if (startsWith(buf, Buffer.from('BM')) && buf.length >= 26) { + return { width: buf.readInt32LE(18), height: Math.abs(buf.readInt32LE(22)) }; + } + + // WEBP — RIFF container; VP8/VP8L/VP8X each store dimensions + // differently in the chunk that follows the 'WEBP' tag. + if (startsWith(buf, Buffer.from('RIFF')) && buf.length >= 30) { + const fourCc = buf.subarray(12, 16).toString('latin1'); + if (fourCc === 'VP8 ') { + return { + width: buf.readUInt16LE(26) & 0x3fff, + height: buf.readUInt16LE(28) & 0x3fff, + }; + } + if (fourCc === 'VP8L' && buf.length >= 25) { + const bits = buf.readUInt32LE(21); + return { + width: (bits & 0x3fff) + 1, + height: ((bits >> 14) & 0x3fff) + 1, + }; + } + if (fourCc === 'VP8X') { + const width = 1 + (buf[24]! | (buf[25]! << 8) | (buf[26]! << 16)); + const height = 1 + (buf[27]! | (buf[28]! << 8) | (buf[29]! << 16)); + return { width, height }; + } + } + + // JPEG — scan segment markers for a Start-Of-Frame (SOFn) marker, + // whose payload carries height/width as big-endian uint16. + if (startsWith(buf, [0xff, 0xd8])) { + let offset = 2; + while (offset + 9 < buf.length) { + if (buf[offset] !== 0xff) { + offset += 1; + continue; + } + const marker = buf[offset + 1]!; + // SOFn markers carry frame dimensions; skip SOF4/SOF8/SOF12 (0xc4/0xc8/0xcc). + if ( + marker >= 0xc0 && + marker <= 0xcf && + marker !== 0xc4 && + marker !== 0xc8 && + marker !== 0xcc + ) { + return { + height: buf.readUInt16BE(offset + 5), + width: buf.readUInt16BE(offset + 7), + }; + } + // Standalone markers (RSTn, SOI, EOI) carry no length field. + if (marker === 0xd8 || marker === 0xd9 || (marker >= 0xd0 && marker <= 0xd7)) { + offset += 2; + continue; + } + const segmentLength = buf.readUInt16BE(offset + 2); + if (segmentLength < 2) break; + offset += 2 + segmentLength; + } + } + + return null; +} + +function getSuffix(path: string): string { + const idx = path.lastIndexOf('.'); + if (idx === -1) return ''; + // POSIX `.suffix` treats `foo.tar.gz` → `.gz` and `foo/.bashrc` → '' (leading-dot is "no suffix"). + const lastSep = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\')); + if (idx <= lastSep + 1) return ''; + return path.slice(idx).toLowerCase(); +} + +export function detectFileType(path: string, header?: Buffer | Uint8Array): FileType { + const suffix = getSuffix(path); + let mediaHint: FileType | null = null; + if (suffix in TEXT_MIME_BY_SUFFIX) { + mediaHint = { kind: 'text', mimeType: TEXT_MIME_BY_SUFFIX[suffix]! }; + } else if (suffix in IMAGE_MIME_BY_SUFFIX) { + mediaHint = { kind: 'image', mimeType: IMAGE_MIME_BY_SUFFIX[suffix]! }; + } else if (suffix in VIDEO_MIME_BY_SUFFIX) { + mediaHint = { kind: 'video', mimeType: VIDEO_MIME_BY_SUFFIX[suffix]! }; + } + + // When a header is supplied, cross-validate against the ext hint — + // a mismatch reports `unknown` rather than blindly trusting the + // extension. When ext hint + sniff agree on kind, prefer the ext's + // mimeType so the reported MIME matches what the filename advertised. + // A disagreement on `kind` (e.g. `.mp4` with JPEG magic) still + // collapses to `unknown`. + if (header !== undefined) { + const buf = toBuffer(header); + const sniffed = sniffMediaFromMagic(buf); + if (sniffed) { + if (mediaHint) { + if (sniffed.kind !== mediaHint.kind) { + return { kind: 'unknown', mimeType: '' }; + } + return mediaHint; + } + return sniffed; + } + if (buf.includes(0x00)) { + return { kind: 'unknown', mimeType: '' }; + } + // No sniff and no NUL: fall through to hint / text / unknown logic. + } + + if (mediaHint) return mediaHint; + if (NON_TEXT_SUFFIXES.has(suffix)) { + return { kind: 'unknown', mimeType: '' }; + } + return { kind: 'text', mimeType: 'text/plain' }; +} diff --git a/packages/agent-core/src/tools/support/git-worktree.ts b/packages/agent-core/src/tools/support/git-worktree.ts new file mode 100644 index 000000000..fedf55e1a --- /dev/null +++ b/packages/agent-core/src/tools/support/git-worktree.ts @@ -0,0 +1,98 @@ +/** + * Marker-based git work-tree detection. Never spawns `git`; failures return + * null so callers can fall back to their safer path. + */ + +import * as posixPath from 'node:path/posix'; +import * as win32Path from 'node:path/win32'; + +import type { Kaos } from '@moonshot-ai/kaos'; + +import type { PathClass } from '../policies/path-access'; + +const S_IFMT = 0o170000; +const S_IFDIR = 0o040000; +const S_IFREG = 0o100000; + +export interface GitWorkTreeMarker { + readonly dotGitPath: string; + readonly controlDirPath: string; +} + +function pathMod(pathClass: PathClass): typeof posixPath { + return pathClass === 'win32' ? win32Path : posixPath; +} + +export async function findGitWorkTreeMarker( + kaos: Kaos, + cwd: string, +): Promise { + const pathClass = kaos.pathClass(); + const mod = pathMod(pathClass); + if (cwd.length === 0 || !mod.isAbsolute(cwd)) return null; + + let current = mod.normalize(cwd); + for (let depth = 0; depth < 256; depth += 1) { + const dotGitPath = mod.join(current, '.git'); + const hit = await probeGitMarker(kaos, dotGitPath, current, pathClass); + if (hit !== null) return hit; + + const parent = mod.dirname(current); + if (parent === current) return null; + current = parent; + } + return null; +} + +async function probeGitMarker( + kaos: Kaos, + dotGitPath: string, + markerParent: string, + pathClass: PathClass, +): Promise { + let stat: Awaited>; + try { + stat = await kaos.stat(dotGitPath); + } catch { + return null; + } + + if (isMode(stat.stMode, S_IFDIR)) return { dotGitPath, controlDirPath: dotGitPath }; + if (!isMode(stat.stMode, S_IFREG)) return null; + + let content: string; + try { + content = await kaos.readText(dotGitPath); + } catch { + return null; + } + const controlDirPath = parseGitDir(content, markerParent, pathClass); + return controlDirPath === undefined ? null : { dotGitPath, controlDirPath }; +} + +function isMode(stMode: number, mode: number): boolean { + return (stMode & S_IFMT) === mode; +} + +/** Drop UTF-8 BOM and any leading whitespace (incl. `\r\n`) before content checks. */ +function stripLeadingNoise(content: string): string { + let s = content; + if (s.codePointAt(0) === 0xfeff) s = s.slice(1); + return s.trimStart(); +} + +function parseGitDir( + content: string, + markerParent: string, + pathClass: PathClass, +): string | undefined { + const line = stripLeadingNoise(content).split(/\r?\n/, 1)[0]?.trim(); + if (line === undefined || !line.startsWith('gitdir:')) return undefined; + + const rawPath = line.slice('gitdir:'.length).trim(); + if (rawPath.length === 0) return undefined; + + const mod = pathMod(pathClass); + const absolute = mod.isAbsolute(rawPath) ? rawPath : mod.join(markerParent, rawPath); + return mod.normalize(absolute); +} diff --git a/packages/agent-core/src/tools/support/input-schema.ts b/packages/agent-core/src/tools/support/input-schema.ts new file mode 100644 index 000000000..0c0fb2d14 --- /dev/null +++ b/packages/agent-core/src/tools/support/input-schema.ts @@ -0,0 +1,62 @@ +/** + * Shared helper for deriving the JSON Schema that a tool advertises to the + * model for its parameters. + * + * A tool's parameter schema describes the *input* the model is expected to + * supply. zod v4's `toJSONSchema` defaults to the *output* view, which marks + * any field carrying a chain-tail `.default()` as `required` — producing a + * schema that simultaneously declares a `default` and lists the field as + * required. That contradiction also makes the runtime AJV validator reject + * legal calls that omit the defaulted fields. + * + * Always render parameter schemas through this helper so the `io: 'input'` + * view is applied uniformly and defaulted fields remain optional, while the + * closed-object guard (`additionalProperties: false`) is kept so unknown + * arguments are still rejected. + */ + +import { z } from 'zod'; + +/** + * Convert a zod schema into the input JSON Schema exposed to the model. + * + * @param schema - The zod schema describing the tool's parameters. + * @returns A draft-07 JSON Schema rendered with the input view. + */ +export function toInputJsonSchema(schema: z.ZodType): Record { + const jsonSchema = z.toJSONSchema(schema, { + target: 'draft-7', + io: 'input', + }); + closeObjectNodes(jsonSchema); + return jsonSchema; +} + +/** + * Re-assert `additionalProperties: false` on every object node. + * + * The input view drops `additionalProperties: false` from `z.object` nodes + * because, before unknown-key stripping, an *input* object may legally carry + * extra keys. But a tool's parameter schema is a model-facing contract that + * the runtime validates with AJV only — there is no zod parse/strip step + * before dispatch — so without the closed-object guard a misspelled argument + * passes validation and is silently ignored. Restoring it keeps unknown + * arguments rejected, matching the output view's pre-input-view behavior. + * + * Nodes that already declare `additionalProperties` (e.g. `z.record`) are + * left untouched. + */ +function closeObjectNodes(value: unknown): void { + if (Array.isArray(value)) { + for (const item of value) closeObjectNodes(item); + return; + } + if (typeof value !== 'object' || value === null) return; + const node = value as Record; + if (node['type'] === 'object' && node['additionalProperties'] === undefined) { + node['additionalProperties'] = false; + } + for (const child of Object.values(node)) { + closeObjectNodes(child); + } +} diff --git a/packages/agent-core/src/tools/support/list-directory.ts b/packages/agent-core/src/tools/support/list-directory.ts new file mode 100644 index 000000000..433aa9f6e --- /dev/null +++ b/packages/agent-core/src/tools/support/list-directory.ts @@ -0,0 +1,127 @@ +/** + * list-directory — compact 2-level directory tree for LLM context. + * + * Used by GlobTool when rejecting a `**`-leading pattern: appending a + * snapshot of the workspace root helps the LLM re-scope its pattern + * without a second round-trip. + * + * Width caps keep the system-prompt token budget bounded: + * - Depth 0 (root): up to LIST_DIR_ROOT_WIDTH entries + * - Depth 1 (children of root dirs): up to LIST_DIR_CHILD_WIDTH entries + * - Truncated levels show "... and N more" so the LLM knows more exists. + */ + +import * as posixPath from 'node:path/posix'; +import * as win32Path from 'node:path/win32'; + +import type { Kaos } from '@moonshot-ai/kaos'; + +export const LIST_DIR_ROOT_WIDTH = 30; +export const LIST_DIR_CHILD_WIDTH = 10; + +type PathClass = 'posix' | 'win32'; + +interface Entry { + readonly name: string; + readonly isDir: boolean; +} + +async function collectEntries( + kaos: Kaos, + dirPath: string, + maxWidth: number, + pathClass: PathClass, +): Promise<{ entries: Entry[]; total: number; readable: boolean }> { + const all: Entry[] = []; + try { + for await (const fullPath of kaos.iterdir(dirPath)) { + const name = basename(fullPath, pathClass); + let isDir = false; + try { + const st = await kaos.stat(fullPath); + // StatResult mirrors POSIX stat; derive the file type from the + // mode bits (S_IFMT mask → S_IFDIR == 0o040000). + isDir = (st.stMode & 0o170000) === 0o040000; + } catch { + // Unreadable entries keep isDir=false; still list the name. + } + all.push({ name, isDir }); + } + } catch { + return { entries: [], total: 0, readable: false }; + } + all.sort((a, b) => { + if (a.isDir !== b.isDir) return a.isDir ? -1 : 1; + return a.name.localeCompare(b.name); + }); + return { entries: all.slice(0, maxWidth), total: all.length, readable: true }; +} + +function pathMod(pathClass: PathClass): typeof posixPath { + return pathClass === 'win32' ? win32Path : posixPath; +} + +function basename(p: string, pathClass: PathClass): string { + return pathMod(pathClass).basename(p); +} + +/** + * Return a 2-level tree listing of `workDir` suitable for inclusion in a + * tool error message. Returns `"(empty directory)"` if the directory is + * empty, or an error marker line if the directory itself is unreadable. + */ +export async function listDirectory(kaos: Kaos, workDir: string): Promise { + const lines: string[] = []; + const pathClass = kaos.pathClass(); + const { entries, total, readable } = await collectEntries( + kaos, + workDir, + LIST_DIR_ROOT_WIDTH, + pathClass, + ); + if (!readable) return '[not readable]'; + const remaining = total - entries.length; + + for (let i = 0; i < entries.length; i++) { + const entry = entries[i]; + if (entry === undefined) continue; + const { name, isDir } = entry; + const isLast = i === entries.length - 1 && remaining === 0; + const connector = isLast ? '└── ' : '├── '; + + if (isDir) { + lines.push(`${connector}${name}/`); + const childPrefix = isLast ? ' ' : '│ '; + const childDir = joinPath(workDir, name, pathClass); + const child = await collectEntries(kaos, childDir, LIST_DIR_CHILD_WIDTH, pathClass); + if (!child.readable) { + lines.push(`${childPrefix}└── [not readable]`); + continue; + } + const childRemaining = child.total - child.entries.length; + for (let j = 0; j < child.entries.length; j++) { + const ce = child.entries[j]; + if (ce === undefined) continue; + const cIsLast = j === child.entries.length - 1 && childRemaining === 0; + const cConnector = cIsLast ? '└── ' : '├── '; + const suffix = ce.isDir ? '/' : ''; + lines.push(`${childPrefix}${cConnector}${ce.name}${suffix}`); + } + if (childRemaining > 0) { + lines.push(`${childPrefix}└── ... and ${String(childRemaining)} more`); + } + } else { + lines.push(`${connector}${name}`); + } + } + + if (remaining > 0) { + lines.push(`└── ... and ${String(remaining)} more entries`); + } + + return lines.length > 0 ? lines.join('\n') : '(empty directory)'; +} + +function joinPath(parent: string, child: string, pathClass: PathClass): string { + return pathMod(pathClass).join(parent, child); +} diff --git a/packages/agent-core/src/tools/support/result-builder.ts b/packages/agent-core/src/tools/support/result-builder.ts new file mode 100644 index 000000000..8618d5671 --- /dev/null +++ b/packages/agent-core/src/tools/support/result-builder.ts @@ -0,0 +1,149 @@ +import type { + ExecutableToolErrorResult, + ExecutableToolSuccessResult, +} from '../../loop/types'; + +const DEFAULT_MAX_CHARS = 50_000; +const DEFAULT_MAX_LINE_LENGTH = 2000; +const TRUNCATION_MARKER = '[...truncated]'; +const TRUNCATION_MESSAGE = 'Output is truncated to fit in the message.'; + +export interface ToolResultBuilderOptions { + readonly maxChars?: number; + readonly maxLineLength?: number | null; +} + +export type ExecutableToolResultBuilderResult = ( + | ExecutableToolSuccessResult + | ExecutableToolErrorResult +) & { + readonly output: string; + readonly message: string; + readonly truncated: boolean; + readonly brief?: string; +}; + +export class ToolResultBuilder { + private readonly maxChars: number; + private readonly maxLineLength: number | null; + + private readonly buffer: string[] = []; + private nCharsValue = 0; + private truncationHappened = false; + + constructor(options: ToolResultBuilderOptions = {}) { + this.maxChars = options.maxChars ?? DEFAULT_MAX_CHARS; + this.maxLineLength = + options.maxLineLength === undefined ? DEFAULT_MAX_LINE_LENGTH : options.maxLineLength; + + if (this.maxLineLength !== null && this.maxLineLength <= TRUNCATION_MARKER.length) { + throw new Error('maxLineLength must be greater than the truncation marker length.'); + } + } + + get nChars(): number { + return this.nCharsValue; + } + + write(text: string): number { + if (this.nCharsValue >= this.maxChars) { + if (text.length > 0 && !this.truncationHappened) { + this.buffer.push(TRUNCATION_MARKER); + this.nCharsValue += TRUNCATION_MARKER.length; + this.truncationHappened = true; + } + return 0; + } + + const lines = text.match(/[^\r\n]*(?:\r\n|[\n\r])|[^\r\n]+/g) ?? []; + if (lines.length === 0) return 0; + + let charsWritten = 0; + for (const originalLine of lines) { + if (this.nCharsValue >= this.maxChars) { + if (!this.truncationHappened) { + this.buffer.push(TRUNCATION_MARKER); + this.nCharsValue += TRUNCATION_MARKER.length; + this.truncationHappened = true; + } + break; + } + + const remainingChars = this.maxChars - this.nCharsValue; + const limit = + this.maxLineLength === null + ? remainingChars + : Math.min(remainingChars, this.maxLineLength); + let line = originalLine; + if (line.length > limit) { + const lineBreak = /[\r\n]+$/.exec(line)?.[0] ?? ''; + const suffix = TRUNCATION_MARKER + lineBreak; + const effectiveMaxLength = Math.max(limit, suffix.length); + line = line.slice(0, effectiveMaxLength - suffix.length) + suffix; + } + if (line !== originalLine) { + this.truncationHappened = true; + } + + this.buffer.push(line); + charsWritten += line.length; + this.nCharsValue += line.length; + } + + return charsWritten; + } + + ok(message = '', options: { readonly brief?: string } = {}): ExecutableToolResultBuilderResult { + let finalMessage = message; + if (finalMessage.length > 0 && !finalMessage.endsWith('.')) { + finalMessage += '.'; + } + if (this.truncationHappened) { + finalMessage = + finalMessage.length === 0 ? TRUNCATION_MESSAGE : `${finalMessage} ${TRUNCATION_MESSAGE}`; + } + + const output = this.buffer.join(''); + const shouldAppendMessage = + finalMessage.length > 0 && (this.truncationHappened || output.length === 0); + return { + isError: false, + output: shouldAppendMessage + ? output.length === 0 + ? finalMessage + : output.endsWith('\n') + ? `${output}${finalMessage}` + : `${output}\n${finalMessage}` + : output, + message: finalMessage, + truncated: this.truncationHappened, + brief: options.brief, + }; + } + + error( + message: string, + options: { readonly brief?: string } = {}, + ): ExecutableToolResultBuilderResult { + const finalMessage = this.truncationHappened + ? message.length === 0 + ? TRUNCATION_MESSAGE + : `${message} ${TRUNCATION_MESSAGE}` + : message; + const output = this.buffer.join(''); + return { + isError: true, + output: + finalMessage.length === 0 + ? output + : output.length === 0 + ? finalMessage + : output.endsWith('\n') + ? `${output}${finalMessage}` + : `${output}\n${finalMessage}`, + message: finalMessage, + truncated: this.truncationHappened, + brief: options.brief, + }; + } +} diff --git a/packages/agent-core/src/tools/support/rg-locator.ts b/packages/agent-core/src/tools/support/rg-locator.ts new file mode 100644 index 000000000..a551c9083 --- /dev/null +++ b/packages/agent-core/src/tools/support/rg-locator.ts @@ -0,0 +1,363 @@ +/** + * rg-locator — hybrid ripgrep binary resolution. + * + * Lookup order (first hit wins): + * 1. System PATH (`which rg`) — fastest, respects developer setup + * 2. Bundled vendor binary (hook; not wired yet — `getVendorRgPath` is a stub) + * 3. `/bin/rg` — persistent cache for this app. + * 4. CDN download to /bin/ — one-off bootstrap + * + * If steps 1-4 all fail, callers receive a structured error they can + * turn into a user-facing "install ripgrep" hint instead of the naked + * `spawn rg ENOENT`. + */ + +import { createHash } from 'node:crypto'; +import { createWriteStream, existsSync } from 'node:fs'; +import { chmod, mkdir, mkdtemp, readFile, rename, rm, stat } from 'node:fs/promises'; +import { homedir, tmpdir } from 'node:os'; +import { basename, join } from 'node:path'; +import { Readable } from 'node:stream'; +import { pipeline } from 'node:stream/promises'; + +import { extract as extractTar } from 'tar'; +import { type Entry, fromBuffer as yauzlFromBuffer } from 'yauzl'; + +import { abortable } from '../../utils/abort'; + +const RG_VERSION = '15.0.0'; +const RG_BASE_URL = 'https://code.kimi.com/kimi-code/rg'; +const DOWNLOAD_TIMEOUT_MS = 600_000; +const RG_ARCHIVE_SHA256: Record = { + 'ripgrep-15.0.0-aarch64-apple-darwin.tar.gz': + '98bb2e61e7277ba0ea72d2ae2592497fd8d2940934a16b122448d302a6637e3b', + 'ripgrep-15.0.0-aarch64-pc-windows-msvc.zip': + '572709c8770cb7f9385d725cb06d2bcd9537ec24d4dd17b1be1d65a876f8b591', + 'ripgrep-15.0.0-aarch64-unknown-linux-gnu.tar.gz': + '15f8cc2fab12d88491c54d49f38589922a9d6a7353c29b0a0856727bcdf80754', + 'ripgrep-15.0.0-x86_64-apple-darwin.tar.gz': + '44128c733d127ddbda461e01225a68b5f9997cfe7635242a797f645ca674a71a', + 'ripgrep-15.0.0-x86_64-pc-windows-msvc.zip': + '21a98bf42c4da97ca543c010e764cc6dec8b9b7538d05f8d21874016385e0860', + 'ripgrep-15.0.0-x86_64-unknown-linux-musl.tar.gz': + '253ad0fd5fef0d64cba56c70dccdacc1916d4ed70ad057cc525fcdb0c3bbd2a7', +}; + +export type RgResolutionSource = + | 'system-path' + | 'vendor' + | 'share-bin-cached' + | 'share-bin-downloaded'; + +export interface RgResolution { + readonly path: string; + readonly source: RgResolutionSource; +} + +export interface EnsureRgPathOptions { + readonly shareDir?: string | undefined; + /** + * Cancels this caller's wait. A shared bootstrap download that is already in + * progress may continue so other callers can still use the same result. + */ + readonly signal?: AbortSignal | undefined; +} + +/** + * Resolve the absolute path to a usable `rg` binary, downloading it + * into `/bin/` if necessary. Multiple concurrent callers are + * serialized by a module-level lock so the download happens at most + * once per process. + */ +export async function ensureRgPath(options: EnsureRgPathOptions = {}): Promise { + options.signal?.throwIfAborted(); + const resolution = resolveRgPath(options.shareDir ?? getShareDir(), options.signal); + return options.signal === undefined ? resolution : abortable(resolution, options.signal); +} + +async function resolveRgPath( + shareDir: string, + signal?: AbortSignal | undefined, +): Promise { + const existing = await findExistingRg(shareDir); + if (existing) return existing; + signal?.throwIfAborted(); + return downloadRgWithLock(shareDir); +} + +/** + * Pure-lookup variant for test harnesses that want to assert on the + * resolution order without triggering a real download. + */ +export async function findExistingRg(shareDir: string): Promise { + const binName = rgBinaryName(); + const systemRg = await whichRg(); + if (systemRg !== undefined) return { path: systemRg, source: 'system-path' }; + const vendorPath = getVendorRgPath(binName); + if (vendorPath !== undefined && (await isExecutableFile(vendorPath))) { + return { path: vendorPath, source: 'vendor' }; + } + const cachePath = join(shareDir, 'bin', binName); + if (await isExecutableFile(cachePath)) { + return { path: cachePath, source: 'share-bin-cached' }; + } + return undefined; +} + +let downloadPromise: Promise | undefined; +async function downloadRgWithLock(shareDir: string): Promise { + if (downloadPromise !== undefined) return downloadPromise; + downloadPromise = (async () => { + try { + const existing = await findExistingRg(shareDir); + if (existing) return existing; + const binPath = await downloadAndInstallRg(shareDir); + return { path: binPath, source: 'share-bin-downloaded' }; + } finally { + downloadPromise = undefined; + } + })(); + return downloadPromise; +} + +function rgBinaryName(): string { + return process.platform === 'win32' ? 'rg.exe' : 'rg'; +} + +function getShareDir(): string { + const override = process.env['KIMI_CODE_HOME']; + if (override !== undefined && override !== '') return override; + return join(homedir(), '.kimi-code'); +} + +function getVendorRgPath(_binName: string): string | undefined { + return undefined; +} + +async function whichRg(): Promise { + const pathEnv = process.env['PATH'] ?? ''; + const sep = process.platform === 'win32' ? ';' : ':'; + const binName = rgBinaryName(); + for (const dir of pathEnv.split(sep)) { + if (dir === '') continue; + const candidate = join(dir, binName); + try { + const st = await stat(candidate); + if (st.isFile()) return candidate; + } catch { + /* not here, try next */ + } + } + return undefined; +} + +async function isExecutableFile(p: string): Promise { + try { + const st = await stat(p); + return st.isFile(); + } catch { + return false; + } +} + +/** @internal for tests — rust-style `--` target triple. */ +export function detectTarget(): string | undefined { + const arch = process.arch === 'x64' ? 'x86_64' : process.arch === 'arm64' ? 'aarch64' : undefined; + if (arch === undefined) return undefined; + + if (process.platform === 'darwin') return `${arch}-apple-darwin`; + if (process.platform === 'linux') { + return arch === 'x86_64' ? 'x86_64-unknown-linux-musl' : 'aarch64-unknown-linux-gnu'; + } + if (process.platform === 'win32') return `${arch}-pc-windows-msvc`; + return undefined; +} + +async function downloadAndInstallRg(shareDir: string): Promise { + const target = detectTarget(); + if (target === undefined) { + throw new Error( + `Unsupported platform/arch for ripgrep download: ${process.platform}/${process.arch}`, + ); + } + + // Windows ripgrep releases ship as `.zip`; macOS / Linux as `.tar.gz`. + // The extraction branch inside the try block handles the format-specific + // unpack; the fetch + download-to-tmp pipeline is identical. + const isWindows = target.includes('windows'); + const archiveExt = isWindows ? 'zip' : 'tar.gz'; + const archiveName = `ripgrep-${RG_VERSION}-${target}.${archiveExt}`; + const expectedSha256 = RG_ARCHIVE_SHA256[archiveName]; + if (expectedSha256 === undefined) { + throw new Error(`No pinned SHA-256 is configured for ripgrep archive ${archiveName}`); + } + const url = `${RG_BASE_URL}/${archiveName}`; + + const binDir = join(shareDir, 'bin'); + await mkdir(binDir, { recursive: true }); + const destination = join(binDir, rgBinaryName()); + + const tmp = await mkdtemp(join(tmpdir(), 'kimi-rg-')); + try { + const archivePath = join(tmp, archiveName); + + const controller = new AbortController(); + const timeoutHandle = setTimeout(() => { + controller.abort(); + }, DOWNLOAD_TIMEOUT_MS); + let resp: Response; + try { + resp = await fetch(url, { signal: controller.signal }); + } finally { + clearTimeout(timeoutHandle); + } + if (!resp.ok || resp.body === null) { + throw new Error(`Failed to download ripgrep: HTTP ${String(resp.status)} ${resp.statusText}`); + } + const write = createWriteStream(archivePath); + // Readable.fromWeb is typed as accepting a web ReadableStream; the + // undici/fetch body matches that shape at runtime. + await pipeline(Readable.fromWeb(resp.body as never), write); + await verifyArchiveChecksum(archivePath, archiveName, expectedSha256); + + if (isWindows) { + await extractRgFromZip(archivePath, destination); + // Windows does not need `chmod +x`: execution is gated by the + // `.exe` extension + NTFS ACLs, which are already correct. + } else { + const extractDir = join(tmp, 'extract'); + await mkdir(extractDir, { recursive: true }); + // tar.gz uses hard-coded prefix because the CDN's tar.gz layout is stable + // and known from upstream releases; zip branch uses basename matching as + // a looser contract so a CDN prefix change doesn't silently fall through. + await extractTar({ + file: archivePath, + cwd: extractDir, + gzip: true, + filter: (entryPath: string) => entryPath.endsWith(`/${rgBinaryName()}`), + }); + const extracted = join(extractDir, `ripgrep-${RG_VERSION}-${target}`, rgBinaryName()); + if (!existsSync(extracted)) { + throw new Error( + `Ripgrep archive did not contain expected binary at ${extracted}. ` + + 'CDN content may have changed.', + ); + } + await rename(extracted, destination); + await chmod(destination, 0o755); + } + return destination; + } finally { + await rm(tmp, { recursive: true, force: true }); + } +} + +/** @internal for tests — fail closed before extracting downloaded bytes. */ +export async function verifyArchiveChecksum( + archivePath: string, + archiveName: string, + expectedSha256: string, +): Promise { + const actualSha256 = createHash('sha256') + .update(await readFile(archivePath)) + .digest('hex'); + if (actualSha256 !== expectedSha256) { + throw new Error( + `Ripgrep archive checksum mismatch for ${archiveName}: expected ${expectedSha256}, ` + + `got ${actualSha256}. CDN content may have changed.`, + ); + } +} + +/** + * Read the downloaded `.zip` at `archivePath`, find the `rg.exe` entry + * (basename match), and stream it out to `destination`. Throws with + * the shared "CDN content may have + * changed" sentinel when the archive holds no matching entry — same + * failure semantics as the tar.gz path's `existsSync(extracted)` gate + * so callers see a single actionable message. + */ +export async function extractRgFromZip(archivePath: string, destination: string): Promise { + const buf = await readFile(archivePath); + const binName = rgBinaryName(); // 'rg.exe' on win32 + await new Promise((resolve, reject) => { + yauzlFromBuffer(buf, { lazyEntries: true }, (openErr, zipfile) => { + if (openErr !== null || zipfile === undefined) { + reject(new Error(`Failed to open ripgrep archive: ${openErr?.message ?? 'unknown error'}`)); + return; + } + let found = false; + const onEntry = (entry: Entry): void => { + // Match on basename (not full path) — keeps the matcher robust + // against CDN repackaging tweaks (e.g. an unexpected + // `ripgrep-X.Y.Z-TARGET/` prefix change). + if (basename(entry.fileName) !== binName) { + zipfile.readEntry(); + return; + } + found = true; + zipfile.openReadStream(entry, (streamErr, stream) => { + if (streamErr !== null) { + reject( + new Error(`Failed to read ${entry.fileName} from archive: ${streamErr.message}`), + ); + zipfile.close(); + return; + } + const out = createWriteStream(destination); + void (async () => { + try { + await pipeline(stream, out); + zipfile.close(); + resolve(); + } catch (error) { + zipfile.close(); + reject(error instanceof Error ? error : new Error(String(error))); + } + })(); + }); + }; + zipfile.on('entry', onEntry); + zipfile.on('end', () => { + // With lazyEntries:true, `end` fires only after readEntry() is called + // for every central-directory entry. We stop calling readEntry() once + // `found` becomes true, so `end` only reaches this branch on the + // not-found path. + if (!found) { + reject( + new Error( + `Ripgrep archive did not contain expected binary '${binName}'. ` + + 'CDN content may have changed.', + ), + ); + } + }); + zipfile.on('error', (err: Error) => { + reject(err); + }); + zipfile.readEntry(); + }); + }); +} + +/** + * User-facing error message to show when `ensureRgPath` throws. Kept + * in one place so the Grep / Glob / Bash plumbing can reuse it. + */ +export function rgUnavailableMessage(cause: unknown): string { + const detail = + cause instanceof Error ? cause.message : typeof cause === 'string' ? cause : 'unknown error'; + const shareBin = join(getShareDir(), 'bin', rgBinaryName()); + return ( + `ripgrep (rg) is not available and the automatic bootstrap failed.\n` + + `\n` + + `Error: ${detail}\n` + + `\n` + + `Fix options:\n` + + ` macOS: brew install ripgrep\n` + + ` Ubuntu: sudo apt-get install ripgrep\n` + + ` Other: https://github.com/BurntSushi/ripgrep#installation\n` + + `\n` + + `Alternatively, drop a static rg binary at ${shareBin}` + ); +} diff --git a/packages/agent-core/src/tools/support/workspace.ts b/packages/agent-core/src/tools/support/workspace.ts new file mode 100644 index 000000000..c108c7c88 --- /dev/null +++ b/packages/agent-core/src/tools/support/workspace.ts @@ -0,0 +1,17 @@ +/** + * WorkspaceConfig — defines the roots that tools are allowed to access. + * + * Injected through each Tool's constructor. Not passed through Runtime: + * the Runtime keeps a small fixed shape and workspace limits live on + * the Tool side. + * + * Paths should already be canonicalized lexically (absolute + normalized); + * callers are responsible for normalizing before constructing this config. + */ + +export interface WorkspaceConfig { + /** Primary workspace directory (absolute, canonicalized). */ + readonly workspaceDir: string; + /** Extra allowed roots (e.g. `--add-dir` CLI flag). */ + readonly additionalDirs: readonly string[]; +} diff --git a/packages/agent-core/src/utils/abort.ts b/packages/agent-core/src/utils/abort.ts new file mode 100644 index 000000000..61e3f1685 --- /dev/null +++ b/packages/agent-core/src/utils/abort.ts @@ -0,0 +1,61 @@ +export function abortError(): Error { + const error = new Error('Aborted'); + error.name = 'AbortError'; + return error; +} + +export function abortable(promise: Promise, signal: AbortSignal): Promise { + signal.throwIfAborted(); + return new Promise((resolve, reject) => { + const onAbort = () => { + reject(abortError()); + }; + signal.addEventListener('abort', onAbort, { once: true }); + promise.then(resolve, reject).finally(() => { + signal.removeEventListener('abort', onAbort); + }); + }); +} + +export function linkAbortSignal(source: AbortSignal, target: AbortController): () => void { + const onAbort = () => { + target.abort(source.reason); + }; + if (source.aborted) { + onAbort(); + return () => {}; + } + source.addEventListener('abort', onAbort, { once: true }); + return () => { + source.removeEventListener('abort', onAbort); + }; +} + +export interface DeadlineAbortSignal { + readonly signal: AbortSignal; + readonly timedOut: () => boolean; + readonly clear: () => void; +} + +export function createDeadlineAbortSignal( + source: AbortSignal, + timeoutMs: number, +): DeadlineAbortSignal { + const controller = new AbortController(); + const unlinkAbortSignal = linkAbortSignal(source, controller); + let didTimeout = false; + let timeout: ReturnType | undefined = setTimeout(() => { + didTimeout = true; + controller.abort(abortError()); + }, timeoutMs); + + return { + signal: controller.signal, + timedOut: () => didTimeout, + clear: () => { + if (timeout !== undefined) clearTimeout(timeout); + timeout = undefined; + unlinkAbortSignal(); + }, + }; +} diff --git a/packages/agent-core/src/utils/completion-budget.ts b/packages/agent-core/src/utils/completion-budget.ts new file mode 100644 index 000000000..abcf027bb --- /dev/null +++ b/packages/agent-core/src/utils/completion-budget.ts @@ -0,0 +1,153 @@ +import type { + ChatProvider, + Message, + ModelCapability, + Tool, +} from '@moonshot-ai/kosong'; + +import { + estimateTokens, + estimateTokensForMessages, + estimateTokensForTools, +} from './tokens'; + +/** + * Desired completion-token budget for the next LLM step. + * + * The budget is a request, not a guarantee: it is clamped against the + * current input size and the model's context window before being applied + * to the provider. This avoids two failure modes for Kimi reasoning + * models: + * 1. A small cap can return HTTP 200 with empty `content` because the + * whole budget was spent on `reasoning_content`. + * 2. A large cap may exceed the remaining context window and trigger + * `Invalid request: Your request exceeded model token limit`. + */ +export interface CompletionBudget { + /** Desired completion budget when the model context window allows it. */ + readonly desired: number; + /** + * Safety margin reserved between current input and the context limit, + * to absorb tokenizer estimation error and provider-side overhead. + */ + readonly safetyMargin?: number | undefined; +} + +const MIN_FLOOR = 1; +const DEFAULT_SAFETY_MARGIN = 1024; +const DEFAULT_DESIRED_BUDGET = 32000; + +/** + * Resolve the completion budget for a turn from configuration and Kimi + * environment variables. + * + * Priority (first wins): `KIMI_MODEL_MAX_COMPLETION_TOKENS`, + * `KIMI_MODEL_MAX_TOKENS` (legacy alias), `reservedContextSize`, + * `DEFAULT_DESIRED_BUDGET` (32000, preserves pre-PR-2332 behavior). + * + * Operators can opt out of clamping entirely by setting the env var to + * `0` or a negative integer; in that case this function returns + * `undefined`, which `applyCompletionBudget` treats as a no-op. + */ +export function resolveCompletionBudget(args: { + readonly reservedContextSize?: number | undefined; + readonly env?: NodeJS.ProcessEnv | undefined; +}): CompletionBudget | undefined { + const env = args.env ?? process.env; + const fromNew = parseEnvBudget(env['KIMI_MODEL_MAX_COMPLETION_TOKENS']); + if (fromNew !== 'absent') { + return fromNew === 'disabled' ? undefined : { desired: fromNew }; + } + const fromLegacy = parseEnvBudget(env['KIMI_MODEL_MAX_TOKENS']); + if (fromLegacy !== 'absent') { + return fromLegacy === 'disabled' ? undefined : { desired: fromLegacy }; + } + if (args.reservedContextSize !== undefined && args.reservedContextSize > 0) { + return { desired: args.reservedContextSize }; + } + return { desired: DEFAULT_DESIRED_BUDGET }; +} + +type EnvBudget = number | 'disabled' | 'absent'; + +function parseEnvBudget(raw: string | undefined): EnvBudget { + if (raw === undefined || raw === '') return 'absent'; + const n = Number(raw); + if (!Number.isFinite(n) || !Number.isInteger(n)) return 'absent'; + if (n <= 0) return 'disabled'; + return n; +} + +/** + * Compute the effective `max_completion_tokens` cap for the next request. + * + * cap = clamp(desired, MIN_FLOOR, max_context_tokens - input - safetyMargin) + * + * `input` accounts for everything the provider will actually serialize: + * the conversation history, the system prompt, and the tool schemas. + * Counting only `messages` underestimates by enough to push a near-limit + * request past the model context window. + * + * When the model context size is unknown, the desired value is returned + * unchanged (floored at `MIN_FLOOR`). + * + * When the remaining window is non-positive (input already at or above + * the limit), `MIN_FLOOR` is returned — we can't honor a meaningful cap + * and the API will surface the overflow on its own. + * + * Note: the floor never exceeds `remaining`, so a near-full context + * cannot be pushed past the limit by `MIN_FLOOR` itself. + */ +export function computeCompletionBudgetCap(args: { + readonly budget: CompletionBudget; + readonly capability: ModelCapability | undefined; + readonly messages: readonly Message[]; + readonly systemPrompt?: string | undefined; + readonly tools?: readonly Tool[] | undefined; +}): number { + const desired = args.budget.desired; + const safetyMargin = args.budget.safetyMargin ?? DEFAULT_SAFETY_MARGIN; + const maxCtx = args.capability?.max_context_tokens ?? 0; + if (maxCtx <= 0) { + return Math.max(MIN_FLOOR, desired); + } + const input = + estimateTokensForMessages([...args.messages]) + + estimateTokens(args.systemPrompt ?? '') + + estimateTokensForTools(args.tools ?? []); + const remaining = maxCtx - input - safetyMargin; + if (remaining <= 0) { + return MIN_FLOOR; + } + return Math.max(MIN_FLOOR, Math.min(desired, remaining)); +} + +/** + * Apply a completion budget to a provider via its optional + * `withMaxCompletionTokens` capability. Returns the original provider + * unchanged when no budget is configured or the provider opts out. + * + * The returned provider is intentionally a shallow clone that shares the + * original's HTTP client. Callers MUST treat it as a single-step value + * and NOT persist it back to durable agent state — see the F3 discussion + * in `KimiChatProvider._clone()`. + */ +export function applyCompletionBudget(args: { + readonly provider: ChatProvider; + readonly budget: CompletionBudget | undefined; + readonly capability: ModelCapability | undefined; + readonly messages: readonly Message[]; + readonly systemPrompt?: string | undefined; + readonly tools?: readonly Tool[] | undefined; +}): ChatProvider { + if (args.budget === undefined) return args.provider; + if (args.provider.withMaxCompletionTokens === undefined) return args.provider; + const cap = computeCompletionBudgetCap({ + budget: args.budget, + capability: args.capability, + messages: args.messages, + systemPrompt: args.systemPrompt, + tools: args.tools, + }); + return args.provider.withMaxCompletionTokens(cap); +} diff --git a/packages/agent-core/src/utils/environment.ts b/packages/agent-core/src/utils/environment.ts new file mode 100644 index 000000000..a69065a1b --- /dev/null +++ b/packages/agent-core/src/utils/environment.ts @@ -0,0 +1,186 @@ +/** + * Environment — cross-platform probe of OS / shell. + * + * Detection is a pure function of injected probes (`platform` / `arch` / + * `release` / `env` / `isFile` / `findExecutable`) so the same suite runs + * identically on any host OS. `detectEnvironmentFromNode()` bundles the + * Node defaults for production callers. + * + * On Windows the probe expects Git Bash (the canonical POSIX shell that + * ships with Git for Windows). If it cannot be located the function + * throws `KimiError` with code `shell.git_bash_not_found`; the SDK layer + * can wrap that into a user-facing install hint. Set `KIMI_SHELL_PATH` + * to override. + */ + +import { constants as fsConstants } from 'node:fs'; +import { access } from 'node:fs/promises'; +import * as nodeOs from 'node:os'; + +import { ErrorCodes, KimiError } from '#/errors'; + +// `OsKind` carries 'macOS' / 'Linux' / 'Windows' for known platforms and +// falls back to the raw `process.platform` string for unknown ones (e.g. +// 'freebsd'). Typed as `string` so the union isn't inhabited-by-string. +export type OsKind = string; +export type ShellName = 'bash' | 'sh'; + +export interface Environment { + readonly osKind: OsKind; + readonly osArch: string; + readonly osVersion: string; + readonly shellName: ShellName; + readonly shellPath: string; +} + +export interface EnvironmentDeps { + // Accepts the full Node `Platform` enum plus arbitrary strings for + // forward-compatible OS kinds. + readonly platform: string; + readonly arch: string; + readonly release: string; + readonly env: Record; + readonly isFile: (path: string) => Promise; + readonly findExecutable: (name: string) => Promise; +} + +function resolveOsKind(platform: string): OsKind { + switch (platform) { + case 'darwin': + return 'macOS'; + case 'linux': + return 'Linux'; + case 'win32': + return 'Windows'; + default: + return platform; + } +} + +export async function detectEnvironment(deps: EnvironmentDeps): Promise { + const osKind = resolveOsKind(deps.platform); + const osArch = deps.arch; + const osVersion = deps.release; + + if (deps.platform === 'win32') { + const shellPath = await locateWindowsGitBash(deps); + return { osKind, osArch, osVersion, shellName: 'bash', shellPath }; + } + + const candidates: readonly string[] = ['/bin/bash', '/usr/bin/bash', '/usr/local/bin/bash']; + let found: string | undefined; + for (const p of candidates) { + if (await deps.isFile(p)) { + found = p; + break; + } + } + if (found !== undefined) { + return { osKind, osArch, osVersion, shellName: 'bash', shellPath: found }; + } + return { osKind, osArch, osVersion, shellName: 'sh', shellPath: '/bin/sh' }; +} + +async function locateWindowsGitBash(deps: EnvironmentDeps): Promise { + const checked: string[] = []; + + const override = deps.env['KIMI_SHELL_PATH']?.trim(); + if (override !== undefined && override.length > 0) { + checked.push(override); + if (await deps.isFile(override)) { + return override; + } + } + + const gitExe = await deps.findExecutable('git.exe'); + if (gitExe !== undefined) { + const inferred = inferGitBashFromGitExe(gitExe); + if (inferred !== undefined) { + checked.push(inferred); + if (await deps.isFile(inferred)) { + return inferred; + } + } + } + + const candidates: string[] = [ + 'C:\\Program Files\\Git\\bin\\bash.exe', + 'C:\\Program Files (x86)\\Git\\bin\\bash.exe', + ]; + const localAppData = deps.env['LOCALAPPDATA']?.trim(); + if (localAppData !== undefined && localAppData.length > 0) { + candidates.push(`${localAppData}\\Programs\\Git\\bin\\bash.exe`); + } + for (const candidate of candidates) { + checked.push(candidate); + if (await deps.isFile(candidate)) { + return candidate; + } + } + + throw new KimiError( + ErrorCodes.SHELL_GIT_BASH_NOT_FOUND, + `Git Bash was not found on this Windows host. Install Git for Windows from https://gitforwindows.org/ or set KIMI_SHELL_PATH to a bash.exe. Checked: ${checked.join(', ')}.`, + ); +} + +// Most Git for Windows installs put `git.exe` in `\cmd\git.exe`, +// with bash at `\bin\bash.exe`. Portable installs sometimes put +// both in `\bin\`. Walk back to the parent of `cmd` / `bin` and +// re-anchor under `bin\bash.exe`. +function inferGitBashFromGitExe(gitExe: string): string | undefined { + const sep = gitExe.includes('\\') ? '\\' : '/'; + const parts = gitExe.split(sep); + for (let i = parts.length - 2; i >= 0; i -= 1) { + const segment = parts[i]; + if (segment === 'cmd' || segment === 'bin') { + const root = parts.slice(0, i).join(sep); + return root.length === 0 ? `bin${sep}bash.exe` : `${root}${sep}bin${sep}bash.exe`; + } + } + return undefined; +} + +/** + * Production convenience — derive the deps bag from Node's ambient surface. + */ +export async function detectEnvironmentFromNode(): Promise { + const platform = process.platform; + const env = process.env as Record; + const isFile = async (path: string): Promise => { + try { + await access(path, fsConstants.F_OK); + return true; + } catch { + return false; + } + }; + return detectEnvironment({ + platform, + arch: process.arch, + release: nodeOs.release(), + env, + isFile, + findExecutable: (name: string) => findExecutableOnPath(name, env['PATH'], platform, isFile), + }); +} + +async function findExecutableOnPath( + name: string, + pathEnv: string | undefined, + platform: string, + isFile: (p: string) => Promise, +): Promise { + if (pathEnv === undefined || pathEnv.length === 0) return undefined; + const listSep = platform === 'win32' ? ';' : ':'; + const dirSep = platform === 'win32' ? '\\' : '/'; + for (const rawDir of pathEnv.split(listSep)) { + const dir = rawDir.trim(); + if (dir.length === 0) continue; + const candidate = dir.endsWith(dirSep) ? `${dir}${name}` : `${dir}${dirSep}${name}`; + if (await isFile(candidate)) { + return candidate; + } + } + return undefined; +} diff --git a/packages/agent-core/src/utils/fs.ts b/packages/agent-core/src/utils/fs.ts new file mode 100644 index 000000000..79aef2e49 --- /dev/null +++ b/packages/agent-core/src/utils/fs.ts @@ -0,0 +1,187 @@ +/** + * Low-level POSIX durability primitives. + * + * Two concerns that every durable write must handle: + * 1. file *contents* — solved by `fh.sync()` after the write + * 2. directory *entries* — solved by opening the parent directory and + * calling `fh.sync()` on the directory handle + * + * `fh.sync()` on a file does NOT guarantee that the directory entry + * pointing at that file has been committed. On POSIX a crash between + * the file-content fsync and the parent-directory fsync can leave the + * file's bytes on disk with no visible name. The primary durable path + * is POSIX; Windows is best-effort — NTFS's MoveFileEx commits the + * dirent inside the file fsync, so a separate directory fsync is a + * no-op (and EISDIR-fails on `open(dir, 'r')`). + */ +import { randomBytes } from 'node:crypto'; +import { closeSync, fsyncSync, openSync } from 'node:fs'; +import * as nodeFs from 'node:fs'; +import { open, rename, unlink } from 'node:fs/promises'; +import { dirname } from 'node:path'; + +/** + * Open a directory read-only and fsync it, then close. Used to make a + * freshly-created or renamed file's directory entry durable. + * + * Windows: noop. `open(dir, 'r')` throws EISDIR, and NTFS commits the + * dirent transaction inside the file fsync anyway — the separate dir + * fsync would buy nothing even if we could issue it. + */ +export async function syncDir(dirPath: string): Promise { + if (process.platform === 'win32') return; + const dirFh = await open(dirPath, 'r'); + try { + await dirFh.sync(); + } finally { + await dirFh.close(); + } +} +/** + * Synchronous variant of `syncDir`. Used by batched drain paths where a + * single timer fire needs to be an atomic event-loop step. Windows + * mirrors the async variant — noop. + */ +export function syncDirSync(dirPath: string): void { + if (process.platform === 'win32') return; + const fd = openSync(dirPath, 'r'); + try { + fsyncSync(fd); + } finally { + closeSync(fd); + } +} +/** + * Write `content` to `filePath` atomically and durably: + * 1. Write content to `.tmp`, fsync it, close it. + * 2. Rename `.tmp` → `filePath` (atomic on POSIX). + * 3. fsync the parent directory so the rename is durable. + * + * On any failure before the rename the `.tmp` file is removed so the + * caller's directory is not left with a half-written leftover. A + * failure *after* the rename (i.e. in the parent-directory fsync) is + * surfaced to the caller — the content is already in place, but + * durability is not guaranteed. + */ +export async function writeFileAtomicDurable( + filePath: string, + content: string | Uint8Array, +): Promise { + const tmpPath = filePath + '.tmp'; + let renamed = false; + try { + const fh = await open(tmpPath, 'w'); + try { + await fh.writeFile(content); + await fh.sync(); + } finally { + await fh.close(); + } + // Windows pre-unlink for MoveFileEx parity. + if (process.platform === 'win32') { + try { + await unlink(filePath); + } catch (error) { + const code = (error as NodeJS.ErrnoException).code; + if (code !== 'ENOENT') throw error; + } + } + await rename(tmpPath, filePath); + renamed = true; + await syncDir(dirname(filePath)); + } finally { + if (!renamed) { + // Best-effort cleanup of the `.tmp` file if we never got to the + // rename. Swallow ENOENT because the file may not exist (open + // itself failed) or may already have been unlinked. + try { + await unlink(tmpPath); + } catch { + /* ignore */ + } + } + } +} + +/** + * atomicWrite — cross-platform atomic file replacement. + * + * Guarantees that readers never observe a half-written file: + * 1. Write content to a uniquely-named temp file in the same directory. + * 2. fsync the temp file so the bytes are durable. + * 3. rename(tmp, target) — atomic on POSIX. + * 4. On any failure before the rename, unlink the temp file (best effort). + * + * Does NOT fsync the parent directory; callers that need full POSIX + * crash durability should `await syncDir(dirname(path))` after this call. + * + * NOT suitable for append-only paths (wire.jsonl). Those use + * `JournalWriter.append()` which writes at the current file position. + */ + +/** + * fsync a file descriptor using the callback-based `fs.fsync`. We go + * through the module namespace (`nodeFs.fsync`) rather than + * `FileHandle.sync()` so vitest's `vi.spyOn(fs, 'fsync')` can + * intercept the call for fault-injection tests. + */ +function syncFd(fd: number): Promise { + return new Promise((resolve, reject) => { + nodeFs.fsync(fd, (err) => { + if (err) { + reject(err); + return; + } + resolve(); + }); + }); +} + +/** + * Atomically write `content` to `filePath`. If the target already exists + * it is replaced; if it does not exist it is created. + * + * @param filePath — absolute or relative path to the target file. + * @param content — string or binary payload to write. + * @param _syncOverride — test seam: override the fsync implementation for + * fault injection. Production callers must never supply this. + */ +export async function atomicWrite( + filePath: string, + content: string | Uint8Array, + _syncOverride?: (fd: number) => Promise, +): Promise { + const hex = randomBytes(4).toString('hex'); + const tmpPath = `${filePath}.tmp.${process.pid}.${hex}`; + let renamed = false; + try { + const fh = await open(tmpPath, 'w'); + try { + await fh.writeFile(content); + await (_syncOverride ?? syncFd)(fh.fd); + } finally { + await fh.close(); + } + // Windows `fs.rename` maps to MoveFileEx and fails with EPERM if + // the target is held by another handle. Pre-unlinking + // before the rename turns this into the POSIX-style "replace" case. + if (process.platform === 'win32') { + try { + await unlink(filePath); + } catch (error) { + const code = (error as NodeJS.ErrnoException).code; + if (code !== 'ENOENT') throw error; + } + } + await rename(tmpPath, filePath); + renamed = true; + } finally { + if (!renamed) { + try { + await unlink(tmpPath); + } catch { + /* ignore — file may not exist if open itself failed */ + } + } + } +} diff --git a/packages/agent-core/src/utils/hero-slug.ts b/packages/agent-core/src/utils/hero-slug.ts new file mode 100644 index 000000000..e460a7806 --- /dev/null +++ b/packages/agent-core/src/utils/hero-slug.ts @@ -0,0 +1,267 @@ +/** Hero-name slug generator: produces "hero-hero-hero" with collision fallback. */ + +import { randomInt } from 'node:crypto'; + +export const HERO_NAMES = [ + // --- Marvel --- + 'iron-man', + 'spider-man', + 'captain-america', + 'thor', + 'hulk', + 'black-widow', + 'hawkeye', + 'black-panther', + 'doctor-strange', + 'scarlet-witch', + 'vision', + 'falcon', + 'war-machine', + 'ant-man', + 'wasp', + 'captain-marvel', + 'gamora', + 'star-lord', + 'groot', + 'rocket', + 'drax', + 'mantis', + 'nebula', + 'shang-chi', + 'moon-knight', + 'ms-marvel', + 'she-hulk', + 'echo', + 'wolverine', + 'cyclops', + 'storm', + 'jean-grey', + 'rogue', + 'beast', + 'nightcrawler', + 'colossus', + 'shadowcat', + 'jubilee', + 'cable', + 'deadpool', + 'bishop', + 'magik', + 'iceman', + 'archangel', + 'psylocke', + 'dazzler', + 'forge', + 'havok', + 'polaris', + 'emma-frost', + 'namor', + 'silver-surfer', + 'adam-warlock', + 'nova', + 'quasar', + 'sentry', + 'blue-marvel', + 'spectrum', + 'squirrel-girl', + 'cloak', + 'dagger', + 'punisher', + 'elektra', + 'luke-cage', + 'iron-fist', + 'jessica-jones', + 'daredevil', + 'blade', + 'ghost-rider', + 'morbius', + 'venom', + 'carnage', + 'silk', + 'spider-gwen', + 'miles-morales', + 'america-chavez', + 'kate-bishop', + 'yelena-belova', + 'white-tiger', + 'moon-girl', + 'devil-dinosaur', + 'amadeus-cho', + 'riri-williams', + 'kamala-khan', + 'sam-alexander', + 'nova-prime', + 'medusa', + 'black-bolt', + 'crystal', + 'karnak', + 'gorgon', + 'lockjaw', + 'quake', + 'mockingbird', + 'bobbi-morse', + 'maria-hill', + 'nick-fury', + 'phil-coulson', + 'winter-soldier', + 'us-agent', + 'patriot', + 'speed', + 'wiccan', + 'hulkling', + 'stature', + 'yellowjacket', + 'tigra', + 'hellcat', + 'valkyrie', + 'sif', + 'beta-ray-bill', + 'hercules', + 'wonder-man', + 'taskmaster', + 'domino', + 'cannonball', + 'sunspot', + 'wolfsbane', + 'warpath', + 'multiple-man', + 'banshee', + 'siryn', + 'monet', + 'rictor', + 'shatterstar', + 'longshot', + 'daken', + 'x-23', + 'fantomex', + // --- DC --- + 'batman', + 'superman', + 'wonder-woman', + 'flash', + 'aquaman', + 'green-lantern', + 'martian-manhunter', + 'cyborg', + 'hawkgirl', + 'green-arrow', + 'black-canary', + 'zatanna', + 'constantine', + 'shazam', + 'blue-beetle', + 'booster-gold', + 'firestorm', + 'atom', + 'hawkman', + 'plastic-man', + 'red-tornado', + 'starfire', + 'raven', + 'beast-boy', + 'robin', + 'nightwing', + 'batgirl', + 'batwoman', + 'red-hood', + 'signal', + 'orphan', + 'spoiler', + 'catwoman', + 'huntress', + 'supergirl', + 'superboy', + 'power-girl', + 'steel', + 'stargirl', + 'wildcat', + 'doctor-fate', + 'mister-terrific', + 'hourman', + 'sandman', + 'spectre', + 'phantom-stranger', + 'swamp-thing', + 'animal-man', + 'deadman', + 'vixen', + 'black-lightning', + 'static', + 'icon', + 'rocket-dc', + 'captain-atom', + 'fire', + 'ice', + 'elongated-man', + 'metamorpho', + 'black-hawk', + 'crimson-avenger', + 'doctor-mid-nite', + 'jakeem-thunder', + 'mister-miracle', + 'big-barda', + 'orion', + 'lightray', + 'forager', + 'killer-frost', + 'jessica-cruz', + 'simon-baz', + 'john-stewart', + 'guy-gardner', + 'kyle-rayner', + 'hal-jordan', + 'wally-west', + 'barry-allen', + 'jay-garrick', + 'impulse', + 'kid-flash', + 'donna-troy', + 'tempest', + 'aqualad', + 'miss-martian', + 'terra', + 'jericho', + 'ravager', + 'red-star', + 'pantha', + 'argent', + 'damage', + 'jade', + 'obsidian', + 'cyclone', + 'atom-smasher', + 'maxima', + 'starman', + 'liberty-belle', + 'dove', + 'hawk', + 'blue-devil', + 'creeper', + 'ragman', + 'thunder', +] as const satisfies readonly [string, ...string[]]; + +const MAX_ATTEMPTS = 20; + +function pickHero(): string { + return HERO_NAMES[randomInt(HERO_NAMES.length)]!; +} + +function assembleSlug(): string { + return `${pickHero()}-${pickHero()}-${pickHero()}`; +} + +export function generateHeroSlug(id: string, existing: Set): string { + let slug = ''; + let collided = true; + for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) { + slug = assembleSlug(); + if (!existing.has(slug)) { + collided = false; + break; + } + } + if (collided) { + slug = `${slug}-${id.slice(0, 8)}`; + } + return slug; +} diff --git a/packages/agent-core/src/utils/render-prompt.ts b/packages/agent-core/src/utils/render-prompt.ts new file mode 100644 index 000000000..579665e8c --- /dev/null +++ b/packages/agent-core/src/utils/render-prompt.ts @@ -0,0 +1,19 @@ +import nunjucks from 'nunjucks'; + +/** + * Shared prompt template renderer. + * + * All prompt templates (system prompt, tool descriptions, compaction + * instruction, ...) use nunjucks `{{ var }}` / `{% if %}` syntax and render + * through this one function. + * + * - `autoescape: false` — prompt text is not HTML; `<`, `>`, `&` must pass + * through verbatim. + * - `throwOnUndefined: true` — a missing variable is a loud error, never a + * silently leaked `{{ placeholder }}` in the text sent to the model. + */ +const env = new nunjucks.Environment(null, { autoescape: false, throwOnUndefined: true }); + +export function renderPrompt(template: string, vars: Record): string { + return env.renderString(template, vars); +} diff --git a/packages/agent-core/src/utils/tokens.ts b/packages/agent-core/src/utils/tokens.ts new file mode 100644 index 000000000..5ccef7d00 --- /dev/null +++ b/packages/agent-core/src/utils/tokens.ts @@ -0,0 +1,63 @@ +import type { ContentPart, Message, Tool } from '@moonshot-ai/kosong'; + +/** + * Estimate token count from text using a character-based heuristic. + * - ASCII (~4 chars per token) + * - CJK and other non-ASCII (~1 char per token) + * The estimate is transient — the next LLM call returns the real count + * and supersedes this value. Used to keep `tokenCountWithPending` + * monotonic between LLM round-trips without paying for a tokenizer. + */ +export function estimateTokens(text: string): number { + let asciiCount = 0; + let nonAsciiCount = 0; + for (const char of text) { + if (char.codePointAt(0)! <= 127) { + asciiCount++; + } else { + nonAsciiCount++; + } + } + return Math.ceil(asciiCount / 4) + nonAsciiCount; +} + +export function estimateTokensForMessages(messages: Message[]): number { + let total = 0; + for (const message of messages) { + total += estimateTokensForMessage(message); + } + return total; +} + +export function estimateTokensForTools(tools: readonly Tool[]): number { + let total = 0; + for (const tool of tools) { + total += estimateTokens(tool.name); + total += estimateTokens(tool.description); + total += estimateTokens(JSON.stringify(tool.parameters)); + } + return total; +} + +export function estimateTokensForMessage(message: Message): number { + let total = estimateTokens(message.role); + for (const part of message.content) { + total += estimateTokensForContentPart(part); + } + if (message.toolCalls !== undefined) { + for (const call of message.toolCalls) { + total += estimateTokens(call.function.name); + total += estimateTokens(JSON.stringify(call.function.arguments)); + } + } + return total; +} + +export function estimateTokensForContentPart(part: ContentPart): number { + if (part.type === 'text') { + return estimateTokens(part.text); + } else if (part.type === 'think') { + return estimateTokens(part.think); + } + return 0; +} diff --git a/packages/agent-core/src/utils/types.ts b/packages/agent-core/src/utils/types.ts new file mode 100644 index 000000000..45a0d1c9d --- /dev/null +++ b/packages/agent-core/src/utils/types.ts @@ -0,0 +1,13 @@ +export type Promisify = [T] extends [Promise] ? T : Promise; +export type PromisifyMethods = { + [K in keyof T]: T[K] extends (...args: infer Args) => infer Return + ? (...args: Args) => Promisify + : never; +}; + +export type Promisable = [T] extends [Promise] ? T | Awaited : T | Promise; +export type PromisableMethods = { + [K in keyof T]: T[K] extends (...args: infer Args) => infer Return + ? (...args: Args) => Promisable + : never; +}; diff --git a/packages/agent-core/src/utils/workdir-slug.ts b/packages/agent-core/src/utils/workdir-slug.ts new file mode 100644 index 000000000..5f8da29a6 --- /dev/null +++ b/packages/agent-core/src/utils/workdir-slug.ts @@ -0,0 +1,11 @@ +const MAX_WORKDIR_SLUG_LENGTH = 40; + +export function slugifyWorkDirName(name: string): string { + const slug = name + .toLowerCase() + .replaceAll(/[^a-z0-9._-]+/g, '-') + .replaceAll(/^-+|-+$/g, '') + .slice(0, MAX_WORKDIR_SLUG_LENGTH) + .replaceAll(/^-+|-+$/g, ''); + return slug === '' || slug === '.' || slug === '..' ? 'workspace' : slug; +} diff --git a/packages/agent-core/src/version.ts b/packages/agent-core/src/version.ts new file mode 100644 index 000000000..5436e6c6f --- /dev/null +++ b/packages/agent-core/src/version.ts @@ -0,0 +1,13 @@ +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; + +export function getCoreVersion(): string { + try { + const pkgUrl = new URL('../package.json', import.meta.url); + const raw = readFileSync(fileURLToPath(pkgUrl), 'utf-8'); + const pkg = JSON.parse(raw) as { version?: unknown }; + return typeof pkg.version === 'string' ? pkg.version : '0.0.0'; + } catch { + return '0.0.0'; + } +} diff --git a/packages/agent-core/test/agent/background-manager.test.ts b/packages/agent-core/test/agent/background-manager.test.ts new file mode 100644 index 000000000..882fb9016 --- /dev/null +++ b/packages/agent-core/test/agent/background-manager.test.ts @@ -0,0 +1,628 @@ +/** + * Covers: BackgroundManager (the agent-aware subclass). + * + * Confirms that BPM lifecycle transitions are translated into + * agent.emitEvent({ type: 'background.task.*' }) so SDK / TUI + * subscribers can react in real time without polling. + */ + +import { mkdtemp, rm } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { Readable } from 'node:stream'; +import type { Writable } from 'node:stream'; + +import type { KaosProcess } from '@moonshot-ai/kaos'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import { BackgroundManager } from '../../src/agent/background'; +import type { AgentEvent } from '../../src/rpc/events'; +import { appendTaskOutput, writeTask } from '../../src/tools/background/persist'; + +interface FakeAgent { + emitEvent: (event: AgentEvent) => void; + emittedEvents: AgentEvent[]; + hooks?: { fireAndForgetTrigger: ReturnType }; + turn: { + hasActiveTurn: boolean; + waitForCurrentTurn: () => Promise; + steer: (...args: unknown[]) => number | null; + }; + context: { appendUserMessage: (...args: unknown[]) => void }; + records: { restoring: boolean; logRecord: (record: unknown) => void }; + telemetry: { track: ReturnType }; + background: BackgroundManager; +} + +function makeAgent(options: { hooks?: FakeAgent['hooks'] } = {}): FakeAgent { + const emitted: AgentEvent[] = []; + const agent = { + emittedEvents: emitted, + emitEvent: (event: AgentEvent) => { + emitted.push(event); + }, + hooks: options.hooks, + turn: { + hasActiveTurn: false, + waitForCurrentTurn: vi.fn(() => Promise.resolve()), + steer: vi.fn(() => 1), + }, + context: { appendUserMessage: vi.fn() }, + records: { restoring: false, logRecord: vi.fn() }, + telemetry: { track: vi.fn() }, + } as unknown as FakeAgent; + const manager = new BackgroundManager(agent as never); + agent.background = manager; + return agent; +} + +function immediateProcess(exitCode: number): KaosProcess { + return { + stdin: { write: vi.fn(), end: vi.fn() } as unknown as Writable, + stdout: Readable.from([]), + stderr: Readable.from([]), + pid: 30000 + exitCode, + exitCode, + wait: vi.fn().mockResolvedValue(exitCode) as KaosProcess['wait'], + kill: vi.fn().mockResolvedValue(undefined) as KaosProcess['kill'], + }; +} + +function pendingProcess(): KaosProcess { + let resolveWait: (code: number) => void = () => {}; + const waitPromise = new Promise((res) => { + resolveWait = res; + }); + let currentExitCode: number | null = null; + return { + stdin: { write: vi.fn(), end: vi.fn() } as unknown as Writable, + stdout: Readable.from([]), + stderr: Readable.from([]), + pid: 99999, + get exitCode(): number | null { + return currentExitCode; + }, + wait: () => waitPromise, + kill: vi.fn(async () => { + if (currentExitCode === null) { + currentExitCode = 143; + resolveWait(143); + } + }) as unknown as KaosProcess['kill'], + }; +} + +describe('BackgroundManager — RPC event emission', () => { + let agent: FakeAgent; + + beforeEach(() => { + agent = makeAgent(); + }); + + afterEach(() => { + agent.background._reset(); + }); + + it('emits background.task.started on register()', () => { + const taskId = agent.background.register(pendingProcess(), 'sleep 60', 'demo'); + + const started = agent.emittedEvents.filter((e) => e.type === 'background.task.started'); + expect(started.length).toBe(1); + expect(started[0]!.info.taskId).toBe(taskId); + expect(started[0]!.info.status).toBe('running'); + expect(agent.telemetry.track).toHaveBeenCalledWith('background_task_created', { + kind: 'bash', + }); + }); + + it('emits background.task.started on registerAgentTask()', () => { + const taskId = agent.background.registerAgentTask(new Promise(() => {}), 'agent task'); + + const started = agent.emittedEvents.filter((e) => e.type === 'background.task.started'); + expect(started.length).toBe(1); + expect(started[0]!.info.taskId).toBe(taskId); + expect(taskId).toMatch(/^agent-/); + expect(agent.telemetry.track).toHaveBeenCalledWith('background_task_created', { + kind: 'agent', + }); + }); + + it('emits background.task.updated on awaiting_approval transitions', () => { + const taskId = agent.background.register(pendingProcess(), 'sleep', 'demo'); + agent.emittedEvents.length = 0; + + agent.background.markAwaitingApproval(taskId, 'needs approval'); + agent.background.clearAwaitingApproval(taskId); + + const updated = agent.emittedEvents.filter((e) => e.type === 'background.task.updated'); + expect(updated.length).toBe(2); + expect(updated[0]!.info.status).toBe('awaiting_approval'); + expect(updated[1]!.info.status).toBe('running'); + }); + + it('emits background.task.terminated on natural exit', async () => { + agent.background.register(immediateProcess(0), 'echo', 'done'); + await new Promise((r) => setTimeout(r, 20)); + + const terminated = agent.emittedEvents.filter((e) => e.type === 'background.task.terminated'); + expect(terminated.length).toBe(1); + expect(terminated[0]!.info.status).toBe('completed'); + }); + + it('tracks successful task completion with duration and no reason', async () => { + const taskId = agent.background.register(immediateProcess(0), 'echo ok', 'done'); + agent.telemetry.track.mockClear(); + + await agent.background.waitForTerminal(taskId); + + expect(agent.telemetry.track).toHaveBeenCalledWith( + 'background_task_completed', + expect.objectContaining({ + kind: 'bash', + success: true, + duration_s: expect.any(Number), + }), + ); + expect(agent.telemetry.track.mock.calls[0]?.[1]).not.toHaveProperty('reason'); + }); + + it('tracks failed task completion with reason=error', async () => { + const taskId = agent.background.register(immediateProcess(1), 'false', 'failed'); + agent.telemetry.track.mockClear(); + + await agent.background.waitForTerminal(taskId); + + expect(agent.telemetry.track).toHaveBeenCalledWith( + 'background_task_completed', + expect.objectContaining({ + kind: 'bash', + success: false, + reason: 'error', + duration_s: expect.any(Number), + }), + ); + }); + + it('tracks timed-out agent tasks with reason=timeout', async () => { + const taskId = agent.background.registerAgentTask(new Promise(() => {}), 'slow agent', { + timeoutMs: 1, + }); + agent.telemetry.track.mockClear(); + + await agent.background.waitForTerminal(taskId); + + expect(agent.telemetry.track).toHaveBeenCalledWith( + 'background_task_completed', + expect.objectContaining({ + kind: 'agent', + success: false, + reason: 'timeout', + duration_s: expect.any(Number), + }), + ); + }); + + it('emits background.task.terminated on stop()', async () => { + const taskId = agent.background.register(pendingProcess(), 'sleep 60', 'long'); + agent.emittedEvents.length = 0; + + await agent.background.stop(taskId, 'user'); + + const terminated = agent.emittedEvents.filter((e) => e.type === 'background.task.terminated'); + expect(terminated.length).toBe(1); + expect(terminated[0]!.info.status).toBe('killed'); + }); + + it('emits background.task.terminated when a restored task is marked lost', async () => { + const sessionDir = await mkdtemp(join(tmpdir(), 'kimi-bg-agent-reconcile-')); + try { + agent.background.attachSessionDir(sessionDir); + await writeTask(sessionDir, { + task_id: 'bash-orphan00', + command: 'sleep 60', + description: 'orphan task', + pid: 99999, + started_at: 1_700_000_000, + ended_at: null, + exit_code: null, + status: 'running', + }); + agent.emittedEvents.length = 0; + + await agent.background.loadFromDisk(); + await agent.background.reconcile(); + + const terminated = agent.emittedEvents.filter( + (e) => e.type === 'background.task.terminated', + ); + expect(terminated.length).toBe(1); + expect(terminated[0]!.info.taskId).toBe('bash-orphan00'); + expect(terminated[0]!.info.status).toBe('lost'); + } finally { + await rm(sessionDir, { recursive: true, force: true }); + } + }); + + it('steers completed agent task notifications into the turn flow', async () => { + const taskId = agent.background.registerAgentTask( + Promise.resolve({ result: 'final subagent summary' }), + 'agent task', + ); + await agent.background.waitForTerminal(taskId); + + await vi.waitFor(() => { + expect(agent.turn.steer).toHaveBeenCalledTimes(1); + }); + expect(agent.turn.waitForCurrentTurn).not.toHaveBeenCalled(); + expect(agent.context.appendUserMessage).not.toHaveBeenCalled(); + + const [content, origin] = vi.mocked(agent.turn.steer).mock.calls[0]!; + expect(origin).toEqual({ + kind: 'background_task', + taskId, + status: 'completed', + notificationId: `task:${taskId}:completed`, + }); + expect((content as Array<{ text: string }>)[0]!.text).toContain( + 'Background agent completed', + ); + expect((content as Array<{ text: string }>)[0]!.text).toContain('final subagent summary'); + }); + + it('steers completed bash task notifications into the turn flow', async () => { + const taskId = agent.background.register(immediateProcess(0), 'echo ok', 'shell task'); + + await agent.background.waitForTerminal(taskId); + + await vi.waitFor(() => { + expect(agent.turn.steer).toHaveBeenCalledTimes(1); + }); + expect(agent.turn.waitForCurrentTurn).not.toHaveBeenCalled(); + expect(agent.context.appendUserMessage).not.toHaveBeenCalled(); + + const [content, origin] = vi.mocked(agent.turn.steer).mock.calls[0]!; + expect(origin).toEqual({ + kind: 'background_task', + taskId, + status: 'completed', + notificationId: `task:${taskId}:completed`, + }); + expect((content as Array<{ text: string }>)[0]!.text).toContain( + 'Background task completed', + ); + expect((content as Array<{ text: string }>)[0]!.text).toContain('shell task completed.'); + }); + + it('steers stopped bash task notifications into the turn flow', async () => { + const taskId = agent.background.register(pendingProcess(), 'sleep 60', 'long shell task'); + + await agent.background.stop(taskId); + + await vi.waitFor(() => { + expect(agent.turn.steer).toHaveBeenCalledTimes(1); + }); + const [content, origin] = vi.mocked(agent.turn.steer).mock.calls[0]!; + expect(origin).toEqual({ + kind: 'background_task', + taskId, + status: 'killed', + notificationId: `task:${taskId}:killed`, + }); + expect((content as Array<{ text: string }>)[0]!.text).toContain('Background task killed'); + expect((content as Array<{ text: string }>)[0]!.text).toContain('long shell task killed.'); + }); + + it('queues background agent notifications without waiting for an active turn', async () => { + agent.turn.hasActiveTurn = true; + const taskId = agent.background.registerAgentTask( + Promise.resolve({ result: 'active turn summary' }), + 'agent task', + ); + await agent.background.waitForTerminal(taskId); + + await vi.waitFor(() => { + expect(agent.turn.steer).toHaveBeenCalledTimes(1); + }); + expect(agent.turn.waitForCurrentTurn).not.toHaveBeenCalled(); + const [content, origin] = vi.mocked(agent.turn.steer).mock.calls[0]!; + expect(origin).toEqual({ + kind: 'background_task', + taskId, + status: 'completed', + notificationId: `task:${taskId}:completed`, + }); + expect((content as Array<{ text: string }>)[0]!.text).toContain('active turn summary'); + }); + + it('replays restored terminal agent task notifications when they were not delivered', async () => { + const sessionDir = await mkdtemp(join(tmpdir(), 'kimi-bg-agent-replay-')); + try { + agent.background.attachSessionDir(sessionDir); + await writeTask(sessionDir, { + task_id: 'agent-done0000', + command: '[agent] restored task', + description: 'restored task', + pid: 0, + started_at: 1_700_000_000, + ended_at: 1_700_000_010, + exit_code: 0, + status: 'completed', + }); + await appendTaskOutput(sessionDir, 'agent-done0000', 'restored subagent summary'); + + await agent.background.loadFromDisk(); + const result = await agent.background.reconcile(); + + expect(result.lost).toEqual([]); + await vi.waitFor(() => { + expect(agent.context.appendUserMessage).toHaveBeenCalledTimes(1); + }); + expect(agent.turn.steer).not.toHaveBeenCalled(); + const [content, origin] = vi.mocked(agent.context.appendUserMessage).mock.calls[0]!; + expect(origin).toEqual({ + kind: 'background_task', + taskId: 'agent-done0000', + status: 'completed', + notificationId: 'task:agent-done0000:completed', + }); + expect((content as Array<{ text: string }>)[0]!.text).toContain( + 'Background agent completed', + ); + expect((content as Array<{ text: string }>)[0]!.text).toContain('restored subagent summary'); + } finally { + await rm(sessionDir, { recursive: true, force: true }); + } + }); + + it('replays restored terminal bash task notifications when they were not delivered', async () => { + const sessionDir = await mkdtemp(join(tmpdir(), 'kimi-bg-bash-replay-')); + try { + agent.background.attachSessionDir(sessionDir); + await writeTask(sessionDir, { + task_id: 'bash-done0000', + command: 'echo done', + description: 'restored shell task', + pid: 12345, + started_at: 1_700_000_000, + ended_at: 1_700_000_010, + exit_code: 0, + status: 'completed', + }); + await appendTaskOutput(sessionDir, 'bash-done0000', 'restored shell output'); + + await agent.background.loadFromDisk(); + const result = await agent.background.reconcile(); + + expect(result.lost).toEqual([]); + await vi.waitFor(() => { + expect(agent.context.appendUserMessage).toHaveBeenCalledTimes(1); + }); + expect(agent.turn.steer).not.toHaveBeenCalled(); + const [content, origin] = vi.mocked(agent.context.appendUserMessage).mock.calls[0]!; + expect(origin).toEqual({ + kind: 'background_task', + taskId: 'bash-done0000', + status: 'completed', + notificationId: 'task:bash-done0000:completed', + }); + expect((content as Array<{ text: string }>)[0]!.text).toContain( + 'Background task completed', + ); + expect((content as Array<{ text: string }>)[0]!.text).toContain('restored shell output'); + } finally { + await rm(sessionDir, { recursive: true, force: true }); + } + }); + + it('reads only a bounded output tail for restored bash task notifications', async () => { + const sessionDir = await mkdtemp(join(tmpdir(), 'kimi-bg-bash-tail-')); + try { + const taskId = 'bash-large000'; + const largeOutput = `early-output-marker\n${'x'.repeat(8_000)}\nfinal output line`; + agent.background.attachSessionDir(sessionDir); + await writeTask(sessionDir, { + task_id: taskId, + command: 'generate large output', + description: 'large shell task', + pid: 12345, + started_at: 1_700_000_000, + ended_at: 1_700_000_010, + exit_code: 0, + status: 'completed', + }); + await appendTaskOutput(sessionDir, taskId, largeOutput); + const readOutputSpy = vi.spyOn(agent.background, 'readOutput'); + const snapshotSpy = vi.spyOn(agent.background, 'getOutputSnapshot'); + + await agent.background.loadFromDisk(); + await agent.background.reconcile(); + + await vi.waitFor(() => { + expect(agent.context.appendUserMessage).toHaveBeenCalledTimes(1); + }); + expect(readOutputSpy).not.toHaveBeenCalled(); + expect(snapshotSpy).toHaveBeenCalledWith(taskId, expect.any(Number)); + expect(snapshotSpy.mock.calls[0]![1]).toBeLessThan(largeOutput.length); + const [content] = vi.mocked(agent.context.appendUserMessage).mock.calls[0]!; + const text = (content as Array<{ text: string }>)[0]!.text; + expect(text).toContain('final output line'); + expect(text).not.toContain('early-output-marker'); + } finally { + await rm(sessionDir, { recursive: true, force: true }); + } + }); + + it('does not replay restored agent task notifications already marked delivered', async () => { + const sessionDir = await mkdtemp(join(tmpdir(), 'kimi-bg-agent-replay-')); + try { + const origin = { + kind: 'background_task', + taskId: 'agent-seen0000', + status: 'completed', + notificationId: 'task:agent-seen0000:completed', + } as const; + agent.background.markDeliveredNotification(origin); + agent.background.attachSessionDir(sessionDir); + await writeTask(sessionDir, { + task_id: 'agent-seen0000', + command: '[agent] already delivered', + description: 'already delivered', + pid: 0, + started_at: 1_700_000_000, + ended_at: 1_700_000_010, + exit_code: 0, + status: 'completed', + }); + await appendTaskOutput(sessionDir, 'agent-seen0000', 'already delivered summary'); + + await agent.background.loadFromDisk(); + await agent.background.reconcile(); + + expect(agent.turn.steer).not.toHaveBeenCalled(); + expect(agent.context.appendUserMessage).not.toHaveBeenCalled(); + } finally { + await rm(sessionDir, { recursive: true, force: true }); + } + }); + + it('does not double-notify newly lost restored agent tasks', async () => { + const sessionDir = await mkdtemp(join(tmpdir(), 'kimi-bg-agent-replay-')); + try { + agent.background.attachSessionDir(sessionDir); + await writeTask(sessionDir, { + task_id: 'agent-run00000', + command: '[agent] interrupted task', + description: 'interrupted task', + pid: 0, + started_at: 1_700_000_000, + ended_at: null, + exit_code: null, + status: 'running', + }); + + await agent.background.loadFromDisk(); + const result = await agent.background.reconcile(); + + expect(result.lost).toEqual(['agent-run00000']); + await vi.waitFor(() => { + expect(agent.context.appendUserMessage).toHaveBeenCalledTimes(1); + }); + expect(agent.turn.steer).not.toHaveBeenCalled(); + const [content, origin] = vi.mocked(agent.context.appendUserMessage).mock.calls[0]!; + expect(origin).toEqual({ + kind: 'background_task', + taskId: 'agent-run00000', + status: 'lost', + notificationId: 'task:agent-run00000:lost', + }); + expect((content as Array<{ text: string }>)[0]!.text).toContain('Background agent lost'); + } finally { + await rm(sessionDir, { recursive: true, force: true }); + } + }); + + it('fires a Notification hook when a background agent notification is delivered', async () => { + const fireAndForgetTrigger = vi.fn(() => Promise.resolve([])); + agent = makeAgent({ hooks: { fireAndForgetTrigger } }); + + const taskId = agent.background.registerAgentTask( + Promise.resolve({ result: 'final agent output' }), + 'inspect repository', + ); + await agent.background.wait(taskId); + + await vi.waitFor(() => { + expect(agent.turn.steer).toHaveBeenCalled(); + expect(fireAndForgetTrigger).toHaveBeenCalled(); + }); + expect(fireAndForgetTrigger).toHaveBeenCalledWith('Notification', { + matcherValue: 'task.completed', + inputData: { + sink: 'context', + notificationType: 'task.completed', + title: 'Background agent completed', + body: 'inspect repository completed.', + severity: 'info', + sourceKind: 'background_task', + sourceId: taskId, + }, + }); + }); + + it('does not let Notification hook failures interrupt background notification delivery', async () => { + const fireAndForgetTrigger = vi.fn(() => { + throw new Error('notification hook failed'); + }); + agent = makeAgent({ hooks: { fireAndForgetTrigger } }); + + const taskId = agent.background.registerAgentTask( + Promise.resolve({ result: 'final agent output' }), + 'inspect repository', + ); + await agent.background.wait(taskId); + + await vi.waitFor(() => { + expect(agent.turn.steer).toHaveBeenCalled(); + expect(fireAndForgetTrigger).toHaveBeenCalled(); + }); + expect(agent.turn.steer).toHaveBeenCalledWith( + [ + { + type: 'text', + text: expect.stringContaining(`source_id="${taskId}"`), + }, + ], + { + kind: 'background_task', + taskId, + status: 'completed', + notificationId: `task:${taskId}:completed`, + }, + ); + }); + + it('fires Notification hooks for bash background task notifications', async () => { + const fireAndForgetTrigger = vi.fn(() => Promise.resolve([])); + agent = makeAgent({ hooks: { fireAndForgetTrigger } }); + + const taskId = agent.background.register(immediateProcess(0), 'echo', 'done'); + await agent.background.waitForTerminal(taskId); + + await vi.waitFor(() => { + expect(agent.turn.steer).toHaveBeenCalled(); + expect(fireAndForgetTrigger).toHaveBeenCalled(); + }); + expect(agent.context.appendUserMessage).not.toHaveBeenCalled(); + expect(fireAndForgetTrigger).toHaveBeenCalledWith('Notification', { + matcherValue: 'task.completed', + inputData: { + sink: 'context', + notificationType: 'task.completed', + title: 'Background task completed', + body: 'done completed.', + severity: 'info', + sourceKind: 'background_task', + sourceId: taskId, + }, + }); + }); + + it('tracks stopped tasks as killed even without a stop reason', async () => { + const taskId = agent.background.register(pendingProcess(), 'sleep 60', 'long'); + agent.telemetry.track.mockClear(); + + await agent.background.stop(taskId); + + expect(agent.telemetry.track).toHaveBeenCalledWith( + 'background_task_completed', + expect.objectContaining({ + success: false, + reason: 'killed', + }), + ); + }); + + // Note: the `records.restoring` guard is enforced inside `Agent.emitEvent` + // (see agent/index.ts). BackgroundManager unconditionally forwards + // lifecycle events to the agent; suppression is the agent's job. +}); diff --git a/packages/agent-core/test/agent/basic.test.ts b/packages/agent-core/test/agent/basic.test.ts new file mode 100644 index 000000000..0a3e4da1a --- /dev/null +++ b/packages/agent-core/test/agent/basic.test.ts @@ -0,0 +1,138 @@ +import type { ToolCall } from '@moonshot-ai/kosong'; +import { expect, it } from 'vitest'; + +import { createCommandKaos, testAgent } from './harness/agent'; + +it('runs a text-only agent turn from prompt to completion', async () => { + const ctx = testAgent(); + ctx.configure(); + + ctx.mockNextResponse({ type: 'think', think: '' }, { type: 'text', text: '' }); + await ctx.rpc.prompt({ input: [{ type: 'text', text: 'Hello' }] }); + + expect(await ctx.untilTurnEnd()).toMatchInlineSnapshot(` + [wire] turn.prompt { "input": [ { "type": "text", "text": "Hello" } ], "origin": { "kind": "user" }, "time": "