@steerable/agent-shell-web 0.6.48 → 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.
- package/package.json +4 -4
- package/src/components/AgentSidebar.tsx +411 -298
- package/src/components/ChatHeader.tsx +74 -4
- package/src/components/chat/ApprovalModal.tsx +8 -0
- package/src/components/chat/ApprovalPromptProvider.tsx +16 -1
- package/src/components/chat/AskUserPromptProvider.tsx +14 -0
- package/src/components/chat/AssistantMessage.tsx +1 -1
- package/src/components/chat/ChatInput.tsx +82 -31
- package/src/components/chat/SuggestedReplies.tsx +5 -2
- package/src/components/chat/TurnFilesCard.tsx +417 -69
- package/src/components/chat/turn-files.ts +273 -0
- package/src/components/settings/PortableSettingsPanel.tsx +493 -0
- package/src/components/settings/PythonRunnerSettingsPanel.tsx +231 -0
- package/src/layouts/AgentLayout.tsx +23 -0
- package/src/lib/electron-bridge.ts +44 -0
- package/src/lib/host-paste.ts +85 -0
- package/src/lib/local-api.ts +18 -0
- package/src/lib/portable.ts +229 -0
- package/src/lib/tauri-bridge.ts +40 -1
- package/src/main.tsx +2 -0
- package/src/pages/AgentPage.tsx +17 -0
- package/src/pages/SettingsPage.tsx +15 -1
- package/src/styles/index.css +29 -0
- package/src/vite-env.d.ts +2 -0
- package/vite.base.js +1 -0
|
@@ -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
|
+
}
|