@steerable/agent-shell-web 0.6.61 → 0.6.62

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@steerable/agent-shell-web",
3
- "version": "0.6.61",
3
+ "version": "0.6.62",
4
4
  "type": "module",
5
5
  "description": "Steerable framework — product-neutral renderer SPA source for @steerable/agent-shell (React + Vite). Consumed by product web entry points via the `@/` alias (anchored at this package's src/) and the createProductViteConfig factory. Published as a source package (src/), plus the compiled vite.base config factory (vite.base.js + .d.ts) for Node-side config loading.",
6
6
  "license": "Apache-2.0",
@@ -45,8 +45,8 @@
45
45
  "react-router-dom": "^6.30.0",
46
46
  "rehype-raw": "^7.0.0",
47
47
  "remark-gfm": "^4.0.1",
48
- "@steerable/agent-protocol": "0.6.61",
49
- "@steerable/agent-ui": "0.6.61"
48
+ "@steerable/agent-protocol": "0.6.62",
49
+ "@steerable/agent-ui": "0.6.62"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@tailwindcss/vite": "^4.3.0",
@@ -59,7 +59,7 @@
59
59
  "typescript": "^5.8.3",
60
60
  "vite": "^5.4.21",
61
61
  "vitest": "^3.2.4",
62
- "@steerable/pack-sdk": "0.6.61"
62
+ "@steerable/pack-sdk": "0.6.62"
63
63
  },
64
64
  "scripts": {
65
65
  "gen": "echo agent-shell-web",
@@ -46,12 +46,12 @@ export function todoProgressCopy(todos: SessionTodo[]): string {
46
46
  const summary = summarizeTodos(todos);
47
47
  if (summary.total === 0) return '';
48
48
  if (summary.completed === summary.total) {
49
- return t('{completed}/{total} done', { completed: summary.completed, total: summary.total });
49
+ return `${summary.completed}/${summary.total}`;
50
50
  }
51
51
  const currentIndex = todos.findIndex((todo) => todo.status === 'in_progress');
52
52
  const step =
53
53
  currentIndex >= 0 ? currentIndex + 1 : Math.min(summary.completed + 1, summary.total);
54
- return t('On step {step}/{total}', { step, total: summary.total });
54
+ return `${step}/${summary.total}`;
55
55
  }
56
56
 
57
57
  export function TodoItems({
@@ -211,17 +211,6 @@ export function SessionTodoList({ todos }: { todos: SessionTodo[] }) {
211
211
  <span>{t('Task list')}</span>
212
212
  </div>
213
213
  <div className="flex items-center gap-1.5">
214
- {progressLabel && (
215
- <span
216
- className={`rounded-full px-1.5 py-0.2 text-[10px] font-medium ${
217
- isAllCompleted
218
- ? 'bg-emerald-500/10 text-emerald-600 dark:text-emerald-400'
219
- : 'bg-blue-500/10 text-blue-600 dark:text-blue-400'
220
- }`}
221
- >
222
- {progressLabel}
223
- </span>
224
- )}
225
214
  <button
226
215
  type="button"
227
216
  onClick={() => setUserToggled(false)}
@@ -17,8 +17,12 @@ export interface TurnFile {
17
17
  additions?: number;
18
18
  /** 删除行数(展示用,如 -0)。 */
19
19
  deletions?: number;
20
- /** 类别:deliverable = present_files 声明的最终交付文件;intermediate = 其余本轮写过的文件。 */
20
+ /** 类别:deliverable = 本轮交付卡片;intermediate = 其余本轮写过的文件。 */
21
21
  category?: 'deliverable' | 'intermediate';
22
+ /** 后端已经按分层证据完成选择;前端必须原样采用 category。 */
23
+ selection?: 'resolved';
24
+ /** 交付选择的来源,供调试和后续展示使用。 */
25
+ deliverySource?: 'generation' | 'final-reference' | 'present-files' | 'inference';
22
26
  /** present_files 给出的一行说明(交付卡片副标题)。 */
23
27
  description?: string;
24
28
  }
@@ -41,6 +45,13 @@ export function parseTurnFiles(raw: unknown): TurnFile[] | null {
41
45
  ...(rec.category === 'deliverable' || rec.category === 'intermediate'
42
46
  ? { category: rec.category }
43
47
  : {}),
48
+ ...(rec.selection === 'resolved' ? { selection: rec.selection } : {}),
49
+ ...(rec.deliverySource === 'generation' ||
50
+ rec.deliverySource === 'final-reference' ||
51
+ rec.deliverySource === 'present-files' ||
52
+ rec.deliverySource === 'inference'
53
+ ? { deliverySource: rec.deliverySource }
54
+ : {}),
44
55
  ...(typeof rec.description === 'string' && rec.description
45
56
  ? { description: rec.description }
46
57
  : {}),
@@ -103,8 +114,8 @@ export const DELIVERABLE_EXTENSIONS = new Set([
103
114
  ]);
104
115
 
105
116
  /**
106
- * 后端按 present_files 声明给出 category;没有 category 的是声明机制上线前
107
- * 落库的消息,退回扩展名规则。
117
+ * 单文件扩展名猜测。回合里有多份文件时,`groupTurnFiles` 会再拿掉同名预览。
118
+ * 没有 category 的是声明机制上线前落库的消息。
108
119
  */
109
120
  export function getTurnFileCategory(file: TurnFile): 'deliverable' | 'intermediate' {
110
121
  if (file.category) return file.category;
@@ -247,6 +258,54 @@ function parseDiffStats(diffText: string): { additions: number; deletions: numbe
247
258
  return { additions, deletions };
248
259
  }
249
260
 
261
+ const SOURCE_EXTENSIONS = new Set([
262
+ '.pptx', '.ppt', '.key', // shell-neutral:allow — Office 幻灯片扩展名,不是产品品牌
263
+ '.docx', '.doc', '.pages',
264
+ '.xlsx', '.xls', '.xlsm', '.numbers',
265
+ ]);
266
+
267
+ const PREVIEW_NAME = /预览|截图|图像版|preview|screenshot|thumbnail/i;
268
+
269
+ /**
270
+ * 旧消息没有 selection=resolved,只纠正最明确的一种历史误判:同目录的
271
+ * 幻灯片与预览 PDF / 截图都被标成交付物。新消息完全采用后端结论。
272
+ */
273
+ function selectLegacyDeliverablePaths(files: readonly TurnFile[]): Set<string> {
274
+ const chosen = new Set(
275
+ files.filter((file) => getTurnFileCategory(file) === 'deliverable').map((file) => file.path),
276
+ );
277
+ const sources = files
278
+ .filter((file) => SOURCE_EXTENSIONS.has(getFileExtension(file.path)))
279
+ .map((file) => file.path);
280
+ for (const candidate of [...chosen]) {
281
+ const source = sources.find((item) => isLegacyPreviewOf(candidate, item));
282
+ if (!source) continue;
283
+ chosen.delete(candidate);
284
+ chosen.add(source);
285
+ }
286
+ return new Set(chosen);
287
+ }
288
+
289
+ function isLegacyPreviewOf(filePath: string, sourcePath: string): boolean {
290
+ if (filePath === sourcePath || getFileExtension(sourcePath) !== '.pptx') return false; // shell-neutral:allow — Office extension
291
+ const ext = getFileExtension(filePath);
292
+ if (ext !== '.pdf' && !['.png', '.jpg', '.jpeg', '.webp'].includes(ext)) return false;
293
+ const file = splitTurnFilePath(filePath);
294
+ const source = splitTurnFilePath(sourcePath);
295
+ const normalized = (name: string) =>
296
+ name
297
+ .replace(/\.[^.]+$/, '')
298
+ .replace(PREVIEW_NAME, '')
299
+ .replace(/[\s._\-—–()()【】[\]]+/g, '')
300
+ .toLowerCase()
301
+ .normalize('NFC');
302
+ if (normalized(file.name) === normalized(source.name)) return true;
303
+ return (
304
+ PREVIEW_NAME.test(file.name) &&
305
+ file.dir.normalize('NFC') === source.dir.normalize('NFC')
306
+ );
307
+ }
308
+
250
309
  /** 把本轮文件按最终交付物(最终文件)与中间修改文件分类,并合并行数统计。 */
251
310
  export function groupTurnFiles(
252
311
  files: TurnFile[],
@@ -287,8 +346,16 @@ export function groupTurnFiles(
287
346
  let totalAdditions = 0;
288
347
  let totalDeletions = 0;
289
348
 
349
+ const backendResolved =
350
+ validFiles.length > 0 && validFiles.every((file) => file.selection === 'resolved');
351
+ const deliverablePaths = backendResolved
352
+ ? new Set(
353
+ validFiles.filter((file) => file.category === 'deliverable').map((file) => file.path),
354
+ )
355
+ : selectLegacyDeliverablePaths(validFiles);
356
+
290
357
  for (const file of validFiles) {
291
- const cat = getTurnFileCategory(file);
358
+ const cat = deliverablePaths.has(file.path) ? 'deliverable' : 'intermediate';
292
359
  // 匹配统计:尝试直接匹配或 basename 匹配
293
360
  let stats = statsByPath.get(file.path);
294
361
  if (!stats) {