@linxin666/dsh-client-ui-task-board 0.3.17 → 0.3.18

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/README.i18n.yaml CHANGED
@@ -2,5 +2,5 @@
2
2
  # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
3
  # after editing either side, bring the other along and re-record with:
4
4
  # node scripts/verify-docs.mjs --write <dir>
5
- README.md: 979b56e69392839180e9f7ba7dbdbbc8a2e0f46c
6
- README.zh.md: aa6fff12e88a5a8df7099bed43ec155dabbf7c72
5
+ README.md: eda801f7c7e9b6811eabe5f710ef87308ab6d9ed
6
+ README.zh.md: 61cbdb083679dae12edb35f658531386094767db
package/README.md CHANGED
@@ -5,7 +5,7 @@ English | [中文](README.zh.md)
5
5
  A hot-pluggable DeepSeek Harness (DSH) Web GUI plugin with a Host-authoritative task ledger, real DSH session execution, Host cron scheduling, and optional cross-platform idle-sleep protection. It is mounted through `cordis.patch.yml` and the profile mechanism and does not modify DSH source code.
6
6
 
7
7
  - The browser is an asynchronous view; closing the page does not stop Host scheduling or execution settlement.
8
- - Every run creates a separate DSH session and applies pinned workspace, agent preset, and permission before sending the task prompt.
8
+ - Every run applies the pinned workspace, agent preset, and permission before sending the task prompt; by default each run creates its own DSH session, and a task can opt into continuing in its previous session instead (issue #1419).
9
9
  - The display may turn off while optional power protection keeps the computer from entering idle system sleep.
10
10
 
11
11
  ## Features
@@ -16,7 +16,8 @@ A hot-pluggable DeepSeek Harness (DSH) Web GUI plugin with a Host-authoritative
16
16
  - **Claim provenance wrap and source audit**: executing a continuation card (a card with a frozen snapshot) mandatorily wraps the task instruction in a source-declaration template — freeze instant, source session, and an unreviewed-content warning — composed after the handover preamble so the picking-up agent stays wary of stored prompt injection in card text. The session issuing a create/update action is stamped into the snapshot (frozenBy, re-stamped when the snapshot is replaced), and the session issuing a run/rerun lands on the execution record (initiatedBy) together with a captured copy of the freeze provenance; both are visible in the task detail. The initiator is client-asserted audit metadata, not a trust boundary.
17
17
  - **Host-authoritative ledger**: tasks, schedules, and execution records live in `$DSH_HOME/task-board/ledger-v2.json`; browser actions become confirmed Host transactions.
18
18
  - **Bounded execution history**: each task keeps the most recent 20 execution records; the oldest runs are trimmed when a new run starts, so ledger size and write cost stay bounded regardless of how often a task has run.
19
- - **Real execution**: manual and scheduled runs use the same Host runner, create a fresh session, rename it, apply the agent preset and `/permission <id>`, then queue the task prompt.
19
+ - **Real execution**: manual and scheduled runs use the same Host runner, which by default creates a fresh session, renames it, applies the agent preset and `/permission <id>`, then queues the task prompt.
20
+ - **Optional session reuse**: a task can opt into continuing in its previous execution's session (issue #1419). Reuse happens only when that session is idle and still present in the runtime roster — the Host then re-applies the pinned permission and model on it and queues the prompt, keeping the conversation title and history; otherwise the run mints a fresh session as before, so an unknown roster or a busy session never blocks a scheduled run.
20
21
  - **Fail-closed pins**: a missing workspace, missing or broken preset, or rejected permission command fails before the task prompt is sent.
21
22
  - **Host scheduler**: 5-field cron supports `*`, `*/n`, ranges, comma lists, Sunday `0/7`, and standard day-of-month/day-of-week OR semantics in the Host local time zone.
22
23
  - **Deterministic recovery**: a running execution with a recorded session is observed after restart; an interrupted start without a session id is cancelled and is not resent.
package/README.zh.md CHANGED
@@ -5,7 +5,7 @@
5
5
  一个可热插拔的 DeepSeek Harness (DSH) Web GUI 插件,提供 Host 权威任务账本、真实 DSH 会话执行、Host cron 调度和可选的跨平台空闲睡眠保护。插件只通过 `cordis.patch.yml` 与 profile 机制挂载,不修改 DSH 源码。
6
6
 
7
7
  - 浏览器只是异步视图;关闭页面不会停止 Host 调度或执行结算。
8
- - 每次运行创建独立 DSH 会话,并在发送任务 Prompt 前应用钉住的工作区、agent 预设与权限。
8
+ - 每次运行在发送任务 Prompt 前应用钉住的工作区、agent 预设与权限;默认每次新建独立 DSH 会话,任务也可选择改为在上一会话中继续(issue #1419)。
9
9
  - 可选电源保护允许显示器熄灭,同时阻止整机因空闲进入系统睡眠。
10
10
 
11
11
  ## 功能
@@ -16,7 +16,8 @@
16
16
  - **领卡来源声明包裹与来源审计**:执行续接卡片(带冻结快照的卡片)时,任务指令被来源声明模板强制包裹——冻结时间、来源会话与未经人工审查提示,组合在交接前言之后,使接手 Agent 对卡片文本中的存储型提示注入保持警惕。create/update 动作的发起方会话织入快照(frozenBy,快照被替换时重新盖章),run/rerun 的发起方会话连同冻结来源的捕获副本一起落在执行记录(initiatedBy)上,两者均可在任务详情查看。发起方为客户端断言的审计元数据,不构成信任边界。
17
17
  - **Host 权威账本**:任务、计划和执行记录存于 `$DSH_HOME/task-board/ledger-v2.json`;浏览器动作只有经 Host 确认后才成为 UI 状态。
18
18
  - **有界执行历史**:每个任务只保留最近 20 条执行记录;新运行开始时截掉最旧的记录,使账本大小与每次写入成本不随任务历史无限增长。
19
- - **真实执行**:手动运行和定时运行共用 Host runner,新建独立会话、重命名、应用 agent 预设和 `/permission <id>`,再以 queue 模式发送任务 Prompt。
19
+ - **真实执行**:手动运行和定时运行共用 Host runner,默认新建独立会话、重命名、应用 agent 预设和 `/permission <id>`,再以 queue 模式发送任务 Prompt。
20
+ - **可选会话复用**:任务可选择在上一执行会话中继续(issue #1419)。仅当该会话空闲且仍在运行时会话名册中才复用——Host 会重新应用钉住的权限与模型再以 queue 模式发送 Prompt,会话标题与历史保持不变;否则照旧新建会话,因此名册未知或会话正忙都不会阻塞定时运行。
20
21
  - **钉子失败即关闭**:工作区缺失、预设缺失或损坏、权限命令被拒绝时,任务 Prompt 不会发送。
21
22
  - **Host 调度器**:5 段 cron 支持 `*`、`*/n`、范围、逗号列表、周日 `0/7` 和标准的日期/星期 OR 语义,时间基准为 Host 本地时区。
22
23
  - **确定性恢复**:已有 session id 的 running execution 在重启后继续观察;没有 session id 的启动中断会取消且不会重发。
package/lib/client.js CHANGED
@@ -95,6 +95,7 @@ window.__ModuleLoader__.load({
95
95
  mode: normalizeTargetId(input.mode),
96
96
  permission: isTaskPermission(input.permission) ? input.permission : void 0,
97
97
  model: normalizeTargetId(input.model),
98
+ reuseSession: input.reuseSession === true ? true : void 0,
98
99
  ...input.freeze === void 0 ? {} : { freeze: freezeOf(input.freeze, now) },
99
100
  ...input.handover === void 0 ? {} : { handover: {
100
101
  ...input.handover,
@@ -533,6 +534,7 @@ window.__ModuleLoader__.load({
533
534
  bundledAt: now
534
535
  };
535
536
  if ("permission" in patch && patch.permission !== void 0 && patch.permission !== task.permission || "handover" in patch) next.permissionConfirmedAt = void 0;
537
+ if ("reuseSession" in patch) next.reuseSession = patch.reuseSession === true ? true : void 0;
536
538
  if (workspaceId !== void 0 || "workspaceId" in patch) next.workspaceId = workspaceId;
537
539
  if (mode !== void 0 || "mode" in patch) next.mode = mode;
538
540
  if (permission !== void 0 || "permission" in patch) next.permission = permission;
@@ -1280,6 +1282,7 @@ window.__ModuleLoader__.load({
1280
1282
  if (record.workspaceId !== void 0 && typeof record.workspaceId !== "string") return false;
1281
1283
  if (record.mode !== void 0 && typeof record.mode !== "string") return false;
1282
1284
  if (record.permission !== void 0 && typeof record.permission !== "string") return false;
1285
+ if (record.reuseSession !== void 0 && typeof record.reuseSession !== "boolean") return false;
1283
1286
  if (!Array.isArray(record.executions)) return false;
1284
1287
  for (const execution of record.executions) {
1285
1288
  if (typeof execution !== "object" || execution === null) return false;
@@ -1389,6 +1392,7 @@ window.__ModuleLoader__.load({
1389
1392
  task.mode = normalizeTargetId(row.mode);
1390
1393
  task.archivedAt = typeof row.archivedAt === "number" && Number.isFinite(row.archivedAt) ? row.archivedAt : void 0;
1391
1394
  task.permission = isTaskPermission(row.permission) ? row.permission : void 0;
1395
+ task.reuseSession = row.reuseSession === true ? true : void 0;
1392
1396
  task.freeze = normalizeFreeze(row.freeze);
1393
1397
  task.handover = normalizeHandover(row.handover);
1394
1398
  task.permissionConfirmedAt = typeof row.permissionConfirmedAt === "number" && Number.isFinite(row.permissionConfirmedAt) ? row.permissionConfirmedAt : void 0;
@@ -1536,9 +1540,13 @@ window.__ModuleLoader__.load({
1536
1540
  "detail.permissionConfirmed": "已确认权限绑定 · {time}",
1537
1541
  "detail.title": "任务详情",
1538
1542
  "detail.edit": "编辑",
1543
+ "detail.duplicate": "复制为新任务",
1544
+ "detail.duplicateAndEdit": "修改并新建副本",
1539
1545
  "detail.close": "关闭",
1540
1546
  "edit.title": "编辑任务",
1541
1547
  "edit.save": "保存",
1548
+ "new.duplicateTitle": "新建任务(副本)",
1549
+ "new.archiveOriginal": "创建后归档原任务",
1542
1550
  "detail.prompt": "执行 Prompt",
1543
1551
  "detail.description": "描述",
1544
1552
  "detail.execution": "执行记录",
@@ -1597,6 +1605,8 @@ window.__ModuleLoader__.load({
1597
1605
  "exec.permission.danger-full-access": "完全访问",
1598
1606
  "exec.model.default": "宿主默认(agent-default-model)",
1599
1607
  "exec.model.unknown": "(未知模型/回退默认)",
1608
+ "exec.reuseSession": "在同一对话继续",
1609
+ "exec.reuseSessionHint": "开启后,本任务的后续执行在上一次会话里继续(该会话空闲且仍存在时),不再每次新建对话;每次复用时都会重新应用上面钉住的权限与模型。",
1600
1610
  "detail.executionSettings": "执行设置",
1601
1611
  "exec.hint": "执行时生效:工作区决定执行会话落在哪个工作区;模式决定会话的 agent 预设;权限经 /permission 命令应用到会话。留空则使用运行时默认。",
1602
1612
  "settings.title": "任务看板",
@@ -1682,9 +1692,13 @@ window.__ModuleLoader__.load({
1682
1692
  "detail.permissionConfirmed": "Permission confirmed · {time}",
1683
1693
  "detail.title": "Task Detail",
1684
1694
  "detail.edit": "Edit",
1695
+ "detail.duplicate": "Duplicate Task",
1696
+ "detail.duplicateAndEdit": "Edit as New Copy",
1685
1697
  "detail.close": "Close",
1686
1698
  "edit.title": "Edit Task",
1687
1699
  "edit.save": "Save",
1700
+ "new.duplicateTitle": "New Task (Copy)",
1701
+ "new.archiveOriginal": "Archive original task upon creation",
1688
1702
  "detail.prompt": "Run Prompt",
1689
1703
  "detail.description": "Description",
1690
1704
  "detail.execution": "Execution History",
@@ -1743,6 +1757,8 @@ window.__ModuleLoader__.load({
1743
1757
  "exec.permission.danger-full-access": "Full Access",
1744
1758
  "exec.model.default": "Host default (agent-default-model)",
1745
1759
  "exec.model.unknown": " (unknown / fallback to default)",
1760
+ "exec.reuseSession": "Continue in the same conversation",
1761
+ "exec.reuseSessionHint": "When on, later runs continue in the previous session (when that session is idle and still exists) instead of starting a new conversation each time; the pinned permission and model above are re-applied on every reuse.",
1746
1762
  "detail.executionSettings": "Execution Settings",
1747
1763
  "exec.hint": "Applied when the task runs: the workspace decides where the execution session lands; the mode composes the session's agent preset; the permission is applied through the /permission command. Blank = runtime default.",
1748
1764
  "settings.title": "Task Board",
@@ -1991,20 +2007,23 @@ window.__ModuleLoader__.load({
1991
2007
  * Creates through the Host and closes only after the Host confirms it.
1992
2008
  */
1993
2009
  /** New-task form overlay. */
1994
- function NewTaskModal({ controller, onClose }) {
1995
- const [title, setTitle] = (0, react.useState)("");
1996
- const [description, setDescription] = (0, react.useState)("");
1997
- const [prompt, setPrompt] = (0, react.useState)("");
1998
- const [workspaceId, setWorkspaceId] = (0, react.useState)("");
1999
- const [mode, setMode] = (0, react.useState)("");
2000
- const [permission, setPermission] = (0, react.useState)("");
2001
- const [model, setModel] = (0, react.useState)("");
2002
- const [scheduleEnabled, setScheduleEnabled] = (0, react.useState)(false);
2003
- const [scheduleCron, setScheduleCron] = (0, react.useState)("");
2010
+ function NewTaskModal({ controller, onClose, initialTask, onDuplicateSuccess }) {
2011
+ const isDuplicate = initialTask !== void 0;
2012
+ const [title, setTitle] = (0, react.useState)(initialTask?.title ?? "");
2013
+ const [description, setDescription] = (0, react.useState)(initialTask?.description ?? "");
2014
+ const [prompt, setPrompt] = (0, react.useState)(initialTask?.prompt ?? "");
2015
+ const [workspaceId, setWorkspaceId] = (0, react.useState)(initialTask?.workspaceId ?? "");
2016
+ const [mode, setMode] = (0, react.useState)(initialTask?.mode ?? "");
2017
+ const [permission, setPermission] = (0, react.useState)(initialTask?.permission ?? "");
2018
+ const [model, setModel] = (0, react.useState)(initialTask?.model ?? "");
2019
+ const [reuseSession, setReuseSession] = (0, react.useState)(initialTask?.reuseSession ?? false);
2020
+ const [scheduleEnabled, setScheduleEnabled] = (0, react.useState)(initialTask?.schedule?.enabled ?? false);
2021
+ const [scheduleCron, setScheduleCron] = (0, react.useState)(initialTask?.schedule?.cron ?? "");
2004
2022
  const [scheduleError, setScheduleError] = (0, react.useState)(void 0);
2005
2023
  const [freezeText, setFreezeText] = (0, react.useState)("");
2006
2024
  const [freezeError, setFreezeError] = (0, react.useState)(void 0);
2007
- const [handoverText, setHandoverText] = (0, react.useState)("");
2025
+ const [handoverText, setHandoverText] = (0, react.useState)(initialTask?.handover?.references !== void 0 ? initialTask.handover.references.join("\n") : "");
2026
+ const [archiveOriginal, setArchiveOriginal] = (0, react.useState)(true);
2008
2027
  const [error, setError] = (0, react.useState)(void 0);
2009
2028
  const [pending, setPending] = (0, react.useState)(false);
2010
2029
  const [options, setOptions] = (0, react.useState)(controller.getSnapshot().executionOptions);
@@ -2047,6 +2066,7 @@ window.__ModuleLoader__.load({
2047
2066
  mode: mode === "" ? void 0 : mode,
2048
2067
  permission: permission === "" ? void 0 : permission,
2049
2068
  model: model === "" ? void 0 : model,
2069
+ ...reuseSession ? { reuseSession: true } : {},
2050
2070
  schedule: scheduleEnabled ? {
2051
2071
  enabled: true,
2052
2072
  cron: scheduleCron.trim()
@@ -2056,13 +2076,16 @@ window.__ModuleLoader__.load({
2056
2076
  setError(controller.getSnapshot().transportError ?? t("new.required"));
2057
2077
  return;
2058
2078
  }
2079
+ if (isDuplicate && archiveOriginal && initialTask !== void 0) if (onDuplicateSuccess !== void 0) await onDuplicateSuccess(initialTask.id);
2080
+ else await controller.archiveTask(initialTask.id);
2059
2081
  onClose();
2060
2082
  };
2061
2083
  /** Next-run preview for a valid armed cron (creation-time only). */
2062
2084
  const scheduleNextRun = scheduleEnabled && scheduleCron.trim() !== "" && isValidCron(scheduleCron) ? nextRunAtMs(scheduleCron, Date.now()) : void 0;
2085
+ const modalTitle = isDuplicate ? t("new.duplicateTitle") : t("board.new");
2063
2086
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(ModalShell, {
2064
- ariaLabel: t("board.new"),
2065
- title: t("board.new"),
2087
+ ariaLabel: modalTitle,
2088
+ title: modalTitle,
2066
2089
  error,
2067
2090
  pending,
2068
2091
  submitLabel: t("new.submit"),
@@ -2204,6 +2227,20 @@ window.__ModuleLoader__.load({
2204
2227
  }, item.id))]
2205
2228
  })]
2206
2229
  }),
2230
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2231
+ className: board_module_css_default.scheduleToggle,
2232
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2233
+ type: "checkbox",
2234
+ checked: reuseSession,
2235
+ onChange: (event) => {
2236
+ setReuseSession(event.target.checked);
2237
+ }
2238
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("exec.reuseSession") })]
2239
+ }),
2240
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2241
+ className: board_module_css_default.detailText,
2242
+ children: t("exec.reuseSessionHint")
2243
+ }),
2207
2244
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
2208
2245
  className: board_module_css_default.detailSection,
2209
2246
  children: [
@@ -2264,6 +2301,17 @@ window.__ModuleLoader__.load({
2264
2301
  })
2265
2302
  ] })
2266
2303
  ]
2304
+ }),
2305
+ isDuplicate && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2306
+ className: board_module_css_default.checkboxLabel,
2307
+ style: { marginTop: "12px" },
2308
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2309
+ type: "checkbox",
2310
+ checked: archiveOriginal,
2311
+ onChange: (event) => {
2312
+ setArchiveOriginal(event.target.checked);
2313
+ }
2314
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("new.archiveOriginal") })]
2267
2315
  })
2268
2316
  ]
2269
2317
  });
@@ -2674,6 +2722,21 @@ window.__ModuleLoader__.load({
2674
2722
  }, item.id))
2675
2723
  ]
2676
2724
  })]
2725
+ }),
2726
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2727
+ className: board_module_css_default.scheduleToggle,
2728
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2729
+ type: "checkbox",
2730
+ checked: task.reuseSession === true,
2731
+ disabled: pending,
2732
+ onChange: (event) => {
2733
+ controller.updateTask(task.id, { reuseSession: event.target.checked });
2734
+ }
2735
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("exec.reuseSession") })]
2736
+ }),
2737
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2738
+ className: board_module_css_default.detailText,
2739
+ children: t("exec.reuseSessionHint")
2677
2740
  })
2678
2741
  ]
2679
2742
  });
@@ -2806,12 +2869,14 @@ window.__ModuleLoader__.load({
2806
2869
  function TaskDetail({ controller, task }) {
2807
2870
  const [confirmDelete, setConfirmDelete] = (0, react.useState)(false);
2808
2871
  const [showEdit, setShowEdit] = (0, react.useState)(false);
2872
+ const [showDuplicate, setShowDuplicate] = (0, react.useState)(false);
2809
2873
  const [latest, setLatest] = (0, react.useState)(task);
2810
2874
  (0, react.useEffect)(() => {
2811
2875
  setLatest(task);
2812
2876
  }, [task]);
2813
2877
  (0, react.useEffect)(() => {
2814
2878
  setShowEdit(false);
2879
+ setShowDuplicate(false);
2815
2880
  }, [task.id]);
2816
2881
  const current = latest;
2817
2882
  const snapshot = controller.getSnapshot();
@@ -3046,6 +3111,16 @@ window.__ModuleLoader__.load({
3046
3111
  },
3047
3112
  children: t("detail.edit")
3048
3113
  }),
3114
+ !archived && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3115
+ type: "button",
3116
+ className: board_module_css_default.ghostButton,
3117
+ disabled: pending,
3118
+ onClick: () => {
3119
+ setShowDuplicate(true);
3120
+ },
3121
+ title: canEditTaskContent(current) ? t("detail.duplicate") : t("detail.duplicateAndEdit"),
3122
+ children: canEditTaskContent(current) ? t("detail.duplicate") : t("detail.duplicateAndEdit")
3123
+ }),
3049
3124
  !archived && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3050
3125
  type: "button",
3051
3126
  className: board_module_css_default.primaryButton,
@@ -3115,6 +3190,17 @@ window.__ModuleLoader__.load({
3115
3190
  onClose: () => {
3116
3191
  setShowEdit(false);
3117
3192
  }
3193
+ }),
3194
+ showDuplicate && !archived && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NewTaskModal, {
3195
+ controller,
3196
+ initialTask: current,
3197
+ onClose: () => {
3198
+ setShowDuplicate(false);
3199
+ },
3200
+ onDuplicateSuccess: async (sourceId) => {
3201
+ await controller.archiveTask(sourceId);
3202
+ controller.closeTask();
3203
+ }
3118
3204
  })
3119
3205
  ]
3120
3206
  });
@@ -4550,7 +4636,7 @@ window.__ModuleLoader__.load({
4550
4636
  /** The building package's version, when the bundle carries it. */
4551
4637
  function bakedVersion() {
4552
4638
  try {
4553
- return "0.3.17";
4639
+ return "0.3.18";
4554
4640
  } catch {
4555
4641
  return;
4556
4642
  }