@ai-zen/cli 1.0.0-alpha.2 → 1.0.0-alpha.22
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 +514 -0
- package/README.md +136 -39
- package/README.zh.md +136 -39
- package/dist/agent-creator.d.ts +11 -0
- package/dist/agent-creator.d.ts.map +1 -1
- package/dist/agent-creator.js +52 -21
- 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/config-editor.d.ts +13 -5
- package/dist/config-editor.d.ts.map +1 -1
- package/dist/config-editor.js +36 -27
- package/dist/config-editor.js.map +1 -1
- package/dist/config.d.ts +24 -3
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +55 -15
- package/dist/config.js.map +1 -1
- package/dist/conversation-commands/registry.d.ts +123 -4
- package/dist/conversation-commands/registry.d.ts.map +1 -1
- package/dist/conversation-commands/registry.js +92 -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 +71 -13
- package/dist/conversation-repository.d.ts.map +1 -1
- package/dist/conversation-repository.js +81 -24
- package/dist/conversation-repository.js.map +1 -1
- package/dist/cwd-tracker-plugin.d.ts.map +1 -1
- package/dist/cwd-tracker-plugin.js +6 -2
- 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 +290 -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 +224 -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 +34 -0
- package/dist/i18n/preferences.d.ts.map +1 -0
- package/dist/i18n/preferences.js +69 -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 +293 -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 +228 -0
- package/dist/i18n/zh.js.map +1 -0
- package/dist/index.js +61 -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 +55 -0
- package/dist/sdk-log.d.ts.map +1 -0
- package/dist/sdk-log.js +91 -0
- package/dist/sdk-log.js.map +1 -0
- package/dist/session-pointer.d.ts +22 -9
- package/dist/session-pointer.d.ts.map +1 -1
- package/dist/session-pointer.js +55 -23
- 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 +5 -5
- package/dist/sub-agent-guard-plugin.d.ts.map +1 -1
- package/dist/sub-agent-guard-plugin.js +7 -7
- package/dist/sub-agent-guard-plugin.js.map +1 -1
- package/dist/tui/chat-session.d.ts +110 -59
- package/dist/tui/chat-session.d.ts.map +1 -1
- package/dist/tui/chat-session.js +268 -120
- package/dist/tui/chat-session.js.map +1 -1
- package/dist/tui/components.d.ts +48 -2
- package/dist/tui/components.d.ts.map +1 -1
- package/dist/tui/components.js +96 -3
- package/dist/tui/components.js.map +1 -1
- package/dist/tui/config-wizard/detail.d.ts +12 -6
- package/dist/tui/config-wizard/detail.d.ts.map +1 -1
- package/dist/tui/config-wizard/detail.js +33 -32
- package/dist/tui/config-wizard/detail.js.map +1 -1
- package/dist/tui/config-wizard/engine.d.ts.map +1 -1
- package/dist/tui/config-wizard/engine.js +27 -21
- 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 +216 -152
- package/dist/tui/config-wizard/fields.js.map +1 -1
- package/dist/tui/config-wizard/kv-editor.d.ts.map +1 -1
- package/dist/tui/config-wizard/kv-editor.js +9 -5
- package/dist/tui/config-wizard/kv-editor.js.map +1 -1
- package/dist/tui/config-wizard/list.d.ts.map +1 -1
- package/dist/tui/config-wizard/list.js +2 -1
- 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.map +1 -1
- package/dist/tui/config-wizard/prompt-editor.js +4 -3
- 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 +29 -20
- 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 +24 -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 +16 -6
- 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 +17 -19
- 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 +41 -26
- 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 +18 -3
- package/dist/tui/config-wizard/types.d.ts.map +1 -1
- package/dist/tui/config-wizard/wizard.d.ts.map +1 -1
- package/dist/tui/config-wizard/wizard.js +4 -3
- package/dist/tui/config-wizard/wizard.js.map +1 -1
- package/dist/tui/highlight.d.ts +3 -2
- package/dist/tui/highlight.d.ts.map +1 -1
- package/dist/tui/highlight.js +3 -2
- package/dist/tui/highlight.js.map +1 -1
- 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/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/chat-commands.d.ts +64 -3
- package/dist/tui/screens/chat-commands.d.ts.map +1 -1
- package/dist/tui/screens/chat-commands.js +483 -157
- package/dist/tui/screens/chat-commands.js.map +1 -1
- package/dist/tui/screens/chat.d.ts +10 -5
- package/dist/tui/screens/chat.d.ts.map +1 -1
- package/dist/tui/screens/chat.js +464 -301
- 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 +8 -8
- package/dist/tui/screens/index.d.ts.map +1 -1
- package/dist/tui/screens/index.js +7 -7
- 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 +46 -1
- package/dist/tui/screens/input.d.ts.map +1 -1
- package/dist/tui/screens/input.js +83 -8
- package/dist/tui/screens/input.js.map +1 -1
- package/dist/tui/screens/layout.d.ts +21 -3
- package/dist/tui/screens/layout.d.ts.map +1 -1
- package/dist/tui/screens/layout.js +20 -9
- package/dist/tui/screens/layout.js.map +1 -1
- package/dist/tui/screens/lines.d.ts +12 -41
- package/dist/tui/screens/lines.d.ts.map +1 -1
- package/dist/tui/screens/lines.js +90 -98
- package/dist/tui/screens/lines.js.map +1 -1
- package/dist/tui/screens/messages-view.d.ts +10 -0
- package/dist/tui/screens/messages-view.d.ts.map +1 -0
- package/dist/tui/screens/messages-view.js +116 -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 +15 -0
- package/dist/tui/screens/notice-bar.js.map +1 -0
- package/dist/tui/screens/notice.d.ts +64 -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 +3 -2
- 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 +7 -43
- 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 +44 -0
- package/dist/tui/screens/use-input-editing.d.ts.map +1 -0
- package/dist/tui/screens/use-input-editing.js +192 -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 +35 -0
- package/dist/tui/screens/use-wizard-host.d.ts.map +1 -0
- package/dist/tui/screens/use-wizard-host.js +103 -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 +20 -7
- package/dist/tui/text.d.ts.map +1 -1
- package/dist/tui/text.js +111 -37
- package/dist/tui/text.js.map +1 -1
- package/dist/tui/theme.d.ts +7 -1
- package/dist/tui/theme.d.ts.map +1 -1
- package/dist/tui/theme.js +15 -3
- 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/package.json +4 -3
- 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 -13
- package/dist/tui/screens/state.d.ts.map +0 -1
- package/dist/tui/screens/state.js +0 -75
- package/dist/tui/screens/state.js.map +0 -1
package/dist/tui/screens/chat.js
CHANGED
|
@@ -1,30 +1,42 @@
|
|
|
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 { keyDebug } from "../key-debug.js";
|
|
6
|
+
import { phaseLabel } from "../phase.js";
|
|
7
|
+
import { AgentNS } from "@ai-zen/agents-core";
|
|
5
8
|
import { theme } from "../theme.js";
|
|
6
|
-
import { Spinner, StatusBar, SlashMenu } from "../components.js";
|
|
7
|
-
import { layoutInput, usableFrameRows
|
|
9
|
+
import { Spinner, StatusBar, statusBarRows, SlashMenu } from "../components.js";
|
|
10
|
+
import { graphemes, layoutInput, usableFrameRows } from "../text.js";
|
|
8
11
|
import { ChatSession } from "../chat-session.js";
|
|
9
|
-
import { matchCommandHints } from "../../conversation-commands/registry.js";
|
|
10
|
-
import { readConfig,
|
|
12
|
+
import { commandBlocksInput, matchCommandHints, parseCommandInput, } from "../../conversation-commands/registry.js";
|
|
13
|
+
import { readConfig, readReasoningPresets, saveConfig, writeMcpConfig } from "../../config.js";
|
|
11
14
|
import { readAgentStore } from "../../agents-store.js";
|
|
12
|
-
import { resolveCredential } from "../../config-editor.js";
|
|
13
15
|
import { ConfigWizard } from "../config-wizard/index.js";
|
|
14
|
-
import { computeInputCursorPosition
|
|
16
|
+
import { computeInputCursorPosition } from "./input.js";
|
|
15
17
|
import { computeChatLayout } from "./layout.js";
|
|
16
|
-
import {
|
|
17
|
-
import { LineView
|
|
18
|
+
import { FrameBuffer } from "./frame-buffer.js";
|
|
19
|
+
import { LineView } from "./render.js";
|
|
20
|
+
import { messageToLines, transcriptLines } from "./messages-view.js";
|
|
18
21
|
import { PickerOverlay, Confirm } from "./overlays.js";
|
|
22
|
+
import { CursorInputRow } from "./input-row.js";
|
|
23
|
+
import { chooseHandoffMode } from "./handoff-choice.js";
|
|
19
24
|
import { runCommand } from "./chat-commands.js";
|
|
25
|
+
import { describeEffortView } from "../../reasoning-effort.js";
|
|
26
|
+
import { t } from "../../i18n/index.js";
|
|
27
|
+
import { NoticeChannel, noticeLines } from "./notice.js";
|
|
28
|
+
import { NoticeBar } from "./notice-bar.js";
|
|
29
|
+
import { useChatViewport } from "./use-chat-viewport.js";
|
|
30
|
+
import { useChatOverlays } from "./use-overlays.js";
|
|
31
|
+
import { useWizardHost } from "./use-wizard-host.js";
|
|
32
|
+
import { useInputEditing } from "./use-input-editing.js";
|
|
33
|
+
import { handleCtrlDExit, handleScrollKey } from "./common-keys.js";
|
|
20
34
|
/**
|
|
21
35
|
* 计算回车时应提交的文本。
|
|
22
36
|
*
|
|
23
37
|
* 斜杠候选菜单可见时(输入以 `/` 开头且尚未输入参数),回车应**直接执行高亮命令**,
|
|
24
38
|
* 而不是提交半截输入 —— 否则用户只输入 `/` 或未补全的命令名时,回车会落到
|
|
25
39
|
* 「未知命令」报错(体验很差)。非命令输入原样返回。
|
|
26
|
-
*
|
|
27
|
-
* `menuIndex` 越界时回落到最后一项,避免菜单看似有选中却回车落空。
|
|
28
40
|
*/
|
|
29
41
|
export function resolveSubmitText(input, hints, menuIndex) {
|
|
30
42
|
const slash = input.startsWith("/") && !input.includes("\n") && !input.includes(" ");
|
|
@@ -38,263 +50,273 @@ export function Chat(props) {
|
|
|
38
50
|
const { exit, suspendTerminal } = useApp();
|
|
39
51
|
const { setCursorPosition } = useCursor();
|
|
40
52
|
const { columns, rows } = useWindowSize();
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
live: null,
|
|
44
|
-
});
|
|
45
|
-
const [input, setInput] = useState(props.initialInput ?? "");
|
|
46
|
-
/** 光标位置:以 code point 计的字符索引(0..len) */
|
|
47
|
-
const [cursor, setCursor] = useState(Array.from(props.initialInput ?? "").length);
|
|
48
|
-
const [menuIndex, setMenuIndex] = useState(0);
|
|
49
|
-
// 输入变化后候选集随之改变:高亮重置回首项,避免索引越界(看似选中实为无高亮 / 回车落空)
|
|
50
|
-
useEffect(() => {
|
|
51
|
-
setMenuIndex(0);
|
|
52
|
-
}, [input]);
|
|
53
|
-
const [confirmState, setConfirmState] = useState(null);
|
|
54
|
-
const [pickerState, setPickerState] = useState(null);
|
|
55
|
-
/** 配置向导:非 null 时整屏接管(凭据设置 / 配置中心) */
|
|
56
|
-
const [wizardStep, setWizardStep] = useState(null);
|
|
57
|
-
const [wizardConfig, setWizardConfig] = useState(null);
|
|
58
|
-
/** 可用 Agent 列表(供配置中心「默认 Agent」选择) */
|
|
59
|
-
const [wizardAgents, setWizardAgents] = useState([]);
|
|
60
|
-
/** MCP 配置快照(全局 + 项目),打开向导时刷新 */
|
|
61
|
-
const [wizardMcp, setWizardMcp] = useState(null);
|
|
62
|
-
/** Agent / Sub-agent 定义仓储(快照 + 读写),打开向导时刷新 */
|
|
63
|
-
const [wizardAgentStore, setWizardAgentStore] = useState(null);
|
|
64
|
-
/** 当前会话所用端点 id(打开向导时刷新,用于判断改动是否影响本会话) */
|
|
65
|
-
const currentEndpointRef = useRef(null);
|
|
66
|
-
const [history, setHistory] = useState([]);
|
|
67
|
-
const historyIndex = useRef(-1);
|
|
53
|
+
/** 覆盖层(确认框 / 选择器):弹窗与 resolve 生命周期见 use-overlays.ts */
|
|
54
|
+
const { confirmState, pickerState, setConfirmState, setPickerState, resolveConfirm, resolvePicker } = useChatOverlays();
|
|
68
55
|
const sessionRef = useRef(null);
|
|
69
56
|
const [bootError, setBootError] = useState(null);
|
|
70
57
|
const [ready, setReady] = useState(false);
|
|
71
|
-
/**
|
|
72
|
-
const
|
|
73
|
-
/**
|
|
74
|
-
const
|
|
75
|
-
|
|
58
|
+
/** 视口:滚动状态与翻页 / 按行 / 滚轮换算(唯一入口见 use-chat-viewport.ts) */
|
|
59
|
+
const { viewTop, layoutRef, scrollByPage, scrollByLine, scrollByWheel, scrollToBottom } = useChatViewport();
|
|
60
|
+
/** 本轮是否在生成中(spinner / 输入禁用) */
|
|
61
|
+
const [busy, setBusy] = useState(false);
|
|
62
|
+
/** 生成速率快照(token/s):生成中为实时值,结束后保留本轮平均 */
|
|
63
|
+
const [rate, setRate] = useState(null);
|
|
64
|
+
/**
|
|
65
|
+
* 当前模型 id —— **从会话(即 Agent 定义)读**,不作为 prop 传入。
|
|
66
|
+
*
|
|
67
|
+
* 模型的唯一真相源是 `AgentDefinition.modelId`:所以状态栏、凭据检查、思考状态
|
|
68
|
+
* 与 SDK 实发请求用的永远是同一个值,不会出现「显示 A、实发 B」。
|
|
69
|
+
*/
|
|
70
|
+
const [modelId, setModelId] = useState("");
|
|
71
|
+
/**
|
|
72
|
+
* 思考强度展示文案(关 / 低 / 中 / 高 / 最高 / 开 / —)。
|
|
73
|
+
*
|
|
74
|
+
* 真相在配置里(模型的 `defaultParams`),这里只镜像一份供状态栏用:
|
|
75
|
+
* `/think` 写入后由命令层回调刷新,切模型(会话重建)时靠 `modelId` 变化重读。
|
|
76
|
+
*/
|
|
77
|
+
const [effortView, setEffortView] = useState(null);
|
|
78
|
+
/**
|
|
79
|
+
* 语言切换计数(`/lang`):`t()` 是模块级函数,语言变了已渲染的文案不会自更新,
|
|
80
|
+
* 递增这个计数只为逼 React 重跑一遍渲染函数。
|
|
81
|
+
*/
|
|
82
|
+
const [localeTick, bumpLocale] = useState(0);
|
|
83
|
+
/**
|
|
84
|
+
* IME 光标位置:由光标所在行**渲染后实测**而来(渲染器给的帧内坐标)。
|
|
85
|
+
* 带上 key —— 只有「产生它的那个输入状态」与当前一致时才能用,否则就是上一帧的陈旧值。
|
|
86
|
+
*/
|
|
87
|
+
const [imePosition, setImePosition] = useState(null);
|
|
88
|
+
/** 乐观回显:提交后、真实 user 消息落进 messages 之前,先显示用户提问 */
|
|
89
|
+
const [pendingUser, setPendingUser] = useState(null);
|
|
90
|
+
/** 转录显示起点(/clear 清屏用:隐藏此索引之前的消息,但不改上下文) */
|
|
91
|
+
const [transcriptFrom, setTranscriptFrom] = useState(0);
|
|
92
|
+
/** Ctrl+D 双击退出计时 */
|
|
93
|
+
const ctrlDAtRef = useRef(0);
|
|
94
|
+
/**
|
|
95
|
+
* 通知通道:notice / error 走独立栏位,绝不进对话转录(messages)。
|
|
96
|
+
*/
|
|
97
|
+
const noticeChannelRef = useRef(null);
|
|
98
|
+
if (noticeChannelRef.current == null)
|
|
99
|
+
noticeChannelRef.current = new NoticeChannel();
|
|
100
|
+
const noticeChannel = noticeChannelRef.current;
|
|
101
|
+
/**
|
|
102
|
+
* 通知队列(最近的在最后)。通道内部保留最近 `MAX_NOTICE_HISTORY` 条,
|
|
103
|
+
* 这里只做镜像:单槽 state 会让「同一批摘要 / 分阶段进度」只剩最后一条。
|
|
104
|
+
*/
|
|
105
|
+
const [notices, setNotices] = useState(noticeChannel.notices);
|
|
106
|
+
useEffect(() => noticeChannel.subscribe(setNotices), [noticeChannel]);
|
|
107
|
+
/**
|
|
108
|
+
* 配置向导宿主(凭据 / 配置中心):整屏接管与「关闭后让改动生效」见
|
|
109
|
+
* use-wizard-host.ts。这里只提供出口:通知走通知通道、重启 / 重载走会话。
|
|
110
|
+
*/
|
|
111
|
+
const { wizardStep, wizardConfig, wizardPresets, wizardAgents, wizardMcp, wizardAgentStore, openWizard, openKeyEditor, closeWizard, } = useWizardHost({
|
|
112
|
+
modelId,
|
|
113
|
+
notify: (text, tone) => noticeChannel.push(text, tone),
|
|
114
|
+
hasSession: () => sessionRef.current != null,
|
|
115
|
+
restart: () => exit({ restart: true, messages: props.messages }),
|
|
116
|
+
reloadSession: () => {
|
|
117
|
+
void sessionRef.current
|
|
118
|
+
?.reload()
|
|
119
|
+
.then(() => noticeChannel.push(t("notice.reloaded")))
|
|
120
|
+
.catch((error) => noticeChannel.push(t("chat.sessionReloadFailed", { error: error?.message ?? error }), "error"));
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
/**
|
|
124
|
+
* 重挂载附带的系统提示(如 `/model` 的切换结果)。
|
|
125
|
+
*
|
|
126
|
+
* 用 ref 而非 props 直读:本值只在首次挂载消费一次,若写进 create 的依赖数组,
|
|
127
|
+
* 会把「展示一条提示」误变成「重建会话」。
|
|
128
|
+
*/
|
|
129
|
+
const initialNoticeRef = useRef(props.initialNotice);
|
|
130
|
+
/** 钩子带来的首条消息:只在会话建好后发一次,重挂载不再重发 */
|
|
131
|
+
const initialPromptRef = useRef(props.initialPrompt);
|
|
132
|
+
/**
|
|
133
|
+
* 渲染信号 + 合帧:SDK 内容变更 → FrameBuffer 合帧 → setTick 触发重渲染。
|
|
134
|
+
* 转录内容始终从 `agent.messages` 现取,tick 只负责「告诉 React 该重画了」。
|
|
135
|
+
*/
|
|
136
|
+
const [, setTick] = useState(0);
|
|
137
|
+
const frameBufferRef = useRef(null);
|
|
138
|
+
if (frameBufferRef.current == null) {
|
|
139
|
+
frameBufferRef.current = new FrameBuffer(() => setTick((t) => t + 1));
|
|
140
|
+
}
|
|
141
|
+
// 思考强度展示:从当前模型的 defaultParams 派生;`/think` 写入后由命令层回调刷新,
|
|
142
|
+
// 切模型(会话重建)时靠 modelId 变化重读。
|
|
143
|
+
useEffect(() => {
|
|
144
|
+
if (!modelId)
|
|
145
|
+
return;
|
|
146
|
+
let alive = true;
|
|
147
|
+
// 档位存在 Agent 定义上(`modelParams`),可选档位在策略表里(`reasoningPreset` 指向),两者都要读
|
|
148
|
+
Promise.all([readConfig(), readAgentStore(), readReasoningPresets()])
|
|
149
|
+
.then(([config, store, presets]) => {
|
|
150
|
+
const model = config.models?.find((item) => item.id === modelId);
|
|
151
|
+
const agentId = props.agentId ?? "default";
|
|
152
|
+
const definition = store.agents.find((item) => item.id === agentId) ?? null;
|
|
153
|
+
if (alive)
|
|
154
|
+
setEffortView(describeEffortView(model, presets, definition));
|
|
155
|
+
})
|
|
156
|
+
.catch(() => {
|
|
157
|
+
if (alive)
|
|
158
|
+
setEffortView(null);
|
|
159
|
+
});
|
|
160
|
+
return () => {
|
|
161
|
+
alive = false;
|
|
162
|
+
};
|
|
163
|
+
// localeTick:切语言后档位文案要重算(它缓存的是已本地化的字符串,不是档位名)
|
|
164
|
+
}, [modelId, props.agentId, localeTick]);
|
|
76
165
|
// 创建会话
|
|
77
166
|
useEffect(() => {
|
|
78
167
|
let disposed = false;
|
|
168
|
+
const notifier = {
|
|
169
|
+
onChange: (immediate) => {
|
|
170
|
+
if (disposed)
|
|
171
|
+
return;
|
|
172
|
+
frameBufferRef.current?.push(null, immediate);
|
|
173
|
+
},
|
|
174
|
+
onNotice: (text, tone) => {
|
|
175
|
+
if (!disposed)
|
|
176
|
+
noticeChannel.push(text, tone);
|
|
177
|
+
},
|
|
178
|
+
};
|
|
79
179
|
ChatSession.create({
|
|
80
|
-
modelId: props.modelId,
|
|
81
180
|
agentId: props.agentId,
|
|
82
181
|
messages: props.messages,
|
|
83
182
|
conversationId: props.conversationId,
|
|
84
183
|
conversationName: props.conversationName,
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
dispatch(event);
|
|
89
|
-
})
|
|
184
|
+
// 自动迁移:与手动 /migrate、/handoff 共用同一套「交接方式三选一」选择器
|
|
185
|
+
requestMode: (info) => chooseHandoffMode(setPickerState, t("handoff.pickTitle", { used: info.promptTokens, limit: info.handoffTokens })),
|
|
186
|
+
}, notifier)
|
|
90
187
|
.then((session) => {
|
|
91
188
|
if (disposed) {
|
|
92
189
|
session.dispose();
|
|
93
190
|
return;
|
|
94
191
|
}
|
|
95
192
|
sessionRef.current = session;
|
|
193
|
+
setModelId(session.modelId);
|
|
96
194
|
setReady(true);
|
|
195
|
+
// 重挂载附带的提示只需展示一次(消费后置空,避免再次触发)
|
|
196
|
+
if (initialNoticeRef.current) {
|
|
197
|
+
noticeChannel.push(initialNoticeRef.current);
|
|
198
|
+
initialNoticeRef.current = undefined;
|
|
199
|
+
}
|
|
200
|
+
// shell 钩子:会话就绪后立即把原命令当**用户消息**发出去(不经命令解析)
|
|
201
|
+
if (initialPromptRef.current) {
|
|
202
|
+
const prompt = initialPromptRef.current;
|
|
203
|
+
initialPromptRef.current = undefined;
|
|
204
|
+
setTick((t) => t + 1);
|
|
205
|
+
void runSubmit(prompt, { asMessage: true });
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
setTick((t) => t + 1);
|
|
97
209
|
})
|
|
98
210
|
.catch((error) => setBootError(error?.message ?? String(error)));
|
|
99
211
|
return () => {
|
|
100
212
|
disposed = true;
|
|
213
|
+
frameBufferRef.current?.dispose();
|
|
101
214
|
sessionRef.current?.dispose();
|
|
102
215
|
};
|
|
103
216
|
}, [
|
|
104
|
-
props.modelId,
|
|
105
217
|
props.agentId,
|
|
106
218
|
props.messages,
|
|
107
219
|
props.conversationId,
|
|
108
220
|
props.conversationName,
|
|
109
221
|
]);
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const resolvePicker = (value) => {
|
|
117
|
-
setPickerState((ps) => {
|
|
118
|
-
ps?.resolve(value);
|
|
119
|
-
return null;
|
|
120
|
-
});
|
|
121
|
-
};
|
|
122
|
-
// ---- 配置向导(凭据 / 配置中心)----
|
|
123
|
-
/** 打开配置向导:先取配置 / MCP / Agent 快照,再切入指定步骤 */
|
|
124
|
-
const openWizard = async (step) => {
|
|
125
|
-
try {
|
|
126
|
-
const config = await readConfig();
|
|
127
|
-
currentEndpointRef.current = resolveCredential(config, props.modelId)?.endpointId ?? null;
|
|
128
|
-
const agentStore = await readAgentStore();
|
|
129
|
-
setWizardConfig(config);
|
|
130
|
-
setWizardAgents(agentStore.agents.map((agent) => ({ id: agent.id, name: agent.name || agent.id })));
|
|
131
|
-
setWizardAgentStore(agentStore);
|
|
132
|
-
setWizardMcp({ global: await readMcpConfig("global"), project: await readMcpConfig("project") });
|
|
133
|
-
setWizardStep(step);
|
|
134
|
-
}
|
|
135
|
-
catch (error) {
|
|
136
|
-
dispatch({ type: "error", text: `读取配置失败:${error?.message ?? error}` });
|
|
137
|
-
}
|
|
138
|
-
};
|
|
139
|
-
/** `/key`:直达「当前模型所用端点」的 API Key 编辑步骤 */
|
|
140
|
-
const openKeyEditor = async () => {
|
|
141
|
-
try {
|
|
142
|
-
const config = await readConfig();
|
|
143
|
-
const credential = resolveCredential(config, props.modelId);
|
|
144
|
-
if (!credential) {
|
|
145
|
-
dispatch({
|
|
146
|
-
type: "error",
|
|
147
|
-
text: `模型 ${props.modelId} 未绑定可用端点,请用 /config 检查配置`,
|
|
148
|
-
});
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
currentEndpointRef.current = credential.endpointId;
|
|
152
|
-
setWizardConfig(config);
|
|
153
|
-
setWizardStep({ kind: "edit", field: "apiKey", endpointId: credential.endpointId });
|
|
154
|
-
}
|
|
155
|
-
catch (error) {
|
|
156
|
-
dispatch({ type: "error", text: `读取配置失败:${error?.message ?? error}` });
|
|
157
|
-
}
|
|
158
|
-
};
|
|
159
|
-
/** 向导关闭:渲染变更摘要;若改到当前端点则重建会话(消息保留) */
|
|
160
|
-
const closeWizard = (result) => {
|
|
161
|
-
setWizardStep(null);
|
|
162
|
-
setWizardConfig(null);
|
|
163
|
-
setWizardMcp(null);
|
|
164
|
-
setWizardAgentStore(null);
|
|
165
|
-
for (const line of result.summary)
|
|
166
|
-
dispatch({ type: "notice", text: `配置已更新:${line}` });
|
|
167
|
-
if (!result.dirty)
|
|
168
|
-
return;
|
|
169
|
-
// 会话尚未建立(启动失败进入向导):重挂载对话屏重试
|
|
170
|
-
if (!sessionRef.current) {
|
|
171
|
-
exit({ restart: true, messages: props.messages });
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
const endpointChanged = !!currentEndpointRef.current && result.changedEndpointIds.includes(currentEndpointRef.current);
|
|
175
|
-
if (endpointChanged || result.mcpChanged || result.agentsChanged) {
|
|
176
|
-
sessionRef.current
|
|
177
|
-
.reload()
|
|
178
|
-
.then(() => dispatch({ type: "notice", text: "已按新配置重建会话(消息与上下文保留)" }))
|
|
179
|
-
.catch((error) => dispatch({ type: "error", text: `重建会话失败:${error?.message ?? error}` }));
|
|
180
|
-
}
|
|
181
|
-
};
|
|
182
|
-
// ---- 滚动视口控制(应用内翻页,回看历史)----
|
|
183
|
-
/** 跳到指定顶部行;越界钳制;一旦贴到底部则恢复「自动跟随」 */
|
|
184
|
-
const scrollTo = (nextTop) => {
|
|
185
|
-
const L = layoutRef.current;
|
|
186
|
-
if (!L)
|
|
187
|
-
return;
|
|
188
|
-
const maxTop = Math.max(0, L.totalLines - L.budget);
|
|
189
|
-
const clamped = Math.max(0, Math.min(maxTop, nextTop));
|
|
190
|
-
setViewTop(clamped >= maxTop ? null : clamped);
|
|
191
|
-
};
|
|
192
|
-
/** 翻页滚动(dir:-1 上翻 / +1 下翻) */
|
|
193
|
-
const scrollByPage = (dir) => {
|
|
194
|
-
const L = layoutRef.current;
|
|
195
|
-
if (!L)
|
|
196
|
-
return;
|
|
197
|
-
scrollTo(L.viewTop + dir * Math.max(1, L.budget - 1));
|
|
198
|
-
};
|
|
199
|
-
/** 单行滚动(dir:-1 上 / +1 下) */
|
|
200
|
-
const scrollByLine = (dir) => {
|
|
201
|
-
const L = layoutRef.current;
|
|
202
|
-
if (!L)
|
|
203
|
-
return;
|
|
204
|
-
scrollTo(L.viewTop + dir);
|
|
205
|
-
};
|
|
206
|
-
/** 回到最新(贴底,恢复自动跟随) */
|
|
207
|
-
const scrollToBottom = () => setViewTop(null);
|
|
208
|
-
// ---- 输入编辑(光标感知)----
|
|
209
|
-
/** 替换整个输入并把光标移到末尾 */
|
|
210
|
-
const setInputText = (text) => {
|
|
211
|
-
setInput(text);
|
|
212
|
-
setCursor(Array.from(text).length);
|
|
213
|
-
};
|
|
214
|
-
/** 在光标处插入文本 */
|
|
215
|
-
const insertAtCursor = (text) => {
|
|
216
|
-
const chars = Array.from(input);
|
|
217
|
-
const cp = Math.max(0, Math.min(cursor, chars.length));
|
|
218
|
-
setInput(chars.slice(0, cp).join("") + text + chars.slice(cp).join(""));
|
|
219
|
-
setCursor(cp + Array.from(text).length);
|
|
220
|
-
};
|
|
221
|
-
/** 删除光标前一个字符(Backspace) */
|
|
222
|
-
const deleteBeforeCursor = () => {
|
|
223
|
-
const chars = Array.from(input);
|
|
224
|
-
const cp = Math.max(0, Math.min(cursor, chars.length));
|
|
225
|
-
if (cp === 0)
|
|
226
|
-
return;
|
|
227
|
-
setInput(chars.slice(0, cp - 1).join("") + chars.slice(cp).join(""));
|
|
228
|
-
setCursor(cp - 1);
|
|
229
|
-
};
|
|
230
|
-
/** 删除光标后一个字符(Delete) */
|
|
231
|
-
const deleteAfterCursor = () => {
|
|
232
|
-
const chars = Array.from(input);
|
|
233
|
-
const cp = Math.max(0, Math.min(cursor, chars.length));
|
|
234
|
-
if (cp >= chars.length)
|
|
222
|
+
// 速率采样:Spinner 的重渲染只影响它自己,不会带着状态栏一起刷,所以生成中要自己定时读快照。
|
|
223
|
+
// 生成结束(busy → false)时再读一次,拿本轮平均速率并留在状态栏上。
|
|
224
|
+
useEffect(() => {
|
|
225
|
+
const read = () => setRate(sessionRef.current?.rateSample ?? null);
|
|
226
|
+
read();
|
|
227
|
+
if (!busy)
|
|
235
228
|
return;
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
/** 光标移到当前逻辑行行首(Home) */
|
|
244
|
-
const moveCursorToLineStart = () => {
|
|
245
|
-
const chars = Array.from(input);
|
|
246
|
-
const cp = Math.max(0, Math.min(cursor, chars.length));
|
|
247
|
-
let i = cp;
|
|
248
|
-
while (i > 0 && chars[i - 1] !== "\n")
|
|
249
|
-
i -= 1;
|
|
250
|
-
setCursor(i);
|
|
251
|
-
};
|
|
252
|
-
/** 光标移到当前逻辑行行尾(End) */
|
|
253
|
-
const moveCursorToLineEnd = () => {
|
|
254
|
-
const chars = Array.from(input);
|
|
255
|
-
const cp = Math.max(0, Math.min(cursor, chars.length));
|
|
256
|
-
let i = cp;
|
|
257
|
-
while (i < chars.length && chars[i] !== "\n")
|
|
258
|
-
i += 1;
|
|
259
|
-
setCursor(i);
|
|
229
|
+
const timer = setInterval(read, 250);
|
|
230
|
+
return () => clearInterval(timer);
|
|
231
|
+
}, [busy]);
|
|
232
|
+
/** 清屏但保留上下文:把显示起点推到当前消息末尾 */
|
|
233
|
+
const clearTranscript = () => {
|
|
234
|
+
setTranscriptFrom(sessionRef.current?.messages.length ?? 0);
|
|
235
|
+
noticeChannel.clear();
|
|
260
236
|
};
|
|
237
|
+
/**
|
|
238
|
+
* 输入编辑(文本 / 光标 / 历史 / kill ring / undo / stash):
|
|
239
|
+
* 全部语义在 use-input-editing.ts,这里只把通知出口接上。
|
|
240
|
+
*/
|
|
241
|
+
const { input, cursor, setInputText, insertAtCursor, deleteBeforeCursor, deleteAfterCursor, deleteWordBeforeCursor, deleteWordAfterCursor, deleteToStartOfLine, deleteToEndOfLine, deletePrevWhitespace, yank, undoEdit, toggleStash, moveCursor, moveCursorToLineStart, moveCursorToLineEnd, moveCursorByWord, historyPrev, historyNext, clearDraft, rememberSubmitted, } = useInputEditing({
|
|
242
|
+
initialText: props.initialInput,
|
|
243
|
+
notify: (text, tone) => noticeChannel.push(text, tone),
|
|
244
|
+
});
|
|
245
|
+
/** 斜杠候选菜单的高亮索引;输入一变就回到第一个候选 */
|
|
246
|
+
const [menuIndex, setMenuIndex] = useState(0);
|
|
247
|
+
useEffect(() => {
|
|
248
|
+
setMenuIndex(0);
|
|
249
|
+
}, [input]);
|
|
261
250
|
// 命令候选人
|
|
262
251
|
const slash = input.startsWith("/") && !input.includes("\n") && !input.includes(" ");
|
|
263
252
|
const hints = slash ? matchCommandHints(input.slice(1)) : [];
|
|
264
|
-
const runSubmit = async (text) => {
|
|
253
|
+
const runSubmit = async (text, options = {}) => {
|
|
265
254
|
const trimmed = text.trim();
|
|
266
255
|
if (!trimmed || !sessionRef.current || busy)
|
|
267
256
|
return;
|
|
268
|
-
|
|
269
|
-
historyIndex.current = -1;
|
|
257
|
+
clearDraft();
|
|
270
258
|
scrollToBottom();
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
259
|
+
noticeChannel.clear();
|
|
260
|
+
// 斜杠命令:命令名怎么解析、哪些命令会调用模型(要拦输入框),都来自命令元数据
|
|
261
|
+
const command = options.asMessage ? null : parseCommandInput(trimmed);
|
|
262
|
+
if (command) {
|
|
263
|
+
const name = command.name;
|
|
264
|
+
const blocksInput = commandBlocksInput(name);
|
|
265
|
+
if (blocksInput)
|
|
266
|
+
setBusy(true);
|
|
267
|
+
try {
|
|
268
|
+
await runCommand(name, trimmed, {
|
|
269
|
+
session: sessionRef.current,
|
|
270
|
+
notice: (text, tone) => noticeChannel.push(text, tone),
|
|
271
|
+
clearTranscript,
|
|
272
|
+
rows: rows ?? 24,
|
|
273
|
+
exit,
|
|
274
|
+
suspendTerminal,
|
|
275
|
+
openWizard,
|
|
276
|
+
openKeyEditor,
|
|
277
|
+
doExit,
|
|
278
|
+
setPickerState,
|
|
279
|
+
setConfirmState,
|
|
280
|
+
setInputText,
|
|
281
|
+
setEffortView,
|
|
282
|
+
refreshLocale: () => bumpLocale((n) => n + 1),
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
finally {
|
|
286
|
+
if (blocksInput)
|
|
287
|
+
setBusy(false);
|
|
288
|
+
}
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
rememberSubmitted(trimmed);
|
|
292
|
+
const base = sessionRef.current.messages.filter((m) => m.role === AgentNS.Role.User).length;
|
|
293
|
+
setPendingUser({ text: trimmed, base });
|
|
294
|
+
setBusy(true);
|
|
295
|
+
try {
|
|
296
|
+
await sessionRef.current.send(trimmed);
|
|
297
|
+
}
|
|
298
|
+
finally {
|
|
299
|
+
setBusy(false);
|
|
300
|
+
setPendingUser(null);
|
|
301
|
+
}
|
|
290
302
|
};
|
|
291
303
|
const doExit = () => {
|
|
292
|
-
// 会话已由 ConversationPersistPlugin 实时落盘,退出无需再询问是否保存
|
|
293
304
|
exit("done");
|
|
294
305
|
};
|
|
306
|
+
/**
|
|
307
|
+
* 两个 `useInput`(空闲 / 忙碌)共用的键位上下文 —— 判定逻辑在 common-keys.ts。
|
|
308
|
+
* 放这里而不是各自内联,是为了让「什么时候能滚、Ctrl+D 怎么退」只有一份定义。
|
|
309
|
+
*/
|
|
310
|
+
const scrollKeys = { scrollByWheel, scrollByPage, scrollByLine };
|
|
311
|
+
const ctrlDExit = {
|
|
312
|
+
lastAt: ctrlDAtRef,
|
|
313
|
+
notify: () => noticeChannel.push(t("notice.pressCtrlDAgain")),
|
|
314
|
+
exit: doExit,
|
|
315
|
+
};
|
|
295
316
|
// 输入处理
|
|
296
317
|
useInput((char, key) => {
|
|
297
|
-
//
|
|
318
|
+
// 按键诊断(AIZ_DEBUG_KEYS=1):必须在任何 return 之前记录,否则被吞掉的键不会留痕
|
|
319
|
+
keyDebug(char, key);
|
|
298
320
|
if (bootError) {
|
|
299
321
|
if (char === "k" || char === "K") {
|
|
300
322
|
void openWizard({ kind: "menu" });
|
|
@@ -304,55 +326,114 @@ export function Chat(props) {
|
|
|
304
326
|
exit("done");
|
|
305
327
|
return;
|
|
306
328
|
}
|
|
307
|
-
//
|
|
308
|
-
if (
|
|
309
|
-
return;
|
|
310
|
-
// 翻页滚动(回看历史)
|
|
311
|
-
if (key.pageUp) {
|
|
312
|
-
scrollByPage(-1);
|
|
313
|
-
return;
|
|
314
|
-
}
|
|
315
|
-
if (key.pageDown) {
|
|
316
|
-
scrollByPage(1);
|
|
329
|
+
// 回看历史类按键(滚轮 / PgUp/PgDn / ⌘[ ⌘] / ⌘; ⌘' / Alt+V)与忙碌态共用
|
|
330
|
+
if (handleScrollKey(char, key, scrollKeys))
|
|
317
331
|
return;
|
|
318
|
-
}
|
|
319
|
-
// 换行:Ctrl+Enter / Alt+Enter / Shift+Enter
|
|
320
|
-
// (仅当终端**原生**以 Kitty CSI-u 序列上报这些组合键时才生效 —— 本 CLI 不再
|
|
321
|
-
// 主动查询/开启该协议,见 tui/index.tsx 的 RENDER_OPTIONS;换行主推 Ctrl+N)
|
|
322
332
|
if (key.return && (key.ctrl || key.meta || key.shift)) {
|
|
323
333
|
insertAtCursor("\n");
|
|
324
334
|
return;
|
|
325
335
|
}
|
|
326
336
|
if (char === "c" && key.ctrl) {
|
|
337
|
+
// Claude Code:生成中中断;空闲时有草稿就先清空,已空则退出
|
|
327
338
|
if (busy)
|
|
328
339
|
sessionRef.current?.abort();
|
|
340
|
+
else if (input.length > 0) {
|
|
341
|
+
setInputText("");
|
|
342
|
+
noticeChannel.push(t("notice.inputCleared"));
|
|
343
|
+
}
|
|
329
344
|
else
|
|
330
345
|
void doExit();
|
|
331
346
|
return;
|
|
332
347
|
}
|
|
333
|
-
if (char === "d" && key.ctrl) {
|
|
334
|
-
|
|
348
|
+
if (char === "d" && key.ctrl && input.length > 0) {
|
|
349
|
+
// Claude Code:有草稿时删光标后一个字符(空闲态独有)
|
|
350
|
+
deleteAfterCursor();
|
|
335
351
|
return;
|
|
336
352
|
}
|
|
353
|
+
if (handleCtrlDExit(char, key, ctrlDExit))
|
|
354
|
+
return;
|
|
337
355
|
if (char === "l" && key.ctrl) {
|
|
338
|
-
|
|
356
|
+
clearTranscript();
|
|
339
357
|
return;
|
|
340
358
|
}
|
|
359
|
+
// ---- kill / yank / undo / stash(照抄 Claude Code / readline)----
|
|
341
360
|
if (char === "u" && key.ctrl) {
|
|
342
|
-
|
|
361
|
+
// 删到行首。macOS 上 ⌘⌫ 会被终端映射成 Ctrl+U,故不能做成「清空全部」
|
|
362
|
+
deleteToStartOfLine();
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
if (char === "k" && key.ctrl) {
|
|
366
|
+
deleteToEndOfLine();
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
if (char === "w" && key.ctrl) {
|
|
370
|
+
// 删到前一空白(标点不切分:`src/utils/foo.ts` 一次删完)
|
|
371
|
+
deletePrevWhitespace();
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
if (char === "y" && key.ctrl) {
|
|
375
|
+
yank(false);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
if (char === "y" && key.meta) {
|
|
379
|
+
yank(true);
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
// Ctrl+_ / Ctrl+Shift+-:撤销上一次输入编辑
|
|
383
|
+
if ((char === "_" && key.ctrl) || (char === "-" && key.ctrl && key.shift)) {
|
|
384
|
+
undoEdit();
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
if (char === "s" && key.ctrl) {
|
|
388
|
+
toggleStash();
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
if ((char === "v" && key.ctrl) || (key.super && (char === "v" || char === "V"))) {
|
|
392
|
+
// 图片粘贴暂未支持:需要先做「文件 → 内容块」的能力探测(见后续迭代)
|
|
393
|
+
noticeChannel.push(t("notice.pasteImageUnsupported"), "error");
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
// readline 的行首 / 行尾(macOS 上部分终端把 ⌘← / ⌘→ 发成 ^A / ^E)
|
|
397
|
+
if (char === "a" && key.ctrl) {
|
|
398
|
+
moveCursorToLineStart();
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
if (char === "e" && key.ctrl) {
|
|
402
|
+
moveCursorToLineEnd();
|
|
343
403
|
return;
|
|
344
404
|
}
|
|
345
405
|
if (char === "j" && key.ctrl) {
|
|
346
406
|
insertAtCursor("\n");
|
|
347
407
|
return;
|
|
348
408
|
}
|
|
349
|
-
//
|
|
350
|
-
if (char === "n" && key.ctrl) {
|
|
351
|
-
|
|
409
|
+
// Ctrl+P / Ctrl+N:历史上下(Claude Code 语义;换行改用 Ctrl+J / Alt+Enter)
|
|
410
|
+
if ((char === "p" && key.ctrl) || (char === "n" && key.ctrl)) {
|
|
411
|
+
const up = char === "p";
|
|
412
|
+
if (hints.length > 0) {
|
|
413
|
+
setMenuIndex((i) => (up ? (i - 1 + hints.length) % hints.length : (i + 1) % hints.length));
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
if (up)
|
|
417
|
+
historyPrev();
|
|
418
|
+
else
|
|
419
|
+
historyNext();
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
// 按词删除:Alt+Backspace / Ctrl+Backspace 删前一个词;Alt+D / Ctrl+Delete 删后一个词。
|
|
423
|
+
// 必须在下面「单字符 Backspace/Delete」之前判定,否则带修饰键的删除只掉一个字符。
|
|
424
|
+
if (key.backspace && (key.ctrl || key.meta || key.super)) {
|
|
425
|
+
deleteWordBeforeCursor();
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
if (key.delete && (key.ctrl || key.meta || key.super)) {
|
|
429
|
+
deleteWordAfterCursor();
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
if (char === "d" && key.meta) {
|
|
433
|
+
deleteWordAfterCursor();
|
|
352
434
|
return;
|
|
353
435
|
}
|
|
354
436
|
if (key.return) {
|
|
355
|
-
// 斜杠候选菜单可见时回车直接执行高亮命令,避免把半截输入(如仅 `/`)当命令提交
|
|
356
437
|
void runSubmit(resolveSubmitText(input, hints, menuIndex));
|
|
357
438
|
return;
|
|
358
439
|
}
|
|
@@ -361,7 +442,6 @@ export function Chat(props) {
|
|
|
361
442
|
return;
|
|
362
443
|
}
|
|
363
444
|
if (key.upArrow || key.downArrow) {
|
|
364
|
-
// Shift / Alt + ↑↓:单行滚动(优先于历史召回)
|
|
365
445
|
if (key.shift || key.meta) {
|
|
366
446
|
scrollByLine(key.upArrow ? -1 : 1);
|
|
367
447
|
return;
|
|
@@ -370,40 +450,37 @@ export function Chat(props) {
|
|
|
370
450
|
setMenuIndex((i) => key.upArrow ? (i - 1 + hints.length) % hints.length : (i + 1) % hints.length);
|
|
371
451
|
return;
|
|
372
452
|
}
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
setInputText(history[idx]);
|
|
378
|
-
}
|
|
379
|
-
else if (key.downArrow && historyIndex.current >= 0) {
|
|
380
|
-
const idx = historyIndex.current + 1;
|
|
381
|
-
if (idx >= history.length) {
|
|
382
|
-
historyIndex.current = -1;
|
|
383
|
-
setInputText("");
|
|
384
|
-
}
|
|
385
|
-
else {
|
|
386
|
-
historyIndex.current = idx;
|
|
387
|
-
setInputText(history[idx]);
|
|
388
|
-
}
|
|
389
|
-
}
|
|
453
|
+
if (key.upArrow)
|
|
454
|
+
historyPrev();
|
|
455
|
+
else
|
|
456
|
+
historyNext();
|
|
390
457
|
return;
|
|
391
458
|
}
|
|
392
|
-
// 光标移动:← → 单字符,Ctrl/Alt+← → 跨词,Home / End 到行首 / 行尾
|
|
393
459
|
if (key.leftArrow) {
|
|
394
|
-
if (key.ctrl || key.meta)
|
|
395
|
-
|
|
460
|
+
if (key.ctrl || key.meta || key.super)
|
|
461
|
+
moveCursorByWord(-1);
|
|
396
462
|
else
|
|
397
463
|
moveCursor(-1);
|
|
398
464
|
return;
|
|
399
465
|
}
|
|
400
466
|
if (key.rightArrow) {
|
|
401
|
-
if (key.ctrl || key.meta)
|
|
402
|
-
|
|
467
|
+
if (key.ctrl || key.meta || key.super)
|
|
468
|
+
moveCursorByWord(1);
|
|
403
469
|
else
|
|
404
470
|
moveCursor(1);
|
|
405
471
|
return;
|
|
406
472
|
}
|
|
473
|
+
// 跨词移动的「兜底键」:macOS 上 Option+←/→ 往往发的是 ESC b / ESC f
|
|
474
|
+
// (readline 的 backward-word / forward-word),并不带 leftArrow 标记;
|
|
475
|
+
// 而 Ctrl+←/→ 常被系统「Mission Control → 左右切换空间」截走,应用根本收不到。
|
|
476
|
+
if (char === "b" && key.meta) {
|
|
477
|
+
moveCursorByWord(-1);
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
if (char === "f" && key.meta) {
|
|
481
|
+
moveCursorByWord(1);
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
407
484
|
if (key.home) {
|
|
408
485
|
moveCursorToLineStart();
|
|
409
486
|
return;
|
|
@@ -413,8 +490,13 @@ export function Chat(props) {
|
|
|
413
490
|
return;
|
|
414
491
|
}
|
|
415
492
|
if (key.escape) {
|
|
416
|
-
|
|
417
|
-
|
|
493
|
+
// Claude Code:生成中 Esc = 中断本轮;否则清空草稿并回到底部
|
|
494
|
+
if (busy)
|
|
495
|
+
sessionRef.current?.abort();
|
|
496
|
+
else {
|
|
497
|
+
setInputText("");
|
|
498
|
+
scrollToBottom();
|
|
499
|
+
}
|
|
418
500
|
return;
|
|
419
501
|
}
|
|
420
502
|
if (key.backspace) {
|
|
@@ -425,70 +507,151 @@ export function Chat(props) {
|
|
|
425
507
|
deleteAfterCursor();
|
|
426
508
|
return;
|
|
427
509
|
}
|
|
428
|
-
if (char && !key.ctrl && !key.meta) {
|
|
510
|
+
if (char && !key.ctrl && !key.meta && !key.super) {
|
|
429
511
|
insertAtCursor(char);
|
|
430
512
|
}
|
|
431
|
-
},
|
|
513
|
+
},
|
|
514
|
+
// 生成 / 自动迁移期间主输入框整体失活:此时输入框已被 Spinner 顶替,若仍激活,
|
|
515
|
+
// 按键会被静默吞进隐藏的输入缓冲、待本轮结束后才冒出来(即「迁移过程中输入框
|
|
516
|
+
// 没被拦住」)。失活后改由下方「忙碌期」处理器只放行中止 / 退出 / 回看滚动。
|
|
517
|
+
{ isActive: !busy && !confirmState && !pickerState && !wizardStep });
|
|
518
|
+
// 忙碌期(生成 / 迁移中)专用:输入框不可编辑,仅保留「中止(Ctrl+C) / 退出(Ctrl+D) / 回看滚动」。
|
|
519
|
+
useInput((char, key) => {
|
|
520
|
+
keyDebug(char, key);
|
|
521
|
+
// 忙碌期只剩「中止 / 退出 / 回看」,滚轮同样生效(生成时正是最想往回看的时机)
|
|
522
|
+
if (handleScrollKey(char, key, scrollKeys))
|
|
523
|
+
return;
|
|
524
|
+
if (char === "c" && key.ctrl) {
|
|
525
|
+
sessionRef.current?.abort();
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
if (key.escape) {
|
|
529
|
+
// 忙碌期 Esc 也充当「中断」(与 Claude Code 一致)
|
|
530
|
+
sessionRef.current?.abort();
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
if (handleCtrlDExit(char, key, ctrlDExit))
|
|
534
|
+
return;
|
|
535
|
+
if ((key.upArrow || key.downArrow) && (key.shift || key.meta || key.super)) {
|
|
536
|
+
scrollByLine(key.upArrow ? -1 : 1);
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
if (char === "v" && key.super) {
|
|
540
|
+
scrollByPage(-1);
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
}, { isActive: busy && !confirmState && !pickerState && !wizardStep });
|
|
432
544
|
// 配置向导:整屏接管(凭据设置 / 配置中心),关闭后回到对话屏
|
|
433
545
|
if (wizardStep && wizardConfig) {
|
|
434
|
-
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) => {
|
|
546
|
+
return (_jsx(ConfigWizard, { config: wizardConfig, reasoningPresets: wizardPresets, initialStep: wizardStep, closeOnSave: wizardStep.kind !== "menu", agents: wizardAgents, mcp: wizardMcp ?? undefined, agentStore: wizardAgentStore ?? undefined, onSave: saveConfig, onSaveMcp: async (scope, cfg) => {
|
|
435
547
|
await writeMcpConfig(cfg, scope);
|
|
436
548
|
}, onClose: closeWizard }));
|
|
437
549
|
}
|
|
438
550
|
if (bootError) {
|
|
439
551
|
setCursorPosition(undefined);
|
|
440
|
-
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [
|
|
552
|
+
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") })] }));
|
|
441
553
|
}
|
|
442
554
|
const session = sessionRef.current;
|
|
443
|
-
const
|
|
555
|
+
const lastTokens = session?.lastUsage?.total_tokens;
|
|
556
|
+
const totalTokens = session?.totalUsage.total_tokens;
|
|
444
557
|
const cols = columns ?? 80;
|
|
558
|
+
const messages = session?.messages ?? [];
|
|
559
|
+
const userCount = messages.filter((m) => m.role === AgentNS.Role.User).length;
|
|
560
|
+
const showPending = pendingUser != null && userCount <= pendingUser.base;
|
|
561
|
+
const transcript = transcriptLines(messages.slice(transcriptFrom), cols);
|
|
562
|
+
if (showPending) {
|
|
563
|
+
transcript.push(...messageToLines({ id: "__pending_user", role: AgentNS.Role.User, content: pendingUser.text }, cols));
|
|
564
|
+
}
|
|
565
|
+
const noticeTextLines = noticeLines(notices, cols);
|
|
566
|
+
// 状态栏:先按当前状态算出「折行后实际几行」,交给布局预留高度(否则窄屏下一旦提示折行,
|
|
567
|
+
// 整帧就会高出一行,把输入框顶出屏幕)。这里用 `viewTop` 状态判断滚动,而不是
|
|
568
|
+
// `layout.scrolled` —— 后者要等布局算完才有,会与行数测量形成循环。
|
|
569
|
+
const statusProps = {
|
|
570
|
+
model: modelId,
|
|
571
|
+
agent: props.agentId ?? "default",
|
|
572
|
+
effort: effortView?.text ?? null,
|
|
573
|
+
effortDimmed: effortView?.dimmed ?? false,
|
|
574
|
+
lastTokens,
|
|
575
|
+
totalTokens,
|
|
576
|
+
busy,
|
|
577
|
+
hint: !ready
|
|
578
|
+
? t("input.hint.initializing")
|
|
579
|
+
: viewTop != null
|
|
580
|
+
? t("input.hint.scrolling")
|
|
581
|
+
: t("input.hint.ready"),
|
|
582
|
+
rate,
|
|
583
|
+
columns: cols,
|
|
584
|
+
};
|
|
585
|
+
const statusRows = statusBarRows(statusProps, cols);
|
|
445
586
|
const layout = computeChatLayout({
|
|
446
587
|
columns: cols,
|
|
447
588
|
rows: usableFrameRows(rows ?? 24),
|
|
448
|
-
|
|
449
|
-
live: state.live,
|
|
589
|
+
transcript,
|
|
450
590
|
input,
|
|
451
591
|
busy,
|
|
452
592
|
hintCount: hints.length,
|
|
593
|
+
noticeCount: noticeTextLines.length,
|
|
453
594
|
overlay: confirmState ? "confirm" : pickerState ? "picker" : "none",
|
|
454
595
|
pickerItems: pickerState?.items.length ?? 0,
|
|
455
596
|
viewTop,
|
|
597
|
+
statusBarLines: statusRows.length,
|
|
456
598
|
});
|
|
457
599
|
layoutRef.current = layout;
|
|
458
600
|
const isPlaceholder = input.length === 0;
|
|
459
|
-
const inputLayout = layoutInput(isPlaceholder ? "
|
|
601
|
+
const inputLayout = layoutInput(isPlaceholder ? t("input.placeholder") : input, isPlaceholder ? 0 : cursor, Math.max(4, cols - 4));
|
|
460
602
|
const bodyLines = inputLayout.lines;
|
|
461
|
-
//
|
|
603
|
+
// 光标所在行的三段拆分(与 `CursorInputRow` 的渲染一一对应)
|
|
604
|
+
const cursorRowIndex = inputLayout.cursorRow;
|
|
605
|
+
const cursorRowChars = graphemes(bodyLines[cursorRowIndex] ?? "");
|
|
606
|
+
const cursorColInRow = Math.min(inputLayout.cursorCol, cursorRowChars.length);
|
|
607
|
+
const cursorBefore = cursorRowChars.slice(0, cursorColInRow).join("");
|
|
608
|
+
const cursorAt = cursorRowChars[cursorColInRow] ?? " ";
|
|
609
|
+
const cursorAfter = cursorRowChars.slice(cursorColInRow + 1).join("");
|
|
610
|
+
/** 实测结果的身份标记:输入段落一改,上一帧的实测值就作废 */
|
|
611
|
+
const cursorKey = `${cursorRowIndex}\u0000${cursorBefore}\u0000${cursorAt}\u0000${cursorAfter}`;
|
|
612
|
+
/** 渲染后实测(值不变则不会重渲染);闭包捕获当帧的 key,保证标的是「这一帧」的结果 */
|
|
613
|
+
const onCursorMeasure = (position) => {
|
|
614
|
+
setImePosition((prev) => prev && prev.position.x === position.x && prev.position.y === position.y
|
|
615
|
+
? prev
|
|
616
|
+
: { key: cursorKey, position });
|
|
617
|
+
};
|
|
618
|
+
// 把终端硬件光标移到编辑光标处,让 IME(输入法)候选框正确定位:
|
|
619
|
+
// 首选「渲染器实测」(宽字符 / emoji 的宽度由 Ink 自己算,不会偏);
|
|
620
|
+
// 首次渲染还没量到时,退回手算值顶一帧(下一帧即被实测值纠正)。
|
|
462
621
|
if (!busy && !confirmState && !pickerState) {
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
622
|
+
const measured = imePosition && imePosition.key === cursorKey ? imePosition.position : null;
|
|
623
|
+
setCursorPosition(measured ??
|
|
624
|
+
computeInputCursorPosition({
|
|
625
|
+
transcriptLines: layout.lines.length,
|
|
626
|
+
// 通知栏夹在对话区与斜杠菜单之间:漏算会让硬件光标整体偏上(多行通知如 /help 最明显)
|
|
627
|
+
noticeCount: noticeTextLines.length,
|
|
628
|
+
hintCount: hints.length,
|
|
629
|
+
cursorRow: inputLayout.cursorRow,
|
|
630
|
+
cursorCol: inputLayout.cursorCol,
|
|
631
|
+
lineText: bodyLines[inputLayout.cursorRow] ?? "",
|
|
632
|
+
placeholder: isPlaceholder,
|
|
633
|
+
}));
|
|
471
634
|
}
|
|
472
635
|
else {
|
|
473
636
|
setCursorPosition(undefined);
|
|
474
637
|
}
|
|
475
|
-
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: [hints.length > 0 ? _jsx(SlashMenu, { hints: hints, selected: menuIndex }) : 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 ? (
|
|
638
|
+
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: [_jsx(NoticeBar, { lines: noticeTextLines }), hints.length > 0 ? _jsx(SlashMenu, { hints: hints, selected: menuIndex }) : 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 ? (
|
|
639
|
+
// 阶段随 SDK 事件推进:连接中 → 排队中 → 思考 / 输出 / 调用工具 → 等工具返回。
|
|
640
|
+
// 渲染期现取(不另存 state):chunk 与轮次边界都会触发重渲染,
|
|
641
|
+
// 而 `busy` 置 true 本身也会重渲一次,捕得到首轮的「连接中」。
|
|
642
|
+
_jsx(Spinner, { label: phaseLabel(sessionRef.current?.phase ?? null) })) : (bodyLines.map((line, i) => {
|
|
476
643
|
const prompt = i === 0 ? (_jsx(Text, { color: theme.brand[0], bold: true, children: "❯ " })) : (_jsx(Text, { children: " " }));
|
|
477
644
|
if (isPlaceholder) {
|
|
478
|
-
|
|
645
|
+
// 占位态不画「假光标」(那个行尾的 ▏):硬件光标这时就停在提示语开头
|
|
646
|
+
// (x = 前缀宽),终端自己会把它显示出来;再画一个方块等于屏幕上两个光标,
|
|
647
|
+
// 而且位置还在提示语末尾,反而误导。
|
|
648
|
+
return (_jsxs(Text, { wrap: "truncate", children: [prompt, _jsx(Text, { color: theme.faint, children: line })] }, i));
|
|
479
649
|
}
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
const at = rowChars[inputLayout.cursorCol];
|
|
484
|
-
const after = rowChars.slice(inputLayout.cursorCol + 1).join("");
|
|
485
|
-
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));
|
|
650
|
+
// 光标行:把光标字符包成独立节点,渲染后由渲染器给出真实坐标(IME 定位)
|
|
651
|
+
if (i === cursorRowIndex) {
|
|
652
|
+
return (_jsx(CursorInputRow, { firstLine: i === 0, before: cursorBefore, at: cursorAt, after: cursorAfter, onMeasure: onCursorMeasure }, i));
|
|
486
653
|
}
|
|
487
654
|
return (_jsxs(Text, { wrap: "truncate", children: [prompt, _jsx(Text, { children: line })] }, i));
|
|
488
|
-
})) })] })), _jsx(StatusBar, {
|
|
489
|
-
? "初始化中…"
|
|
490
|
-
: layout.scrolled
|
|
491
|
-
? "↕ 滚动中 · PgUp/PgDn 翻页 · 发送回到底部"
|
|
492
|
-
: "Enter 发送 · Ctrl+N 换行 · / 命令 · PgUp 看历史" })] }));
|
|
655
|
+
})) })] })), _jsx(StatusBar, { ...statusProps })] }));
|
|
493
656
|
}
|
|
494
657
|
//# sourceMappingURL=chat.js.map
|