cc-viewer 1.7.0 → 1.7.2
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-B224r-8M.js +2 -0
- package/dist/assets/{MdxEditorPanel-C5Q6uUyH.js → MdxEditorPanel-BUubiE6Y.js} +1 -1
- package/dist/assets/{Mobile-MoBp9c8k.js → Mobile-CKmfRRvo.js} +1 -1
- package/dist/assets/index-CjsQEQc2.js +2 -0
- package/dist/assets/seqResourceLoaders-C-T_EMiO.js +2 -0
- package/dist/assets/{seqResourceLoaders-CxuD1CYl.css → seqResourceLoaders-DFrHmgnK.css} +1 -1
- package/dist/index.html +1 -1
- package/package.json +2 -1
- package/server/lib/log-management.js +1 -0
- package/server/lib/log-watcher.js +29 -3
- package/server/lib/stats-worker.js +21 -2
- package/server/lib/v2/adapter.js +123 -18
- package/server/lib/v2/journal.js +9 -5
- package/server/lib/v2/jsonl-read.js +127 -0
- package/server/lib/v2/live-feed.js +222 -20
- package/server/lib/v2/meta-rows.js +361 -0
- package/server/lib/v2/replay.js +5 -5
- package/server/lib/v2/session-select.js +69 -2
- 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/im.js +1 -1
- 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/server/workspace-registry.js +5 -0
- 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,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dev-only instrumentation for the Markdown render pipeline.
|
|
3
|
+
*
|
|
4
|
+
* Gated on `import.meta.env.DEV` — production builds compile every export to a
|
|
5
|
+
* no-op and tree-shake the module state away, so runtime cost in prod is zero.
|
|
6
|
+
*
|
|
7
|
+
* API:
|
|
8
|
+
* measureParse(fn) — wrap a sync parse call; records duration as a
|
|
9
|
+
* parse sample and emits an `md-parse` measure.
|
|
10
|
+
* recordMountSample(ms) — append a mount-cycle duration (typically the
|
|
11
|
+
* delta between render-return and the next effect)
|
|
12
|
+
* and emit an `md-mount` measure.
|
|
13
|
+
*
|
|
14
|
+
* Inspection:
|
|
15
|
+
* window.__mdStats.summary() → { parseP50, parseP95, mountP50, mountP95, ... }
|
|
16
|
+
* window.__mdStats.reset()
|
|
17
|
+
* window.__mdStats.samples → raw { parse: [], mount: [] }
|
|
18
|
+
*
|
|
19
|
+
* DevTools Performance timeline shows `md-parse` / `md-mount` measures under
|
|
20
|
+
* the User Timing track. A recorded slice of a streaming session can then be
|
|
21
|
+
* inspected to attribute time between parse, reconcile, layout, and paint.
|
|
22
|
+
*
|
|
23
|
+
* The module auto-prunes performance.measure entries once per 500 samples so
|
|
24
|
+
* long-running dev sessions do not accumulate unbounded DevTools entries.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
const DEV = typeof import.meta !== 'undefined'
|
|
28
|
+
&& import.meta.env
|
|
29
|
+
&& import.meta.env.DEV === true;
|
|
30
|
+
|
|
31
|
+
const MAX_SAMPLES = 2000;
|
|
32
|
+
const CLEAR_MARKS_EVERY = 500;
|
|
33
|
+
|
|
34
|
+
export function percentile(sorted, p) {
|
|
35
|
+
if (sorted.length === 0) return 0;
|
|
36
|
+
const idx = Math.min(sorted.length - 1, Math.floor(sorted.length * p));
|
|
37
|
+
return sorted[idx];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function createStats() {
|
|
41
|
+
const parse = [];
|
|
42
|
+
const mount = [];
|
|
43
|
+
let writeCount = 0;
|
|
44
|
+
|
|
45
|
+
function pushBounded(arr, v) {
|
|
46
|
+
arr.push(v);
|
|
47
|
+
if (arr.length > MAX_SAMPLES) arr.splice(0, arr.length - MAX_SAMPLES);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function maybeClearMarks() {
|
|
51
|
+
writeCount += 1;
|
|
52
|
+
if (writeCount % CLEAR_MARKS_EVERY === 0
|
|
53
|
+
&& typeof performance !== 'undefined'
|
|
54
|
+
&& typeof performance.clearMeasures === 'function') {
|
|
55
|
+
try {
|
|
56
|
+
performance.clearMeasures('md-parse');
|
|
57
|
+
performance.clearMeasures('md-mount');
|
|
58
|
+
} catch { /* ignore */ }
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
samples: { parse, mount },
|
|
64
|
+
recordParse(ms) { pushBounded(parse, ms); maybeClearMarks(); },
|
|
65
|
+
recordMount(ms) { pushBounded(mount, ms); maybeClearMarks(); },
|
|
66
|
+
summary() {
|
|
67
|
+
const p = [...parse].sort((a, b) => a - b);
|
|
68
|
+
const m = [...mount].sort((a, b) => a - b);
|
|
69
|
+
return {
|
|
70
|
+
parseN: parse.length,
|
|
71
|
+
parseP50: +percentile(p, 0.5).toFixed(2),
|
|
72
|
+
parseP95: +percentile(p, 0.95).toFixed(2),
|
|
73
|
+
parseMax: +(p[p.length - 1] || 0).toFixed(2),
|
|
74
|
+
mountN: mount.length,
|
|
75
|
+
mountP50: +percentile(m, 0.5).toFixed(2),
|
|
76
|
+
mountP95: +percentile(m, 0.95).toFixed(2),
|
|
77
|
+
mountMax: +(m[m.length - 1] || 0).toFixed(2),
|
|
78
|
+
};
|
|
79
|
+
},
|
|
80
|
+
reset() {
|
|
81
|
+
parse.length = 0;
|
|
82
|
+
mount.length = 0;
|
|
83
|
+
writeCount = 0;
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const stats = DEV ? createStats() : null;
|
|
89
|
+
|
|
90
|
+
if (DEV && typeof window !== 'undefined') {
|
|
91
|
+
window.__mdStats = stats;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function measureParse(fn) {
|
|
95
|
+
if (!DEV) return fn();
|
|
96
|
+
const start = performance.now();
|
|
97
|
+
const result = fn();
|
|
98
|
+
const ms = performance.now() - start;
|
|
99
|
+
stats.recordParse(ms);
|
|
100
|
+
try { performance.measure('md-parse', { start, end: start + ms }); } catch { /* ignore */ }
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function recordMountSample(ms) {
|
|
105
|
+
if (!DEV || !Number.isFinite(ms) || ms < 0) return;
|
|
106
|
+
stats.recordMount(ms);
|
|
107
|
+
try {
|
|
108
|
+
const start = performance.now() - ms;
|
|
109
|
+
performance.measure('md-mount', { start, end: start + ms });
|
|
110
|
+
} catch { /* ignore */ }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export const DEV_PROFILER_ENABLED = DEV;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// 检测 markdown 内容是否含 MDXEditor 默认不支持的扩展语法
|
|
2
|
+
// 命中任意一项时,打开 .md 文件应自动 fallback 到旧 marked 渲染(保留语义完整性),
|
|
3
|
+
// 避免 MDXEditor 解析时丢失或损坏这些扩展块。
|
|
4
|
+
|
|
5
|
+
const RE_MERMAID_FENCE = /^[ \t]*```\s*mermaid\b/m;
|
|
6
|
+
const RE_MATH_FENCE = /^[ \t]*```\s*(?:math|latex|tex|katex)\b/im;
|
|
7
|
+
const RE_BLOCK_MATH = /\$\$[\s\S]+?\$\$/;
|
|
8
|
+
const RE_DIRECTIVE = /^[ \t]*:::[A-Za-z][\w-]*\b/m;
|
|
9
|
+
|
|
10
|
+
// 行内数学 $...$ 容易和货币符号 / 转义符 / 代码块里的 $ 撞,规则:
|
|
11
|
+
// - 必须在同一行内成对
|
|
12
|
+
// - $ 前后不能是数字(排除 $5、3$)
|
|
13
|
+
// - 至少包含一个数学常见字符(字母、运算符、括号)
|
|
14
|
+
// - 整体不少于 3 字符
|
|
15
|
+
// - 排除位于代码块(```)内的情况由调用方先剥除三连引号块(这里做粗略过滤)
|
|
16
|
+
// 注:lookbehind `(?<!...)` 在老 Safari (<16.4) 抛 SyntaxError;construct 用 try/catch 包裹,
|
|
17
|
+
// 失败时 RE_INLINE_MATH = null,相当于跳过行内数学检测(保守 fallback)。
|
|
18
|
+
let RE_INLINE_MATH = null;
|
|
19
|
+
try {
|
|
20
|
+
RE_INLINE_MATH = new RegExp("(?<![\\d$\\\\])\\$([^\\s$][^$\\n]{0,200}?[^\\s$\\\\])\\$(?![\\d])");
|
|
21
|
+
} catch {
|
|
22
|
+
RE_INLINE_MATH = null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function stripFences(md) {
|
|
26
|
+
// 粗略剥掉 ``` 围栏代码块,避免行内数学误判命中代码示例里的 $...$
|
|
27
|
+
return md.replace(/```[\s\S]*?```/g, '');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function detectMdExtensions(markdown) {
|
|
31
|
+
const result = {
|
|
32
|
+
hasMermaid: false,
|
|
33
|
+
hasKatex: false,
|
|
34
|
+
hasDirective: false,
|
|
35
|
+
anyExtension: false,
|
|
36
|
+
};
|
|
37
|
+
if (typeof markdown !== 'string' || markdown.length === 0) {
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
// 整体 try/catch 兜底:极端情况(畸形 markdown / 某些环境的 regex 异常)下不应让组件崩白屏。
|
|
41
|
+
try {
|
|
42
|
+
if (RE_MERMAID_FENCE.test(markdown)) result.hasMermaid = true;
|
|
43
|
+
if (RE_MATH_FENCE.test(markdown) || RE_BLOCK_MATH.test(markdown)) {
|
|
44
|
+
result.hasKatex = true;
|
|
45
|
+
}
|
|
46
|
+
if (RE_DIRECTIVE.test(markdown)) result.hasDirective = true;
|
|
47
|
+
|
|
48
|
+
if (!result.hasKatex && RE_INLINE_MATH) {
|
|
49
|
+
const stripped = stripFences(markdown);
|
|
50
|
+
if (RE_INLINE_MATH.test(stripped)) {
|
|
51
|
+
const inlineMatch = stripped.match(RE_INLINE_MATH);
|
|
52
|
+
if (inlineMatch && /[a-zA-Z\\^_=<>+\-*/(){}[\]]/.test(inlineMatch[1])) {
|
|
53
|
+
result.hasKatex = true;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
} catch {
|
|
58
|
+
// 检测失败时返回保守 false(即把内容当成纯标准 markdown 让 MDXEditor 处理),
|
|
59
|
+
// 避免阻塞用户。如果真有扩展语法,MDXEditor 自身的 onError 会兜底。
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
result.anyExtension = result.hasMermaid || result.hasKatex || result.hasDirective;
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// 持久记忆 markdown 内 <a> 链接拦截:白名单设计——只放行 #anchor 和单段 .md basename。
|
|
2
|
+
// CachePopoverContent.jsx + MemoryDetailModal.jsx 共用,避免双份 paste 漂移。
|
|
3
|
+
//
|
|
4
|
+
// 返回 discriminated union(而非简单 string | null)的理由:
|
|
5
|
+
// 调用方需要区分三种动作——打开 detail / 透传给浏览器(锚点滚动)/ 阻止默认行为。
|
|
6
|
+
// { open: name } / { allow: true } / { reject: true } 让调用方一目了然,
|
|
7
|
+
// 编译期就能 catch 漏分支(不像 string | null 容易把 reject 误当 allow)。
|
|
8
|
+
//
|
|
9
|
+
// 拒绝规则(白名单优先):
|
|
10
|
+
// - 任意 scheme(http://, javascript:, chrome-extension:, custom-app:, x-anything: 等)一律拒绝。
|
|
11
|
+
// 持久记忆面板内不导航外站;上游受信任 markdown 也禁止 deep-link 至浏览器协议处理器。
|
|
12
|
+
// - 绝对路径 / 路径分隔符 / `..` 段 / 隐藏文件(以 . 开头)
|
|
13
|
+
// - 非 .md 后缀
|
|
14
|
+
const ANCHOR_PREFIX = '#';
|
|
15
|
+
|
|
16
|
+
export function parseMemoryLink(hrefRaw) {
|
|
17
|
+
if (!hrefRaw) return { reject: true };
|
|
18
|
+
|
|
19
|
+
const trimmed = hrefRaw.trim();
|
|
20
|
+
if (!trimmed) return { reject: true };
|
|
21
|
+
|
|
22
|
+
// 锚点链接(#section)放过,让浏览器原地滚动
|
|
23
|
+
if (trimmed.startsWith(ANCHOR_PREFIX)) return { allow: true };
|
|
24
|
+
|
|
25
|
+
// 任何 scheme 标记(首段 [a-z][a-z0-9+.-]*: 模式)一律拒绝。
|
|
26
|
+
// 大小写不敏感(toLowerCase 规范化防 `JaVaScRiPt:` 之类绕过)。
|
|
27
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(trimmed)) return { reject: true };
|
|
28
|
+
|
|
29
|
+
// 去掉 query/hash + URL 解码 → 候选 basename
|
|
30
|
+
let candidate;
|
|
31
|
+
try {
|
|
32
|
+
candidate = decodeURIComponent(trimmed.split('#')[0].split('?')[0]);
|
|
33
|
+
} catch {
|
|
34
|
+
return { reject: true };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!candidate) return { reject: true };
|
|
38
|
+
if (candidate.startsWith('/') || candidate.startsWith('\\')) return { reject: true };
|
|
39
|
+
if (candidate.includes('/') || candidate.includes('\\')) return { reject: true };
|
|
40
|
+
if (candidate === '..' || candidate.startsWith('.')) return { reject: true };
|
|
41
|
+
if (!/\.md$/i.test(candidate)) return { reject: true };
|
|
42
|
+
|
|
43
|
+
return { open: candidate };
|
|
44
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Blurred-overlay mask for the seven hamburger-menu feature modals (Log
|
|
2
|
+
// Management, Export user prompts, Plugin Management, CCV Process Manager,
|
|
3
|
+
// Messaging Integration, Hot-Switch Proxy, Edit System Prompt).
|
|
4
|
+
//
|
|
5
|
+
// Values mirror the AskUserQuestion / plan approval overlay (`.backdrop` in
|
|
6
|
+
// src/components/approval/ApprovalModal.module.css) — KEEP IN SYNC, guarded by
|
|
7
|
+
// test/modal-mask.test.js. Applied per Modal instance via the antd semantic
|
|
8
|
+
// `styles={{ mask }}` API: inline style, so it cannot leak to other modals —
|
|
9
|
+
// a global `.ant-modal-mask` rule would hit every Modal and Modal.confirm in
|
|
10
|
+
// the app, which is explicitly out of scope.
|
|
11
|
+
export const BLUR_MASK_STYLE = Object.freeze({
|
|
12
|
+
background: 'rgba(0, 0, 0, 0.45)',
|
|
13
|
+
backdropFilter: 'blur(2px)',
|
|
14
|
+
WebkitBackdropFilter: 'blur(2px)',
|
|
15
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// 汉堡菜单「钉住 → 快捷方式」的全局持久化(localStorage,跨项目共享)。
|
|
2
|
+
// 纯函数:parse / serialize / toggle;localStorage I/O 留在组件侧(AppHeader)。
|
|
3
|
+
// 存储值是一个菜单 key 字符串数组,顺序即用户钉住(pin)的先后顺序。
|
|
4
|
+
|
|
5
|
+
export const PINNED_KEY = 'ccv_pinnedMenuKeys';
|
|
6
|
+
|
|
7
|
+
// 解析 localStorage 原始字符串 → string[]。
|
|
8
|
+
// 任何异常(null / 非 JSON / 非数组)一律降级为 [],并过滤掉非字符串元素 + 去重。
|
|
9
|
+
export function parsePinned(raw) {
|
|
10
|
+
if (typeof raw !== 'string' || raw === '') return [];
|
|
11
|
+
let arr;
|
|
12
|
+
try {
|
|
13
|
+
arr = JSON.parse(raw);
|
|
14
|
+
} catch {
|
|
15
|
+
return [];
|
|
16
|
+
}
|
|
17
|
+
if (!Array.isArray(arr)) return [];
|
|
18
|
+
const seen = new Set();
|
|
19
|
+
const out = [];
|
|
20
|
+
for (const item of arr) {
|
|
21
|
+
if (typeof item !== 'string' || item === '') continue;
|
|
22
|
+
if (seen.has(item)) continue;
|
|
23
|
+
seen.add(item);
|
|
24
|
+
out.push(item);
|
|
25
|
+
}
|
|
26
|
+
return out;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// 序列化 string[] → 可写入 localStorage 的字符串。
|
|
30
|
+
export function serializePinned(arr) {
|
|
31
|
+
return JSON.stringify(Array.isArray(arr) ? arr : []);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// 切换某个 key 的钉住状态:已存在则移除,不存在则追加到末尾(保持插入顺序)。
|
|
35
|
+
// 返回新数组(不修改入参);顺带去重。
|
|
36
|
+
export function togglePinned(arr, key) {
|
|
37
|
+
const list = parsePinnedArray(arr);
|
|
38
|
+
if (typeof key !== 'string' || key === '') return list;
|
|
39
|
+
if (list.includes(key)) return list.filter(k => k !== key);
|
|
40
|
+
return [...list, key];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// 内部:把任意入参规整成「去重的字符串数组」,避免脏数据传播。
|
|
44
|
+
function parsePinnedArray(arr) {
|
|
45
|
+
if (!Array.isArray(arr)) return [];
|
|
46
|
+
const seen = new Set();
|
|
47
|
+
const out = [];
|
|
48
|
+
for (const item of arr) {
|
|
49
|
+
if (typeof item !== 'string' || item === '') continue;
|
|
50
|
+
if (seen.has(item)) continue;
|
|
51
|
+
seen.add(item);
|
|
52
|
+
out.push(item);
|
|
53
|
+
}
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// 把 preset shortcut items 序列化为 onUpdatePreferences 的 API payload。
|
|
2
|
+
// dismissed 是 Set;为 undefined 时不写入 payload,避免覆盖服务端已有值。
|
|
3
|
+
export function buildPresetShortcutsPayload(items, dismissed) {
|
|
4
|
+
const payload = {
|
|
5
|
+
presetShortcuts: items.map(i => {
|
|
6
|
+
const o = { teamName: i.teamName, description: i.description };
|
|
7
|
+
if (i.builtinId) o.builtinId = i.builtinId;
|
|
8
|
+
if (i.modified) o.modified = true;
|
|
9
|
+
return o;
|
|
10
|
+
}),
|
|
11
|
+
};
|
|
12
|
+
if (dismissed) payload.dismissedBuiltinPresets = [...dismissed];
|
|
13
|
+
return payload;
|
|
14
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// Per-project alias persistence.
|
|
2
|
+
//
|
|
3
|
+
// User-facing UI:
|
|
4
|
+
// header「当前项目 cc-viewer」 hover → pencil icon → modal → input → save
|
|
5
|
+
//
|
|
6
|
+
// Effects when set:
|
|
7
|
+
// - document.title overrides projectName (handled by AppBase._applyDocTitle)
|
|
8
|
+
// - header / mobile ctx label append " (alias)" after projectName
|
|
9
|
+
//
|
|
10
|
+
// Storage: localStorage, keyed by projectName. Stale aliases for projects no
|
|
11
|
+
// longer present are harmless (inert until matched projectName reappears).
|
|
12
|
+
//
|
|
13
|
+
// Cross-tab sync: native `storage` event fires in OTHER tabs only. Same-tab
|
|
14
|
+
// updates need explicit pubsub — we expose an EventTarget so AppHeader / Mobile
|
|
15
|
+
// / AppBase pick up local mutations without a full reload.
|
|
16
|
+
|
|
17
|
+
// Key prefix mirrors existing `ccv_` convention (cf. ccv_themeColor / ccv_viewMode).
|
|
18
|
+
// Known limitation: keyed by projectName basename (not full path; cf. interceptor.js _projectName),
|
|
19
|
+
// so two projects sharing a basename (~/work/cc-viewer + ~/tmp/cc-viewer) collide on one alias.
|
|
20
|
+
// Accepted for v1; real fix would key on a full-path hash (server must also send projectPath).
|
|
21
|
+
const KEY_PREFIX = 'ccv_projectAlias_';
|
|
22
|
+
const MAX_LEN = 32; // taskbar typically truncates 30-50; 32 is the realistic cap.
|
|
23
|
+
|
|
24
|
+
// Same-tab pubsub channel. EventTarget is native everywhere we ship (Electron,
|
|
25
|
+
// modern web) — no library, no leaked listeners on module reload.
|
|
26
|
+
// Event payload: { detail: { projectName, alias } }
|
|
27
|
+
let _bus = typeof EventTarget !== 'undefined' ? new EventTarget() : null;
|
|
28
|
+
const EVT_CHANGE = 'change';
|
|
29
|
+
|
|
30
|
+
function _keyFor(projectName) {
|
|
31
|
+
if (!projectName || typeof projectName !== 'string') return null;
|
|
32
|
+
return `${KEY_PREFIX}${projectName}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Normalise user input. Strips:
|
|
36
|
+
// - C0/C1 control characters U+0000-U+001F, U+007F-U+009F (render as garbage / NUL)
|
|
37
|
+
// - Standard whitespace controls (\r \n \t are within C0)
|
|
38
|
+
// - Unicode line/paragraph separators
|
|
39
|
+
// - BiDi overrides - and - — a paste containing
|
|
40
|
+
// RIGHT-TO-LEFT-OVERRIDE flips surrounding chrome text direction tab-wide.
|
|
41
|
+
// No current exploit but trivial to block (review P1-A).
|
|
42
|
+
// Stripped sequences collapse to a single space, then trim + truncate to MAX_LEN.
|
|
43
|
+
// Empty result → caller treats as "clear".
|
|
44
|
+
export function normalizeAlias(raw) {
|
|
45
|
+
if (typeof raw !== 'string') return '';
|
|
46
|
+
return raw
|
|
47
|
+
.replace(/[\u0000-\u001f\u007f-\u009f\u2028\u2029\u202a-\u202e\u2066-\u2069]+/g, ' ')
|
|
48
|
+
.trim()
|
|
49
|
+
.slice(0, MAX_LEN);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function getProjectAlias(projectName) {
|
|
53
|
+
const key = _keyFor(projectName);
|
|
54
|
+
if (!key) return '';
|
|
55
|
+
try {
|
|
56
|
+
const v = localStorage.getItem(key);
|
|
57
|
+
return typeof v === 'string' ? v : '';
|
|
58
|
+
} catch { return ''; }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Returns true on success, false on failure (quota / private mode / disabled).
|
|
62
|
+
// Normalisation happens here so callers (Modal save handler / migrations) can't
|
|
63
|
+
// store malformed values. Empty after normalisation → clearProjectAlias path.
|
|
64
|
+
export function setProjectAlias(projectName, raw) {
|
|
65
|
+
const key = _keyFor(projectName);
|
|
66
|
+
if (!key) return false;
|
|
67
|
+
const value = normalizeAlias(raw);
|
|
68
|
+
try {
|
|
69
|
+
if (value === '') {
|
|
70
|
+
localStorage.removeItem(key);
|
|
71
|
+
} else {
|
|
72
|
+
localStorage.setItem(key, value);
|
|
73
|
+
}
|
|
74
|
+
_emitChange(projectName, value);
|
|
75
|
+
return true;
|
|
76
|
+
} catch { return false; }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function clearProjectAlias(projectName) {
|
|
80
|
+
const key = _keyFor(projectName);
|
|
81
|
+
if (!key) return false;
|
|
82
|
+
try {
|
|
83
|
+
localStorage.removeItem(key);
|
|
84
|
+
_emitChange(projectName, '');
|
|
85
|
+
return true;
|
|
86
|
+
} catch { return false; }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function _emitChange(projectName, alias) {
|
|
90
|
+
if (!_bus) return;
|
|
91
|
+
try {
|
|
92
|
+
_bus.dispatchEvent(new CustomEvent(EVT_CHANGE, { detail: { projectName, alias } }));
|
|
93
|
+
} catch {
|
|
94
|
+
// Subscriber threw — swallowed so the localStorage write isn't undone.
|
|
95
|
+
// dispatchEvent itself can also fail in some environments where CustomEvent
|
|
96
|
+
// isn't a real constructor; both cases are non-fatal for persistence.
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Subscribe to alias changes for a specific project — fires for both
|
|
101
|
+
// same-tab updates (via internal EventTarget) and cross-tab updates
|
|
102
|
+
// (via window 'storage' event).
|
|
103
|
+
// onChange(alias) — called with the new alias string ('' = cleared).
|
|
104
|
+
// Returns an unsubscribe function.
|
|
105
|
+
export function subscribeToAlias(projectName, onChange) {
|
|
106
|
+
if (!projectName || typeof onChange !== 'function') return () => {};
|
|
107
|
+
const targetKey = _keyFor(projectName);
|
|
108
|
+
|
|
109
|
+
const sameTabHandler = (e) => {
|
|
110
|
+
if (e?.detail?.projectName === projectName) onChange(e.detail.alias || '');
|
|
111
|
+
};
|
|
112
|
+
const crossTabHandler = (e) => {
|
|
113
|
+
if (e?.key === targetKey) onChange(e.newValue || '');
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
if (_bus) _bus.addEventListener(EVT_CHANGE, sameTabHandler);
|
|
117
|
+
if (typeof window !== 'undefined') window.addEventListener('storage', crossTabHandler);
|
|
118
|
+
|
|
119
|
+
return () => {
|
|
120
|
+
if (_bus) _bus.removeEventListener(EVT_CHANGE, sameTabHandler);
|
|
121
|
+
if (typeof window !== 'undefined') window.removeEventListener('storage', crossTabHandler);
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Test-only: replaces the internal EventTarget bus so a test run starts with
|
|
126
|
+
// no leftover listeners from prior test files. Also useful in dev for HMR
|
|
127
|
+
// recovery (review P1-C). Never call from production code — listeners on the
|
|
128
|
+
// old bus stop receiving updates.
|
|
129
|
+
export function __resetBusForTest() {
|
|
130
|
+
_bus = typeof EventTarget !== 'undefined' ? new EventTarget() : null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Exported for tests — lets unit tests assert key shape without poking at
|
|
134
|
+
// localStorage internals.
|
|
135
|
+
export const _internals = { KEY_PREFIX, MAX_LEN, _keyFor };
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt classification utilities for PTY prompt detection.
|
|
3
|
+
* Shared by ChatView.jsx and ChatMessage.jsx.
|
|
4
|
+
*
|
|
5
|
+
* NOTE: test/permission-detect.test.js 直接 import 本模块的真实现(不再内嵌副本),
|
|
6
|
+
* 修改本文件后跑该测试即可验证行为。
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export function isPlanApprovalPrompt(prompt) {
|
|
10
|
+
if (!prompt || !prompt.question) return false;
|
|
11
|
+
const q = prompt.question.toLowerCase();
|
|
12
|
+
// 文本判定:question 含 "plan" + (approv/proceed/accept)
|
|
13
|
+
if (/plan/i.test(q) && (/approv/i.test(q) || /proceed/i.test(q) || /accept/i.test(q))) {
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
// Options 模式兜底:Claude CLI plan inquirer prompt 不一定带 "plan" 关键字
|
|
17
|
+
// (如 "Would you like to proceed?"),但典型有 3 选项 = Approve / Approve with edits / Reject。
|
|
18
|
+
// 通过这个模式识别,保证 cliMode 下 plan modal 能弹出。
|
|
19
|
+
// 与 isDangerousOperationPrompt 的 2 选项 allow/deny 兜底正交(dangerous L15 早退避免冲突)。
|
|
20
|
+
if (Array.isArray(prompt.options) && prompt.options.length === 3) {
|
|
21
|
+
const texts = prompt.options.map(o => (o.text || '').toLowerCase());
|
|
22
|
+
const hasApprove = texts.some(t => /\bapprov/i.test(t));
|
|
23
|
+
const hasEdit = texts.some(t => /\bedits?\b|\bmodify/i.test(t));
|
|
24
|
+
const hasReject = texts.some(t => /\breject\b|\bdeny\b|keep planning|\bno,/i.test(t));
|
|
25
|
+
if (hasApprove && hasEdit && hasReject) return true;
|
|
26
|
+
}
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Pick the option number that APPROVES a plan, for auto-approval.
|
|
32
|
+
* Mirrors the approve-button heuristic in ChatMessage._renderTool_ExitPlanMode:
|
|
33
|
+
* the first option whose text matches /yes|approve|accept|proceed/i AND is NOT a
|
|
34
|
+
* feedback/edit option (those open a textarea on manual click, so auto-submitting them
|
|
35
|
+
* would be wrong); otherwise the first option; otherwise 1 (the CLI default "approve").
|
|
36
|
+
* Never returns a reject or feedback option when a plain approve option exists.
|
|
37
|
+
*
|
|
38
|
+
* @param {Array<{number:number,text?:string}>} options - plan prompt options
|
|
39
|
+
* @returns {number} the option number to send for approval
|
|
40
|
+
*/
|
|
41
|
+
export function pickPlanApproveOptionNumber(options) {
|
|
42
|
+
if (!Array.isArray(options) || options.length === 0) return 1;
|
|
43
|
+
const approve = options.find(o => {
|
|
44
|
+
const text = (o && o.text) || '';
|
|
45
|
+
return /yes|approve|accept|proceed/i.test(text) && !/type|tell|change|feedback|edit/i.test(text);
|
|
46
|
+
});
|
|
47
|
+
if (approve && typeof approve.number === 'number') return approve.number;
|
|
48
|
+
const first = options[0];
|
|
49
|
+
return (first && typeof first.number === 'number') ? first.number : 1;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function isDangerousOperationPrompt(prompt) {
|
|
53
|
+
if (!prompt || !prompt.question) return false;
|
|
54
|
+
const q = prompt.question;
|
|
55
|
+
if (isPlanApprovalPrompt(prompt)) return false;
|
|
56
|
+
// Match Claude Code permission prompt patterns:
|
|
57
|
+
// - "Do you want to make this edit" / "Do you want to write" / "Do you want to proceed"
|
|
58
|
+
// - "Allow X to Y" / "Want to allow" / "wants to (execute|run|...)"
|
|
59
|
+
// - "May Claude read/write/execute..." / "grant access/permission" / "permit"
|
|
60
|
+
if (/do you want to (make this edit|write|proceed|create|delete)|allow\b.*\bto\b|want to allow|wants to (execute|run|read|write|access|create|delete|modify|use)|may .*(read|write|execute|run|access|create|delete|modify)|grant .*(access|permission)|permit/i.test(q)) {
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
// Also detect by options: if options contain both Allow/Yes and Deny/No keywords
|
|
64
|
+
if (prompt.options && prompt.options.length >= 2) {
|
|
65
|
+
const texts = prompt.options.map(o => (o.text || '').toLowerCase());
|
|
66
|
+
const hasAllow = texts.some(t => /^allow|^yes/i.test(t));
|
|
67
|
+
const hasDeny = texts.some(t => /^no$|^no[^a-z]|^deny|^reject/i.test(t));
|
|
68
|
+
if (hasAllow && hasDeny) return true;
|
|
69
|
+
}
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Parse tool name and input from the PTY buffer surrounding a detected prompt.
|
|
75
|
+
* Used to populate ToolApprovalPanel for subAgent permission prompts that bypass hooks.
|
|
76
|
+
*
|
|
77
|
+
* @param {string} buf - ANSI-stripped PTY buffer (up to 4KB)
|
|
78
|
+
* @param {string} question - Detected prompt question text
|
|
79
|
+
* @param {Array} options - Detected prompt options [{text, number, selected}]
|
|
80
|
+
* @returns {{ toolName: string, input: object }}
|
|
81
|
+
*/
|
|
82
|
+
export function parseToolInfoFromBuffer(buf, question, options) {
|
|
83
|
+
const q = (question || '').toLowerCase();
|
|
84
|
+
const optTexts = (options || []).map(o => o.text || '').join(' ').toLowerCase();
|
|
85
|
+
|
|
86
|
+
// 1. Bash: buffer contains "Bash command" or "Run shell command"
|
|
87
|
+
if (/bash command|run shell command/i.test(buf)) {
|
|
88
|
+
const cmdMatch = buf.match(/(?:Bash command|Run shell command)\s*\n\s*\n((?:\s{4,}.+\n?)+)/i);
|
|
89
|
+
const cmd = cmdMatch ? cmdMatch[1].replace(/^\s{4}/gm, '').trim() : null;
|
|
90
|
+
return { toolName: 'Bash', input: cmd ? { command: cmd } : { description: question } };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// 2. Edit: "make this edit to <path>"
|
|
94
|
+
const editMatch = q.match(/make this edit to\s+(.+?)\s*\??$/);
|
|
95
|
+
if (editMatch) return { toolName: 'Edit', input: { file_path: editMatch[1] } };
|
|
96
|
+
|
|
97
|
+
// 3. Write: "write" + path
|
|
98
|
+
const writeMatch = q.match(/write (?:this new file|to)\s+(.+?)\s*\??$/);
|
|
99
|
+
if (writeMatch) return { toolName: 'Write', input: { file_path: writeMatch[1] } };
|
|
100
|
+
if (/write/i.test(q)) return { toolName: 'Write', input: { description: question } };
|
|
101
|
+
|
|
102
|
+
// 4. Read: "read <path>" or options contain "allow reading from <path>"
|
|
103
|
+
const readMatch = q.match(/read\s+(.+?)\s*\??$/) || optTexts.match(/allow reading from\s+(.+?)(?:\s+from this project)?/);
|
|
104
|
+
if (readMatch) return { toolName: 'Read', input: { file_path: readMatch[1] } };
|
|
105
|
+
|
|
106
|
+
// 5. WebFetch/WebSearch
|
|
107
|
+
if (/fetch|url/i.test(q)) return { toolName: 'WebFetch', input: { description: question } };
|
|
108
|
+
if (/search/i.test(q)) return { toolName: 'WebSearch', input: { description: question } };
|
|
109
|
+
|
|
110
|
+
// 6. Fallback
|
|
111
|
+
return { toolName: 'Tool', input: { description: question } };
|
|
112
|
+
}
|