@ai-zen/cli 1.0.0-alpha.2 → 1.0.0-alpha.20
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 +356 -0
- package/README.md +99 -28
- package/README.zh.md +99 -28
- package/dist/agent-creator.d.ts +11 -0
- package/dist/agent-creator.d.ts.map +1 -1
- package/dist/agent-creator.js +20 -11
- package/dist/agent-creator.js.map +1 -1
- package/dist/agents-store.d.ts.map +1 -1
- package/dist/agents-store.js +40 -3
- 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 +46 -0
- package/dist/auto-migrate-confirm.js.map +1 -0
- package/dist/config-editor.d.ts +4 -4
- package/dist/config-editor.d.ts.map +1 -1
- package/dist/config-editor.js +6 -8
- package/dist/config-editor.js.map +1 -1
- package/dist/config.d.ts +23 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +26 -2
- package/dist/config.js.map +1 -1
- package/dist/conversation-commands/registry.d.ts.map +1 -1
- package/dist/conversation-commands/registry.js +4 -1
- 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 +2 -3
- package/dist/conversation-persist-plugin.js.map +1 -1
- package/dist/conversation-repository.d.ts +5 -1
- package/dist/conversation-repository.d.ts.map +1 -1
- package/dist/conversation-repository.js +1 -2
- package/dist/conversation-repository.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 +86 -0
- package/dist/handoff-service.js.map +1 -0
- package/dist/index.js +21 -3
- package/dist/index.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 +61 -0
- package/dist/model-switch.js.map +1 -0
- package/dist/reasoning-effort.d.ts +77 -0
- package/dist/reasoning-effort.d.ts.map +1 -0
- package/dist/reasoning-effort.js +137 -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 +48 -0
- package/dist/sdk-log.d.ts.map +1 -0
- package/dist/sdk-log.js +67 -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 +15 -13
- 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 +83 -59
- package/dist/tui/chat-session.d.ts.map +1 -1
- package/dist/tui/chat-session.js +161 -105
- package/dist/tui/chat-session.js.map +1 -1
- package/dist/tui/components.d.ts +7 -2
- package/dist/tui/components.d.ts.map +1 -1
- package/dist/tui/components.js +3 -3
- package/dist/tui/components.js.map +1 -1
- package/dist/tui/config-wizard/engine.js +1 -1
- 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 +58 -6
- package/dist/tui/config-wizard/fields.js.map +1 -1
- package/dist/tui/config-wizard/menu.d.ts.map +1 -1
- package/dist/tui/config-wizard/menu.js +2 -3
- package/dist/tui/config-wizard/menu.js.map +1 -1
- package/dist/tui/config-wizard/step-defaults.d.ts.map +1 -1
- package/dist/tui/config-wizard/step-defaults.js +4 -11
- package/dist/tui/config-wizard/step-defaults.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 +1 -0
- package/dist/tui/index.d.ts.map +1 -1
- package/dist/tui/index.js +87 -40
- package/dist/tui/index.js.map +1 -1
- package/dist/tui/input-editing.d.ts +49 -0
- package/dist/tui/input-editing.d.ts.map +1 -0
- package/dist/tui/input-editing.js +87 -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/screens/chat-commands.d.ts +12 -2
- package/dist/tui/screens/chat-commands.d.ts.map +1 -1
- package/dist/tui/screens/chat-commands.js +204 -17
- package/dist/tui/screens/chat-commands.js.map +1 -1
- package/dist/tui/screens/chat.d.ts +2 -5
- package/dist/tui/screens/chat.d.ts.map +1 -1
- package/dist/tui/screens/chat.js +460 -98
- package/dist/tui/screens/chat.js.map +1 -1
- 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 +41 -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.d.ts +35 -0
- package/dist/tui/screens/input.d.ts.map +1 -1
- package/dist/tui/screens/input.js +66 -0
- package/dist/tui/screens/input.js.map +1 -1
- package/dist/tui/screens/layout.d.ts +13 -3
- package/dist/tui/screens/layout.d.ts.map +1 -1
- package/dist/tui/screens/layout.js +15 -8
- 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 +87 -95
- 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 +115 -0
- package/dist/tui/screens/messages-view.js.map +1 -0
- package/dist/tui/screens/notice.d.ts +32 -0
- package/dist/tui/screens/notice.d.ts.map +1 -0
- package/dist/tui/screens/notice.js +46 -0
- package/dist/tui/screens/notice.js.map +1 -0
- 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.map +1 -1
- package/dist/tui/screens/splash.js +7 -1
- package/dist/tui/screens/splash.js.map +1 -1
- package/dist/tui/text.d.ts +12 -2
- package/dist/tui/text.d.ts.map +1 -1
- package/dist/tui/text.js +17 -3
- 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/types.d.ts +7 -5
- package/dist/types.d.ts.map +1 -1
- package/package.json +3 -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,36 @@
|
|
|
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 { parseWheel, WHEEL_STEP } from "../mouse.js";
|
|
7
|
+
import { AgentNS } from "@ai-zen/agents-core";
|
|
5
8
|
import { theme } from "../theme.js";
|
|
6
9
|
import { Spinner, StatusBar, SlashMenu } from "../components.js";
|
|
7
10
|
import { layoutInput, usableFrameRows, isTerminalReply } from "../text.js";
|
|
8
11
|
import { ChatSession } from "../chat-session.js";
|
|
9
12
|
import { matchCommandHints } from "../../conversation-commands/registry.js";
|
|
10
|
-
import { readConfig, readMcpConfig, saveConfig, writeMcpConfig } from "../../config.js";
|
|
13
|
+
import { readConfig, readMcpConfig, readReasoningPresets, saveConfig, writeMcpConfig } from "../../config.js";
|
|
11
14
|
import { readAgentStore } from "../../agents-store.js";
|
|
12
15
|
import { resolveCredential } from "../../config-editor.js";
|
|
13
16
|
import { ConfigWizard } from "../config-wizard/index.js";
|
|
14
|
-
import { computeInputCursorPosition, wordLeft, wordRight } from "./input.js";
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
17
|
+
import { computeInputCursorPosition, deleteToLineEnd, deleteToLineStart, deleteWordLeft, deleteWordRight, wordLeft, wordRight } from "./input.js";
|
|
18
|
+
import { deleteToPrevWhitespace, killedText, killAt, popUndo, pushKill, pushUndo, } from "../input-editing.js";
|
|
19
|
+
import { clampViewTop, computeChatLayout } from "./layout.js";
|
|
20
|
+
import { FrameBuffer } from "./frame-buffer.js";
|
|
21
|
+
import { LineView } from "./render.js";
|
|
22
|
+
import { messageToLines, transcriptLines } from "./messages-view.js";
|
|
18
23
|
import { PickerOverlay, Confirm } from "./overlays.js";
|
|
24
|
+
import { chooseHandoffMode } from "./handoff-choice.js";
|
|
19
25
|
import { runCommand } from "./chat-commands.js";
|
|
26
|
+
import { describeEffort } from "../../reasoning-effort.js";
|
|
27
|
+
import { NoticeChannel, noticeLines } from "./notice.js";
|
|
20
28
|
/**
|
|
21
29
|
* 计算回车时应提交的文本。
|
|
22
30
|
*
|
|
23
31
|
* 斜杠候选菜单可见时(输入以 `/` 开头且尚未输入参数),回车应**直接执行高亮命令**,
|
|
24
32
|
* 而不是提交半截输入 —— 否则用户只输入 `/` 或未补全的命令名时,回车会落到
|
|
25
33
|
* 「未知命令」报错(体验很差)。非命令输入原样返回。
|
|
26
|
-
*
|
|
27
|
-
* `menuIndex` 越界时回落到最后一项,避免菜单看似有选中却回车落空。
|
|
28
34
|
*/
|
|
29
35
|
export function resolveSubmitText(input, hints, menuIndex) {
|
|
30
36
|
const slash = input.startsWith("/") && !input.includes("\n") && !input.includes(" ");
|
|
@@ -38,15 +44,10 @@ export function Chat(props) {
|
|
|
38
44
|
const { exit, suspendTerminal } = useApp();
|
|
39
45
|
const { setCursorPosition } = useCursor();
|
|
40
46
|
const { columns, rows } = useWindowSize();
|
|
41
|
-
const [state, dispatch] = useReducer(chatReducer, {
|
|
42
|
-
blocks: props.clearTranscript ? [] : messagesToBlocks(props.messages ?? []),
|
|
43
|
-
live: null,
|
|
44
|
-
});
|
|
45
47
|
const [input, setInput] = useState(props.initialInput ?? "");
|
|
46
48
|
/** 光标位置:以 code point 计的字符索引(0..len) */
|
|
47
49
|
const [cursor, setCursor] = useState(Array.from(props.initialInput ?? "").length);
|
|
48
50
|
const [menuIndex, setMenuIndex] = useState(0);
|
|
49
|
-
// 输入变化后候选集随之改变:高亮重置回首项,避免索引越界(看似选中实为无高亮 / 回车落空)
|
|
50
51
|
useEffect(() => {
|
|
51
52
|
setMenuIndex(0);
|
|
52
53
|
}, [input]);
|
|
@@ -55,13 +56,10 @@ export function Chat(props) {
|
|
|
55
56
|
/** 配置向导:非 null 时整屏接管(凭据设置 / 配置中心) */
|
|
56
57
|
const [wizardStep, setWizardStep] = useState(null);
|
|
57
58
|
const [wizardConfig, setWizardConfig] = useState(null);
|
|
58
|
-
|
|
59
|
+
const [wizardPresets, setWizardPresets] = useState({});
|
|
59
60
|
const [wizardAgents, setWizardAgents] = useState([]);
|
|
60
|
-
/** MCP 配置快照(全局 + 项目),打开向导时刷新 */
|
|
61
61
|
const [wizardMcp, setWizardMcp] = useState(null);
|
|
62
|
-
/** Agent / Sub-agent 定义仓储(快照 + 读写),打开向导时刷新 */
|
|
63
62
|
const [wizardAgentStore, setWizardAgentStore] = useState(null);
|
|
64
|
-
/** 当前会话所用端点 id(打开向导时刷新,用于判断改动是否影响本会话) */
|
|
65
63
|
const currentEndpointRef = useRef(null);
|
|
66
64
|
const [history, setHistory] = useState([]);
|
|
67
65
|
const historyIndex = useRef(-1);
|
|
@@ -72,36 +70,128 @@ export function Chat(props) {
|
|
|
72
70
|
const [viewTop, setViewTop] = useState(null);
|
|
73
71
|
/** 最近一次布局,供键盘回调读取总行数 / 视口高度(应用内翻页) */
|
|
74
72
|
const layoutRef = useRef(null);
|
|
75
|
-
|
|
73
|
+
/** 本轮是否在生成中(spinner / 输入禁用) */
|
|
74
|
+
const [busy, setBusy] = useState(false);
|
|
75
|
+
/**
|
|
76
|
+
* 当前模型 id —— **从会话(即 Agent 定义)读**,不作为 prop 传入。
|
|
77
|
+
*
|
|
78
|
+
* 模型的唯一真相源是 `AgentDefinition.modelId`:所以状态栏、凭据检查、思考状态
|
|
79
|
+
* 与 SDK 实发请求用的永远是同一个值,不会出现「显示 A、实发 B」。
|
|
80
|
+
*/
|
|
81
|
+
const [modelId, setModelId] = useState("");
|
|
82
|
+
/**
|
|
83
|
+
* 思考强度展示文案(关 / 低 / 中 / 高 / 最高 / 开 / —)。
|
|
84
|
+
*
|
|
85
|
+
* 真相在配置里(模型的 `defaultParams`),这里只镜像一份供状态栏用:
|
|
86
|
+
* `/think` 写入后由命令层回调刷新,切模型(会话重建)时靠 `modelId` 变化重读。
|
|
87
|
+
*/
|
|
88
|
+
const [effortText, setEffortText] = useState(null);
|
|
89
|
+
/** 乐观回显:提交后、真实 user 消息落进 messages 之前,先显示用户提问 */
|
|
90
|
+
const [pendingUser, setPendingUser] = useState(null);
|
|
91
|
+
/** 转录显示起点(/clear 清屏用:隐藏此索引之前的消息,但不改上下文) */
|
|
92
|
+
const [transcriptFrom, setTranscriptFrom] = useState(0);
|
|
93
|
+
// ---- Claude Code 同款编辑状态 ----
|
|
94
|
+
/** kill ring:Ctrl+W/U/K 删掉的文本,Ctrl+Y 取回、Alt+Y 循环 */
|
|
95
|
+
const killRingRef = useRef([]);
|
|
96
|
+
const yankIndexRef = useRef(-1);
|
|
97
|
+
/** 编辑撤销栈(Ctrl+_) */
|
|
98
|
+
const undoStackRef = useRef([]);
|
|
99
|
+
/** 暂存区(Ctrl+S) */
|
|
100
|
+
const stashRef = useRef(null);
|
|
101
|
+
/** Ctrl+D 双击退出计时 */
|
|
102
|
+
const ctrlDAtRef = useRef(0);
|
|
103
|
+
/**
|
|
104
|
+
* 通知通道:notice / error 走独立栏位,绝不进对话转录(messages)。
|
|
105
|
+
*/
|
|
106
|
+
const noticeChannelRef = useRef(null);
|
|
107
|
+
if (noticeChannelRef.current == null)
|
|
108
|
+
noticeChannelRef.current = new NoticeChannel();
|
|
109
|
+
const noticeChannel = noticeChannelRef.current;
|
|
110
|
+
const [notice, setNotice] = useState(null);
|
|
111
|
+
useEffect(() => noticeChannel.subscribe(setNotice), [noticeChannel]);
|
|
112
|
+
/**
|
|
113
|
+
* 重挂载附带的系统提示(如 `/model` 的切换结果)。
|
|
114
|
+
*
|
|
115
|
+
* 用 ref 而非 props 直读:本值只在首次挂载消费一次,若写进 create 的依赖数组,
|
|
116
|
+
* 会把「展示一条提示」误变成「重建会话」。
|
|
117
|
+
*/
|
|
118
|
+
const initialNoticeRef = useRef(props.initialNotice);
|
|
119
|
+
/**
|
|
120
|
+
* 渲染信号 + 合帧:SDK 内容变更 → FrameBuffer 合帧 → setTick 触发重渲染。
|
|
121
|
+
* 转录内容始终从 `agent.messages` 现取,tick 只负责「告诉 React 该重画了」。
|
|
122
|
+
*/
|
|
123
|
+
const [, setTick] = useState(0);
|
|
124
|
+
const frameBufferRef = useRef(null);
|
|
125
|
+
if (frameBufferRef.current == null) {
|
|
126
|
+
frameBufferRef.current = new FrameBuffer(() => setTick((t) => t + 1));
|
|
127
|
+
}
|
|
128
|
+
// 思考强度展示:从当前模型的 defaultParams 派生;`/think` 写入后由命令层回调刷新,
|
|
129
|
+
// 切模型(会话重建)时靠 modelId 变化重读。
|
|
130
|
+
useEffect(() => {
|
|
131
|
+
if (!modelId)
|
|
132
|
+
return;
|
|
133
|
+
let alive = true;
|
|
134
|
+
// 档位存在 Agent 定义上(`modelParams`),可选档位在策略表里(`reasoningPreset` 指向),两者都要读
|
|
135
|
+
Promise.all([readConfig(), readAgentStore(), readReasoningPresets()])
|
|
136
|
+
.then(([config, store, presets]) => {
|
|
137
|
+
const model = config.models?.find((item) => item.id === modelId);
|
|
138
|
+
const agentId = props.agentId ?? "default";
|
|
139
|
+
const definition = store.agents.find((item) => item.id === agentId) ?? null;
|
|
140
|
+
if (alive)
|
|
141
|
+
setEffortText(describeEffort(model, presets, definition));
|
|
142
|
+
})
|
|
143
|
+
.catch(() => {
|
|
144
|
+
if (alive)
|
|
145
|
+
setEffortText(null);
|
|
146
|
+
});
|
|
147
|
+
return () => {
|
|
148
|
+
alive = false;
|
|
149
|
+
};
|
|
150
|
+
}, [modelId, props.agentId]);
|
|
76
151
|
// 创建会话
|
|
77
152
|
useEffect(() => {
|
|
78
153
|
let disposed = false;
|
|
154
|
+
const notifier = {
|
|
155
|
+
onChange: (immediate) => {
|
|
156
|
+
if (disposed)
|
|
157
|
+
return;
|
|
158
|
+
frameBufferRef.current?.push(null, immediate);
|
|
159
|
+
},
|
|
160
|
+
onNotice: (text, tone) => {
|
|
161
|
+
if (!disposed)
|
|
162
|
+
noticeChannel.push(text, tone);
|
|
163
|
+
},
|
|
164
|
+
};
|
|
79
165
|
ChatSession.create({
|
|
80
|
-
modelId: props.modelId,
|
|
81
166
|
agentId: props.agentId,
|
|
82
167
|
messages: props.messages,
|
|
83
168
|
conversationId: props.conversationId,
|
|
84
169
|
conversationName: props.conversationName,
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
dispatch(event);
|
|
89
|
-
})
|
|
170
|
+
// 自动迁移:与手动 /migrate、/handoff 共用同一套「交接方式三选一」选择器
|
|
171
|
+
requestMode: (info) => chooseHandoffMode(setPickerState, `上下文即将超限(${info.promptTokens}/${info.handoffTokens} tokens)· 选择交接方式`),
|
|
172
|
+
}, notifier)
|
|
90
173
|
.then((session) => {
|
|
91
174
|
if (disposed) {
|
|
92
175
|
session.dispose();
|
|
93
176
|
return;
|
|
94
177
|
}
|
|
95
178
|
sessionRef.current = session;
|
|
179
|
+
setModelId(session.modelId);
|
|
96
180
|
setReady(true);
|
|
181
|
+
// 重挂载附带的提示只需展示一次(消费后置空,避免再次触发)
|
|
182
|
+
if (initialNoticeRef.current) {
|
|
183
|
+
noticeChannel.push(initialNoticeRef.current);
|
|
184
|
+
initialNoticeRef.current = undefined;
|
|
185
|
+
}
|
|
186
|
+
setTick((t) => t + 1);
|
|
97
187
|
})
|
|
98
188
|
.catch((error) => setBootError(error?.message ?? String(error)));
|
|
99
189
|
return () => {
|
|
100
190
|
disposed = true;
|
|
191
|
+
frameBufferRef.current?.dispose();
|
|
101
192
|
sessionRef.current?.dispose();
|
|
102
193
|
};
|
|
103
194
|
}, [
|
|
104
|
-
props.modelId,
|
|
105
195
|
props.agentId,
|
|
106
196
|
props.messages,
|
|
107
197
|
props.conversationId,
|
|
@@ -120,32 +210,29 @@ export function Chat(props) {
|
|
|
120
210
|
});
|
|
121
211
|
};
|
|
122
212
|
// ---- 配置向导(凭据 / 配置中心)----
|
|
123
|
-
/** 打开配置向导:先取配置 / MCP / Agent 快照,再切入指定步骤 */
|
|
124
213
|
const openWizard = async (step) => {
|
|
125
214
|
try {
|
|
126
215
|
const config = await readConfig();
|
|
127
|
-
currentEndpointRef.current = resolveCredential(config,
|
|
216
|
+
currentEndpointRef.current = resolveCredential(config, modelId)?.endpointId ?? null;
|
|
128
217
|
const agentStore = await readAgentStore();
|
|
129
218
|
setWizardConfig(config);
|
|
219
|
+
setWizardPresets(await readReasoningPresets());
|
|
130
220
|
setWizardAgents(agentStore.agents.map((agent) => ({ id: agent.id, name: agent.name || agent.id })));
|
|
131
221
|
setWizardAgentStore(agentStore);
|
|
132
222
|
setWizardMcp({ global: await readMcpConfig("global"), project: await readMcpConfig("project") });
|
|
133
223
|
setWizardStep(step);
|
|
134
224
|
}
|
|
135
225
|
catch (error) {
|
|
136
|
-
|
|
226
|
+
noticeChannel.push(`读取配置失败:${error?.message ?? error}`, "error");
|
|
137
227
|
}
|
|
138
228
|
};
|
|
139
229
|
/** `/key`:直达「当前模型所用端点」的 API Key 编辑步骤 */
|
|
140
230
|
const openKeyEditor = async () => {
|
|
141
231
|
try {
|
|
142
232
|
const config = await readConfig();
|
|
143
|
-
const credential = resolveCredential(config,
|
|
233
|
+
const credential = resolveCredential(config, modelId);
|
|
144
234
|
if (!credential) {
|
|
145
|
-
|
|
146
|
-
type: "error",
|
|
147
|
-
text: `模型 ${props.modelId} 未绑定可用端点,请用 /config 检查配置`,
|
|
148
|
-
});
|
|
235
|
+
noticeChannel.push(`模型 ${modelId} 未绑定可用端点,请用 /config 检查配置`, "error");
|
|
149
236
|
return;
|
|
150
237
|
}
|
|
151
238
|
currentEndpointRef.current = credential.endpointId;
|
|
@@ -153,7 +240,7 @@ export function Chat(props) {
|
|
|
153
240
|
setWizardStep({ kind: "edit", field: "apiKey", endpointId: credential.endpointId });
|
|
154
241
|
}
|
|
155
242
|
catch (error) {
|
|
156
|
-
|
|
243
|
+
noticeChannel.push(`读取配置失败:${error?.message ?? error}`, "error");
|
|
157
244
|
}
|
|
158
245
|
};
|
|
159
246
|
/** 向导关闭:渲染变更摘要;若改到当前端点则重建会话(消息保留) */
|
|
@@ -163,10 +250,9 @@ export function Chat(props) {
|
|
|
163
250
|
setWizardMcp(null);
|
|
164
251
|
setWizardAgentStore(null);
|
|
165
252
|
for (const line of result.summary)
|
|
166
|
-
|
|
253
|
+
noticeChannel.push(`配置已更新:${line}`);
|
|
167
254
|
if (!result.dirty)
|
|
168
255
|
return;
|
|
169
|
-
// 会话尚未建立(启动失败进入向导):重挂载对话屏重试
|
|
170
256
|
if (!sessionRef.current) {
|
|
171
257
|
exit({ restart: true, messages: props.messages });
|
|
172
258
|
return;
|
|
@@ -175,50 +261,53 @@ export function Chat(props) {
|
|
|
175
261
|
if (endpointChanged || result.mcpChanged || result.agentsChanged) {
|
|
176
262
|
sessionRef.current
|
|
177
263
|
.reload()
|
|
178
|
-
.then(() =>
|
|
179
|
-
.catch((error) =>
|
|
264
|
+
.then(() => noticeChannel.push("已按新配置重建会话(消息与上下文保留)"))
|
|
265
|
+
.catch((error) => noticeChannel.push(`重建会话失败:${error?.message ?? error}`, "error"));
|
|
180
266
|
}
|
|
181
267
|
};
|
|
182
268
|
// ---- 滚动视口控制(应用内翻页,回看历史)----
|
|
183
|
-
/** 跳到指定顶部行;越界钳制;一旦贴到底部则恢复「自动跟随」 */
|
|
184
269
|
const scrollTo = (nextTop) => {
|
|
185
270
|
const L = layoutRef.current;
|
|
186
271
|
if (!L)
|
|
187
272
|
return;
|
|
188
|
-
|
|
189
|
-
const clamped = Math.max(0, Math.min(maxTop, nextTop));
|
|
190
|
-
setViewTop(clamped >= maxTop ? null : clamped);
|
|
273
|
+
setViewTop(clampViewTop(L.totalLines, L.budget, nextTop));
|
|
191
274
|
};
|
|
192
|
-
/** 翻页滚动(dir:-1 上翻 / +1 下翻) */
|
|
193
275
|
const scrollByPage = (dir) => {
|
|
194
276
|
const L = layoutRef.current;
|
|
195
277
|
if (!L)
|
|
196
278
|
return;
|
|
197
279
|
scrollTo(L.viewTop + dir * Math.max(1, L.budget - 1));
|
|
198
280
|
};
|
|
199
|
-
/** 单行滚动(dir:-1 上 / +1 下) */
|
|
200
281
|
const scrollByLine = (dir) => {
|
|
201
282
|
const L = layoutRef.current;
|
|
202
283
|
if (!L)
|
|
203
284
|
return;
|
|
204
285
|
scrollTo(L.viewTop + dir);
|
|
205
286
|
};
|
|
206
|
-
/**
|
|
287
|
+
/** 鼠标滚轮:一格滚 `WHEEL_STEP` 行(与按行滚动共用一个钳制入口) */
|
|
288
|
+
const scrollByWheel = (dir) => {
|
|
289
|
+
const L = layoutRef.current;
|
|
290
|
+
if (!L)
|
|
291
|
+
return;
|
|
292
|
+
scrollTo(L.viewTop + dir * WHEEL_STEP);
|
|
293
|
+
};
|
|
207
294
|
const scrollToBottom = () => setViewTop(null);
|
|
295
|
+
/** 清屏但保留上下文:把显示起点推到当前消息末尾 */
|
|
296
|
+
const clearTranscript = () => {
|
|
297
|
+
setTranscriptFrom(sessionRef.current?.messages.length ?? 0);
|
|
298
|
+
setNotice(null);
|
|
299
|
+
};
|
|
208
300
|
// ---- 输入编辑(光标感知)----
|
|
209
|
-
/** 替换整个输入并把光标移到末尾 */
|
|
210
301
|
const setInputText = (text) => {
|
|
211
302
|
setInput(text);
|
|
212
303
|
setCursor(Array.from(text).length);
|
|
213
304
|
};
|
|
214
|
-
/** 在光标处插入文本 */
|
|
215
305
|
const insertAtCursor = (text) => {
|
|
216
306
|
const chars = Array.from(input);
|
|
217
307
|
const cp = Math.max(0, Math.min(cursor, chars.length));
|
|
218
308
|
setInput(chars.slice(0, cp).join("") + text + chars.slice(cp).join(""));
|
|
219
309
|
setCursor(cp + Array.from(text).length);
|
|
220
310
|
};
|
|
221
|
-
/** 删除光标前一个字符(Backspace) */
|
|
222
311
|
const deleteBeforeCursor = () => {
|
|
223
312
|
const chars = Array.from(input);
|
|
224
313
|
const cp = Math.max(0, Math.min(cursor, chars.length));
|
|
@@ -227,7 +316,6 @@ export function Chat(props) {
|
|
|
227
316
|
setInput(chars.slice(0, cp - 1).join("") + chars.slice(cp).join(""));
|
|
228
317
|
setCursor(cp - 1);
|
|
229
318
|
};
|
|
230
|
-
/** 删除光标后一个字符(Delete) */
|
|
231
319
|
const deleteAfterCursor = () => {
|
|
232
320
|
const chars = Array.from(input);
|
|
233
321
|
const cp = Math.max(0, Math.min(cursor, chars.length));
|
|
@@ -235,12 +323,73 @@ export function Chat(props) {
|
|
|
235
323
|
return;
|
|
236
324
|
setInput(chars.slice(0, cp).join("") + chars.slice(cp + 1).join(""));
|
|
237
325
|
};
|
|
238
|
-
/**
|
|
326
|
+
/** 套用纯函数算好的「文本 + 光标」结果(无变化则不触发重渲染) */
|
|
327
|
+
const applyEdit = (r) => {
|
|
328
|
+
if (r.text === input && r.cursor === cursor)
|
|
329
|
+
return;
|
|
330
|
+
undoStackRef.current = pushUndo(undoStackRef.current, { text: input, cursor });
|
|
331
|
+
setInput(r.text);
|
|
332
|
+
setCursor(r.cursor);
|
|
333
|
+
};
|
|
334
|
+
/** 删除类编辑:额外把删掉的文本放进 kill ring(Ctrl+Y 可取回) */
|
|
335
|
+
const applyKill = (r) => {
|
|
336
|
+
if (r.text === input && r.cursor === cursor)
|
|
337
|
+
return;
|
|
338
|
+
killRingRef.current = pushKill(killRingRef.current, killedText(input, r.text));
|
|
339
|
+
yankIndexRef.current = -1;
|
|
340
|
+
applyEdit(r);
|
|
341
|
+
};
|
|
342
|
+
const deleteWordBeforeCursor = () => applyKill(deleteWordLeft(input, cursor));
|
|
343
|
+
const deleteWordAfterCursor = () => applyKill(deleteWordRight(input, cursor));
|
|
344
|
+
const deleteToEndOfLine = () => applyKill(deleteToLineEnd(input, cursor));
|
|
345
|
+
const deleteToStartOfLine = () => applyKill(deleteToLineStart(input, cursor));
|
|
346
|
+
/** Ctrl+W:删到前一空白(标点不切分,整条路径一次删掉) */
|
|
347
|
+
const deletePrevWhitespace = () => applyKill(deleteToPrevWhitespace(input, cursor));
|
|
348
|
+
/** Ctrl+Y / Alt+Y:插入 kill ring 中的一段(index 循环) */
|
|
349
|
+
const yank = (advance) => {
|
|
350
|
+
const ring = killRingRef.current;
|
|
351
|
+
if (ring.length === 0)
|
|
352
|
+
return;
|
|
353
|
+
const next = advance ? yankIndexRef.current + 1 : 0;
|
|
354
|
+
if (next >= ring.length)
|
|
355
|
+
return;
|
|
356
|
+
yankIndexRef.current = next;
|
|
357
|
+
const text = killAt(ring, next) ?? "";
|
|
358
|
+
insertAtCursor(text);
|
|
359
|
+
};
|
|
360
|
+
/** Ctrl+_:撤销上一次编辑 */
|
|
361
|
+
const undoEdit = () => {
|
|
362
|
+
const popped = popUndo(undoStackRef.current);
|
|
363
|
+
if (!popped)
|
|
364
|
+
return;
|
|
365
|
+
undoStackRef.current = popped.rest;
|
|
366
|
+
setInput(popped.state.text);
|
|
367
|
+
setCursor(popped.state.cursor);
|
|
368
|
+
};
|
|
369
|
+
/** Ctrl+S:暂存 / 恢复输入 */
|
|
370
|
+
const toggleStash = () => {
|
|
371
|
+
if (input.length > 0) {
|
|
372
|
+
stashRef.current = { text: input, cursor };
|
|
373
|
+
setInputText("");
|
|
374
|
+
noticeChannel.push("已暂存输入(Ctrl+S 恢复)");
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
const stashed = stashRef.current;
|
|
378
|
+
if (!stashed)
|
|
379
|
+
return;
|
|
380
|
+
stashRef.current = null;
|
|
381
|
+
setInput(stashed.text);
|
|
382
|
+
setCursor(stashed.cursor);
|
|
383
|
+
noticeChannel.push("已恢复暂存的输入");
|
|
384
|
+
};
|
|
385
|
+
/** Ctrl+V:粘粘图片 —— 暂未支持(需先做「文件 → 内容块」能力探测,见 SKILL/后续迭代) */
|
|
386
|
+
const pasteImageUnsupported = () => {
|
|
387
|
+
noticeChannel.push("暂不支持粘贴图片(Ctrl+V 预留给图片粘贴,当前未实现)", "error");
|
|
388
|
+
};
|
|
239
389
|
const moveCursor = (delta) => {
|
|
240
390
|
const len = Array.from(input).length;
|
|
241
391
|
setCursor((c) => Math.max(0, Math.min(len, c + delta)));
|
|
242
392
|
};
|
|
243
|
-
/** 光标移到当前逻辑行行首(Home) */
|
|
244
393
|
const moveCursorToLineStart = () => {
|
|
245
394
|
const chars = Array.from(input);
|
|
246
395
|
const cp = Math.max(0, Math.min(cursor, chars.length));
|
|
@@ -249,7 +398,6 @@ export function Chat(props) {
|
|
|
249
398
|
i -= 1;
|
|
250
399
|
setCursor(i);
|
|
251
400
|
};
|
|
252
|
-
/** 光标移到当前逻辑行行尾(End) */
|
|
253
401
|
const moveCursorToLineEnd = () => {
|
|
254
402
|
const chars = Array.from(input);
|
|
255
403
|
const cp = Math.max(0, Math.min(cursor, chars.length));
|
|
@@ -268,33 +416,56 @@ export function Chat(props) {
|
|
|
268
416
|
setInputText("");
|
|
269
417
|
historyIndex.current = -1;
|
|
270
418
|
scrollToBottom();
|
|
419
|
+
setNotice(null);
|
|
271
420
|
if (trimmed.startsWith("/")) {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
421
|
+
// 命令名取首个 token:支持 `/migrate summary`、`/handoff self` 这类带参数的写法
|
|
422
|
+
const name = trimmed.slice(1).split(/\s+/)[0].toLowerCase();
|
|
423
|
+
// 交接/迁移会调用模型写文档(较慢):期间应像自动迁移一样拦住输入框。
|
|
424
|
+
const blocksInput = name === "migrate" || name === "migration" || name === "handoff";
|
|
425
|
+
if (blocksInput)
|
|
426
|
+
setBusy(true);
|
|
427
|
+
try {
|
|
428
|
+
await runCommand(name, trimmed, {
|
|
429
|
+
session: sessionRef.current,
|
|
430
|
+
notice: (text, tone) => noticeChannel.push(text, tone),
|
|
431
|
+
clearTranscript,
|
|
432
|
+
rows: rows ?? 24,
|
|
433
|
+
exit,
|
|
434
|
+
suspendTerminal,
|
|
435
|
+
openWizard,
|
|
436
|
+
openKeyEditor,
|
|
437
|
+
doExit,
|
|
438
|
+
setPickerState,
|
|
439
|
+
setConfirmState,
|
|
440
|
+
setInputText,
|
|
441
|
+
setEffortText,
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
finally {
|
|
445
|
+
if (blocksInput)
|
|
446
|
+
setBusy(false);
|
|
447
|
+
}
|
|
286
448
|
return;
|
|
287
449
|
}
|
|
288
450
|
setHistory((h) => [...h, trimmed]);
|
|
289
|
-
|
|
451
|
+
const base = sessionRef.current.messages.filter((m) => m.role === AgentNS.Role.User).length;
|
|
452
|
+
setPendingUser({ text: trimmed, base });
|
|
453
|
+
setBusy(true);
|
|
454
|
+
try {
|
|
455
|
+
await sessionRef.current.send(trimmed);
|
|
456
|
+
}
|
|
457
|
+
finally {
|
|
458
|
+
setBusy(false);
|
|
459
|
+
setPendingUser(null);
|
|
460
|
+
}
|
|
290
461
|
};
|
|
291
462
|
const doExit = () => {
|
|
292
|
-
// 会话已由 ConversationPersistPlugin 实时落盘,退出无需再询问是否保存
|
|
293
463
|
exit("done");
|
|
294
464
|
};
|
|
295
465
|
// 输入处理
|
|
296
466
|
useInput((char, key) => {
|
|
297
|
-
//
|
|
467
|
+
// 按键诊断(AIZ_DEBUG_KEYS=1):必须在任何 return 之前记录,否则被吞掉的键不会留痕
|
|
468
|
+
keyDebug(char, key);
|
|
298
469
|
if (bootError) {
|
|
299
470
|
if (char === "k" || char === "K") {
|
|
300
471
|
void openWizard({ kind: "menu" });
|
|
@@ -304,10 +475,16 @@ export function Chat(props) {
|
|
|
304
475
|
exit("done");
|
|
305
476
|
return;
|
|
306
477
|
}
|
|
307
|
-
//
|
|
478
|
+
// 鼠标滚轮:滚视口。**必须**排在下面 `isTerminalReply` 之前——那里会把整条鼠标序列
|
|
479
|
+
// 当「终端上报」吃掉,放后面就永远收不到滚轮。其余鼠标事件(点击 / 拖拽 / 横向滚轮)
|
|
480
|
+
// 交给 `isTerminalReply` 丢弃,绝不落下到输入框。
|
|
481
|
+
const wheel = parseWheel(char);
|
|
482
|
+
if (wheel) {
|
|
483
|
+
scrollByWheel(wheel.direction);
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
308
486
|
if (isTerminalReply(char))
|
|
309
487
|
return;
|
|
310
|
-
// 翻页滚动(回看历史)
|
|
311
488
|
if (key.pageUp) {
|
|
312
489
|
scrollByPage(-1);
|
|
313
490
|
return;
|
|
@@ -316,43 +493,147 @@ export function Chat(props) {
|
|
|
316
493
|
scrollByPage(1);
|
|
317
494
|
return;
|
|
318
495
|
}
|
|
319
|
-
//
|
|
320
|
-
//
|
|
321
|
-
|
|
496
|
+
// 翻页的「兜底键」:部分终端把 PgUp/PgDn 留给自己滚 scrollback(应用收不到)。
|
|
497
|
+
// 注意 `Ctrl+V` 预留给图片粘贴(当前未实现),不再兼任 page-down。
|
|
498
|
+
if (char === "v" && key.meta) {
|
|
499
|
+
scrollByPage(-1);
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
// macOS 习惯键:⌘[ / ⌘] 翻页、⌘; / ⌘' 按行滚动。
|
|
503
|
+
// Cmd 只在 kitty 键盘协议下才会带 super 位(或终端把 ⌘ 组合映射成带 8 号
|
|
504
|
+
// 修饰位的转义序列,那种会落到上面的 meta 分支里,同样生效)。
|
|
505
|
+
if (key.super && (char === "[" || char === "]")) {
|
|
506
|
+
scrollByPage(char === "[" ? -1 : 1);
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
if (key.super && (char === ";" || char === "'")) {
|
|
510
|
+
scrollByLine(char === ";" ? -1 : 1);
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
322
513
|
if (key.return && (key.ctrl || key.meta || key.shift)) {
|
|
323
514
|
insertAtCursor("\n");
|
|
324
515
|
return;
|
|
325
516
|
}
|
|
326
517
|
if (char === "c" && key.ctrl) {
|
|
518
|
+
// Claude Code:生成中中断;空闲时有草稿就先清空,已空则退出
|
|
327
519
|
if (busy)
|
|
328
520
|
sessionRef.current?.abort();
|
|
521
|
+
else if (input.length > 0) {
|
|
522
|
+
setInputText("");
|
|
523
|
+
noticeChannel.push("已清空输入 · 再按一次 Ctrl+C 退出");
|
|
524
|
+
}
|
|
329
525
|
else
|
|
330
526
|
void doExit();
|
|
331
527
|
return;
|
|
332
528
|
}
|
|
333
529
|
if (char === "d" && key.ctrl) {
|
|
334
|
-
|
|
530
|
+
// Claude Code:有草稿时删光标后一个字符;空输入需双击确认退出
|
|
531
|
+
if (input.length > 0) {
|
|
532
|
+
deleteAfterCursor();
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
const now = Date.now();
|
|
536
|
+
if (now - ctrlDAtRef.current < 800) {
|
|
537
|
+
void doExit();
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
ctrlDAtRef.current = now;
|
|
541
|
+
noticeChannel.push("再按一次 Ctrl+D 退出");
|
|
335
542
|
return;
|
|
336
543
|
}
|
|
337
544
|
if (char === "l" && key.ctrl) {
|
|
338
|
-
|
|
545
|
+
clearTranscript();
|
|
339
546
|
return;
|
|
340
547
|
}
|
|
548
|
+
// ---- kill / yank / undo / stash(照抄 Claude Code / readline)----
|
|
341
549
|
if (char === "u" && key.ctrl) {
|
|
342
|
-
|
|
550
|
+
// 删到行首。macOS 上 ⌘⌫ 会被终端映射成 Ctrl+U,故不能做成「清空全部」
|
|
551
|
+
deleteToStartOfLine();
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
if (char === "k" && key.ctrl) {
|
|
555
|
+
deleteToEndOfLine();
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
if (char === "w" && key.ctrl) {
|
|
559
|
+
// 删到前一空白(标点不切分:`src/utils/foo.ts` 一次删完)
|
|
560
|
+
deletePrevWhitespace();
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
if (char === "y" && key.ctrl) {
|
|
564
|
+
yank(false);
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
if (char === "y" && key.meta) {
|
|
568
|
+
yank(true);
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
// Ctrl+_ / Ctrl+Shift+-:撤销上一次输入编辑
|
|
572
|
+
if ((char === "_" && key.ctrl) || (char === "-" && key.ctrl && key.shift)) {
|
|
573
|
+
undoEdit();
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
if (char === "s" && key.ctrl) {
|
|
577
|
+
toggleStash();
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
if ((char === "v" && key.ctrl) || (key.super && (char === "v" || char === "V"))) {
|
|
581
|
+
pasteImageUnsupported();
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
// readline 的行首 / 行尾(macOS 上部分终端把 ⌘← / ⌘→ 发成 ^A / ^E)
|
|
585
|
+
if (char === "a" && key.ctrl) {
|
|
586
|
+
moveCursorToLineStart();
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
if (char === "e" && key.ctrl) {
|
|
590
|
+
moveCursorToLineEnd();
|
|
343
591
|
return;
|
|
344
592
|
}
|
|
345
593
|
if (char === "j" && key.ctrl) {
|
|
346
594
|
insertAtCursor("\n");
|
|
347
595
|
return;
|
|
348
596
|
}
|
|
349
|
-
//
|
|
350
|
-
if (char === "n" && key.ctrl) {
|
|
351
|
-
|
|
597
|
+
// Ctrl+P / Ctrl+N:历史上下(Claude Code 语义;换行改用 Ctrl+J / Alt+Enter)
|
|
598
|
+
if ((char === "p" && key.ctrl) || (char === "n" && key.ctrl)) {
|
|
599
|
+
const up = char === "p";
|
|
600
|
+
if (hints.length > 0) {
|
|
601
|
+
setMenuIndex((i) => (up ? (i - 1 + hints.length) % hints.length : (i + 1) % hints.length));
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
if (up && history.length > 0) {
|
|
605
|
+
const idx = historyIndex.current < 0 ? history.length - 1 : Math.max(0, historyIndex.current - 1);
|
|
606
|
+
historyIndex.current = idx;
|
|
607
|
+
setInputText(history[idx]);
|
|
608
|
+
}
|
|
609
|
+
else if (!up && historyIndex.current >= 0) {
|
|
610
|
+
const idx = historyIndex.current + 1;
|
|
611
|
+
if (idx >= history.length) {
|
|
612
|
+
historyIndex.current = -1;
|
|
613
|
+
setInputText("");
|
|
614
|
+
}
|
|
615
|
+
else {
|
|
616
|
+
historyIndex.current = idx;
|
|
617
|
+
setInputText(history[idx]);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
// 按词删除:Alt+Backspace / Ctrl+Backspace 删前一个词;Alt+D / Ctrl+Delete 删后一个词。
|
|
623
|
+
// 必须在下面「单字符 Backspace/Delete」之前判定,否则带修饰键的删除只掉一个字符。
|
|
624
|
+
if (key.backspace && (key.ctrl || key.meta || key.super)) {
|
|
625
|
+
deleteWordBeforeCursor();
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
if (key.delete && (key.ctrl || key.meta || key.super)) {
|
|
629
|
+
deleteWordAfterCursor();
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
if (char === "d" && key.meta) {
|
|
633
|
+
deleteWordAfterCursor();
|
|
352
634
|
return;
|
|
353
635
|
}
|
|
354
636
|
if (key.return) {
|
|
355
|
-
// 斜杠候选菜单可见时回车直接执行高亮命令,避免把半截输入(如仅 `/`)当命令提交
|
|
356
637
|
void runSubmit(resolveSubmitText(input, hints, menuIndex));
|
|
357
638
|
return;
|
|
358
639
|
}
|
|
@@ -361,7 +642,6 @@ export function Chat(props) {
|
|
|
361
642
|
return;
|
|
362
643
|
}
|
|
363
644
|
if (key.upArrow || key.downArrow) {
|
|
364
|
-
// Shift / Alt + ↑↓:单行滚动(优先于历史召回)
|
|
365
645
|
if (key.shift || key.meta) {
|
|
366
646
|
scrollByLine(key.upArrow ? -1 : 1);
|
|
367
647
|
return;
|
|
@@ -370,7 +650,6 @@ export function Chat(props) {
|
|
|
370
650
|
setMenuIndex((i) => key.upArrow ? (i - 1 + hints.length) % hints.length : (i + 1) % hints.length);
|
|
371
651
|
return;
|
|
372
652
|
}
|
|
373
|
-
// 历史召回
|
|
374
653
|
if (key.upArrow && history.length > 0) {
|
|
375
654
|
const idx = historyIndex.current < 0 ? history.length - 1 : Math.max(0, historyIndex.current - 1);
|
|
376
655
|
historyIndex.current = idx;
|
|
@@ -389,21 +668,31 @@ export function Chat(props) {
|
|
|
389
668
|
}
|
|
390
669
|
return;
|
|
391
670
|
}
|
|
392
|
-
// 光标移动:← → 单字符,Ctrl/Alt+← → 跨词,Home / End 到行首 / 行尾
|
|
393
671
|
if (key.leftArrow) {
|
|
394
|
-
if (key.ctrl || key.meta)
|
|
672
|
+
if (key.ctrl || key.meta || key.super)
|
|
395
673
|
setCursor(wordLeft(input, cursor));
|
|
396
674
|
else
|
|
397
675
|
moveCursor(-1);
|
|
398
676
|
return;
|
|
399
677
|
}
|
|
400
678
|
if (key.rightArrow) {
|
|
401
|
-
if (key.ctrl || key.meta)
|
|
679
|
+
if (key.ctrl || key.meta || key.super)
|
|
402
680
|
setCursor(wordRight(input, cursor));
|
|
403
681
|
else
|
|
404
682
|
moveCursor(1);
|
|
405
683
|
return;
|
|
406
684
|
}
|
|
685
|
+
// 跨词移动的「兜底键」:macOS 上 Option+←/→ 往往发的是 ESC b / ESC f
|
|
686
|
+
// (readline 的 backward-word / forward-word),并不带 leftArrow 标记;
|
|
687
|
+
// 而 Ctrl+←/→ 常被系统「Mission Control → 左右切换空间」截走,应用根本收不到。
|
|
688
|
+
if (char === "b" && key.meta) {
|
|
689
|
+
setCursor(wordLeft(input, cursor));
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
if (char === "f" && key.meta) {
|
|
693
|
+
setCursor(wordRight(input, cursor));
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
407
696
|
if (key.home) {
|
|
408
697
|
moveCursorToLineStart();
|
|
409
698
|
return;
|
|
@@ -413,8 +702,13 @@ export function Chat(props) {
|
|
|
413
702
|
return;
|
|
414
703
|
}
|
|
415
704
|
if (key.escape) {
|
|
416
|
-
|
|
417
|
-
|
|
705
|
+
// Claude Code:生成中 Esc = 中断本轮;否则清空草稿并回到底部
|
|
706
|
+
if (busy)
|
|
707
|
+
sessionRef.current?.abort();
|
|
708
|
+
else {
|
|
709
|
+
setInputText("");
|
|
710
|
+
scrollToBottom();
|
|
711
|
+
}
|
|
418
712
|
return;
|
|
419
713
|
}
|
|
420
714
|
if (key.backspace) {
|
|
@@ -425,13 +719,72 @@ export function Chat(props) {
|
|
|
425
719
|
deleteAfterCursor();
|
|
426
720
|
return;
|
|
427
721
|
}
|
|
428
|
-
if (char && !key.ctrl && !key.meta) {
|
|
722
|
+
if (char && !key.ctrl && !key.meta && !key.super) {
|
|
429
723
|
insertAtCursor(char);
|
|
430
724
|
}
|
|
431
|
-
},
|
|
725
|
+
},
|
|
726
|
+
// 生成 / 自动迁移期间主输入框整体失活:此时输入框已被 Spinner 顶替,若仍激活,
|
|
727
|
+
// 按键会被静默吞进隐藏的输入缓冲、待本轮结束后才冒出来(即「迁移过程中输入框
|
|
728
|
+
// 没被拦住」)。失活后改由下方「忙碌期」处理器只放行中止 / 退出 / 回看滚动。
|
|
729
|
+
{ isActive: !busy && !confirmState && !pickerState && !wizardStep });
|
|
730
|
+
// 忙碌期(生成 / 迁移中)专用:输入框不可编辑,仅保留「中止(Ctrl+C) / 退出(Ctrl+D) / 回看滚动」。
|
|
731
|
+
useInput((char, key) => {
|
|
732
|
+
keyDebug(char, key);
|
|
733
|
+
// 忙碌期只剩「中止 / 退出 / 回看」,滚轮同样生效(生成时正是最想往回看的时机)
|
|
734
|
+
const wheel = parseWheel(char);
|
|
735
|
+
if (wheel) {
|
|
736
|
+
scrollByWheel(wheel.direction);
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
if (isTerminalReply(char))
|
|
740
|
+
return;
|
|
741
|
+
if (char === "c" && key.ctrl) {
|
|
742
|
+
sessionRef.current?.abort();
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
if (key.escape) {
|
|
746
|
+
// 忙碌期 Esc 也充当「中断」(与 Claude Code 一致)
|
|
747
|
+
sessionRef.current?.abort();
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
if (char === "d" && key.ctrl) {
|
|
751
|
+
const now = Date.now();
|
|
752
|
+
if (now - ctrlDAtRef.current < 800) {
|
|
753
|
+
void doExit();
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
ctrlDAtRef.current = now;
|
|
757
|
+
noticeChannel.push("再按一次 Ctrl+D 退出");
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
if (key.pageUp) {
|
|
761
|
+
scrollByPage(-1);
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
if (key.pageDown) {
|
|
765
|
+
scrollByPage(1);
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
if ((key.upArrow || key.downArrow) && (key.shift || key.meta || key.super)) {
|
|
769
|
+
scrollByLine(key.upArrow ? -1 : 1);
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
if (char === "v" && (key.meta || key.super)) {
|
|
773
|
+
scrollByPage(-1);
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
if (key.super && (char === "[" || char === "]")) {
|
|
777
|
+
scrollByPage(char === "[" ? -1 : 1);
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
if (key.super && (char === ";" || char === "'")) {
|
|
781
|
+
scrollByLine(char === ";" ? -1 : 1);
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
}, { isActive: busy && !confirmState && !pickerState && !wizardStep });
|
|
432
785
|
// 配置向导:整屏接管(凭据设置 / 配置中心),关闭后回到对话屏
|
|
433
786
|
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) => {
|
|
787
|
+
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
788
|
await writeMcpConfig(cfg, scope);
|
|
436
789
|
}, onClose: closeWizard }));
|
|
437
790
|
}
|
|
@@ -440,16 +793,25 @@ export function Chat(props) {
|
|
|
440
793
|
return (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsxs(Text, { color: theme.error, wrap: "truncate", children: ["\u2716 \u65E0\u6CD5\u542F\u52A8\u5BF9\u8BDD\uFF1A", bootError] }), _jsx(Text, { color: theme.dim, wrap: "truncate", children: "\u6309 k \u6253\u5F00\u914D\u7F6E\u4E2D\u5FC3\uFF08\u53EF\u8BBE\u7F6E API Key / \u7AEF\u70B9\u5730\u5740\uFF09\u00B7 Enter \u8FD4\u56DE" })] }));
|
|
441
794
|
}
|
|
442
795
|
const session = sessionRef.current;
|
|
443
|
-
const
|
|
796
|
+
const lastTokens = session?.lastUsage?.total_tokens;
|
|
797
|
+
const totalTokens = session?.totalUsage.total_tokens;
|
|
444
798
|
const cols = columns ?? 80;
|
|
799
|
+
const messages = session?.messages ?? [];
|
|
800
|
+
const userCount = messages.filter((m) => m.role === AgentNS.Role.User).length;
|
|
801
|
+
const showPending = pendingUser != null && userCount <= pendingUser.base;
|
|
802
|
+
const transcript = transcriptLines(messages.slice(transcriptFrom), cols);
|
|
803
|
+
if (showPending) {
|
|
804
|
+
transcript.push(...messageToLines({ id: "__pending_user", role: AgentNS.Role.User, content: pendingUser.text }, cols));
|
|
805
|
+
}
|
|
806
|
+
const noticeTextLines = notice ? noticeLines(notice, cols) : [];
|
|
445
807
|
const layout = computeChatLayout({
|
|
446
808
|
columns: cols,
|
|
447
809
|
rows: usableFrameRows(rows ?? 24),
|
|
448
|
-
|
|
449
|
-
live: state.live,
|
|
810
|
+
transcript,
|
|
450
811
|
input,
|
|
451
812
|
busy,
|
|
452
813
|
hintCount: hints.length,
|
|
814
|
+
noticeCount: noticeTextLines.length,
|
|
453
815
|
overlay: confirmState ? "confirm" : pickerState ? "picker" : "none",
|
|
454
816
|
pickerItems: pickerState?.items.length ?? 0,
|
|
455
817
|
viewTop,
|
|
@@ -472,7 +834,7 @@ export function Chat(props) {
|
|
|
472
834
|
else {
|
|
473
835
|
setCursorPosition(undefined);
|
|
474
836
|
}
|
|
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 ? (_jsx(Spinner, { label: "\u601D\u8003\u4E2D\u2026" })) : (bodyLines.map((line, i) => {
|
|
837
|
+
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: [noticeTextLines.length > 0 ? (_jsx(Box, { flexDirection: "column", paddingX: 1, children: noticeTextLines.map((line, i) => (_jsx(Text, { color: notice?.tone === "error" ? theme.error : theme.dim, wrap: "truncate", children: line }, i))) })) : null, 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 ? (_jsx(Spinner, { label: "\u601D\u8003\u4E2D\u2026" })) : (bodyLines.map((line, i) => {
|
|
476
838
|
const prompt = i === 0 ? (_jsx(Text, { color: theme.brand[0], bold: true, children: "❯ " })) : (_jsx(Text, { children: " " }));
|
|
477
839
|
if (isPlaceholder) {
|
|
478
840
|
return (_jsxs(Text, { wrap: "truncate", children: [prompt, _jsx(Text, { color: theme.faint, children: line }), i === bodyLines.length - 1 ? (_jsx(Text, { color: theme.brand[1], children: "\u258F" })) : null] }, i));
|
|
@@ -485,10 +847,10 @@ export function Chat(props) {
|
|
|
485
847
|
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));
|
|
486
848
|
}
|
|
487
849
|
return (_jsxs(Text, { wrap: "truncate", children: [prompt, _jsx(Text, { children: line })] }, i));
|
|
488
|
-
})) })] })), _jsx(StatusBar, { model:
|
|
850
|
+
})) })] })), _jsx(StatusBar, { model: modelId, agent: props.agentId ?? "default", effort: effortText, lastTokens: lastTokens, totalTokens: totalTokens, busy: busy, hint: !ready
|
|
489
851
|
? "初始化中…"
|
|
490
852
|
: layout.scrolled
|
|
491
853
|
? "↕ 滚动中 · PgUp/PgDn 翻页 · 发送回到底部"
|
|
492
|
-
: "Enter 发送 · Ctrl+
|
|
854
|
+
: "Enter 发送 · Ctrl+J 换行 · / 命令 · PgUp 看历史" })] }));
|
|
493
855
|
}
|
|
494
856
|
//# sourceMappingURL=chat.js.map
|