@steerable/agent-shell-web 0.6.47 → 0.6.49

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.
@@ -13,6 +13,14 @@ export interface TurnFile {
13
13
  kind: 'created' | 'modified';
14
14
  /** 字节数(展示用,可缺省)。 */
15
15
  size?: number;
16
+ /** 新增行数(展示用,如 +110)。 */
17
+ additions?: number;
18
+ /** 删除行数(展示用,如 -0)。 */
19
+ deletions?: number;
20
+ /** 类别:deliverable = present_files 声明的最终交付文件;intermediate = 其余本轮写过的文件。 */
21
+ category?: 'deliverable' | 'intermediate';
22
+ /** present_files 给出的一行说明(交付卡片副标题)。 */
23
+ description?: string;
16
24
  }
17
25
 
18
26
  /** 解析 SSE 事件 / 持久化元数据里的文件列表;形状不符返回 null(与 parseTurnBlocks 同约定)。 */
@@ -28,6 +36,14 @@ export function parseTurnFiles(raw: unknown): TurnFile[] | null {
28
36
  path: rec.path,
29
37
  kind: rec.kind,
30
38
  ...(typeof rec.size === 'number' ? { size: rec.size } : {}),
39
+ ...(typeof rec.additions === 'number' ? { additions: rec.additions } : {}),
40
+ ...(typeof rec.deletions === 'number' ? { deletions: rec.deletions } : {}),
41
+ ...(rec.category === 'deliverable' || rec.category === 'intermediate'
42
+ ? { category: rec.category }
43
+ : {}),
44
+ ...(typeof rec.description === 'string' && rec.description
45
+ ? { description: rec.description }
46
+ : {}),
31
47
  });
32
48
  }
33
49
  return files;
@@ -41,6 +57,17 @@ export function splitTurnFilePath(filePath: string): { dir: string; name: string
41
57
  return { dir: normalized.slice(0, idx + 1), name: normalized.slice(idx + 1) };
42
58
  }
43
59
 
60
+ /** 获取小写扩展名(含前导点)。 */
61
+ export function getFileExtension(filePath: string): string {
62
+ const normalized = filePath.replace(/[/\\]+$/, '');
63
+ const lastDot = normalized.lastIndexOf('.');
64
+ const lastSlash = Math.max(normalized.lastIndexOf('/'), normalized.lastIndexOf('\\'));
65
+ if (lastDot > lastSlash && lastDot >= 0) {
66
+ return normalized.slice(lastDot).toLowerCase();
67
+ }
68
+ return '';
69
+ }
70
+
44
71
  /** 紧凑的字节数展示(不足 1 KB 显示 B)。 */
45
72
  export function formatFileSize(size: number | undefined): string | null {
46
73
  if (size == null || !Number.isFinite(size) || size < 0) return null;
@@ -48,3 +75,249 @@ export function formatFileSize(size: number | undefined): string | null {
48
75
  if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`;
49
76
  return `${(size / (1024 * 1024)).toFixed(1)} MB`;
50
77
  }
78
+
79
+ /** 是否为临时文件 / Office 锁定文件(如 ~$ 开头的文件名),产物列表中一律排除。 */
80
+ export function isIgnoredTurnFile(filePath: string): boolean {
81
+ const { name } = splitTurnFilePath(filePath);
82
+ if (!name) return true;
83
+ if (name === '.DS_Store' || name === 'Thumbs.db') return true;
84
+ // Office 临时锁定文件
85
+ if (name.startsWith('~$')) return true;
86
+ // 临时文件与编辑器交换文件
87
+ if (name.endsWith('.tmp') || name.endsWith('.swp') || name.endsWith('~')) return true;
88
+ return false;
89
+ }
90
+
91
+ /** 交付物扩展名(最终产物,如电子表格、幻灯片、文档、图片、音视频、独立页面、压缩包)。 */
92
+ export const DELIVERABLE_EXTENSIONS = new Set([
93
+ // 表格 / Spreadsheets
94
+ '.xlsx', '.xls', '.csv', '.tsv', '.numbers',
95
+ // 幻灯片 / Presentations
96
+ '.pptx', '.ppt', '.key', // shell-neutral:allow — Office 幻灯片扩展名,不是产品品牌
97
+ // 文档 / Documents
98
+ '.docx', '.doc', '.pdf', '.pages', '.epub', '.rtf',
99
+ // 图像与富媒体 / Images & Media
100
+ '.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.mp4', '.mov', '.mp3',
101
+ // 独立文档输出与压缩包 / Standalone HTML & Archives
102
+ '.html', '.htm', '.zip', '.tar.gz', '.tar', '.7z',
103
+ ]);
104
+
105
+ /**
106
+ * 后端按 present_files 声明给出 category;没有 category 的是声明机制上线前
107
+ * 落库的消息,退回扩展名规则。
108
+ */
109
+ export function getTurnFileCategory(file: TurnFile): 'deliverable' | 'intermediate' {
110
+ if (file.category) return file.category;
111
+ const ext = getFileExtension(file.path);
112
+ if (DELIVERABLE_EXTENSIONS.has(ext)) return 'deliverable';
113
+ return 'intermediate';
114
+ }
115
+
116
+ export type DeliverableFileKind =
117
+ | 'spreadsheet'
118
+ | 'presentation'
119
+ | 'document'
120
+ | 'pdf'
121
+ | 'image'
122
+ | 'archive'
123
+ | 'code'
124
+ | 'file';
125
+
126
+ export interface DeliverableFileMeta {
127
+ label: string;
128
+ extBadge: string;
129
+ kind: DeliverableFileKind;
130
+ themeColor: 'emerald' | 'amber' | 'blue' | 'rose' | 'purple' | 'slate';
131
+ }
132
+
133
+ export function getDeliverableMeta(filePath: string): DeliverableFileMeta {
134
+ const ext = getFileExtension(filePath);
135
+ const extClean = ext.replace(/^\./, '').toUpperCase() || 'FILE';
136
+
137
+ switch (ext) {
138
+ case '.xlsx':
139
+ case '.xls':
140
+ case '.csv':
141
+ case '.tsv':
142
+ case '.numbers':
143
+ return {
144
+ label: '电子表格',
145
+ extBadge: extClean,
146
+ kind: 'spreadsheet',
147
+ themeColor: 'emerald',
148
+ };
149
+ case '.pptx': // shell-neutral:allow — Office 幻灯片扩展名,不是产品品牌
150
+ case '.ppt': // shell-neutral:allow — Office 幻灯片扩展名,不是产品品牌
151
+ case '.key':
152
+ return {
153
+ label: '演示文稿',
154
+ extBadge: extClean,
155
+ kind: 'presentation',
156
+ themeColor: 'amber',
157
+ };
158
+ case '.docx':
159
+ case '.doc':
160
+ case '.pages':
161
+ case '.rtf':
162
+ return {
163
+ label: '文档',
164
+ extBadge: extClean,
165
+ kind: 'document',
166
+ themeColor: 'blue',
167
+ };
168
+ case '.pdf':
169
+ return {
170
+ label: '文档',
171
+ extBadge: 'PDF',
172
+ kind: 'pdf',
173
+ themeColor: 'rose',
174
+ };
175
+ case '.png':
176
+ case '.jpg':
177
+ case '.jpeg':
178
+ case '.gif':
179
+ case '.webp':
180
+ case '.svg':
181
+ return {
182
+ label: '图像',
183
+ extBadge: extClean,
184
+ kind: 'image',
185
+ themeColor: 'purple',
186
+ };
187
+ case '.zip':
188
+ case '.tar':
189
+ case '.gz':
190
+ case '.7z':
191
+ return {
192
+ label: '压缩包',
193
+ extBadge: extClean,
194
+ kind: 'archive',
195
+ themeColor: 'amber',
196
+ };
197
+ case '.html':
198
+ case '.htm':
199
+ return {
200
+ label: '网页',
201
+ extBadge: 'HTML',
202
+ kind: 'document',
203
+ themeColor: 'blue',
204
+ };
205
+ default:
206
+ return {
207
+ label: '文件',
208
+ extBadge: extClean,
209
+ kind: 'file',
210
+ themeColor: 'slate',
211
+ };
212
+ }
213
+ }
214
+
215
+ /** 规范化展示路径(若在特定项目/脚本目录下,提取便于阅读的相对路径)。 */
216
+ export function formatIntermediateDisplayPath(filePath: string): string {
217
+ const normalized = filePath.replace(/\\/g, '/');
218
+ const markers = ['/review_work/', '/src/', '/scripts/', '/packages/', '/work/', '/tests/'];
219
+ for (const marker of markers) {
220
+ const idx = normalized.indexOf(marker);
221
+ if (idx >= 0) {
222
+ return normalized.slice(idx + 1);
223
+ }
224
+ }
225
+ const { dir, name } = splitTurnFilePath(filePath);
226
+ if (!dir) return name;
227
+ const parts = dir.replace(/\/$/, '').split(/[/\\]/);
228
+ const lastFolder = parts[parts.length - 1];
229
+ return lastFolder ? `${lastFolder}/${name}` : name;
230
+ }
231
+
232
+ export interface GroupedTurnFiles {
233
+ deliverables: TurnFile[];
234
+ intermediates: TurnFile[];
235
+ totalAdditions: number;
236
+ totalDeletions: number;
237
+ }
238
+
239
+ /** 从 unified diff 解析增删行数。 */
240
+ function parseDiffStats(diffText: string): { additions: number; deletions: number } {
241
+ let additions = 0;
242
+ let deletions = 0;
243
+ for (const line of diffText.split('\n')) {
244
+ if (line.startsWith('+') && !line.startsWith('+++')) additions++;
245
+ else if (line.startsWith('-') && !line.startsWith('---')) deletions++;
246
+ }
247
+ return { additions, deletions };
248
+ }
249
+
250
+ /** 把本轮文件按最终交付物(最终文件)与中间修改文件分类,并合并行数统计。 */
251
+ export function groupTurnFiles(
252
+ files: TurnFile[],
253
+ executedActions?: Array<{ tool?: string; arguments?: unknown; result?: unknown }>,
254
+ ): GroupedTurnFiles {
255
+ // 1. 过滤垃圾/临时锁定文件
256
+ const validFiles = files.filter((f) => !isIgnoredTurnFile(f.path));
257
+
258
+ // 2. 从 executedActions 中补充 diff 统计
259
+ const statsByPath = new Map<string, { additions: number; deletions: number }>();
260
+ if (Array.isArray(executedActions)) {
261
+ for (const act of executedActions) {
262
+ const args = act.arguments && typeof act.arguments === 'object' ? (act.arguments as Record<string, unknown>) : null;
263
+ const rawPath = typeof args?.path === 'string' ? args.path : null;
264
+ if (rawPath) {
265
+ if (act.tool === 'local_edit_file') {
266
+ const res = act.result && typeof act.result === 'object' ? (act.result as Record<string, unknown>) : null;
267
+ if (typeof res?.diff === 'string') {
268
+ const diffStats = parseDiffStats(res.diff);
269
+ const prev = statsByPath.get(rawPath) ?? { additions: 0, deletions: 0 };
270
+ statsByPath.set(rawPath, {
271
+ additions: prev.additions + diffStats.additions,
272
+ deletions: prev.deletions + diffStats.deletions,
273
+ });
274
+ }
275
+ } else if (act.tool === 'local_write_file') {
276
+ if (typeof args?.content === 'string') {
277
+ const lines = args.content ? args.content.split('\n').length : 0;
278
+ statsByPath.set(rawPath, { additions: lines, deletions: 0 });
279
+ }
280
+ }
281
+ }
282
+ }
283
+ }
284
+
285
+ const deliverables: TurnFile[] = [];
286
+ const intermediates: TurnFile[] = [];
287
+ let totalAdditions = 0;
288
+ let totalDeletions = 0;
289
+
290
+ for (const file of validFiles) {
291
+ const cat = getTurnFileCategory(file);
292
+ // 匹配统计:尝试直接匹配或 basename 匹配
293
+ let stats = statsByPath.get(file.path);
294
+ if (!stats) {
295
+ for (const [p, s] of statsByPath.entries()) {
296
+ if (p.endsWith(file.path) || file.path.endsWith(p)) {
297
+ stats = s;
298
+ break;
299
+ }
300
+ }
301
+ }
302
+
303
+ const additions = file.additions ?? stats?.additions;
304
+ const deletions = file.deletions ?? stats?.deletions;
305
+
306
+ const enriched: TurnFile = {
307
+ ...file,
308
+ category: cat,
309
+ ...(additions !== undefined ? { additions } : {}),
310
+ ...(deletions !== undefined ? { deletions } : {}),
311
+ };
312
+
313
+ if (cat === 'deliverable') {
314
+ deliverables.push(enriched);
315
+ } else {
316
+ intermediates.push(enriched);
317
+ if (typeof additions === 'number') totalAdditions += additions;
318
+ if (typeof deletions === 'number') totalDeletions += deletions;
319
+ }
320
+ }
321
+
322
+ return { deliverables, intermediates, totalAdditions, totalDeletions };
323
+ }
@@ -0,0 +1,56 @@
1
+ import { LuInfo } from 'react-icons/lu';
2
+ import { useAppRelease } from '@/components/SidebarRelease';
3
+
4
+ /**
5
+ * 综合设置里的版本与更新。侧栏只显示版本号;检查、下载和安装在这里。
6
+ * 没有桌面更新通道时不渲染。
7
+ */
8
+ export function AppUpdateSettingsPanel() {
9
+ const release = useAppRelease();
10
+ if (!release.version) return null;
11
+ const detail =
12
+ release.actionTitle && release.actionTitle !== release.actionLabel
13
+ ? release.actionTitle
14
+ : null;
15
+
16
+ return (
17
+ <section className="space-y-2" data-testid="settings-section-update">
18
+ <h2 className="flex items-center gap-1.5 text-xs font-semibold text-agent-foreground">
19
+ <LuInfo className="h-3.5 w-3.5 text-agent-muted-foreground" />
20
+ 关于
21
+ </h2>
22
+ <div className="flex items-center gap-3 rounded-agent-md border border-agent-border bg-agent-card p-2.5">
23
+ <div className="min-w-0 flex-1">
24
+ <p className="text-xs font-medium text-agent-foreground">当前版本</p>
25
+ <p
26
+ className="mt-1 text-xs tabular-nums text-agent-muted-foreground"
27
+ data-testid="settings-app-version"
28
+ >
29
+ v{release.version}
30
+ </p>
31
+ {detail ? (
32
+ <p className="mt-1 text-xs leading-relaxed text-agent-muted-foreground">{detail}</p>
33
+ ) : null}
34
+ </div>
35
+ {release.actionLabel ? (
36
+ <button
37
+ type="button"
38
+ className={[
39
+ 'flex h-7 shrink-0 items-center rounded-full px-3 text-xs transition-colors',
40
+ release.emphasize
41
+ ? 'bg-agent-foreground font-medium text-agent-canvas hover:opacity-90'
42
+ : 'border border-agent-border text-agent-foreground hover:bg-agent-foreground/5',
43
+ release.clickable ? '' : 'cursor-default opacity-60',
44
+ ].join(' ')}
45
+ data-testid="settings-app-update"
46
+ title={release.actionTitle}
47
+ disabled={!release.clickable}
48
+ onClick={release.run}
49
+ >
50
+ {release.actionLabel}
51
+ </button>
52
+ ) : null}
53
+ </div>
54
+ </section>
55
+ );
56
+ }