@ai-zen/cli 1.0.0-alpha.3 → 1.0.0-alpha.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +885 -0
- package/LICENSE +21 -0
- package/README.md +135 -30
- package/README.zh.md +135 -30
- package/dist/agent-creator.d.ts +11 -0
- package/dist/agent-creator.d.ts.map +1 -1
- package/dist/agent-creator.js +50 -20
- package/dist/agent-creator.js.map +1 -1
- package/dist/agent-switch.d.ts +61 -0
- package/dist/agent-switch.d.ts.map +1 -0
- package/dist/agent-switch.js +83 -0
- package/dist/agent-switch.js.map +1 -0
- package/dist/agents-store.d.ts +6 -1
- package/dist/agents-store.d.ts.map +1 -1
- package/dist/agents-store.js +46 -4
- package/dist/agents-store.js.map +1 -1
- package/dist/auto-migrate-confirm.d.ts +43 -0
- package/dist/auto-migrate-confirm.d.ts.map +1 -0
- package/dist/auto-migrate-confirm.js +47 -0
- package/dist/auto-migrate-confirm.js.map +1 -0
- package/dist/clipboard.d.ts +68 -0
- package/dist/clipboard.d.ts.map +1 -0
- package/dist/clipboard.js +177 -0
- package/dist/clipboard.js.map +1 -0
- package/dist/config-editor.d.ts +42 -5
- package/dist/config-editor.d.ts.map +1 -1
- package/dist/config-editor.js +80 -27
- package/dist/config-editor.js.map +1 -1
- package/dist/config.d.ts +76 -17
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +143 -39
- package/dist/config.js.map +1 -1
- package/dist/conversation-commands/registry.d.ts +127 -4
- package/dist/conversation-commands/registry.d.ts.map +1 -1
- package/dist/conversation-commands/registry.js +93 -14
- package/dist/conversation-commands/registry.js.map +1 -1
- package/dist/conversation-persist-plugin.d.ts +0 -1
- package/dist/conversation-persist-plugin.d.ts.map +1 -1
- package/dist/conversation-persist-plugin.js +7 -16
- package/dist/conversation-persist-plugin.js.map +1 -1
- package/dist/conversation-repository.d.ts +79 -18
- package/dist/conversation-repository.d.ts.map +1 -1
- package/dist/conversation-repository.js +96 -30
- package/dist/conversation-repository.js.map +1 -1
- package/dist/cwd-tracker-plugin.d.ts.map +1 -1
- package/dist/cwd-tracker-plugin.js +22 -7
- package/dist/cwd-tracker-plugin.js.map +1 -1
- package/dist/format-time.d.ts +17 -6
- package/dist/format-time.d.ts.map +1 -1
- package/dist/format-time.js +32 -14
- package/dist/format-time.js.map +1 -1
- package/dist/handoff-service.d.ts +19 -0
- package/dist/handoff-service.d.ts.map +1 -0
- package/dist/handoff-service.js +89 -0
- package/dist/handoff-service.js.map +1 -0
- package/dist/hook.d.ts.map +1 -1
- package/dist/hook.js +69 -15
- package/dist/hook.js.map +1 -1
- package/dist/i18n/en-wizard.d.ts +8 -0
- package/dist/i18n/en-wizard.d.ts.map +1 -0
- package/dist/i18n/en-wizard.js +303 -0
- package/dist/i18n/en-wizard.js.map +1 -0
- package/dist/i18n/en.d.ts +7 -0
- package/dist/i18n/en.d.ts.map +1 -0
- package/dist/i18n/en.js +247 -0
- package/dist/i18n/en.js.map +1 -0
- package/dist/i18n/index.d.ts +57 -0
- package/dist/i18n/index.d.ts.map +1 -0
- package/dist/i18n/index.js +109 -0
- package/dist/i18n/index.js.map +1 -0
- package/dist/i18n/preferences.d.ts +37 -0
- package/dist/i18n/preferences.d.ts.map +1 -0
- package/dist/i18n/preferences.js +73 -0
- package/dist/i18n/preferences.js.map +1 -0
- package/dist/i18n/zh-wizard.d.ts +11 -0
- package/dist/i18n/zh-wizard.d.ts.map +1 -0
- package/dist/i18n/zh-wizard.js +306 -0
- package/dist/i18n/zh-wizard.js.map +1 -0
- package/dist/i18n/zh.d.ts +10 -0
- package/dist/i18n/zh.d.ts.map +1 -0
- package/dist/i18n/zh.js +251 -0
- package/dist/i18n/zh.js.map +1 -0
- package/dist/index.js +72 -25
- package/dist/index.js.map +1 -1
- package/dist/mcp-command.d.ts +32 -0
- package/dist/mcp-command.d.ts.map +1 -0
- package/dist/mcp-command.js +37 -0
- package/dist/mcp-command.js.map +1 -0
- package/dist/mode.d.ts +31 -4
- package/dist/mode.d.ts.map +1 -1
- package/dist/mode.js +42 -7
- package/dist/mode.js.map +1 -1
- package/dist/model-switch.d.ts +58 -0
- package/dist/model-switch.d.ts.map +1 -0
- package/dist/model-switch.js +62 -0
- package/dist/model-switch.js.map +1 -0
- package/dist/reasoning-effort.d.ts +96 -0
- package/dist/reasoning-effort.d.ts.map +1 -0
- package/dist/reasoning-effort.js +164 -0
- package/dist/reasoning-effort.js.map +1 -0
- package/dist/reasoning-presets.d.ts +34 -0
- package/dist/reasoning-presets.d.ts.map +1 -0
- package/dist/reasoning-presets.js +85 -0
- package/dist/reasoning-presets.js.map +1 -0
- package/dist/sdk-log.d.ts +84 -0
- package/dist/sdk-log.d.ts.map +1 -0
- package/dist/sdk-log.js +167 -0
- package/dist/sdk-log.js.map +1 -0
- package/dist/session-pointer.d.ts.map +1 -1
- package/dist/session-pointer.js +10 -4
- package/dist/session-pointer.js.map +1 -1
- package/dist/stdio-runner.d.ts.map +1 -1
- package/dist/stdio-runner.js +32 -30
- package/dist/stdio-runner.js.map +1 -1
- package/dist/sub-agent-guard-plugin.d.ts +10 -7
- package/dist/sub-agent-guard-plugin.d.ts.map +1 -1
- package/dist/sub-agent-guard-plugin.js +12 -9
- package/dist/sub-agent-guard-plugin.js.map +1 -1
- package/dist/tui/chat-history.d.ts +29 -0
- package/dist/tui/chat-history.d.ts.map +1 -0
- package/dist/tui/chat-history.js +55 -0
- package/dist/tui/chat-history.js.map +1 -0
- package/dist/tui/chat-session.d.ts +177 -57
- package/dist/tui/chat-session.d.ts.map +1 -1
- package/dist/tui/chat-session.js +362 -126
- package/dist/tui/chat-session.js.map +1 -1
- package/dist/tui/components.d.ts +113 -7
- package/dist/tui/components.d.ts.map +1 -1
- package/dist/tui/components.js +177 -12
- package/dist/tui/components.js.map +1 -1
- package/dist/tui/config-wizard/detail.d.ts +18 -10
- package/dist/tui/config-wizard/detail.d.ts.map +1 -1
- package/dist/tui/config-wizard/detail.js +53 -46
- package/dist/tui/config-wizard/detail.js.map +1 -1
- package/dist/tui/config-wizard/engine.d.ts +2 -2
- package/dist/tui/config-wizard/engine.d.ts.map +1 -1
- package/dist/tui/config-wizard/engine.js +48 -26
- package/dist/tui/config-wizard/engine.js.map +1 -1
- package/dist/tui/config-wizard/fields.d.ts +3 -1
- package/dist/tui/config-wizard/fields.d.ts.map +1 -1
- package/dist/tui/config-wizard/fields.js +202 -167
- package/dist/tui/config-wizard/fields.js.map +1 -1
- package/dist/tui/config-wizard/frame.js +1 -1
- package/dist/tui/config-wizard/kv-editor.d.ts.map +1 -1
- package/dist/tui/config-wizard/kv-editor.js +13 -5
- package/dist/tui/config-wizard/kv-editor.js.map +1 -1
- package/dist/tui/config-wizard/list.d.ts +3 -1
- package/dist/tui/config-wizard/list.d.ts.map +1 -1
- package/dist/tui/config-wizard/list.js +4 -4
- package/dist/tui/config-wizard/list.js.map +1 -1
- package/dist/tui/config-wizard/mcp-helpers.d.ts.map +1 -1
- package/dist/tui/config-wizard/mcp-helpers.js +4 -3
- package/dist/tui/config-wizard/mcp-helpers.js.map +1 -1
- package/dist/tui/config-wizard/menu.d.ts.map +1 -1
- package/dist/tui/config-wizard/menu.js +33 -21
- package/dist/tui/config-wizard/menu.js.map +1 -1
- package/dist/tui/config-wizard/prompt-editor.d.ts +8 -1
- package/dist/tui/config-wizard/prompt-editor.d.ts.map +1 -1
- package/dist/tui/config-wizard/prompt-editor.js +11 -7
- package/dist/tui/config-wizard/prompt-editor.js.map +1 -1
- package/dist/tui/config-wizard/prompt.d.ts.map +1 -1
- package/dist/tui/config-wizard/prompt.js +8 -7
- package/dist/tui/config-wizard/prompt.js.map +1 -1
- package/dist/tui/config-wizard/step-agents.d.ts.map +1 -1
- package/dist/tui/config-wizard/step-agents.js +50 -29
- package/dist/tui/config-wizard/step-agents.js.map +1 -1
- package/dist/tui/config-wizard/step-defaults.d.ts.map +1 -1
- package/dist/tui/config-wizard/step-defaults.js +77 -27
- package/dist/tui/config-wizard/step-defaults.js.map +1 -1
- package/dist/tui/config-wizard/step-endpoints.d.ts.map +1 -1
- package/dist/tui/config-wizard/step-endpoints.js +27 -14
- package/dist/tui/config-wizard/step-endpoints.js.map +1 -1
- package/dist/tui/config-wizard/step-mcp.d.ts.map +1 -1
- package/dist/tui/config-wizard/step-mcp.js +29 -25
- package/dist/tui/config-wizard/step-mcp.js.map +1 -1
- package/dist/tui/config-wizard/step-models.d.ts.map +1 -1
- package/dist/tui/config-wizard/step-models.js +98 -43
- package/dist/tui/config-wizard/step-models.js.map +1 -1
- package/dist/tui/config-wizard/step-new-endpoint.d.ts.map +1 -1
- package/dist/tui/config-wizard/step-new-endpoint.js +16 -12
- package/dist/tui/config-wizard/step-new-endpoint.js.map +1 -1
- package/dist/tui/config-wizard/step-simple.d.ts.map +1 -1
- package/dist/tui/config-wizard/step-simple.js +18 -17
- package/dist/tui/config-wizard/step-simple.js.map +1 -1
- package/dist/tui/config-wizard/types.d.ts +31 -3
- package/dist/tui/config-wizard/types.d.ts.map +1 -1
- package/dist/tui/config-wizard/types.js.map +1 -1
- package/dist/tui/file-index.d.ts +38 -0
- package/dist/tui/file-index.d.ts.map +1 -0
- package/dist/tui/file-index.js +117 -0
- package/dist/tui/file-index.js.map +1 -0
- package/dist/tui/index.d.ts +18 -1
- package/dist/tui/index.d.ts.map +1 -1
- package/dist/tui/index.js +107 -49
- package/dist/tui/index.js.map +1 -1
- package/dist/tui/input-editing.d.ts +52 -0
- package/dist/tui/input-editing.d.ts.map +1 -0
- package/dist/tui/input-editing.js +91 -0
- package/dist/tui/input-editing.js.map +1 -0
- package/dist/tui/key-debug.d.ts +62 -0
- package/dist/tui/key-debug.d.ts.map +1 -0
- package/dist/tui/key-debug.js +92 -0
- package/dist/tui/key-debug.js.map +1 -0
- package/dist/tui/mention.d.ts +47 -0
- package/dist/tui/mention.d.ts.map +1 -0
- package/dist/tui/mention.js +152 -0
- package/dist/tui/mention.js.map +1 -0
- package/dist/tui/mouse.d.ts +55 -0
- package/dist/tui/mouse.d.ts.map +1 -0
- package/dist/tui/mouse.js +74 -0
- package/dist/tui/mouse.js.map +1 -0
- package/dist/tui/phase.d.ts +64 -0
- package/dist/tui/phase.d.ts.map +1 -0
- package/dist/tui/phase.js +112 -0
- package/dist/tui/phase.js.map +1 -0
- package/dist/tui/screens/agent-tabs.d.ts +53 -0
- package/dist/tui/screens/agent-tabs.d.ts.map +1 -0
- package/dist/tui/screens/agent-tabs.js +81 -0
- package/dist/tui/screens/agent-tabs.js.map +1 -0
- package/dist/tui/screens/chat-commands.d.ts +82 -3
- package/dist/tui/screens/chat-commands.d.ts.map +1 -1
- package/dist/tui/screens/chat-commands.js +688 -160
- package/dist/tui/screens/chat-commands.js.map +1 -1
- package/dist/tui/screens/chat.d.ts +57 -5
- package/dist/tui/screens/chat.d.ts.map +1 -1
- package/dist/tui/screens/chat.js +799 -304
- package/dist/tui/screens/chat.js.map +1 -1
- package/dist/tui/screens/common-keys.d.ts +36 -0
- package/dist/tui/screens/common-keys.d.ts.map +1 -0
- package/dist/tui/screens/common-keys.js +67 -0
- package/dist/tui/screens/common-keys.js.map +1 -0
- package/dist/tui/screens/frame-buffer.d.ts +47 -0
- package/dist/tui/screens/frame-buffer.d.ts.map +1 -0
- package/dist/tui/screens/frame-buffer.js +78 -0
- package/dist/tui/screens/frame-buffer.js.map +1 -0
- package/dist/tui/screens/handoff-choice.d.ts +37 -0
- package/dist/tui/screens/handoff-choice.d.ts.map +1 -0
- package/dist/tui/screens/handoff-choice.js +42 -0
- package/dist/tui/screens/handoff-choice.js.map +1 -0
- package/dist/tui/screens/index.d.ts +11 -9
- package/dist/tui/screens/index.d.ts.map +1 -1
- package/dist/tui/screens/index.js +9 -8
- package/dist/tui/screens/index.js.map +1 -1
- package/dist/tui/screens/input-row.d.ts +64 -0
- package/dist/tui/screens/input-row.d.ts.map +1 -0
- package/dist/tui/screens/input-row.js +67 -0
- package/dist/tui/screens/input-row.js.map +1 -0
- package/dist/tui/screens/input.d.ts +77 -1
- package/dist/tui/screens/input.d.ts.map +1 -1
- package/dist/tui/screens/input.js +125 -8
- package/dist/tui/screens/input.js.map +1 -1
- package/dist/tui/screens/layout.d.ts +42 -3
- package/dist/tui/screens/layout.d.ts.map +1 -1
- package/dist/tui/screens/layout.js +43 -13
- package/dist/tui/screens/layout.js.map +1 -1
- package/dist/tui/screens/lines.d.ts +6 -36
- package/dist/tui/screens/lines.d.ts.map +1 -1
- package/dist/tui/screens/lines.js +8 -71
- package/dist/tui/screens/lines.js.map +1 -1
- package/dist/tui/screens/messages-view.d.ts +29 -0
- package/dist/tui/screens/messages-view.d.ts.map +1 -0
- package/dist/tui/screens/messages-view.js +162 -0
- package/dist/tui/screens/messages-view.js.map +1 -0
- package/dist/tui/screens/notice-bar.d.ts +5 -0
- package/dist/tui/screens/notice-bar.d.ts.map +1 -0
- package/dist/tui/screens/notice-bar.js +21 -0
- package/dist/tui/screens/notice-bar.js.map +1 -0
- package/dist/tui/screens/notice.d.ts +66 -0
- package/dist/tui/screens/notice.d.ts.map +1 -0
- package/dist/tui/screens/notice.js +106 -0
- package/dist/tui/screens/notice.js.map +1 -0
- package/dist/tui/screens/overlays.d.ts.map +1 -1
- package/dist/tui/screens/overlays.js +4 -3
- package/dist/tui/screens/overlays.js.map +1 -1
- package/dist/tui/screens/render.d.ts +2 -4
- package/dist/tui/screens/render.d.ts.map +1 -1
- package/dist/tui/screens/render.js +19 -50
- package/dist/tui/screens/render.js.map +1 -1
- package/dist/tui/screens/splash.d.ts +1 -1
- package/dist/tui/screens/splash.d.ts.map +1 -1
- package/dist/tui/screens/splash.js +12 -3
- package/dist/tui/screens/splash.js.map +1 -1
- package/dist/tui/screens/use-chat-viewport.d.ts +22 -0
- package/dist/tui/screens/use-chat-viewport.d.ts.map +1 -0
- package/dist/tui/screens/use-chat-viewport.js +50 -0
- package/dist/tui/screens/use-chat-viewport.js.map +1 -0
- package/dist/tui/screens/use-input-editing.d.ts +69 -0
- package/dist/tui/screens/use-input-editing.d.ts.map +1 -0
- package/dist/tui/screens/use-input-editing.js +220 -0
- package/dist/tui/screens/use-input-editing.js.map +1 -0
- package/dist/tui/screens/use-overlays.d.ts +25 -0
- package/dist/tui/screens/use-overlays.d.ts.map +1 -0
- package/dist/tui/screens/use-overlays.js +32 -0
- package/dist/tui/screens/use-overlays.js.map +1 -0
- package/dist/tui/screens/use-wizard-host.d.ts +46 -0
- package/dist/tui/screens/use-wizard-host.d.ts.map +1 -0
- package/dist/tui/screens/use-wizard-host.js +123 -0
- package/dist/tui/screens/use-wizard-host.js.map +1 -0
- package/dist/tui/status-segments.d.ts +37 -0
- package/dist/tui/status-segments.d.ts.map +1 -0
- package/dist/tui/status-segments.js +166 -0
- package/dist/tui/status-segments.js.map +1 -0
- package/dist/tui/text.d.ts +27 -7
- package/dist/tui/text.d.ts.map +1 -1
- package/dist/tui/text.js +116 -38
- package/dist/tui/text.js.map +1 -1
- package/dist/tui/theme.d.ts +2 -0
- package/dist/tui/theme.d.ts.map +1 -1
- package/dist/tui/theme.js +2 -0
- package/dist/tui/theme.js.map +1 -1
- package/dist/tui/token-rate.d.ts +83 -0
- package/dist/tui/token-rate.d.ts.map +1 -0
- package/dist/tui/token-rate.js +195 -0
- package/dist/tui/token-rate.js.map +1 -0
- package/dist/types.d.ts +7 -8
- package/dist/types.d.ts.map +1 -1
- package/dist/user-content.d.ts +41 -0
- package/dist/user-content.d.ts.map +1 -0
- package/dist/user-content.js +37 -0
- package/dist/user-content.js.map +1 -0
- package/dist/version-compare.d.ts +37 -0
- package/dist/version-compare.d.ts.map +1 -0
- package/dist/version-compare.js +105 -0
- package/dist/version-compare.js.map +1 -0
- package/dist/version.js.map +1 -1
- package/package.json +5 -4
- package/dist/auto-migrate-confirm-plugin.d.ts +0 -58
- package/dist/auto-migrate-confirm-plugin.d.ts.map +0 -1
- package/dist/auto-migrate-confirm-plugin.js +0 -61
- package/dist/auto-migrate-confirm-plugin.js.map +0 -1
- package/dist/migration-service.d.ts +0 -15
- package/dist/migration-service.d.ts.map +0 -1
- package/dist/migration-service.js +0 -59
- package/dist/migration-service.js.map +0 -1
- package/dist/tui/screens/state.d.ts +0 -30
- package/dist/tui/screens/state.d.ts.map +0 -1
- package/dist/tui/screens/state.js +0 -100
- package/dist/tui/screens/state.js.map +0 -1
package/dist/tui/screens/chat.js
CHANGED
|
@@ -1,30 +1,61 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
//
|
|
3
|
-
import { useEffect,
|
|
2
|
+
// 对话屏幕 —— 以 SDK 的 `agent.messages` 为唯一数据源渲染(无自定义中间模型)
|
|
3
|
+
import { useEffect, useRef, useState } from "react";
|
|
4
4
|
import { Box, Text, useApp, useCursor, useInput, useWindowSize } from "ink";
|
|
5
|
+
import { spawn } from "node:child_process";
|
|
6
|
+
import { disableMouseTracking } from "../mouse.js";
|
|
7
|
+
import { setSdkNoticeSink } from "../../sdk-log.js";
|
|
8
|
+
import { keyDebug } from "../key-debug.js";
|
|
9
|
+
import { phaseLabel } from "../phase.js";
|
|
10
|
+
import { AgentNS } from "@ai-zen/agents-core";
|
|
5
11
|
import { theme } from "../theme.js";
|
|
6
|
-
import { Spinner, StatusBar,
|
|
7
|
-
import { layoutInput, usableFrameRows
|
|
12
|
+
import { Spinner, StatusBar, statusBarRows, BottomMenu, PastedImagesBar } from "../components.js";
|
|
13
|
+
import { graphemes, layoutInput, usableFrameRows } from "../text.js";
|
|
8
14
|
import { ChatSession } from "../chat-session.js";
|
|
9
|
-
import { matchCommandHints } from "../../conversation-commands/registry.js";
|
|
10
|
-
import { readConfig, readMcpConfig, saveConfig, writeMcpConfig } from "../../config.js";
|
|
15
|
+
import { commandBlocksInput, matchCommandHints, parseCommandInput, } from "../../conversation-commands/registry.js";
|
|
16
|
+
import { readConfig, readMcpConfig, readReasoningPresets, saveConfig, writeMcpConfig } from "../../config.js";
|
|
11
17
|
import { readAgentStore } from "../../agents-store.js";
|
|
12
|
-
import { resolveCredential } from "../../config-editor.js";
|
|
13
18
|
import { ConfigWizard } from "../config-wizard/index.js";
|
|
14
|
-
import { computeInputCursorPosition,
|
|
15
|
-
import { computeChatLayout } from "./layout.js";
|
|
16
|
-
import {
|
|
17
|
-
import { LineView
|
|
19
|
+
import { computeInputCursorPosition, inputWindow } from "./input.js";
|
|
20
|
+
import { AGENT_TABS_LINES, computeChatLayout } from "./layout.js";
|
|
21
|
+
import { FrameBuffer } from "./frame-buffer.js";
|
|
22
|
+
import { LineView } from "./render.js";
|
|
23
|
+
import { messageToLines, transcriptLines } from "./messages-view.js";
|
|
18
24
|
import { PickerOverlay, Confirm } from "./overlays.js";
|
|
25
|
+
import { CursorInputRow } from "./input-row.js";
|
|
26
|
+
import { chooseHandoffMode } from "./handoff-choice.js";
|
|
19
27
|
import { runCommand } from "./chat-commands.js";
|
|
28
|
+
import { describeEffortView } from "../../reasoning-effort.js";
|
|
29
|
+
import { t } from "../../i18n/index.js";
|
|
30
|
+
import { NoticeChannel, noticeLines } from "./notice.js";
|
|
31
|
+
import { NoticeBar } from "./notice-bar.js";
|
|
32
|
+
import { AgentTabs } from "./agent-tabs.js";
|
|
33
|
+
import { useChatViewport } from "./use-chat-viewport.js";
|
|
34
|
+
import { useChatOverlays } from "./use-overlays.js";
|
|
35
|
+
import { useWizardHost } from "./use-wizard-host.js";
|
|
36
|
+
import { useInputEditing } from "./use-input-editing.js";
|
|
37
|
+
import { extractMention, mentionInsertText, rankPaths } from "../mention.js";
|
|
38
|
+
import { userDrafts } from "../chat-history.js";
|
|
39
|
+
import { listProjectPaths } from "../file-index.js";
|
|
40
|
+
import { MAX_CLIPBOARD_TEXT_BYTES, readClipboardImage, readClipboardText } from "../../clipboard.js";
|
|
41
|
+
import { handleCtrlDExit, handleScrollKey } from "./common-keys.js";
|
|
42
|
+
/**
|
|
43
|
+
* 单条消息最多附几张粘贴图片。
|
|
44
|
+
*
|
|
45
|
+
* 顶在 4 张:一张手机截图内联后 base64 常在 1~3MB,再多一次请求就是几十 MB,
|
|
46
|
+
* 既扫 token 也容易撞端点的请求体上限。
|
|
47
|
+
*/
|
|
48
|
+
export const MAX_PASTED_IMAGES = 4;
|
|
49
|
+
/** 字节数 → 一位小数的 MB(提示文案用) */
|
|
50
|
+
export function formatMegabytes(bytes) {
|
|
51
|
+
return (bytes / 1024 / 1024).toFixed(1);
|
|
52
|
+
}
|
|
20
53
|
/**
|
|
21
54
|
* 计算回车时应提交的文本。
|
|
22
55
|
*
|
|
23
56
|
* 斜杠候选菜单可见时(输入以 `/` 开头且尚未输入参数),回车应**直接执行高亮命令**,
|
|
24
57
|
* 而不是提交半截输入 —— 否则用户只输入 `/` 或未补全的命令名时,回车会落到
|
|
25
58
|
* 「未知命令」报错(体验很差)。非命令输入原样返回。
|
|
26
|
-
*
|
|
27
|
-
* `menuIndex` 越界时回落到最后一项,避免菜单看似有选中却回车落空。
|
|
28
59
|
*/
|
|
29
60
|
export function resolveSubmitText(input, hints, menuIndex) {
|
|
30
61
|
const slash = input.startsWith("/") && !input.includes("\n") && !input.includes(" ");
|
|
@@ -34,268 +65,512 @@ export function resolveSubmitText(input, hints, menuIndex) {
|
|
|
34
65
|
const picked = hints[index]?.names[0];
|
|
35
66
|
return picked ? `/${picked}` : input;
|
|
36
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* 忙碌期那一行 spinner 的文案。
|
|
70
|
+
*
|
|
71
|
+
* 生成中由 SDK 的阶段驱动(`phaseLabel`);但 `/update`、`/migrate` 这类
|
|
72
|
+
* **阻塞输入的命令**执行时根本没有生成轮次,`phase` 是 `null`,`phaseLabel(null)`
|
|
73
|
+
* 会兜底成「处理中…」——与实际动作毫无关系。命令期间改显命令名,
|
|
74
|
+
* 至少能看出「在跑哪条命令」,具体进度由命令自己 push 通知(查版本 / 开始安装 …)。
|
|
75
|
+
*/
|
|
76
|
+
export function busySpinnerLabel(phase, command) {
|
|
77
|
+
if (phase)
|
|
78
|
+
return phaseLabel(phase);
|
|
79
|
+
return command ? t("cmd.running", { name: command }) : phaseLabel(null);
|
|
80
|
+
}
|
|
37
81
|
export function Chat(props) {
|
|
82
|
+
const deps = {
|
|
83
|
+
readConfig,
|
|
84
|
+
readReasoningPresets,
|
|
85
|
+
readAgentStore,
|
|
86
|
+
createSession: ChatSession.create,
|
|
87
|
+
listProjectPaths,
|
|
88
|
+
readClipboardImage,
|
|
89
|
+
readClipboardText,
|
|
90
|
+
wizardReaders: {
|
|
91
|
+
readConfig,
|
|
92
|
+
readMcpConfig,
|
|
93
|
+
readReasoningPresets,
|
|
94
|
+
readAgentStore,
|
|
95
|
+
},
|
|
96
|
+
saveConfig,
|
|
97
|
+
writeMcpConfig,
|
|
98
|
+
...props.deps,
|
|
99
|
+
};
|
|
38
100
|
const { exit, suspendTerminal } = useApp();
|
|
39
101
|
const { setCursorPosition } = useCursor();
|
|
40
102
|
const { columns, rows } = useWindowSize();
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
blocks: trimTranscript(props.clearTranscript ? [] : messagesToBlocks(props.messages ?? [])),
|
|
44
|
-
live: null,
|
|
45
|
-
});
|
|
46
|
-
const [input, setInput] = useState(props.initialInput ?? "");
|
|
47
|
-
/** 光标位置:以 code point 计的字符索引(0..len) */
|
|
48
|
-
const [cursor, setCursor] = useState(Array.from(props.initialInput ?? "").length);
|
|
49
|
-
const [menuIndex, setMenuIndex] = useState(0);
|
|
50
|
-
// 输入变化后候选集随之改变:高亮重置回首项,避免索引越界(看似选中实为无高亮 / 回车落空)
|
|
51
|
-
useEffect(() => {
|
|
52
|
-
setMenuIndex(0);
|
|
53
|
-
}, [input]);
|
|
54
|
-
const [confirmState, setConfirmState] = useState(null);
|
|
55
|
-
const [pickerState, setPickerState] = useState(null);
|
|
56
|
-
/** 配置向导:非 null 时整屏接管(凭据设置 / 配置中心) */
|
|
57
|
-
const [wizardStep, setWizardStep] = useState(null);
|
|
58
|
-
const [wizardConfig, setWizardConfig] = useState(null);
|
|
59
|
-
/** 可用 Agent 列表(供配置中心「默认 Agent」选择) */
|
|
60
|
-
const [wizardAgents, setWizardAgents] = useState([]);
|
|
61
|
-
/** MCP 配置快照(全局 + 项目),打开向导时刷新 */
|
|
62
|
-
const [wizardMcp, setWizardMcp] = useState(null);
|
|
63
|
-
/** Agent / Sub-agent 定义仓储(快照 + 读写),打开向导时刷新 */
|
|
64
|
-
const [wizardAgentStore, setWizardAgentStore] = useState(null);
|
|
65
|
-
/** 当前会话所用端点 id(打开向导时刷新,用于判断改动是否影响本会话) */
|
|
66
|
-
const currentEndpointRef = useRef(null);
|
|
67
|
-
const [history, setHistory] = useState([]);
|
|
68
|
-
const historyIndex = useRef(-1);
|
|
103
|
+
/** 覆盖层(确认框 / 选择器):弹窗与 resolve 生命周期见 use-overlays.ts */
|
|
104
|
+
const { confirmState, pickerState, setConfirmState, setPickerState, resolveConfirm, resolvePicker } = useChatOverlays();
|
|
69
105
|
const sessionRef = useRef(null);
|
|
70
106
|
const [bootError, setBootError] = useState(null);
|
|
71
107
|
const [ready, setReady] = useState(false);
|
|
72
|
-
/**
|
|
73
|
-
const
|
|
74
|
-
/**
|
|
75
|
-
const
|
|
76
|
-
|
|
108
|
+
/** 视口:滚动状态与翻页 / 按行 / 滚轮换算(唯一入口见 use-chat-viewport.ts) */
|
|
109
|
+
const { viewTop, layoutRef, scrollByPage, scrollByLine, scrollByWheel, scrollToBottom } = useChatViewport();
|
|
110
|
+
/** 本轮是否在生成中(spinner / 输入禁用) */
|
|
111
|
+
const [busy, setBusy] = useState(false);
|
|
112
|
+
/** 正在执行的**阻塞型命令**名(`blocksInput: true` 的 `/update` / `/migrate` …),见 `busySpinnerLabel` */
|
|
113
|
+
const [busyCommand, setBusyCommand] = useState(null);
|
|
114
|
+
/** 生成速率快照(token/s):生成中为实时值,结束后保留本轮平均 */
|
|
115
|
+
const [rate, setRate] = useState(null);
|
|
116
|
+
/**
|
|
117
|
+
* 当前模型 id —— **从会话(即 Agent 定义)读**,不作为 prop 传入。
|
|
118
|
+
*
|
|
119
|
+
* 模型的唯一真相源是 `AgentDefinition.modelId`:所以状态栏、凭据检查、思考状态
|
|
120
|
+
* 与 SDK 实发请求用的永远是同一个值,不会出现「显示 A、实发 B」。
|
|
121
|
+
*/
|
|
122
|
+
const [modelId, setModelId] = useState("");
|
|
123
|
+
/**
|
|
124
|
+
* 思考强度展示文案(关 / 低 / 中 / 高 / 最高 / 开 / —)。
|
|
125
|
+
*
|
|
126
|
+
* 真相在配置里(模型的 `defaultParams`),这里只镜像一份供状态栏用:
|
|
127
|
+
* `/think` 写入后由命令层回调刷新,切模型(会话重建)时靠 `modelId` 变化重读。
|
|
128
|
+
*/
|
|
129
|
+
const [effortView, setEffortView] = useState(null);
|
|
130
|
+
/**
|
|
131
|
+
* 语言切换计数(`/lang`):`t()` 是模块级函数,语言变了已渲染的文案不会自更新,
|
|
132
|
+
* 递增这个计数只为逼 React 重跑一遍渲染函数。
|
|
133
|
+
*/
|
|
134
|
+
const [localeTick, bumpLocale] = useState(0);
|
|
135
|
+
/**
|
|
136
|
+
* IME 光标位置:由光标所在行**渲染后实测**而来(渲染器给的帧内坐标)。
|
|
137
|
+
* 带上 key —— 只有「产生它的那个输入状态」与当前一致时才能用,否则就是上一帧的陈旧值。
|
|
138
|
+
*/
|
|
139
|
+
const [imePosition, setImePosition] = useState(null);
|
|
140
|
+
/** 乐观回显:提交后、真实 user 消息落进 messages 之前,先显示用户提问 */
|
|
141
|
+
const [pendingUser, setPendingUser] = useState(null);
|
|
142
|
+
/**
|
|
143
|
+
* 待发送的粘贴图片(`Ctrl+V` 累积)。
|
|
144
|
+
*
|
|
145
|
+
* 存 base64 而不是落盘:SDK 的 `fileToContentSection()` 正好接受 `{ base64, mime }`,
|
|
146
|
+
* 于是粘贴全程不碰磁盘 —— 也就不会在 `$TMPDIR` 里留下一堆无人清理的 png。
|
|
147
|
+
*/
|
|
148
|
+
const [pastedImages, setPastedImages] = useState([]);
|
|
149
|
+
/** 转录显示起点(/clear 清屏用:隐藏此索引之前的消息,但不改上下文) */
|
|
150
|
+
const [transcriptFrom, setTranscriptFrom] = useState(0);
|
|
151
|
+
/**
|
|
152
|
+
* 当前查看的 Agent 视图:`"main"` 或某个子 Agent 的视图 id。
|
|
153
|
+
*
|
|
154
|
+
* 只影响**消息列表的数据源**(主 Agent 的 `messages` / 子 Agent 自己的 `messages`),
|
|
155
|
+
* 不改上下文、不影响发送目标:发出的消息永远属于主 Agent。
|
|
156
|
+
*/
|
|
157
|
+
const [activeAgentId, setActiveAgentId] = useState("main");
|
|
158
|
+
/** Ctrl+D 双击退出计时 */
|
|
159
|
+
const ctrlDAtRef = useRef(0);
|
|
160
|
+
/**
|
|
161
|
+
* 通知通道:notice / error 走独立栏位,绝不进对话转录(messages)。
|
|
162
|
+
*/
|
|
163
|
+
const noticeChannelRef = useRef(null);
|
|
164
|
+
if (noticeChannelRef.current == null)
|
|
165
|
+
noticeChannelRef.current = new NoticeChannel();
|
|
166
|
+
const noticeChannel = noticeChannelRef.current;
|
|
167
|
+
/**
|
|
168
|
+
* 通知队列(最近的在最后)。通道内部保留最近 `MAX_NOTICE_HISTORY` 条,
|
|
169
|
+
* 这里只做镜像:单槽 state 会让「同一批摘要 / 分阶段进度」只剩最后一条。
|
|
170
|
+
*/
|
|
171
|
+
const [notices, setNotices] = useState(noticeChannel.notices);
|
|
172
|
+
useEffect(() => noticeChannel.subscribe(setNotices), [noticeChannel]);
|
|
173
|
+
/**
|
|
174
|
+
* SDK 的 warn / error 直接进通知栏 —— **通用通道**,不为某个功能单独接线。
|
|
175
|
+
*
|
|
176
|
+
* 以前 SDK 日志被整体接管到文件里(不然 `console.log` 会撕坏 Ink 的帧),代价是
|
|
177
|
+
* 「SDK 说了什么」用户在界面上一个字都看不见,只能事后翻 sdk.log —— 排查成本极高
|
|
178
|
+
* (连「工具文件把进程杀了」这种事都只能靠猜)。这里注册一个 sink 把 warn/error
|
|
179
|
+
* 引过来:以后 SDK 任何模块只要打日志,界面上自动就有,不必为新特性再传一个回调。
|
|
180
|
+
*
|
|
181
|
+
* 晚绑定、启动期缓冲、去重、封顶都在 `sdk-log.ts` 里做,这里只做接线。
|
|
182
|
+
*/
|
|
183
|
+
useEffect(() => {
|
|
184
|
+
setSdkNoticeSink((level, message) => noticeChannel.push(message, level));
|
|
185
|
+
return () => setSdkNoticeSink(null);
|
|
186
|
+
}, [noticeChannel]);
|
|
187
|
+
/**
|
|
188
|
+
* 配置向导宿主(凭据 / 配置中心):整屏接管与「关闭后让改动生效」见
|
|
189
|
+
* use-wizard-host.ts。这里只提供出口:通知走通知通道、重启 / 重载走会话。
|
|
190
|
+
*/
|
|
191
|
+
const { wizardStep, wizardConfig, wizardPresets, wizardAgents, wizardMcp, wizardAgentStore, openWizard, openKeyEditor, closeWizard, } = useWizardHost({
|
|
192
|
+
modelId,
|
|
193
|
+
notify: (text, tone) => noticeChannel.push(text, tone),
|
|
194
|
+
hasSession: () => sessionRef.current != null,
|
|
195
|
+
restart: () => exit({ restart: true, messages: props.messages }),
|
|
196
|
+
reloadSession: () => {
|
|
197
|
+
void sessionRef.current
|
|
198
|
+
?.reload()
|
|
199
|
+
.then(() => noticeChannel.push(t("notice.reloaded")))
|
|
200
|
+
.catch((error) => noticeChannel.push(t("chat.sessionReloadFailed", { error: error?.message ?? error }), "error"));
|
|
201
|
+
},
|
|
202
|
+
// 向导读快照 / 落盘也走注入:集成测试不该碰真实配置目录
|
|
203
|
+
readers: deps.wizardReaders,
|
|
204
|
+
});
|
|
205
|
+
/**
|
|
206
|
+
* 重挂载附带的系统提示(如 `/model` 的切换结果)。
|
|
207
|
+
*
|
|
208
|
+
* 用 ref 而非 props 直读:本值只在首次挂载消费一次,若写进 create 的依赖数组,
|
|
209
|
+
* 会把「展示一条提示」误变成「重建会话」。
|
|
210
|
+
*/
|
|
211
|
+
const initialNoticeRef = useRef(props.initialNotice);
|
|
212
|
+
/** 钩子带来的首条消息:只在会话建好后发一次,重挂载不再重发 */
|
|
213
|
+
const initialPromptRef = useRef(props.initialPrompt);
|
|
214
|
+
/**
|
|
215
|
+
* 渲染信号 + 合帧:SDK 内容变更 → FrameBuffer 合帧 → setTick 触发重渲染。
|
|
216
|
+
* 转录内容始终从 `agent.messages` 现取,tick 只负责「告诉 React 该重画了」。
|
|
217
|
+
*/
|
|
218
|
+
const [, setTick] = useState(0);
|
|
219
|
+
const frameBufferRef = useRef(null);
|
|
220
|
+
if (frameBufferRef.current == null) {
|
|
221
|
+
frameBufferRef.current = new FrameBuffer(() => setTick((t) => t + 1));
|
|
222
|
+
}
|
|
223
|
+
// 思考强度展示:从当前模型的 defaultParams 派生;`/think` 写入后由命令层回调刷新,
|
|
224
|
+
// 切模型(会话重建)时靠 modelId 变化重读。
|
|
225
|
+
useEffect(() => {
|
|
226
|
+
if (!modelId)
|
|
227
|
+
return;
|
|
228
|
+
let alive = true;
|
|
229
|
+
// 档位存在 Agent 定义上(`modelParams`),可选档位在策略表里(`reasoningPreset` 指向),两者都要读
|
|
230
|
+
Promise.all([deps.readConfig(), deps.readAgentStore(), deps.readReasoningPresets()])
|
|
231
|
+
.then(([config, store, presets]) => {
|
|
232
|
+
const model = config.models?.find((item) => item.id === modelId);
|
|
233
|
+
const agentId = props.agentId ?? "default";
|
|
234
|
+
const definition = store.agents.find((item) => item.id === agentId) ?? null;
|
|
235
|
+
if (alive)
|
|
236
|
+
setEffortView(describeEffortView(model, presets, definition));
|
|
237
|
+
})
|
|
238
|
+
.catch(() => {
|
|
239
|
+
if (alive)
|
|
240
|
+
setEffortView(null);
|
|
241
|
+
});
|
|
242
|
+
return () => {
|
|
243
|
+
alive = false;
|
|
244
|
+
};
|
|
245
|
+
// localeTick:切语言后档位文案要重算(它缓存的是已本地化的字符串,不是档位名)
|
|
246
|
+
}, [modelId, props.agentId, localeTick]);
|
|
77
247
|
// 创建会话
|
|
78
248
|
useEffect(() => {
|
|
79
249
|
let disposed = false;
|
|
80
|
-
|
|
81
|
-
|
|
250
|
+
const notifier = {
|
|
251
|
+
onChange: (immediate) => {
|
|
252
|
+
if (disposed)
|
|
253
|
+
return;
|
|
254
|
+
frameBufferRef.current?.push(null, immediate);
|
|
255
|
+
},
|
|
256
|
+
onNotice: (text, tone) => {
|
|
257
|
+
if (!disposed)
|
|
258
|
+
noticeChannel.push(text, tone);
|
|
259
|
+
},
|
|
260
|
+
};
|
|
261
|
+
deps.createSession({
|
|
82
262
|
agentId: props.agentId,
|
|
83
263
|
messages: props.messages,
|
|
84
264
|
conversationId: props.conversationId,
|
|
85
265
|
conversationName: props.conversationName,
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
dispatch(event);
|
|
90
|
-
})
|
|
266
|
+
// 自动迁移:与手动 /migrate、/handoff 共用同一套「交接方式三选一」选择器
|
|
267
|
+
requestMode: (info) => chooseHandoffMode(setPickerState, t("handoff.pickTitle", { used: info.promptTokens, limit: info.handoffTokens })),
|
|
268
|
+
}, notifier)
|
|
91
269
|
.then((session) => {
|
|
92
270
|
if (disposed) {
|
|
93
271
|
session.dispose();
|
|
94
272
|
return;
|
|
95
273
|
}
|
|
96
274
|
sessionRef.current = session;
|
|
275
|
+
setModelId(session.modelId);
|
|
97
276
|
setReady(true);
|
|
277
|
+
// 重挂载附带的提示只需展示一次(消费后置空,避免再次触发)
|
|
278
|
+
if (initialNoticeRef.current) {
|
|
279
|
+
noticeChannel.push(initialNoticeRef.current);
|
|
280
|
+
initialNoticeRef.current = undefined;
|
|
281
|
+
}
|
|
282
|
+
// shell 钩子:会话就绪后立即把原命令当**用户消息**发出去(不经命令解析)
|
|
283
|
+
if (initialPromptRef.current) {
|
|
284
|
+
const prompt = initialPromptRef.current;
|
|
285
|
+
initialPromptRef.current = undefined;
|
|
286
|
+
setTick((t) => t + 1);
|
|
287
|
+
void runSubmit(prompt, { asMessage: true });
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
setTick((t) => t + 1);
|
|
98
291
|
})
|
|
99
292
|
.catch((error) => setBootError(error?.message ?? String(error)));
|
|
100
293
|
return () => {
|
|
101
294
|
disposed = true;
|
|
295
|
+
frameBufferRef.current?.dispose();
|
|
102
296
|
sessionRef.current?.dispose();
|
|
103
297
|
};
|
|
104
298
|
}, [
|
|
105
|
-
props.modelId,
|
|
106
299
|
props.agentId,
|
|
107
300
|
props.messages,
|
|
108
301
|
props.conversationId,
|
|
109
302
|
props.conversationName,
|
|
110
303
|
]);
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
304
|
+
// 速率采样:Spinner 的重渲染只影响它自己,不会带着状态栏一起刷,所以生成中要自己定时读快照。
|
|
305
|
+
// 生成结束(busy → false)时再读一次,拿本轮平均速率并留在状态栏上。
|
|
306
|
+
useEffect(() => {
|
|
307
|
+
const read = () => setRate(sessionRef.current?.rateSample ?? null);
|
|
308
|
+
read();
|
|
309
|
+
if (!busy)
|
|
310
|
+
return;
|
|
311
|
+
const timer = setInterval(read, 250);
|
|
312
|
+
return () => clearInterval(timer);
|
|
313
|
+
}, [busy]);
|
|
314
|
+
/** 清屏但保留上下文:把显示起点推到当前消息末尾 */
|
|
315
|
+
const clearTranscript = () => {
|
|
316
|
+
setTranscriptFrom(sessionRef.current?.messages.length ?? 0);
|
|
317
|
+
noticeChannel.clear();
|
|
116
318
|
};
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
319
|
+
/**
|
|
320
|
+
* 输入编辑(文本 / 光标 / 历史 / kill ring / undo / stash):
|
|
321
|
+
* 全部语义在 use-input-editing.ts,这里只把通知出口接上。
|
|
322
|
+
*/
|
|
323
|
+
const { input, cursor, setInputText, insertAtCursor, replaceRange, deleteBeforeCursor, deleteAfterCursor, deleteWordBeforeCursor, deleteWordAfterCursor, deleteToStartOfLine, deleteToEndOfLine, deletePrevWhitespace, yank, undoEdit, toggleStash, moveCursor, moveCursorToLineStart, moveCursorToLineEnd, moveCursorUpRow, moveCursorDownRow, moveCursorByWord, historyPrev, historyNext, clearDraft, } = useInputEditing({
|
|
324
|
+
initialText: props.initialInput,
|
|
325
|
+
notify: (text, tone) => noticeChannel.push(text, tone),
|
|
326
|
+
// 历史来自 messages(唯一真相源):自带图片,`↑` 回到带图消息时附件也一并回来
|
|
327
|
+
historySource: () => userDrafts(sessionRef.current?.messages ?? []),
|
|
328
|
+
restoreImages: (images) => setPastedImages(images),
|
|
329
|
+
});
|
|
330
|
+
/** 斜杠候选菜单的高亮索引;输入一变就回到第一个候选 */
|
|
331
|
+
const [menuIndex, setMenuIndex] = useState(0);
|
|
332
|
+
useEffect(() => {
|
|
333
|
+
setMenuIndex(0);
|
|
334
|
+
}, [input]);
|
|
335
|
+
/** 项目文件索引(`@` 补全的候选源);null = 还没扫过 */
|
|
336
|
+
const [fileIndex, setFileIndex] = useState(null);
|
|
337
|
+
/** 用户按 Esc 主动关掉了 `@` 菜单:输入 / 光标一变就重新允许弹出 */
|
|
338
|
+
const [mentionOff, setMentionOff] = useState(false);
|
|
339
|
+
useEffect(() => {
|
|
340
|
+
setMentionOff(false);
|
|
341
|
+
}, [input, cursor]);
|
|
342
|
+
// 命令候选人
|
|
343
|
+
const slash = input.startsWith("/") && !input.includes("\n") && !input.includes(" ");
|
|
344
|
+
const hints = slash ? matchCommandHints(input.slice(1)) : [];
|
|
345
|
+
/**
|
|
346
|
+
* `@` 文件补全的上下文:光标是否停在一个 `@token` 里(判据见 `mention.ts`)。
|
|
347
|
+
*
|
|
348
|
+
* 与斜杠命令**互斥**:`/` 开头就是在写命令,此时不弹文件菜单。
|
|
349
|
+
*/
|
|
350
|
+
const mention = slash || mentionOff ? null : extractMention(input, cursor);
|
|
351
|
+
const fileMatches = mention && fileIndex ? rankPaths(mention.query, fileIndex) : [];
|
|
352
|
+
/** 索引还没就绪:菜单先占一行「扫描中…」,否则敲下 `@` 后一片空白像没反应 */
|
|
353
|
+
const scanningFiles = mention != null && fileIndex == null;
|
|
354
|
+
/** 当前菜单条数(两种菜单互斥,非零的那个说了算) */
|
|
355
|
+
const menuCount = slash ? hints.length : fileMatches.length;
|
|
356
|
+
/**
|
|
357
|
+
* 菜单**想吃**的行数:候选条数 + 「扫描中…」占位。
|
|
358
|
+
*
|
|
359
|
+
* 实际能拿多少由布局决定(`computeChatLayout` 按输入框上方的剩余空间放行,
|
|
360
|
+
* 不设固定上限)—— 这里交上去的是「想要多少」,渲染与 IME 光标一律读回
|
|
361
|
+
* `layout.hintRows`,三处同源。
|
|
362
|
+
*/
|
|
363
|
+
const menuWant = slash ? hints.length : fileMatches.length + (scanningFiles ? 1 : 0);
|
|
364
|
+
/** 菜单条目:斜杠命令带说明,`@` 路径目录带尾斜杠(提示「选中后继续下钻」) */
|
|
365
|
+
const menuItems = slash
|
|
366
|
+
? hints.map((hint) => ({ key: hint.names[0], label: hint.label, hint: hint.description }))
|
|
367
|
+
: fileMatches.map((entry) => ({
|
|
368
|
+
key: entry.path,
|
|
369
|
+
label: entry.dir ? `${entry.path}/` : entry.path,
|
|
370
|
+
}));
|
|
371
|
+
/** 每次「重新唤起 `@`」都问一次索引:`listProjectPaths` 内部有 15s 缓存,未过期时几乎零成本,
|
|
372
|
+
* 过期则顺手重扫 —— 于是会话里新建的文件不会因为「只扫过一次」而永远看不到。 */
|
|
373
|
+
useEffect(() => {
|
|
374
|
+
if (mention == null)
|
|
375
|
+
return;
|
|
376
|
+
let alive = true;
|
|
377
|
+
void deps.listProjectPaths(process.cwd())
|
|
378
|
+
.then((paths) => {
|
|
379
|
+
if (alive)
|
|
380
|
+
setFileIndex(paths);
|
|
381
|
+
})
|
|
382
|
+
.catch(() => {
|
|
383
|
+
if (alive)
|
|
384
|
+
setFileIndex([]);
|
|
121
385
|
});
|
|
386
|
+
return () => {
|
|
387
|
+
alive = false;
|
|
388
|
+
};
|
|
389
|
+
// 只在「从无菜单到有菜单」时触发:同一 token 内每敲一个键都重扫毫无意义
|
|
390
|
+
}, [mention == null]);
|
|
391
|
+
/**
|
|
392
|
+
* 把 `@` 菜单里高亮的那条补进草稿。
|
|
393
|
+
*
|
|
394
|
+
* 落字走 `replaceRange`:替换的是**整个 token**(`@src/tu` 这种半截路径、或光标停在
|
|
395
|
+
* 中间时右半边残留),并且会记一条撤销快照 —— 补错了 `Ctrl+_` 能退回。
|
|
396
|
+
* 目录带尾斜杠,补完菜单会重新过滤出该目录下的内容,于是可以一路钻下去。
|
|
397
|
+
*/
|
|
398
|
+
const completeMentionSelection = () => {
|
|
399
|
+
if (!mention || fileMatches.length === 0)
|
|
400
|
+
return;
|
|
401
|
+
const entry = fileMatches[Math.max(0, Math.min(menuIndex, fileMatches.length - 1))];
|
|
402
|
+
if (!entry)
|
|
403
|
+
return;
|
|
404
|
+
replaceRange(mention.start, mention.end, mentionInsertText(entry.path, entry.dir));
|
|
122
405
|
};
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
dispatch({ type: "error", text: `读取配置失败:${error?.message ?? error}` });
|
|
138
|
-
}
|
|
139
|
-
};
|
|
140
|
-
/** `/key`:直达「当前模型所用端点」的 API Key 编辑步骤 */
|
|
141
|
-
const openKeyEditor = async () => {
|
|
142
|
-
try {
|
|
143
|
-
const config = await readConfig();
|
|
144
|
-
const credential = resolveCredential(config, props.modelId);
|
|
145
|
-
if (!credential) {
|
|
146
|
-
dispatch({
|
|
147
|
-
type: "error",
|
|
148
|
-
text: `模型 ${props.modelId} 未绑定可用端点,请用 /config 检查配置`,
|
|
149
|
-
});
|
|
406
|
+
/**
|
|
407
|
+
* `Ctrl+V`:**先问剪贴板要图片,没有图片就退回粘贴文本**。
|
|
408
|
+
*
|
|
409
|
+
* 顺序不能反 —— 文本一旦先粘进去,再发现「其实剪贴板里是图片」就晚了(草稿里已经
|
|
410
|
+
* 多了一坨东西)。所以永远是「图片优先、文本兑底」,这也就是用户按 Ctrl+V 的默认期待。
|
|
411
|
+
*
|
|
412
|
+
* 异步不阻塞输入:`readClipboardImage()` 要 spawn 一个子进程(几十 ms),期间用户
|
|
413
|
+
* 还能继续打字;回到结果时只做一次 setState / 插字。
|
|
414
|
+
*/
|
|
415
|
+
const pasteClipboard = async () => {
|
|
416
|
+
const result = await deps.readClipboardImage();
|
|
417
|
+
if (result.kind === "image") {
|
|
418
|
+
if (pastedImages.length >= MAX_PASTED_IMAGES) {
|
|
419
|
+
noticeChannel.push(t("notice.imageLimit", { n: MAX_PASTED_IMAGES }), "error");
|
|
150
420
|
return;
|
|
151
421
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
}
|
|
156
|
-
catch (error) {
|
|
157
|
-
dispatch({ type: "error", text: `读取配置失败:${error?.message ?? error}` });
|
|
422
|
+
setPastedImages((list) => [...list, result.image]);
|
|
423
|
+
noticeChannel.push(t("notice.imagePasted", { n: pastedImages.length + 1 }));
|
|
424
|
+
return;
|
|
158
425
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
setWizardStep(null);
|
|
163
|
-
setWizardConfig(null);
|
|
164
|
-
setWizardMcp(null);
|
|
165
|
-
setWizardAgentStore(null);
|
|
166
|
-
for (const line of result.summary)
|
|
167
|
-
dispatch({ type: "notice", text: `配置已更新:${line}` });
|
|
168
|
-
if (!result.dirty)
|
|
169
|
-
return;
|
|
170
|
-
// 会话尚未建立(启动失败进入向导):重挂载对话屏重试
|
|
171
|
-
if (!sessionRef.current) {
|
|
172
|
-
exit({ restart: true, messages: props.messages });
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
const endpointChanged = !!currentEndpointRef.current && result.changedEndpointIds.includes(currentEndpointRef.current);
|
|
176
|
-
if (endpointChanged || result.mcpChanged || result.agentsChanged) {
|
|
177
|
-
sessionRef.current
|
|
178
|
-
.reload()
|
|
179
|
-
.then(() => dispatch({ type: "notice", text: "已按新配置重建会话(消息与上下文保留)" }))
|
|
180
|
-
.catch((error) => dispatch({ type: "error", text: `重建会话失败:${error?.message ?? error}` }));
|
|
426
|
+
if (result.kind === "too-large") {
|
|
427
|
+
noticeChannel.push(t("notice.imageTooLarge", { size: formatMegabytes(result.bytes) }), "error");
|
|
428
|
+
return;
|
|
181
429
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
const L = layoutRef.current;
|
|
187
|
-
if (!L)
|
|
188
|
-
return;
|
|
189
|
-
const maxTop = Math.max(0, L.totalLines - L.budget);
|
|
190
|
-
const clamped = Math.max(0, Math.min(maxTop, nextTop));
|
|
191
|
-
setViewTop(clamped >= maxTop ? null : clamped);
|
|
192
|
-
};
|
|
193
|
-
/** 翻页滚动(dir:-1 上翻 / +1 下翻) */
|
|
194
|
-
const scrollByPage = (dir) => {
|
|
195
|
-
const L = layoutRef.current;
|
|
196
|
-
if (!L)
|
|
430
|
+
// 没有图片 → 当成普通粘贴:把剪贴板文本插到光标处
|
|
431
|
+
const pasted = await deps.readClipboardText();
|
|
432
|
+
if (pasted.kind === "unavailable") {
|
|
433
|
+
noticeChannel.push(t("notice.clipboardUnavailable"), "error");
|
|
197
434
|
return;
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
435
|
+
}
|
|
436
|
+
if (pasted.kind === "too-large") {
|
|
437
|
+
noticeChannel.push(t("notice.textTooLarge", {
|
|
438
|
+
size: Math.round(pasted.bytes / 1024),
|
|
439
|
+
limit: MAX_CLIPBOARD_TEXT_BYTES / 1024,
|
|
440
|
+
}), "error");
|
|
204
441
|
return;
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
const scrollToBottom = () => setViewTop(null);
|
|
209
|
-
// ---- 输入编辑(光标感知)----
|
|
210
|
-
/** 替换整个输入并把光标移到末尾 */
|
|
211
|
-
const setInputText = (text) => {
|
|
212
|
-
setInput(text);
|
|
213
|
-
setCursor(Array.from(text).length);
|
|
214
|
-
};
|
|
215
|
-
/** 在光标处插入文本 */
|
|
216
|
-
const insertAtCursor = (text) => {
|
|
217
|
-
const chars = Array.from(input);
|
|
218
|
-
const cp = Math.max(0, Math.min(cursor, chars.length));
|
|
219
|
-
setInput(chars.slice(0, cp).join("") + text + chars.slice(cp).join(""));
|
|
220
|
-
setCursor(cp + Array.from(text).length);
|
|
221
|
-
};
|
|
222
|
-
/** 删除光标前一个字符(Backspace) */
|
|
223
|
-
const deleteBeforeCursor = () => {
|
|
224
|
-
const chars = Array.from(input);
|
|
225
|
-
const cp = Math.max(0, Math.min(cursor, chars.length));
|
|
226
|
-
if (cp === 0)
|
|
227
|
-
return;
|
|
228
|
-
setInput(chars.slice(0, cp - 1).join("") + chars.slice(cp).join(""));
|
|
229
|
-
setCursor(cp - 1);
|
|
230
|
-
};
|
|
231
|
-
/** 删除光标后一个字符(Delete) */
|
|
232
|
-
const deleteAfterCursor = () => {
|
|
233
|
-
const chars = Array.from(input);
|
|
234
|
-
const cp = Math.max(0, Math.min(cursor, chars.length));
|
|
235
|
-
if (cp >= chars.length)
|
|
442
|
+
}
|
|
443
|
+
if (pasted.text.length === 0) {
|
|
444
|
+
noticeChannel.push(t("notice.clipboardEmpty"));
|
|
236
445
|
return;
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
const moveCursor = (delta) => {
|
|
241
|
-
const len = Array.from(input).length;
|
|
242
|
-
setCursor((c) => Math.max(0, Math.min(len, c + delta)));
|
|
243
|
-
};
|
|
244
|
-
/** 光标移到当前逻辑行行首(Home) */
|
|
245
|
-
const moveCursorToLineStart = () => {
|
|
246
|
-
const chars = Array.from(input);
|
|
247
|
-
const cp = Math.max(0, Math.min(cursor, chars.length));
|
|
248
|
-
let i = cp;
|
|
249
|
-
while (i > 0 && chars[i - 1] !== "\n")
|
|
250
|
-
i -= 1;
|
|
251
|
-
setCursor(i);
|
|
252
|
-
};
|
|
253
|
-
/** 光标移到当前逻辑行行尾(End) */
|
|
254
|
-
const moveCursorToLineEnd = () => {
|
|
255
|
-
const chars = Array.from(input);
|
|
256
|
-
const cp = Math.max(0, Math.min(cursor, chars.length));
|
|
257
|
-
let i = cp;
|
|
258
|
-
while (i < chars.length && chars[i] !== "\n")
|
|
259
|
-
i += 1;
|
|
260
|
-
setCursor(i);
|
|
446
|
+
}
|
|
447
|
+
// 去掉末尾单个换行:剪贴板文本几乎总带一个尾换行,粘完光标原地跳行很莫妙
|
|
448
|
+
insertAtCursor(pasted.text.replace(/\n$/, ""));
|
|
261
449
|
};
|
|
262
|
-
|
|
263
|
-
const slash = input.startsWith("/") && !input.includes("\n") && !input.includes(" ");
|
|
264
|
-
const hints = slash ? matchCommandHints(input.slice(1)) : [];
|
|
265
|
-
const runSubmit = async (text) => {
|
|
450
|
+
const runSubmit = async (text, options = {}) => {
|
|
266
451
|
const trimmed = text.trim();
|
|
267
452
|
if (!trimmed || !sessionRef.current || busy)
|
|
268
453
|
return;
|
|
269
|
-
|
|
270
|
-
historyIndex.current = -1;
|
|
454
|
+
clearDraft();
|
|
271
455
|
scrollToBottom();
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
456
|
+
// 发出去的消息永远属于主 Agent:停在子 Agent 视图上会让用户看不到自己的提问
|
|
457
|
+
setActiveAgentId("main");
|
|
458
|
+
noticeChannel.clear();
|
|
459
|
+
// 斜杠命令:命令名怎么解析、哪些命令会调用模型(要拦输入框),都来自命令元数据
|
|
460
|
+
const command = options.asMessage ? null : parseCommandInput(trimmed);
|
|
461
|
+
if (command) {
|
|
462
|
+
const name = command.name;
|
|
463
|
+
const blocksInput = commandBlocksInput(name);
|
|
464
|
+
if (blocksInput) {
|
|
465
|
+
setBusy(true);
|
|
466
|
+
setBusyCommand(name);
|
|
467
|
+
}
|
|
468
|
+
try {
|
|
469
|
+
await runCommand(name, trimmed, {
|
|
470
|
+
session: sessionRef.current,
|
|
471
|
+
notice: (text, tone) => noticeChannel.push(text, tone),
|
|
472
|
+
clearTranscript,
|
|
473
|
+
rows: rows ?? 24,
|
|
474
|
+
exit,
|
|
475
|
+
suspendTerminal,
|
|
476
|
+
openWizard,
|
|
477
|
+
openKeyEditor,
|
|
478
|
+
doExit,
|
|
479
|
+
restartProcess,
|
|
480
|
+
setPickerState,
|
|
481
|
+
setConfirmState,
|
|
482
|
+
setInputText,
|
|
483
|
+
setEffortView,
|
|
484
|
+
refreshLocale: () => bumpLocale((n) => n + 1),
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
finally {
|
|
488
|
+
if (blocksInput) {
|
|
489
|
+
setBusy(false);
|
|
490
|
+
setBusyCommand(null);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
const base = sessionRef.current.messages.filter((m) => m.role === AgentNS.Role.User).length;
|
|
496
|
+
// 先取快照再清空:`send` 要用同一个数组,而输入区要立刻回到「没有附件」的状态
|
|
497
|
+
const images = pastedImages;
|
|
498
|
+
setPastedImages([]);
|
|
499
|
+
setPendingUser({ text: trimmed, base, images });
|
|
500
|
+
setBusy(true);
|
|
501
|
+
try {
|
|
502
|
+
await sessionRef.current.send(trimmed, images);
|
|
503
|
+
}
|
|
504
|
+
finally {
|
|
505
|
+
setBusy(false);
|
|
506
|
+
setPendingUser(null);
|
|
507
|
+
}
|
|
291
508
|
};
|
|
292
509
|
const doExit = () => {
|
|
293
|
-
// 会话已由 ConversationPersistPlugin 实时落盘,退出无需再询问是否保存
|
|
294
510
|
exit("done");
|
|
295
511
|
};
|
|
512
|
+
/**
|
|
513
|
+
* 真·重启进程(`/update` 装完新版本后调用)。
|
|
514
|
+
*
|
|
515
|
+
* 不能复用 `exit({ restart: true })`:那条路只是 `continue` 回 `mount(<Chat/>)`,
|
|
516
|
+
* 同一个进程、内存里还是旧模块图——对「磁盘上的代码变了」这种升级等于没装。
|
|
517
|
+
* 这里退到 shell 的视角:**spawn 一个新的自己**(新进程重新加载新 dist),
|
|
518
|
+
* 父进程在挂起态里等子进程退出,然后跟着退出。
|
|
519
|
+
*
|
|
520
|
+
* 三个必须注意的细节:
|
|
521
|
+
*
|
|
522
|
+
* 1. **在挂起态里退出**。`suspendTerminal` 的 `beginSuspend()` 会 `pauseInput()`
|
|
523
|
+
* (raw 模式关闭),而 `endSuspend()` 会 `resumeInput()` 把它重新打开。若等
|
|
524
|
+
* `suspendTerminal` 返回后再 `process.exit`,终端就停在 raw 模式,回到 shell
|
|
525
|
+
* 后按键全是乱码。所以子进程一结束就**就地**退出。
|
|
526
|
+
* 2. **先关鼠标上报**。`mount()` 按 Ink 实例开关它,而本实例此刻只是「挂起」并未
|
|
527
|
+
* 退出 —— 不关的话,子进程若异常死亡,终端会一直把滚轮写成转义序列。
|
|
528
|
+
* 3. **不转发 `process.argv.slice(2)`**。`ai --tui --new "..."` 那套是 shell 兜底
|
|
529
|
+
* 钩子的一次性载荷,重启时重放会把同一条提示词再发一遍。新进程按常规启动即
|
|
530
|
+
* 续接上一轮会话(重启前已落盘)。
|
|
531
|
+
*/
|
|
532
|
+
const restartProcess = () => suspendTerminal(() => new Promise((resolve) => {
|
|
533
|
+
const entry = process.argv[1];
|
|
534
|
+
if (!entry) {
|
|
535
|
+
// 拿不到入口脚本(理论上不会发生):不重启,退回 TUI,由调用方收尾
|
|
536
|
+
resolve();
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
disableMouseTracking();
|
|
540
|
+
const child = spawn(process.execPath, [entry], { stdio: "inherit" });
|
|
541
|
+
child.on("error", () => process.exit(1));
|
|
542
|
+
child.on("exit", (code) => process.exit(code ?? 0));
|
|
543
|
+
}));
|
|
544
|
+
/** 本次会话已出现的子 Agent(渲染期现取;`sessionRef` 在首次渲染时为 null) */
|
|
545
|
+
const subAgentViews = sessionRef.current?.subAgents ?? [];
|
|
546
|
+
/**
|
|
547
|
+
* Agent 栏目切换(← / →):在「主 Agent + 各子 Agent」之间循环。
|
|
548
|
+
*
|
|
549
|
+
* 键位分流见下方两个 `useInput`:输入框**为空**或**生成中**(输入框已被 Spinner 顶替)
|
|
550
|
+
* 时,左右键本来就无事可做,正好用来切换;输入框**有内容**时左右键仍然是移光标——
|
|
551
|
+
* 不能为了切栏目把文本编辑的左右移动废掉。
|
|
552
|
+
*/
|
|
553
|
+
const switchAgentView = (dir) => {
|
|
554
|
+
const ids = ["main", ...subAgentViews.map((item) => item.id)];
|
|
555
|
+
// 旧 id 已失效(会话重建)时 indexOf 为 -1,归一后从主 Agent 起步
|
|
556
|
+
const current = Math.max(0, ids.indexOf(activeAgentId));
|
|
557
|
+
setActiveAgentId(ids[(current + dir + ids.length) % ids.length]);
|
|
558
|
+
scrollToBottom();
|
|
559
|
+
};
|
|
560
|
+
/**
|
|
561
|
+
* 两个 `useInput`(空闲 / 忙碌)共用的键位上下文 —— 判定逻辑在 common-keys.ts。
|
|
562
|
+
* 放这里而不是各自内联,是为了让「什么时候能滚、Ctrl+D 怎么退」只有一份定义。
|
|
563
|
+
*/
|
|
564
|
+
const scrollKeys = { scrollByWheel, scrollByPage, scrollByLine };
|
|
565
|
+
const ctrlDExit = {
|
|
566
|
+
lastAt: ctrlDAtRef,
|
|
567
|
+
notify: () => noticeChannel.push(t("notice.pressCtrlDAgain")),
|
|
568
|
+
exit: doExit,
|
|
569
|
+
};
|
|
296
570
|
// 输入处理
|
|
297
571
|
useInput((char, key) => {
|
|
298
|
-
//
|
|
572
|
+
// 按键诊断(AIZ_DEBUG_KEYS=1):必须在任何 return 之前记录,否则被吞掉的键不会留痕
|
|
573
|
+
keyDebug(char, key);
|
|
299
574
|
if (bootError) {
|
|
300
575
|
if (char === "k" || char === "K") {
|
|
301
576
|
void openWizard({ kind: "menu" });
|
|
@@ -305,106 +580,184 @@ export function Chat(props) {
|
|
|
305
580
|
exit("done");
|
|
306
581
|
return;
|
|
307
582
|
}
|
|
308
|
-
//
|
|
309
|
-
if (
|
|
583
|
+
// 回看历史类按键(滚轮 / PgUp/PgDn / ⌘[ ⌘] / ⌘; ⌘' / Alt+V)与忙碌态共用
|
|
584
|
+
if (handleScrollKey(char, key, scrollKeys))
|
|
310
585
|
return;
|
|
311
|
-
// 翻页滚动(回看历史)
|
|
312
|
-
if (key.pageUp) {
|
|
313
|
-
scrollByPage(-1);
|
|
314
|
-
return;
|
|
315
|
-
}
|
|
316
|
-
if (key.pageDown) {
|
|
317
|
-
scrollByPage(1);
|
|
318
|
-
return;
|
|
319
|
-
}
|
|
320
|
-
// 换行:Ctrl+Enter / Alt+Enter / Shift+Enter
|
|
321
|
-
// (仅当终端**原生**以 Kitty CSI-u 序列上报这些组合键时才生效 —— 本 CLI 不再
|
|
322
|
-
// 主动查询/开启该协议,见 tui/index.tsx 的 RENDER_OPTIONS;换行主推 Ctrl+N)
|
|
323
586
|
if (key.return && (key.ctrl || key.meta || key.shift)) {
|
|
324
587
|
insertAtCursor("\n");
|
|
325
588
|
return;
|
|
326
589
|
}
|
|
327
590
|
if (char === "c" && key.ctrl) {
|
|
591
|
+
// Claude Code:生成中中断;空闲时有草稿就先清空,已空则退出
|
|
328
592
|
if (busy)
|
|
329
593
|
sessionRef.current?.abort();
|
|
594
|
+
else if (input.length > 0) {
|
|
595
|
+
setInputText("");
|
|
596
|
+
noticeChannel.push(t("notice.inputCleared"));
|
|
597
|
+
}
|
|
330
598
|
else
|
|
331
599
|
void doExit();
|
|
332
600
|
return;
|
|
333
601
|
}
|
|
334
|
-
if (char === "d" && key.ctrl) {
|
|
335
|
-
|
|
602
|
+
if (char === "d" && key.ctrl && input.length > 0) {
|
|
603
|
+
// Claude Code:有草稿时删光标后一个字符(空闲态独有)
|
|
604
|
+
deleteAfterCursor();
|
|
336
605
|
return;
|
|
337
606
|
}
|
|
607
|
+
if (handleCtrlDExit(char, key, ctrlDExit))
|
|
608
|
+
return;
|
|
338
609
|
if (char === "l" && key.ctrl) {
|
|
339
|
-
|
|
610
|
+
clearTranscript();
|
|
340
611
|
return;
|
|
341
612
|
}
|
|
613
|
+
// ---- kill / yank / undo / stash(照抄 Claude Code / readline)----
|
|
342
614
|
if (char === "u" && key.ctrl) {
|
|
343
|
-
|
|
615
|
+
// 删到行首。macOS 上 ⌘⌫ 会被终端映射成 Ctrl+U,故不能做成「清空全部」
|
|
616
|
+
deleteToStartOfLine();
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
if (char === "k" && key.ctrl) {
|
|
620
|
+
deleteToEndOfLine();
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
if (char === "w" && key.ctrl) {
|
|
624
|
+
// 删到前一空白(标点不切分:`src/utils/foo.ts` 一次删完)
|
|
625
|
+
deletePrevWhitespace();
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
if (char === "y" && key.ctrl) {
|
|
629
|
+
yank(false);
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
if (char === "y" && key.meta) {
|
|
633
|
+
yank(true);
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
// Ctrl+_ / Ctrl+Shift+-:撤销上一次输入编辑
|
|
637
|
+
if ((char === "_" && key.ctrl) || (char === "-" && key.ctrl && key.shift)) {
|
|
638
|
+
undoEdit();
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
if (char === "s" && key.ctrl) {
|
|
642
|
+
toggleStash();
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
if ((char === "v" && key.ctrl) || (key.super && (char === "v" || char === "V"))) {
|
|
646
|
+
// 剪贴板里有图片就附在消息上(发送时转成内容块),没有就退回粘贴文本
|
|
647
|
+
void pasteClipboard();
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
// readline 的行首 / 行尾(macOS 上部分终端把 ⌘← / ⌘→ 发成 ^A / ^E)
|
|
651
|
+
if (char === "a" && key.ctrl) {
|
|
652
|
+
moveCursorToLineStart();
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
if (char === "e" && key.ctrl) {
|
|
656
|
+
moveCursorToLineEnd();
|
|
344
657
|
return;
|
|
345
658
|
}
|
|
346
659
|
if (char === "j" && key.ctrl) {
|
|
347
660
|
insertAtCursor("\n");
|
|
348
661
|
return;
|
|
349
662
|
}
|
|
350
|
-
//
|
|
351
|
-
if (char === "n" && key.ctrl) {
|
|
352
|
-
|
|
663
|
+
// Ctrl+P / Ctrl+N:历史上下(Claude Code 语义;换行改用 Ctrl+J / Alt+Enter)
|
|
664
|
+
if ((char === "p" && key.ctrl) || (char === "n" && key.ctrl)) {
|
|
665
|
+
const up = char === "p";
|
|
666
|
+
if (menuCount > 0) {
|
|
667
|
+
// 同上:视觉「上」(Ctrl+P)是 +1
|
|
668
|
+
setMenuIndex((i) => (up ? (i + 1) % menuCount : (i - 1 + menuCount) % menuCount));
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
if (up)
|
|
672
|
+
historyPrev();
|
|
673
|
+
else
|
|
674
|
+
historyNext();
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
// 按词删除:Alt+Backspace / Ctrl+Backspace 删前一个词;Alt+D / Ctrl+Delete 删后一个词。
|
|
678
|
+
// 必须在下面「单字符 Backspace/Delete」之前判定,否则带修饰键的删除只掉一个字符。
|
|
679
|
+
if (key.backspace && (key.ctrl || key.meta || key.super)) {
|
|
680
|
+
deleteWordBeforeCursor();
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
if (key.delete && (key.ctrl || key.meta || key.super)) {
|
|
684
|
+
deleteWordAfterCursor();
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
if (char === "d" && key.meta) {
|
|
688
|
+
deleteWordAfterCursor();
|
|
353
689
|
return;
|
|
354
690
|
}
|
|
355
691
|
if (key.return) {
|
|
356
|
-
//
|
|
692
|
+
// `@` 菜单开着时回车 = 补全(不发送):让「选完继续接着写」成为默认动作,
|
|
693
|
+
// 否则选完就得再敲一个空格才能换行/继续,很容易误发半截消息。
|
|
694
|
+
if (mention && fileMatches.length > 0) {
|
|
695
|
+
completeMentionSelection();
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
357
698
|
void runSubmit(resolveSubmitText(input, hints, menuIndex));
|
|
358
699
|
return;
|
|
359
700
|
}
|
|
360
|
-
if (key.tab &&
|
|
701
|
+
if (key.tab && menuCount > 0) {
|
|
702
|
+
if (mention) {
|
|
703
|
+
completeMentionSelection();
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
361
706
|
setInputText(`/${hints[menuIndex]?.names[0] ?? ""} `);
|
|
362
707
|
return;
|
|
363
708
|
}
|
|
364
709
|
if (key.upArrow || key.downArrow) {
|
|
365
|
-
// Shift / Alt + ↑↓:单行滚动(优先于历史召回)
|
|
366
710
|
if (key.shift || key.meta) {
|
|
367
711
|
scrollByLine(key.upArrow ? -1 : 1);
|
|
368
712
|
return;
|
|
369
713
|
}
|
|
370
|
-
if (
|
|
371
|
-
|
|
714
|
+
if (menuCount > 0) {
|
|
715
|
+
// 底部锚定菜单里**下标大 = 视觉靠上**(`items[0]` 贴着输入框,越往后越往上),
|
|
716
|
+
// 所以视觉「上」是 +1、视觉「下」是 −1;取模环绕:到顶溢出回到底部、到底溢出回到顶部。
|
|
717
|
+
// 写反了方向就与视觉相反 —— 只有 ≥ 3 条候选时才能从测试里看出来。
|
|
718
|
+
setMenuIndex((i) => key.upArrow ? (i + 1) % menuCount : (i - 1 + menuCount) % menuCount);
|
|
372
719
|
return;
|
|
373
720
|
}
|
|
374
|
-
//
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
else
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
historyIndex.current = idx;
|
|
388
|
-
setInputText(history[idx]);
|
|
389
|
-
}
|
|
390
|
-
}
|
|
721
|
+
// 多行输入:先按**视觉行**走光标(含软折行,宽度与渲染一致),走到首/末行才翻历史
|
|
722
|
+
const wrapWidth = Math.max(4, (columns ?? 80) - 4);
|
|
723
|
+
if (key.upArrow ? moveCursorUpRow(wrapWidth) : moveCursorDownRow(wrapWidth))
|
|
724
|
+
return;
|
|
725
|
+
if (key.upArrow)
|
|
726
|
+
historyPrev();
|
|
727
|
+
else
|
|
728
|
+
historyNext();
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
// Agent 栏目:输入框为空时光标停在 0,←/→ 本无作用 → 拿来切换主 / 子 Agent
|
|
732
|
+
if ((key.leftArrow || key.rightArrow) && input.length === 0 && subAgentViews.length > 0) {
|
|
733
|
+
switchAgentView(key.leftArrow ? -1 : 1);
|
|
391
734
|
return;
|
|
392
735
|
}
|
|
393
|
-
// 光标移动:← → 单字符,Ctrl/Alt+← → 跨词,Home / End 到行首 / 行尾
|
|
394
736
|
if (key.leftArrow) {
|
|
395
|
-
if (key.ctrl || key.meta)
|
|
396
|
-
|
|
737
|
+
if (key.ctrl || key.meta || key.super)
|
|
738
|
+
moveCursorByWord(-1);
|
|
397
739
|
else
|
|
398
740
|
moveCursor(-1);
|
|
399
741
|
return;
|
|
400
742
|
}
|
|
401
743
|
if (key.rightArrow) {
|
|
402
|
-
if (key.ctrl || key.meta)
|
|
403
|
-
|
|
744
|
+
if (key.ctrl || key.meta || key.super)
|
|
745
|
+
moveCursorByWord(1);
|
|
404
746
|
else
|
|
405
747
|
moveCursor(1);
|
|
406
748
|
return;
|
|
407
749
|
}
|
|
750
|
+
// 跨词移动的「兜底键」:macOS 上 Option+←/→ 往往发的是 ESC b / ESC f
|
|
751
|
+
// (readline 的 backward-word / forward-word),并不带 leftArrow 标记;
|
|
752
|
+
// 而 Ctrl+←/→ 常被系统「Mission Control → 左右切换空间」截走,应用根本收不到。
|
|
753
|
+
if (char === "b" && key.meta) {
|
|
754
|
+
moveCursorByWord(-1);
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
if (char === "f" && key.meta) {
|
|
758
|
+
moveCursorByWord(1);
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
408
761
|
if (key.home) {
|
|
409
762
|
moveCursorToLineStart();
|
|
410
763
|
return;
|
|
@@ -414,8 +767,25 @@ export function Chat(props) {
|
|
|
414
767
|
return;
|
|
415
768
|
}
|
|
416
769
|
if (key.escape) {
|
|
417
|
-
|
|
418
|
-
|
|
770
|
+
// `@` 菜单开着时,Esc 只关菜单:清草稿太粗暴(`@` 打一半按 Esc 会连正文一起没)
|
|
771
|
+
if (mention && menuWant > 0) {
|
|
772
|
+
setMentionOff(true);
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
// 有附件时先清附件(草稿保留)——误粘一张图后总得有个退路,
|
|
776
|
+
// 而又不能为了这个把 ESC 变成「清空一切」的随机键
|
|
777
|
+
if (!busy && pastedImages.length > 0) {
|
|
778
|
+
setPastedImages([]);
|
|
779
|
+
noticeChannel.push(t("notice.imagesCleared"));
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
// Claude Code:生成中 Esc = 中断本轮;否则清空草稿并回到底部
|
|
783
|
+
if (busy)
|
|
784
|
+
sessionRef.current?.abort();
|
|
785
|
+
else {
|
|
786
|
+
setInputText("");
|
|
787
|
+
scrollToBottom();
|
|
788
|
+
}
|
|
419
789
|
return;
|
|
420
790
|
}
|
|
421
791
|
if (key.backspace) {
|
|
@@ -426,70 +796,195 @@ export function Chat(props) {
|
|
|
426
796
|
deleteAfterCursor();
|
|
427
797
|
return;
|
|
428
798
|
}
|
|
429
|
-
if (char && !key.ctrl && !key.meta) {
|
|
799
|
+
if (char && !key.ctrl && !key.meta && !key.super) {
|
|
430
800
|
insertAtCursor(char);
|
|
431
801
|
}
|
|
432
|
-
},
|
|
802
|
+
},
|
|
803
|
+
// 生成 / 自动迁移期间主输入框整体失活:此时输入框已被 Spinner 顶替,若仍激活,
|
|
804
|
+
// 按键会被静默吞进隐藏的输入缓冲、待本轮结束后才冒出来(即「迁移过程中输入框
|
|
805
|
+
// 没被拦住」)。失活后改由下方「忙碌期」处理器只放行中止 / 退出 / 回看滚动。
|
|
806
|
+
{ isActive: !busy && !confirmState && !pickerState && !wizardStep });
|
|
807
|
+
// 忙碌期(生成 / 迁移中)专用:输入框不可编辑,仅保留「中止(Ctrl+C) / 退出(Ctrl+D) / 回看滚动」。
|
|
808
|
+
useInput((char, key) => {
|
|
809
|
+
keyDebug(char, key);
|
|
810
|
+
// 忙碌期只剩「中止 / 退出 / 回看」,滚轮同样生效(生成时正是最想往回看的时机)
|
|
811
|
+
if (handleScrollKey(char, key, scrollKeys))
|
|
812
|
+
return;
|
|
813
|
+
if (char === "c" && key.ctrl) {
|
|
814
|
+
sessionRef.current?.abort();
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
if (key.escape) {
|
|
818
|
+
// 忙碌期 Esc 也充当「中断」(与 Claude Code 一致)
|
|
819
|
+
sessionRef.current?.abort();
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
if (handleCtrlDExit(char, key, ctrlDExit))
|
|
823
|
+
return;
|
|
824
|
+
if ((key.upArrow || key.downArrow) && (key.shift || key.meta || key.super)) {
|
|
825
|
+
scrollByLine(key.upArrow ? -1 : 1);
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
// Agent 栏目:生成中输入框已失活,←/→ 空闲 → 直接切换(此时最想看子 Agent 在干什么)
|
|
829
|
+
if ((key.leftArrow || key.rightArrow) && subAgentViews.length > 0) {
|
|
830
|
+
switchAgentView(key.leftArrow ? -1 : 1);
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
if (char === "v" && key.super) {
|
|
834
|
+
scrollByPage(-1);
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
}, { isActive: busy && !confirmState && !pickerState && !wizardStep });
|
|
433
838
|
// 配置向导:整屏接管(凭据设置 / 配置中心),关闭后回到对话屏
|
|
434
839
|
if (wizardStep && wizardConfig) {
|
|
435
|
-
return (_jsx(ConfigWizard, { config: wizardConfig, initialStep: wizardStep, closeOnSave: wizardStep.kind !== "menu", agents: wizardAgents, mcp: wizardMcp ?? undefined, agentStore: wizardAgentStore ?? undefined, onSave: saveConfig, onSaveMcp: async (scope, cfg) => {
|
|
436
|
-
await writeMcpConfig(cfg, scope);
|
|
840
|
+
return (_jsx(ConfigWizard, { config: wizardConfig, reasoningPresets: wizardPresets, initialStep: wizardStep, closeOnSave: wizardStep.kind !== "menu", agents: wizardAgents, mcp: wizardMcp ?? undefined, agentStore: wizardAgentStore ?? undefined, onSave: deps.saveConfig, onSaveMcp: async (scope, cfg) => {
|
|
841
|
+
await deps.writeMcpConfig(cfg, scope);
|
|
437
842
|
}, onClose: closeWizard }));
|
|
438
843
|
}
|
|
439
844
|
if (bootError) {
|
|
440
845
|
setCursorPosition(undefined);
|
|
441
|
-
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [
|
|
846
|
+
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(Text, { color: theme.error, wrap: "truncate", children: t("chat.bootFailed", { error: bootError }) }), _jsx(Text, { color: theme.dim, wrap: "truncate", children: t("chat.bootHint") })] }));
|
|
442
847
|
}
|
|
443
848
|
const session = sessionRef.current;
|
|
444
|
-
const
|
|
849
|
+
const lastTokens = session?.lastUsage?.total_tokens;
|
|
850
|
+
const totalTokens = session?.totalUsage.total_tokens;
|
|
445
851
|
const cols = columns ?? 80;
|
|
852
|
+
const messages = session?.messages ?? [];
|
|
853
|
+
const subAgents = session?.subAgents ?? [];
|
|
854
|
+
/** 当前查看的子 Agent;不在列表里(会话重建后 id 失效)时回落到主 Agent */
|
|
855
|
+
const activeSub = subAgents.find((item) => item.id === activeAgentId) ?? null;
|
|
856
|
+
const userCount = messages.filter((m) => m.role === AgentNS.Role.User).length;
|
|
857
|
+
// 乐观回显只属于主 Agent 的视图(子 Agent 不会收到用户直接发的消息)
|
|
858
|
+
const showPending = !activeSub && pendingUser != null && userCount <= pendingUser.base;
|
|
859
|
+
// 子 Agent 视图读它**自己**的 messages(从第 0 条起,不受主会话 /clear 的显示起点影响)
|
|
860
|
+
const transcript = transcriptLines(activeSub ? activeSub.agent.messages : messages.slice(transcriptFrom), cols);
|
|
861
|
+
if (showPending) {
|
|
862
|
+
// 带粘贴图片的消息:乐观回显要与实际发送的结构一致(文本段 + 图片占位块),
|
|
863
|
+
// 否则图片那几条会在真实消息落地前「消失」一下。
|
|
864
|
+
const images = pendingUser.images ?? [];
|
|
865
|
+
transcript.push(...messageToLines({
|
|
866
|
+
id: "__pending_user",
|
|
867
|
+
role: AgentNS.Role.User,
|
|
868
|
+
content: images.length > 0
|
|
869
|
+
? [
|
|
870
|
+
{ type: "text", text: pendingUser.text },
|
|
871
|
+
...images.map(() => ({ type: "image_url", image_url: { url: "" } })),
|
|
872
|
+
]
|
|
873
|
+
: pendingUser.text,
|
|
874
|
+
}, cols));
|
|
875
|
+
}
|
|
876
|
+
const noticeTextLines = noticeLines(notices, cols);
|
|
877
|
+
/** Agent 栏目条目:无子 Agent 时整条栏目不存在(不占高度、也不接管左右键) */
|
|
878
|
+
const agentTabs = subAgents.length > 0
|
|
879
|
+
? [
|
|
880
|
+
{ id: "main", label: t("agentTabs.main") },
|
|
881
|
+
...subAgents.map((item) => ({
|
|
882
|
+
id: item.id,
|
|
883
|
+
label: t("agentTabs.sub", { n: item.index }),
|
|
884
|
+
status: item.status,
|
|
885
|
+
})),
|
|
886
|
+
]
|
|
887
|
+
: [];
|
|
888
|
+
const activeTabId = activeSub ? activeSub.id : "main";
|
|
889
|
+
const agentTabLines = agentTabs.length > 0 ? AGENT_TABS_LINES : 0;
|
|
890
|
+
// 状态栏:先按当前状态算出「折行后实际几行」,交给布局预留高度(否则窄屏下一旦提示折行,
|
|
891
|
+
// 整帧就会高出一行,把输入框顶出屏幕)。这里用 `viewTop` 状态判断滚动,而不是
|
|
892
|
+
// `layout.scrolled` —— 后者要等布局算完才有,会与行数测量形成循环。
|
|
893
|
+
const statusProps = {
|
|
894
|
+
model: modelId,
|
|
895
|
+
agent: props.agentId ?? "default",
|
|
896
|
+
effort: effortView?.text ?? null,
|
|
897
|
+
effortDimmed: effortView?.dimmed ?? false,
|
|
898
|
+
lastTokens,
|
|
899
|
+
totalTokens,
|
|
900
|
+
busy,
|
|
901
|
+
hint: !ready
|
|
902
|
+
? t("input.hint.initializing")
|
|
903
|
+
: viewTop != null
|
|
904
|
+
? t("input.hint.scrolling")
|
|
905
|
+
: t("input.hint.ready"),
|
|
906
|
+
rate,
|
|
907
|
+
columns: cols,
|
|
908
|
+
};
|
|
909
|
+
const statusRows = statusBarRows(statusProps, cols);
|
|
910
|
+
/** 附件提示行:有粘贴图片时占 1 行(与 `computeChatLayout` 的 `attachmentLines` 同一个判据) */
|
|
911
|
+
const attachmentLines = pastedImages.length > 0 ? 1 : 0;
|
|
446
912
|
const layout = computeChatLayout({
|
|
447
913
|
columns: cols,
|
|
448
914
|
rows: usableFrameRows(rows ?? 24),
|
|
449
|
-
|
|
450
|
-
live: state.live,
|
|
915
|
+
transcript,
|
|
451
916
|
input,
|
|
452
917
|
busy,
|
|
453
|
-
hintCount:
|
|
918
|
+
hintCount: menuWant,
|
|
919
|
+
noticeCount: noticeTextLines.length,
|
|
920
|
+
attachmentLines,
|
|
921
|
+
agentTabLines,
|
|
454
922
|
overlay: confirmState ? "confirm" : pickerState ? "picker" : "none",
|
|
455
923
|
pickerItems: pickerState?.items.length ?? 0,
|
|
456
924
|
viewTop,
|
|
925
|
+
statusBarLines: statusRows.length,
|
|
457
926
|
});
|
|
458
927
|
layoutRef.current = layout;
|
|
459
928
|
const isPlaceholder = input.length === 0;
|
|
460
|
-
const inputLayout = layoutInput(isPlaceholder ? "
|
|
929
|
+
const inputLayout = layoutInput(isPlaceholder ? t("input.placeholder") : input, isPlaceholder ? 0 : cursor, Math.max(4, cols - 4));
|
|
461
930
|
const bodyLines = inputLayout.lines;
|
|
462
|
-
|
|
931
|
+
/**
|
|
932
|
+
* 输入区可见窗口:最多 `INPUT_MAX_ROWS` 行,且**跟随光标**(与 `computeChatLayout`
|
|
933
|
+
* 的 `inputHeight` 同源)。`cursorRowIndex` 是**窗口内**的行号 —— 渲染用哪一行、
|
|
934
|
+
* 硬件光标 y 都基于它;用全文本的行号会让 IME 候选框跑到屏外。
|
|
935
|
+
*/
|
|
936
|
+
const inputWin = inputWindow(bodyLines.length, inputLayout.cursorRow);
|
|
937
|
+
const shownLines = bodyLines.slice(inputWin.from, inputWin.to);
|
|
938
|
+
// 光标所在行的三段拆分(与 `CursorInputRow` 的渲染一一对应)
|
|
939
|
+
const cursorRowIndex = inputLayout.cursorRow - inputWin.from;
|
|
940
|
+
const cursorRowChars = graphemes(shownLines[cursorRowIndex] ?? "");
|
|
941
|
+
const cursorColInRow = Math.min(inputLayout.cursorCol, cursorRowChars.length);
|
|
942
|
+
const cursorBefore = cursorRowChars.slice(0, cursorColInRow).join("");
|
|
943
|
+
const cursorAt = cursorRowChars[cursorColInRow] ?? " ";
|
|
944
|
+
const cursorAfter = cursorRowChars.slice(cursorColInRow + 1).join("");
|
|
945
|
+
/** 实测结果的身份标记:输入段落一改,上一帧的实测值就作废 */
|
|
946
|
+
const cursorKey = `${cursorRowIndex}\u0000${cursorBefore}\u0000${cursorAt}\u0000${cursorAfter}`;
|
|
947
|
+
/** 渲染后实测(值不变则不会重渲染);闭包捕获当帧的 key,保证标的是「这一帧」的结果 */
|
|
948
|
+
const onCursorMeasure = (position) => {
|
|
949
|
+
setImePosition((prev) => prev && prev.position.x === position.x && prev.position.y === position.y
|
|
950
|
+
? prev
|
|
951
|
+
: { key: cursorKey, position });
|
|
952
|
+
};
|
|
953
|
+
// 把终端硬件光标移到编辑光标处,让 IME(输入法)候选框正确定位:
|
|
954
|
+
// 首选「渲染器实测」(宽字符 / emoji 的宽度由 Ink 自己算,不会偏);
|
|
955
|
+
// 首次渲染还没量到时,退回手算值顶一帧(下一帧即被实测值纠正)。
|
|
463
956
|
if (!busy && !confirmState && !pickerState) {
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
957
|
+
const measured = imePosition && imePosition.key === cursorKey ? imePosition.position : null;
|
|
958
|
+
setCursorPosition(measured ??
|
|
959
|
+
computeInputCursorPosition({
|
|
960
|
+
transcriptLines: layout.lines.length,
|
|
961
|
+
// Agent 栏目与通知栏都夹在对话区与输入行之间:漏算会让硬件光标整体偏上
|
|
962
|
+
agentTabCount: agentTabLines,
|
|
963
|
+
noticeCount: noticeTextLines.length,
|
|
964
|
+
attachmentCount: attachmentLines,
|
|
965
|
+
hintCount: layout.hintRows,
|
|
966
|
+
cursorRow: cursorRowIndex,
|
|
967
|
+
cursorCol: inputLayout.cursorCol,
|
|
968
|
+
lineText: bodyLines[inputLayout.cursorRow] ?? "",
|
|
969
|
+
placeholder: isPlaceholder,
|
|
970
|
+
}));
|
|
472
971
|
}
|
|
473
972
|
else {
|
|
474
973
|
setCursorPosition(undefined);
|
|
475
974
|
}
|
|
476
|
-
return (_jsxs(Box, { flexDirection: "column", width: cols, children: [layout.lines.map((line) => (_jsx(LineView, { line: line }, line.key))), confirmState ? (_jsx(Confirm, { question: confirmState.question, onAnswer: resolveConfirm })) : pickerState ? (_jsx(PickerOverlay, { title: pickerState.title, items: pickerState.items, columns: cols, onPick: resolvePicker })) : (_jsxs(Box, { flexDirection: "column", children: [
|
|
477
|
-
const prompt = i === 0 ? (_jsx(Text, { color: theme.brand[0], bold: true, children: "❯ " })) : (_jsx(Text, { children: " " }));
|
|
975
|
+
return (_jsxs(Box, { flexDirection: "column", width: cols, children: [layout.lines.map((line) => (_jsx(LineView, { line: line }, line.key))), confirmState ? (_jsx(Confirm, { question: confirmState.question, onAnswer: resolveConfirm })) : pickerState ? (_jsx(PickerOverlay, { title: pickerState.title, items: pickerState.items, columns: cols, onPick: resolvePicker })) : (_jsxs(Box, { flexDirection: "column", children: [agentTabs.length > 0 ? _jsx(AgentTabs, { tabs: agentTabs, activeId: activeTabId }) : null, _jsx(NoticeBar, { lines: noticeTextLines }), attachmentLines > 0 ? _jsx(PastedImagesBar, { count: pastedImages.length }) : null, menuWant > 0 ? (_jsx(BottomMenu, { items: menuItems, selected: menuIndex, maxRows: layout.hintRows, columns: cols, topLine: scanningFiles ? t("mention.scanning") : undefined })) : null, _jsx(Box, { paddingX: 1, children: _jsx(Text, { color: theme.border, wrap: "truncate", children: "─".repeat(Math.max(1, cols - 2)) }) }), _jsx(Box, { flexDirection: "column", paddingX: 1, children: busy ? (_jsx(Spinner, { label: busySpinnerLabel(sessionRef.current?.phase ?? null, busyCommand) })) : (shownLines.map((line, i) => {
|
|
976
|
+
const prompt = i === 0 && inputWin.from === 0 ? (_jsx(Text, { color: theme.brand[0], bold: true, children: "❯ " })) : (_jsx(Text, { children: " " }));
|
|
478
977
|
if (isPlaceholder) {
|
|
479
|
-
|
|
978
|
+
// 占位态不画「假光标」(那个行尾的 ▏):硬件光标这时就停在提示语开头
|
|
979
|
+
// (x = 前缀宽),终端自己会把它显示出来;再画一个方块等于屏幕上两个光标,
|
|
980
|
+
// 而且位置还在提示语末尾,反而误导。
|
|
981
|
+
return (_jsxs(Text, { wrap: "truncate", children: [prompt, _jsx(Text, { color: theme.faint, children: line })] }, i));
|
|
480
982
|
}
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
const at = rowChars[inputLayout.cursorCol];
|
|
485
|
-
const after = rowChars.slice(inputLayout.cursorCol + 1).join("");
|
|
486
|
-
return (_jsxs(Text, { wrap: "truncate", children: [prompt, _jsx(Text, { children: before }), _jsx(Text, { inverse: true, color: theme.brand[0], children: at ?? " " }), _jsx(Text, { children: after })] }, i));
|
|
983
|
+
// 光标行:把光标字符包成独立节点,渲染后由渲染器给出真实坐标(IME 定位)
|
|
984
|
+
if (i === cursorRowIndex) {
|
|
985
|
+
return (_jsx(CursorInputRow, { firstLine: i === 0, before: cursorBefore, at: cursorAt, after: cursorAfter, onMeasure: onCursorMeasure }, i));
|
|
487
986
|
}
|
|
488
987
|
return (_jsxs(Text, { wrap: "truncate", children: [prompt, _jsx(Text, { children: line })] }, i));
|
|
489
|
-
})) })] })), _jsx(StatusBar, {
|
|
490
|
-
? "初始化中…"
|
|
491
|
-
: layout.scrolled
|
|
492
|
-
? "↕ 滚动中 · PgUp/PgDn 翻页 · 发送回到底部"
|
|
493
|
-
: "Enter 发送 · Ctrl+N 换行 · / 命令 · PgUp 看历史" })] }));
|
|
988
|
+
})) })] })), _jsx(StatusBar, { ...statusProps })] }));
|
|
494
989
|
}
|
|
495
990
|
//# sourceMappingURL=chat.js.map
|