cc-viewer 1.7.0 → 1.7.1
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/dist/assets/App-gUSmU6Ny.js +2 -0
- package/dist/assets/{MdxEditorPanel-C5Q6uUyH.js → MdxEditorPanel-DAR39A3S.js} +1 -1
- package/dist/assets/{Mobile-MoBp9c8k.js → Mobile-qfRsRth0.js} +1 -1
- package/dist/assets/index-B51Ci0GL.js +2 -0
- package/dist/assets/{seqResourceLoaders-CxuD1CYl.css → seqResourceLoaders-DFrHmgnK.css} +1 -1
- package/dist/assets/seqResourceLoaders-DZJxMMaR.js +2 -0
- package/dist/index.html +1 -1
- package/package.json +2 -1
- package/server/lib/log-watcher.js +29 -3
- package/server/lib/v2/adapter.js +71 -6
- package/server/lib/v2/live-feed.js +110 -3
- package/server/lib/v2/meta-rows.js +360 -0
- package/server/lib/v2/v2-writer.js +15 -0
- package/server/lib/wire-compress.js +167 -0
- package/server/routes/events.js +96 -26
- package/server/routes/logs.js +15 -12
- package/server/routes/v2.js +60 -0
- package/server/routes/workspaces.js +7 -6
- package/server/server.js +23 -1
- package/src/utils/apiUrl.js +26 -0
- package/src/utils/askFallback.js +21 -0
- package/src/utils/askOptionDesc.js +55 -0
- package/src/utils/askPortalMatcher.js +54 -0
- package/src/utils/autoApproveOptions.js +27 -0
- package/src/utils/avatarAnimationPostPass.js +49 -0
- package/src/utils/builtinPresets.js +18 -0
- package/src/utils/clearCheckpoint.js +7 -0
- package/src/utils/commandValidator.js +45 -0
- package/src/utils/contentFilter.js +620 -0
- package/src/utils/contextRaw.js +18 -0
- package/src/utils/contextSidebarNavigation.js +16 -0
- package/src/utils/contextTurns.js +117 -0
- package/src/utils/displayScaleHelper.js +47 -0
- package/src/utils/effectiveModel.js +13 -0
- package/src/utils/entry-slim.js +563 -0
- package/src/utils/entryCache.js +144 -0
- package/src/utils/errorReport.js +6 -0
- package/src/utils/fileExpandedPathsStorage.js +70 -0
- package/src/utils/fileIcons.jsx +36 -0
- package/src/utils/fileOpen.js +37 -0
- package/src/utils/formatters.js +48 -0
- package/src/utils/gitApi.js +50 -0
- package/src/utils/gitTreeBuilder.js +19 -0
- package/src/utils/helpers.js +846 -0
- package/src/utils/identityHeal.js +99 -0
- package/src/utils/imConnState.js +59 -0
- package/src/utils/imOrigin.js +24 -0
- package/src/utils/imTr.js +7 -0
- package/src/utils/imageCompress.js +83 -0
- package/src/utils/imageDownscale.js +28 -0
- package/src/utils/imageResize.js +112 -0
- package/src/utils/ingestPipeline.js +47 -0
- package/src/utils/lazyWithReload.js +53 -0
- package/src/utils/markdown.js +30 -0
- package/src/utils/markdownIncremental.js +39 -0
- package/src/utils/markdownProfiler.js +113 -0
- package/src/utils/mdExtensionDetect.js +64 -0
- package/src/utils/memoryLinkParser.js +44 -0
- package/src/utils/modalMask.js +15 -0
- package/src/utils/monotime.js +6 -0
- package/src/utils/pinnedMenu.js +55 -0
- package/src/utils/presetShortcuts.js +14 -0
- package/src/utils/projectAlias.js +135 -0
- package/src/utils/promptClassifier.js +112 -0
- package/src/utils/promptDetect.js +196 -0
- package/src/utils/promptNav.js +60 -0
- package/src/utils/ptyChunkBuilder.js +251 -0
- package/src/utils/quickMenuHoverIntent.js +35 -0
- package/src/utils/rateLimitParser.js +119 -0
- package/src/utils/readResultPool.js +106 -0
- package/src/utils/refreshCachedItemProp.js +30 -0
- package/src/utils/requestType.js +208 -0
- package/src/utils/resizeCalc.js +21 -0
- package/src/utils/resolveLocalized.js +17 -0
- package/src/utils/searchApi.js +47 -0
- package/src/utils/searchReplace.js +51 -0
- package/src/utils/seqResourceLoaders.js +79 -0
- package/src/utils/sessionManager.js +471 -0
- package/src/utils/sessionMerge.js +180 -0
- package/src/utils/skillModalController.js +111 -0
- package/src/utils/skillsParser.js +116 -0
- package/src/utils/slashCommandLabels.js +80 -0
- package/src/utils/splitDragCalc.js +73 -0
- package/src/utils/stickyBottomController.js +643 -0
- package/src/utils/svgSanitize.js +48 -0
- package/src/utils/systemTags.js +47 -0
- package/src/utils/tClaude.js +20 -0
- package/src/utils/teamModalBuilder.js +351 -0
- package/src/utils/teamSessionParser.js +190 -0
- package/src/utils/teammateAvatars.js +194 -0
- package/src/utils/teammateDetector.js +121 -0
- package/src/utils/termDiag.js +114 -0
- package/src/utils/terminalClipboard.js +117 -0
- package/src/utils/terminalWriteQueue.js +398 -0
- package/src/utils/thinkingMerge.js +49 -0
- package/src/utils/toolCatalog.js +29 -0
- package/src/utils/toolResultBuilder.js +393 -0
- package/src/utils/toolResultClassifier.js +16 -0
- package/src/utils/toolResultCore.js +167 -0
- package/src/utils/toolsDiff.js +44 -0
- package/src/utils/toolsXmlFormatter.js +3 -0
- package/src/utils/ultraplanController.js +109 -0
- package/src/utils/ultraplanExperts.js +86 -0
- package/src/utils/ultraplanTemplates.js +154 -0
- package/src/utils/userImageRefs.js +49 -0
- package/src/utils/v3Assembler.js +112 -0
- package/src/utils/v3Rows.js +48 -0
- package/src/utils/voicePackPlayer.js +233 -0
- package/src/utils/webSearchGrouping.js +111 -0
- package/src/utils/workflowFormat.js +52 -0
- package/src/utils/workflowRuns.js +118 -0
- package/src/utils/workflowStore.js +94 -0
- package/dist/assets/App-DTkacfRW.js +0 -2
- package/dist/assets/index-Ac8vnV3v.js +0 -2
- package/dist/assets/seqResourceLoaders-Cd_TAXTb.js +0 -2
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TerminalWriteQueue —— xterm 写缓冲队列,TerminalPanel + ScratchTerminal 共享。
|
|
3
|
+
*
|
|
4
|
+
* 设计目标(仅一个核心优化 + 防回归):
|
|
5
|
+
* 1) 解决原实现 `_writeBuffer = _writeBuffer.slice(N)` 的 O(n²) 字符串切片
|
|
6
|
+
* —— 大流量(/resume 1MB+)下每帧复制整个剩余 buffer,导致 794ms self
|
|
7
|
+
* + GC +56%。改用「string[] queue + offset 指针」算法,整体 O(n)。
|
|
8
|
+
* 2) 每帧仅 write 一个 chunk,节奏与原实现等价:
|
|
9
|
+
* - 不做 single-frame multi-chunk drain(会让 /resume 滚动从平滑变跳顿)
|
|
10
|
+
* - 不设 MAX_BYTES_PER_FRAME(tab 切回时主线程暴吃 200ms)
|
|
11
|
+
*
|
|
12
|
+
* 消化力自适应(AIMD,Windows DOM 渲染器洪泛掉帧治理):
|
|
13
|
+
* - chunk 尺寸动态 ∈ [4KB, 32KB](构造 opts.initialChunkBytes 设初值,默认 32KB)。
|
|
14
|
+
* 依据 xterm write(data, callback) 的解析完成回调计时:单 chunk 耗时 >24ms
|
|
15
|
+
* 减半(把原子解析单元压进帧预算),<8ms 连续 3 次 +4KB。快机稳定 32KB
|
|
16
|
+
* (现状零变化),慢机收敛 4~8KB。
|
|
17
|
+
* - callback 安全边界(旧注释「dispose 时 callback 丢 → 永久死锁」的根治版):
|
|
18
|
+
* callback 仅用于计时与 outstanding 记账,**永不门控写出**;唯一软背压是
|
|
19
|
+
* outstanding > 2×chunk 时跳过本帧(仍续约 rAF),且只在「本 epoch 已观察到
|
|
20
|
+
* callback 正常回调」(_cbSeen)后才启用——mock/不回调的终端行为零变化;
|
|
21
|
+
* 配 500ms fail-open(callback 失联即清账恢复写),只会 fail-open 不会
|
|
22
|
+
* fail-closed。reset()/dispose() 时 epoch++ 丢弃旧回调(terminal.reset 不清
|
|
23
|
+
* xterm WriteBuffer,resync 后旧 cb 仍会到达,防串台污染计时)。
|
|
24
|
+
*
|
|
25
|
+
* 顺带修复(既有缺陷):
|
|
26
|
+
* - UTF-16 surrogate pair 在 32KB 边界硬切 → emoji 显示为 �。
|
|
27
|
+
* 新算法在切片末位检测 high surrogate,优先回退 1 让下轮带出整对;
|
|
28
|
+
* 回退会变 0 时改前进 1(保证整对完整,轻微超 CHUNK_SIZE 可接受)。
|
|
29
|
+
* - terminal.write 抛异常(罕见,dispose 中途 / WebGL contextLoss)
|
|
30
|
+
* → 原实现 buf 已清丢失。新实现 try/catch 内回滚 head/offset,停续约
|
|
31
|
+
* rAF 防死循环;下次 push 时重新触发,数据尽量不丢。
|
|
32
|
+
* - unmount 数据丢失(既有 bug):drain() 同步排空 buffer 给 xterm。
|
|
33
|
+
*
|
|
34
|
+
* 不做的事(避免功能退化):
|
|
35
|
+
* - 不改变 ScratchTerminal 是否分帧(原本无分帧,保持)
|
|
36
|
+
* - 不处理 D3 同步 _flushWrite 调用点的字节序问题(既有 bug,超出范围)
|
|
37
|
+
* - 不引入异步 callback / Promise,保持 rAF 同步语义
|
|
38
|
+
*
|
|
39
|
+
* 积压自保(Windows ConPTY 洪泛防卡死):
|
|
40
|
+
* - ConPTY 把 TUI 输出转译成全屏重绘序列,入速可远超 rAF 32KB/帧(≈2MB/s)的出速,
|
|
41
|
+
* queue 无界堆积 → 内存膨胀 → GC 长停顿 → 页面卡死(极端 renderer OOM)。
|
|
42
|
+
* - 积压超 highWaterBytes(默认 2MB ≈ 1 秒待写量)时从 head「整项」丢弃回落到
|
|
43
|
+
* trimTargetBytes(默认 512KB),只动 head/offset 指针、绝不从项中间切;
|
|
44
|
+
* 序列可被 PTY 分块切到相邻两项,故丢弃后对保留的新 head 项做锚点推进
|
|
45
|
+
* (offset 跳到下一个 ESC / LF 之后,见 _maybeTrim doc)防孤儿序列尾巴
|
|
46
|
+
* 按字面渲染。丢弃后下一帧先写黄字提示(前缀 \x18 CAN
|
|
47
|
+
* 中止半截转义 + \x1b[?2026l 退出可能被撕裂配对的同步输出模式),依赖洪泛流
|
|
48
|
+
* 自带的全屏重绘自愈,
|
|
49
|
+
* 不调 term.reset()(保 scrollback、避免 WebGL 重建抖动)。
|
|
50
|
+
* - 单次 /resume 1MB+ push 低于水位不会误触;每帧节奏不变
|
|
51
|
+
* (不踩 tab 切回暴吃 / /resume 跳顿坑)。
|
|
52
|
+
* - reset():清空队列但保持可用(服务端 data-resync 对齐时用,区别于 dispose 终态);
|
|
53
|
+
* 同时把 chunk 复位到构造初值(平台保守初值,慢机由 AIMD 再收敛)并取消在途 rAF。
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
import { diagCount, diagSet, diagEwma } from './termDiag.js';
|
|
57
|
+
import { now } from './monotime.js';
|
|
58
|
+
|
|
59
|
+
const CHUNK_SIZE = 32 * 1024; // 自适应上限(= 原固定值,快机行为不变)
|
|
60
|
+
const CHUNK_MIN = 4 * 1024; // 自适应下限
|
|
61
|
+
const ADAPT_SLOW_MS = 24; // 单 chunk 解析耗时超此值 → 减半
|
|
62
|
+
const ADAPT_FAST_MS = 8; // 低于此值连续 ADAPT_FAST_STREAK 次 → +4KB
|
|
63
|
+
const ADAPT_FAST_STREAK = 3;
|
|
64
|
+
const ADAPT_STEP_UP = 4 * 1024;
|
|
65
|
+
const CB_FAIL_OPEN_MS = 500; // callback 失联超此值清账恢复写(fail-open)
|
|
66
|
+
const GC_THRESHOLD_HEAD = 64; // queue 头部消费指针超 64 项触发压缩
|
|
67
|
+
const GC_CONSUMED_RATIO = 2; // head×2 > 总项数(已消费过半)也触发(防长尾占内存)
|
|
68
|
+
const GC_RATIO_MIN_HEAD = 8; // 比例触发的最小 head(避免小队列频繁 slice)
|
|
69
|
+
const HIGH_WATER_BYTES = 2 * 1024 * 1024; // 积压上限:超过即丢最旧整项
|
|
70
|
+
const TRIM_TARGET_BYTES = 512 * 1024; // 丢弃后回落的目标水位(留迟滞带防抖)
|
|
71
|
+
// 丢弃提示:\x18 (CAN) 中止 xterm 解析器中可能残留的半截转义序列;随后 \x1b[?2026l
|
|
72
|
+
// 幂等退出 DEC 2026 同步输出模式——直通态下 Claude Code 自发的 ?2026h/?2026l 可能分属
|
|
73
|
+
// 不同队列项,trim 撕裂配对会让渲染停到 xterm 内置 1s 超时(静默尾部场景更久)才自愈。
|
|
74
|
+
const TRIM_NOTICE = '\x18\x1b[?2026l\r\n\x1b[33m[cc-viewer] output trimmed (renderer behind)\x1b[0m\r\n';
|
|
75
|
+
// 带内复位(替代带外 terminal.reset()):reset() API 立即清解析器状态,但 xterm 内部
|
|
76
|
+
// WriteBuffer 里已 write 未解析的字节会在 reset **之后**继续解析——它们的起点是解析暂停点
|
|
77
|
+
// (可能在序列正中间),ground state 下半截序列按字面渲染成 `0;134m` 类残片并永久留在
|
|
78
|
+
// scrollback 里。改为把复位作为字节流推进写队列,按序排在残留内容之后,解析器状态全程连续。
|
|
79
|
+
// 各字节职责(关键:组合内**无任何序列清 scrollback**,重连/resync 后历史可上拉):
|
|
80
|
+
// \x07 BEL —— 若解析器停在 OSC payload 中,终结之(ground 态下是无声 bell,无害)
|
|
81
|
+
// \x18 CAN —— 若停在 CSI/ESC 中,中止之 → GROUND(零残片防线**全靠这两字节**,非靠 RIS)
|
|
82
|
+
// \x1b[2J ED2 —— 仅清可视区(scrollOnEraseInDisplay 默认 false=就地擦除,不滚入历史),
|
|
83
|
+
// 给重连/反压 resync 的快照一块干净视口,避免与陈旧当前帧重叠重复
|
|
84
|
+
// \x1b[H CUP —— 光标归位视口左上(替代 RIS 的光标复位)
|
|
85
|
+
// \x1b[!p DECSTR —— 软复位 SGR/modes/charset(替代 RIS 的属性复位),不动任何 buffer
|
|
86
|
+
// 历史曾用 \x1bc RIS(= Terminal.reset(),新建 Buffer 连 scrollback 一起清空)——已弃:
|
|
87
|
+
// 它让每次 ws 重连 / 反压 resync 都把用户历史清成只剩一屏、上拉不到。已用真实 xterm 6.0
|
|
88
|
+
// headless 验证:本组合保留 scrollback 且对半截序列零残片(test/terminal-pipeline-oracle)。
|
|
89
|
+
export const INBAND_RESET = '\x07\x18\x1b[2J\x1b[H\x1b[!p';
|
|
90
|
+
|
|
91
|
+
export class TerminalWriteQueue {
|
|
92
|
+
/**
|
|
93
|
+
* @param {() => any | null} getTerminal - 返回当前 xterm 实例(或 null)
|
|
94
|
+
* @param {{ highWaterBytes?: number, trimTargetBytes?: number, initialChunkBytes?: number, onTrim?: () => void }} [opts]
|
|
95
|
+
* - 积压自保水位,移动端可传更小值(内存预算低)
|
|
96
|
+
* - initialChunkBytes:自适应 chunk 初值(Windows DOM 渲染器建议 16KB),
|
|
97
|
+
* 运行期按消化耗时在 [CHUNK_MIN, CHUNK_SIZE] 内 AIMD 调节
|
|
98
|
+
* - onTrim:积压**实际丢弃**队列项时回调(每次 trim 触发,调用方自行节流)。
|
|
99
|
+
* 整项丢弃不撕裂 ANSI/surrogate,但丢掉的内容对增量输出流不会自愈——
|
|
100
|
+
* 调用方应借此向服务端发 resync-request 请求权威快照对齐。
|
|
101
|
+
*/
|
|
102
|
+
constructor(getTerminal, opts) {
|
|
103
|
+
this._getTerminal = getTerminal;
|
|
104
|
+
this._queue = [];
|
|
105
|
+
this._head = 0; // 已完整消费的 queue 项数
|
|
106
|
+
this._offset = 0; // queue[head] 中已消费的字符偏移
|
|
107
|
+
this._rafId = 0; // 0 = 无定时器(避免 null vs number 歧义)
|
|
108
|
+
this._unmounted = false;
|
|
109
|
+
this._highWater = opts?.highWaterBytes || HIGH_WATER_BYTES;
|
|
110
|
+
this._trimTarget = opts?.trimTargetBytes || TRIM_TARGET_BYTES;
|
|
111
|
+
this._trimmedSinceFlush = false;
|
|
112
|
+
this._onTrim = typeof opts?.onTrim === 'function' ? opts.onTrim : null;
|
|
113
|
+
// ── 消化力自适应状态 ──
|
|
114
|
+
const init = opts?.initialChunkBytes || CHUNK_SIZE;
|
|
115
|
+
this._initialChunk = Math.max(CHUNK_MIN, Math.min(CHUNK_SIZE, init)); // reset() 复位基准
|
|
116
|
+
this._chunkSize = this._initialChunk;
|
|
117
|
+
this._fastStreak = 0; // 连续快回调计数(AIMD 增窗)
|
|
118
|
+
this._epoch = 0; // reset/dispose 递增,丢弃旧 write callback
|
|
119
|
+
this._outstanding = 0; // 已写出未回调的字符数
|
|
120
|
+
this._cbSeen = false; // 本 epoch 是否观察到 callback 正常回调(门控前置条件)
|
|
121
|
+
this._lastWriteSentAt = 0; // fail-open 计时基准
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** 单 chunk 解析耗时 → AIMD 调节 chunk 尺寸 */
|
|
125
|
+
_adapt(dt) {
|
|
126
|
+
if (dt > ADAPT_SLOW_MS) {
|
|
127
|
+
this._chunkSize = Math.max(CHUNK_MIN, this._chunkSize >> 1);
|
|
128
|
+
this._fastStreak = 0;
|
|
129
|
+
} else if (dt < ADAPT_FAST_MS) {
|
|
130
|
+
if (++this._fastStreak >= ADAPT_FAST_STREAK) {
|
|
131
|
+
this._fastStreak = 0;
|
|
132
|
+
this._chunkSize = Math.min(CHUNK_SIZE, this._chunkSize + ADAPT_STEP_UP);
|
|
133
|
+
}
|
|
134
|
+
} else {
|
|
135
|
+
this._fastStreak = 0;
|
|
136
|
+
}
|
|
137
|
+
diagSet('chunkSize', this._chunkSize);
|
|
138
|
+
diagEwma('cbLatencyEwma', dt);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* 异步写入(与原 _throttledWrite 等价入口)
|
|
143
|
+
* @param {string} data
|
|
144
|
+
*/
|
|
145
|
+
push(data) {
|
|
146
|
+
if (!data || this._unmounted) return;
|
|
147
|
+
if (typeof data !== 'string') return; // 当前 cc-viewer 仅传 string
|
|
148
|
+
this._queue.push(data);
|
|
149
|
+
this._maybeTrim();
|
|
150
|
+
this._schedule();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* 积压自保:超 highWater 时从 head 整项丢弃回落到 trimTarget。
|
|
155
|
+
* 只推进 head/offset 指针(与 _flush 的消费语义完全一致),不从项中间切;
|
|
156
|
+
* 但序列可被 PTY 分块切到相邻两项——被丢项若以半截转义序列结尾,保留的新
|
|
157
|
+
* head 项开头就是孤儿尾巴(ground state 下被 xterm 按字面渲染成 `[9m`/
|
|
158
|
+
* `2;8;145;178m` 类乱码,TRIM_NOTICE 的 \x18 只能中止解析器内的半截序列,
|
|
159
|
+
* 管不了它)。故丢弃后对新 head 项做锚点推进:offset 跳到项内下一个 ESC
|
|
160
|
+
* (全新序列起点)或 LF 之后——与服务端 ansi-safe-slice.js 同语义;窗口内
|
|
161
|
+
* 无锚点则保持原样(纯文本无撕裂风险)。丢弃后置标记,下一帧 _flush 先写
|
|
162
|
+
* TRIM_NOTICE 告知用户。
|
|
163
|
+
*/
|
|
164
|
+
_maybeTrim() {
|
|
165
|
+
let pending = this._pendingBytes();
|
|
166
|
+
diagSet('writeQPendingBytes', pending);
|
|
167
|
+
if (pending <= this._highWater) return;
|
|
168
|
+
// 单项超大(如 >2MB /resume 快照)时下方循环一项都不会丢——此时不得置
|
|
169
|
+
// trim 标记/计数,否则会给用户写假的 "output trimmed" 黄字(数据其实完整交付)
|
|
170
|
+
const headBefore = this._head;
|
|
171
|
+
// length-1:最新一项永不丢(单项超大如 /resume 快照时整段保留,终端必须呈现最新状态)
|
|
172
|
+
while (this._head < this._queue.length - 1 && pending > this._trimTarget) {
|
|
173
|
+
pending -= this._queue[this._head].length - this._offset;
|
|
174
|
+
this._head++;
|
|
175
|
+
this._offset = 0;
|
|
176
|
+
}
|
|
177
|
+
if (this._head === headBefore) return; // 实际什么都没丢 → 不报 trim
|
|
178
|
+
// 锚点推进(见方法 doc):跳过可能的孤儿序列尾巴。ESC 优先于 LF(与服务端一致),
|
|
179
|
+
// 顺带跳过项首孤立低代理(配对的高代理在被丢项尾部)。
|
|
180
|
+
const keptHead = this._queue[this._head];
|
|
181
|
+
if (keptHead) {
|
|
182
|
+
let anchor = -1;
|
|
183
|
+
let afterLf = -1;
|
|
184
|
+
const limit = Math.min(keptHead.length, 4096);
|
|
185
|
+
for (let i = 0; i < limit; i++) {
|
|
186
|
+
const c = keptHead.charCodeAt(i);
|
|
187
|
+
if (c === 0x1b) { anchor = i; break; }
|
|
188
|
+
if (c === 0x0a && afterLf === -1) afterLf = i + 1;
|
|
189
|
+
}
|
|
190
|
+
let off = anchor !== -1 ? anchor : (afterLf !== -1 ? afterLf : 0);
|
|
191
|
+
const c0 = keptHead.charCodeAt(off);
|
|
192
|
+
if (c0 >= 0xdc00 && c0 <= 0xdfff) off++;
|
|
193
|
+
this._offset = off;
|
|
194
|
+
}
|
|
195
|
+
this._trimmedSinceFlush = true;
|
|
196
|
+
diagCount('trimCount');
|
|
197
|
+
// 通知调用方丢弃已发生(见构造 opts.onTrim doc)。回调异常不得污染 push 路径。
|
|
198
|
+
if (this._onTrim) {
|
|
199
|
+
try { this._onTrim(); } catch { }
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
_schedule() {
|
|
204
|
+
if (this._rafId || this._unmounted) return;
|
|
205
|
+
this._rafId = requestAnimationFrame(() => this._flush());
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
_flush() {
|
|
209
|
+
this._rafId = 0;
|
|
210
|
+
if (this._unmounted) return;
|
|
211
|
+
const term = this._getTerminal();
|
|
212
|
+
if (!term) return;
|
|
213
|
+
|
|
214
|
+
// 软背压:xterm 内未消化字节超 2×chunk 时跳过本帧(仍续约 rAF,队列不滞留)。
|
|
215
|
+
// 仅在本 epoch 观察到 callback 正常回调后启用(mock/不回调的终端零变化);
|
|
216
|
+
// callback 失联超 CB_FAIL_OPEN_MS 即清账恢复写——只 fail-open 不 fail-closed。
|
|
217
|
+
if (this._cbSeen && this._outstanding > 2 * this._chunkSize) {
|
|
218
|
+
if (now() - this._lastWriteSentAt > CB_FAIL_OPEN_MS) {
|
|
219
|
+
this._outstanding = 0;
|
|
220
|
+
this._fastStreak = 0;
|
|
221
|
+
} else {
|
|
222
|
+
if (this._head < this._queue.length) this._schedule();
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// 积压丢弃过:先写提示行(独立于下方 out 的回滚语义;写失败保留标记下帧重试)
|
|
228
|
+
if (this._trimmedSinceFlush) {
|
|
229
|
+
try {
|
|
230
|
+
term.write(TRIM_NOTICE);
|
|
231
|
+
this._trimmedSinceFlush = false;
|
|
232
|
+
} catch { /* 与下方 write 同等容错 */ }
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// 取出最多 _chunkSize 字符到 out。每帧仅一次 write,节奏与原实现等价。
|
|
236
|
+
// 指针快照须在消费循环**之前**(原实现误在循环后快照,write 抛错时恢复的是
|
|
237
|
+
// 消费后指针 = 该 chunk 静默丢失,与下方注释的回滚契约相反——已修正)。
|
|
238
|
+
const headBefore = this._head;
|
|
239
|
+
const offsetBefore = this._offset;
|
|
240
|
+
const CHUNK = this._chunkSize;
|
|
241
|
+
let out = '';
|
|
242
|
+
while (this._head < this._queue.length && out.length < CHUNK) {
|
|
243
|
+
const head = this._queue[this._head];
|
|
244
|
+
const offset = this._offset;
|
|
245
|
+
const remaining = head.length - offset;
|
|
246
|
+
const need = CHUNK - out.length;
|
|
247
|
+
if (need <= 0) break;
|
|
248
|
+
|
|
249
|
+
if (remaining <= need) {
|
|
250
|
+
// 整段消费 head
|
|
251
|
+
out += offset === 0 ? head : head.slice(offset);
|
|
252
|
+
this._head++;
|
|
253
|
+
this._offset = 0;
|
|
254
|
+
} else {
|
|
255
|
+
// 部分消费:从 offset 切出 need 字符
|
|
256
|
+
let cut = offset + need;
|
|
257
|
+
// UTF-16 surrogate 守卫:若末位是高代理,要么回退 1(下轮带出整对),
|
|
258
|
+
// 要么前进 1(仅 1 char 时把整对带出,轻微超 CHUNK_SIZE)。
|
|
259
|
+
const codeAtEnd = head.charCodeAt(cut - 1);
|
|
260
|
+
if (codeAtEnd >= 0xD800 && codeAtEnd <= 0xDBFF) {
|
|
261
|
+
if (cut - 1 > offset) {
|
|
262
|
+
cut--; // 正常回退
|
|
263
|
+
} else if (cut < head.length) {
|
|
264
|
+
cut++; // 仅 1 char 高代理 → 前进带出整对
|
|
265
|
+
}
|
|
266
|
+
// else: head 末位的孤立高代理(数据本身坏的)→ 照原样发,xterm 会容错显示
|
|
267
|
+
}
|
|
268
|
+
out += head.slice(offset, cut);
|
|
269
|
+
this._offset = cut;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (!out) return;
|
|
274
|
+
|
|
275
|
+
// try/catch + 回滚:xterm.write 同步抛错时停续约 rAF 防死循环。
|
|
276
|
+
// 数据保留语义:head/offset 是逻辑指针,queue 项本身没被 splice,
|
|
277
|
+
// 回滚到 write 之前就能让下次 _flush 重新组装相同的 out 重试。
|
|
278
|
+
// GC 在 write 成功后才执行(见下方),确保失败回滚永远有效。
|
|
279
|
+
const epoch = this._epoch;
|
|
280
|
+
const sentAt = now();
|
|
281
|
+
this._outstanding += out.length;
|
|
282
|
+
this._lastWriteSentAt = sentAt;
|
|
283
|
+
try {
|
|
284
|
+
// callback 仅做计时与记账(AIMD 输入),写出本身从不等待它
|
|
285
|
+
term.write(out, () => {
|
|
286
|
+
if (epoch !== this._epoch) return; // reset/dispose 后旧回调 → 丢弃防串台
|
|
287
|
+
this._cbSeen = true;
|
|
288
|
+
this._outstanding = Math.max(0, this._outstanding - out.length);
|
|
289
|
+
this._adapt(now() - sentAt);
|
|
290
|
+
});
|
|
291
|
+
} catch {
|
|
292
|
+
// 双向记账守恒:成功路径由 callback 减账,失败路径在此立即回滚——
|
|
293
|
+
// 任何分支下 outstanding ≥0 且不累积。抛错后有意**不**续约 rAF
|
|
294
|
+
// (write 持续抛时 rAF 会 60fps 空转),数据保留在队列,下次 push 重触发。
|
|
295
|
+
this._head = headBefore;
|
|
296
|
+
this._offset = offsetBefore;
|
|
297
|
+
this._outstanding = Math.max(0, this._outstanding - out.length);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// 周期压缩 queue:write 成功后才 GC,避免 splice 已消费部分破坏失败回滚的有效性。
|
|
302
|
+
// head 索引超 GC_THRESHOLD_HEAD / 头部已消费比例高时一次性回收。
|
|
303
|
+
// (head++ 后 offset 必为 0,slice 后头部对齐到当前 head 的实际位置 0,
|
|
304
|
+
// 不会破坏 _offset 与 head 的语义对应。)
|
|
305
|
+
if (
|
|
306
|
+
this._head > GC_THRESHOLD_HEAD ||
|
|
307
|
+
(this._head > GC_RATIO_MIN_HEAD && this._head * GC_CONSUMED_RATIO > this._queue.length)
|
|
308
|
+
) {
|
|
309
|
+
this._queue = this._queue.slice(this._head);
|
|
310
|
+
this._head = 0;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (this._head < this._queue.length) this._schedule();
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* 同步排空(unmount 前调用,防最后 16ms 数据丢失)。
|
|
318
|
+
* 只 write 一次合并 buffer,失败静默吞掉(已经在 unmount 路径上)。
|
|
319
|
+
*/
|
|
320
|
+
drain() {
|
|
321
|
+
if (this._unmounted) return;
|
|
322
|
+
const term = this._getTerminal();
|
|
323
|
+
if (!term) return;
|
|
324
|
+
// trim 后 rAF 尚未跑就 unmount:提示行也要随排空写出,不静默吞掉
|
|
325
|
+
if (this._trimmedSinceFlush) {
|
|
326
|
+
this._trimmedSinceFlush = false;
|
|
327
|
+
try { term.write(TRIM_NOTICE); } catch { /* unmount 路径,吞掉 */ }
|
|
328
|
+
}
|
|
329
|
+
if (this._head >= this._queue.length) return;
|
|
330
|
+
let out = '';
|
|
331
|
+
while (this._head < this._queue.length) {
|
|
332
|
+
const head = this._queue[this._head];
|
|
333
|
+
out += this._offset === 0 ? head : head.slice(this._offset);
|
|
334
|
+
this._head++;
|
|
335
|
+
this._offset = 0;
|
|
336
|
+
}
|
|
337
|
+
if (out) {
|
|
338
|
+
try { term.write(out); } catch { /* unmount 路径,吞掉 */ }
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* 清空队列但保持可用(服务端 data-resync 对齐时用,区别于 dispose 的终态)。
|
|
344
|
+
* epoch++ 丢弃在途旧 write callback(terminal.reset 不清 xterm WriteBuffer);
|
|
345
|
+
* chunk 复位到构造初值(保留平台保守初值:Windows 16KB / 其余 32KB——
|
|
346
|
+
* 快机 resync 后快照重放不必从 4KB 爬坡,慢机由 AIMD 重新收敛);取消在途 rAF。
|
|
347
|
+
*/
|
|
348
|
+
reset() {
|
|
349
|
+
if (this._unmounted) return;
|
|
350
|
+
if (this._rafId) {
|
|
351
|
+
cancelAnimationFrame(this._rafId);
|
|
352
|
+
this._rafId = 0;
|
|
353
|
+
}
|
|
354
|
+
this._queue.length = 0;
|
|
355
|
+
this._head = 0;
|
|
356
|
+
this._offset = 0;
|
|
357
|
+
this._trimmedSinceFlush = false;
|
|
358
|
+
this._epoch++;
|
|
359
|
+
this._outstanding = 0;
|
|
360
|
+
this._cbSeen = false;
|
|
361
|
+
this._fastStreak = 0;
|
|
362
|
+
this._chunkSize = this._initialChunk;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* 释放资源。dispose 后 push 静默忽略,rAF 取消,在途 callback 按 epoch 丢弃。
|
|
367
|
+
*/
|
|
368
|
+
dispose() {
|
|
369
|
+
this._unmounted = true;
|
|
370
|
+
if (this._rafId) {
|
|
371
|
+
cancelAnimationFrame(this._rafId);
|
|
372
|
+
this._rafId = 0;
|
|
373
|
+
}
|
|
374
|
+
this._queue.length = 0;
|
|
375
|
+
this._head = 0;
|
|
376
|
+
this._offset = 0;
|
|
377
|
+
this._epoch++;
|
|
378
|
+
this._outstanding = 0;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/** 测试用 —— 返回当前队列剩余字节数 */
|
|
382
|
+
_pendingBytes() {
|
|
383
|
+
let total = 0;
|
|
384
|
+
for (let i = this._head; i < this._queue.length; i++) {
|
|
385
|
+
total += this._queue[i].length;
|
|
386
|
+
}
|
|
387
|
+
return total - this._offset;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// 暴露常量给测试 / 外部参考(不要在生产代码读这些,行为是不变的契约)
|
|
392
|
+
TerminalWriteQueue.CHUNK_SIZE = CHUNK_SIZE;
|
|
393
|
+
TerminalWriteQueue.CHUNK_MIN = CHUNK_MIN;
|
|
394
|
+
TerminalWriteQueue.GC_THRESHOLD_HEAD = GC_THRESHOLD_HEAD;
|
|
395
|
+
TerminalWriteQueue.HIGH_WATER_BYTES = HIGH_WATER_BYTES;
|
|
396
|
+
TerminalWriteQueue.TRIM_TARGET_BYTES = TRIM_TARGET_BYTES;
|
|
397
|
+
TerminalWriteQueue.TRIM_NOTICE = TRIM_NOTICE;
|
|
398
|
+
TerminalWriteQueue.CB_FAIL_OPEN_MS = CB_FAIL_OPEN_MS;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thinking-block merging helper.
|
|
3
|
+
*
|
|
4
|
+
* Anthropic 的「交错思考(interleaved thinking)」格式会在单条 assistant message 的
|
|
5
|
+
* content 数组里放多个 thinking 块(与 tool_use 交错排列)。视图层应把它们合并成
|
|
6
|
+
* 单个「思考过程」折叠块,各段非空 thinking 文本用 markdown 水平分隔线连接,视觉上
|
|
7
|
+
* 彼此分隔;否则同一请求会渲染出多个折叠框,非常冗长。
|
|
8
|
+
*
|
|
9
|
+
* mergeThinkingBlocks(content) 返回 { text, isEmpty, count }:
|
|
10
|
+
* - count : content 中 type==='thinking' 的块数(含空/redacted/非字符串)。
|
|
11
|
+
* - text : 各非空、且非纯 '---' 的 thinking 文本 trim 后用 '\n\n---\n\n' 连接;
|
|
12
|
+
* 无可用段时为 ''。
|
|
13
|
+
* - isEmpty : 无任何可用 thinking 文本时为 true(全部为空/redacted/非字符串)。
|
|
14
|
+
*
|
|
15
|
+
* 注意:count > 0 不蕴含 isEmpty === false——存在 thinking 块但全为空/redacted 时
|
|
16
|
+
* count>0 且 isEmpty===true。caller 需同时判断:count>0 才渲染折叠框、isEmpty 决定
|
|
17
|
+
* 显示空提示还是合并文本(见 ChatMessage 两个渲染器)。
|
|
18
|
+
*
|
|
19
|
+
* 分隔符必须用 '\n\n---\n\n'(前后空行):marked 在 `段落\n---`(无空行)下会把
|
|
20
|
+
* '---' 解析成 setext H2,加空行后才是 <hr> 水平线。过滤/trim/join 规则与 web-search
|
|
21
|
+
* synthesis 合并先例(ChatMessage.jsx 的 mergedSynthesisText)保持一致。
|
|
22
|
+
*
|
|
23
|
+
* 设计上故意做成纯 JS(无 React 依赖),让 Node test runner 能直接 import。
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const THINKING_SEPARATOR = '\n\n---\n\n';
|
|
27
|
+
|
|
28
|
+
export function mergeThinkingBlocks(content) {
|
|
29
|
+
if (!Array.isArray(content)) {
|
|
30
|
+
return { text: '', isEmpty: true, count: 0 };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let count = 0;
|
|
34
|
+
const segments = [];
|
|
35
|
+
|
|
36
|
+
for (const block of content) {
|
|
37
|
+
if (!block || typeof block !== 'object' || block.type !== 'thinking') continue;
|
|
38
|
+
count++;
|
|
39
|
+
const raw = typeof block.thinking === 'string' ? block.thinking : '';
|
|
40
|
+
const trimmed = raw.trim();
|
|
41
|
+
if (trimmed && trimmed !== '---') segments.push(trimmed);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
text: segments.join(THINKING_SEPARATOR),
|
|
46
|
+
isEmpty: segments.length === 0,
|
|
47
|
+
count,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single source of truth for the built-in tool catalog.
|
|
3
|
+
*
|
|
4
|
+
* `TOOL_CATALOG` is an ordered, function-grouped list of every built-in tool
|
|
5
|
+
* that ships a concept doc at `concepts/<lang>/Tool-<name>.md`. It drives:
|
|
6
|
+
* - the "all tools" catalog modal (src/components/common/ToolsHelp.jsx)
|
|
7
|
+
* - the Tool-* whitelist in ConceptHelp (src/components/common/ConceptHelp.jsx)
|
|
8
|
+
*
|
|
9
|
+
* Keep this in sync with the shipped Tool-*.md docs — the guard test
|
|
10
|
+
* `test/tool-catalog-concepts.test.js` fails if any catalog tool lacks a doc
|
|
11
|
+
* in any language directory.
|
|
12
|
+
*
|
|
13
|
+
* Category `key` maps to i18n `ui.toolCatalog.cat.<key>` in src/i18n.js.
|
|
14
|
+
*/
|
|
15
|
+
export const TOOL_CATALOG = [
|
|
16
|
+
{ key: 'agent', tools: ['Agent', 'TaskCreate', 'TaskGet', 'TaskUpdate', 'TaskList', 'TaskOutput', 'TaskStop'] },
|
|
17
|
+
{ key: 'team', tools: ['TeamCreate', 'TeamDelete', 'SendMessage', 'Workflow', 'Monitor'] },
|
|
18
|
+
{ key: 'file', tools: ['Read', 'Edit', 'Write', 'NotebookEdit'] },
|
|
19
|
+
{ key: 'search', tools: ['Glob', 'Grep', 'ToolSearch'] },
|
|
20
|
+
{ key: 'terminal', tools: ['Bash'] },
|
|
21
|
+
{ key: 'web', tools: ['WebFetch', 'WebSearch', 'Artifact', 'DesignSync'] },
|
|
22
|
+
{ key: 'planning', tools: ['EnterPlanMode', 'ExitPlanMode', 'AskUserQuestion', 'Skill', 'ReportFindings'] },
|
|
23
|
+
{ key: 'worktree', tools: ['EnterWorktree', 'ExitWorktree'] },
|
|
24
|
+
{ key: 'schedule', tools: ['CronCreate', 'CronDelete', 'CronList', 'ScheduleWakeup', 'PushNotification', 'RemoteTrigger'] },
|
|
25
|
+
{ key: 'ide', tools: ['getDiagnostics', 'executeCode', 'LSP'] },
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
// Flat list of all tool names (40) — order follows TOOL_CATALOG.
|
|
29
|
+
export const ALL_TOOL_NAMES = TOOL_CATALOG.flatMap((c) => c.tools);
|