dsh-fps-workbench 0.1.77 → 0.1.79

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/client.js CHANGED
@@ -51,9 +51,46 @@ window.__ModuleLoader__.load({
51
51
 
52
52
  function reportDiagnostic(message) {
53
53
  console.error(`[claude-workbench] ${message}`);
54
+ runtimeFailure = true;
55
+ compatibilityChanged();
54
56
  }
55
57
 
56
58
  const en = {
59
+ draft_loading: "Restoring attachments…",
60
+ draft_saving: "Saving attachments on this device…",
61
+ draft_saved: "Attachments saved on this device · kept for 30 days",
62
+ draft_error: "Attachments could not be saved or restored. Keep this window open and retry.",
63
+ draft_memory: "Attachments are available in this window only.",
64
+ tasksTitle: "Task status",
65
+ tasksPlan: "Plan",
66
+ tasksNoPlan: "No plan has been recorded in this session.",
67
+ tasksBackground: "Background tasks",
68
+ tasksEmpty: "No background tasks are reported by DSH.",
69
+ tasksUnavailable: "Background task status is unavailable. Use the native session header.",
70
+ tasksSettled: "Finished",
71
+ tasksNative: "Open native details and controls",
72
+ task_running: "Running",
73
+ task_stopping: "Stopping",
74
+ task_completed: "Completed",
75
+ task_failed: "Failed",
76
+ task_killed: "Stopped",
77
+ task_pending: "Pending",
78
+ task_in_progress: "In progress",
79
+ task_unknown: "Unknown",
80
+ compatTitle: "Compatibility check",
81
+ compatIntro: "Checks the interfaces available in this client. This does not test a model or execute tools. Missing optional components keep their native entry points.",
82
+ compatComposer: "Conversation composer",
83
+ compatFiles: "File attachments and metadata",
84
+ compatPreview: "Native file preview",
85
+ compatJobs: "Native background tasks",
86
+ compatStorage: "Attachment persistence",
87
+ compatReady: "Available",
88
+ compatUnavailable: "Unavailable / native fallback",
89
+ compatRefresh: "Check again",
90
+ compatCopy: "Copy diagnostic summary",
91
+ compatCopied: "Copied",
92
+ compatRuntimeFailure: "A component error was observed. A failed FPS composer falls back to DSH’s native input.",
93
+
57
94
  greetingMorning: 'Good morning',
58
95
  greetingAfternoon: 'Good afternoon',
59
96
  greetingEvening: 'Good evening',
@@ -140,6 +177,8 @@ window.__ModuleLoader__.load({
140
177
  usageDreaminaBalance: 'Credit balance',
141
178
  usageKeySpent: 'Key spend',
142
179
  usageKeyRemaining: 'Key spending limit remaining',
180
+ usageOrganizationSpend: 'Org spend · 7 days',
181
+ usageOrganizationTokens: 'Org tokens · 7 days',
143
182
  usageDreaminaReset: 'Estimated subscription credit reset',
144
183
  usageExtraBalance: 'Extra usage balance',
145
184
  usageFiveHours: '5-hour limit',
@@ -235,6 +274,41 @@ window.__ModuleLoader__.load({
235
274
  permissionEnable: 'Enable',
236
275
  };
237
276
  const zh = {
277
+ draft_loading: "正在恢复附件…",
278
+ draft_saving: "正在将附件保存到本机…",
279
+ draft_saved: "附件已保存到本机 · 保留 30 天",
280
+ draft_error: "附件保存或恢复失败,请保留当前窗口并重试。",
281
+ draft_memory: "附件仅保留在当前窗口。",
282
+ tasksTitle: "任务状态",
283
+ tasksPlan: "计划步骤",
284
+ tasksNoPlan: "当前会话尚未记录计划步骤。",
285
+ tasksBackground: "后台任务",
286
+ tasksEmpty: "DSH 暂未报告后台任务。",
287
+ tasksUnavailable: "暂时无法读取后台任务,请使用会话顶栏的原生入口。",
288
+ tasksSettled: "已结束",
289
+ tasksNative: "打开原生详情与操作",
290
+ task_running: "运行中",
291
+ task_stopping: "正在停止",
292
+ task_completed: "已完成",
293
+ task_failed: "失败",
294
+ task_killed: "已停止",
295
+ task_pending: "待处理",
296
+ task_in_progress: "进行中",
297
+ task_unknown: "未知",
298
+ compatTitle: "兼容性自检",
299
+ compatIntro: "检查当前客户端可用接口,不调用模型或执行工具。可选组件缺失时保留 DSH 原生入口。",
300
+ compatComposer: "会话输入框",
301
+ compatFiles: "文件附件与信息",
302
+ compatPreview: "原生文件预览",
303
+ compatJobs: "原生后台任务",
304
+ compatStorage: "附件持久化",
305
+ compatReady: "可用",
306
+ compatUnavailable: "不可用 / 使用原生入口",
307
+ compatRefresh: "重新检查",
308
+ compatCopy: "复制诊断摘要",
309
+ compatCopied: "已复制",
310
+ compatRuntimeFailure: "检测到组件异常。FPS 输入框渲染失败时会回退到 DSH 原生输入框。",
311
+
238
312
  greetingMorning: '早上好',
239
313
  greetingAfternoon: '下午好',
240
314
  greetingEvening: '晚上好',
@@ -321,6 +395,8 @@ window.__ModuleLoader__.load({
321
395
  usageDreaminaBalance: '积分余额',
322
396
  usageKeySpent: 'Key 已用金额',
323
397
  usageKeyRemaining: 'Key 剩余支出额度',
398
+ usageOrganizationSpend: '组织花费 · 近 7 天',
399
+ usageOrganizationTokens: '组织 Token · 近 7 天',
324
400
  usageDreaminaReset: '订阅积分重置(推算)',
325
401
  usageExtraBalance: '加量余额',
326
402
  usageFiveHours: '5 小时限额',
@@ -472,6 +548,24 @@ window.__ModuleLoader__.load({
472
548
  }
473
549
 
474
550
  const CSS = `
551
+ .cw-draft-status { padding: 3px 12px 8px; color: var(--cw-text-muted); font-size: 11px; }
552
+ .cw-draft-status button, .cw-compatibility button { border: 1px solid var(--cw-border); border-radius: 6px; padding: 4px 8px; margin: 5px; background: transparent; color: inherit; cursor: pointer; }
553
+ .cw-compatibility { padding: 12px 0 20px; margin-bottom: 20px; border-bottom: 1px solid var(--cw-border); font-size: 12px; }
554
+ .cw-compatibility summary { font-size: 14px; cursor: pointer; }
555
+ .cw-long-tasks h3 { font-size: 12px; font-weight: 500; color: var(--cw-text-secondary); margin: 16px 0 8px; }
556
+ .cw-long-tasks summary { cursor: pointer; font-size: 12px; color: var(--cw-text-secondary); padding: 6px 0; }
557
+ .cw-task-list, .cw-task-plan { list-style: none; padding: 0; margin: 4px 0; }
558
+ .cw-task-row, .cw-task-plan li { display: flex; gap: 9px; padding: 7px 0; font-size: 12px; }
559
+ .cw-task-dot { flex: 0 0 12px; width: 12px; height: 12px; margin-top: 2px; border: 1px solid currentColor; border-radius: 50%; color: var(--cw-text-muted); font-size: 9px; line-height: 11px; text-align: center; }
560
+ [data-status="pending"] > .cw-task-dot { border-style: dashed; }
561
+ [data-status="running"] > .cw-task-dot, [data-status="in_progress"] > .cw-task-dot { color: var(--cw-accent); border-width: 2px; }
562
+ [data-status="failed"] > .cw-task-dot { color: var(--cw-danger); }
563
+ .cw-task-row[data-status="completed"], .cw-task-plan li[data-status="completed"] { color: var(--cw-text-muted); }
564
+ .cw-task-main { display: flex; flex: 1; flex-direction: column; gap: 3px; min-width: 0; }
565
+ .cw-task-main > span, .cw-task-main small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
566
+ .cw-task-main small, .cw-native-jobs > small { color: var(--cw-text-muted); font-size: 11px; }
567
+ .cw-native-jobs { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-top: 9px; }
568
+
475
569
  .cw-image-settings { padding: 4px 0 16px; color: var(--dsw-alias-label-primary); }
476
570
  .cw-image-settings h2 { margin: 0 0 5px; font-size: 15px; font-weight: 600; }
477
571
  .cw-image-settings p { margin: 0; color: var(--dsw-alias-label-secondary); font-size: 13px; line-height: 1.55; }
@@ -1557,6 +1651,7 @@ window.__ModuleLoader__.load({
1557
1651
 
1558
1652
  componentDidCatch(error) {
1559
1653
  reportDiagnostic(`slot render failed: ${error?.message || error}`);
1654
+ this.props.onFailure?.();
1560
1655
  }
1561
1656
 
1562
1657
  render() {
@@ -1677,6 +1772,7 @@ window.__ModuleLoader__.load({
1677
1772
  }
1678
1773
 
1679
1774
  return h('section', { className: 'cw-image-settings', 'aria-labelledby': 'cw-image-heading' },
1775
+ h(CompatibilityPanel, { t }),
1680
1776
  h('h2', { id: 'cw-image-heading' }, t('imageSettings')),
1681
1777
  h('p', null, t('imageSettingsIntro')),
1682
1778
  loading ? h('p', { role: 'status' }, t('imageSettingsLoad'))
@@ -2339,44 +2435,164 @@ window.__ModuleLoader__.load({
2339
2435
  }
2340
2436
  }
2341
2437
 
2342
- function createAttachmentDrafts() {
2438
+ // Browser/profile-local storage; file bytes never leave the device until Send.
2439
+ function createAttachmentStorage(indexedDB = globalThis.indexedDB, timeoutMs = 10000) {
2440
+ let database;
2441
+ function open() {
2442
+ if (database) return database;
2443
+ const pending = new Promise((resolve, reject) => {
2444
+ if (!indexedDB) return reject(new Error('Storage unavailable'));
2445
+ let settled = false;
2446
+ const fail = (error) => {
2447
+ if (settled) return;
2448
+ settled = true; clearTimeout(timer); reject(error);
2449
+ };
2450
+ const timer = setTimeout(() => fail(new Error('Storage open timed out')), timeoutMs);
2451
+ let request;
2452
+ try { request = indexedDB.open('fps-attachment-drafts', 1); }
2453
+ catch (error) { fail(error); return; }
2454
+ request.onupgradeneeded = () => {
2455
+ if (settled) { request.transaction?.abort(); return; }
2456
+ request.result.createObjectStore('drafts', { keyPath: 'id' });
2457
+ };
2458
+ request.onerror = () => fail(request.error);
2459
+ request.onblocked = () => fail(new Error('Storage blocked'));
2460
+ request.onsuccess = () => {
2461
+ const db = request.result;
2462
+ if (settled) { db.close(); return; }
2463
+ settled = true; clearTimeout(timer);
2464
+ db.onversionchange = () => { db.close(); database = undefined; };
2465
+ resolve(db);
2466
+ };
2467
+ });
2468
+ database = pending;
2469
+ pending.catch(() => { if (database === pending) database = undefined; });
2470
+ return pending;
2471
+ }
2472
+ async function transaction(mode, action) {
2473
+ const db = await open();
2474
+ return new Promise((resolve, reject) => {
2475
+ const tx = db.transaction('drafts', mode);
2476
+ let value, settled = false;
2477
+ function finish(error) {
2478
+ if (settled) return;
2479
+ settled = true; clearTimeout(timer);
2480
+ if (error) reject(error); else resolve(value);
2481
+ }
2482
+ const timer = setTimeout(() => {
2483
+ // Abort before allowing a retry: an old write must never commit later.
2484
+ try { tx.abort(); } catch { /* Transaction already settled. */ }
2485
+ finish(new Error('Draft storage timed out'));
2486
+ }, timeoutMs);
2487
+ tx.oncomplete = () => finish();
2488
+ tx.onabort = tx.onerror = () => finish(tx.error || new Error('Draft transaction failed'));
2489
+ try {
2490
+ const request = action(tx.objectStore('drafts'));
2491
+ if (request) request.onsuccess = () => { value = request.result; };
2492
+ } catch (error) {
2493
+ try { tx.abort(); } catch { /* Transaction already settled. */ }
2494
+ finish(error);
2495
+ }
2496
+ });
2497
+ }
2498
+ const encode = (file) => ({ blob: file, name: file.name, type: file.type, lastModified: file.lastModified });
2499
+ const decode = (file) => {
2500
+ if (!(file?.blob instanceof Blob) || typeof file.name !== 'string') throw new Error('Invalid saved attachment');
2501
+ return new File([file.blob], file.name, { type: file.type || '', lastModified: file.lastModified });
2502
+ };
2503
+ return {
2504
+ async load() {
2505
+ const rows = await transaction('readonly', (store) => store.getAll());
2506
+ const valid = [];
2507
+ for (const row of rows) {
2508
+ if (Date.now() - row.updatedAt > 30 * 86400_000) {
2509
+ await transaction('readwrite', (store) => store.delete(row.id));
2510
+ continue;
2511
+ }
2512
+ valid.push({ id: row.id, images: row.images.map(decode), files: row.files.map(decode) });
2513
+ }
2514
+ return valid;
2515
+ },
2516
+ save(id, draft) {
2517
+ return transaction('readwrite', (store) => draft.images.length || draft.files.length
2518
+ ? store.put({ id, updatedAt: Date.now(), images: draft.images.map(encode), files: draft.files.map(encode) })
2519
+ : store.delete(id));
2520
+ },
2521
+ clear: () => transaction('readwrite', (store) => store.clear()),
2522
+ };
2523
+ }
2524
+
2525
+ function createAttachmentDrafts(storage) {
2343
2526
  const drafts = new Map();
2344
2527
  const listeners = new Set();
2345
2528
  const empty = { images: [], files: [], submission: null };
2346
2529
  const read = (id) => drafts.get(id) ?? empty;
2347
-
2348
- function write(id, draft) {
2530
+ let status = storage ? 'loading' : 'memory';
2531
+ let chain = Promise.resolve();
2532
+ let revision = 0;
2533
+ let hydrationFailed = false;
2534
+ const dirty = new Map();
2535
+ const touched = new Set();
2536
+ const notify = () => { for (const listener of listeners) listener(); };
2537
+ async function hydrate() {
2538
+ if (!storage) return;
2539
+ return storage.load().then((rows) => {
2540
+ for (const row of rows) if (!touched.has(row.id)) drafts.set(row.id,
2541
+ { images: row.images, files: row.files, submission: null });
2542
+ hydrationFailed = false; status = dirty.size ? 'saving' : 'saved'; notify();
2543
+ }).catch(() => { hydrationFailed = true; status = 'error'; notify(); });
2544
+ }
2545
+ const ready = hydrate();
2546
+ chain = ready;
2547
+ function persist(id, draft) {
2548
+ if (!storage) return;
2549
+ const current = ++revision;
2550
+ dirty.set(id, { draft, revision: current });
2551
+ status = 'saving';
2552
+ chain = chain.then(() => storage.save(id, draft)).then(() => {
2553
+ if (dirty.get(id)?.revision === current) dirty.delete(id);
2554
+ status = dirty.size || hydrationFailed ? 'error' : 'saved'; notify();
2555
+ }, () => { status = 'error'; notify(); });
2556
+ }
2557
+ function write(id, draft, durable = true) {
2558
+ touched.add(id);
2349
2559
  if (draft.images.length || draft.files.length || draft.submission) drafts.set(id, draft);
2350
2560
  else drafts.delete(id);
2351
- for (const listener of listeners) listener();
2561
+ if (durable) persist(id, draft);
2562
+ notify();
2352
2563
  }
2353
-
2354
2564
  return {
2355
- read,
2356
- subscribe(listener) {
2357
- listeners.add(listener);
2358
- return () => listeners.delete(listener);
2359
- },
2565
+ read, ready, flush: () => chain,
2566
+ status: () => status,
2567
+ subscribe(listener) { listeners.add(listener); return () => listeners.delete(listener); },
2360
2568
  replace(id, kind, change) {
2361
2569
  const draft = read(id);
2362
- if (!id || draft.submission) return;
2570
+ if (!id || draft.submission || !['images', 'files'].includes(kind)) return;
2363
2571
  write(id, { ...draft, [kind]: change(draft[kind]) });
2364
2572
  },
2365
2573
  begin(id) {
2366
2574
  const draft = read(id);
2367
- if (!id || draft.submission) return null;
2575
+ if (!id || draft.submission || status === 'loading') return null;
2368
2576
  const submission = Symbol();
2369
- write(id, { ...draft, submission });
2577
+ write(id, { ...draft, submission }, false);
2370
2578
  return submission;
2371
2579
  },
2372
2580
  finish(id, submission, accepted = false) {
2373
2581
  const draft = read(id);
2374
2582
  if (draft.submission !== submission) return;
2375
- write(id, accepted ? empty : { ...draft, submission: null });
2583
+ write(id, accepted ? empty : { ...draft, submission: null }, accepted);
2584
+ },
2585
+ async retry() {
2586
+ await chain;
2587
+ if (hydrationFailed) await hydrate();
2588
+ for (const [id, { draft }] of [...dirty]) persist(id, draft);
2589
+ notify();
2590
+ await chain;
2376
2591
  },
2377
2592
  };
2378
2593
  }
2379
- const attachmentDrafts = createAttachmentDrafts();
2594
+ const attachmentDrafts = createAttachmentDrafts(
2595
+ typeof window !== 'undefined' && !window.__CLAUDE_WORKBENCH_TEST__ ? createAttachmentStorage() : undefined);
2380
2596
 
2381
2597
  function DraftImage({ file, fallback }) {
2382
2598
  const [previewUrl, setPreviewUrl] = React.useState('');
@@ -3050,6 +3266,167 @@ window.__ModuleLoader__.load({
3050
3266
  );
3051
3267
  }
3052
3268
 
3269
+ const EMPTY_JOBS = { rows: {}, observed: {} };
3270
+ const noopSubscribe = () => () => {};
3271
+ const compatibilityListeners = new Set();
3272
+ let compatibilityRevision = 0;
3273
+ let runtimeFailure = false;
3274
+ function compatibilityChanged() {
3275
+ compatibilityRevision += 1;
3276
+ for (const listener of compatibilityListeners) listener();
3277
+ }
3278
+ function subscribeCompatibility(listener) {
3279
+ compatibilityListeners.add(listener);
3280
+ return () => compatibilityListeners.delete(listener);
3281
+ }
3282
+ const readCompatibilityRevision = () => compatibilityRevision;
3283
+ function compatibilityChecks(services, slots, storageStatus) {
3284
+ const method = (object, key) => typeof object?.[key] === 'function';
3285
+ return [
3286
+ ['compatComposer', ['register', 'inject', 'subscribe', 'entriesOfSlot'].every((key) => method(slots, key)) && !services.composerFallback],
3287
+ ['compatFiles', method(services.fileUploads, 'upload') && method(services.workspaceFiles, 'stat')],
3288
+ ['compatPreview', method(services.sidebarRight, 'openResource')],
3289
+ ['compatJobs', method(services.jobs?.state, 'subscribe') && method(services.jobs?.state, 'getSnapshot') && method(services.jobs, 'watchRows')],
3290
+ ['compatStorage', ['saved', 'saving'].includes(storageStatus)],
3291
+ ];
3292
+ }
3293
+ function CompatibilityPanel({ t }) {
3294
+ React.useSyncExternalStore(subscribeCompatibility, readCompatibilityRevision);
3295
+ const storageStatus = React.useSyncExternalStore(attachmentDrafts.subscribe, attachmentDrafts.status);
3296
+ const [copied, setCopied] = React.useState(false);
3297
+ const checks = compatibilityChecks(runtime, runtime.ctx?.slots, storageStatus);
3298
+ async function copy() {
3299
+ const report = { plugin: 'FPS', checks: Object.fromEntries(checks),
3300
+ runtimeErrorObserved: runtimeFailure, nativeComposerFallback: !!runtime.composerFallback };
3301
+ try { await navigator.clipboard.writeText(JSON.stringify(report, null, 2)); setCopied(true); }
3302
+ catch { setCopied(false); }
3303
+ }
3304
+ return h('details', { className: 'cw-compatibility' },
3305
+ h('summary', null, t('compatTitle')),
3306
+ h('p', { className: 'cw-context-subtle' }, t('compatIntro')),
3307
+ ...checks.map(([key, available]) => h('div', { key, className: 'cw-context-row' },
3308
+ h('span', null, t(key)), h('span', { 'data-ready': available }, t(available ? 'compatReady' : 'compatUnavailable')))),
3309
+ runtimeFailure ? h('p', { role: 'status' }, t('compatRuntimeFailure')) : null,
3310
+ h('button', { type: 'button', onClick: () => { setCopied(false); compatibilityChanged(); } }, t('compatRefresh')),
3311
+ h('button', { type: 'button', onClick: copy }, t(copied ? 'compatCopied' : 'compatCopy')),
3312
+ );
3313
+ }
3314
+ function orderJobs(rows) {
3315
+ const live = (job) => ['running', 'stopping'].includes(job.status);
3316
+ return (Array.isArray(rows) ? rows : []).filter((job) => job && typeof job.id === 'string')
3317
+ .slice().sort((a, b) => Number(live(b)) - Number(live(a)) ||
3318
+ (live(a) ? a.startedAt - b.startedAt : (b.finishedAt ?? b.startedAt) - (a.finishedAt ?? a.startedAt)));
3319
+ }
3320
+ function jobElapsed(job, now) {
3321
+ const end = ['running', 'stopping'].includes(job.status) ? now : job.finishedAt;
3322
+ if (!Number.isFinite(end) || !Number.isFinite(job.startedAt)) return '—';
3323
+ const seconds = Math.max(0, Math.floor((end - job.startedAt) / 1000));
3324
+ return seconds < 60 ? `${seconds}s` : `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
3325
+ }
3326
+ function TaskPlan({ useProjection, t }) {
3327
+ const todos = useProjection('todos');
3328
+ if (!Array.isArray(todos) || !todos.length) return h('p', { className: 'cw-context-subtle' }, t('tasksNoPlan'));
3329
+ return h('ul', { className: 'cw-task-plan' }, todos.map((todo, index) => h('li', {
3330
+ key: todo.id ?? index, 'data-status': todo.status,
3331
+ }, h('span', { className: 'cw-task-dot', 'aria-label': t(`task_${todo.status}`) },
3332
+ todo.status === 'completed' ? '✓' : todo.status === 'in_progress' ? '·' : ''),
3333
+ h('span', null, todo.content))));
3334
+ }
3335
+ function LongTasks(props) {
3336
+ const t = (key) => translate(props, key);
3337
+ React.useSyncExternalStore(subscribeCompatibility, readCompatibilityRevision);
3338
+ const jobs = runtime.jobs;
3339
+ const subscribe = React.useCallback((listener) => jobs?.state?.subscribe?.(listener) ?? (() => {}), [jobs]);
3340
+ const snapshot = React.useCallback(() => jobs?.state?.getSnapshot?.() ?? EMPTY_JOBS, [jobs]);
3341
+ const state = React.useSyncExternalStore(subscribe, snapshot);
3342
+ const [watchFailed, setWatchFailed] = React.useState(false);
3343
+ React.useEffect(() => {
3344
+ setWatchFailed(false);
3345
+ if (!jobs?.watchRows || !props.sessionId) return;
3346
+ try { return jobs.watchRows(props.sessionId); }
3347
+ catch { setWatchFailed(true); }
3348
+ }, [jobs, props.sessionId]);
3349
+ const rows = orderJobs(state.rows?.[props.sessionId]);
3350
+ const live = rows.filter((job) => ['running', 'stopping'].includes(job.status));
3351
+ const settled = rows.filter((job) => !['running', 'stopping'].includes(job.status));
3352
+ const [now, setNow] = React.useState(Date.now);
3353
+ React.useEffect(() => {
3354
+ if (!live.length) return;
3355
+ const timer = setInterval(() => setNow(Date.now()), 1000);
3356
+ return () => clearInterval(timer);
3357
+ }, [live.length]);
3358
+ function row(job) {
3359
+ const statusKey = `task_${job.status}`;
3360
+ return h('li', { key: job.id, className: 'cw-task-row', 'data-status': job.status },
3361
+ h('span', { className: 'cw-task-dot', 'aria-hidden': true }, job.status === 'completed' ? '✓' : ''),
3362
+ h('div', { className: 'cw-task-main' },
3363
+ h('span', { title: job.label }, job.label || job.kind),
3364
+ h('small', null, t(Object.hasOwn(en, statusKey) ? statusKey : 'task_unknown'), ' · ', jobElapsed(job, now)),
3365
+ job.progress || job.detail ? h('small', { title: job.progress || job.detail }, job.progress || job.detail) : null));
3366
+ }
3367
+ return h('section', { className: 'cw-context-section cw-long-tasks' },
3368
+ h('h2', { className: 'cw-context-heading' }, t('tasksTitle')),
3369
+ h('details', { className: 'cw-task-plan-details', open: undefined },
3370
+ h('summary', null, t('tasksPlan')),
3371
+ h(WorkbenchBoundary, null, h(TaskPlan, { useProjection: props.useProjection, t }))),
3372
+ h('h3', null, t('tasksBackground'), live.length ? ` · ${live.length}` : ''),
3373
+ !jobs || watchFailed ? h('p', { className: 'cw-context-subtle' }, t('tasksUnavailable'))
3374
+ : !rows.length ? h('p', { className: 'cw-context-subtle' }, t('tasksEmpty')) : null,
3375
+ live.length ? h('ul', { className: 'cw-task-list' }, live.map(row)) : null,
3376
+ settled.length ? h('details', { className: 'cw-task-settled' },
3377
+ h('summary', null, `${t('tasksSettled')} · ${settled.length}`),
3378
+ h('ul', { className: 'cw-task-list' }, settled.map(row))) : null,
3379
+ rows.length && props.renderSlot ? h('div', { className: 'cw-native-jobs' },
3380
+ h('small', null, t('tasksNative')),
3381
+ props.renderSlot('workbench.context.jobs', {})) : null,
3382
+ );
3383
+ }
3384
+
3385
+ // Own only the mirror and its subscription; never dispose a native entry.
3386
+ function disposeSafely(dispose) {
3387
+ try { if (typeof dispose === 'function') dispose(); }
3388
+ catch (error) { reportDiagnostic(`integration cleanup failed: ${error?.message || error}`); }
3389
+ }
3390
+ function mirrorNativeEntry(slots, source, target, match, onError) {
3391
+ let active = true, current, dispose, unsubscribe;
3392
+ function sync() {
3393
+ if (!active) return;
3394
+ try {
3395
+ const entry = slots.entriesOfSlot(source).find(match);
3396
+ if (current === entry) return;
3397
+ disposeSafely(dispose); dispose = undefined; current = undefined;
3398
+ if (entry) {
3399
+ dispose = slots.register({ name: target, locale: entry.locale, inject: entry.inject }, entry.component);
3400
+ current = entry;
3401
+ }
3402
+ } catch (error) {
3403
+ // Native registry notifications must never receive our exception.
3404
+ disposeSafely(dispose); dispose = current = undefined;
3405
+ reportDiagnostic(`optional slot mirror unavailable: ${error?.message || error}`);
3406
+ onError?.();
3407
+ }
3408
+ }
3409
+ function cleanup() {
3410
+ if (!active) return;
3411
+ active = false;
3412
+ disposeSafely(unsubscribe); disposeSafely(dispose);
3413
+ unsubscribe = dispose = current = undefined;
3414
+ }
3415
+ try {
3416
+ unsubscribe = slots.subscribe(source, sync);
3417
+ sync();
3418
+ } catch (error) {
3419
+ cleanup();
3420
+ reportDiagnostic(`optional slot subscription unavailable: ${error?.message || error}`);
3421
+ onError?.();
3422
+ }
3423
+ return cleanup;
3424
+ }
3425
+ function bridgeNativeJobs(slots) {
3426
+ return mirrorNativeEntry(slots, 'conversation.session.header.actions', 'workbench.context.jobs',
3427
+ (entry) => entry.options?.id === 'job-list');
3428
+ }
3429
+
3053
3430
  function ContextPanelBase(props) {
3054
3431
  const t = (key) => translate(props, key);
3055
3432
  const state = props.useSessions((value) => value) || {};
@@ -3108,6 +3485,7 @@ window.__ModuleLoader__.load({
3108
3485
  ) : h('div', { className: 'cw-context-subtle' }, t('contextEmpty')),
3109
3486
  categories.length ? h('p', { className: 'cw-context-subtle cw-context-note' }, t('contextEstimate')) : null,
3110
3487
  ),
3488
+ h(WorkbenchBoundary, null, h(LongTasks, { ...props, key: props.sessionId })),
3111
3489
  h(ContextOutputs, { key: props.sessionId, sessionId: props.sessionId, cwd, files: outputFiles, t }),
3112
3490
  h('section', { className: 'cw-context-section' },
3113
3491
  h('details', { className: 'cw-context-details' },
@@ -3157,6 +3535,8 @@ window.__ModuleLoader__.load({
3157
3535
  });
3158
3536
  const percentFormat = new Intl.NumberFormat(locale, { style: 'percent', maximumFractionDigits: 1 });
3159
3537
  const creditFormat = new Intl.NumberFormat(locale, { maximumFractionDigits: 2 });
3538
+ const compareNames = new Intl.Collator('en', { sensitivity: 'base' }).compare;
3539
+ const sortedUsage = [...usage].sort((first, second) => compareNames(first.name, second.name));
3160
3540
  const metric = (id, label, value) => h('div', {
3161
3541
  key: id, className: 'cw-metric-head cw-provider-balance',
3162
3542
  }, h('span', null, label), h('strong', null, value));
@@ -3170,7 +3550,7 @@ window.__ModuleLoader__.load({
3170
3550
  `${creditFormat.format(credits.remaining)} / ${creditFormat.format(credits.total)}`)) : null,
3171
3551
  );
3172
3552
 
3173
- return h('div', { className: 'cw-provider-usage', 'aria-live': 'polite' }, usage.map((provider) => {
3553
+ return h('div', { className: 'cw-provider-usage', 'aria-live': 'polite' }, sortedUsage.map((provider) => {
3174
3554
  const name = provider.id === 'dreamina' ? t('usageDreaminaName') : provider.name;
3175
3555
  const duplicate = usage.some((other) => other !== provider && other.name === provider.name);
3176
3556
  return h('section', { key: provider.id },
@@ -3183,6 +3563,10 @@ window.__ModuleLoader__.load({
3183
3563
  provider.balance ? metric('extra', t('usageExtraBalance'), formatMoney(provider.balance)) : null,
3184
3564
  provider.charges?.map((charge) => metric(charge.id,
3185
3565
  t(charge.id === 'spent' ? 'usageKeySpent' : 'usageKeyRemaining'), formatMoney(charge))),
3566
+ provider.organizationSpend?.length
3567
+ ? metric('organization-spend', t('usageOrganizationSpend'), provider.organizationSpend.map(formatMoney).join(' · ')) : null,
3568
+ Number.isFinite(provider.organizationTokens)
3569
+ ? metric('organization-tokens', t('usageOrganizationTokens'), formatTokenCount(provider.organizationTokens)) : null,
3186
3570
  provider.windows?.filter(({ id }) => Object.hasOwn(USAGE_WINDOW_LABELS, id)).map(windowView),
3187
3571
  Number.isFinite(provider.credits)
3188
3572
  ? metric('credits', t('usageDreaminaBalance'), creditFormat.format(provider.credits)) : null,
@@ -3198,7 +3582,7 @@ window.__ModuleLoader__.load({
3198
3582
  const meter = contextMeter(useProjection('contextPressure'));
3199
3583
  const usage = useProjection('tokenUsage');
3200
3584
  const [open, setOpen] = React.useState(false);
3201
- const { triggerRef, menuRef, position } = useAnchoredPopup(open, 340, 660);
3585
+ const { triggerRef, menuRef, position } = useAnchoredPopup(open, 340, 720);
3202
3586
  const ready = Boolean(position);
3203
3587
  const popupId = React.useId();
3204
3588
  const rows = [
@@ -3532,6 +3916,7 @@ window.__ModuleLoader__.load({
3532
3916
  const readAttachments = React.useCallback(() => attachmentDrafts.read(sessionId), [sessionId]);
3533
3917
  const { images, files, submission: draftSubmission } =
3534
3918
  React.useSyncExternalStore(attachmentDrafts.subscribe, readAttachments);
3919
+ const draftStorageStatus = React.useSyncExternalStore(attachmentDrafts.subscribe, attachmentDrafts.status);
3535
3920
  const [commandSending, setSending] = React.useState(false);
3536
3921
  const sending = commandSending || Boolean(draftSubmission);
3537
3922
  const [recognizing, setRecognizing] = React.useState(false);
@@ -3605,6 +3990,7 @@ window.__ModuleLoader__.load({
3605
3990
  }, [commandLauncherOpen]);
3606
3991
  const running = Boolean(session?.running);
3607
3992
  const unavailable = Boolean(props.disabled || props.blocked || !sessionId || !promptAllowed);
3993
+ const draftLoading = draftStorageStatus === 'loading';
3608
3994
  const voiceLocked = unavailable || sending;
3609
3995
  const previousVoiceDraft = voiceDraft.current;
3610
3996
  voiceDraft.current = {
@@ -3645,7 +4031,7 @@ window.__ModuleLoader__.load({
3645
4031
  },
3646
4032
  }), []);
3647
4033
  const t = (key) => translate(props, key);
3648
- const canSend = Boolean(!unavailable && !sending && (text.trim() || images.length || files.length));
4034
+ const canSend = Boolean(!unavailable && !draftLoading && !sending && (text.trim() || images.length || files.length));
3649
4035
  const slashQuery = images.length === 0 ? commandQuery(text) : null;
3650
4036
  const commandMenuVisible = commandLauncherOpen || (slashQuery !== null && commandDismissed !== text);
3651
4037
  const commandChoices = commandLauncherOpen && text.trim() && slashQuery === null
@@ -3728,6 +4114,7 @@ window.__ModuleLoader__.load({
3728
4114
  }, [commandMenuVisible, sessionId]);
3729
4115
 
3730
4116
  function addImageFiles(fileList) {
4117
+ if (draftLoading) { setError(t('draft_loading')); return; }
3731
4118
  const available = Math.max(0, MAX_IMAGES - readAttachments().images.length);
3732
4119
  const incoming = Array.from(fileList || []).filter((file) => file?.type?.startsWith('image/')).slice(0, available);
3733
4120
  if (incoming.length === 0) return;
@@ -3749,6 +4136,7 @@ window.__ModuleLoader__.load({
3749
4136
  }
3750
4137
 
3751
4138
  function addDraftFiles(fileList) {
4139
+ if (draftLoading) { setError(t('draft_loading')); return; }
3752
4140
  const incoming = Array.from(fileList || []).filter(Boolean);
3753
4141
  if (incoming.length === 0) return;
3754
4142
  if (readAttachments().files.length + incoming.length > MAX_FILES) {
@@ -4105,7 +4493,7 @@ window.__ModuleLoader__.load({
4105
4493
  ) : null,
4106
4494
  h('div', { className: 'cw-composer-row' },
4107
4495
  h(ComposerAddMenu, {
4108
- disabled: unavailable || sending,
4496
+ disabled: unavailable || draftLoading || sending,
4109
4497
  t,
4110
4498
  onImages: () => {
4111
4499
  if (images.length < MAX_IMAGES) fileInputRef.current?.click();
@@ -4174,6 +4562,12 @@ window.__ModuleLoader__.load({
4174
4562
  ),
4175
4563
  ),
4176
4564
  props.accessory || null,
4565
+ (images.length || files.length || ['loading', 'error'].includes(draftStorageStatus))
4566
+ ? h('div', { className: 'cw-draft-status', role: 'status' },
4567
+ t(`draft_${draftStorageStatus}`),
4568
+ draftStorageStatus === 'error' ? h('button', {
4569
+ type: 'button', onClick: () => attachmentDrafts.retry(),
4570
+ }, t('imageRetry')) : null) : null,
4177
4571
  error ? h('div', { className: 'cw-composer-error', role: 'alert' }, error) : null,
4178
4572
  props.blocked?.reason ? h('div', { className: 'cw-composer-error' }, props.blocked.reason) : null,
4179
4573
  ),
@@ -4191,41 +4585,55 @@ window.__ModuleLoader__.load({
4191
4585
 
4192
4586
  const HeroGreeting = withBoundary(HeroGreetingBase);
4193
4587
  const ToolCallMessage = withBoundary(ToolCallMessageBase);
4194
- const Composer = withBoundary(ComposerBase);
4588
+ function Composer(props, fallback) {
4589
+ const required = ['useSession', 'useAccess', 'useProjection', 'useSessions', 'renderSlot'];
4590
+ if (required.some((name) => typeof props[name] !== 'function')) {
4591
+ fallback();
4592
+ return null;
4593
+ }
4594
+ return h(WorkbenchBoundary, { onFailure: fallback }, h(ComposerBase, props));
4595
+ }
4195
4596
  const ContextPanel = withBoundary(ContextPanelBase);
4196
4597
  const HeaderContextButton = withBoundary(ContextButton);
4197
4598
 
4198
4599
  function registerComposer(slots) {
4199
- const disposeComposer = slots.register({
4200
- name: 'conversation.composer.bar', priority: -1,
4201
- children: { [WORKBENCH_VOICE_SLOT]: { kind: 'single', scope: 'session' } },
4202
- }, Composer);
4203
- let voiceEntry;
4204
- let disposeVoice;
4205
- function syncVoice() {
4206
- // DSH owns the original activity slot. Reuse its microphone contribution
4207
- // in a Workbench child slot without redeclaring the native slot.
4208
- const entry = slots.entriesOfSlot(NATIVE_INPUT_ACTIVITY)[0];
4209
- if (entry === voiceEntry) return;
4210
- disposeVoice?.();
4211
- disposeVoice = undefined;
4212
- voiceEntry = entry;
4213
- if (entry?.locale !== 'voice-input') return;
4214
- try {
4215
- disposeVoice = slots.register({
4216
- name: WORKBENCH_VOICE_SLOT, locale: entry.locale, inject: entry.inject,
4217
- }, entry.component);
4218
- } catch (error) {
4219
- reportDiagnostic(`voice input activation failed: ${error?.message || error}`);
4220
- }
4600
+ let active = true, fallbackPending = false, disposeComposer, disposeVoice;
4601
+ function cleanup() {
4602
+ if (!active) return;
4603
+ active = false;
4604
+ // Child mirrors/subscriptions go first, before their parent slot vanishes.
4605
+ disposeSafely(disposeVoice); disposeSafely(disposeComposer);
4606
+ disposeVoice = disposeComposer = undefined;
4607
+ }
4608
+ function fallback() {
4609
+ if (!active || fallbackPending) return;
4610
+ fallbackPending = true;
4611
+ // This callback belongs to one registration, never a newer replacement.
4612
+ queueMicrotask(() => {
4613
+ if (!active) return;
4614
+ runtime.composerFallback = true;
4615
+ cleanup(); compatibilityChanged();
4616
+ });
4221
4617
  }
4222
- const unsubscribe = slots.subscribe(NATIVE_INPUT_ACTIVITY, syncVoice);
4223
- syncVoice();
4224
- return () => {
4225
- unsubscribe();
4226
- disposeVoice?.();
4227
- disposeComposer();
4228
- };
4618
+ if (!['register', 'subscribe', 'entriesOfSlot'].every((key) => typeof slots?.[key] === 'function')) {
4619
+ runtime.composerFallback = true; compatibilityChanged(); return cleanup;
4620
+ }
4621
+ try {
4622
+ disposeComposer = slots.register({
4623
+ name: 'conversation.composer.bar', priority: -1,
4624
+ children: { [WORKBENCH_VOICE_SLOT]: { kind: 'single', scope: 'session' } },
4625
+ }, (props) => Composer(props, fallback));
4626
+ disposeVoice = mirrorNativeEntry(slots, NATIVE_INPUT_ACTIVITY, WORKBENCH_VOICE_SLOT,
4627
+ (entry) => entry.locale === 'voice-input', fallback);
4628
+ if (fallbackPending) {
4629
+ runtime.composerFallback = true; cleanup();
4630
+ } else runtime.composerFallback = false;
4631
+ } catch (error) {
4632
+ runtime.composerFallback = true; cleanup();
4633
+ reportDiagnostic(`composer integration unavailable: ${error?.message || error}`);
4634
+ }
4635
+ compatibilityChanged();
4636
+ return cleanup;
4229
4637
  }
4230
4638
 
4231
4639
  return {
@@ -4233,7 +4641,7 @@ window.__ModuleLoader__.load({
4233
4641
  'slots', 'locale', 'shortcuts', 'configForms', 'uiWorkspace', 'workspaces', 'sessions', 'remote',
4234
4642
  ],
4235
4643
  ...(typeof window !== 'undefined' && window.__CLAUDE_WORKBENCH_TEST__
4236
- ? { testHooks: { commandQuery, filterCommandRows, commandRowsFromDescriptors, commandInsertion, executeHostCommand, summarizeToolCall, summarizeWorkGroup, isDangerousPermission, rootModelGroup, formatTokenCount, contextPercent, mentionQuery, referenceRows, submitMode, formatBytes, voiceDraftInsertion, createSessionHistory, createAttachmentDrafts } }
4644
+ ? { testHooks: { commandQuery, filterCommandRows, commandRowsFromDescriptors, commandInsertion, executeHostCommand, summarizeToolCall, summarizeWorkGroup, isDangerousPermission, rootModelGroup, formatTokenCount, contextPercent, mentionQuery, referenceRows, submitMode, formatBytes, voiceDraftInsertion, createSessionHistory, createAttachmentDrafts, createAttachmentStorage, compatibilityChecks, orderJobs, jobElapsed, registerComposer, bridgeNativeJobs } }
4237
4645
  : {}),
4238
4646
  apply(ctx) {
4239
4647
  runtime.ctx = ctx;
@@ -4241,6 +4649,16 @@ window.__ModuleLoader__.load({
4241
4649
  runtime.workspaces = ctx.get('workspaces');
4242
4650
  runtime.sessions = ctx.get('sessions');
4243
4651
  runtime.composerSettings = ctx.configForms.get('ui-conversation');
4652
+ try {
4653
+ ctx.inject(['jobs'], (scope) => {
4654
+ scope.effect(() => {
4655
+ const jobs = scope.get('jobs'); runtime.jobs = jobs; compatibilityChanged();
4656
+ return () => { if (runtime.jobs === jobs) { runtime.jobs = undefined; compatibilityChanged(); } };
4657
+ }, 'fps: native background jobs');
4658
+ });
4659
+ } catch (error) {
4660
+ reportDiagnostic(`background jobs unavailable: ${error?.message || error}`);
4661
+ }
4244
4662
  for (const [property, namespace] of [
4245
4663
  ['commandsRemote', 'commands'], ['fileReferences', 'fileReferences'],
4246
4664
  ['sessionReferences', 'sessionReferenceResolver'], ['permissionPresets', 'permissionPresets'],
@@ -4249,9 +4667,9 @@ window.__ModuleLoader__.load({
4249
4667
  ctx.inject([`remote.${namespace}`], (scope) => {
4250
4668
  scope.effect(() => {
4251
4669
  const service = scope.remote?.[namespace];
4252
- runtime[property] = service;
4670
+ runtime[property] = service; compatibilityChanged();
4253
4671
  return () => {
4254
- if (runtime[property] === service) runtime[property] = undefined;
4672
+ if (runtime[property] === service) { runtime[property] = undefined; compatibilityChanged(); }
4255
4673
  };
4256
4674
  }, `claude-workbench: ${namespace}`);
4257
4675
  });
@@ -4362,7 +4780,7 @@ window.__ModuleLoader__.load({
4362
4780
  install('image generation settings', () => ctx.slots.inject(
4363
4781
  'plugins.bundle.config',
4364
4782
  guardInjection('image generation settings', () => ctx.slots.register({
4365
- name: 'plugins.bundle.config', key: 'dsh-fps-workbench', locale: NS,
4783
+ name: 'plugins.bundle.config', key: '@local/claude-code-workbench', locale: NS,
4366
4784
  }, withBoundary(ImageGenerationSettings))),
4367
4785
  )),
4368
4786
  install('new session greeting', () => ctx.slots.inject(
@@ -4401,8 +4819,10 @@ window.__ModuleLoader__.load({
4401
4819
  'sidebar.right.pane.tab',
4402
4820
  guardInjection('context body', () => ctx.slots.register({
4403
4821
  name: 'sidebar.right.pane.tab', key: CONTEXT_TAB,
4822
+ children: { 'workbench.context.jobs': { kind: 'single', scope: 'session' } },
4404
4823
  }, ContextPanel)),
4405
4824
  );
4825
+ const disposeJobs = install('native job details', () => bridgeNativeJobs(ctx.slots));
4406
4826
  const disposeHeader = ctx.slots.inject(
4407
4827
  'conversation.session.header.utilities',
4408
4828
  guardInjection('session header utilities', () => ctx.slots.register({
@@ -4410,6 +4830,7 @@ window.__ModuleLoader__.load({
4410
4830
  }, HeaderContextButton)),
4411
4831
  );
4412
4832
  return () => {
4833
+ disposeJobs();
4413
4834
  disposeHeader();
4414
4835
  disposeBody();
4415
4836
  disposeType();