@yolk_vat-y/dsh-project-memory 0.4.3 → 0.4.4

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/client.js CHANGED
@@ -25,6 +25,9 @@ window.__ModuleLoader__.load({
25
25
  "style.mono": "终端",
26
26
  "panel.syncing": "同步中…",
27
27
  "panel.sync-failed": "同步失败",
28
+ "panel.hints-on": "显示提示信息",
29
+ "panel.hints-off": "隐藏提示信息",
30
+ "drag.cancel": "取消拖拽",
28
31
  "panel.empty": "暂无任务",
29
32
  "panel.empty-desc": "让模型开始工作并维护 todo 清单后会自动建档",
30
33
  "panel.no-session": "还没有会话",
@@ -62,6 +65,9 @@ window.__ModuleLoader__.load({
62
65
  "style.mono": "Mono",
63
66
  "panel.syncing": "Syncing…",
64
67
  "panel.sync-failed": "Sync failed",
68
+ "panel.hints-on": "Show hints",
69
+ "panel.hints-off": "Hide hints",
70
+ "drag.cancel": "Cancel drag",
65
71
  "panel.empty": "No Tasks",
66
72
  "panel.empty-desc": "Tasks are created automatically when the model maintains a todo list",
67
73
  "panel.no-session": "No session yet",
@@ -93,13 +99,12 @@ window.__ModuleLoader__.load({
93
99
  };
94
100
  }
95
101
  //#endregion
96
- //#region src/client/task-store.ts
102
+ //#region src/client/task-data-store.ts
97
103
  /**
98
- * Task Panel Client Store(自包含:不依赖宿主 store 包)
99
- * Reactive store for task list, bound task, UI state.
100
- * 数据由 /tasks、/task 命令执行结果(JSON 载荷)写入,localStorage 持久化 UI 状态。
104
+ * Task Data Store — 仅管服务端同步的数据(tasks、boundTaskId、archivedCount)
105
+ * - 单向数据流:命令执行结果 → setTasks → 广播给其他标签页
106
+ * - 无 UI 状态,无 localStorage,纯内存 + BroadcastChannel 同步
101
107
  */
102
- /** 从命令输出文本中解析任务快照 JSON(支持 fenced ```json 块或尾部对象)。 */
103
108
  function parseTaskPayloadText(text) {
104
109
  if (!text) return null;
105
110
  let jsonBlock = null;
@@ -122,7 +127,87 @@ window.__ModuleLoader__.load({
122
127
  return null;
123
128
  }
124
129
  }
125
- const STORAGE_KEY = "dsh-pm-task-panel-state";
130
+ const SYNC_CHANNEL = typeof window !== "undefined" ? new BroadcastChannel("dsh-pm-tasks-data") : null;
131
+ let dataState = {
132
+ tasks: [],
133
+ boundTaskId: null,
134
+ archivedCount: 0,
135
+ lastUpdate: 0
136
+ };
137
+ const dataListeners = /* @__PURE__ */ new Set();
138
+ function setDataState(next) {
139
+ dataState = next;
140
+ for (const listener of dataListeners) listener();
141
+ }
142
+ function broadcastDataUpdate(tasks, archivedCount) {
143
+ if (!SYNC_CHANNEL) return;
144
+ SYNC_CHANNEL.postMessage({
145
+ type: "PROJECT_TASKS_UPDATED",
146
+ payload: {
147
+ tasks,
148
+ archivedCount
149
+ }
150
+ });
151
+ }
152
+ if (SYNC_CHANNEL) SYNC_CHANNEL.onmessage = (event) => {
153
+ const msg = event.data;
154
+ if (msg?.type === "PROJECT_TASKS_UPDATED" && msg.payload) {
155
+ const { tasks, archivedCount } = msg.payload;
156
+ setDataState((prev) => ({
157
+ ...prev,
158
+ tasks: tasks || [],
159
+ archivedCount: archivedCount ?? 0,
160
+ lastUpdate: Date.now()
161
+ }));
162
+ }
163
+ };
164
+ const taskDataStore = {
165
+ subscribe(listener) {
166
+ dataListeners.add(listener);
167
+ return () => {
168
+ dataListeners.delete(listener);
169
+ };
170
+ },
171
+ getSnapshot() {
172
+ return dataState;
173
+ },
174
+ actions: {
175
+ setTasks(payload) {
176
+ const nextTasks = payload.tasks || [];
177
+ const nextArchived = payload.archivedCount ?? 0;
178
+ setDataState({
179
+ tasks: nextTasks,
180
+ boundTaskId: payload.boundTaskId ?? null,
181
+ archivedCount: nextArchived,
182
+ lastUpdate: Date.now()
183
+ });
184
+ broadcastDataUpdate(nextTasks, nextArchived);
185
+ },
186
+ reset() {
187
+ setDataState({
188
+ tasks: [],
189
+ boundTaskId: null,
190
+ archivedCount: 0,
191
+ lastUpdate: 0
192
+ });
193
+ }
194
+ }
195
+ };
196
+ function useTaskData() {
197
+ return (0, react.useSyncExternalStore)(taskDataStore.subscribe, taskDataStore.getSnapshot, taskDataStore.getSnapshot);
198
+ }
199
+ function useTaskDataActions() {
200
+ return taskDataStore.actions;
201
+ }
202
+ //#endregion
203
+ //#region src/client/task-ui-store.ts
204
+ /**
205
+ * Task UI Store — 仅管本地 UI 状态(closed、minimized、position、expandedIds、theme)
206
+ * - 持久化到 localStorage(key: dsh-pm-task-panel-ui)
207
+ * - 不跨标签页同步(每个标签页独立)
208
+ * - 启动/刷新强制 closed: true,面板默认不显示
209
+ */
210
+ const STORAGE_KEY = "dsh-pm-task-panel-ui";
126
211
  function defaultPosition() {
127
212
  if (typeof window !== "undefined") return {
128
213
  x: Math.max(16, window.innerWidth - 408),
@@ -133,138 +218,276 @@ window.__ModuleLoader__.load({
133
218
  y: 0
134
219
  };
135
220
  }
136
- function getDefaultState() {
221
+ /**
222
+ * 启动时的初始状态:
223
+ * - closed 恒为 true(不读取 localStorage 的 closed),刷新后面板隐藏;
224
+ * - 其余(展开项/位置/主题)可恢复上次会话偏好,minimized 默认折叠成迷你条。
225
+ */
226
+ function getDefaultUIState() {
137
227
  if (typeof window !== "undefined") try {
138
228
  const saved = localStorage.getItem(STORAGE_KEY);
139
229
  if (saved) {
140
230
  const parsed = JSON.parse(saved);
141
231
  return {
142
- tasks: Array.isArray(parsed.tasks) ? parsed.tasks : [],
143
- boundTaskId: parsed.boundTaskId ?? null,
144
- archivedCount: parsed.archivedCount ?? 0,
145
232
  expandedTaskIds: Array.isArray(parsed.expandedTaskIds) ? parsed.expandedTaskIds : [],
146
233
  panelPosition: parsed.panelPosition ?? defaultPosition(),
147
234
  minimized: parsed.minimized !== false,
148
- theme: typeof parsed.theme === "string" ? parsed.theme : "native",
149
- closed: !!parsed.closed,
150
- lastUpdate: parsed.lastUpdate ?? 0
235
+ closed: true,
236
+ theme: typeof parsed.theme === "string" ? parsed.theme : "native"
151
237
  };
152
238
  }
153
239
  } catch {}
154
240
  return {
155
- tasks: [],
156
- boundTaskId: null,
157
- archivedCount: 0,
158
241
  expandedTaskIds: [],
159
242
  panelPosition: defaultPosition(),
160
243
  minimized: true,
161
- closed: false,
162
- theme: "native",
163
- lastUpdate: 0
244
+ closed: true,
245
+ theme: "native"
164
246
  };
165
247
  }
166
- function persist(state) {
248
+ function persistUI(state) {
167
249
  if (typeof window === "undefined") return;
168
250
  try {
169
251
  localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
170
252
  } catch {}
171
253
  }
172
- let state = getDefaultState();
173
- const listeners = /* @__PURE__ */ new Set();
174
- function setState(next) {
175
- state = next;
176
- persist(next);
177
- for (const listener of listeners) listener();
254
+ let uiState = getDefaultUIState();
255
+ const uiListeners = /* @__PURE__ */ new Set();
256
+ function setUIState(next) {
257
+ uiState = next;
258
+ persistUI(next);
259
+ for (const listener of uiListeners) listener();
178
260
  }
179
- const taskStore = {
261
+ const taskUIStore = {
180
262
  subscribe(listener) {
181
- listeners.add(listener);
263
+ uiListeners.add(listener);
182
264
  return () => {
183
- listeners.delete(listener);
265
+ uiListeners.delete(listener);
184
266
  };
185
267
  },
186
268
  getSnapshot() {
187
- return state;
269
+ return uiState;
188
270
  },
189
271
  actions: {
190
- setTasks(payload) {
191
- setState({
192
- ...state,
193
- tasks: payload.tasks || [],
194
- boundTaskId: payload.boundTaskId ?? null,
195
- archivedCount: payload.archivedCount ?? 0,
196
- lastUpdate: Date.now()
197
- });
198
- },
199
272
  toggleTaskExpanded(taskId) {
200
- const prev = state;
273
+ const prev = uiState;
201
274
  const ids = prev.expandedTaskIds.includes(taskId) ? prev.expandedTaskIds.filter((id) => id !== taskId) : [...prev.expandedTaskIds, taskId];
202
- setState({
275
+ setUIState({
203
276
  ...prev,
204
277
  expandedTaskIds: ids
205
278
  });
206
279
  },
207
- expandAll() {
208
- const prev = state;
209
- setState({
210
- ...prev,
211
- expandedTaskIds: prev.tasks.map((t) => t.id)
280
+ expandAll(taskIds) {
281
+ setUIState({
282
+ ...uiState,
283
+ expandedTaskIds: taskIds
212
284
  });
213
285
  },
214
286
  setPanelPosition(pos) {
215
- setState({
216
- ...state,
287
+ setUIState({
288
+ ...uiState,
217
289
  panelPosition: pos
218
290
  });
219
291
  },
220
- /** 展开面板(同时解除隐藏/折叠)。 */
221
292
  open() {
222
- setState({
223
- ...state,
293
+ setUIState({
294
+ ...uiState,
224
295
  minimized: false,
225
296
  closed: false
226
297
  });
227
298
  },
228
- /** 折叠成顶部迷你条(仍可见)。 */
229
299
  minimize() {
230
- setState({
231
- ...state,
300
+ setUIState({
301
+ ...uiState,
232
302
  minimized: true,
233
303
  closed: false
234
304
  });
235
305
  },
236
- /** 彻底隐藏(无迷你条);仅能通过 /task、/tasks 或重新加载唤起。 */
237
306
  close() {
238
- setState({
239
- ...state,
307
+ setUIState({
308
+ ...uiState,
240
309
  minimized: true,
241
310
  closed: true
242
311
  });
243
312
  },
244
313
  setTheme(theme) {
245
- setState({
246
- ...state,
314
+ setUIState({
315
+ ...uiState,
247
316
  theme
248
317
  });
249
318
  },
250
- setMinimized(minimized) {
251
- setState({
252
- ...state,
253
- minimized,
254
- closed: minimized ? state.closed : false
255
- });
256
- },
257
319
  reset() {
258
- setState(getDefaultState());
320
+ setUIState(getDefaultUIState());
259
321
  }
260
322
  }
261
323
  };
262
- function useTaskStore() {
263
- return (0, react.useSyncExternalStore)(taskStore.subscribe, taskStore.getSnapshot, taskStore.getSnapshot);
324
+ function useTaskUI() {
325
+ return (0, react.useSyncExternalStore)(taskUIStore.subscribe, taskUIStore.getSnapshot, taskUIStore.getSnapshot);
326
+ }
327
+ function useTaskUIActions() {
328
+ return taskUIStore.actions;
329
+ }
330
+ //#endregion
331
+ //#region src/client/task-hooks.ts
332
+ /**
333
+ * Task Panel Hooks — 步骤拖拽交互逻辑(useTaskDrag)。
334
+ * 只依赖 react + task-data-store 类型,不碰数据/UI store。
335
+ */
336
+ /**
337
+ * useTaskDrag — 步骤拖拽重排(绑定任务内,同一任务卡的步骤行间移动)。
338
+ *
339
+ * 交互模型(拖动全程按住鼠标):
340
+ * - 步骤行上按下(左键、非按钮/输入区域)→ 进入预览;
341
+ * - 按住 350ms 不松,或按住后移动超过 6px → 唤起跟随鼠标的浮动幽灵卡片;
342
+ * - 幽灵悬停到本任务卡内的步骤行时显示蓝色插入线(上半=插到该行前,下半=插到该行后);
343
+ * - 松手(mouseup):落在有效位置 → onReorder 落子;落在卡外/别的任务卡/原行
344
+ * (插入自身前后)/按钮输入等控件上 → 取消;
345
+ * - 350ms 内原地松手 = 普通点击,不产生幽灵;Esc → 取消。
346
+ */
347
+ function useTaskDrag(taskId, steps, isBound, onReorder) {
348
+ const [dragState, setDragState] = (0, react.useState)(null);
349
+ const [dropTargetIndex, setDropTargetIndex] = (0, react.useState)(null);
350
+ const dragRef = (0, react.useRef)(dragState);
351
+ dragRef.current = dragState;
352
+ const longPressTimer = (0, react.useRef)(void 0);
353
+ const dragOrigin = (0, react.useRef)(null);
354
+ const clearTimer = (0, react.useCallback)(() => {
355
+ if (longPressTimer.current !== void 0) {
356
+ window.clearTimeout(longPressTimer.current);
357
+ longPressTimer.current = void 0;
358
+ }
359
+ }, []);
360
+ const cancelDrag = (0, react.useCallback)(() => {
361
+ clearTimer();
362
+ dragOrigin.current = null;
363
+ document.body.style.userSelect = "";
364
+ setDropTargetIndex(null);
365
+ setDragState(null);
366
+ }, [clearTimer]);
367
+ const startDrag = (0, react.useCallback)((fromIndex, stepContent, e) => {
368
+ if (!isBound || e.button !== 0) return;
369
+ if (dragRef.current) return;
370
+ e.preventDefault();
371
+ e.stopPropagation();
372
+ document.body.style.userSelect = "none";
373
+ dragOrigin.current = {
374
+ x: e.clientX,
375
+ y: e.clientY
376
+ };
377
+ setDropTargetIndex(null);
378
+ setDragState({
379
+ fromIndex,
380
+ stepContent,
381
+ clientX: e.clientX,
382
+ clientY: e.clientY,
383
+ dragging: false
384
+ });
385
+ clearTimer();
386
+ longPressTimer.current = window.setTimeout(() => {
387
+ longPressTimer.current = void 0;
388
+ setDragState((prev) => prev ? {
389
+ ...prev,
390
+ dragging: true
391
+ } : null);
392
+ }, 350);
393
+ }, [isBound, clearTimer]);
394
+ /** 计算某行上 y 坐标对应的插入位(行数下标:上半=该行前,下半=该行后);非本任务卡返回 null */
395
+ const dropIndexAt = (0, react.useCallback)((row, clientY) => {
396
+ const parentOl = row.closest("ol");
397
+ if (!parentOl || !parentOl.closest(`article[data-task-id="${taskId}"]`)) return null;
398
+ const index = Array.from(parentOl.children).indexOf(row);
399
+ const rect = row.getBoundingClientRect();
400
+ return clientY < rect.top + rect.height / 2 ? index : index + 1;
401
+ }, [taskId]);
402
+ /** 在 (x,y) 处结算:命中有效落点则重排,否则取消;并结束本次拖拽 */
403
+ const commitDrop = (0, react.useCallback)((x, y) => {
404
+ const cur = dragRef.current;
405
+ if (!cur || !cur.dragging) return;
406
+ const el = document.elementFromPoint(x, y);
407
+ if (el && !el.closest("button, input, textarea, a")) {
408
+ const row = el.closest("[data-step-row]");
409
+ if (row) {
410
+ const drop = dropIndexAt(row, y);
411
+ if (drop !== null) {
412
+ if ((drop > cur.fromIndex ? drop - 1 : drop) !== cur.fromIndex) onReorder(cur.fromIndex, drop);
413
+ }
414
+ }
415
+ }
416
+ cancelDrag();
417
+ }, [
418
+ dropIndexAt,
419
+ onReorder,
420
+ cancelDrag
421
+ ]);
422
+ const active = dragState !== null;
423
+ (0, react.useEffect)(() => {
424
+ if (!active) return;
425
+ const onMouseMove = (e) => {
426
+ const cur = dragRef.current;
427
+ if (!cur) return;
428
+ if (!cur.dragging) {
429
+ const origin = dragOrigin.current;
430
+ if (!origin) return;
431
+ if (Math.hypot(e.clientX - origin.x, e.clientY - origin.y) < 6) return;
432
+ clearTimer();
433
+ setDragState((prev) => prev ? {
434
+ ...prev,
435
+ dragging: true,
436
+ clientX: e.clientX,
437
+ clientY: e.clientY
438
+ } : null);
439
+ } else setDragState((prev) => prev ? {
440
+ ...prev,
441
+ clientX: e.clientX,
442
+ clientY: e.clientY
443
+ } : null);
444
+ const row = document.elementFromPoint(e.clientX, e.clientY)?.closest("[data-step-row]");
445
+ setDropTargetIndex(row ? dropIndexAt(row, e.clientY) : null);
446
+ };
447
+ const onMouseUp = (e) => {
448
+ const cur = dragRef.current;
449
+ if (!cur) return;
450
+ if (cur.dragging) commitDrop(e.clientX, e.clientY);
451
+ else cancelDrag();
452
+ };
453
+ const onClick = (e) => {
454
+ const cur = dragRef.current;
455
+ if (!cur || !cur.dragging) return;
456
+ commitDrop(e.clientX, e.clientY);
457
+ };
458
+ const onKeyDown = (e) => {
459
+ if (e.key === "Escape") cancelDrag();
460
+ };
461
+ window.addEventListener("mousemove", onMouseMove);
462
+ window.addEventListener("mouseup", onMouseUp);
463
+ window.addEventListener("click", onClick, true);
464
+ window.addEventListener("keydown", onKeyDown);
465
+ return () => {
466
+ window.removeEventListener("mousemove", onMouseMove);
467
+ window.removeEventListener("mouseup", onMouseUp);
468
+ window.removeEventListener("click", onClick, true);
469
+ window.removeEventListener("keydown", onKeyDown);
470
+ document.body.style.userSelect = "";
471
+ };
472
+ }, [
473
+ active,
474
+ taskId,
475
+ dropIndexAt,
476
+ onReorder,
477
+ cancelDrag,
478
+ commitDrop
479
+ ]);
480
+ (0, react.useEffect)(() => () => clearTimer(), [clearTimer]);
481
+ return {
482
+ dragState,
483
+ dropTargetIndex,
484
+ startDrag,
485
+ cancelDrag
486
+ };
264
487
  }
265
488
  //#endregion
266
489
  //#region \0dsh-css:/home/sxt/project/dsh-project-memory/src/client/TaskPanel.module.css.mjs
267
- const css$1 = ".vZSZnG_panel,.vZSZnG_pill{pointer-events:auto;font-family:inherit}.vZSZnG_panel{background:var(--dsw-alias-bg-layer-2,var(--dsw-alias-bg-base,#fff));border:1px solid var(--dsw-alias-border-l2,#7f7f7f4d);border-radius:14px;flex-direction:column;width:380px;max-width:calc(100vw - 24px);max-height:min(70vh,640px);animation:.18s ease-out vZSZnG_panelIn;display:flex;position:fixed;overflow:hidden;box-shadow:0 12px 32px #00000029,0 2px 8px #00000014}@keyframes vZSZnG_panelIn{0%{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:none}}.vZSZnG_dragHandle{cursor:grab;z-index:1;justify-content:center;align-items:center;height:12px;display:flex;position:absolute;top:0;left:0;right:0}.vZSZnG_dragHandle:active{cursor:grabbing}.vZSZnG_handleGrip{background:var(--dsw-alias-separator-primary,#7f7f7f59);opacity:.8;border-radius:2px;width:32px;height:3px}.vZSZnG_header{border-bottom:1px solid var(--dsw-alias-border-l1,#7f7f7f2e);-webkit-user-select:none;user-select:none;justify-content:space-between;align-items:center;gap:8px;padding:10px 10px 8px 14px;display:flex}.vZSZnG_headerLeft{align-items:center;gap:6px;min-width:0;display:flex}.vZSZnG_headerIcon{color:var(--dsw-alias-label-secondary);flex:none}.vZSZnG_headerTitle{color:var(--dsw-alias-label-primary);white-space:nowrap;margin:0;font-size:13px;font-weight:600;line-height:20px}.vZSZnG_headerRight{align-items:center;gap:2px;margin-left:auto;display:flex}.vZSZnG_counts{color:var(--dsw-alias-label-tertiary);white-space:nowrap;margin-right:4px;font-size:11px;line-height:16px}.vZSZnG_boundBadge{color:var(--dsw-alias-label-primary-foreground,#fff);background:var(--dsw-alias-button-primary-fill,#3a6ef5);white-space:nowrap;border-radius:7px;flex:none;padding:0 6px;font-size:10px;line-height:14px}.vZSZnG_softBadge{color:var(--dsw-alias-label-tertiary);background:var(--dsw-alias-interactive-bg-hover,#7f7f7f1f);white-space:nowrap;border-radius:7px;flex:none;padding:0 6px;font-size:10px;line-height:14px}.vZSZnG_notice{color:var(--dsw-alias-state-warn-label,#b7791f);background:var(--dsw-alias-interactive-bg-hover,#7f7f7f1a);word-break:break-all;border-radius:8px;margin:8px 12px 0;padding:6px 10px;font-size:12px;line-height:18px}.vZSZnG_taskList{scrollbar-width:thin;scrollbar-color:var(--dsw-alias-scrollbar-bg-l2,transparent) transparent;flex-direction:column;gap:6px;padding:8px;display:flex;overflow-y:auto}.vZSZnG_emptyState{text-align:center;color:var(--dsw-alias-label-secondary);padding:36px 24px 30px}.vZSZnG_emptyTitle{color:var(--dsw-alias-label-primary);margin:0 0 6px;font-size:13px;font-weight:600}.vZSZnG_emptyDesc{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:20px}.vZSZnG_syncHint{color:var(--dsw-alias-label-dimmed);justify-content:center;align-items:center;gap:6px;margin:10px 0 0;font-size:11px;display:flex}.vZSZnG_card{border:1px solid var(--dsw-alias-border-l1,#7f7f7f29);background:var(--dsw-alias-bg-base,transparent);border-radius:10px;transition:border-color .15s;overflow:hidden}.vZSZnG_cardBound{border-color:var(--dsw-alias-border-l3,#7f7f7f59);box-shadow:inset 0 0 0 .5px var(--dsw-alias-border-l3,transparent)}.vZSZnG_cardHead{cursor:pointer;text-align:left;background:0 0;border:none;justify-content:space-between;align-items:center;gap:8px;width:100%;padding:8px 10px 7px;display:flex}.vZSZnG_cardHead:hover{background:var(--dsw-alias-interactive-bg-hover,#7f7f7f14)}.vZSZnG_cardTitleRow{align-items:center;gap:6px;min-width:0;display:flex}.vZSZnG_cardTitle{color:var(--dsw-alias-label-primary);white-space:nowrap;text-overflow:ellipsis;font-size:13px;font-weight:500;line-height:20px;overflow:hidden}.vZSZnG_cardMeta{color:var(--dsw-alias-label-tertiary);flex:none;align-items:center;gap:8px;display:flex}.vZSZnG_progressText{font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-secondary);font-size:11px;line-height:16px}.vZSZnG_updated{color:var(--dsw-alias-label-dimmed);font-size:10px;line-height:14px}.vZSZnG_chevron{color:var(--dsw-alias-label-tertiary);display:inline-flex}.vZSZnG_track{background:var(--dsw-alias-interactive-bg-hover,#7f7f7f1f);border-radius:1px;height:2px;margin:0 10px 6px;overflow:hidden}.vZSZnG_trackFill{background:var(--dsw-alias-label-primary-bluish,var(--dsw-alias-button-primary-fill,#3a6ef5));border-radius:1px;height:100%;transition:width .2s}.vZSZnG_cardBody{border-top:1px solid var(--dsw-alias-border-l1,#7f7f7f24);padding:8px 10px 10px}.vZSZnG_section{margin-bottom:8px}.vZSZnG_section:last-child{margin-bottom:0}.vZSZnG_sectionLabel{color:var(--dsw-alias-label-secondary);align-items:center;gap:6px;margin-bottom:4px;font-size:11px;line-height:16px;display:flex}.vZSZnG_sectionCount{background:var(--dsw-alias-interactive-bg-hover,#7f7f7f1a);color:var(--dsw-alias-label-tertiary);border-radius:6px;padding:0 5px;font-size:10px;line-height:14px}.vZSZnG_stepsList{flex-direction:column;gap:1px;margin:0;padding:0;list-style:none;display:flex}.vZSZnG_stepRow{align-items:flex-start;gap:7px;padding:3px 2px;display:flex}.vZSZnG_stepRow>svg{flex:none;margin-top:2px}.vZSZnG_stepDone{color:var(--dsw-alias-label-dimmed)}.vZSZnG_stepRun{color:var(--dsw-alias-label-primary-bluish,#3a6ef5)}.vZSZnG_stepPending{background:var(--dsw-alias-border-l3,#7f7f7f80);border-radius:50%;flex:none;width:8px;height:8px;margin:5px 3px}.vZSZnG_stepContent{color:var(--dsw-alias-label-secondary);word-break:break-word;font-size:12px;line-height:20px}.vZSZnG_stepContentDone{color:var(--dsw-alias-label-dimmed);text-decoration:line-through}.vZSZnG_stepContentRun{color:var(--dsw-alias-label-primary)}.vZSZnG_muted{color:var(--dsw-alias-label-dimmed);font-size:11px;line-height:18px}.vZSZnG_filesList{flex-direction:column;margin:0;padding:0;list-style:none;display:flex}.vZSZnG_fileRow{cursor:pointer;text-align:left;background:0 0;border:none;border-radius:6px;align-items:center;gap:6px;width:100%;padding:3px 4px;display:flex}.vZSZnG_fileRow:hover{background:var(--dsw-alias-interactive-bg-hover,#7f7f7f14)}.vZSZnG_fileDot{background:var(--dsw-alias-border-l3,#7f7f7f80);border-radius:50%;flex:none;width:5px;height:5px}.vZSZnG_filePath{color:var(--dsw-alias-label-secondary);white-space:nowrap;text-overflow:ellipsis;text-align:left;direction:rtl;font-family:ui-monospace,SF Mono,Menlo,Consolas,monospace;font-size:11px;line-height:18px;overflow:hidden}.vZSZnG_fileLine{color:var(--dsw-alias-label-dimmed);flex:none;font-size:10px;line-height:18px}.vZSZnG_cardFooter{gap:6px;margin-top:8px;display:flex}.vZSZnG_spinning svg{animation:.9s linear infinite vZSZnG_spin}@keyframes vZSZnG_spin{to{transform:rotate(360deg)}}@media (prefers-reduced-motion:reduce){.vZSZnG_panel{animation:none}.vZSZnG_trackFill,.vZSZnG_card,.vZSZnG_pill{transition:none}.vZSZnG_spinning svg{animation-duration:2.4s}}.vZSZnG_miniBar{border:1px solid var(--dsw-alias-border-l3,#7f7f7f4d);background:var(--dsw-alias-bg-layer-2,var(--dsw-alias-bg-base,#fff));max-width:min(46vw,340px);height:34px;color:var(--dsw-alias-label-primary);cursor:grab;-webkit-user-select:none;user-select:none;pointer-events:auto;border-radius:999px;align-items:center;gap:7px;padding:0 12px;font-size:12px;line-height:18px;transition:background .15s,border-color .15s;display:inline-flex;position:fixed;box-shadow:0 6px 20px #00000024,0 1px 4px #00000014}.vZSZnG_miniBar:active{cursor:grabbing}.vZSZnG_miniBar:hover{background:var(--dsw-alias-interactive-bg-hover,#7f7f7f14)}.vZSZnG_miniIcon{color:var(--dsw-alias-label-secondary);flex:none}.vZSZnG_miniText{white-space:nowrap;text-overflow:ellipsis;overflow:hidden}.vZSZnG_miniChevron{color:var(--dsw-alias-label-tertiary);flex:none}.vZSZnG_taskList{flex:auto;min-height:0}.vZSZnG_cardBody{overscroll-behavior:contain;max-height:300px;overflow-y:auto}.vZSZnG_card{flex-direction:column;display:flex}.vZSZnG_boundaryFallback{border:1px solid var(--dsw-alias-border-l3,#7f7f7f4d);background:var(--dsw-alias-bg-layer-2,#fff);color:var(--dsw-alias-label-secondary);cursor:pointer;pointer-events:auto;border-radius:999px;padding:4px 10px;font-size:12px;line-height:18px;position:fixed;top:64px;right:16px}.vZSZnG_stepToggle{cursor:pointer;background:0 0;border:none;border-radius:4px;flex:none;align-items:center;margin-top:2px;padding:0;display:inline-flex}.vZSZnG_stepToggle:hover{background:var(--dsw-alias-interactive-bg-hover,#7f7f7f1f)}.vZSZnG_stepToggle:disabled{cursor:default;opacity:.5}.vZSZnG_stepEditable{cursor:text;border-radius:3px}.vZSZnG_stepEditable:hover{outline:1px dashed var(--dsw-alias-border-l3,#7f7f7f66);outline-offset:1px}.vZSZnG_stepInput{min-width:0;font:inherit;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-base,transparent);border:1px solid var(--dsw-alias-border-l3,#7f7f7f73);resize:none;box-sizing:border-box;white-space:pre-wrap;word-break:break-word;border-radius:5px;flex:auto;width:auto;min-width:0;padding:0 5px;font-size:12px;line-height:20px;display:block;overflow:hidden}.vZSZnG_cardTitleEditable{cursor:text;border-radius:3px}.vZSZnG_cardTitleEditable:hover{outline:1px dashed var(--dsw-alias-border-l3,#7f7f7f66);outline-offset:1px}.vZSZnG_panel[data-theme=glass],.vZSZnG_miniBar[data-theme=glass]{background:color-mix(in srgb, var(--dsw-alias-bg-layer-2,#fff) 90%, transparent);-webkit-backdrop-filter:blur(18px)saturate(1.2);border:1px solid color-mix(in srgb, var(--dsw-alias-border-l2,#7f7f7f4d) 80%, transparent)}.vZSZnG_panel[data-theme=glass] .vZSZnG_card{background:color-mix(in srgb, var(--dsw-alias-bg-base,transparent) 84%, transparent);border-color:color-mix(in srgb, var(--dsw-alias-border-l1,#7f7f7f29) 60%, transparent)}.vZSZnG_panel[data-theme=glass] .vZSZnG_cardBody{border-top-color:color-mix(in srgb, var(--dsw-alias-border-l1,#7f7f7f24) 60%, transparent)}@media (prefers-reduced-transparency:reduce){.vZSZnG_panel[data-theme=glass],.vZSZnG_miniBar[data-theme=glass]{background:var(--dsw-alias-bg-layer-2,#fff);-webkit-backdrop-filter:none;backdrop-filter:none}.vZSZnG_panel[data-theme=glass] .vZSZnG_card{background:var(--dsw-alias-bg-base,transparent)}}@supports not ((-webkit-backdrop-filter:blur(1px)) or (backdrop-filter:blur(1px))){.vZSZnG_panel[data-theme=glass],.vZSZnG_miniBar[data-theme=glass]{background:var(--dsw-alias-bg-layer-2,#fff)}}.vZSZnG_panel[data-theme=brutal]{box-shadow:none;border-width:1px;border-radius:6px}.vZSZnG_panel[data-theme=brutal] .vZSZnG_card{border:1px solid var(--dsw-alias-border-l3,#7f7f7f6b);background:var(--dsw-alias-bg-base,transparent);box-shadow:none;border-radius:3px}.vZSZnG_panel[data-theme=brutal] .vZSZnG_cardHead{padding:7px 10px 6px}.vZSZnG_panel[data-theme=brutal] .vZSZnG_cardTitle{letter-spacing:.01em;font-weight:650}.vZSZnG_panel[data-theme=brutal] .vZSZnG_headerTitle{letter-spacing:.03em}.vZSZnG_panel[data-theme=brutal] .vZSZnG_boundBadge{border-radius:2px}.vZSZnG_panel[data-theme=brutal] .vZSZnG_track{border-radius:0;height:3px}.vZSZnG_panel[data-theme=brutal] .vZSZnG_trackFill{border-radius:0}.vZSZnG_panel[data-theme=brutal] .vZSZnG_cardBody{border-top:1px solid var(--dsw-alias-border-l2,#7f7f7f3d)}.vZSZnG_panel[data-theme=mono] .vZSZnG_headerTitle,.vZSZnG_panel[data-theme=mono] .vZSZnG_counts,.vZSZnG_panel[data-theme=mono] .vZSZnG_cardTitle,.vZSZnG_panel[data-theme=mono] .vZSZnG_progressText,.vZSZnG_panel[data-theme=mono] .vZSZnG_stepContent,.vZSZnG_panel[data-theme=mono] .vZSZnG_filePath{font-family:ui-monospace,SF Mono,Menlo,Consolas,Liberation Mono,monospace}.vZSZnG_panel[data-theme=mono] .vZSZnG_cardTitle{font-size:12px}.vZSZnG_panel[data-theme=mono] .vZSZnG_stepContent{font-size:11.5px}.vZSZnG_panel[data-theme=mono] .vZSZnG_card{border-radius:5px}.vZSZnG_panel[data-theme=mono] .vZSZnG_trackFill{border-radius:0}.vZSZnG_panel[data-theme=mono] .vZSZnG_stepPending{border-radius:1px}.vZSZnG_panel[data-theme=mono] .vZSZnG_updated{letter-spacing:.02em}.vZSZnG_headerIconBtn{color:inherit;cursor:pointer;background:0 0;border:none;border-radius:0;flex:none;justify-content:center;align-items:center;margin:0;padding:0;display:inline-flex}.vZSZnG_headerIcon{color:var(--dsw-alias-label-secondary);flex:none;display:block}.vZSZnG_headerLeft{min-width:0}.vZSZnG_headerTitle{text-overflow:ellipsis;flex:0 auto;min-width:0;overflow:hidden}.vZSZnG_headerRight{min-width:0}.vZSZnG_counts{text-overflow:ellipsis;flex:none;max-width:45%;margin-left:auto;overflow:hidden}";
490
+ const css$1 = ".vZSZnG_panel{pointer-events:auto;background:var(--dsw-alias-bg-layer-2,var(--dsw-alias-bg-base,#fff));border:1px solid var(--dsw-alias-border-l2,#7f7f7f4d);border-radius:14px;flex-direction:column;width:380px;max-width:calc(100vw - 24px);max-height:min(70vh,640px);font-family:inherit;animation:.18s ease-out vZSZnG_panelIn;display:flex;position:fixed;overflow:hidden;box-shadow:0 12px 32px #00000029,0 2px 8px #00000014}@keyframes vZSZnG_panelIn{0%{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:none}}.vZSZnG_dragHandle{cursor:grab;z-index:1;justify-content:center;align-items:center;height:12px;display:flex;position:absolute;top:0;left:0;right:0}.vZSZnG_dragHandle:active{cursor:grabbing}.vZSZnG_handleGrip{background:var(--dsw-alias-separator-primary,#7f7f7f59);opacity:.8;border-radius:2px;width:32px;height:3px}.vZSZnG_header{border-bottom:1px solid var(--dsw-alias-border-l1,#7f7f7f2e);-webkit-user-select:none;user-select:none;justify-content:space-between;align-items:center;gap:8px;padding:10px 10px 8px 14px;display:flex}.vZSZnG_headerLeft{align-items:center;gap:6px;min-width:0;display:flex}.vZSZnG_headerIcon{color:var(--dsw-alias-label-secondary);flex:none}.vZSZnG_headerTitle{color:var(--dsw-alias-label-primary);white-space:nowrap;margin:0;font-size:13px;font-weight:600;line-height:20px}.vZSZnG_headerRight{align-items:center;gap:2px;margin-left:auto;display:flex}.vZSZnG_counts{color:var(--dsw-alias-label-tertiary);white-space:nowrap;margin-right:4px;font-size:11px;line-height:16px}.vZSZnG_boundBadge{color:var(--dsw-alias-label-primary-foreground,#fff);background:var(--dsw-alias-button-primary-fill,#3a6ef5);white-space:nowrap;border-radius:7px;flex:none;padding:0 6px;font-size:10px;line-height:14px}.vZSZnG_notice{color:var(--dsw-alias-state-warn-label,#b7791f);background:var(--dsw-alias-interactive-bg-hover,#7f7f7f1a);word-break:break-all;border-radius:8px;margin:8px 12px 0;padding:6px 10px;font-size:12px;line-height:18px}.vZSZnG_taskList{scrollbar-width:thin;scrollbar-color:var(--dsw-alias-scrollbar-bg-l2,transparent) transparent;flex-direction:column;gap:6px;padding:8px;display:flex;overflow-y:auto}.vZSZnG_emptyState{text-align:center;color:var(--dsw-alias-label-secondary);padding:36px 24px 30px}.vZSZnG_emptyTitle{color:var(--dsw-alias-label-primary);margin:0 0 6px;font-size:13px;font-weight:600}.vZSZnG_emptyDesc{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:20px}.vZSZnG_syncHint{color:var(--dsw-alias-label-dimmed);justify-content:center;align-items:center;gap:6px;margin:10px 0 0;font-size:11px;display:flex}.vZSZnG_card{border:1px solid var(--dsw-alias-border-l1,#7f7f7f29);background:var(--dsw-alias-bg-base,transparent);border-radius:10px;transition:border-color .15s;overflow:hidden}.vZSZnG_cardBound{border-color:var(--dsw-alias-border-l3,#7f7f7f59);box-shadow:inset 0 0 0 .5px var(--dsw-alias-border-l3,transparent)}.vZSZnG_cardHead{cursor:pointer;text-align:left;background:0 0;border:none;justify-content:space-between;align-items:center;gap:8px;width:100%;padding:8px 10px 7px;display:flex}.vZSZnG_cardHead:hover{background:var(--dsw-alias-interactive-bg-hover,#7f7f7f14)}.vZSZnG_cardTitleRow{align-items:center;gap:6px;min-width:0;display:flex}.vZSZnG_cardTitle{color:var(--dsw-alias-label-primary);white-space:nowrap;text-overflow:ellipsis;font-size:13px;font-weight:500;line-height:20px;overflow:hidden}.vZSZnG_cardMeta{color:var(--dsw-alias-label-tertiary);flex:none;align-items:center;gap:8px;display:flex}.vZSZnG_progressText{font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-secondary);font-size:11px;line-height:16px}.vZSZnG_updated{color:var(--dsw-alias-label-dimmed);font-size:10px;line-height:14px}.vZSZnG_chevron{color:var(--dsw-alias-label-tertiary);display:inline-flex}.vZSZnG_track{background:var(--dsw-alias-interactive-bg-hover,#7f7f7f1f);border-radius:1px;height:2px;margin:0 10px 6px;overflow:hidden}.vZSZnG_trackFill{background:var(--dsw-alias-label-primary-bluish,var(--dsw-alias-button-primary-fill,#3a6ef5));border-radius:1px;height:100%;transition:width .2s}.vZSZnG_cardBody{border-top:1px solid var(--dsw-alias-border-l1,#7f7f7f24);overscroll-behavior:contain;max-height:300px;padding:8px 10px 10px;overflow-y:auto}.vZSZnG_section{margin-bottom:8px}.vZSZnG_section:last-child{margin-bottom:0}.vZSZnG_sectionLabel{color:var(--dsw-alias-label-secondary);align-items:center;gap:6px;margin-bottom:4px;font-size:11px;line-height:16px;display:flex}.vZSZnG_sectionCount{background:var(--dsw-alias-interactive-bg-hover,#7f7f7f1a);color:var(--dsw-alias-label-tertiary);border-radius:6px;padding:0 5px;font-size:10px;line-height:14px}.vZSZnG_stepsList{flex-direction:column;gap:1px;margin:0;padding:0;list-style:none;display:flex}.vZSZnG_stepRow{align-items:flex-start;gap:7px;padding:3px 2px;display:flex}.vZSZnG_stepRow>svg{flex:none;margin-top:2px}.vZSZnG_stepDone{color:var(--dsw-alias-label-dimmed)}.vZSZnG_stepRun{color:var(--dsw-alias-label-primary-bluish,#3a6ef5)}.vZSZnG_stepPending{background:var(--dsw-alias-border-l3,#7f7f7f80);border-radius:50%;flex:none;width:8px;height:8px;margin:5px 3px}.vZSZnG_stepContent{color:var(--dsw-alias-label-secondary);word-break:break-word;font-size:12px;line-height:20px}.vZSZnG_stepContentDone{color:var(--dsw-alias-label-dimmed);text-decoration:line-through}.vZSZnG_stepContentRun{color:var(--dsw-alias-label-primary)}.vZSZnG_muted{color:var(--dsw-alias-label-dimmed);font-size:11px;line-height:18px}.vZSZnG_filesList{flex-direction:column;margin:0;padding:0;list-style:none;display:flex}.vZSZnG_fileRow{cursor:pointer;text-align:left;background:0 0;border:none;border-radius:6px;align-items:center;gap:6px;width:100%;padding:3px 4px;display:flex}.vZSZnG_fileRow:hover{background:var(--dsw-alias-interactive-bg-hover,#7f7f7f14)}.vZSZnG_fileDot{background:var(--dsw-alias-border-l3,#7f7f7f80);border-radius:50%;flex:none;width:5px;height:5px}.vZSZnG_filePath{color:var(--dsw-alias-label-secondary);white-space:nowrap;text-overflow:ellipsis;text-align:left;direction:rtl;font-family:ui-monospace,SF Mono,Menlo,Consolas,monospace;font-size:11px;line-height:18px;overflow:hidden}.vZSZnG_fileLine{color:var(--dsw-alias-label-dimmed);flex:none;font-size:10px;line-height:18px}.vZSZnG_cardFooter{gap:6px;margin-top:8px;display:flex}@media (prefers-reduced-motion:reduce){.vZSZnG_panel{animation:none}.vZSZnG_trackFill,.vZSZnG_card{transition:none}}.vZSZnG_miniBar{border:1px solid var(--dsw-alias-border-l3,#7f7f7f4d);background:var(--dsw-alias-bg-layer-2,var(--dsw-alias-bg-base,#fff));max-width:min(46vw,340px);height:34px;color:var(--dsw-alias-label-primary);cursor:grab;-webkit-user-select:none;user-select:none;pointer-events:auto;border-radius:999px;align-items:center;gap:7px;padding:0 12px;font-size:12px;line-height:18px;transition:background .15s,border-color .15s;display:inline-flex;position:fixed;box-shadow:0 6px 20px #00000024,0 1px 4px #00000014}.vZSZnG_miniBar:active{cursor:grabbing}.vZSZnG_miniBar:hover{background:var(--dsw-alias-interactive-bg-hover,#7f7f7f14)}.vZSZnG_miniIcon{color:var(--dsw-alias-label-secondary);flex:none}.vZSZnG_miniText{white-space:nowrap;text-overflow:ellipsis;overflow:hidden}.vZSZnG_miniChevron{color:var(--dsw-alias-label-tertiary);flex:none}.vZSZnG_taskList{flex:auto;min-height:0}.vZSZnG_card{flex-direction:column;display:flex}.vZSZnG_boundaryFallback{border:1px solid var(--dsw-alias-border-l3,#7f7f7f4d);background:var(--dsw-alias-bg-layer-2,#fff);color:var(--dsw-alias-label-secondary);cursor:pointer;pointer-events:auto;border-radius:999px;padding:4px 10px;font-size:12px;line-height:18px;position:fixed;top:64px;right:16px}.vZSZnG_stepToggle{cursor:pointer;background:0 0;border:none;border-radius:4px;flex:none;align-items:center;margin-top:2px;padding:0;display:inline-flex}.vZSZnG_stepToggle:hover{background:var(--dsw-alias-interactive-bg-hover,#7f7f7f1f)}.vZSZnG_stepToggle:disabled{cursor:default;opacity:.5}.vZSZnG_stepEditable{cursor:text;border-radius:3px}.vZSZnG_stepEditable:hover{outline:1px dashed var(--dsw-alias-border-l3,#7f7f7f66);outline-offset:1px}.vZSZnG_stepInput{box-sizing:border-box;width:auto;min-width:0;font:inherit;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-base,transparent);border:1px solid var(--dsw-alias-border-l3,#7f7f7f73);border-radius:5px;flex:auto;padding:0 5px;font-size:12px;line-height:20px}.vZSZnG_stepDragging{opacity:.3;background:var(--dsw-alias-interactive-bg-hover,#7f7f7f14)}.vZSZnG_cardTitleEditable{cursor:text;border-radius:3px}.vZSZnG_cardTitleEditable:hover{outline:1px dashed var(--dsw-alias-border-l3,#7f7f7f66);outline-offset:1px}.vZSZnG_panel[data-theme=glass],.vZSZnG_miniBar[data-theme=glass]{background:color-mix(in srgb, var(--dsw-alias-bg-layer-2,#fff) 90%, transparent);-webkit-backdrop-filter:blur(18px)saturate(1.2);border:1px solid color-mix(in srgb, var(--dsw-alias-border-l2,#7f7f7f4d) 80%, transparent)}.vZSZnG_panel[data-theme=glass] .vZSZnG_card{background:color-mix(in srgb, var(--dsw-alias-bg-base,transparent) 84%, transparent);border-color:color-mix(in srgb, var(--dsw-alias-border-l1,#7f7f7f29) 60%, transparent)}.vZSZnG_panel[data-theme=glass] .vZSZnG_cardBody{border-top-color:color-mix(in srgb, var(--dsw-alias-border-l1,#7f7f7f24) 60%, transparent)}@media (prefers-reduced-transparency:reduce){.vZSZnG_panel[data-theme=glass],.vZSZnG_miniBar[data-theme=glass]{background:var(--dsw-alias-bg-layer-2,#fff);-webkit-backdrop-filter:none;backdrop-filter:none}.vZSZnG_panel[data-theme=glass] .vZSZnG_card{background:var(--dsw-alias-bg-base,transparent)}}@supports not ((-webkit-backdrop-filter:blur(1px)) or (backdrop-filter:blur(1px))){.vZSZnG_panel[data-theme=glass],.vZSZnG_miniBar[data-theme=glass]{background:var(--dsw-alias-bg-layer-2,#fff)}}.vZSZnG_panel[data-theme=brutal]{box-shadow:none;border-width:1px;border-radius:6px}.vZSZnG_panel[data-theme=brutal] .vZSZnG_card{border:1px solid var(--dsw-alias-border-l3,#7f7f7f6b);background:var(--dsw-alias-bg-base,transparent);box-shadow:none;border-radius:3px}.vZSZnG_panel[data-theme=brutal] .vZSZnG_cardHead{padding:7px 10px 6px}.vZSZnG_panel[data-theme=brutal] .vZSZnG_cardTitle{letter-spacing:.01em;font-weight:650}.vZSZnG_panel[data-theme=brutal] .vZSZnG_headerTitle{letter-spacing:.03em}.vZSZnG_panel[data-theme=brutal] .vZSZnG_boundBadge{border-radius:2px}.vZSZnG_panel[data-theme=brutal] .vZSZnG_track{border-radius:0;height:3px}.vZSZnG_panel[data-theme=brutal] .vZSZnG_trackFill{border-radius:0}.vZSZnG_panel[data-theme=brutal] .vZSZnG_cardBody{border-top:1px solid var(--dsw-alias-border-l2,#7f7f7f3d)}.vZSZnG_panel[data-theme=mono] .vZSZnG_headerTitle,.vZSZnG_panel[data-theme=mono] .vZSZnG_counts,.vZSZnG_panel[data-theme=mono] .vZSZnG_cardTitle,.vZSZnG_panel[data-theme=mono] .vZSZnG_progressText,.vZSZnG_panel[data-theme=mono] .vZSZnG_stepContent,.vZSZnG_panel[data-theme=mono] .vZSZnG_filePath{font-family:ui-monospace,SF Mono,Menlo,Consolas,Liberation Mono,monospace}.vZSZnG_panel[data-theme=mono] .vZSZnG_cardTitle{font-size:12px}.vZSZnG_panel[data-theme=mono] .vZSZnG_stepContent{font-size:11.5px}.vZSZnG_panel[data-theme=mono] .vZSZnG_card{border-radius:5px}.vZSZnG_panel[data-theme=mono] .vZSZnG_trackFill{border-radius:0}.vZSZnG_panel[data-theme=mono] .vZSZnG_stepPending{border-radius:1px}.vZSZnG_panel[data-theme=mono] .vZSZnG_updated{letter-spacing:.02em}.vZSZnG_headerIconBtn{color:inherit;cursor:pointer;background:0 0;border:none;border-radius:0;flex:none;justify-content:center;align-items:center;margin:0;padding:0;display:inline-flex}.vZSZnG_headerIcon{color:var(--dsw-alias-label-secondary);flex:none;display:block}.vZSZnG_headerLeft{min-width:0}.vZSZnG_headerTitle{text-overflow:ellipsis;flex:0 auto;min-width:0;overflow:hidden}.vZSZnG_headerRight{min-width:0}.vZSZnG_counts{text-overflow:ellipsis;flex:none;max-width:45%;margin-left:auto;overflow:hidden}.vZSZnG_dragGhost{pointer-events:none;z-index:9999;transition:left 50ms,top 50ms;position:fixed;transform:translate(-50%,-50%)}.vZSZnG_dragGhostCard{background:var(--dsw-alias-bg-layer-2,var(--dsw-alias-bg-base,#fff));border:1px solid var(--dsw-alias-border-l3,#7f7f7f59);color:var(--dsw-alias-label-primary);white-space:nowrap;border-radius:8px;align-items:center;gap:8px;max-width:280px;padding:8px 12px;font-size:12px;line-height:20px;display:inline-flex;transform:scale(.9);box-shadow:0 8px 24px #0000002e,0 2px 8px #0000001a}.vZSZnG_dragGhostContent{text-overflow:ellipsis;word-break:break-word;flex:1;min-width:0;overflow:hidden}.vZSZnG_dragGhostClose{width:20px;height:20px;color:var(--dsw-alias-label-tertiary);cursor:pointer;opacity:.6;background:0 0;border:none;border-radius:4px;flex:none;justify-content:center;align-items:center;margin-left:8px;font-size:16px;line-height:1;transition:opacity .15s,background .15s,color .15s;display:inline-flex}.vZSZnG_dragGhostClose:hover{opacity:1;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-interactive-bg-hover,#7f7f7f1f)}.vZSZnG_dropTargetBefore{position:relative}.vZSZnG_dropTargetBefore:before{content:\"\";background:var(--dsw-alias-button-primary-fill,#3a6ef5);height:2px;box-shadow:0 0 4px var(--dsw-alias-button-primary-fill,#3a6ef5);border-radius:1px;animation:.8s ease-in-out infinite vZSZnG_dropPulse;position:absolute;top:-1px;left:0;right:0}.vZSZnG_dropTargetAfter{position:relative}.vZSZnG_dropTargetAfter:after{content:\"\";background:var(--dsw-alias-button-primary-fill,#3a6ef5);height:2px;box-shadow:0 0 4px var(--dsw-alias-button-primary-fill,#3a6ef5);border-radius:1px;animation:.8s ease-in-out infinite vZSZnG_dropPulse;position:absolute;bottom:-1px;left:0;right:0}@keyframes vZSZnG_dropPulse{0%,to{opacity:.6}50%{opacity:1}}";
268
491
  const tagId$1 = "@yolk_vat-y/dsh-project-memory/TaskPanel.module.css";
269
492
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]") === null) {
270
493
  const tag = document.createElement("style");
@@ -274,86 +497,78 @@ window.__ModuleLoader__.load({
274
497
  document.head.appendChild(tag);
275
498
  }
276
499
  var TaskPanel_module_css_default = {
277
- "stepEditable": "vZSZnG_stepEditable",
500
+ "headerLeft": "vZSZnG_headerLeft",
501
+ "counts": "vZSZnG_counts",
502
+ "card": "vZSZnG_card",
503
+ "filePath": "vZSZnG_filePath",
504
+ "headerIcon": "vZSZnG_headerIcon",
505
+ "fileDot": "vZSZnG_fileDot",
506
+ "dropPulse": "vZSZnG_dropPulse",
278
507
  "stepContent": "vZSZnG_stepContent",
279
- "cardBound": "vZSZnG_cardBound",
280
- "updated": "vZSZnG_updated",
281
- "headerRight": "vZSZnG_headerRight",
508
+ "stepInput": "vZSZnG_stepInput",
509
+ "track": "vZSZnG_track",
282
510
  "muted": "vZSZnG_muted",
283
- "fileLine": "vZSZnG_fileLine",
284
- "spinning": "vZSZnG_spinning",
511
+ "cardMeta": "vZSZnG_cardMeta",
512
+ "miniChevron": "vZSZnG_miniChevron",
513
+ "stepRun": "vZSZnG_stepRun",
514
+ "stepEditable": "vZSZnG_stepEditable",
515
+ "dragGhostContent": "vZSZnG_dragGhostContent",
516
+ "chevron": "vZSZnG_chevron",
517
+ "dragGhostCard": "vZSZnG_dragGhostCard",
518
+ "cardBound": "vZSZnG_cardBound",
519
+ "boundaryFallback": "vZSZnG_boundaryFallback",
285
520
  "headerTitle": "vZSZnG_headerTitle",
286
- "miniText": "vZSZnG_miniText",
287
- "boundBadge": "vZSZnG_boundBadge",
288
- "card": "vZSZnG_card",
289
- "spin": "vZSZnG_spin",
290
- "syncHint": "vZSZnG_syncHint",
291
- "emptyDesc": "vZSZnG_emptyDesc",
292
- "stepsList": "vZSZnG_stepsList",
293
- "stepRow": "vZSZnG_stepRow",
294
- "trackFill": "vZSZnG_trackFill",
295
521
  "stepContentDone": "vZSZnG_stepContentDone",
296
- "filesList": "vZSZnG_filesList",
297
- "miniBar": "vZSZnG_miniBar",
298
- "boundaryFallback": "vZSZnG_boundaryFallback",
299
- "handleGrip": "vZSZnG_handleGrip",
300
- "sectionCount": "vZSZnG_sectionCount",
522
+ "fileLine": "vZSZnG_fileLine",
523
+ "sectionLabel": "vZSZnG_sectionLabel",
524
+ "emptyTitle": "vZSZnG_emptyTitle",
525
+ "stepDragging": "vZSZnG_stepDragging",
526
+ "trackFill": "vZSZnG_trackFill",
301
527
  "miniIcon": "vZSZnG_miniIcon",
302
- "panelIn": "vZSZnG_panelIn",
303
- "stepInput": "vZSZnG_stepInput",
304
- "cardTitleRow": "vZSZnG_cardTitleRow",
305
- "filePath": "vZSZnG_filePath",
306
- "panel": "vZSZnG_panel",
307
- "dragHandle": "vZSZnG_dragHandle",
308
- "cardHead": "vZSZnG_cardHead",
309
- "pill": "vZSZnG_pill",
310
- "chevron": "vZSZnG_chevron",
528
+ "progressText": "vZSZnG_progressText",
529
+ "cardTitle": "vZSZnG_cardTitle",
530
+ "cardFooter": "vZSZnG_cardFooter",
531
+ "headerIconBtn": "vZSZnG_headerIconBtn",
311
532
  "header": "vZSZnG_header",
533
+ "miniBar": "vZSZnG_miniBar",
534
+ "dropTargetAfter": "vZSZnG_dropTargetAfter",
535
+ "filesList": "vZSZnG_filesList",
536
+ "boundBadge": "vZSZnG_boundBadge",
537
+ "cardTitleEditable": "vZSZnG_cardTitleEditable",
312
538
  "fileRow": "vZSZnG_fileRow",
313
- "emptyState": "vZSZnG_emptyState",
314
- "headerIconBtn": "vZSZnG_headerIconBtn",
315
- "headerIcon": "vZSZnG_headerIcon",
539
+ "dragGhost": "vZSZnG_dragGhost",
540
+ "cardTitleRow": "vZSZnG_cardTitleRow",
541
+ "cardHead": "vZSZnG_cardHead",
542
+ "panel": "vZSZnG_panel",
543
+ "stepRow": "vZSZnG_stepRow",
544
+ "headerRight": "vZSZnG_headerRight",
545
+ "handleGrip": "vZSZnG_handleGrip",
546
+ "panelIn": "vZSZnG_panelIn",
316
547
  "stepToggle": "vZSZnG_stepToggle",
317
- "taskList": "vZSZnG_taskList",
318
- "cardTitleEditable": "vZSZnG_cardTitleEditable",
319
548
  "notice": "vZSZnG_notice",
320
- "softBadge": "vZSZnG_softBadge",
549
+ "emptyState": "vZSZnG_emptyState",
550
+ "miniText": "vZSZnG_miniText",
551
+ "updated": "vZSZnG_updated",
552
+ "stepsList": "vZSZnG_stepsList",
553
+ "dragHandle": "vZSZnG_dragHandle",
554
+ "taskList": "vZSZnG_taskList",
555
+ "sectionCount": "vZSZnG_sectionCount",
321
556
  "stepDone": "vZSZnG_stepDone",
322
- "stepPending": "vZSZnG_stepPending",
323
- "track": "vZSZnG_track",
324
- "cardTitle": "vZSZnG_cardTitle",
325
- "cardMeta": "vZSZnG_cardMeta",
326
- "fileDot": "vZSZnG_fileDot",
327
- "counts": "vZSZnG_counts",
557
+ "syncHint": "vZSZnG_syncHint",
558
+ "section": "vZSZnG_section",
328
559
  "stepContentRun": "vZSZnG_stepContentRun",
329
- "emptyTitle": "vZSZnG_emptyTitle",
330
- "progressText": "vZSZnG_progressText",
331
- "cardBody": "vZSZnG_cardBody",
332
- "stepRun": "vZSZnG_stepRun",
333
- "cardFooter": "vZSZnG_cardFooter",
334
- "sectionLabel": "vZSZnG_sectionLabel",
335
- "miniChevron": "vZSZnG_miniChevron",
336
- "headerLeft": "vZSZnG_headerLeft",
337
- "section": "vZSZnG_section"
560
+ "dragGhostClose": "vZSZnG_dragGhostClose",
561
+ "emptyDesc": "vZSZnG_emptyDesc",
562
+ "dropTargetBefore": "vZSZnG_dropTargetBefore",
563
+ "stepPending": "vZSZnG_stepPending",
564
+ "cardBody": "vZSZnG_cardBody"
338
565
  };
339
566
  //#endregion
340
- //#region src/client/TaskPanel.tsx
567
+ //#region src/client/TaskComponents.tsx
341
568
  /**
342
- * Task Panel — dsh web `shell.overlay` 浮动任务面板。
343
- *
344
- * 升级原 `/tasks` 文本输出为可交互卡片:
345
- * - 默认收成右侧浮标(不影响用户操作/不挡对话);
346
- * - 展开时通过宿主 `remote.commands.execute(sessionId, '/tasks')` 拉取最新快照
347
- * (命令只读,走 /tasks 同一数据源 = tasks.json,因此与模型维护的 todo 双向一致);
348
- * - 卡片内「切换 / 归档」执行 `/task switch|archive <id>`,服务端返回新快照 JSON,
349
- * 面板即时刷新,无需再跑一次 /tasks;
350
- * - 可拖拽(仅头部把手),位置持久化 localStorage。
351
- *
352
- * 依赖仅 react + @deepseek-ai/dsh-client-ui-primitives(宿主 seed 模块)。
569
+ * Task Panel Presentational 组件 — MiniBar(折叠迷你条)与 TaskCard(任务卡)。
570
+ * 纯渲染 + 卡片局部行内编辑/拖拽状态,动作经 props 回调交给容器(TaskPanel.tsx)。
353
571
  */
354
- function getT() {
355
- return createTranslate(typeof navigator !== "undefined" && navigator.language.startsWith("zh") ? zh : en);
356
- }
357
572
  function timeAgo(iso) {
358
573
  if (!iso) return "";
359
574
  const diff = Date.now() - new Date(iso).getTime();
@@ -369,62 +584,6 @@ window.__ModuleLoader__.load({
369
584
  if (status === "in_progress") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconPlayOutline16, { className: TaskPanel_module_css_default.stepRun });
370
585
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: TaskPanel_module_css_default.stepPending });
371
586
  }
372
- function useSessionId(ctx) {
373
- const [, force] = (0, react.useState)(0);
374
- (0, react.useEffect)(() => {
375
- const list = ctx?.sessions?.list;
376
- if (!list || typeof list.subscribe !== "function") return;
377
- return list.subscribe(() => force((n) => n + 1));
378
- }, [ctx]);
379
- const snap = ctx?.sessions?.list?.getSnapshot?.();
380
- if (!snap) return null;
381
- if (snap.current) return snap.current;
382
- return (Array.isArray(snap.items) ? snap.items.find((s) => !s.blank) ?? snap.items[0] : void 0)?.sessionId ?? null;
383
- }
384
- /** 行内编辑框:自动按内容增高(最多 160px),Enter 提交、Shift+Enter 换行、Esc 取消、失焦提交。 */
385
- function AutoEdit({ value, onCommit, onCancel, singleLine }) {
386
- const [text, setText] = (0, react.useState)(value);
387
- const ref = (0, react.useRef)(null);
388
- const settled = (0, react.useRef)(false);
389
- const once = (fn) => {
390
- if (settled.current) return;
391
- settled.current = true;
392
- fn();
393
- };
394
- const adjust = () => {
395
- const el = ref.current;
396
- if (!el) return;
397
- el.style.height = "auto";
398
- el.style.height = `${Math.min(el.scrollHeight, 160)}px`;
399
- };
400
- (0, react.useEffect)(() => {
401
- adjust();
402
- const el = ref.current;
403
- el?.focus();
404
- el?.select();
405
- }, []);
406
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
407
- ref,
408
- className: TaskPanel_module_css_default.stepInput,
409
- rows: 1,
410
- value: text,
411
- onChange: (e) => {
412
- setText(e.target.value);
413
- adjust();
414
- },
415
- onBlur: () => once(() => onCommit(text)),
416
- onKeyDown: (e) => {
417
- if (e.key === "Escape") {
418
- e.preventDefault();
419
- once(() => onCancel());
420
- } else if (e.key === "Enter" && (singleLine || !e.shiftKey)) {
421
- e.preventDefault();
422
- once(() => onCommit(text));
423
- }
424
- }
425
- });
426
- }
427
- /** 折叠迷你条:可拖拽(按住拖动,轻点展开)。 */
428
587
  function MiniBar({ label, hint, position, theme, onMove, open }) {
429
588
  const barRef = (0, react.useRef)(null);
430
589
  const down = (0, react.useRef)(null);
@@ -484,18 +643,284 @@ window.__ModuleLoader__.load({
484
643
  ]
485
644
  });
486
645
  }
646
+ function TaskCard({ task, isBound, expanded, onToggleExpand, onSwitch, onUnbind, onArchive, onRename, onEditStep, onCycleStatus, onReorderSteps, showHints, syncing, t }) {
647
+ const steps = task.steps || [];
648
+ const { dragState, dropTargetIndex, startDrag, cancelDrag } = useTaskDrag(task.id, steps, isBound, onReorderSteps);
649
+ const [stepEdit, setStepEdit] = (0, react.useState)(null);
650
+ const [titleEdit, setTitleEdit] = (0, react.useState)(null);
651
+ const done = steps.filter((s) => s.status === "completed").length;
652
+ const pct = steps.length > 0 ? Math.round(done / steps.length * 100) : 0;
653
+ const commitStepEdit = (index) => {
654
+ if (!stepEdit || stepEdit.index !== index) return;
655
+ const value = stepEdit.value;
656
+ setStepEdit(null);
657
+ onEditStep(index, value);
658
+ };
659
+ const commitTitleEdit = () => {
660
+ if (titleEdit === null) return;
661
+ const value = titleEdit.value;
662
+ setTitleEdit(null);
663
+ onRename(value);
664
+ };
665
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("article", {
666
+ "data-task-id": task.id,
667
+ className: `${TaskPanel_module_css_default.card}${isBound ? ` ${TaskPanel_module_css_default.cardBound}` : ""}`,
668
+ children: [
669
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
670
+ className: TaskPanel_module_css_default.cardHead,
671
+ onClick: () => {
672
+ if (titleEdit !== null) return;
673
+ onToggleExpand();
674
+ },
675
+ "aria-expanded": expanded,
676
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
677
+ className: TaskPanel_module_css_default.cardTitleRow,
678
+ children: [isBound && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
679
+ className: TaskPanel_module_css_default.boundBadge,
680
+ children: t("task.current")
681
+ }), isBound && titleEdit !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
682
+ className: TaskPanel_module_css_default.stepInput,
683
+ value: titleEdit.value,
684
+ autoFocus: true,
685
+ onChange: (e) => setTitleEdit({ value: e.target.value }),
686
+ onBlur: commitTitleEdit,
687
+ onKeyDown: (e) => {
688
+ if (e.key === "Enter") commitTitleEdit();
689
+ else if (e.key === "Escape") setTitleEdit(null);
690
+ }
691
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
692
+ className: `${TaskPanel_module_css_default.cardTitle}${isBound ? ` ${TaskPanel_module_css_default.cardTitleEditable}` : ""}`,
693
+ title: isBound ? t("task.title-edit") : void 0,
694
+ onDoubleClick: (e) => {
695
+ e.preventDefault();
696
+ e.stopPropagation();
697
+ if (isBound) setTitleEdit({ value: String(task.title ?? "") });
698
+ },
699
+ children: task.title
700
+ })]
701
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
702
+ className: TaskPanel_module_css_default.cardMeta,
703
+ children: [
704
+ steps.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
705
+ className: TaskPanel_module_css_default.progressText,
706
+ children: [
707
+ done,
708
+ "/",
709
+ steps.length
710
+ ]
711
+ }),
712
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
713
+ className: TaskPanel_module_css_default.updated,
714
+ children: timeAgo(task.updatedAt || task.lastActiveAt)
715
+ }),
716
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
717
+ className: TaskPanel_module_css_default.chevron,
718
+ children: expanded ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronUpOutline14, {}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14, {})
719
+ })
720
+ ]
721
+ })]
722
+ }),
723
+ steps.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
724
+ className: TaskPanel_module_css_default.track,
725
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
726
+ className: TaskPanel_module_css_default.trackFill,
727
+ style: { width: `${pct}%` }
728
+ })
729
+ }),
730
+ expanded && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
731
+ className: TaskPanel_module_css_default.cardBody,
732
+ children: [
733
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
734
+ className: TaskPanel_module_css_default.section,
735
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
736
+ className: TaskPanel_module_css_default.sectionLabel,
737
+ children: [t("task.steps"), steps.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
738
+ className: TaskPanel_module_css_default.sectionCount,
739
+ children: steps.length
740
+ })]
741
+ }), steps.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("ol", {
742
+ className: TaskPanel_module_css_default.stepsList,
743
+ children: steps.map((step, i) => {
744
+ const stepIsEditing = stepEdit !== null && stepEdit.index === i;
745
+ const stepIsDragging = dragState?.dragging && dragState.fromIndex === i;
746
+ const isDropTarget = dropTargetIndex === i;
747
+ const isDropTargetAfter = dropTargetIndex === i + 1;
748
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
749
+ "data-step-row": true,
750
+ className: `${TaskPanel_module_css_default.stepRow}${stepIsDragging ? ` ${TaskPanel_module_css_default.stepDragging}` : ""}${isDropTarget ? ` ${TaskPanel_module_css_default.dropTargetBefore}` : ""}${isDropTargetAfter ? ` ${TaskPanel_module_css_default.dropTargetAfter}` : ""}`,
751
+ onMouseDown: (e) => {
752
+ if (e.button !== 0) return;
753
+ if (!isBound || syncing) return;
754
+ if (e.target.closest("button, input, textarea, a")) return;
755
+ startDrag(i, step.content, e);
756
+ },
757
+ children: [isBound ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
758
+ type: "button",
759
+ className: TaskPanel_module_css_default.stepToggle,
760
+ disabled: syncing,
761
+ onClick: (e) => {
762
+ e.stopPropagation();
763
+ onCycleStatus(i);
764
+ },
765
+ title: `${t("step.completed")}/${t("step.in-progress")}/${t("step.pending")}`,
766
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StepIcon, { status: step.status })
767
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StepIcon, { status: step.status }), stepIsEditing ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
768
+ className: TaskPanel_module_css_default.stepInput,
769
+ value: stepEdit.value,
770
+ autoFocus: true,
771
+ onChange: (e) => setStepEdit({
772
+ index: i,
773
+ value: e.target.value
774
+ }),
775
+ onBlur: () => commitStepEdit(i),
776
+ onKeyDown: (e) => {
777
+ if (e.key === "Enter") commitStepEdit(i);
778
+ else if (e.key === "Escape") setStepEdit(null);
779
+ }
780
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
781
+ className: `${TaskPanel_module_css_default.stepContent}${step.status === "completed" ? ` ${TaskPanel_module_css_default.stepContentDone}` : ""}${step.status === "in_progress" ? ` ${TaskPanel_module_css_default.stepContentRun}` : ""}${isBound ? ` ${TaskPanel_module_css_default.stepEditable}` : ""}`,
782
+ title: isBound && showHints ? t("step.edit-hint") : void 0,
783
+ onDoubleClick: (e) => {
784
+ e.stopPropagation();
785
+ if (isBound) setStepEdit({
786
+ index: i,
787
+ value: String(step.content ?? "")
788
+ });
789
+ },
790
+ children: step.content
791
+ })]
792
+ }, i);
793
+ })
794
+ }), dragState && dragState.dragging && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
795
+ className: TaskPanel_module_css_default.dragGhost,
796
+ style: {
797
+ left: dragState.clientX + 12,
798
+ top: dragState.clientY - 20
799
+ },
800
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
801
+ className: TaskPanel_module_css_default.dragGhostCard,
802
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
803
+ className: TaskPanel_module_css_default.dragGhostContent,
804
+ children: dragState.stepContent
805
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
806
+ type: "button",
807
+ className: TaskPanel_module_css_default.dragGhostClose,
808
+ style: { pointerEvents: "auto" },
809
+ onClick: (e) => {
810
+ e.stopPropagation();
811
+ cancelDrag();
812
+ },
813
+ title: showHints ? t("drag.cancel") : void 0,
814
+ "aria-label": t("drag.cancel"),
815
+ children: "×"
816
+ })]
817
+ })
818
+ })] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
819
+ className: TaskPanel_module_css_default.muted,
820
+ children: t("panel.empty-desc")
821
+ })]
822
+ }),
823
+ (task.files?.length ?? 0) > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
824
+ className: TaskPanel_module_css_default.section,
825
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
826
+ className: TaskPanel_module_css_default.sectionLabel,
827
+ children: [t("task.files"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
828
+ className: TaskPanel_module_css_default.sectionCount,
829
+ children: task.files.length
830
+ })]
831
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("ul", {
832
+ className: TaskPanel_module_css_default.filesList,
833
+ children: [task.files.slice(0, 12).map((file, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
834
+ className: TaskPanel_module_css_default.fileRow,
835
+ onClick: () => navigator.clipboard?.writeText(file.path),
836
+ title: `${t("file.copy")}: ${file.path}${file.line ? `:${file.line}` : ""}`,
837
+ children: [
838
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: TaskPanel_module_css_default.fileDot }),
839
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
840
+ className: TaskPanel_module_css_default.filePath,
841
+ children: file.path
842
+ }),
843
+ file.line && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
844
+ className: TaskPanel_module_css_default.fileLine,
845
+ children: [":", file.line]
846
+ })
847
+ ]
848
+ }) }, i)), (task.files?.length ?? 0) > 12 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
849
+ className: TaskPanel_module_css_default.muted,
850
+ children: [
851
+ "… 共 ",
852
+ task.files.length,
853
+ " 个"
854
+ ]
855
+ })]
856
+ })]
857
+ }),
858
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
859
+ className: TaskPanel_module_css_default.cardFooter,
860
+ children: [
861
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
862
+ variant: "outline",
863
+ size: "sm",
864
+ disabled: isBound || syncing,
865
+ onClick: onSwitch,
866
+ children: t("task.switch")
867
+ }),
868
+ isBound && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
869
+ variant: "outline",
870
+ size: "sm",
871
+ disabled: syncing,
872
+ onClick: onUnbind,
873
+ title: t("task.unbind"),
874
+ children: t("task.unbind")
875
+ }),
876
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
877
+ variant: "outline",
878
+ size: "sm",
879
+ disabled: syncing,
880
+ onClick: onArchive,
881
+ children: t("task.archive")
882
+ })
883
+ ]
884
+ })
885
+ ]
886
+ })
887
+ ]
888
+ });
889
+ }
890
+ //#endregion
891
+ //#region src/client/TaskPanel.tsx
487
892
  /**
488
- * 错误边界:任务面板渲染一旦抛错,不再让 shell.overlay 把整条条目退休
489
- * (宿主对崩溃条目会永久移除直到重载页面)。捕获后显示一个纯文本兜底,
490
- * 点击重新渲染。
893
+ * Task Panel — dsh web `shell.overlay` 浮动任务面板。
894
+ * Container 组件:负责数据获取、命令桥接、状态协调
895
+ * Presentational 组件在 TaskComponents.tsx
491
896
  */
897
+ const STATUS_CYCLE = [
898
+ "pending",
899
+ "in_progress",
900
+ "completed"
901
+ ];
902
+ function getT() {
903
+ return createTranslate(typeof navigator !== "undefined" && navigator.language.startsWith("zh") ? zh : en);
904
+ }
905
+ function useSessionId(ctx) {
906
+ const [, force] = (0, react.useState)(0);
907
+ (0, react.useEffect)(() => {
908
+ const list = ctx?.sessions?.list;
909
+ if (!list || typeof list.subscribe !== "function") return;
910
+ return list.subscribe(() => force((n) => n + 1));
911
+ }, [ctx]);
912
+ const snap = ctx?.sessions?.list?.getSnapshot?.();
913
+ if (!snap) return null;
914
+ if (snap.current) return snap.current;
915
+ return (Array.isArray(snap.items) ? snap.items.find((s) => !s.blank) ?? snap.items[0] : void 0)?.sessionId ?? null;
916
+ }
492
917
  var PanelErrorBoundary = class extends react.Component {
493
918
  state = { failed: false };
494
919
  static getDerivedStateFromError() {
495
920
  return { failed: true };
496
921
  }
497
922
  componentDidCatch(error) {
498
- console.warn("[dsh-project-memory] task panel render crashed (contained by boundary):", error);
923
+ console.warn("[dsh-project-memory] task panel render crashed:", error);
499
924
  }
500
925
  render() {
501
926
  if (!this.state.failed) return this.props.children;
@@ -512,22 +937,21 @@ window.__ModuleLoader__.load({
512
937
  }
513
938
  function TaskPanelView({ ctx }) {
514
939
  const t = getT();
515
- const state = useTaskStore();
940
+ const data = useTaskData();
941
+ const ui = useTaskUI();
516
942
  const sessionId = useSessionId(ctx);
517
943
  const [syncing, setSyncing] = (0, react.useState)(false);
518
944
  const [syncedAt, setSyncedAt] = (0, react.useState)(0);
519
- const [copiedPath, setCopiedPath] = (0, react.useState)(null);
520
945
  const [syncError, setSyncError] = (0, react.useState)(null);
521
- const panelRef = (0, react.useRef)(null);
522
- const drag = (0, react.useRef)(null);
523
- const copyTimer = (0, react.useRef)(void 0);
524
- const actions = taskStore.actions;
525
- const activeTasks = state.tasks.filter((task) => !task.archived);
526
- const boundTask = state.boundTaskId ? state.tasks.find((task) => task.id === state.boundTaskId) ?? null : null;
946
+ const [showHints, setShowHints] = (0, react.useState)(true);
947
+ const dataActions = useTaskDataActions();
948
+ const uiActions = useTaskUIActions();
949
+ const activeTasks = data.tasks.filter((task) => !task.archived);
950
+ const boundTask = data.boundTaskId ? data.tasks.find((task) => task.id === data.boundTaskId) ?? null : null;
527
951
  const applyPayload = (text) => {
528
952
  const parsed = parseTaskPayloadText(text);
529
953
  if (!parsed) return false;
530
- actions.setTasks({
954
+ dataActions.setTasks({
531
955
  tasks: parsed.tasks,
532
956
  boundTaskId: parsed.boundId,
533
957
  archivedCount: parsed.archived
@@ -562,7 +986,10 @@ window.__ModuleLoader__.load({
562
986
  if (syncing) return;
563
987
  setSyncing(true);
564
988
  try {
989
+ const prevBoundId = data.boundTaskId;
565
990
  await runLine("/tasks");
991
+ const newBoundId = useTaskData.getSnapshot().boundTaskId;
992
+ if (newBoundId && newBoundId !== prevBoundId) await runLine(`/task switch ${newBoundId}`);
566
993
  } finally {
567
994
  setSyncing(false);
568
995
  }
@@ -576,12 +1003,6 @@ window.__ModuleLoader__.load({
576
1003
  setSyncing(false);
577
1004
  }
578
1005
  };
579
- const [editing, setEditing] = (0, react.useState)(null);
580
- const STATUS_CYCLE = [
581
- "pending",
582
- "in_progress",
583
- "completed"
584
- ];
585
1006
  const pushSteps = (taskId, steps) => {
586
1007
  runLine(`/task todos ${JSON.stringify(steps.map((s) => ({
587
1008
  content: s.content,
@@ -589,7 +1010,7 @@ window.__ModuleLoader__.load({
589
1010
  })))}`);
590
1011
  };
591
1012
  const cycleStatus = (taskId, index) => {
592
- const task = state.tasks.find((tt) => tt.id === taskId);
1013
+ const task = data.tasks.find((tt) => tt.id === taskId);
593
1014
  if (!task) return;
594
1015
  const next = (task.steps || []).map((s, i) => {
595
1016
  if (i !== index) return s;
@@ -602,37 +1023,36 @@ window.__ModuleLoader__.load({
602
1023
  pushSteps(taskId, next);
603
1024
  };
604
1025
  const commitStepText = (taskId, index, value) => {
605
- const task = state.tasks.find((tt) => tt.id === taskId);
1026
+ const task = data.tasks.find((tt) => tt.id === taskId);
606
1027
  const trimmed = value.trim();
607
- if (!task) {
608
- setEditing(null);
609
- return;
610
- }
611
- const same = (task.steps || [])[index]?.content === trimmed;
612
- setEditing(null);
613
- if (same || !trimmed) return;
1028
+ if (!task) return;
1029
+ if ((task.steps || [])[index]?.content === trimmed || !trimmed) return;
614
1030
  const next = (task.steps || []).map((s, i) => i === index ? {
615
1031
  ...s,
616
1032
  content: trimmed
617
1033
  } : s);
618
1034
  pushSteps(taskId, next);
619
1035
  };
620
- const [editingTitle, setEditingTitle] = (0, react.useState)(null);
1036
+ const reorderSteps = (taskId, fromIndex, toIndex) => {
1037
+ if (fromIndex === toIndex) return;
1038
+ const task = data.tasks.find((tt) => tt.id === taskId);
1039
+ if (!task) return;
1040
+ const steps = [...task.steps || []];
1041
+ const [moved] = steps.splice(fromIndex, 1);
1042
+ const targetIndex = toIndex > fromIndex ? toIndex - 1 : toIndex;
1043
+ steps.splice(targetIndex, 0, moved);
1044
+ pushSteps(taskId, steps);
1045
+ };
621
1046
  const commitTitle = (taskId, value) => {
622
- const task = state.tasks.find((tt) => tt.id === taskId);
1047
+ const task = data.tasks.find((tt) => tt.id === taskId);
623
1048
  const trimmed = value.trim();
624
- if (!task) {
625
- setEditingTitle(null);
626
- return;
627
- }
628
- const same = task.title === trimmed;
629
- setEditingTitle(null);
630
- if (same || !trimmed) return;
1049
+ if (!task) return;
1050
+ if (task.title === trimmed || !trimmed) return;
631
1051
  runLine(`/task rename ${taskId} ${JSON.stringify(trimmed)}`);
632
1052
  };
633
1053
  const expand = () => {
634
- actions.open();
635
- if (Date.now() - Math.max(state.lastUpdate, syncedAt) > 3e4) refresh();
1054
+ uiActions.open();
1055
+ if (Date.now() - Math.max(data.lastUpdate, syncedAt) > 3e4) refresh();
636
1056
  };
637
1057
  const THEMES = [
638
1058
  "native",
@@ -640,12 +1060,14 @@ window.__ModuleLoader__.load({
640
1060
  "brutal",
641
1061
  "mono"
642
1062
  ];
643
- const style = THEMES.includes(state.theme) ? state.theme : "native";
1063
+ const style = THEMES.includes(ui.theme) ? ui.theme : "native";
644
1064
  const styleLabel = t(`style.${style}`);
645
1065
  const cycleTheme = () => {
646
1066
  const i = THEMES.indexOf(style);
647
- actions.setTheme(THEMES[(i + 1) % THEMES.length]);
1067
+ uiActions.setTheme(THEMES[(i + 1) % THEMES.length]);
648
1068
  };
1069
+ const panelRef = (0, react.useRef)(null);
1070
+ const drag = (0, react.useRef)(null);
649
1071
  const handleDragStart = (e) => {
650
1072
  if (e.target !== e.currentTarget) return;
651
1073
  const rect = panelRef.current?.getBoundingClientRect();
@@ -660,7 +1082,7 @@ window.__ModuleLoader__.load({
660
1082
  panelRef.current?.offsetHeight;
661
1083
  const x = Math.max(8, Math.min(window.innerWidth - width - 8, ev.clientX - drag.current.dx));
662
1084
  const y = Math.max(8, Math.min(window.innerHeight - 64, ev.clientY - drag.current.dy));
663
- actions.setPanelPosition({
1085
+ uiActions.setPanelPosition({
664
1086
  x,
665
1087
  y
666
1088
  });
@@ -673,31 +1095,36 @@ window.__ModuleLoader__.load({
673
1095
  window.addEventListener("mousemove", onMove);
674
1096
  window.addEventListener("mouseup", onUp);
675
1097
  };
676
- const copyPath = (path) => {
677
- if (copiedPath === path) return;
678
- navigator.clipboard?.writeText(path).catch(() => void 0);
679
- setCopiedPath(path);
680
- window.clearTimeout(copyTimer.current);
681
- copyTimer.current = window.setTimeout(() => setCopiedPath(null), 1200);
682
- };
683
- (0, react.useEffect)(() => () => window.clearTimeout(copyTimer.current), []);
684
1098
  (0, react.useEffect)(() => {
685
1099
  if (!sessionId) return;
686
- const snap = taskStore.getSnapshot();
687
- if (snap.lastUpdate === 0 && !snap.closed) {
688
- const timer = window.setTimeout(() => void refresh(), 300);
689
- return () => window.clearTimeout(timer);
690
- }
1100
+ if (!ui.closed) refresh();
691
1101
  }, [sessionId]);
692
- if (state.closed) return null;
693
- if (state.minimized) {
1102
+ (0, react.useEffect)(() => {
1103
+ const ctxWithEvents = ctx;
1104
+ const handler = () => {
1105
+ uiActions.open();
1106
+ if (Date.now() - Math.max(data.lastUpdate, syncedAt) > 3e4) refresh();
1107
+ };
1108
+ ctxWithEvents?.events?.on?.("dsh:task-panel:show", handler);
1109
+ return () => {
1110
+ ctxWithEvents?.events?.off?.("dsh:task-panel:show", handler);
1111
+ };
1112
+ }, [
1113
+ ctx,
1114
+ uiActions,
1115
+ data.lastUpdate,
1116
+ syncedAt,
1117
+ refresh
1118
+ ]);
1119
+ if (ui.closed) return null;
1120
+ if (ui.minimized) {
694
1121
  const label = boundTask ? `${t("minibar.current")}: ${boundTask.title}` : activeTasks.length > 0 ? `${t("minibar.current")}: ${activeTasks.length} ${t("minibar.tasks")}` : t("minibar.no-task");
695
1122
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MiniBar, {
696
1123
  label,
697
1124
  hint: t("minibar.click-expand"),
698
- position: state.panelPosition,
1125
+ position: ui.panelPosition,
699
1126
  theme: style,
700
- onMove: (pos) => actions.setPanelPosition(pos),
1127
+ onMove: uiActions.setPanelPosition,
701
1128
  open: expand
702
1129
  });
703
1130
  }
@@ -709,8 +1136,8 @@ window.__ModuleLoader__.load({
709
1136
  className: TaskPanel_module_css_default.panel,
710
1137
  ref: panelRef,
711
1138
  style: {
712
- left: state.panelPosition.x,
713
- top: state.panelPosition.y
1139
+ left: ui.panelPosition.x,
1140
+ top: ui.panelPosition.y
714
1141
  },
715
1142
  "data-theme": style === "native" ? void 0 : style,
716
1143
  children: [
@@ -724,7 +1151,7 @@ window.__ModuleLoader__.load({
724
1151
  className: TaskPanel_module_css_default.header,
725
1152
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
726
1153
  className: TaskPanel_module_css_default.headerLeft,
727
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1154
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
728
1155
  type: "button",
729
1156
  className: TaskPanel_module_css_default.headerIconBtn,
730
1157
  onClick: cycleTheme,
@@ -745,17 +1172,15 @@ window.__ModuleLoader__.load({
745
1172
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
746
1173
  variant: "ghost",
747
1174
  size: "sm",
748
- className: syncing ? TaskPanel_module_css_default.spinning : void 0,
749
- onClick: () => void refresh(),
750
- disabled: syncing,
751
- "aria-label": t("panel.refresh"),
752
- title: t("panel.refresh"),
753
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconRefreshOutline16, {})
1175
+ onClick: () => setShowHints(!showHints),
1176
+ "aria-label": showHints ? t("panel.hints-off") : t("panel.hints-on"),
1177
+ title: showHints ? t("panel.hints-off") : t("panel.hints-on"),
1178
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconQuestionOutline14, { size: 16 })
754
1179
  }),
755
1180
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
756
1181
  variant: "ghost",
757
1182
  size: "sm",
758
- onClick: () => actions.minimize(),
1183
+ onClick: () => uiActions.minimize(),
759
1184
  "aria-label": t("panel.minimize"),
760
1185
  title: t("panel.minimize"),
761
1186
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14, {})
@@ -763,7 +1188,7 @@ window.__ModuleLoader__.load({
763
1188
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
764
1189
  variant: "ghost",
765
1190
  size: "sm",
766
- onClick: () => actions.close(),
1191
+ onClick: () => uiActions.close(),
767
1192
  "aria-label": t("panel.close"),
768
1193
  title: t("panel.close"),
769
1194
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCloseOutline16, {})
@@ -802,185 +1227,24 @@ window.__ModuleLoader__.load({
802
1227
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
803
1228
  className: TaskPanel_module_css_default.taskList,
804
1229
  children: [activeTasks.map((task) => {
805
- const steps = task.steps || [];
806
- const done = steps.filter((s) => s.status === "completed").length;
807
- const expanded = state.expandedTaskIds.includes(task.id);
808
- const isBound = state.boundTaskId === task.id;
809
- const pct = steps.length > 0 ? Math.round(done / steps.length * 100) : 0;
810
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("article", {
811
- className: `${TaskPanel_module_css_default.card}${isBound ? ` ${TaskPanel_module_css_default.cardBound}` : ""}`,
812
- children: [
813
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
814
- className: TaskPanel_module_css_default.cardHead,
815
- onClick: () => {
816
- if (editingTitle?.taskId === task.id) return;
817
- actions.toggleTaskExpanded(task.id);
818
- },
819
- "aria-expanded": expanded,
820
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
821
- className: TaskPanel_module_css_default.cardTitleRow,
822
- children: [isBound && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
823
- className: TaskPanel_module_css_default.boundBadge,
824
- children: t("task.current")
825
- }), isBound && editingTitle?.taskId === task.id ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AutoEdit, {
826
- value: editingTitle.value,
827
- singleLine: true,
828
- onCommit: (v) => commitTitle(task.id, v),
829
- onCancel: () => setEditingTitle(null)
830
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
831
- className: `${TaskPanel_module_css_default.cardTitle}${isBound ? ` ${TaskPanel_module_css_default.cardTitleEditable}` : ""}`,
832
- title: isBound ? t("task.title-edit") : void 0,
833
- onDoubleClick: (e) => {
834
- e.preventDefault();
835
- e.stopPropagation();
836
- if (isBound) setEditingTitle({
837
- taskId: task.id,
838
- value: task.title
839
- });
840
- },
841
- children: task.title
842
- })]
843
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
844
- className: TaskPanel_module_css_default.cardMeta,
845
- children: [
846
- steps.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
847
- className: TaskPanel_module_css_default.progressText,
848
- children: [
849
- done,
850
- "/",
851
- steps.length
852
- ]
853
- }),
854
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
855
- className: TaskPanel_module_css_default.updated,
856
- children: timeAgo(task.updatedAt || task.lastActiveAt)
857
- }),
858
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
859
- className: TaskPanel_module_css_default.chevron,
860
- children: expanded ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronUpOutline14, {}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutline14, {})
861
- })
862
- ]
863
- })]
864
- }),
865
- steps.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
866
- className: TaskPanel_module_css_default.track,
867
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
868
- className: TaskPanel_module_css_default.trackFill,
869
- style: { width: `${pct}%` }
870
- })
871
- }),
872
- expanded && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
873
- className: TaskPanel_module_css_default.cardBody,
874
- children: [
875
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
876
- className: TaskPanel_module_css_default.section,
877
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
878
- className: TaskPanel_module_css_default.sectionLabel,
879
- children: [t("task.steps"), steps.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
880
- className: TaskPanel_module_css_default.sectionCount,
881
- children: steps.length
882
- })]
883
- }), steps.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ol", {
884
- className: TaskPanel_module_css_default.stepsList,
885
- children: steps.map((step, i) => {
886
- const isEditing = isBound && editing?.taskId === task.id && editing.index === i;
887
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
888
- className: TaskPanel_module_css_default.stepRow,
889
- children: [isBound ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
890
- type: "button",
891
- className: TaskPanel_module_css_default.stepToggle,
892
- disabled: syncing,
893
- onClick: () => cycleStatus(task.id, i),
894
- title: `${t("step.completed")}/${t("step.in-progress")}/${t("step.pending")}(点击切换)`,
895
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StepIcon, { status: step.status })
896
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StepIcon, { status: step.status }), isEditing ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AutoEdit, {
897
- value: editing.value,
898
- onCommit: (v) => commitStepText(task.id, i, v),
899
- onCancel: () => setEditing(null)
900
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
901
- className: `${TaskPanel_module_css_default.stepContent}${step.status === "completed" ? ` ${TaskPanel_module_css_default.stepContentDone}` : ""}${step.status === "in_progress" ? ` ${TaskPanel_module_css_default.stepContentRun}` : ""}${isBound ? ` ${TaskPanel_module_css_default.stepEditable}` : ""}`,
902
- title: isBound ? t("step.edit-hint") : void 0,
903
- onDoubleClick: () => {
904
- if (isBound) setEditing({
905
- taskId: task.id,
906
- index: i,
907
- value: String(step.content ?? "")
908
- });
909
- },
910
- children: step.content
911
- })]
912
- }, i);
913
- })
914
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
915
- className: TaskPanel_module_css_default.muted,
916
- children: t("panel.empty-desc")
917
- })]
918
- }),
919
- (task.files?.length ?? 0) > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
920
- className: TaskPanel_module_css_default.section,
921
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
922
- className: TaskPanel_module_css_default.sectionLabel,
923
- children: [t("task.files"), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
924
- className: TaskPanel_module_css_default.sectionCount,
925
- children: task.files.length
926
- })]
927
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("ul", {
928
- className: TaskPanel_module_css_default.filesList,
929
- children: [task.files.slice(0, 12).map((file, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
930
- className: TaskPanel_module_css_default.fileRow,
931
- onClick: () => copyPath(file.path),
932
- title: `${t(copiedPath === file.path ? "file.copied" : "file.copy")}: ${file.path}${file.line ? `:${file.line}` : ""}`,
933
- children: [
934
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: TaskPanel_module_css_default.fileDot }),
935
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
936
- className: TaskPanel_module_css_default.filePath,
937
- children: file.path
938
- }),
939
- file.line && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
940
- className: TaskPanel_module_css_default.fileLine,
941
- children: [":", file.line]
942
- })
943
- ]
944
- }) }, i)), (task.files?.length ?? 0) > 12 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
945
- className: TaskPanel_module_css_default.muted,
946
- children: [
947
- "… 共 ",
948
- task.files.length,
949
- " 个"
950
- ]
951
- })]
952
- })]
953
- }),
954
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
955
- className: TaskPanel_module_css_default.cardFooter,
956
- children: [
957
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
958
- variant: "outline",
959
- size: "sm",
960
- disabled: isBound || syncing,
961
- onClick: () => void handleAction("switch", task.id),
962
- children: t("task.switch")
963
- }),
964
- isBound && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
965
- variant: "outline",
966
- size: "sm",
967
- disabled: syncing,
968
- onClick: () => void runLine("/task unbind"),
969
- title: t("task.unbind"),
970
- children: t("task.unbind")
971
- }),
972
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
973
- variant: "outline",
974
- size: "sm",
975
- disabled: syncing,
976
- onClick: () => void handleAction("archive", task.id),
977
- children: t("task.archive")
978
- })
979
- ]
980
- })
981
- ]
982
- })
983
- ]
1230
+ task.steps;
1231
+ const expanded = ui.expandedTaskIds.includes(task.id);
1232
+ const isBound = data.boundTaskId === task.id;
1233
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TaskCard, {
1234
+ task,
1235
+ isBound,
1236
+ expanded,
1237
+ onToggleExpand: () => uiActions.toggleTaskExpanded(task.id),
1238
+ onSwitch: () => handleAction("switch", task.id),
1239
+ onUnbind: () => runLine("/task unbind"),
1240
+ onArchive: () => handleAction("archive", task.id),
1241
+ onRename: (value) => commitTitle(task.id, value),
1242
+ onEditStep: (index, value) => commitStepText(task.id, index, value),
1243
+ onCycleStatus: (index) => cycleStatus(task.id, index),
1244
+ onReorderSteps: (from, to) => reorderSteps(task.id, from, to),
1245
+ showHints,
1246
+ syncing,
1247
+ t
984
1248
  }, task.id);
985
1249
  }), syncing && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
986
1250
  className: TaskPanel_module_css_default.syncHint,
@@ -1009,15 +1273,8 @@ window.__ModuleLoader__.load({
1009
1273
  //#region src/client/TaskCommandNode.tsx
1010
1274
  /**
1011
1275
  * /tasks、/task 命令在会话中的节点渲染器(conversation.chat.commandview,按命令名 key)。
1012
- *
1013
- * 目标:不再让大段文本+JSON 出现在会话里——
1014
- * - 列表型(/tasks、无动词的 /task):只渲染一行极简“已同步 N 套”状态;
1015
- * 同时把节点文本里的 JSON 载荷解析进浮动面板(侧边卡片由它驱动),
1016
- * 面板若被收起则自动展开(等价“输入 /task 就看到卡片”)。
1017
- * - 动作型(/task switch|archive):渲染一行操作结果(切换/归档反馈)。
1018
- * - 错误/执行中:一行极简状态,不倾倒原文。
1276
+ * 列表型命令(/tasks、无动词 /task)同步数据并打开面板。
1019
1277
  */
1020
- /** node = CommandRowOwnerProps.node(CommandNode: name / args / outcome{kind,text}) */
1021
1278
  function TaskCommandNode({ node }) {
1022
1279
  const name = node?.name ?? "task";
1023
1280
  const outcome = node?.outcome ?? null;
@@ -1025,14 +1282,15 @@ window.__ModuleLoader__.load({
1025
1282
  const isList = name === "tasks" || !verb;
1026
1283
  const text = outcome?.text ?? "";
1027
1284
  const parsed = parseTaskPayloadText(text);
1285
+ const live = (0, react.useRef)(outcome === null);
1028
1286
  (0, react.useEffect)(() => {
1029
- if (!parsed) return;
1030
- taskStore.actions.setTasks({
1287
+ if (!parsed || !live.current) return;
1288
+ taskDataStore.actions.setTasks({
1031
1289
  tasks: parsed.tasks,
1032
1290
  boundTaskId: parsed.boundId,
1033
1291
  archivedCount: parsed.archived
1034
1292
  });
1035
- if (isList) taskStore.actions.open();
1293
+ if (isList) taskUIStore.actions.open();
1036
1294
  }, [text]);
1037
1295
  if (outcome === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1038
1296
  className: TaskCommandNode_module_css_default.row,