mocode-ai 1.3.9 → 1.4.0
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/README.md +78 -70
- package/README.zh-CN.md +81 -73
- package/bin/mocode-agent-host.js +1 -1
- package/dist/agent/core.js +226 -293
- package/dist/agent/index.js +14 -10
- package/dist/agent/mode.js +3 -3
- package/dist/agent/runtime-context.js +45 -0
- package/dist/agent/spawn.js +101 -86
- package/dist/agent/tool-helpers.js +134 -0
- package/dist/agent/trace-state.js +118 -0
- package/dist/agents/coordinator.js +6 -1
- package/dist/changeset/index.js +9 -8
- package/dist/config/index.js +109 -77
- package/dist/config/presets.js +3 -1
- package/dist/config/profiles.js +125 -0
- package/dist/context/age-aware.js +2 -4
- package/dist/context/artifacts.js +6 -13
- package/dist/context/budget.js +6 -8
- package/dist/context/classifier.js +1 -2
- package/dist/context/encoders/code.js +2 -6
- package/dist/context/encoders/command.js +1 -3
- package/dist/context/encoders/search.js +1 -3
- package/dist/context/index.js +1 -1
- package/dist/context/lifecycle.js +5 -3
- package/dist/context/pipeline.js +1 -1
- package/dist/context/relevance.js +2 -4
- package/dist/context/token-calibration.js +13 -18
- package/dist/host/stdio.js +73 -12
- package/dist/i18n/index.js +96 -44
- package/dist/index.js +1 -1
- package/dist/llm/index.js +39 -23
- package/dist/llm/provider.js +23 -0
- package/dist/llm/providers/anthropic.js +12 -18
- package/dist/llm/tool-schema.js +48 -0
- package/dist/mcp/client.js +70 -25
- package/dist/mcp/config.js +1 -3
- package/dist/mcp/index.js +3 -1
- package/dist/memory/reflect.js +3 -6
- package/dist/memory/store.js +10 -12
- package/dist/permissions/index.js +66 -22
- package/dist/pet/bridge.js +2 -2
- package/dist/repl/commands/appearance.js +72 -0
- package/dist/repl/commands/compact.js +114 -0
- package/dist/repl/commands/context.js +17 -0
- package/dist/repl/commands/image.js +71 -0
- package/dist/repl/commands/memory.js +51 -0
- package/dist/repl/commands/mode.js +47 -0
- package/dist/repl/commands/model.js +435 -0
- package/dist/repl/commands/pet.js +70 -0
- package/dist/repl/commands/registry.js +46 -0
- package/dist/repl/commands/session.js +102 -0
- package/dist/repl/commands/skill.js +95 -0
- package/dist/repl/commands/system.js +202 -0
- package/dist/repl/commands/tool-group.js +233 -0
- package/dist/repl/commands/types.js +4 -0
- package/dist/repl/commands.js +324 -0
- package/dist/repl/index.js +5 -2577
- package/dist/repl/message-format.js +151 -0
- package/dist/repl/running-input.js +147 -0
- package/dist/repl/runtime.js +816 -0
- package/dist/repl/status-bar.js +161 -0
- package/dist/rollback/index.js +44 -14
- package/dist/runtime/browser-manager.js +1 -1
- package/dist/runtime/dev-server-manager.js +1 -1
- package/dist/runtime/input-injector.js +313 -0
- package/dist/runtime/screen-capture.js +89 -0
- package/dist/runtime/screen-pipeline.js +223 -0
- package/dist/sandbox/index.js +1 -1
- package/dist/sandbox/policy.js +8 -2
- package/dist/session/compact.js +22 -34
- package/dist/session/index.js +2 -2
- package/dist/session/notes.js +39 -5
- package/dist/session/persist.js +14 -8
- package/dist/session/scheduler.js +3 -4
- package/dist/session/trace-metrics.js +6 -3
- package/dist/skills/activation.js +10 -5
- package/dist/skills/discover.js +5 -17
- package/dist/skills/index.js +1 -1
- package/dist/skills/runner.js +28 -44
- package/dist/skills/toolmap.js +5 -2
- package/dist/tools/builtins/browser.js +17 -3
- package/dist/tools/builtins/computer.js +322 -0
- package/dist/tools/builtins/dev-server.js +1 -3
- package/dist/tools/builtins/edit-file.js +21 -7
- package/dist/tools/builtins/grep.js +1 -1
- package/dist/tools/builtins/index.js +28 -25
- package/dist/tools/builtins/memory-forget.js +1 -1
- package/dist/tools/builtins/memory-graph.js +4 -7
- package/dist/tools/builtins/memory-list.js +2 -6
- package/dist/tools/builtins/memory-save.js +8 -2
- package/dist/tools/builtins/memory-search.js +4 -4
- package/dist/tools/builtins/note-append.js +14 -3
- package/dist/tools/builtins/plan-update.js +40 -9
- package/dist/tools/builtins/run-command.js +8 -8
- package/dist/tools/builtins/screenshot.js +11 -87
- package/dist/tools/builtins/task.js +29 -40
- package/dist/tools/builtins/use-skill.js +1 -1
- package/dist/tools/builtins/view-image.js +5 -3
- package/dist/tools/builtins/web-fetch.js +1 -3
- package/dist/tools/builtins/web-search.js +3 -7
- package/dist/tools/builtins/write-file.js +4 -2
- package/dist/tools/constants.js +50 -37
- package/dist/tools/policy.js +221 -0
- package/dist/tools/registry.js +40 -19
- package/dist/tools/resource-lock.js +4 -2
- package/dist/tools/router.js +143 -0
- package/dist/tools/validation.js +6 -5
- package/dist/ui/batch.js +63 -23
- package/dist/ui/clipboard.js +17 -5
- package/dist/ui/content.js +166 -13
- package/dist/ui/diff.js +61 -19
- package/dist/ui/fuzzy.js +35 -2
- package/dist/ui/intervention.js +2 -4
- package/dist/ui/layout-internal/content-write.js +587 -0
- package/dist/ui/layout-internal/core.js +590 -0
- package/dist/ui/layout-internal/input-paint.js +365 -0
- package/dist/ui/layout-internal/screen.js +128 -0
- package/dist/ui/layout-internal/scroll.js +152 -0
- package/dist/ui/layout-internal/selection.js +229 -0
- package/dist/ui/layout-internal/state.js +82 -0
- package/dist/ui/layout-internal/statusbar.js +351 -0
- package/dist/ui/layout-types.js +1 -0
- package/dist/ui/layout.js +3 -2288
- package/dist/ui/markdown.js +10 -2
- package/dist/ui/prompt-internal/editor.js +945 -0
- package/dist/ui/prompt-internal/paste.js +52 -0
- package/dist/ui/prompt-internal/pickers.js +519 -0
- package/dist/ui/prompt-internal/types.js +1 -0
- package/dist/ui/prompt.js +4 -1506
- package/dist/ui/render.js +87 -2
- package/dist/verification/affected.js +11 -5
- package/dist/verification/discovery.js +1 -3
- package/dist/verification/profile.js +11 -3
- package/package.json +11 -2
|
@@ -0,0 +1,590 @@
|
|
|
1
|
+
// 顶层编排 + 桶文件。
|
|
2
|
+
//
|
|
3
|
+
// 显式 re-export 各分层模块的公开 API,对外仍是 ../layout.js 的 `export *`(名单一字不变)。
|
|
4
|
+
// 这里不用 `export *`:那会把「为跨文件引用而提升 export 的内部实现」一并公开
|
|
5
|
+
// (cup / esc / normalizeSelection / SEL_OPEN 等),污染对外 API。
|
|
6
|
+
//
|
|
7
|
+
// 本文件只留「必须站在所有子系统之上」的部分:
|
|
8
|
+
// - alt screen 生命周期:enterAltScreen / exitAltScreen
|
|
9
|
+
// - 模式切换:enterInputMode / enterRunningMode / setUserActive
|
|
10
|
+
// - 鼠标事件入口:handleMouseEvent 及 handler 注册——最大消费者,
|
|
11
|
+
// 跨 content-write / scroll / input-paint / selection 四层,只能待在顶层
|
|
12
|
+
// - 裸 console 劫持:routeConsoleToContent → contentWrite
|
|
13
|
+
//
|
|
14
|
+
// 分层依赖(严格 DAG,零循环 import):
|
|
15
|
+
// screen → selection / statusbar → input-paint → scroll → content-write → 本文件
|
|
16
|
+
// 可变运行态全部在 ./state.ts(单一共享实例,各层 import 同一份)。
|
|
17
|
+
import { stdin, stdout } from 'node:process';
|
|
18
|
+
import { inspect } from 'node:util';
|
|
19
|
+
import { ui, resetTerminalBackground } from '../theme.js';
|
|
20
|
+
import * as content from '../content.js';
|
|
21
|
+
import * as mouse from '../mouse.js';
|
|
22
|
+
import { setMaxCols } from '../batch.js';
|
|
23
|
+
import { copyToClipboard } from '../clipboard.js';
|
|
24
|
+
import { t } from '../../i18n/index.js';
|
|
25
|
+
import { state } from './state.js';
|
|
26
|
+
import { esc, cup, getGeo, setRegion, runningCaretPos, contentMode } from './screen.js';
|
|
27
|
+
import { normalizeSelection, normalizeInputSelection, extractSelectionText, extractInputSelectionText, isInputRow, pasteIntoInput, inputScreenToInputPos, } from './selection.js';
|
|
28
|
+
import { composePlanLines, drawStatusBar, startTurnTimer, stopTurnTimer } from './statusbar.js';
|
|
29
|
+
import { paintInput, repaint } from './input-paint.js';
|
|
30
|
+
import { WHEEL_LINES, screenRowToAbsLine, repaintViewport, scrollBy, scrollWheel, resetScroll, lockScrollToBottom, } from './scroll.js';
|
|
31
|
+
import { contentWrite, contentInsertAfter, contentDeleteFrom, contentReplaceLine, reflowContentForResize, } from './content-write.js';
|
|
32
|
+
export { isTuiActive, getGeo, setRegion, contentMode, isStreamingPaused } from './screen.js';
|
|
33
|
+
export { drawStatusBar, setStatus, stopTurnTimer, startTurnTimerIfRunning, paintLiveAtCursor, clearLiveAtCursor, setLiveUsage, setStatusBase, } from './statusbar.js';
|
|
34
|
+
export { paintInput, paintRunningInput, repaint } from './input-paint.js';
|
|
35
|
+
export { isScrolled, repaintViewport, scrollBy, scrollWheel, resetScroll, lockScrollToBottom, unlockScroll, isScrollLocked, } from './scroll.js';
|
|
36
|
+
export { contentWrite, writeBanner, rewriteBanner, bannerHeight, contentWriteMd, contentWriteMdOnce, clearContent, rewindContent, writeWelcomeBlock, dismissWelcomeBlock, contentInsertAfter, contentDeleteFrom, totalRows, isLastContentRowBlank, normalizeMutationBoundary, normalizeInputBoundary, contentReplaceLine, notifyContentReset, } from './content-write.js';
|
|
37
|
+
// ── 裸 console 防御 ──
|
|
38
|
+
// 第三方库(如 openai SDK)可能用 console.log 直写 stdout,在 RUNNING 态会落到光标所在的底栏
|
|
39
|
+
// 输入框、污染输入。进入 TUI 后把 console.* 劫持到 contentWrite,统一进内容区(运行态下
|
|
40
|
+
// contentWrite 末尾会把真光标归位输入框),既不再泄漏到输入框,也不会破坏 TUI 布局。
|
|
41
|
+
// TUI 外(active=false)不劫持、恢复原始 console,保证 host 子进程 JSON 协议 / 退出日志正常。
|
|
42
|
+
// 开关标志 consoleHookInstalled 见 state.ts。
|
|
43
|
+
const origConsole = {
|
|
44
|
+
log: console.log,
|
|
45
|
+
error: console.error,
|
|
46
|
+
warn: console.warn,
|
|
47
|
+
info: console.info,
|
|
48
|
+
};
|
|
49
|
+
function routeConsoleToContent(method) {
|
|
50
|
+
const c = console;
|
|
51
|
+
c[method] = (...args) => {
|
|
52
|
+
if (state.active && ui.isTTY) {
|
|
53
|
+
let s;
|
|
54
|
+
try {
|
|
55
|
+
s = args.map((a) => (typeof a === 'string' ? a : inspect(a, { depth: 4 }))).join(' ');
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
s = String(args[0]);
|
|
59
|
+
}
|
|
60
|
+
if (!s.endsWith('\n'))
|
|
61
|
+
s += '\n';
|
|
62
|
+
contentWrite(s);
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
origConsole[method](...args);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function installConsoleGuard() {
|
|
70
|
+
if (state.consoleHookInstalled)
|
|
71
|
+
return;
|
|
72
|
+
routeConsoleToContent('log');
|
|
73
|
+
routeConsoleToContent('error');
|
|
74
|
+
routeConsoleToContent('warn');
|
|
75
|
+
routeConsoleToContent('info');
|
|
76
|
+
state.consoleHookInstalled = true;
|
|
77
|
+
}
|
|
78
|
+
function uninstallConsoleGuard() {
|
|
79
|
+
if (!state.consoleHookInstalled)
|
|
80
|
+
return;
|
|
81
|
+
console.log = origConsole.log;
|
|
82
|
+
console.error = origConsole.error;
|
|
83
|
+
console.warn = origConsole.warn;
|
|
84
|
+
console.info = origConsole.info;
|
|
85
|
+
state.consoleHookInstalled = false;
|
|
86
|
+
}
|
|
87
|
+
const USER_ACTIVE_PAUSE_MS = 1500;
|
|
88
|
+
/**
|
|
89
|
+
* 运行态用户打字时调(repl onRunningKey 每次按键):标记活跃 + 重置 flush 定时器。
|
|
90
|
+
* 活跃期间流式只喂缓冲不物理写(光标不入内容区);用户停手 USER_ACTIVE_PAUSE_MS 后 flush:
|
|
91
|
+
* repaintViewport 重画缓冲内容 + drawStatusBar 刷状态行 + 光标归输入框。
|
|
92
|
+
* 解决 IME 候选窗跟流式输出跑:流式写必须 cup 到 contentRow,IME 逐光标跟踪→气泡跟跑;打字时暂停写则光标不动。
|
|
93
|
+
*/
|
|
94
|
+
export function setUserActive() {
|
|
95
|
+
if (!state.active || state.mode !== 'running')
|
|
96
|
+
return;
|
|
97
|
+
state.userActiveUntil = Date.now() + USER_ACTIVE_PAUSE_MS;
|
|
98
|
+
if (state.flushTimer)
|
|
99
|
+
clearTimeout(state.flushTimer);
|
|
100
|
+
state.flushTimer = setTimeout(() => {
|
|
101
|
+
state.flushTimer = null;
|
|
102
|
+
state.userActiveUntil = 0;
|
|
103
|
+
if (state.active && state.mode === 'running' && state.scrollOffset === 0) {
|
|
104
|
+
repaintViewport(); // 重画内容区(显示活跃期间缓冲的流式内容)
|
|
105
|
+
drawStatusBar(); // 刷状态行(活跃期间冻住,现恢复)
|
|
106
|
+
const p = runningCaretPos();
|
|
107
|
+
stdout.write(cup(p.row, p.col));
|
|
108
|
+
}
|
|
109
|
+
}, USER_ACTIVE_PAUSE_MS);
|
|
110
|
+
state.flushTimer.unref();
|
|
111
|
+
}
|
|
112
|
+
/** 清活跃选区(不复制),若原有选区则重画去掉反白。供 ESC / 点击别处 / 退出滚动态等场景调。 */
|
|
113
|
+
export function clearSelection() {
|
|
114
|
+
if (!state.selection && !state.inputSelection)
|
|
115
|
+
return;
|
|
116
|
+
state.selection = null;
|
|
117
|
+
if (state.inputSelection) {
|
|
118
|
+
state.inputSelection = null;
|
|
119
|
+
if (state.active && state.base && state.lastView)
|
|
120
|
+
paintInput(state.lastView);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (state.scrollOffset >= 0)
|
|
124
|
+
repaintViewport();
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* 注册"点击输入框粘贴"回调:当前持有文本输入的模块(prompt.ts 的 promptWithSlashMenu / repl 运行态
|
|
128
|
+
* typeahead)在挂键盘监听时调,退出时传 null 注销。点击输入行(未拖动)时 layout 读剪贴板后回调此函数,
|
|
129
|
+
* 由回调方完成插入(各输入状态的行/光标结构不同,layout 不掺和文本编辑逻辑,只管"贴入"这个动作触发)。
|
|
130
|
+
*/
|
|
131
|
+
export function setPasteHandler(fn) {
|
|
132
|
+
state.pasteHandler = fn;
|
|
133
|
+
}
|
|
134
|
+
/** 注册"点击输入框 → 改光标"回调:prompt.ts 等文本所有者挂此回调后,
|
|
135
|
+
* 鼠标左键落输入行算出的新 (line, col) 会在 prompt 下次 redraw 前喂入此回调
|
|
136
|
+
* (prompt 用它写自己的 cl / cc,真正改 source of truth)。
|
|
137
|
+
* 传 null 注销(在 cleanup / 退出输入态时调,防下个 prompt 实例被旧回调污染)。 */
|
|
138
|
+
export function setCursorChangeHandler(fn) {
|
|
139
|
+
state.cursorChangeHandler = fn;
|
|
140
|
+
}
|
|
141
|
+
export function setOverlayMouseHandler(fn) {
|
|
142
|
+
state.overlayMouseHandler = fn;
|
|
143
|
+
}
|
|
144
|
+
/** picker / 介入面板期间禁用鼠标选区与拖拽(避免 viewport 重画覆盖菜单);滚轮仍可用。面板退出后恢复。 */
|
|
145
|
+
export function setMouseEnabled(v) {
|
|
146
|
+
state.mouseEnabled = v;
|
|
147
|
+
if (!v) {
|
|
148
|
+
state.selecting = false;
|
|
149
|
+
if (state.selection) {
|
|
150
|
+
state.selection = null;
|
|
151
|
+
repaintViewport();
|
|
152
|
+
}
|
|
153
|
+
if (state.inputSelection) {
|
|
154
|
+
state.inputSelection = null;
|
|
155
|
+
if (state.active && state.base && state.lastView)
|
|
156
|
+
paintInput(state.lastView);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
/** 鼠标左键落输入行:进输入态(若是 running)→ 用统一的 display_col 单位算出"目标光标位"
|
|
161
|
+
* → paintInput 用新光标重画(末步 cup 把终端真光标移到点击位,默认即竖线/闪烁块),
|
|
162
|
+
* 并同步调 cursorChangeHandler 让 prompt.ts 改自己的 cl/cc(source of truth),下次按键按新位写字。
|
|
163
|
+
*
|
|
164
|
+
* 单位说明:
|
|
165
|
+
* - paintInput 的 view.cursorCol 单位 = display_col(同 paintInput 的 dispCursorCol,见 prompt.ts:186)
|
|
166
|
+
* - prompt 的 cc 单位 = char_idx(字符索引)
|
|
167
|
+
* 本函数同时给出 targetLine、targetDisplayCol(paintInput 用)、targetCharCol(prompt 用),
|
|
168
|
+
* 故 visitColToCharCol 那一套反推只是用于把 click 屏幕列换成字符偏移,与 paintInput 的视协议完全对齐。
|
|
169
|
+
*
|
|
170
|
+
* 点击行为:
|
|
171
|
+
* - 屏幕 visRow/visCol 用 charWidth 计列(中文/Emoji 全角占 2);落在宽字符中段归到字符左边界。
|
|
172
|
+
* - 累加 flat[0..flatIdx-1] 的 display_w + 字符长度 → targetDisplayCol/targetCharCol,与 paintInput 渲染一致。
|
|
173
|
+
* - 段接缝点击因 paintInput 协议限制(<= 还是 <),会落到前段末;这是 paintInput 全局行为,不归本函数。
|
|
174
|
+
* - 未注册 handler(非输入态)→ 仅视觉真光标可见;cl/cc 不变。 */
|
|
175
|
+
function setInputCursorFromClick(screenRow, screenCol) {
|
|
176
|
+
if (!state.active || !state.base || !state.lastView)
|
|
177
|
+
return;
|
|
178
|
+
// running 态:先收归输入态(enterInputMode 会 paintInput 整帧重画;之后我们再按新光标重画一次覆盖即可)
|
|
179
|
+
if (state.mode !== 'input') {
|
|
180
|
+
enterInputMode(state.statusText);
|
|
181
|
+
}
|
|
182
|
+
const pos = inputScreenToInputPos(screenRow, screenCol);
|
|
183
|
+
if (!pos) {
|
|
184
|
+
paintInput(state.lastView);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
// 三步原子:
|
|
188
|
+
// 1) lastView.cursorLine = targetLine — view.cursorLine 单位 = lines 行号 in dispLines(供 paintInput)
|
|
189
|
+
// 2) lastView.cursorCol = targetDisplayCol — view.cursorCol 单位 = display_col(供 paintInput,
|
|
190
|
+
// paintInput 内部 wrap/flat 后用 cursorCol 算 visLine + cursorVisCol)
|
|
191
|
+
// 3) paintInput 重画——末步 cup 把真光标移到点击位的可视位置
|
|
192
|
+
// 4) cursorChangeHandler(dispatchFlatIdx, dispatchInSegVis) — 给 prompt 的"屏 tap 原坐标",
|
|
193
|
+
// prompt 自己重做 flat 算 cl/cc(扣 chip prefix,与 layout 端的 stamp 时 dispLines 一致即可)。
|
|
194
|
+
// 未注册 handler(非输入态)→ 仅步骤 3 可见真光标移动;步骤 4 不动 prompt 的 cl/cc。
|
|
195
|
+
state.lastView = { ...state.lastView, cursorLine: pos.line, cursorCol: pos.displayCol };
|
|
196
|
+
paintInput(state.lastView);
|
|
197
|
+
if (state.cursorChangeHandler)
|
|
198
|
+
state.cursorChangeHandler(pos.flatIdx, pos.inSegVis);
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* 鼠标事件分发(mouse.setHandler 注册)。
|
|
202
|
+
* - 左键(button 0):内容区按下开选区、拖动扩展(触边自动翻页)、释放只更新高亮**不复制**;
|
|
203
|
+
* 纯点击(未拖动)清空旧选区(点别处的常见预期);落在输入行 → 自动进入输入态 + 光标定位到点击位
|
|
204
|
+
* (沿用 windowInputVis 的软折规则做逆解,支持多行 / 中文宽字符;右键 release 落输入行仍贴入)。
|
|
205
|
+
* - 右键(button 2,单击 = press→release 未拖动):落在输入行 → 读剪贴板贴入(仿常见终端"右键粘贴");
|
|
206
|
+
* 落在内容区 → 复制当前选区(若有)到剪贴板后立即清空高亮(视觉确认"已复制",不留旧选区误导),
|
|
207
|
+
* 静默不弹提示;都无对应状态则 no-op。
|
|
208
|
+
* - 滚轮(button&64,由 mouse.ts 解析为 wheel 事件):照常 scrollBy。
|
|
209
|
+
* 选区坐标存绝对缓冲行(viewportAbsStart + 屏行),故翻页 / 追加新内容期间选区锚点仍指向同段文字。
|
|
210
|
+
*/
|
|
211
|
+
function handleMouseEvent(e) {
|
|
212
|
+
if (!state.active)
|
|
213
|
+
return;
|
|
214
|
+
if (state.overlayMouseHandler && state.overlayMouseHandler(e))
|
|
215
|
+
return; // overlay(composer 等)先接管,消费即止
|
|
216
|
+
if (e.type === 'wheel') {
|
|
217
|
+
// 滚轮始终可用:面板/picker 期间也允许上下查看 agent 输出,
|
|
218
|
+
// 与 onRunningKey 的 PgUp/PgDn 行为一致;mouseEnabled 仅管选区/拖拽。
|
|
219
|
+
scrollWheel(e.dir);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
// 输入行左键点击:即使 mouseEnabled=false(如 intervention 面板期间)也允许定位光标,
|
|
223
|
+
// 提升用户体验(点哪插哪)。拖拽/右键/内容区点击仍受 mouseEnabled 限制。
|
|
224
|
+
if (e.button === 0 && e.type === 'press' && isInputRow(e.row)) {
|
|
225
|
+
setInputCursorFromClick(e.row, e.col);
|
|
226
|
+
const pos = inputScreenToInputPos(e.row, e.col);
|
|
227
|
+
if (pos) {
|
|
228
|
+
if (state.selection) {
|
|
229
|
+
state.selection = null;
|
|
230
|
+
}
|
|
231
|
+
state.inputSelection = {
|
|
232
|
+
anchor: { line: pos.line, col: pos.displayCol },
|
|
233
|
+
end: { line: pos.line, col: pos.displayCol },
|
|
234
|
+
dragged: false,
|
|
235
|
+
};
|
|
236
|
+
paintInput(state.lastView);
|
|
237
|
+
}
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
if (!state.mouseEnabled)
|
|
241
|
+
return;
|
|
242
|
+
const g = getGeo();
|
|
243
|
+
const col = Math.max(0, e.col - 1); // SGR 报表列 1-based → 显示列 0-based
|
|
244
|
+
// 右键释放:落输入行 → 优先复制当前输入框选区(若有),退化为贴入;落内容区 → 复制选区后清高亮。
|
|
245
|
+
if (e.type === 'release' && e.button === 2) {
|
|
246
|
+
if (isInputRow(e.row)) {
|
|
247
|
+
// 输入框选区存在且真拖动过:复制(不动 text 光标,不改 cl/cc),与内容区"copy 后清高亮"对齐
|
|
248
|
+
const inpSel = normalizeInputSelection();
|
|
249
|
+
if (inpSel) {
|
|
250
|
+
const text = extractInputSelectionText(inpSel);
|
|
251
|
+
state.inputSelection = null;
|
|
252
|
+
if (state.lastView)
|
|
253
|
+
paintInput(state.lastView); // 立刻擦反白,视觉反馈
|
|
254
|
+
if (text)
|
|
255
|
+
copyToClipboard(text);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
pasteIntoInput();
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
const sel = normalizeSelection();
|
|
262
|
+
if (!sel)
|
|
263
|
+
return;
|
|
264
|
+
const text = extractSelectionText(sel);
|
|
265
|
+
state.selection = null;
|
|
266
|
+
repaintViewport(); // 复制后清高亮:视觉反馈"已复制",不留旧选区误导
|
|
267
|
+
if (!text)
|
|
268
|
+
return;
|
|
269
|
+
copyToClipboard(text);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
if (e.button !== 0)
|
|
273
|
+
return; // 其余中/右键 press/drag 不处理(终端原生右键菜单等不受影响)
|
|
274
|
+
if (e.type === 'press') {
|
|
275
|
+
// 输入行左键已在上方 mouseEnabled 检查之前处理(支持 intervention 面板期间点击定位光标)。
|
|
276
|
+
// 此处只处理内容区选区。
|
|
277
|
+
state.selecting = true;
|
|
278
|
+
const rowInContent = Math.max(1, Math.min(e.row, g.contentBottom));
|
|
279
|
+
const absLine = screenRowToAbsLine(rowInContent);
|
|
280
|
+
state.selection = { anchorLine: absLine, anchorCol: col, endLine: absLine, endCol: col, dragged: false };
|
|
281
|
+
// 切内容区选区时清掉输入框旧选区(若有)。
|
|
282
|
+
state.inputSelection = null;
|
|
283
|
+
repaintViewport();
|
|
284
|
+
repaint(); // 补一次输入区重画:INPUT 态 repaintViewport 把真光标留在内容区续写位,须靠 repaint 把它带回输入框(否则点内容区会现假闪烁光标,见 issue)
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
if (e.type === 'drag') {
|
|
288
|
+
if (state.inputSelection) {
|
|
289
|
+
// 输入框拖动选区:更新 end,触边不动 scroll(输入框行数有限,无外滚概念);重画底栏。
|
|
290
|
+
const pos = inputScreenToInputPos(e.row, e.col);
|
|
291
|
+
if (pos) {
|
|
292
|
+
if (pos.line !== state.inputSelection.end.line || pos.displayCol !== state.inputSelection.end.col) {
|
|
293
|
+
state.inputSelection.dragged = true;
|
|
294
|
+
state.inputSelection.end = { line: pos.line, col: pos.displayCol };
|
|
295
|
+
}
|
|
296
|
+
paintInput(state.lastView);
|
|
297
|
+
}
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
if (!state.selecting || !state.selection)
|
|
301
|
+
return;
|
|
302
|
+
// 触边自动翻页:motion 事件持续到达时靠此逐步滚动,把选区扩展到滚出去的历史行。
|
|
303
|
+
if (e.row <= 1)
|
|
304
|
+
scrollBy(WHEEL_LINES);
|
|
305
|
+
else if (e.row >= g.contentBottom)
|
|
306
|
+
scrollBy(-WHEEL_LINES);
|
|
307
|
+
const rowInContent = Math.max(1, Math.min(e.row, g.contentBottom));
|
|
308
|
+
const absLine = screenRowToAbsLine(rowInContent);
|
|
309
|
+
if (absLine !== state.selection.endLine || col !== state.selection.endCol)
|
|
310
|
+
state.selection.dragged = true;
|
|
311
|
+
state.selection.endLine = absLine;
|
|
312
|
+
state.selection.endCol = col;
|
|
313
|
+
repaintViewport();
|
|
314
|
+
repaint(); // 同上:把真光标带回输入框,防拖动选区期间光标停留内容区闪烁
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
// release(左键)
|
|
318
|
+
state.selecting = false;
|
|
319
|
+
// 输入框 release 优先:未拖动 → 清选区(光标已定好);拖动过 → 留高亮等右键复制
|
|
320
|
+
if (state.inputSelection) {
|
|
321
|
+
if (!state.inputSelection.dragged) {
|
|
322
|
+
state.inputSelection = null;
|
|
323
|
+
if (state.lastView)
|
|
324
|
+
paintInput(state.lastView);
|
|
325
|
+
}
|
|
326
|
+
// 拖动过:不操作,留高亮
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
if (!state.selection)
|
|
330
|
+
return;
|
|
331
|
+
if (!state.selection.dragged) {
|
|
332
|
+
// 内容区纯点击(未拖动):若落在工具 batch 摘要行上 → 切换展开/折叠;
|
|
333
|
+
// 否则原行为:清选区。batch 反查通过 content lineAt + dynamic import(避免 layout↔batch 循环依赖)。
|
|
334
|
+
const absClick = state.selection.anchorLine; // 起止同行同列,取任一;未拖动时 line = anchor = end
|
|
335
|
+
void (async () => {
|
|
336
|
+
try {
|
|
337
|
+
const m = await import('../batch.js');
|
|
338
|
+
const entry = m.findEntryByAbsLine(absClick);
|
|
339
|
+
if (entry) {
|
|
340
|
+
// 先清选区再改 buffer:contentInsert/Delete 内会立即 repaintViewport;若此时仍保留
|
|
341
|
+
// 旧绝对行选区,会短暂画出一帧错位高亮,随后二次重画,视觉上就是抖动。
|
|
342
|
+
state.selection = null;
|
|
343
|
+
m.toggleEntry(entry.batchId, entry.entryIndex, {
|
|
344
|
+
// 鼠标点击属「回看式展开」:显式 keepViewport=true,视口锚定原位、详情在下方展开,
|
|
345
|
+
// 屏幕不跳。显式传参而非依赖 contentInsertAfter 的默认 true,避免将来内部改传
|
|
346
|
+
// false(跟随屏底)时被 adapter 静默吞掉。
|
|
347
|
+
contentInsertAfter: (after, lines) => contentInsertAfter(after, lines, true),
|
|
348
|
+
contentDeleteFrom: (start, n) => contentDeleteFrom(start, n),
|
|
349
|
+
contentReplaceLine: (absIdx, line) => contentReplaceLine(absIdx, line),
|
|
350
|
+
});
|
|
351
|
+
repaint();
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
const id = m.findBatchByAbsLine(absClick);
|
|
355
|
+
if (id) {
|
|
356
|
+
state.selection = null;
|
|
357
|
+
m.toggleBatch(id, {
|
|
358
|
+
// 同 toggleEntry:鼠标点击是回看式展开,显式 keepViewport=true 锚定视口。
|
|
359
|
+
contentInsertAfter: (after, lines) => contentInsertAfter(after, lines, true),
|
|
360
|
+
contentDeleteFrom: (start, n) => contentDeleteFrom(start, n),
|
|
361
|
+
});
|
|
362
|
+
repaint();
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
catch {
|
|
367
|
+
// batch 不可用(非 TTY 等)→ 走默认清选区路径
|
|
368
|
+
}
|
|
369
|
+
state.selection = null;
|
|
370
|
+
repaintViewport();
|
|
371
|
+
repaint();
|
|
372
|
+
})();
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
// 拖动过:保留高亮选区供右键复制(不在此处复制,复制交给右键释放分支)。
|
|
376
|
+
repaint(); // 同上:把真光标带回输入框
|
|
377
|
+
}
|
|
378
|
+
/** 进入输入态:画空输入框 + 状态行,光标入输入框。 */
|
|
379
|
+
export function enterInputMode(status = t('repl.idle')) {
|
|
380
|
+
state.mode = 'input';
|
|
381
|
+
state.statusText = status;
|
|
382
|
+
state.spinnerFrame = undefined;
|
|
383
|
+
state.runningFrame = -1; // 回 INPUT 态:停状态行 chip 旋转,composeStatus 退回静态 ●
|
|
384
|
+
state.turnStart = null; // 停走时
|
|
385
|
+
stopTurnTimer();
|
|
386
|
+
state.scrollLockUntil = 0; // 轮末:清轮首滚动锁,INPUT 态可自由滚动
|
|
387
|
+
state.frameRow = 0; // 轮末:清 spinner 帧位置(防下轮残留)
|
|
388
|
+
state.frameCol = 0;
|
|
389
|
+
// 运行态若有未 flush 的缓冲内容(用户打字暂停了流式写),切回 INPUT 前重画内容区显示之,免丢内容
|
|
390
|
+
if (state.flushTimer) {
|
|
391
|
+
clearTimeout(state.flushTimer);
|
|
392
|
+
state.flushTimer = null;
|
|
393
|
+
}
|
|
394
|
+
if (state.userActiveUntil) {
|
|
395
|
+
state.userActiveUntil = 0;
|
|
396
|
+
if (state.active)
|
|
397
|
+
repaintViewport();
|
|
398
|
+
}
|
|
399
|
+
if (state.active && state.base) {
|
|
400
|
+
// 先按当前 base.planSummary 重算 planRows(可能从上次会话残留 stale 值),再据此 setRegion
|
|
401
|
+
// 撑出正确脚栏高;否则 plan 撑 2 行时 setRegion(6) 会把 spinner 挤到 plan 第 2 行位置。
|
|
402
|
+
composePlanLines(state.base, getGeo().cols);
|
|
403
|
+
setRegion(4 + state.planRows + 1); // 1 虚拟空 + 1 spinner行 + 1 上线 + 1 输入 + 1 下线 + 1 model行 + plan 多出的行
|
|
404
|
+
paintInput({
|
|
405
|
+
prompt: '❯ ',
|
|
406
|
+
lines: [''],
|
|
407
|
+
cursorLine: 0,
|
|
408
|
+
cursorCol: 0,
|
|
409
|
+
menu: null,
|
|
410
|
+
});
|
|
411
|
+
stdout.write(esc.cursorShow); // 回 INPUT 态:显真光标(运行态藏了)
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
/** 进入运行态:底栏输入行与空闲态同色(非 dim)、可任意位置编辑,cursor 留输入框。footerH 恒 6(虚拟空+spinner行+上线+输入+下线+model行)。新轮回尾(确保新内容可见)。 */
|
|
415
|
+
export function enterRunningMode(status, placeholder) {
|
|
416
|
+
state.mode = 'running';
|
|
417
|
+
state.statusText = status;
|
|
418
|
+
state.spinnerFrame = undefined;
|
|
419
|
+
state.turnStart = Date.now(); // 起走时(整轮从发起到 enterInputMode 止)
|
|
420
|
+
resetScroll(); // 若上轮 INPUT 滚动过(未打字回底),新轮回尾
|
|
421
|
+
lockScrollToBottom(); // 轮首短时锁:吸收发消息前后残留滚轮事件,保 agent 输出从底部开始(锁过期或轮末 enterInputMode 解)
|
|
422
|
+
if (state.active && state.base) {
|
|
423
|
+
// 先按当前 base.planSummary 重算 planRows(可能从上次会话残留 stale 值),再据此 setRegion
|
|
424
|
+
// 撑出正确脚栏高;否则 plan 撑 2 行时 setRegion(6) 会把 spinner 挤到 plan 第 2 行位置。
|
|
425
|
+
composePlanLines(state.base, getGeo().cols);
|
|
426
|
+
setRegion(4 + state.planRows + 1); // 1 虚拟空 + 1 spinner行 + 1 上线 + 1 输入 + 1 下线 + 1 model行 + plan 多出的行
|
|
427
|
+
paintInput({
|
|
428
|
+
prompt: '❯ ',
|
|
429
|
+
lines: [''],
|
|
430
|
+
placeholder,
|
|
431
|
+
cursorLine: 0,
|
|
432
|
+
cursorCol: 0,
|
|
433
|
+
menu: null,
|
|
434
|
+
});
|
|
435
|
+
startTurnTimer(); // 续刷状态行走时(流式期间 spinner 停转,由它兜底)
|
|
436
|
+
contentMode();
|
|
437
|
+
// 真光标可见,停在输入框光标位(供 IME 锚定气泡到光标处)。打字时 setUserActive 暂停流式写、
|
|
438
|
+
// 光标稳定在输入框;流式写时瞬时到 contentRow 但合并写入很快回输入框(光标跟流式是终端常态)。
|
|
439
|
+
// 不再藏光标:藏了 IME 气泡会跑到输入框最右边而非光标处(WT 不锚定隐藏光标)。
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
// ── alt screen 生命周期 + 钩子 ──
|
|
443
|
+
export function enterAltScreen() {
|
|
444
|
+
if (state.active || !ui.isTTY)
|
|
445
|
+
return;
|
|
446
|
+
state.active = true;
|
|
447
|
+
state.lastTerminalCols = getGeo().cols;
|
|
448
|
+
setMaxCols(state.lastTerminalCols); // 同步 batch 展开行宽钳制,防超宽行 auto-wrap 打乱屏位
|
|
449
|
+
stdout.write(esc.altOn);
|
|
450
|
+
// 不动终端窗口背景:底色保持用户终端原色,主题只作用于我们画出的 SGR 颜色。
|
|
451
|
+
stdout.write(esc.mouseOn); // 完整鼠标追踪(按下/拖动/释放/滚轮)→ mouse.swallow 重组 → handleMouseEvent
|
|
452
|
+
mouse.setHandler(handleMouseEvent);
|
|
453
|
+
// 进入 alt screen 前 base 可能已设了 planSummary;按当前 planSummary 重算 planRows,
|
|
454
|
+
// 让首次 setRegion 撑出正确脚栏高(否则 plan 撑 2 行时会被 spinner 行覆盖)。
|
|
455
|
+
if (state.base)
|
|
456
|
+
composePlanLines(state.base, getGeo().cols);
|
|
457
|
+
setRegion(4 + state.planRows + 1); // 1 虚拟空 + 1 spinner行 + 1 上线 + 1 输入 + 1 下线 + 1 model行 + plan 多出的行
|
|
458
|
+
state.contentRow = 1;
|
|
459
|
+
state.contentCol = 1;
|
|
460
|
+
state.segmentStartRow = 1;
|
|
461
|
+
state.scrollOffset = 0;
|
|
462
|
+
state.scrollLockUntil = 0;
|
|
463
|
+
state.mdActive = false;
|
|
464
|
+
state.mdBuf = '';
|
|
465
|
+
state.selection = null;
|
|
466
|
+
state.selecting = false;
|
|
467
|
+
content.reset();
|
|
468
|
+
state.exitHandler = () => exitAltScreen();
|
|
469
|
+
process.on('exit', state.exitHandler);
|
|
470
|
+
state.sigwinchHandler = () => {
|
|
471
|
+
if (!state.active)
|
|
472
|
+
return;
|
|
473
|
+
// 立即(同步)更新行号 + 重设 DECSTBM 区域:不 debounce,否则快速拖动边框时
|
|
474
|
+
// contentRow 停在旧值、区域未更新,spinner/contentWrite 画到旧行号(「思考中在消息堆里」根因)。
|
|
475
|
+
// 重画 repaintViewport 防抖(下面 timer),避免连续拖动闪烁;但行号/区域必须立即正确。
|
|
476
|
+
const g = getGeo(state.footerH);
|
|
477
|
+
setMaxCols(g.cols); // 列宽变 → 展开行钳宽上限同步(后续新展开行生效)
|
|
478
|
+
const total = content.totalRows();
|
|
479
|
+
const committed = content.committedRows();
|
|
480
|
+
// 缩小:contentRow > 新 bottom → 钳到新 bottom
|
|
481
|
+
if (state.contentRow > g.contentBottom)
|
|
482
|
+
state.contentRow = g.contentBottom;
|
|
483
|
+
// 放大:contentRow < 新 bottom 且回尾(offset=0)→ 推进到 min(committed+1, bottom)
|
|
484
|
+
// committed+1 是合法「待写位」;用 total+1 在 hasCurrent=true(breakRow 后)时会多跳一行。
|
|
485
|
+
if (state.scrollOffset === 0 && state.contentRow < g.contentBottom) {
|
|
486
|
+
state.contentRow = Math.min(committed + 1, g.contentBottom);
|
|
487
|
+
}
|
|
488
|
+
if (state.frameRow && state.frameRow > g.contentBottom)
|
|
489
|
+
state.frameRow = g.contentBottom;
|
|
490
|
+
const maxOff = Math.max(0, total - g.contentBottom);
|
|
491
|
+
if (state.scrollOffset > maxOff)
|
|
492
|
+
state.scrollOffset = maxOff;
|
|
493
|
+
setRegion(state.footerH); // 用新 rows 立即重设 DECSTBM 区域(contentBottom 变)
|
|
494
|
+
// 重画防抖(仅 repaint,避免快速拖动闪烁);行号/区域已立即更新
|
|
495
|
+
if (state.resizeTimer)
|
|
496
|
+
clearTimeout(state.resizeTimer);
|
|
497
|
+
state.resizeTimer = setTimeout(() => {
|
|
498
|
+
state.resizeTimer = null;
|
|
499
|
+
if (!state.active)
|
|
500
|
+
return;
|
|
501
|
+
const latest = getGeo(state.footerH);
|
|
502
|
+
// 列宽变化时会重排 markdown;即便仅高度变化或重排后行数不变,也必须重算
|
|
503
|
+
// active segment 的屏幕锚点与当前待写列,避免后续流式 chunk 使用旧坐标。
|
|
504
|
+
const colsChanged = latest.cols !== state.lastTerminalCols;
|
|
505
|
+
reflowContentForResize(latest.cols, colsChanged);
|
|
506
|
+
state.lastTerminalCols = latest.cols;
|
|
507
|
+
repaintViewport(); // 内容区按最新高度和列宽重画;markdown 历史已按新列宽 reflow
|
|
508
|
+
repaint(); // 底栏重画
|
|
509
|
+
}, 100);
|
|
510
|
+
state.resizeTimer?.unref?.();
|
|
511
|
+
};
|
|
512
|
+
process.on('SIGWINCH', state.sigwinchHandler);
|
|
513
|
+
installConsoleGuard(); // 进入 TUI 即接管裸 console 输出,防第三方日志污染输入框
|
|
514
|
+
}
|
|
515
|
+
/** 复原:复位 margins + 显光标 + 退 alt + 还原 raw。幂等。 */
|
|
516
|
+
export function exitAltScreen() {
|
|
517
|
+
if (!state.active)
|
|
518
|
+
return;
|
|
519
|
+
state.active = false;
|
|
520
|
+
uninstallConsoleGuard(); // 退出 TUI 恢复原始 console(不影响 host 子进程 / 退出日志)
|
|
521
|
+
resetTerminalBackground();
|
|
522
|
+
stopTurnTimer(); // 兜底清走时计时器(防异常退出泄漏)
|
|
523
|
+
state.turnStart = null;
|
|
524
|
+
state.scrollLockUntil = 0; // 清轮首滚动锁(防状态泄漏到下次进 alt 屏)
|
|
525
|
+
state.frameRow = 0; // 清 spinner 帧位置(防状态泄漏到下次进 alt 屏)
|
|
526
|
+
state.frameCol = 0;
|
|
527
|
+
mouse.setHandler(null);
|
|
528
|
+
mouse.resetMouse();
|
|
529
|
+
state.selection = null;
|
|
530
|
+
state.selecting = false;
|
|
531
|
+
// raw 还原独立 try:非 TTY / 不支持时 setRawMode 抛错,不应阻断 stdout 恢复(alt 退屏必须执行)。
|
|
532
|
+
try {
|
|
533
|
+
stdin.setRawMode(false); // 还原 raw(RUNNING 态常驻 raw,退出时必须还原,否则终端残留 raw 模式)
|
|
534
|
+
}
|
|
535
|
+
catch {
|
|
536
|
+
// 非 TTY / 不支持:忽略
|
|
537
|
+
}
|
|
538
|
+
try {
|
|
539
|
+
stdout.write('\x1B[r'); // 复位 DECSTBM margins
|
|
540
|
+
stdout.write(esc.cursorShow);
|
|
541
|
+
stdout.write(esc.mouseOff); // 关鼠标追踪(反序:先 1006l 再 1002l 再 1000l)
|
|
542
|
+
stdout.write(esc.altOff); // 退 alt(恢复主屏 + 光标)
|
|
543
|
+
}
|
|
544
|
+
catch {
|
|
545
|
+
// 忽略
|
|
546
|
+
}
|
|
547
|
+
if (state.exitHandler) {
|
|
548
|
+
try {
|
|
549
|
+
process.removeListener('exit', state.exitHandler);
|
|
550
|
+
}
|
|
551
|
+
catch {
|
|
552
|
+
// 忽略
|
|
553
|
+
}
|
|
554
|
+
state.exitHandler = null;
|
|
555
|
+
}
|
|
556
|
+
if (state.sigwinchHandler) {
|
|
557
|
+
try {
|
|
558
|
+
process.removeListener('SIGWINCH', state.sigwinchHandler);
|
|
559
|
+
}
|
|
560
|
+
catch {
|
|
561
|
+
// 忽略
|
|
562
|
+
}
|
|
563
|
+
state.sigwinchHandler = null;
|
|
564
|
+
}
|
|
565
|
+
if (state.resizeTimer) {
|
|
566
|
+
clearTimeout(state.resizeTimer);
|
|
567
|
+
state.resizeTimer = null;
|
|
568
|
+
}
|
|
569
|
+
if (state.flushTimer) {
|
|
570
|
+
clearTimeout(state.flushTimer);
|
|
571
|
+
state.flushTimer = null;
|
|
572
|
+
}
|
|
573
|
+
state.userActiveUntil = 0;
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* 直写终端(绕过 content buffer):仅限弹窗类覆盖层(输入面板)使用——
|
|
577
|
+
* 它画在 content 区之上,关闭后由调用方 repaintViewport() 整幅还原。
|
|
578
|
+
* 不做行宽钳制:调用方自己保证每行 ≤ cols(弹窗几何已按 getGeo() 计)。
|
|
579
|
+
*/
|
|
580
|
+
export function writeDirect(s) {
|
|
581
|
+
try {
|
|
582
|
+
stdout.write(s);
|
|
583
|
+
}
|
|
584
|
+
catch {
|
|
585
|
+
// 忽略
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
export function isActive() {
|
|
589
|
+
return state.active;
|
|
590
|
+
}
|