@tom2012/cc-web 2026.5.19-b → 2026.5.20-b
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 +1 -1
- package/backend/dist/__tests__/cli-prompt-detector.test.d.ts +2 -0
- package/backend/dist/__tests__/cli-prompt-detector.test.d.ts.map +1 -0
- package/backend/dist/__tests__/cli-prompt-detector.test.js +181 -0
- package/backend/dist/__tests__/cli-prompt-detector.test.js.map +1 -0
- package/backend/dist/cli-prompt-detector.d.ts +92 -0
- package/backend/dist/cli-prompt-detector.d.ts.map +1 -0
- package/backend/dist/cli-prompt-detector.js +227 -0
- package/backend/dist/cli-prompt-detector.js.map +1 -0
- package/backend/dist/index.d.ts.map +1 -1
- package/backend/dist/index.js +9 -0
- package/backend/dist/index.js.map +1 -1
- package/backend/dist/routes/projects.d.ts.map +1 -1
- package/backend/dist/routes/projects.js +85 -0
- package/backend/dist/routes/projects.js.map +1 -1
- package/backend/dist/terminal-manager.d.ts.map +1 -1
- package/backend/dist/terminal-manager.js +9 -0
- package/backend/dist/terminal-manager.js.map +1 -1
- package/frontend/dist/assets/{ChatOverlay-CCrB-zxT.js → ChatOverlay-C6C_KmoQ.js} +3 -3
- package/frontend/dist/assets/{GraphPreview-COD5wr7F.js → GraphPreview-3Jt6HVoR.js} +1 -1
- package/frontend/dist/assets/MobilePage-Ccfw-PQN.js +14 -0
- package/frontend/dist/assets/{OfficePreview-BSL5dPUM.js → OfficePreview-CcNfdvDk.js} +2 -2
- package/frontend/dist/assets/{PdfPreview-B8c4Ad75.js → PdfPreview-C_TVMGcZ.js} +1 -1
- package/frontend/dist/assets/{ProjectPage-BxmgnsQT.js → ProjectPage-DdDicE9q.js} +3 -3
- package/frontend/dist/assets/{SettingsPage-B9Eay6TM.js → SettingsPage-GCr7_tnl.js} +1 -1
- package/frontend/dist/assets/{SkillHubPage-MSEv2owz.js → SkillHubPage-qYjj_wYZ.js} +1 -1
- package/frontend/dist/assets/{chevron-down-CCqkGk_8.js → chevron-down-UeSwXtvj.js} +1 -1
- package/frontend/dist/assets/index-B55CtERr.css +1 -0
- package/frontend/dist/assets/{index-DaglaYz2.js → index-Bf5BlYFK.js} +4 -4
- package/frontend/dist/assets/{index-ChSsEM4z.js → index-BoEETvjx.js} +1 -1
- package/frontend/dist/assets/{index-C6zMjhVt.js → index-e0TiJwv-.js} +1 -1
- package/frontend/dist/assets/{jszip.min-AZrJBqFy.js → jszip.min-PNrxBYIs.js} +1 -1
- package/frontend/dist/assets/{search-BloCxLo6.js → search-DY3wMq7s.js} +1 -1
- package/frontend/dist/assets/{select-D0gOR1E7.js → select-CYI-GI6f.js} +1 -1
- package/frontend/dist/index.html +2 -2
- package/package.json +1 -1
- package/frontend/dist/assets/MobilePage-C-aiv-6i.js +0 -14
- package/frontend/dist/assets/index-D7mI92Gj.css +0 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
A self-hosted web application (distributed as npm package) that provides a browser-based interface for [Claude Code](https://docs.anthropic.com/en/docs/claude-code) CLI sessions. Create projects, each with a persistent terminal running Claude Code, and interact with them through a real-time terminal UI.
|
|
4
4
|
|
|
5
|
-
**Current version**: v2026.5.
|
|
5
|
+
**Current version**: v2026.5.20-b | [GitHub](https://github.com/zbc0315/cc-web) | MIT License
|
|
6
6
|
|
|
7
7
|
## Features
|
|
8
8
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli-prompt-detector.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/cli-prompt-detector.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const vitest_1 = require("vitest");
|
|
4
|
+
const cli_prompt_detector_1 = require("../cli-prompt-detector");
|
|
5
|
+
function collect() {
|
|
6
|
+
const events = [];
|
|
7
|
+
const listener = (e) => events.push(e);
|
|
8
|
+
cli_prompt_detector_1.cliPromptDetector.on('event', listener);
|
|
9
|
+
return { events, unsubscribe: () => cli_prompt_detector_1.cliPromptDetector.off('event', listener) };
|
|
10
|
+
}
|
|
11
|
+
const FULL_MENU = [
|
|
12
|
+
' This session is 5d 5h old and 267.4k tokens.',
|
|
13
|
+
'',
|
|
14
|
+
' Resuming the full session will consume...',
|
|
15
|
+
'',
|
|
16
|
+
' ❯ 1. Resume from summary (recommended)',
|
|
17
|
+
' 2. Resume full session as-is',
|
|
18
|
+
" 3. Don't ask me again",
|
|
19
|
+
].join('\n');
|
|
20
|
+
(0, vitest_1.describe)('parseOptions', () => {
|
|
21
|
+
(0, vitest_1.it)('提取三个 numbered options 含 label + recommended 标志', () => {
|
|
22
|
+
const opts = (0, cli_prompt_detector_1.parseOptions)(FULL_MENU);
|
|
23
|
+
(0, vitest_1.expect)(opts).toEqual([
|
|
24
|
+
{ digit: 1, label: 'Resume from summary', recommended: true },
|
|
25
|
+
{ digit: 2, label: 'Resume full session as-is', recommended: false },
|
|
26
|
+
{ digit: 3, label: "Don't ask me again", recommended: false },
|
|
27
|
+
]);
|
|
28
|
+
});
|
|
29
|
+
(0, vitest_1.it)('"(recommended)" 后缀也算 recommended', () => {
|
|
30
|
+
const opts = (0, cli_prompt_detector_1.parseOptions)(' 1. Foo (recommended)\n 2. Bar');
|
|
31
|
+
(0, vitest_1.expect)(opts[0]?.recommended).toBe(true);
|
|
32
|
+
(0, vitest_1.expect)(opts[0]?.label).toBe('Foo');
|
|
33
|
+
(0, vitest_1.expect)(opts[1]?.recommended).toBe(false);
|
|
34
|
+
});
|
|
35
|
+
(0, vitest_1.it)('重复同 digit 取最新(Ink 重绘场景)', () => {
|
|
36
|
+
const text = ' 1. First version\n 2. B\n--- redraw ---\n 1. Updated\n 2. B';
|
|
37
|
+
const opts = (0, cli_prompt_detector_1.parseOptions)(text);
|
|
38
|
+
(0, vitest_1.expect)(opts.find((o) => o.digit === 1)?.label).toBe('Updated');
|
|
39
|
+
});
|
|
40
|
+
(0, vitest_1.it)('option 数字按 digit 排序而非出现顺序', () => {
|
|
41
|
+
const text = ' 3. Third\n 1. First\n 2. Second';
|
|
42
|
+
(0, vitest_1.expect)((0, cli_prompt_detector_1.parseOptions)(text).map((o) => o.digit)).toEqual([1, 2, 3]);
|
|
43
|
+
});
|
|
44
|
+
(0, vitest_1.it)('无 digit 行不被识别', () => {
|
|
45
|
+
const text = 'Just some prose. And "1." in the middle of a sentence is fine.';
|
|
46
|
+
(0, vitest_1.expect)((0, cli_prompt_detector_1.parseOptions)(text)).toEqual([]);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
(0, vitest_1.describe)('cliPromptDetector', () => {
|
|
50
|
+
(0, vitest_1.beforeEach)(() => {
|
|
51
|
+
for (const pid of ['p1', 'p2', 'p_ansi', 'p_split', 'p_idem', 'p_reset', 'p_no_opts', 'p_update']) {
|
|
52
|
+
cli_prompt_detector_1.cliPromptDetector.reset(pid);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
(0, vitest_1.it)('三短语 + ≥2 options → detected emit 含 options 数组', () => {
|
|
56
|
+
const { events } = collect();
|
|
57
|
+
cli_prompt_detector_1.cliPromptDetector.feed('p1', FULL_MENU);
|
|
58
|
+
const detected = events.filter((e) => e.projectId === 'p1' && e.type === 'cli_prompt_detected');
|
|
59
|
+
(0, vitest_1.expect)(detected).toHaveLength(1);
|
|
60
|
+
if (detected[0]?.type === 'cli_prompt_detected') {
|
|
61
|
+
(0, vitest_1.expect)(detected[0].options).toHaveLength(3);
|
|
62
|
+
(0, vitest_1.expect)(detected[0].options[0]?.label).toBe('Resume from summary');
|
|
63
|
+
(0, vitest_1.expect)(detected[0].options[0]?.recommended).toBe(true);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
(0, vitest_1.it)('三短语满足但 options 解析不到(CLI 改格式) → 不 emit detected', () => {
|
|
67
|
+
const { events } = collect();
|
|
68
|
+
// 三短语在文本里,但没有 "<digit>. label" 行(CLI 假设改为别的渲染)
|
|
69
|
+
const broken = 'Resume from summary?\nResume full session?\nDon\'t ask me again? [Y/n]';
|
|
70
|
+
cli_prompt_detector_1.cliPromptDetector.feed('p_no_opts', broken);
|
|
71
|
+
(0, vitest_1.expect)(events.filter((e) => e.projectId === 'p_no_opts')).toHaveLength(0);
|
|
72
|
+
});
|
|
73
|
+
(0, vitest_1.it)('缺任一关键短语 → 不 detect', () => {
|
|
74
|
+
const { events } = collect();
|
|
75
|
+
cli_prompt_detector_1.cliPromptDetector.feed('p1', ' 1. Resume from summary\n 2. Resume full session\n');
|
|
76
|
+
(0, vitest_1.expect)(events.filter((e) => e.projectId === 'p1')).toHaveLength(0);
|
|
77
|
+
});
|
|
78
|
+
(0, vitest_1.it)('ANSI 转义码被 strip 后仍能匹配 + 解析 options', () => {
|
|
79
|
+
const { events } = collect();
|
|
80
|
+
const ansi = `\x1b[2K\x1b[36m❯ 1.\x1b[0m Resume from summary (recommended)\n 2. Resume full session as-is\n 3. Don't ask me again`;
|
|
81
|
+
cli_prompt_detector_1.cliPromptDetector.feed('p_ansi', ansi);
|
|
82
|
+
const detected = events.filter((e) => e.projectId === 'p_ansi' && e.type === 'cli_prompt_detected');
|
|
83
|
+
(0, vitest_1.expect)(detected).toHaveLength(1);
|
|
84
|
+
if (detected[0]?.type === 'cli_prompt_detected') {
|
|
85
|
+
(0, vitest_1.expect)(detected[0].options).toHaveLength(3);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
(0, vitest_1.it)('v-20-b 根因:Ink TUI 用 CHA `\\x1b[<n>G` 替代字面空格,正确还原', () => {
|
|
89
|
+
const { events } = collect();
|
|
90
|
+
// Real Ink TUI output captured from Claude CLI 2.1.144 PTY (see investigation notes).
|
|
91
|
+
// 每个单词间用 CHA 序列定位列号,没有任何字面空格字符 —— 旧 stripAnsi 会得到
|
|
92
|
+
// "Resumefromsummary" 永远匹配不到 fingerprint。
|
|
93
|
+
const inkReal = '\x1b[3G\x1b[38;2;87;105;247m❯\x1b[5G\x1b[38;2;102;102;102m1.\x1b[8G' +
|
|
94
|
+
'\x1b[38;2;87;105;247mResume\x1b[15Gfrom\x1b[20Gsummary\x1b[28G(recommended)\x1b[39m\r\r\n' +
|
|
95
|
+
'\x1b[5G\x1b[38;2;102;102;102m2.\x1b[8G\x1b[39mResume\x1b[15Gfull\x1b[20Gsession\x1b[28Gas-is\r\r\n' +
|
|
96
|
+
"\x1b[5G\x1b[38;2;102;102;102m3.\x1b[8G\x1b[39mDon't\x1b[14Gask\x1b[18Gme\x1b[21Gagain\r\r\n";
|
|
97
|
+
cli_prompt_detector_1.cliPromptDetector.feed('p_ink', inkReal);
|
|
98
|
+
const detected = events.filter((e) => e.projectId === 'p_ink' && e.type === 'cli_prompt_detected');
|
|
99
|
+
(0, vitest_1.expect)(detected).toHaveLength(1);
|
|
100
|
+
if (detected[0]?.type === 'cli_prompt_detected') {
|
|
101
|
+
(0, vitest_1.expect)(detected[0].options).toEqual([
|
|
102
|
+
{ digit: 1, label: 'Resume from summary', recommended: true },
|
|
103
|
+
{ digit: 2, label: 'Resume full session as-is', recommended: false },
|
|
104
|
+
{ digit: 3, label: "Don't ask me again", recommended: false },
|
|
105
|
+
]);
|
|
106
|
+
}
|
|
107
|
+
cli_prompt_detector_1.cliPromptDetector.reset('p_ink');
|
|
108
|
+
});
|
|
109
|
+
(0, vitest_1.it)('CUF `\\x1b[<n>C` 替换为 n 个空格', () => {
|
|
110
|
+
const { events } = collect();
|
|
111
|
+
// Hypothetical CLI using CUF instead of CHA to advance the cursor.
|
|
112
|
+
// The detector should still see well-spaced text after strip.
|
|
113
|
+
const cuf = "Resume\x1b[1Cfrom\x1b[1Csummary\nResume\x1b[1Cfull\x1b[1Csession\n 1. Resume from summary\n 2. Resume full session\n 3. Don't ask me again";
|
|
114
|
+
cli_prompt_detector_1.cliPromptDetector.feed('p_cuf', cuf);
|
|
115
|
+
const detected = events.filter((e) => e.projectId === 'p_cuf' && e.type === 'cli_prompt_detected');
|
|
116
|
+
(0, vitest_1.expect)(detected.length).toBeGreaterThanOrEqual(1);
|
|
117
|
+
cli_prompt_detector_1.cliPromptDetector.reset('p_cuf');
|
|
118
|
+
});
|
|
119
|
+
(0, vitest_1.it)('已 detected 状态下 options 未变 → 不重复 emit(debounced)', () => {
|
|
120
|
+
const { events } = collect();
|
|
121
|
+
cli_prompt_detector_1.cliPromptDetector.feed('p_idem', FULL_MENU);
|
|
122
|
+
cli_prompt_detector_1.cliPromptDetector.feed('p_idem', FULL_MENU);
|
|
123
|
+
cli_prompt_detector_1.cliPromptDetector.feed('p_idem', FULL_MENU);
|
|
124
|
+
const detected = events.filter((e) => e.projectId === 'p_idem' && e.type === 'cli_prompt_detected');
|
|
125
|
+
(0, vitest_1.expect)(detected).toHaveLength(1);
|
|
126
|
+
});
|
|
127
|
+
(0, vitest_1.it)('已 detected 状态下 options 变了(Ink ↑↓ 移 highlight)→ 重发 detected 更新 options', () => {
|
|
128
|
+
const { events } = collect();
|
|
129
|
+
cli_prompt_detector_1.cliPromptDetector.feed('p_update', FULL_MENU);
|
|
130
|
+
// 用户按 ↓ 移 highlight 到 #2,Ink 重绘整段
|
|
131
|
+
const after = [
|
|
132
|
+
' This session is 5d 5h old and 267.4k tokens.',
|
|
133
|
+
' Resuming the full session will consume...',
|
|
134
|
+
' 1. Resume from summary',
|
|
135
|
+
' ❯ 2. Resume full session as-is',
|
|
136
|
+
" 3. Don't ask me again",
|
|
137
|
+
].join('\n');
|
|
138
|
+
cli_prompt_detector_1.cliPromptDetector.feed('p_update', after);
|
|
139
|
+
const detected = events.filter((e) => e.projectId === 'p_update' && e.type === 'cli_prompt_detected');
|
|
140
|
+
(0, vitest_1.expect)(detected.length).toBeGreaterThanOrEqual(2);
|
|
141
|
+
const last = detected[detected.length - 1];
|
|
142
|
+
if (last?.type === 'cli_prompt_detected') {
|
|
143
|
+
(0, vitest_1.expect)(last.options[1]?.recommended).toBe(true);
|
|
144
|
+
(0, vitest_1.expect)(last.options[0]?.recommended).toBe(false);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
(0, vitest_1.it)('detected 后 buffer 滚出关键词 → emit dismissed', () => {
|
|
148
|
+
const { events } = collect();
|
|
149
|
+
cli_prompt_detector_1.cliPromptDetector.feed('p1', FULL_MENU);
|
|
150
|
+
const noise = 'x'.repeat(10 * 1024);
|
|
151
|
+
cli_prompt_detector_1.cliPromptDetector.feed('p1', noise);
|
|
152
|
+
const ev = events.filter((e) => e.projectId === 'p1');
|
|
153
|
+
(0, vitest_1.expect)(ev.map((e) => e.type)).toEqual(['cli_prompt_detected', 'cli_prompt_dismissed']);
|
|
154
|
+
});
|
|
155
|
+
(0, vitest_1.it)('多 project 状态隔离', () => {
|
|
156
|
+
const { events } = collect();
|
|
157
|
+
cli_prompt_detector_1.cliPromptDetector.feed('p1', FULL_MENU);
|
|
158
|
+
cli_prompt_detector_1.cliPromptDetector.feed('p2', 'unrelated output');
|
|
159
|
+
(0, vitest_1.expect)(events.filter((e) => e.projectId === 'p1' && e.type === 'cli_prompt_detected')).toHaveLength(1);
|
|
160
|
+
(0, vitest_1.expect)(events.filter((e) => e.projectId === 'p2')).toHaveLength(0);
|
|
161
|
+
});
|
|
162
|
+
(0, vitest_1.it)('reset 在 active 状态下触发 dismissed', () => {
|
|
163
|
+
const { events } = collect();
|
|
164
|
+
cli_prompt_detector_1.cliPromptDetector.feed('p_reset', FULL_MENU);
|
|
165
|
+
cli_prompt_detector_1.cliPromptDetector.reset('p_reset');
|
|
166
|
+
const ev = events.filter((e) => e.projectId === 'p_reset');
|
|
167
|
+
(0, vitest_1.expect)(ev.map((e) => e.type)).toEqual(['cli_prompt_detected', 'cli_prompt_dismissed']);
|
|
168
|
+
});
|
|
169
|
+
(0, vitest_1.it)('reset 在无 active 状态下不发 dismissed', () => {
|
|
170
|
+
const { events } = collect();
|
|
171
|
+
cli_prompt_detector_1.cliPromptDetector.reset('p1');
|
|
172
|
+
(0, vitest_1.expect)(events.filter((e) => e.projectId === 'p1')).toHaveLength(0);
|
|
173
|
+
});
|
|
174
|
+
(0, vitest_1.it)('getActive 返回当前 options 快照(REST 同步用)', () => {
|
|
175
|
+
cli_prompt_detector_1.cliPromptDetector.feed('p1', FULL_MENU);
|
|
176
|
+
const active = cli_prompt_detector_1.cliPromptDetector.getActive('p1');
|
|
177
|
+
(0, vitest_1.expect)(active?.options).toHaveLength(3);
|
|
178
|
+
(0, vitest_1.expect)(active?.options[0]?.label).toBe('Resume from summary');
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
//# sourceMappingURL=cli-prompt-detector.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli-prompt-detector.test.js","sourceRoot":"","sources":["../../src/__tests__/cli-prompt-detector.test.ts"],"names":[],"mappings":";;AAAA,mCAAyD;AACzD,gEAA6F;AAE7F,SAAS,OAAO;IACd,MAAM,MAAM,GAAqB,EAAE,CAAA;IACnC,MAAM,QAAQ,GAAG,CAAC,CAAiB,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IACtD,uCAAiB,CAAC,EAAE,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAA;IACvC,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,GAAG,EAAE,CAAC,uCAAiB,CAAC,GAAG,CAAC,OAAO,EAAE,QAAQ,CAAC,EAAE,CAAA;AAChF,CAAC;AAED,MAAM,SAAS,GAAG;IAChB,gDAAgD;IAChD,EAAE;IACF,6CAA6C;IAC7C,EAAE;IACF,0CAA0C;IAC1C,kCAAkC;IAClC,2BAA2B;CAC5B,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;AAEZ,IAAA,iBAAQ,EAAC,cAAc,EAAE,GAAG,EAAE;IAC5B,IAAA,WAAE,EAAC,gDAAgD,EAAE,GAAG,EAAE;QACxD,MAAM,IAAI,GAAG,IAAA,kCAAY,EAAC,SAAS,CAAC,CAAA;QACpC,IAAA,eAAM,EAAC,IAAI,CAAC,CAAC,OAAO,CAAC;YACnB,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,qBAAqB,EAAE,WAAW,EAAE,IAAI,EAAE;YAC7D,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,2BAA2B,EAAE,WAAW,EAAE,KAAK,EAAE;YACpE,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,oBAAoB,EAAE,WAAW,EAAE,KAAK,EAAE;SAC9D,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;IAEF,IAAA,WAAE,EAAC,kCAAkC,EAAE,GAAG,EAAE;QAC1C,MAAM,IAAI,GAAG,IAAA,kCAAY,EAAC,kCAAkC,CAAC,CAAA;QAC7D,IAAA,eAAM,EAAC,IAAI,CAAC,CAAC,CAAC,EAAE,WAAW,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QACvC,IAAA,eAAM,EAAC,IAAI,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QAClC,IAAA,eAAM,EAAC,IAAI,CAAC,CAAC,CAAC,EAAE,WAAW,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;IAC1C,CAAC,CAAC,CAAA;IAEF,IAAA,WAAE,EAAC,yBAAyB,EAAE,GAAG,EAAE;QACjC,MAAM,IAAI,GAAG,kEAAkE,CAAA;QAC/E,MAAM,IAAI,GAAG,IAAA,kCAAY,EAAC,IAAI,CAAC,CAAA;QAC/B,IAAA,eAAM,EAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;IAChE,CAAC,CAAC,CAAA;IAEF,IAAA,WAAE,EAAC,2BAA2B,EAAE,GAAG,EAAE;QACnC,MAAM,IAAI,GAAG,qCAAqC,CAAA;QAClD,IAAA,eAAM,EAAC,IAAA,kCAAY,EAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,CAAA;IACnE,CAAC,CAAC,CAAA;IAEF,IAAA,WAAE,EAAC,eAAe,EAAE,GAAG,EAAE;QACvB,MAAM,IAAI,GAAG,gEAAgE,CAAA;QAC7E,IAAA,eAAM,EAAC,IAAA,kCAAY,EAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAA;IACxC,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA;AAEF,IAAA,iBAAQ,EAAC,mBAAmB,EAAE,GAAG,EAAE;IACjC,IAAA,mBAAU,EAAC,GAAG,EAAE;QACd,KAAK,MAAM,GAAG,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,SAAS,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,UAAU,CAAC,EAAE,CAAC;YAClG,uCAAiB,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;QAC9B,CAAC;IACH,CAAC,CAAC,CAAA;IAEF,IAAA,WAAE,EAAC,+CAA+C,EAAE,GAAG,EAAE;QACvD,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,EAAE,CAAA;QAC5B,uCAAiB,CAAC,IAAI,CAAC,IAAI,EAAE,SAAS,CAAC,CAAA;QACvC,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,KAAK,IAAI,IAAI,CAAC,CAAC,IAAI,KAAK,qBAAqB,CAAC,CAAA;QAC/F,IAAA,eAAM,EAAC,QAAQ,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAA;QAChC,IAAI,QAAQ,CAAC,CAAC,CAAC,EAAE,IAAI,KAAK,qBAAqB,EAAE,CAAC;YAChD,IAAA,eAAM,EAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAA;YAC3C,IAAA,eAAM,EAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,IAAI,CAAC,qBAAqB,CAAC,CAAA;YACjE,IAAA,eAAM,EAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,WAAW,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QACxD,CAAC;IACH,CAAC,CAAC,CAAA;IAEF,IAAA,WAAE,EAAC,gDAAgD,EAAE,GAAG,EAAE;QACxD,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,EAAE,CAAA;QAC5B,+CAA+C;QAC/C,MAAM,MAAM,GAAG,wEAAwE,CAAA;QACvF,uCAAiB,CAAC,IAAI,CAAC,WAAW,EAAE,MAAM,CAAC,CAAA;QAC3C,IAAA,eAAM,EAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,KAAK,WAAW,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAA;IAC3E,CAAC,CAAC,CAAA;IAEF,IAAA,WAAE,EAAC,oBAAoB,EAAE,GAAG,EAAE;QAC5B,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,EAAE,CAAA;QAC5B,uCAAiB,CAAC,IAAI,CAAC,IAAI,EAAE,sDAAsD,CAAC,CAAA;QACpF,IAAA,eAAM,EAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,KAAK,IAAI,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAA;IACpE,CAAC,CAAC,CAAA;IAEF,IAAA,WAAE,EAAC,oCAAoC,EAAE,GAAG,EAAE;QAC5C,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,EAAE,CAAA;QAC5B,MAAM,IAAI,GAAG,2HAA2H,CAAA;QACxI,uCAAiB,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAA;QACtC,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,KAAK,QAAQ,IAAI,CAAC,CAAC,IAAI,KAAK,qBAAqB,CAAC,CAAA;QACnG,IAAA,eAAM,EAAC,QAAQ,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAA;QAChC,IAAI,QAAQ,CAAC,CAAC,CAAC,EAAE,IAAI,KAAK,qBAAqB,EAAE,CAAC;YAChD,IAAA,eAAM,EAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAA;QAC7C,CAAC;IACH,CAAC,CAAC,CAAA;IAEF,IAAA,WAAE,EAAC,kDAAkD,EAAE,GAAG,EAAE;QAC1D,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,EAAE,CAAA;QAC5B,sFAAsF;QACtF,kDAAkD;QAClD,0CAA0C;QAC1C,MAAM,OAAO,GACX,qEAAqE;YACrE,2FAA2F;YAC3F,oGAAoG;YACpG,6FAA6F,CAAA;QAC/F,uCAAiB,CAAC,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC,CAAA;QACxC,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,KAAK,OAAO,IAAI,CAAC,CAAC,IAAI,KAAK,qBAAqB,CAAC,CAAA;QAClG,IAAA,eAAM,EAAC,QAAQ,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAA;QAChC,IAAI,QAAQ,CAAC,CAAC,CAAC,EAAE,IAAI,KAAK,qBAAqB,EAAE,CAAC;YAChD,IAAA,eAAM,EAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC;gBAClC,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,qBAAqB,EAAE,WAAW,EAAE,IAAI,EAAE;gBAC7D,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,2BAA2B,EAAE,WAAW,EAAE,KAAK,EAAE;gBACpE,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,EAAE,oBAAoB,EAAE,WAAW,EAAE,KAAK,EAAE;aAC9D,CAAC,CAAA;QACJ,CAAC;QACD,uCAAiB,CAAC,KAAK,CAAC,OAAO,CAAC,CAAA;IAClC,CAAC,CAAC,CAAA;IAEF,IAAA,WAAE,EAAC,4BAA4B,EAAE,GAAG,EAAE;QACpC,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,EAAE,CAAA;QAC5B,mEAAmE;QACnE,8DAA8D;QAC9D,MAAM,GAAG,GAAG,+IAA+I,CAAA;QAC3J,uCAAiB,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,CAAC,CAAA;QACpC,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,KAAK,OAAO,IAAI,CAAC,CAAC,IAAI,KAAK,qBAAqB,CAAC,CAAA;QAClG,IAAA,eAAM,EAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,sBAAsB,CAAC,CAAC,CAAC,CAAA;QACjD,uCAAiB,CAAC,KAAK,CAAC,OAAO,CAAC,CAAA;IAClC,CAAC,CAAC,CAAA;IAEF,IAAA,WAAE,EAAC,iDAAiD,EAAE,GAAG,EAAE;QACzD,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,EAAE,CAAA;QAC5B,uCAAiB,CAAC,IAAI,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAA;QAC3C,uCAAiB,CAAC,IAAI,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAA;QAC3C,uCAAiB,CAAC,IAAI,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAA;QAC3C,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,KAAK,QAAQ,IAAI,CAAC,CAAC,IAAI,KAAK,qBAAqB,CAAC,CAAA;QACnG,IAAA,eAAM,EAAC,QAAQ,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAA;IAClC,CAAC,CAAC,CAAA;IAEF,IAAA,WAAE,EAAC,uEAAuE,EAAE,GAAG,EAAE;QAC/E,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,EAAE,CAAA;QAC5B,uCAAiB,CAAC,IAAI,CAAC,UAAU,EAAE,SAAS,CAAC,CAAA;QAC7C,kCAAkC;QAClC,MAAM,KAAK,GAAG;YACZ,gDAAgD;YAChD,6CAA6C;YAC7C,4BAA4B;YAC5B,kCAAkC;YAClC,2BAA2B;SAC5B,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QACZ,uCAAiB,CAAC,IAAI,CAAC,UAAU,EAAE,KAAK,CAAC,CAAA;QACzC,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,KAAK,UAAU,IAAI,CAAC,CAAC,IAAI,KAAK,qBAAqB,CAAC,CAAA;QACrG,IAAA,eAAM,EAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,sBAAsB,CAAC,CAAC,CAAC,CAAA;QACjD,MAAM,IAAI,GAAG,QAAQ,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAA;QAC1C,IAAI,IAAI,EAAE,IAAI,KAAK,qBAAqB,EAAE,CAAC;YACzC,IAAA,eAAM,EAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,WAAW,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;YAC/C,IAAA,eAAM,EAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,WAAW,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QAClD,CAAC;IACH,CAAC,CAAC,CAAA;IAEF,IAAA,WAAE,EAAC,0CAA0C,EAAE,GAAG,EAAE;QAClD,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,EAAE,CAAA;QAC5B,uCAAiB,CAAC,IAAI,CAAC,IAAI,EAAE,SAAS,CAAC,CAAA;QACvC,MAAM,KAAK,GAAG,GAAG,CAAC,MAAM,CAAC,EAAE,GAAG,IAAI,CAAC,CAAA;QACnC,uCAAiB,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,CAAC,CAAA;QACnC,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,KAAK,IAAI,CAAC,CAAA;QACrD,IAAA,eAAM,EAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,qBAAqB,EAAE,sBAAsB,CAAC,CAAC,CAAA;IACxF,CAAC,CAAC,CAAA;IAEF,IAAA,WAAE,EAAC,gBAAgB,EAAE,GAAG,EAAE;QACxB,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,EAAE,CAAA;QAC5B,uCAAiB,CAAC,IAAI,CAAC,IAAI,EAAE,SAAS,CAAC,CAAA;QACvC,uCAAiB,CAAC,IAAI,CAAC,IAAI,EAAE,kBAAkB,CAAC,CAAA;QAChD,IAAA,eAAM,EAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,KAAK,IAAI,IAAI,CAAC,CAAC,IAAI,KAAK,qBAAqB,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAA;QACtG,IAAA,eAAM,EAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,KAAK,IAAI,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAA;IACpE,CAAC,CAAC,CAAA;IAEF,IAAA,WAAE,EAAC,gCAAgC,EAAE,GAAG,EAAE;QACxC,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,EAAE,CAAA;QAC5B,uCAAiB,CAAC,IAAI,CAAC,SAAS,EAAE,SAAS,CAAC,CAAA;QAC5C,uCAAiB,CAAC,KAAK,CAAC,SAAS,CAAC,CAAA;QAClC,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,KAAK,SAAS,CAAC,CAAA;QAC1D,IAAA,eAAM,EAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,qBAAqB,EAAE,sBAAsB,CAAC,CAAC,CAAA;IACxF,CAAC,CAAC,CAAA;IAEF,IAAA,WAAE,EAAC,iCAAiC,EAAE,GAAG,EAAE;QACzC,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,EAAE,CAAA;QAC5B,uCAAiB,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;QAC7B,IAAA,eAAM,EAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,KAAK,IAAI,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAA;IACpE,CAAC,CAAC,CAAA;IAEF,IAAA,WAAE,EAAC,qCAAqC,EAAE,GAAG,EAAE;QAC7C,uCAAiB,CAAC,IAAI,CAAC,IAAI,EAAE,SAAS,CAAC,CAAA;QACvC,MAAM,MAAM,GAAG,uCAAiB,CAAC,SAAS,CAAC,IAAI,CAAC,CAAA;QAChD,IAAA,eAAM,EAAC,MAAM,EAAE,OAAO,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAA;QACvC,IAAA,eAAM,EAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,IAAI,CAAC,qBAAqB,CAAC,CAAA;IAC/D,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA"}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI Interactive Prompt Detector
|
|
3
|
+
*
|
|
4
|
+
* Claude Code CLI (and likely other Ink-TUI CLI tools) renders interactive
|
|
5
|
+
* select menus through ANSI escape sequences on the PTY. These do NOT show
|
|
6
|
+
* up as stream-json lines, so the ChatOverlay (which only consumes JSON via
|
|
7
|
+
* adapter.parseLineBlocks) is blind to them. The user is then stuck in the
|
|
8
|
+
* terminal panel without knowing it from the chat-only view.
|
|
9
|
+
*
|
|
10
|
+
* This module is a passive PTY-output sniffer that watches for known menu
|
|
11
|
+
* fingerprints (currently only Claude's `--continue` resume menu) and emits
|
|
12
|
+
* `cli_prompt_detected` / `cli_prompt_dismissed` lifecycle events. The
|
|
13
|
+
* ChatOverlay then renders interactive choice buttons whose labels mirror
|
|
14
|
+
* the menu options — the click handler hits a backend API that writes the
|
|
15
|
+
* chosen digit straight to the PTY.
|
|
16
|
+
*
|
|
17
|
+
* Why digits parsed from the menu, not hard-coded 1/2/3:
|
|
18
|
+
* CLAUDE.md 历史教训 #10 — wrapping external CLIs is bypass, not root-cause.
|
|
19
|
+
* If Claude CLI ever reorders the menu, hard-coded `1 = Resume summary` would
|
|
20
|
+
* silently mis-select. Instead we parse "<digit>. <label>" lines out of the
|
|
21
|
+
* ANSI-stripped buffer and bind buttons to the digit-label pairs we actually
|
|
22
|
+
* saw. If parsing fails (CLI changed format wholesale), we emit nothing —
|
|
23
|
+
* the user falls back to the terminal panel, which is the pre-feature state.
|
|
24
|
+
*
|
|
25
|
+
* State machine: per-project ring buffer of recent (ANSI-stripped) PTY output;
|
|
26
|
+
* on every feed() we re-evaluate "any fingerprint visible + options parsable?"
|
|
27
|
+
* and emit only on transitions (debounced — no per-chunk spam).
|
|
28
|
+
*/
|
|
29
|
+
import { EventEmitter } from 'events';
|
|
30
|
+
/** Public event union (mirrors approval-manager style). */
|
|
31
|
+
export type CliPromptEvent = {
|
|
32
|
+
type: 'cli_prompt_detected';
|
|
33
|
+
projectId: string;
|
|
34
|
+
kind: CliPromptKind;
|
|
35
|
+
/** Best-effort human label for the menu (en/zh-agnostic). */
|
|
36
|
+
label: string;
|
|
37
|
+
/** Parsed digit-label pairs the user can choose between. */
|
|
38
|
+
options: CliPromptOption[];
|
|
39
|
+
detectedAt: number;
|
|
40
|
+
} | {
|
|
41
|
+
type: 'cli_prompt_dismissed';
|
|
42
|
+
projectId: string;
|
|
43
|
+
kind: CliPromptKind;
|
|
44
|
+
};
|
|
45
|
+
export type CliPromptKind = 'claude_resume_session';
|
|
46
|
+
export interface CliPromptOption {
|
|
47
|
+
/** The digit shown next to the option in the menu (typically 1-based). */
|
|
48
|
+
digit: number;
|
|
49
|
+
/** Human label after the digit, e.g. "Resume from summary". */
|
|
50
|
+
label: string;
|
|
51
|
+
/** Highlighted by ❯ or marked "(recommended)" — UI default-focus hint. */
|
|
52
|
+
recommended: boolean;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Parse menu options out of the ANSI-stripped buffer.
|
|
56
|
+
*
|
|
57
|
+
* Matches lines shaped like:
|
|
58
|
+
* " 1. Resume from summary (recommended)"
|
|
59
|
+
* " ❯ 2. Resume full session as-is"
|
|
60
|
+
*
|
|
61
|
+
* Returns options keyed by parsed digit, latest occurrence wins (Ink redraws
|
|
62
|
+
* the same menu repeatedly as the user navigates with ↑/↓; the *latest* slice
|
|
63
|
+
* of buffer reflects the current state including which option is highlighted).
|
|
64
|
+
*/
|
|
65
|
+
export declare function parseOptions(buffer: string): CliPromptOption[];
|
|
66
|
+
interface ActiveState {
|
|
67
|
+
kind: CliPromptKind;
|
|
68
|
+
label: string;
|
|
69
|
+
detectedAt: number;
|
|
70
|
+
options: CliPromptOption[];
|
|
71
|
+
}
|
|
72
|
+
declare class CliPromptDetector extends EventEmitter {
|
|
73
|
+
private readonly states;
|
|
74
|
+
/**
|
|
75
|
+
* Feed a chunk of raw PTY output. Synchronous, side-effect: may emit
|
|
76
|
+
* 'cli_prompt_detected' or 'cli_prompt_dismissed' on state transitions.
|
|
77
|
+
*/
|
|
78
|
+
feed(projectId: string, raw: string): void;
|
|
79
|
+
/** Drop all state for a project (call on terminal exit / project delete). */
|
|
80
|
+
reset(projectId: string): void;
|
|
81
|
+
/**
|
|
82
|
+
* Snapshot of the currently active prompt for a project, if any. Used by
|
|
83
|
+
* the REST endpoint that lets the frontend re-sync after WS reconnect /
|
|
84
|
+
* page refresh — without it, a client that misses the transition emit
|
|
85
|
+
* would never know the CLI is still waiting for input.
|
|
86
|
+
*/
|
|
87
|
+
getActive(projectId: string): ActiveState | null;
|
|
88
|
+
private matchFingerprint;
|
|
89
|
+
}
|
|
90
|
+
export declare const cliPromptDetector: CliPromptDetector;
|
|
91
|
+
export {};
|
|
92
|
+
//# sourceMappingURL=cli-prompt-detector.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli-prompt-detector.d.ts","sourceRoot":"","sources":["../src/cli-prompt-detector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AAItC,2DAA2D;AAC3D,MAAM,MAAM,cAAc,GACtB;IACE,IAAI,EAAE,qBAAqB,CAAC;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,aAAa,CAAC;IACpB,6DAA6D;IAC7D,KAAK,EAAE,MAAM,CAAC;IACd,4DAA4D;IAC5D,OAAO,EAAE,eAAe,EAAE,CAAC;IAC3B,UAAU,EAAE,MAAM,CAAC;CACpB,GACD;IACE,IAAI,EAAE,sBAAsB,CAAC;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,aAAa,CAAC;CACrB,CAAC;AAEN,MAAM,MAAM,aAAa,GAAG,uBAAuB,CAAC;AAEpD,MAAM,WAAW,eAAe;IAC9B,0EAA0E;IAC1E,KAAK,EAAE,MAAM,CAAC;IACd,+DAA+D;IAC/D,KAAK,EAAE,MAAM,CAAC;IACd,0EAA0E;IAC1E,WAAW,EAAE,OAAO,CAAC;CACtB;AAwDD;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,eAAe,EAAE,CAmB9D;AAED,UAAU,WAAW;IACnB,IAAI,EAAE,aAAa,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,eAAe,EAAE,CAAC;CAC5B;AAOD,cAAM,iBAAkB,SAAQ,YAAY;IAC1C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAmC;IAE1D;;;OAGG;IACH,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI;IAmE1C,6EAA6E;IAC7E,KAAK,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAY9B;;;;;OAKG;IACH,SAAS,CAAC,SAAS,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI;IAIhD,OAAO,CAAC,gBAAgB;CAMzB;AAYD,eAAO,MAAM,iBAAiB,mBAA0B,CAAC"}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* CLI Interactive Prompt Detector
|
|
4
|
+
*
|
|
5
|
+
* Claude Code CLI (and likely other Ink-TUI CLI tools) renders interactive
|
|
6
|
+
* select menus through ANSI escape sequences on the PTY. These do NOT show
|
|
7
|
+
* up as stream-json lines, so the ChatOverlay (which only consumes JSON via
|
|
8
|
+
* adapter.parseLineBlocks) is blind to them. The user is then stuck in the
|
|
9
|
+
* terminal panel without knowing it from the chat-only view.
|
|
10
|
+
*
|
|
11
|
+
* This module is a passive PTY-output sniffer that watches for known menu
|
|
12
|
+
* fingerprints (currently only Claude's `--continue` resume menu) and emits
|
|
13
|
+
* `cli_prompt_detected` / `cli_prompt_dismissed` lifecycle events. The
|
|
14
|
+
* ChatOverlay then renders interactive choice buttons whose labels mirror
|
|
15
|
+
* the menu options — the click handler hits a backend API that writes the
|
|
16
|
+
* chosen digit straight to the PTY.
|
|
17
|
+
*
|
|
18
|
+
* Why digits parsed from the menu, not hard-coded 1/2/3:
|
|
19
|
+
* CLAUDE.md 历史教训 #10 — wrapping external CLIs is bypass, not root-cause.
|
|
20
|
+
* If Claude CLI ever reorders the menu, hard-coded `1 = Resume summary` would
|
|
21
|
+
* silently mis-select. Instead we parse "<digit>. <label>" lines out of the
|
|
22
|
+
* ANSI-stripped buffer and bind buttons to the digit-label pairs we actually
|
|
23
|
+
* saw. If parsing fails (CLI changed format wholesale), we emit nothing —
|
|
24
|
+
* the user falls back to the terminal panel, which is the pre-feature state.
|
|
25
|
+
*
|
|
26
|
+
* State machine: per-project ring buffer of recent (ANSI-stripped) PTY output;
|
|
27
|
+
* on every feed() we re-evaluate "any fingerprint visible + options parsable?"
|
|
28
|
+
* and emit only on transitions (debounced — no per-chunk spam).
|
|
29
|
+
*/
|
|
30
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
31
|
+
exports.cliPromptDetector = void 0;
|
|
32
|
+
exports.parseOptions = parseOptions;
|
|
33
|
+
const events_1 = require("events");
|
|
34
|
+
const BUFFER_CAP_CHARS = 8 * 1024;
|
|
35
|
+
/**
|
|
36
|
+
* Stable fingerprints — chosen to be robust against minor wording drift:
|
|
37
|
+
* - "Resume from summary" / "Resume full session" / "Don't ask me again"
|
|
38
|
+
* are the three menu options. Requiring ALL THREE makes false positives
|
|
39
|
+
* near-impossible (no normal conversation hits this combo) while still
|
|
40
|
+
* tolerating any single line being reworded one release at a time.
|
|
41
|
+
*/
|
|
42
|
+
const FINGERPRINTS = [
|
|
43
|
+
{
|
|
44
|
+
kind: 'claude_resume_session',
|
|
45
|
+
label: 'Claude 会话恢复选项',
|
|
46
|
+
phrases: ['Resume from summary', 'Resume full session', "Don't ask me again"],
|
|
47
|
+
minOptions: 2,
|
|
48
|
+
},
|
|
49
|
+
];
|
|
50
|
+
/**
|
|
51
|
+
* Strip ANSI CSI sequences down to plain text that fingerprint phrases can be
|
|
52
|
+
* matched against.
|
|
53
|
+
*
|
|
54
|
+
* v-20-b root-cause fix: Ink TUI (used by Claude CLI) lays out text via
|
|
55
|
+
* column-positioning escapes (CHA = `\x1b[<n>G`, CUF = `\x1b[<n>C`) instead of
|
|
56
|
+
* literal whitespace, so "Resume from summary" arrives on the PTY as
|
|
57
|
+
* `Resume\x1b[15Gfrom\x1b[20Gsummary`. The previous strip dropped CHA/CUF
|
|
58
|
+
* outright, collapsing the line to "Resumefromsummary" and making the
|
|
59
|
+
* fingerprint phrases impossible to match. We now replace those two cursor-
|
|
60
|
+
* advance escapes with a single space — that's enough whitespace for fingerprint
|
|
61
|
+
* phrases and the digit-label option regex to find their normal-looking text,
|
|
62
|
+
* without trying to reconstruct exact column alignment (which we don't need).
|
|
63
|
+
*
|
|
64
|
+
* Order matters: CHA / CUF must be handled BEFORE the generic CSI sweep,
|
|
65
|
+
* otherwise the generic regex eats them first and the replacement never runs.
|
|
66
|
+
*/
|
|
67
|
+
function stripAnsi(s) {
|
|
68
|
+
// eslint-disable-next-line no-control-regex
|
|
69
|
+
return s
|
|
70
|
+
.replace(/\x1b\[\d*G/g, ' ')
|
|
71
|
+
.replace(/\x1b\[(\d*)C/g, (_m, n) => {
|
|
72
|
+
const count = Number(n || 1);
|
|
73
|
+
return ' '.repeat(Number.isFinite(count) && count > 0 && count <= 200 ? count : 1);
|
|
74
|
+
})
|
|
75
|
+
.replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, '')
|
|
76
|
+
.replace(/\x1b\][^\x07]*\x07/g, '');
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Parse menu options out of the ANSI-stripped buffer.
|
|
80
|
+
*
|
|
81
|
+
* Matches lines shaped like:
|
|
82
|
+
* " 1. Resume from summary (recommended)"
|
|
83
|
+
* " ❯ 2. Resume full session as-is"
|
|
84
|
+
*
|
|
85
|
+
* Returns options keyed by parsed digit, latest occurrence wins (Ink redraws
|
|
86
|
+
* the same menu repeatedly as the user navigates with ↑/↓; the *latest* slice
|
|
87
|
+
* of buffer reflects the current state including which option is highlighted).
|
|
88
|
+
*/
|
|
89
|
+
function parseOptions(buffer) {
|
|
90
|
+
// ❯ = ❯; allowed leading whitespace; capture digit + label.
|
|
91
|
+
// Use [ \t]* to NOT cross newlines and keep matches per-line.
|
|
92
|
+
const re = /^[ \t]*(❯|>)?[ \t]*(\d+)\.[ \t]+(.+?)[ \t]*$/gm;
|
|
93
|
+
const byDigit = new Map();
|
|
94
|
+
let m;
|
|
95
|
+
while ((m = re.exec(buffer)) !== null) {
|
|
96
|
+
const marker = m[1];
|
|
97
|
+
const digit = Number(m[2]);
|
|
98
|
+
const rawLabel = m[3] ?? '';
|
|
99
|
+
if (!Number.isFinite(digit) || digit <= 0)
|
|
100
|
+
continue;
|
|
101
|
+
const isRecommended = marker === '❯' || /\(recommended\)/i.test(rawLabel);
|
|
102
|
+
// Strip the "(recommended)" tag from the human label — the flag carries
|
|
103
|
+
// that semantic; keeping it in the label would also clutter the button.
|
|
104
|
+
const cleanLabel = rawLabel.replace(/\s*\(recommended\)\s*$/i, '').trim();
|
|
105
|
+
if (!cleanLabel)
|
|
106
|
+
continue;
|
|
107
|
+
byDigit.set(digit, { digit, label: cleanLabel, recommended: isRecommended });
|
|
108
|
+
}
|
|
109
|
+
return [...byDigit.values()].sort((a, b) => a.digit - b.digit);
|
|
110
|
+
}
|
|
111
|
+
class CliPromptDetector extends events_1.EventEmitter {
|
|
112
|
+
constructor() {
|
|
113
|
+
super(...arguments);
|
|
114
|
+
this.states = new Map();
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Feed a chunk of raw PTY output. Synchronous, side-effect: may emit
|
|
118
|
+
* 'cli_prompt_detected' or 'cli_prompt_dismissed' on state transitions.
|
|
119
|
+
*/
|
|
120
|
+
feed(projectId, raw) {
|
|
121
|
+
const stripped = stripAnsi(raw);
|
|
122
|
+
if (!stripped)
|
|
123
|
+
return;
|
|
124
|
+
let state = this.states.get(projectId);
|
|
125
|
+
if (!state) {
|
|
126
|
+
state = { buffer: '', active: null };
|
|
127
|
+
this.states.set(projectId, state);
|
|
128
|
+
}
|
|
129
|
+
state.buffer += stripped;
|
|
130
|
+
if (state.buffer.length > BUFFER_CAP_CHARS) {
|
|
131
|
+
state.buffer = state.buffer.slice(state.buffer.length - BUFFER_CAP_CHARS);
|
|
132
|
+
}
|
|
133
|
+
const match = this.matchFingerprint(state.buffer);
|
|
134
|
+
if (match) {
|
|
135
|
+
// codex P1-1(跨帧混拼)已审:parseOptions 用 latest-wins by digit,
|
|
136
|
+
// Ink 每帧重绘整段时新行覆盖旧行;唯一残留风险是 PTY chunk 撕开一帧
|
|
137
|
+
// 中间到达(极罕见,多数实现按行 flush)。即使发生,UI 展示和后端
|
|
138
|
+
// 校验同一份 options,行为一致;不修这个为 lastAnchor slice,因为
|
|
139
|
+
// anchor 选最后一个 phrase 出现位置会切掉前面的 option 行(test 验证)。
|
|
140
|
+
const options = parseOptions(state.buffer);
|
|
141
|
+
if (options.length < match.fp.minOptions) {
|
|
142
|
+
// Phrases visible but we can't read the options off the screen.
|
|
143
|
+
// Stay silent — see CLAUDE.md 历史教训 #10. If currently active,
|
|
144
|
+
// do NOT dismiss yet either: the menu is plausibly still up, options
|
|
145
|
+
// just got obscured by redraw. Buffer-rolloff handles real dismissal.
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (!state.active) {
|
|
149
|
+
const detectedAt = Date.now();
|
|
150
|
+
state.active = { kind: match.fp.kind, label: match.fp.label, detectedAt, options };
|
|
151
|
+
this.emit('event', {
|
|
152
|
+
type: 'cli_prompt_detected',
|
|
153
|
+
projectId,
|
|
154
|
+
kind: match.fp.kind,
|
|
155
|
+
label: match.fp.label,
|
|
156
|
+
options,
|
|
157
|
+
detectedAt,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
else if (!sameOptions(state.active.options, options)) {
|
|
161
|
+
// Same menu still up but options shifted (different "recommended"
|
|
162
|
+
// highlight, or upstream tweaked label wording mid-session). Update
|
|
163
|
+
// in place; clients re-render from the new options array. No
|
|
164
|
+
// dismissed/detected pair so the card doesn't flicker.
|
|
165
|
+
state.active = { ...state.active, options };
|
|
166
|
+
this.emit('event', {
|
|
167
|
+
type: 'cli_prompt_detected',
|
|
168
|
+
projectId,
|
|
169
|
+
kind: match.fp.kind,
|
|
170
|
+
label: match.fp.label,
|
|
171
|
+
options,
|
|
172
|
+
detectedAt: state.active.detectedAt,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
else if (state.active) {
|
|
177
|
+
const prev = state.active;
|
|
178
|
+
state.active = null;
|
|
179
|
+
this.emit('event', {
|
|
180
|
+
type: 'cli_prompt_dismissed',
|
|
181
|
+
projectId,
|
|
182
|
+
kind: prev.kind,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
/** Drop all state for a project (call on terminal exit / project delete). */
|
|
187
|
+
reset(projectId) {
|
|
188
|
+
const state = this.states.get(projectId);
|
|
189
|
+
if (state?.active) {
|
|
190
|
+
this.emit('event', {
|
|
191
|
+
type: 'cli_prompt_dismissed',
|
|
192
|
+
projectId,
|
|
193
|
+
kind: state.active.kind,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
this.states.delete(projectId);
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Snapshot of the currently active prompt for a project, if any. Used by
|
|
200
|
+
* the REST endpoint that lets the frontend re-sync after WS reconnect /
|
|
201
|
+
* page refresh — without it, a client that misses the transition emit
|
|
202
|
+
* would never know the CLI is still waiting for input.
|
|
203
|
+
*/
|
|
204
|
+
getActive(projectId) {
|
|
205
|
+
return this.states.get(projectId)?.active ?? null;
|
|
206
|
+
}
|
|
207
|
+
matchFingerprint(buffer) {
|
|
208
|
+
for (const fp of FINGERPRINTS) {
|
|
209
|
+
if (fp.phrases.every((p) => buffer.includes(p)))
|
|
210
|
+
return { fp };
|
|
211
|
+
}
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
function sameOptions(a, b) {
|
|
216
|
+
if (a.length !== b.length)
|
|
217
|
+
return false;
|
|
218
|
+
for (let i = 0; i < a.length; i++) {
|
|
219
|
+
const x = a[i];
|
|
220
|
+
const y = b[i];
|
|
221
|
+
if (x.digit !== y.digit || x.label !== y.label || x.recommended !== y.recommended)
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
exports.cliPromptDetector = new CliPromptDetector();
|
|
227
|
+
//# sourceMappingURL=cli-prompt-detector.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli-prompt-detector.js","sourceRoot":"","sources":["../src/cli-prompt-detector.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAuFA,QAAA,MAAM,GAAG,6CAAY,CAAC;AAw0BtB,eAAe,GAAG,CAAC"}
|
package/backend/dist/index.js
CHANGED
|
@@ -71,6 +71,7 @@ const request_log_1 = require("./middleware/request-log");
|
|
|
71
71
|
const hooks_1 = __importStar(require("./routes/hooks"));
|
|
72
72
|
const approval_1 = __importDefault(require("./routes/approval"));
|
|
73
73
|
const approval_manager_1 = require("./approval-manager");
|
|
74
|
+
const cli_prompt_detector_1 = require("./cli-prompt-detector");
|
|
74
75
|
const notify_1 = __importDefault(require("./routes/notify"));
|
|
75
76
|
const notify_service_1 = require("./notify-service");
|
|
76
77
|
const git_1 = __importDefault(require("./routes/git"));
|
|
@@ -357,6 +358,14 @@ approval_manager_1.approvalManager.subscribe((evt) => {
|
|
|
357
358
|
safeSend(client, payload);
|
|
358
359
|
}
|
|
359
360
|
});
|
|
361
|
+
// CLI interactive prompt detection (Claude --continue resume menu, etc.).
|
|
362
|
+
// Carries no sensitive tool input; the event body is just a stable kind +
|
|
363
|
+
// generic label. Broadcast to all project clients including view-only —
|
|
364
|
+
// they should also see "切到终端" hints, since they may still want to know
|
|
365
|
+
// the project is blocked waiting for input.
|
|
366
|
+
cli_prompt_detector_1.cliPromptDetector.on('event', (evt) => {
|
|
367
|
+
broadcastJson(evt.projectId, evt);
|
|
368
|
+
});
|
|
360
369
|
function isLocalWs(req) {
|
|
361
370
|
const ip = req.socket.remoteAddress || '';
|
|
362
371
|
return ip === '127.0.0.1' || ip === '::1' || ip === '::ffff:127.0.0.1';
|