cc-viewer 1.6.348 → 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/README.md +13 -1
- package/cli.js +116 -37
- package/dist/assets/App-gUSmU6Ny.js +2 -0
- package/dist/assets/{MdxEditorPanel-ualoRFOL.js → MdxEditorPanel-DAR39A3S.js} +1 -1
- package/dist/assets/Mobile-qfRsRth0.js +1 -0
- package/dist/assets/{_baseUniq-CG81mgN_.js → _baseUniq-D9_E5ZDp.js} +1 -1
- package/dist/assets/{arc-C161J4f2.js → arc-D4iFrlhb.js} +1 -1
- package/dist/assets/{architectureDiagram-Q4EWVU46-BmbDMdn9.js → architectureDiagram-Q4EWVU46-f1neo7hg.js} +1 -1
- package/dist/assets/{blockDiagram-DXYQGD6D-DXpMvdxd.js → blockDiagram-DXYQGD6D-DWi9e9tG.js} +1 -1
- package/dist/assets/{c4Diagram-AHTNJAMY-8Mypg0IU.js → c4Diagram-AHTNJAMY-DAdj49A2.js} +1 -1
- package/dist/assets/{channel-B2khdmvc.js → channel-B3VthT0m.js} +1 -1
- package/dist/assets/{chunk-4BX2VUAB-I61r1g6z.js → chunk-4BX2VUAB-BrPcAHV3.js} +1 -1
- package/dist/assets/{chunk-4TB4RGXK-VFPlRE5E.js → chunk-4TB4RGXK-Bvopc1V9.js} +1 -1
- package/dist/assets/{chunk-55IACEB6-DvCamg_6.js → chunk-55IACEB6-Juw-7Z3i.js} +1 -1
- package/dist/assets/{chunk-EDXVE4YY-Dj4Q8RwR.js → chunk-EDXVE4YY-DVYP9tpB.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-_bgQuuJh.js → chunk-FMBD7UC4-VQ7VOXl3.js} +1 -1
- package/dist/assets/{chunk-OYMX7WX6-3to38LyN.js → chunk-OYMX7WX6-F8Rj4ZUZ.js} +1 -1
- package/dist/assets/{chunk-QZHKN3VN-Zl26wKKz.js → chunk-QZHKN3VN-Dn7K3yi9.js} +1 -1
- package/dist/assets/{chunk-YZCP3GAM-RLYPrlh0.js → chunk-YZCP3GAM-UjO-KTsn.js} +1 -1
- package/dist/assets/classDiagram-6PBFFD2Q-DmSozxoO.js +1 -0
- package/dist/assets/classDiagram-v2-HSJHXN6E-DmSozxoO.js +1 -0
- package/dist/assets/clone-BQtvy9Ae.js +1 -0
- package/dist/assets/{cose-bilkent-S5V4N54A-CsYOjJ0V.js → cose-bilkent-S5V4N54A-BDhNj8rq.js} +1 -1
- package/dist/assets/{dagre-KV5264BT-C3YXXaFU.js → dagre-KV5264BT-BzdcDuyt.js} +1 -1
- package/dist/assets/{diagram-5BDNPKRD-DUCOg1ld.js → diagram-5BDNPKRD-DP_SX65N.js} +1 -1
- package/dist/assets/{diagram-G4DWMVQ6-BtlLl_zX.js → diagram-G4DWMVQ6-B-pBFw8Q.js} +1 -1
- package/dist/assets/{diagram-MMDJMWI5-1Qp9xfOk.js → diagram-MMDJMWI5-6XSfN1eN.js} +1 -1
- package/dist/assets/{diagram-TYMM5635-Cj7VMp3L.js → diagram-TYMM5635-DJvGxvpE.js} +1 -1
- package/dist/assets/{erDiagram-SMLLAGMA-CVZVn541.js → erDiagram-SMLLAGMA-CpqdpGYj.js} +1 -1
- package/dist/assets/{flowDiagram-DWJPFMVM-CiU9Fkkv.js → flowDiagram-DWJPFMVM-C9m7wjwI.js} +1 -1
- package/dist/assets/{ganttDiagram-T4ZO3ILL-aRaG97ID.js → ganttDiagram-T4ZO3ILL-CU0XOfY3.js} +1 -1
- package/dist/assets/{gitGraphDiagram-UUTBAWPF-Cr2LJlvX.js → gitGraphDiagram-UUTBAWPF-ipyGULnP.js} +1 -1
- package/dist/assets/{graph-B59lDyMn.js → graph-BR2KmES9.js} +1 -1
- package/dist/assets/{index-C1ptnCpq.js → index--wgRW_OB.js} +1 -1
- package/dist/assets/index-B51Ci0GL.js +2 -0
- package/dist/assets/{index-CRAWA8PC.js → index-C-be29ey.js} +1 -1
- package/dist/assets/{index-CSM1Kw8l.js → index-C16fiVhv.js} +1 -1
- package/dist/assets/{index-Ddk-kg22.js → index-D5YruOKD.js} +1 -1
- package/dist/assets/{index-BnhNIjbj.js → index-D5tSeMU7.js} +1 -1
- package/dist/assets/{index-vFEQq-NB.js → index-DuZQ6FqR.js} +1 -1
- package/dist/assets/{index-CO2hZ1s4.js → index-OWzSX2T8.js} +1 -1
- package/dist/assets/{infoDiagram-42DDH7IO-CnO22bw8.js → infoDiagram-42DDH7IO-BQWQjfFX.js} +1 -1
- package/dist/assets/{ishikawaDiagram-UXIWVN3A-ByNzpScH.js → ishikawaDiagram-UXIWVN3A-Df-KH_Hz.js} +1 -1
- package/dist/assets/{journeyDiagram-VCZTEJTY-yaVaCWPq.js → journeyDiagram-VCZTEJTY-CMbB_4G9.js} +1 -1
- package/dist/assets/{jszip.min-BdVFP3OW.js → jszip.min-B6k2t9Ch.js} +1 -1
- package/dist/assets/{kanban-definition-6JOO6SKY-RIPR_RQ3.js → kanban-definition-6JOO6SKY-CImdlKN3.js} +1 -1
- package/dist/assets/{layout-0rzel0v_.js → layout-DQl4aw09.js} +1 -1
- package/dist/assets/{linear-sktN0tB6.js → linear-CLD3aPlK.js} +1 -1
- package/dist/assets/mermaid.core-CN35dvKJ.js +7 -0
- package/dist/assets/{min-rspGOkz_.js → min-_D86henJ.js} +1 -1
- package/dist/assets/{mindmap-definition-QFDTVHPH-BsVzZzTz.js → mindmap-definition-QFDTVHPH-2JVcu1nk.js} +1 -1
- package/dist/assets/{pieDiagram-DEJITSTG-CsoWsw4f.js → pieDiagram-DEJITSTG-Dik-m5u7.js} +1 -1
- package/dist/assets/{quadrantDiagram-34T5L4WZ-CTMKuCAI.js → quadrantDiagram-34T5L4WZ-Bm5l40Jq.js} +1 -1
- package/dist/assets/{requirementDiagram-MS252O5E-C67256te.js → requirementDiagram-MS252O5E-Ck9w5rTj.js} +1 -1
- package/dist/assets/{sankeyDiagram-XADWPNL6-X8Y7KnW0.js → sankeyDiagram-XADWPNL6-BbGVCFEf.js} +1 -1
- package/dist/assets/{seqResourceLoaders-B5Zy_CC_.css → seqResourceLoaders-DFrHmgnK.css} +1 -1
- package/dist/assets/seqResourceLoaders-DZJxMMaR.js +2 -0
- package/dist/assets/{sequenceDiagram-FGHM5R23-Cils0TAp.js → sequenceDiagram-FGHM5R23-vjvEdksk.js} +1 -1
- package/dist/assets/{stateDiagram-FHFEXIEX-DFnohvpG.js → stateDiagram-FHFEXIEX-DTqMoiB2.js} +1 -1
- package/dist/assets/{stateDiagram-v2-QKLJ7IA2-BTX_4o81.js → stateDiagram-v2-QKLJ7IA2-CNbAwnhu.js} +1 -1
- package/dist/assets/{timeline-definition-GMOUNBTQ-DrIYFrzZ.js → timeline-definition-GMOUNBTQ-Cw8O-52d.js} +1 -1
- package/dist/assets/{vendor-antd-D5HmSDSg.js → vendor-antd-DI7JL-mE.js} +1 -1
- package/dist/assets/{vendor-codemirror-C5yuj7ze.js → vendor-codemirror-B9c49dtM.js} +1 -1
- package/dist/assets/{vendor-mdxeditor-BcEooTvb.js → vendor-mdxeditor-B6cpBtIE.js} +2 -2
- package/dist/assets/{vendor-qrcode-d9LCn0-9.js → vendor-qrcode-Dn90-s-u.js} +1 -1
- package/dist/assets/{vendor-virtuoso-K9m4MN6a.js → vendor-virtuoso-CpdSKD_6.js} +1 -1
- package/dist/assets/{vennDiagram-DHZGUBPP-lRqBYTr9.js → vennDiagram-DHZGUBPP-DCuqymb7.js} +1 -1
- package/dist/assets/{wardley-RL74JXVD-e8YfxpOe.js → wardley-RL74JXVD-BJ20wJUU.js} +1 -1
- package/dist/assets/{wardleyDiagram-NUSXRM2D-Bxrg774h.js → wardleyDiagram-NUSXRM2D-Vz6US3qt.js} +1 -1
- package/dist/assets/{xychartDiagram-5P7HB3ND-DWhZqhq8.js → xychartDiagram-5P7HB3ND-CzkLKrmi.js} +1 -1
- package/dist/index.html +4 -4
- package/findcc.js +4 -3
- package/package.json +3 -1
- package/server/i18n.js +21 -141
- package/server/interceptor.js +211 -403
- package/server/lib/async-write-queue.js +12 -1
- package/server/lib/delta-reconstructor.js +3 -3
- package/server/lib/error-report.js +45 -0
- package/server/lib/im-log-watcher.js +68 -8
- package/server/lib/interceptor-core.js +19 -174
- package/server/lib/log-management.js +214 -247
- package/server/lib/log-stream.js +59 -108
- package/server/lib/log-watcher.js +62 -274
- package/server/lib/log-zip.js +148 -0
- package/server/lib/session-boundary.js +266 -0
- package/server/lib/session-pin-store.js +10 -19
- package/server/lib/stats-worker.js +138 -267
- package/server/lib/user-prompt-extract.js +250 -0
- package/server/lib/v2/adapter.js +1124 -0
- package/server/lib/v2/blob-store.js +44 -0
- package/server/lib/v2/conversation-store.js +217 -0
- package/server/lib/v2/convert-manager.js +79 -0
- package/server/lib/v2/convert-worker.js +42 -0
- package/server/lib/v2/convert.js +409 -0
- package/server/lib/v2/identity.js +204 -0
- package/server/lib/v2/journal.js +62 -0
- package/server/lib/v2/layout.js +167 -0
- package/server/lib/v2/live-feed.js +558 -0
- package/server/lib/v2/meta-rows.js +360 -0
- package/server/lib/v2/migrate-prompt.js +54 -0
- package/server/lib/v2/replay.js +191 -0
- package/server/lib/v2/session-select.js +222 -0
- package/server/lib/v2/singleflight.js +104 -0
- package/server/lib/v2/v2-writer.js +528 -0
- package/server/lib/v2/verify.js +233 -0
- package/server/lib/wire-compress.js +167 -0
- package/server/routes/events.js +120 -150
- package/server/routes/files-fs.js +4 -2
- package/server/routes/im.js +11 -2
- package/server/routes/logs.js +209 -164
- package/server/routes/preferences.js +0 -3
- package/server/routes/project-meta.js +2 -3
- package/server/routes/session-pin.js +7 -8
- package/server/routes/v2.js +60 -0
- package/server/routes/workspaces.js +50 -14
- package/server/server.js +70 -66
- package/server/workspace-registry.js +24 -4
- 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-VTo4NVdo.js +0 -2
- package/dist/assets/Mobile-CcmYvMm_.js +0 -1
- package/dist/assets/classDiagram-6PBFFD2Q-BXnyUgsj.js +0 -1
- package/dist/assets/classDiagram-v2-HSJHXN6E-BXnyUgsj.js +0 -1
- package/dist/assets/clone-md-Mc1-T.js +0 -1
- package/dist/assets/index-DCo650PG.js +0 -2
- package/dist/assets/mermaid.core-C2Parj1q.js +0 -7
- package/dist/assets/seqResourceLoaders-BVozq68i.js +0 -2
- package/server/lib/instance-registry.js +0 -57
- package/server/lib/jsonl-archive.js +0 -180
- package/server/lib/teammate-detect.js +0 -32
- package/server/lib/zip-safety.js +0 -68
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* promptDetect —— PTY prompt 检测(ChatView._detectPrompt 的提取 + 线性化重写)。
|
|
3
|
+
*
|
|
4
|
+
* 为什么存在:旧实现的 Pattern 1/2 多行正则带嵌套量词
|
|
5
|
+
* /([^\n]*\?)\s*\n((?:\s*[❯>]?\s*\d+\.\s+[^\n]+\n?){2,})(?:\n[^\d❯>\n][^\n]*|\n)*$/
|
|
6
|
+
* 中 `\s*` 可吃换行、`[^\n]+` 可吃空格、行内 `\d+\.`(版本号)提供多重切分点,
|
|
7
|
+
* 失配时回溯组合数按行数指数爆炸——实测 /plugins 菜单形态文本 4 行 84ms、
|
|
8
|
+
* 6 行 2.2s、8 行 >90s;_ptyBuffer 4KB ≈ 40~80 行 → 单次同步 match 永久占死
|
|
9
|
+
* 主线程(Windows /plugins 菜单整页卡死的根因)。
|
|
10
|
+
*
|
|
11
|
+
* detectPromptInBuffer 用「行式扫描」复刻旧正则语义,可证 O(n·行长):
|
|
12
|
+
* 全程只对单行跑 `^...$` 锚定、无嵌套量词的正则。语义对齐要点(以
|
|
13
|
+
* test/prompt-detect.test.js 的等价回归 fixture 为准绳):
|
|
14
|
+
* - 按 /\r?\n/ 分行:Windows ConPTY 输出 CRLF,行尾 \r 必须在分行时吃掉
|
|
15
|
+
* (单行正则的 `.`/`$` 都不容忍 \r,legacy 靠 [^\n] 容忍——此处对齐);
|
|
16
|
+
* - leftmost:与 JS regex 一致,自上而下取第一个结构成立的 question 行;
|
|
17
|
+
* - Pattern 1 question 行须以 `?` 结尾(容尾随空白);选项块=连续编号行,
|
|
18
|
+
* 块内/块前允许空行;块后所有行须为 trailing(空行或首字符 ∉ {数字,❯,>});
|
|
19
|
+
* - Pattern 2 question 为任意非空行;选项块成员=两段空白结构
|
|
20
|
+
* `\s+[❯>]?\s+内容` 的行(与旧块正则同构,单段空白如 " X" 不是成员);
|
|
21
|
+
* 须 ≥2 项且含 ❯/> 选中项;trailing 定义不同(空行或首字符 ∉ {空白,❯,>});
|
|
22
|
+
* 与旧实现一致,结构首次匹配后验证失败即整体放弃(不向后重试);
|
|
23
|
+
* - 输出形状 {question, options:[{number,text,selected}]} 与旧实现逐字段一致。
|
|
24
|
+
*
|
|
25
|
+
* The original regex implementation (detectPromptLegacy) and its
|
|
26
|
+
* ccv_legacy_prompt_detect escape hatch were removed after the linear rewrite
|
|
27
|
+
* (first shipped in 1.6.308) proved stable in the field; its behavior is
|
|
28
|
+
* pinned by the golden-master corpus in test/prompt-detect.test.js.
|
|
29
|
+
*
|
|
30
|
+
* 纯 JS、无浏览器全局依赖(node:test 可直接 import)。
|
|
31
|
+
*/
|
|
32
|
+
import { now } from './monotime.js';
|
|
33
|
+
|
|
34
|
+
// 与 ChatView._stripAnsi 同源(4 个正则均线性安全)。单一实现:ChatView 与
|
|
35
|
+
// test/permission-detect.test.js 均从这里 import,消除多份拷贝漂移。
|
|
36
|
+
export function stripAnsi(str) {
|
|
37
|
+
// Remove CSI sequences (ESC [ ... final byte), OSC sequences (ESC ] ... ST), and other escape sequences
|
|
38
|
+
return str
|
|
39
|
+
.replace(/\x1b\[[?0-9;]*[A-Za-z]/g, '')
|
|
40
|
+
.replace(/\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g, '')
|
|
41
|
+
.replace(/\x1b[^[\]](.|$)/g, '')
|
|
42
|
+
.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f]/g, '');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 跨 write 撕裂防护:把 data 切成 [可安全 strip 的前段, 尾部未终结的 CSI/OSC 半截]。
|
|
46
|
+
// ANSI 序列被 PTY 分片切断时("\x1b[3" | "6m…")按单次 write strip 会让两半都剥不掉、
|
|
47
|
+
// 残字污染 buffer 致漏检;调用方把 carry 缓带到下一片拼接后再 strip。
|
|
48
|
+
// maxCarry 上限防畸形流(如 OSC 永不终结)让 carry 无限囤积——超限放弃缓带按原样剥。
|
|
49
|
+
export function splitTrailingAnsiCarry(data, maxCarry = 512) {
|
|
50
|
+
const m = data.match(/\x1b(?:\[[?0-9;]*|\][^\x07\x1b]*)?$/);
|
|
51
|
+
if (m && m[0].length < maxCarry) return [data.slice(0, m.index), m[0]];
|
|
52
|
+
return [data, ''];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// false-positive 过滤(从 ChatView 平移,单行正则、线性)。返回 true = 应忽略。
|
|
56
|
+
export function isFalsePositiveQuestion(question) {
|
|
57
|
+
// 形如文件/目录路径或状态栏输出
|
|
58
|
+
if (/^[■\s]*[~\/.:]/.test(question) && /\//.test(question)) return true;
|
|
59
|
+
// Claude Code 计时/状态输出(如 "*Crunchedfor2m18s")
|
|
60
|
+
if (/^[*■✦⏎]/.test(question)) return true;
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ── 单行判定正则(均 ^...$ 锚定、无嵌套量词,单行长度有界 → 线性)──
|
|
65
|
+
// Pattern 1 选项行:可缩进 + 可选 ❯/> 光标 + 编号 + ". " + 文本
|
|
66
|
+
const P1_OPTION_RE = /^\s*([❯>])?\s*(\d+)\.\s+(.+)$/;
|
|
67
|
+
// Pattern 2 块成员准入:与 legacy 块正则 \s+[❯>]?\s+[^\n]+ 同构——
|
|
68
|
+
// ≥1 空白 + 可选光标 + ≥1 空白 + ≥1 字符。单段空白行(" X")不是成员。
|
|
69
|
+
const P2_MEMBER_RE = /^\s+[❯>]?\s+./;
|
|
70
|
+
// Pattern 2 选项行解析(与旧 ChatView 的 per-line 解析一致)
|
|
71
|
+
const P2_OPTION_PARSE_RE = /^\s*([❯>])?\s+(.+)$/;
|
|
72
|
+
// trailing 合法性为「反向」判定:match = 首字符打断 trailing → 该 prompt 不成立。
|
|
73
|
+
// Pattern 1 合法 trailing 行 = 空行或首字符 ∉ {数字,❯,>}
|
|
74
|
+
const P1_TRAILING_BREAK_RE = /^[\d❯>]/;
|
|
75
|
+
// Pattern 2 合法 trailing 行 = 空行或首字符 ∉ {空白,❯,>}
|
|
76
|
+
const P2_TRAILING_BREAK_RE = /^[\s❯>]/;
|
|
77
|
+
|
|
78
|
+
// overrun 监测阈值:超过仅告警 + 计数(无熔断、无降级——线性实现 4KB 封顶
|
|
79
|
+
// 实践上不会触发,留作 Windows 实机回报的哨兵指标)
|
|
80
|
+
const OVERRUN_MS = 50;
|
|
81
|
+
|
|
82
|
+
// 诊断统计(termDiag 接线读取;>OVERRUN_MS 计 overrun)
|
|
83
|
+
const _stats = { calls: 0, lastMs: 0, maxMs: 0, overruns: 0 };
|
|
84
|
+
export function getPromptDetectStats() {
|
|
85
|
+
return { ..._stats };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function parseP1Option(line) {
|
|
89
|
+
const m = line.match(P1_OPTION_RE);
|
|
90
|
+
if (!m) return null;
|
|
91
|
+
return { number: parseInt(m[2], 10), text: m[3].trim(), selected: !!m[1] };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Pattern 1:编号选项 — "Question?\n ❯ 1. Option A\n 2. Option B"
|
|
96
|
+
* 允许尾随空行与 hint 行(如 "\n\nEsc to cancel · Tab to amend")。
|
|
97
|
+
*/
|
|
98
|
+
function detectPattern1(lines) {
|
|
99
|
+
for (let i = 0; i < lines.length - 1; i++) {
|
|
100
|
+
const q = lines[i].trimEnd();
|
|
101
|
+
if (!q.endsWith('?')) continue;
|
|
102
|
+
// 问句行下方:跳过空行后须紧跟编号选项块(块内允许夹空行)
|
|
103
|
+
const options = [];
|
|
104
|
+
let j = i + 1;
|
|
105
|
+
let lastOptionEnd = j; // 选项块后第一行(trailing 起点)
|
|
106
|
+
for (; j < lines.length; j++) {
|
|
107
|
+
const line = lines[j];
|
|
108
|
+
if (!line.trim()) continue; // 空行:块前/块内均容忍
|
|
109
|
+
const opt = parseP1Option(line);
|
|
110
|
+
if (!opt) break; // 非选项非空行:块结束
|
|
111
|
+
options.push(opt);
|
|
112
|
+
lastOptionEnd = j + 1;
|
|
113
|
+
}
|
|
114
|
+
if (options.length < 2) continue;
|
|
115
|
+
// 块后全部行须为 trailing(空行或首字符 ∉ {数字,❯,>}),否则该问句不成立
|
|
116
|
+
let trailingOk = true;
|
|
117
|
+
for (let k = lastOptionEnd; k < lines.length; k++) {
|
|
118
|
+
if (P1_TRAILING_BREAK_RE.test(lines[k])) { trailingOk = false; break; }
|
|
119
|
+
}
|
|
120
|
+
if (!trailingOk) continue;
|
|
121
|
+
return { question: q.trim(), options };
|
|
122
|
+
}
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Pattern 2:无编号光标选项(Ink Select)— "Some prompt text\n ❯ Allow once\n Deny"
|
|
128
|
+
* 问句行可不带 "?";须 ≥2 项且含 ❯/> 选中项。与旧实现一致:结构首次匹配后
|
|
129
|
+
* 验证(项数/选中)失败即整体放弃,不向后重试。
|
|
130
|
+
*/
|
|
131
|
+
function detectPattern2(lines) {
|
|
132
|
+
for (let i = 0; i < lines.length - 1; i++) {
|
|
133
|
+
if (lines[i].length === 0) continue; // question 须非空行(与旧 [^\n]+ 一致,纯空白行也算)
|
|
134
|
+
// 块成员:两段空白结构(P2_MEMBER_RE,与 legacy 块正则同构);块内允许夹空行
|
|
135
|
+
const memberLines = [];
|
|
136
|
+
let j = i + 1;
|
|
137
|
+
let lastOptionEnd = j;
|
|
138
|
+
for (; j < lines.length; j++) {
|
|
139
|
+
const line = lines[j];
|
|
140
|
+
if (!line.trim()) continue;
|
|
141
|
+
if (!P2_MEMBER_RE.test(line)) break;
|
|
142
|
+
memberLines.push(line);
|
|
143
|
+
lastOptionEnd = j + 1;
|
|
144
|
+
}
|
|
145
|
+
if (memberLines.length < 2) continue;
|
|
146
|
+
let trailingOk = true;
|
|
147
|
+
for (let k = lastOptionEnd; k < lines.length; k++) {
|
|
148
|
+
if (P2_TRAILING_BREAK_RE.test(lines[k])) { trailingOk = false; break; }
|
|
149
|
+
}
|
|
150
|
+
if (!trailingOk) continue;
|
|
151
|
+
// 结构成立(leftmost)→ 解析验证;失败则与旧实现一致整体放弃
|
|
152
|
+
const parsed = [];
|
|
153
|
+
for (const line of memberLines) {
|
|
154
|
+
const m = line.match(P2_OPTION_PARSE_RE);
|
|
155
|
+
if (m && m[2].trim()) {
|
|
156
|
+
parsed.push({ number: parsed.length + 1, text: m[2].trim(), selected: !!m[1] });
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (parsed.length >= 2 && parsed.some(p => p.selected)) {
|
|
160
|
+
const question = lines[i].trim();
|
|
161
|
+
// 纯空白问句行:与旧实现等价——结构成立但 question 为空即整体放弃(不向后重试)
|
|
162
|
+
if (!question) return null;
|
|
163
|
+
return { question, options: parsed };
|
|
164
|
+
}
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* 检测 PTY buffer 尾部是否呈现交互式 prompt。
|
|
172
|
+
* @param {string} buf - ANSI-stripped PTY buffer(≤4KB)
|
|
173
|
+
* @returns {{question: string, options: Array<{number:number,text:string,selected:boolean}>} | null}
|
|
174
|
+
* 不含 false-positive 过滤(调用方按需用 isFalsePositiveQuestion,
|
|
175
|
+
* ChatView 对「过滤」与「未检出」有不同的 dismiss 语义)。
|
|
176
|
+
*/
|
|
177
|
+
export function detectPromptInBuffer(buf) {
|
|
178
|
+
const t0 = now();
|
|
179
|
+
let result = null;
|
|
180
|
+
const trimmed = (buf || '').trimEnd();
|
|
181
|
+
if (trimmed) {
|
|
182
|
+
// /\r?\n/ 分行吃掉 CRLF 行尾的 \r(Windows ConPTY),否则单行正则的
|
|
183
|
+
// `.`/`$` 均不容忍 \r → 选项行整行失配 → 目标平台全量漏检
|
|
184
|
+
const lines = trimmed.split(/\r?\n/);
|
|
185
|
+
result = detectPattern1(lines) || detectPattern2(lines);
|
|
186
|
+
}
|
|
187
|
+
const ms = now() - t0;
|
|
188
|
+
_stats.calls++;
|
|
189
|
+
_stats.lastMs = ms;
|
|
190
|
+
if (ms > _stats.maxMs) _stats.maxMs = ms;
|
|
191
|
+
if (ms > OVERRUN_MS) {
|
|
192
|
+
_stats.overruns++;
|
|
193
|
+
try { console.warn(`[cc-viewer] promptDetect overrun: ${ms.toFixed(1)}ms (buf ${trimmed.length}B)`); } catch {}
|
|
194
|
+
}
|
|
195
|
+
return result;
|
|
196
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { getSlashCommandLabel } from './slashCommandLabels.js';
|
|
2
|
+
|
|
3
|
+
// buildPromptNavItems:从「当前可见项」与权威的 mainAgentSessions 计算用户 Prompt 导航的数据项。
|
|
4
|
+
// 纯函数(无 React/DOM),渲染留在 ChatView——便于单测覆盖会话边界标记、去重与无 ts 容错。
|
|
5
|
+
//
|
|
6
|
+
// @param {Array<{props?:{role?:string,text?:string,timestamp?:string|null}}>} visible 当前渲染项
|
|
7
|
+
// @param {Array<{messages?:Array<{_timestamp?:string}>}>} mainAgentSessions 权威会话数组
|
|
8
|
+
// @returns {Array<{display:string, visibleIdx:number, timestamp:string|null, sessionIdx:number|null, newSession?:boolean}>}
|
|
9
|
+
export function buildPromptNavItems(visible, mainAgentSessions) {
|
|
10
|
+
if (!Array.isArray(visible) || visible.length === 0) return [];
|
|
11
|
+
|
|
12
|
+
// 会话分界:用权威的 mainAgentSessions 把每条 prompt 的 _timestamp 映射到所属 session 序号。
|
|
13
|
+
// 不依赖主视图的 <Divider>(其在角色过滤时会被滤掉),保证导航里始终能标出会话边界。
|
|
14
|
+
const sessions = mainAgentSessions || [];
|
|
15
|
+
const tsToSession = new Map();
|
|
16
|
+
for (let si = 0; si < sessions.length; si++) {
|
|
17
|
+
const msgs = sessions[si] && sessions[si].messages;
|
|
18
|
+
if (!Array.isArray(msgs)) continue;
|
|
19
|
+
for (const m of msgs) {
|
|
20
|
+
const ts = m && m._timestamp;
|
|
21
|
+
if (ts != null && !tsToSession.has(ts)) tsToSession.set(ts, si);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const prompts = [];
|
|
26
|
+
const seen = new Set();
|
|
27
|
+
for (let i = 0; i < visible.length; i++) {
|
|
28
|
+
const props = visible[i] && visible[i].props;
|
|
29
|
+
if (!props || props.role !== 'user') continue;
|
|
30
|
+
const raw = props.text || '';
|
|
31
|
+
if (!raw) continue;
|
|
32
|
+
// 清理图片标记,只保留文字部分用于导航列表显示
|
|
33
|
+
const cleaned = raw
|
|
34
|
+
.replace(/\[Image(?:\s*#\d+)?(?::?\s*source)?:\s*[^\]]+\]/gi, '')
|
|
35
|
+
.replace(/"\/tmp\/cc-viewer-uploads\/[^"]+"/g, '')
|
|
36
|
+
.trim();
|
|
37
|
+
if (!cleaned) continue;
|
|
38
|
+
// 内置 slash 命令(/theme /clear …)在 nav 列表里也显示本地化标签,与主气泡保持一致;
|
|
39
|
+
// 未命中白名单的命令/普通文本走原文。
|
|
40
|
+
const text = getSlashCommandLabel(cleaned) || cleaned;
|
|
41
|
+
const key = text.substring(0, 100);
|
|
42
|
+
if (seen.has(key)) continue;
|
|
43
|
+
seen.add(key);
|
|
44
|
+
const display = text.length > 80 ? text.substring(0, 80) + '...' : text;
|
|
45
|
+
// 使用 visible 索引作为定位标识(兼容无 timestamp 的遗留消息)
|
|
46
|
+
const sessionIdx = (props.timestamp != null && tsToSession.has(props.timestamp))
|
|
47
|
+
? tsToSession.get(props.timestamp) : null;
|
|
48
|
+
prompts.push({ display, visibleIdx: i, timestamp: props.timestamp || null, sessionIdx });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 标记跨 session 的 prompt(其前插入会话分隔线)。session 未知(无 ts)的 prompt 不打断链路。
|
|
52
|
+
let lastSessionIdx = null;
|
|
53
|
+
for (const p of prompts) {
|
|
54
|
+
if (p.sessionIdx == null) continue;
|
|
55
|
+
if (lastSessionIdx != null && p.sessionIdx !== lastSessionIdx) p.newSession = true;
|
|
56
|
+
lastSessionIdx = p.sessionIdx;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return prompts;
|
|
60
|
+
}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PTY Chunk Builder — pure functions for building keystroke sequences.
|
|
3
|
+
* Separates "what to send" from "how to send".
|
|
4
|
+
*
|
|
5
|
+
* Claude Code AskUserQuestion prompt model:
|
|
6
|
+
* - Single question: options list, Enter selects and submits
|
|
7
|
+
* - Multi question: tabbed form [Q1] [Q2] ... [Submit]
|
|
8
|
+
* - Single select: ↓↓...Enter (selects and auto-advances to next tab)
|
|
9
|
+
* - Multi select: ↓Space↓Space...→ (toggles then → to next tab)
|
|
10
|
+
* - Last tab → to Submit, Enter to confirm
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const ARROW_DOWN = '\x1b[B';
|
|
14
|
+
const ARROW_UP = '\x1b[A';
|
|
15
|
+
const ARROW_RIGHT = '\x1b[C';
|
|
16
|
+
const SPACE = ' ';
|
|
17
|
+
const ENTER = '\r';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Build navigation arrows from currentIdx to targetIdx.
|
|
21
|
+
*/
|
|
22
|
+
function buildArrows(currentIdx, targetIdx) {
|
|
23
|
+
const chunks = [];
|
|
24
|
+
const diff = targetIdx - currentIdx;
|
|
25
|
+
const arrow = diff > 0 ? ARROW_DOWN : ARROW_UP;
|
|
26
|
+
for (let i = 0; i < Math.abs(diff); i++) {
|
|
27
|
+
chunks.push(arrow);
|
|
28
|
+
}
|
|
29
|
+
return chunks;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Get current cursor position from prompt.
|
|
34
|
+
*/
|
|
35
|
+
function getCursorIdx(prompt) {
|
|
36
|
+
if (prompt && prompt.options) {
|
|
37
|
+
const idx = prompt.options.findIndex(o => o.selected);
|
|
38
|
+
return idx >= 0 ? idx : 0;
|
|
39
|
+
}
|
|
40
|
+
return 0;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Build chunks for a single-select answer.
|
|
45
|
+
* Navigate to target option, then Enter.
|
|
46
|
+
* @param {object} answer - { optionIndex, isLast }
|
|
47
|
+
* @param {object} prompt - ptyPrompt with options
|
|
48
|
+
* @param {boolean} isMultiQuestion - whether this is part of a multi-question form
|
|
49
|
+
*/
|
|
50
|
+
export function buildSingleSelectChunks(answer, prompt, isMultiQuestion = false) {
|
|
51
|
+
const chunks = [];
|
|
52
|
+
let currentIdx = getCursorIdx(prompt);
|
|
53
|
+
|
|
54
|
+
// For multi-question, map optionIndex to prompt option by number
|
|
55
|
+
let targetIdx = answer.optionIndex;
|
|
56
|
+
if (prompt && prompt.options) {
|
|
57
|
+
const targetNumber = answer.optionIndex + 1;
|
|
58
|
+
const found = prompt.options.findIndex(o => o.number === targetNumber);
|
|
59
|
+
if (found >= 0) targetIdx = found;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
chunks.push(...buildArrows(currentIdx, targetIdx));
|
|
63
|
+
chunks.push(ENTER); // Select and confirm (auto-advances in multi-question)
|
|
64
|
+
|
|
65
|
+
// Multi-question last question: Enter above advances to Review page,
|
|
66
|
+
// need another Enter to confirm "Submit answers"
|
|
67
|
+
if (isMultiQuestion && answer.isLast) {
|
|
68
|
+
chunks.push(ENTER);
|
|
69
|
+
}
|
|
70
|
+
return chunks;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Build chunks for a multi-select answer.
|
|
75
|
+
* Navigate + Space for each selection, then → to advance tab.
|
|
76
|
+
* @param {object} answer - { selectedIndices, isLast }
|
|
77
|
+
* @param {object} prompt - ptyPrompt with options
|
|
78
|
+
* @param {boolean} isMultiQuestion - whether this is part of a multi-question form
|
|
79
|
+
*/
|
|
80
|
+
export function buildMultiSelectChunks(answer, prompt, isMultiQuestion = false) {
|
|
81
|
+
const chunks = [];
|
|
82
|
+
const indices = (answer.selectedIndices || []).slice().sort((a, b) => a - b);
|
|
83
|
+
let currentIdx = getCursorIdx(prompt);
|
|
84
|
+
|
|
85
|
+
for (const targetIdx of indices) {
|
|
86
|
+
chunks.push(...buildArrows(currentIdx, targetIdx));
|
|
87
|
+
chunks.push(SPACE); // Toggle
|
|
88
|
+
currentIdx = targetIdx;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// → to advance to next tab (or Submit tab if last)
|
|
92
|
+
chunks.push(ARROW_RIGHT);
|
|
93
|
+
|
|
94
|
+
// Last question in multi-question, or single question: Enter on Submit tab
|
|
95
|
+
if (answer.isLast || !isMultiQuestion) {
|
|
96
|
+
chunks.push(ENTER);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return chunks;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Build chunks for a single-select "Other" (free text) answer.
|
|
104
|
+
*
|
|
105
|
+
* Single-question mode:
|
|
106
|
+
* Navigate to "Type something" → Enter (activate text input) → type text → Enter (confirm)
|
|
107
|
+
*
|
|
108
|
+
* Multi-question tabbed form:
|
|
109
|
+
* In tabbed forms, Enter on "Type something" auto-advances to the next tab
|
|
110
|
+
* (same as regular single-select). Text input is NOT inline — Claude Code
|
|
111
|
+
* prompts for it separately after the form is submitted.
|
|
112
|
+
* So we treat it like a regular single-select: navigate → Enter.
|
|
113
|
+
*
|
|
114
|
+
* @param {object} answer - { optionIndex, text, isLast }
|
|
115
|
+
* @param {object} prompt - ptyPrompt with options
|
|
116
|
+
* @param {boolean} isMultiQuestion - whether part of multi-question form
|
|
117
|
+
*/
|
|
118
|
+
export function buildOtherChunks(answer, prompt, isMultiQuestion = false) {
|
|
119
|
+
const chunks = [];
|
|
120
|
+
let currentIdx = getCursorIdx(prompt);
|
|
121
|
+
const targetIdx = answer.optionIndex;
|
|
122
|
+
|
|
123
|
+
chunks.push(...buildArrows(currentIdx, targetIdx));
|
|
124
|
+
|
|
125
|
+
// "Type something" 选项:直接输入文字,不需要 Enter 激活
|
|
126
|
+
// inquirer 在光标停在此选项时接受直接键入文字
|
|
127
|
+
const text = answer.text || '';
|
|
128
|
+
for (const ch of text) {
|
|
129
|
+
chunks.push(ch);
|
|
130
|
+
}
|
|
131
|
+
chunks.push(ENTER); // Confirm text and submit
|
|
132
|
+
|
|
133
|
+
// Multi-question last question: Enter above auto-advances to Submit tab,
|
|
134
|
+
// need another Enter to confirm
|
|
135
|
+
if (isMultiQuestion && answer.isLast) {
|
|
136
|
+
chunks.push(ENTER);
|
|
137
|
+
}
|
|
138
|
+
return chunks;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Build chunks for a multi-select "Other" (Type something) answer.
|
|
143
|
+
* "Type something" is a text-input option in the checkbox list.
|
|
144
|
+
* Sequence: navigate → type text + sacrifice_char → → (settle) → ↑ (exit, drops sacrifice_char) → → (next tab) → Enter (if last)
|
|
145
|
+
* inquirer drops the last char on ↓/↑ exit; sacrifice char absorbs the loss.
|
|
146
|
+
* Uses ↑ instead of ↓ to exit: "Type something" is the last option, so ↓ keeps cursor there
|
|
147
|
+
* and → may re-enter text mode. ↑ moves to a non-text option, making → reliably navigate tabs.
|
|
148
|
+
* @param {object} answer - { optionIndex, text, isLast }
|
|
149
|
+
* @param {object} prompt - ptyPrompt with options
|
|
150
|
+
* @param {boolean} isMultiQuestion - whether part of multi-question form
|
|
151
|
+
*/
|
|
152
|
+
export function buildMultiSelectOtherChunks(answer, prompt, isMultiQuestion = false) {
|
|
153
|
+
const chunks = [];
|
|
154
|
+
let currentIdx = getCursorIdx(prompt);
|
|
155
|
+
const targetIdx = answer.optionIndex;
|
|
156
|
+
|
|
157
|
+
chunks.push(...buildArrows(currentIdx, targetIdx));
|
|
158
|
+
|
|
159
|
+
// Type text directly into the "Type something" field
|
|
160
|
+
// Typing auto-checks the checkbox — no Space/Enter needed
|
|
161
|
+
const text = answer.text || '';
|
|
162
|
+
for (const ch of text) {
|
|
163
|
+
chunks.push(ch);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Sacrifice char: ↑/↓ drops exactly one character when exiting text input mode.
|
|
167
|
+
// Append ONE duplicate of the last char so the exit key drops the sacrifice, not the real text.
|
|
168
|
+
if (text.length > 0) {
|
|
169
|
+
const chars = [...text]; // handle multi-byte (e.g. CJK) correctly
|
|
170
|
+
chunks.push(chars[chars.length - 1]); // sacrifice for ↑
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// → in text input mode: no-op (cursor already at end), provides settleMs delay
|
|
174
|
+
chunks.push(ARROW_RIGHT);
|
|
175
|
+
// ↑ exits text input mode — drops one char (the sacrifice above), real text preserved.
|
|
176
|
+
// Use ↑ instead of ↓: "Type something" is typically the last option, so ↓ may leave
|
|
177
|
+
// cursor on the same item (no option below). ↑ moves to the previous *non-text* option,
|
|
178
|
+
// ensuring the subsequent → is interpreted as tab navigation, not text cursor movement.
|
|
179
|
+
chunks.push(ARROW_UP);
|
|
180
|
+
// → goes to next tab (Submit tab for last/single question, next question tab otherwise)
|
|
181
|
+
chunks.push(ARROW_RIGHT);
|
|
182
|
+
|
|
183
|
+
// Enter on Submit/Review page to confirm — only for last question or single-question form
|
|
184
|
+
if (answer.isLast || !isMultiQuestion) {
|
|
185
|
+
chunks.push(ENTER);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return chunks;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Build chunks for a single answer (dispatches by type).
|
|
193
|
+
* @param {object} answer - { type, optionIndex, selectedIndices, text, isLast, isMultiSelect }
|
|
194
|
+
* @param {object} prompt - ptyPrompt with options
|
|
195
|
+
* @param {boolean} isMultiQuestion - whether part of multi-question form
|
|
196
|
+
*/
|
|
197
|
+
export function buildChunksForAnswer(answer, prompt, isMultiQuestion = false) {
|
|
198
|
+
if (answer.type === 'multi') {
|
|
199
|
+
return buildMultiSelectChunks(answer, prompt, isMultiQuestion);
|
|
200
|
+
}
|
|
201
|
+
if (answer.type === 'other' && answer.isMultiSelect) {
|
|
202
|
+
return buildMultiSelectOtherChunks(answer, prompt, isMultiQuestion);
|
|
203
|
+
}
|
|
204
|
+
if (answer.type === 'other') {
|
|
205
|
+
return buildOtherChunks(answer, prompt, isMultiQuestion);
|
|
206
|
+
}
|
|
207
|
+
return buildSingleSelectChunks(answer, prompt, isMultiQuestion);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* settleMs after a bracket-paste-end chunk. Ink TUI needs ~1 frame to settle
|
|
212
|
+
* paste→normal state; if \r arrives in the same PTY write it gets swallowed.
|
|
213
|
+
*/
|
|
214
|
+
export const BRACKET_PASTE_SUBMIT_SETTLE_MS = 250;
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* 剥离粘贴文本里内嵌的 bracketed-paste end 序列 `\x1b[201~`。
|
|
218
|
+
*
|
|
219
|
+
* 防 paste-injection:xterm.js 6.0 的 bracketed paste 不 sanitize 粘贴内容(上游修复
|
|
220
|
+
* PR #5716 在 7.0),剪贴板里的 `\x1b[201~` 会提前闭合 paste 包裹,余下字节被当作真实
|
|
221
|
+
* 按键/控制序列注入。剥离后再包裹即可堵住(该序列正常文本中不会出现,剥离无副作用)。
|
|
222
|
+
* 同时剥 `\x1b[200~` 以防成对伪造。
|
|
223
|
+
*
|
|
224
|
+
* 循环剥离到稳定:单趟 /g replace 删掉中间一个完整序列后,左右残片会重新相邻拼成
|
|
225
|
+
* 新序列(如 `\x1b[20` + `\x1b[201~` + `1~` → 删中段后 `\x1b[20`+`1~` = `\x1b[201~`),
|
|
226
|
+
* 绕过单趟剥离。循环到不再变化才安全。
|
|
227
|
+
* 正则覆盖 7-bit `\x1b[`(ESC [)与 8-bit C1 CSI `\x9b` 两种 introducer。
|
|
228
|
+
* @param {string} text
|
|
229
|
+
* @returns {string}
|
|
230
|
+
*/
|
|
231
|
+
export function sanitizeBracketPasteText(text) {
|
|
232
|
+
if (typeof text !== 'string') return text;
|
|
233
|
+
const re = /(?:\x1b\[|\x9b)20[01]~/g;
|
|
234
|
+
let prev;
|
|
235
|
+
do {
|
|
236
|
+
prev = text;
|
|
237
|
+
text = text.replace(re, '');
|
|
238
|
+
} while (text !== prev);
|
|
239
|
+
return text;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Wrap content in bracket paste mode and append Enter as a separate chunk,
|
|
244
|
+
* so the consumer can space them with settleMs.
|
|
245
|
+
* @param {string} content - non-empty string to paste-and-submit
|
|
246
|
+
* @returns {string[]} [pasteBlock, '\r']; [] if content is falsy or non-string
|
|
247
|
+
*/
|
|
248
|
+
export function buildBracketPasteSubmitChunks(content) {
|
|
249
|
+
if (typeof content !== 'string' || content.length === 0) return [];
|
|
250
|
+
return [`\x1b[200~${sanitizeBracketPasteText(content)}\x1b[201~`, ENTER];
|
|
251
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// 级联子菜单 hover-intent(终端工具栏与对话输入栏的四芒星快捷菜单共用):
|
|
2
|
+
// 从下排行斜向上滑向其子菜单高处选项时,路径会扫过相邻行,立即切换会抢走展开态
|
|
3
|
+
// 致子菜单中途消失。进入(已有展开行时)/离开都延迟 HOVER_INTENT_MS 提交,扫过的
|
|
4
|
+
// 行来不及生效,路径上的误触发被后续 clearTimeout 撤销;无展开行时进入即时响应不付延迟。
|
|
5
|
+
|
|
6
|
+
export const HOVER_INTENT_MS = 140;
|
|
7
|
+
|
|
8
|
+
// getExpanded 必须现场读宿主最新值(class 读 this.state,hooks 读 latest-value ref):
|
|
9
|
+
// 延迟提交时展开行可能已被后续 enter/点击改写,闭包快照会误收别行的展开态。
|
|
10
|
+
// skip:宿主整体不参与 hover 展开(如 iPad tap 的 synthetic mouseEnter 会与 click 切换互抵);
|
|
11
|
+
// holdOpen:延迟收起提交时刻判定是否保住展开(如 AgentTeam 启用请求进行中保住 loading 反馈)。
|
|
12
|
+
export function createQuickMenuHoverIntent({ getExpanded, setExpanded, skip, holdOpen }) {
|
|
13
|
+
let timer = null;
|
|
14
|
+
return {
|
|
15
|
+
enter(key) {
|
|
16
|
+
if (skip?.()) return;
|
|
17
|
+
clearTimeout(timer);
|
|
18
|
+
const current = getExpanded();
|
|
19
|
+
if (current === key) return;
|
|
20
|
+
if (current === null) setExpanded(key);
|
|
21
|
+
else timer = setTimeout(() => setExpanded(key), HOVER_INTENT_MS);
|
|
22
|
+
},
|
|
23
|
+
leave(key) {
|
|
24
|
+
if (skip?.()) return;
|
|
25
|
+
clearTimeout(timer);
|
|
26
|
+
timer = setTimeout(() => {
|
|
27
|
+
if (holdOpen?.(key)) return;
|
|
28
|
+
if (getExpanded() === key) setExpanded(null);
|
|
29
|
+
}, HOVER_INTENT_MS);
|
|
30
|
+
},
|
|
31
|
+
cancel() {
|
|
32
|
+
clearTimeout(timer);
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// 解析 Anthropic 订阅(coding plan / OAuth)在响应头里下发的统一限流信息
|
|
2
|
+
// (anthropic-ratelimit-unified-* 家族),归一化成前端血条旁「套餐用量」组件可直接用的结构。
|
|
3
|
+
//
|
|
4
|
+
// 纯函数、零依赖:仅做字符串 → 数值的安全转换,便于单元测试,也避免被
|
|
5
|
+
// ESM 后缀缺失的传递依赖污染(参考 readResultPool 测试注释)。
|
|
6
|
+
//
|
|
7
|
+
// 数据来源:cc-viewer 拦截器已把响应头原样写进日志(interceptor.js),前端的
|
|
8
|
+
// request.response.headers 即可直接读到,无需任何服务端改动。
|
|
9
|
+
//
|
|
10
|
+
// 关键事实(基于真实日志校验):
|
|
11
|
+
// - 所有取值都是字符串,例如 utilization="0.19"(0~1 的占比,不是 token 数);
|
|
12
|
+
// - reset 是 unix epoch「秒」,需 ×1000 转毫秒;
|
|
13
|
+
// - key 集合并非固定(个别项目还多出 -fallback 等),解析必须容忍缺失/多余 key。
|
|
14
|
+
|
|
15
|
+
const PREFIX = 'anthropic-ratelimit-unified-';
|
|
16
|
+
|
|
17
|
+
// 安全转数值:非有限值(NaN/Infinity/null/空串)一律返回 null,避免污染显示。
|
|
18
|
+
function toNum(v) {
|
|
19
|
+
if (v == null || v === '') return null;
|
|
20
|
+
const n = Number(v);
|
|
21
|
+
return Number.isFinite(n) ? n : null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// 把 headers 的 key 统一成小写做大小写无关查找(fetch 一般已小写,这里再兜底一次)。
|
|
25
|
+
function lowerKeyMap(headers) {
|
|
26
|
+
const map = {};
|
|
27
|
+
for (const k of Object.keys(headers)) map[k.toLowerCase()] = headers[k];
|
|
28
|
+
return map;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 解析单个窗口(如 '5h' / '7d')。三项全缺则返回 null(该窗口不存在)。
|
|
33
|
+
* @returns {{id:string, utilization:number|null, status:string|null, resetAt:number|null}|null}
|
|
34
|
+
*/
|
|
35
|
+
function parseWindow(map, id) {
|
|
36
|
+
const utilization = toNum(map[`${PREFIX}${id}-utilization`]);
|
|
37
|
+
const resetSec = toNum(map[`${PREFIX}${id}-reset`]);
|
|
38
|
+
const status = map[`${PREFIX}${id}-status`] || null;
|
|
39
|
+
if (utilization == null && resetSec == null && !status) return null;
|
|
40
|
+
return {
|
|
41
|
+
id,
|
|
42
|
+
utilization, // 0~1 占比
|
|
43
|
+
status, // allowed / rejected / queued ...
|
|
44
|
+
resetAt: resetSec != null ? resetSec * 1000 : null, // epoch 秒 → 毫秒
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 解析统一限流响应头。
|
|
50
|
+
* @param {Record<string,string>|null|undefined} headers - request.response.headers
|
|
51
|
+
* @returns {null | {
|
|
52
|
+
* source: 'plan',
|
|
53
|
+
* windows: Array<{id, utilization, status, resetAt}>,
|
|
54
|
+
* overallStatus: string|null,
|
|
55
|
+
* representativeClaim: string|null,
|
|
56
|
+
* overage: { status: string|null, disabledReason: string|null },
|
|
57
|
+
* fallbackPercentage: number|null,
|
|
58
|
+
* }}
|
|
59
|
+
* 无任何 unified header 时返回 null(调用方据此决定不渲染套餐用量)。
|
|
60
|
+
*/
|
|
61
|
+
export function parseRateLimitHeaders(headers) {
|
|
62
|
+
if (!headers || typeof headers !== 'object') return null;
|
|
63
|
+
const map = lowerKeyMap(headers);
|
|
64
|
+
|
|
65
|
+
// 没有任何 unified-* 头 → 不是订阅计划下发的限流信息,直接放弃。
|
|
66
|
+
const hasUnified = Object.keys(map).some((k) => k.startsWith(PREFIX));
|
|
67
|
+
if (!hasUnified) return null;
|
|
68
|
+
|
|
69
|
+
const windows = [parseWindow(map, '5h'), parseWindow(map, '7d')].filter(Boolean);
|
|
70
|
+
if (windows.length === 0) return null;
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
source: 'plan',
|
|
74
|
+
windows,
|
|
75
|
+
overallStatus: map[`${PREFIX}status`] || null,
|
|
76
|
+
representativeClaim: map[`${PREFIX}representative-claim`] || null,
|
|
77
|
+
overage: {
|
|
78
|
+
status: map[`${PREFIX}overage-status`] || null,
|
|
79
|
+
disabledReason: map[`${PREFIX}overage-disabled-reason`] || null,
|
|
80
|
+
},
|
|
81
|
+
fallbackPercentage: toNum(map[`${PREFIX}fallback-percentage`]),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* 从 requests 列表里取「最近一条带统一限流头」的响应,解析成套餐用量。
|
|
87
|
+
* 统一限流头是账号级、随每条 200 响应下发的,所以从后往前找第一条可解析的即可
|
|
88
|
+
* (不依赖 isMainAgent,保持本模块零依赖、可独立单测)。
|
|
89
|
+
* @param {Array} requests
|
|
90
|
+
* @returns {ReturnType<typeof parseRateLimitHeaders>}
|
|
91
|
+
*/
|
|
92
|
+
export function extractLatestPlanUsage(requests) {
|
|
93
|
+
if (!Array.isArray(requests)) return null;
|
|
94
|
+
for (let i = requests.length - 1; i >= 0; i--) {
|
|
95
|
+
const headers = requests[i] && requests[i].response && requests[i].response.headers;
|
|
96
|
+
if (!headers) continue;
|
|
97
|
+
const pu = parseRateLimitHeaders(headers);
|
|
98
|
+
if (pu) return pu;
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* 选出「当前绑定」的代表窗口,用于 pill 的主色与主进度。
|
|
105
|
+
* 规则:representative-claim='five_hour' → 5h;明显的周/天 → 7d;
|
|
106
|
+
* 无法识别时回落到使用率更高的窗口(更接近“离限额最近”的语义)。
|
|
107
|
+
*/
|
|
108
|
+
export function pickHeadlineWindow(planUsage) {
|
|
109
|
+
if (!planUsage || !Array.isArray(planUsage.windows) || planUsage.windows.length === 0) return null;
|
|
110
|
+
const claim = planUsage.representativeClaim;
|
|
111
|
+
let id = null;
|
|
112
|
+
if (claim === 'five_hour') id = '5h';
|
|
113
|
+
else if (claim && /(day|week|7d|seven)/i.test(claim)) id = '7d';
|
|
114
|
+
if (id) {
|
|
115
|
+
const hit = planUsage.windows.find((w) => w.id === id);
|
|
116
|
+
if (hit) return hit;
|
|
117
|
+
}
|
|
118
|
+
return planUsage.windows.reduce((a, b) => ((b.utilization || 0) > (a.utilization || 0) ? b : a));
|
|
119
|
+
}
|