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.
Files changed (123) hide show
  1. package/dist/assets/App-B224r-8M.js +2 -0
  2. package/dist/assets/{MdxEditorPanel-C5Q6uUyH.js → MdxEditorPanel-BUubiE6Y.js} +1 -1
  3. package/dist/assets/{Mobile-MoBp9c8k.js → Mobile-CKmfRRvo.js} +1 -1
  4. package/dist/assets/index-CjsQEQc2.js +2 -0
  5. package/dist/assets/seqResourceLoaders-C-T_EMiO.js +2 -0
  6. package/dist/assets/{seqResourceLoaders-CxuD1CYl.css → seqResourceLoaders-DFrHmgnK.css} +1 -1
  7. package/dist/index.html +1 -1
  8. package/package.json +2 -1
  9. package/server/lib/log-management.js +1 -0
  10. package/server/lib/log-watcher.js +29 -3
  11. package/server/lib/stats-worker.js +21 -2
  12. package/server/lib/v2/adapter.js +123 -18
  13. package/server/lib/v2/journal.js +9 -5
  14. package/server/lib/v2/jsonl-read.js +127 -0
  15. package/server/lib/v2/live-feed.js +222 -20
  16. package/server/lib/v2/meta-rows.js +361 -0
  17. package/server/lib/v2/replay.js +5 -5
  18. package/server/lib/v2/session-select.js +69 -2
  19. package/server/lib/v2/v2-writer.js +15 -0
  20. package/server/lib/wire-compress.js +167 -0
  21. package/server/routes/events.js +96 -26
  22. package/server/routes/im.js +1 -1
  23. package/server/routes/logs.js +15 -12
  24. package/server/routes/v2.js +60 -0
  25. package/server/routes/workspaces.js +7 -6
  26. package/server/server.js +23 -1
  27. package/server/workspace-registry.js +5 -0
  28. package/src/utils/apiUrl.js +26 -0
  29. package/src/utils/askFallback.js +21 -0
  30. package/src/utils/askOptionDesc.js +55 -0
  31. package/src/utils/askPortalMatcher.js +54 -0
  32. package/src/utils/autoApproveOptions.js +27 -0
  33. package/src/utils/avatarAnimationPostPass.js +49 -0
  34. package/src/utils/builtinPresets.js +18 -0
  35. package/src/utils/clearCheckpoint.js +7 -0
  36. package/src/utils/commandValidator.js +45 -0
  37. package/src/utils/contentFilter.js +620 -0
  38. package/src/utils/contextRaw.js +18 -0
  39. package/src/utils/contextSidebarNavigation.js +16 -0
  40. package/src/utils/contextTurns.js +117 -0
  41. package/src/utils/displayScaleHelper.js +47 -0
  42. package/src/utils/effectiveModel.js +13 -0
  43. package/src/utils/entry-slim.js +563 -0
  44. package/src/utils/entryCache.js +144 -0
  45. package/src/utils/errorReport.js +6 -0
  46. package/src/utils/fileExpandedPathsStorage.js +70 -0
  47. package/src/utils/fileIcons.jsx +36 -0
  48. package/src/utils/fileOpen.js +37 -0
  49. package/src/utils/formatters.js +48 -0
  50. package/src/utils/gitApi.js +50 -0
  51. package/src/utils/gitTreeBuilder.js +19 -0
  52. package/src/utils/helpers.js +846 -0
  53. package/src/utils/identityHeal.js +99 -0
  54. package/src/utils/imConnState.js +59 -0
  55. package/src/utils/imOrigin.js +24 -0
  56. package/src/utils/imTr.js +7 -0
  57. package/src/utils/imageCompress.js +83 -0
  58. package/src/utils/imageDownscale.js +28 -0
  59. package/src/utils/imageResize.js +112 -0
  60. package/src/utils/ingestPipeline.js +47 -0
  61. package/src/utils/lazyWithReload.js +53 -0
  62. package/src/utils/markdown.js +30 -0
  63. package/src/utils/markdownIncremental.js +39 -0
  64. package/src/utils/markdownProfiler.js +113 -0
  65. package/src/utils/mdExtensionDetect.js +64 -0
  66. package/src/utils/memoryLinkParser.js +44 -0
  67. package/src/utils/modalMask.js +15 -0
  68. package/src/utils/monotime.js +6 -0
  69. package/src/utils/pinnedMenu.js +55 -0
  70. package/src/utils/presetShortcuts.js +14 -0
  71. package/src/utils/projectAlias.js +135 -0
  72. package/src/utils/promptClassifier.js +112 -0
  73. package/src/utils/promptDetect.js +196 -0
  74. package/src/utils/promptNav.js +60 -0
  75. package/src/utils/ptyChunkBuilder.js +251 -0
  76. package/src/utils/quickMenuHoverIntent.js +35 -0
  77. package/src/utils/rateLimitParser.js +119 -0
  78. package/src/utils/readResultPool.js +106 -0
  79. package/src/utils/refreshCachedItemProp.js +30 -0
  80. package/src/utils/requestType.js +208 -0
  81. package/src/utils/resizeCalc.js +21 -0
  82. package/src/utils/resolveLocalized.js +17 -0
  83. package/src/utils/searchApi.js +47 -0
  84. package/src/utils/searchReplace.js +51 -0
  85. package/src/utils/seqResourceLoaders.js +79 -0
  86. package/src/utils/sessionManager.js +471 -0
  87. package/src/utils/sessionMerge.js +180 -0
  88. package/src/utils/skillModalController.js +111 -0
  89. package/src/utils/skillsParser.js +116 -0
  90. package/src/utils/slashCommandLabels.js +80 -0
  91. package/src/utils/splitDragCalc.js +73 -0
  92. package/src/utils/stickyBottomController.js +643 -0
  93. package/src/utils/svgSanitize.js +48 -0
  94. package/src/utils/systemTags.js +47 -0
  95. package/src/utils/tClaude.js +20 -0
  96. package/src/utils/teamModalBuilder.js +351 -0
  97. package/src/utils/teamSessionParser.js +190 -0
  98. package/src/utils/teammateAvatars.js +194 -0
  99. package/src/utils/teammateDetector.js +121 -0
  100. package/src/utils/termDiag.js +114 -0
  101. package/src/utils/terminalClipboard.js +117 -0
  102. package/src/utils/terminalWriteQueue.js +398 -0
  103. package/src/utils/thinkingMerge.js +49 -0
  104. package/src/utils/toolCatalog.js +29 -0
  105. package/src/utils/toolResultBuilder.js +393 -0
  106. package/src/utils/toolResultClassifier.js +16 -0
  107. package/src/utils/toolResultCore.js +167 -0
  108. package/src/utils/toolsDiff.js +44 -0
  109. package/src/utils/toolsXmlFormatter.js +3 -0
  110. package/src/utils/ultraplanController.js +109 -0
  111. package/src/utils/ultraplanExperts.js +86 -0
  112. package/src/utils/ultraplanTemplates.js +154 -0
  113. package/src/utils/userImageRefs.js +49 -0
  114. package/src/utils/v3Assembler.js +112 -0
  115. package/src/utils/v3Rows.js +48 -0
  116. package/src/utils/voicePackPlayer.js +233 -0
  117. package/src/utils/webSearchGrouping.js +111 -0
  118. package/src/utils/workflowFormat.js +52 -0
  119. package/src/utils/workflowRuns.js +118 -0
  120. package/src/utils/workflowStore.js +94 -0
  121. package/dist/assets/App-DTkacfRW.js +0 -2
  122. package/dist/assets/index-Ac8vnV3v.js +0 -2
  123. package/dist/assets/seqResourceLoaders-Cd_TAXTb.js +0 -2
@@ -0,0 +1,144 @@
1
+ const DB_NAME = 'ccv_entryCache';
2
+ const STORE_NAME = 'entries';
3
+ const CACHE_KEY = 'cache';
4
+ const META_KEY = 'ccv_cacheMeta';
5
+ const DB_VERSION = 2;
6
+ const MAX_AGE = 7 * 24 * 60 * 60 * 1000; // 7 天过期
7
+
8
+ // 单例 DB 连接,避免每次操作都重新打开
9
+ let _dbInstance = null;
10
+ let _dbPromise = null;
11
+
12
+ function getDB() {
13
+ if (_dbInstance) return Promise.resolve(_dbInstance);
14
+ if (_dbPromise) return _dbPromise;
15
+ _dbPromise = new Promise((resolve, reject) => {
16
+ const req = indexedDB.open(DB_NAME, DB_VERSION);
17
+ req.onupgradeneeded = () => {
18
+ const db = req.result;
19
+ if (!db.objectStoreNames.contains(STORE_NAME)) {
20
+ db.createObjectStore(STORE_NAME);
21
+ }
22
+ };
23
+ req.onsuccess = () => {
24
+ _dbInstance = req.result;
25
+ _dbInstance.onclose = () => { _dbInstance = null; };
26
+ _dbPromise = null;
27
+ resolve(_dbInstance);
28
+ };
29
+ req.onerror = () => {
30
+ _dbPromise = null;
31
+ reject(req.error);
32
+ };
33
+ });
34
+ return _dbPromise;
35
+ }
36
+
37
+ // 写入序列化:丢弃排队中的过时写入,只保留最新一次
38
+ let _writeId = 0;
39
+
40
+ function saveMeta(projectName, entries) {
41
+ try {
42
+ const last = entries[entries.length - 1];
43
+ const lastTs = last?.timestamp || null;
44
+ if (lastTs) {
45
+ localStorage.setItem(META_KEY, JSON.stringify({ projectName, lastTs, count: entries.length }));
46
+ }
47
+ } catch {
48
+ // 静默
49
+ }
50
+ }
51
+
52
+ function clearMeta() {
53
+ try {
54
+ localStorage.removeItem(META_KEY);
55
+ } catch {
56
+ // 静默
57
+ }
58
+ }
59
+
60
+ /**
61
+ * 同步读取缓存元数据(用于 initSSE 构造增量请求参数)
62
+ * @returns {{ projectName: string, lastTs: string, count: number } | null}
63
+ */
64
+ export function getCacheMeta() {
65
+ try {
66
+ const raw = localStorage.getItem(META_KEY);
67
+ if (!raw) return null;
68
+ const meta = JSON.parse(raw);
69
+ if (meta?.lastTs && meta?.count > 0 && meta?.projectName) return meta;
70
+ return null;
71
+ } catch {
72
+ return null;
73
+ }
74
+ }
75
+
76
+ // Wire v3 (V3.S3a): persisted-record protocol gate. NOT a DB_VERSION bump —
77
+ // a version bump wipes every user's cache at upgrade time regardless of the
78
+ // CCV_WIRE_V3 flag (breaking the dark launch); tagging records instead means
79
+ // only a genuine flag-state skew between save and load invalidates.
80
+ const PROTOCOL_VERSION = 1;
81
+
82
+ export async function saveEntries(projectName, entries) {
83
+ if (!projectName || !Array.isArray(entries) || entries.length === 0) return;
84
+ const myId = ++_writeId;
85
+ try {
86
+ const db = await getDB();
87
+ // 被更新的写入请求取代,丢弃本次
88
+ if (myId !== _writeId) return;
89
+ saveMeta(projectName, entries);
90
+ return new Promise((resolve) => {
91
+ const tx = db.transaction(STORE_NAME, 'readwrite');
92
+ tx.objectStore(STORE_NAME).put({ projectName, entries, ts: Date.now(), protocolVersion: PROTOCOL_VERSION }, CACHE_KEY);
93
+ tx.oncomplete = () => resolve();
94
+ tx.onerror = () => resolve();
95
+ });
96
+ } catch {
97
+ // 静默
98
+ }
99
+ }
100
+
101
+ export async function loadEntries(projectName) {
102
+ try {
103
+ const db = await getDB();
104
+ return new Promise((resolve) => {
105
+ const tx = db.transaction(STORE_NAME, 'readonly');
106
+ const req = tx.objectStore(STORE_NAME).get(CACHE_KEY);
107
+ req.onsuccess = () => {
108
+ const data = req.result;
109
+ if (!data || data.projectName !== projectName || !Array.isArray(data.entries) || data.entries.length === 0) {
110
+ resolve(null);
111
+ } else if ((data.protocolVersion || 0) > PROTOCOL_VERSION) {
112
+ // Written by a newer client shape than this build understands.
113
+ clearEntries();
114
+ resolve(null);
115
+ } else if (data.ts && Date.now() - data.ts > MAX_AGE) {
116
+ // 缓存超过 7 天,清除并返回 null
117
+ clearEntries();
118
+ resolve(null);
119
+ } else {
120
+ resolve(data.entries);
121
+ }
122
+ };
123
+ req.onerror = () => resolve(null);
124
+ });
125
+ } catch {
126
+ return null;
127
+ }
128
+ }
129
+
130
+ export async function clearEntries() {
131
+ clearMeta();
132
+ try {
133
+ const db = await getDB();
134
+ return new Promise((resolve) => {
135
+ const tx = db.transaction(STORE_NAME, 'readwrite');
136
+ tx.objectStore(STORE_NAME).delete(CACHE_KEY);
137
+ tx.oncomplete = () => resolve();
138
+ tx.onerror = () => resolve();
139
+ });
140
+ } catch {
141
+ // 静默
142
+ }
143
+ }
144
+
@@ -0,0 +1,6 @@
1
+ // Re-export shell — the implementation moved VERBATIM to server/lib/error-report.js
2
+ // in wire-v2 step S2: server-side modules (server/lib/v2/*) must honor the same
3
+ // reportSwallowed convention (CLAUDE.md), and `src/` is not shipped in the npm
4
+ // package (`files` array), so the canonical home has to live under server/.
5
+ // The module is dependency-free (console.warn + Map), hence CLIENT-SAFE.
6
+ export { reportSwallowed, MAX_REPORTS_PER_TAG, _resetForTest } from '../../server/lib/error-report.js';
@@ -0,0 +1,70 @@
1
+ // 文件浏览器 + GitChanges 文件夹层级状态的 sessionStorage 持久化。
2
+ //
3
+ // 设计要点:
4
+ // - sessionStorage 跟 tab 生命周期绑定;按 projectName 拆 key,避免不同项目状态串扰。
5
+ // - projectName 由后端 /api/project-name 返回,已在 interceptor.js 里 sanitize 成 [a-zA-Z0-9_\-\.],
6
+ // 可直接拼 key 后缀,无需 encodeURIComponent。
7
+ // - 空 projectName(启动期 race / 本地日志模式):跳过读写,避免污染 ":" 这种孤儿 key。
8
+ // - Safari Private Mode / 容量满 / sandbox:sessionStorage 访问会抛,外层 try/catch 兜底返回空 Set。
9
+ // - 旧值损坏(被扩展改坏、非数组):JSON.parse 失败回退空 Set。
10
+ //
11
+ // 两个 bucket 语义反向但独立:
12
+ // FILE_PREFIX 存"展开"集合(FileExplorer 默认全折叠,展开的存)
13
+ // GIT_COLLAPSED_PREFIX 存"折叠"集合(GitChanges 默认全展开,折叠的存)
14
+ // 由 wrapper 函数名(loadExpandedPaths vs loadGitChangesCollapsedDirs)明示,
15
+ // 防 maintainer 混用。
16
+
17
+ const FILE_PREFIX = 'ccv_fileExpandedPaths:';
18
+ const GIT_COLLAPSED_PREFIX = 'ccv_gitChangesCollapsedDirs:';
19
+
20
+ function keyFor(prefix, projectName) {
21
+ if (!projectName || typeof projectName !== 'string') return null;
22
+ return prefix + projectName;
23
+ }
24
+
25
+ function loadSet(prefix, projectName) {
26
+ const key = keyFor(prefix, projectName);
27
+ if (!key) return new Set();
28
+ try {
29
+ const raw = sessionStorage.getItem(key);
30
+ if (!raw) return new Set();
31
+ const arr = JSON.parse(raw);
32
+ if (!Array.isArray(arr)) return new Set();
33
+ return new Set(arr.filter(p => typeof p === 'string'));
34
+ } catch {
35
+ return new Set();
36
+ }
37
+ }
38
+
39
+ function saveSet(prefix, projectName, set) {
40
+ const key = keyFor(prefix, projectName);
41
+ if (!key) return;
42
+ try {
43
+ // 空集走 removeItem 而非写 '[]':避免 sessionStorage 留孤儿 entry,并让下次
44
+ // load 的 `!raw` 快路径生效;与 load 的"无值 → 空 Set"语义对齐。
45
+ if (!set || set.size === 0) {
46
+ sessionStorage.removeItem(key);
47
+ return;
48
+ }
49
+ sessionStorage.setItem(key, JSON.stringify(Array.from(set)));
50
+ } catch {
51
+ /* private mode / quota — 忽略 */
52
+ }
53
+ }
54
+
55
+ // FileExplorer:存"展开"集合,默认全折叠
56
+ export function loadExpandedPaths(projectName) {
57
+ return loadSet(FILE_PREFIX, projectName);
58
+ }
59
+ export function saveExpandedPaths(projectName, set) {
60
+ saveSet(FILE_PREFIX, projectName, set);
61
+ }
62
+
63
+ // GitChanges:存"折叠"集合(key 形态 `${repoPath}::${dirPath}`),默认全展开。
64
+ // 语义跟 FileExplorer 反向,函数名明示防混用。
65
+ export function loadGitChangesCollapsedDirs(projectName) {
66
+ return loadSet(GIT_COLLAPSED_PREFIX, projectName);
67
+ }
68
+ export function saveGitChangesCollapsedDirs(projectName, set) {
69
+ saveSet(GIT_COLLAPSED_PREFIX, projectName, set);
70
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * 文件/文件夹图标 — 统一管理扩展名颜色和 SVG 渲染。
3
+ *
4
+ * FileExplorer、GitChanges 等组件共用此模块,
5
+ * 新增文件类型颜色只需修改 EXT_COLORS。
6
+ */
7
+ import React from 'react';
8
+
9
+ const EXT_COLORS = {
10
+ js: '#e8d44d', jsx: '#61dafb', ts: '#3178c6', tsx: '#3178c6',
11
+ json: '#999', md: '#519aba', css: '#a86fd9', scss: '#cd6799', less: '#a86fd9',
12
+ html: '#e34c26', htm: '#e34c26', xml: '#e34c26',
13
+ py: '#3572a5', go: '#00add8', rs: '#dea584', rb: '#cc342d',
14
+ java: '#b07219', c: '#555', cpp: '#f34b7d', h: '#555',
15
+ sh: '#4eaa25', bash: '#4eaa25', zsh: '#4eaa25',
16
+ yml: '#cb171e', yaml: '#cb171e', toml: '#999',
17
+ svg: '#e34c26', png: '#a86fd9', jpg: '#a86fd9', jpeg: '#a86fd9', gif: '#a86fd9', bmp: '#a86fd9', ico: '#a86fd9', icns: '#a86fd9', webp: '#a86fd9', avif: '#a86fd9',
18
+ };
19
+
20
+ export function getFileIcon(name, type) {
21
+ if (type === 'directory') {
22
+ return (
23
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="var(--color-accent-yellow)" stroke="none">
24
+ <path d="M2 6c0-1.1.9-2 2-2h5l2 2h9a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V6z"/>
25
+ </svg>
26
+ );
27
+ }
28
+ const ext = name.includes('.') ? name.split('.').pop().toLowerCase() : '';
29
+ const color = EXT_COLORS[ext] || '#888';
30
+ return (
31
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth="1.5">
32
+ <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
33
+ <polyline points="14 2 14 8 20 8"/>
34
+ </svg>
35
+ );
36
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * 文件打开策略 — 统一管理哪些文件类型应由系统默认应用打开。
3
+ *
4
+ * 所有需要判断"是否用系统应用打开"的调用方都通过此模块,
5
+ * 以后新增类型只需修改这里。
6
+ */
7
+
8
+ import { apiUrl } from './apiUrl';
9
+
10
+ /** 需要系统默认应用打开的扩展名 */
11
+ const SYSTEM_OPEN_EXTS = new Set([
12
+ 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx',
13
+ 'odt', 'ods', 'odp', 'pdf',
14
+ ]);
15
+
16
+ /**
17
+ * 尝试用系统默认应用打开文件。
18
+ * 如果文件扩展名匹配 SYSTEM_OPEN_EXTS,调用 /api/open-file 并返回 true;
19
+ * 否则返回 false,由调用方自行处理。
20
+ *
21
+ * @param {string} path - 文件路径
22
+ * @param {string} source - 调用来源,用于区分不同场景的潜在交互差异
23
+ * 'file-explorer' | 'git-changes' | 'git-diff' | 'chat-message'
24
+ * @returns {boolean} 是否已处理
25
+ */
26
+ export function tryOpenWithSystem(path, source) {
27
+ const ext = (path || '').split('.').pop().toLowerCase();
28
+ if (!SYSTEM_OPEN_EXTS.has(ext)) return false;
29
+
30
+ fetch(apiUrl('/api/open-file'), {
31
+ method: 'POST',
32
+ headers: { 'Content-Type': 'application/json' },
33
+ body: JSON.stringify({ path, source }),
34
+ }).catch(() => {});
35
+
36
+ return true;
37
+ }
@@ -0,0 +1,48 @@
1
+ // 共享展示格式化工具。
2
+ // formatSize 用 4 档 (B/KB/MB/GB) — 与 WorkspaceList 旧版语义一致,覆盖原 LogTable 的 3 档版本。
3
+ // formatTimestamp 接 cc-viewer 日志 ts 字符串 (YYYYMMDD_HHMMSS...);mobile=true 时省略年份。
4
+ export function formatSize(bytes) {
5
+ if (!bytes) return '0 B';
6
+ const units = ['B', 'KB', 'MB', 'GB'];
7
+ let i = 0;
8
+ let size = bytes;
9
+ while (size >= 1024 && i < units.length - 1) { size /= 1024; i++; }
10
+ return `${size.toFixed(i > 0 ? 1 : 0)} ${units[i]}`;
11
+ }
12
+
13
+ export function formatTimestamp(ts, mobile) {
14
+ if (!ts || ts.length < 15) return ts;
15
+ if (mobile) return `${ts.slice(4, 6)}-${ts.slice(6, 8)} ${ts.slice(9, 11)}:${ts.slice(11, 13)}:${ts.slice(13, 15)}`;
16
+ return `${ts.slice(0, 4)}-${ts.slice(4, 6)}-${ts.slice(6, 8)} ${ts.slice(9, 11)}:${ts.slice(11, 13)}:${ts.slice(13, 15)}`;
17
+ }
18
+
19
+ // 共享时钟格式化原语(本地时区,零填充)。formatPromptNavTime(Prompt 导航)与
20
+ // ChatMessage.formatTime(气泡时间)复用同一实现,使「HH:MM:SS」/「MM-DD HH:MM:SS」格式
21
+ // 单一来源、无需再手工同步两处。入参为已构造的 Date。
22
+ const _pad2 = (n) => String(n).padStart(2, '0');
23
+ export function formatHms(d) {
24
+ return `${_pad2(d.getHours())}:${_pad2(d.getMinutes())}:${_pad2(d.getSeconds())}`;
25
+ }
26
+ export function formatMonthDayTime(d) {
27
+ return `${_pad2(d.getMonth() + 1)}-${_pad2(d.getDate())} ${formatHms(d)}`;
28
+ }
29
+
30
+ // formatPromptNavTime 接 ISO 8601 / Date-可解析字符串(消息的 _timestamp,如 ChatView 用户 Prompt
31
+ // 导航传入的 props.timestamp),输出本地时区的 "MM-DD HH:MM:SS"。缺失/非法 → ''。
32
+ export function formatPromptNavTime(ts) {
33
+ if (!ts) return '';
34
+ try {
35
+ const d = new Date(ts);
36
+ if (isNaN(d.getTime())) return '';
37
+ return formatMonthDayTime(d);
38
+ } catch { return ''; }
39
+ }
40
+
41
+ // Single source of truth for the context-fullness severity color, shared by the
42
+ // desktop header bar, the mobile header tag, and the cache popover so all three
43
+ // surfaces always agree for the same percentage.
44
+ // Thresholds 75/55: keeps a felt buffer before the auto-compact trigger (~83.5%)
45
+ // under the raw-occupancy interpretation of the percentage.
46
+ export function contextSeverityColor(percent) {
47
+ return percent >= 75 ? 'var(--color-error-light)' : percent >= 55 ? 'var(--color-warning-light)' : 'var(--color-success)';
48
+ }
@@ -0,0 +1,50 @@
1
+ import { apiUrl } from './apiUrl';
2
+
3
+ export async function fetchAllRepos() {
4
+ let repoList;
5
+ try {
6
+ const repoRes = await fetch(apiUrl('/api/git-repos'));
7
+ if (!repoRes.ok) throw new Error('No git-repos endpoint');
8
+ const data = await repoRes.json();
9
+ repoList = data.repos || [];
10
+ } catch {
11
+ // 回退:旧服务器没有 /api/git-repos,用 /api/git-status 兼容单仓库
12
+ try {
13
+ const statusRes = await fetch(apiUrl('/api/git-status'));
14
+ if (!statusRes.ok) return [];
15
+ const data = await statusRes.json();
16
+ return [{ name: '.', path: '.', isRoot: true, changes: data.changes || [], insertions: data.insertions || 0, deletions: data.deletions || 0, commits: [], hasUpstream: false }];
17
+ } catch {
18
+ return [];
19
+ }
20
+ }
21
+ // GET /api/git-status?repo=<path> → { changes, insertions, deletions, insertions_capped? }
22
+ // GET /api/git-log-unpushed?repo=<path> → { commits, hasUpstream, branch?, upstream?, truncated?, totalCount? }
23
+ // commit shape: { hash, shortHash, author, date (ISO), subject, files: [{status, file}] }
24
+ // 详见 server/lib/git-diff.js: getUnpushedCommits / server.js: /api/git-log-unpushed handler.
25
+ const results = await Promise.all(
26
+ repoList.map(async (repo) => {
27
+ const [statusData, commitsData] = await Promise.all([
28
+ fetch(apiUrl(`/api/git-status?repo=${encodeURIComponent(repo.path)}`))
29
+ .then(r => r.ok ? r.json() : { changes: [], insertions: 0, deletions: 0 })
30
+ .catch(() => ({ changes: [], insertions: 0, deletions: 0 })),
31
+ fetch(apiUrl(`/api/git-log-unpushed?repo=${encodeURIComponent(repo.path)}`))
32
+ .then(r => r.ok ? r.json() : { commits: [], hasUpstream: false })
33
+ .catch(() => ({ commits: [], hasUpstream: false })),
34
+ ]);
35
+ return {
36
+ ...repo,
37
+ changes: statusData.changes || [],
38
+ insertions: statusData.insertions || 0,
39
+ deletions: statusData.deletions || 0,
40
+ commits: commitsData.commits || [],
41
+ hasUpstream: !!commitsData.hasUpstream,
42
+ branch: commitsData.branch || null,
43
+ upstream: commitsData.upstream || null,
44
+ truncated: !!commitsData.truncated,
45
+ totalCount: typeof commitsData.totalCount === 'number' ? commitsData.totalCount : (commitsData.commits || []).length,
46
+ };
47
+ })
48
+ );
49
+ return results;
50
+ }
@@ -0,0 +1,19 @@
1
+ // 将扁平的文件变更列表构建为目录树
2
+ // 后端 `git status --porcelain -uall` 会把新目录展开到具体文件。若因旧 server
3
+ // 未重启或其它原因返回了目录占位(尾斜杠路径),直接跳过——不能把目录名误
4
+ // 当作文件渲染,也不能入 tree 造成空节点。用户重启 server 后会拿到真实文件。
5
+ export function buildGitTree(changes) {
6
+ const root = { dirs: {}, files: [] };
7
+ for (const change of changes) {
8
+ if (!change.file || change.file.endsWith('/')) continue;
9
+ const parts = change.file.split('/').filter(Boolean);
10
+ if (parts.length === 0) continue;
11
+ let node = root;
12
+ for (let i = 0; i < parts.length - 1; i++) {
13
+ if (!node.dirs[parts[i]]) node.dirs[parts[i]] = { dirs: {}, files: [] };
14
+ node = node.dirs[parts[i]];
15
+ }
16
+ node.files.push({ name: parts[parts.length - 1], status: change.status, fullPath: change.file });
17
+ }
18
+ return root;
19
+ }