@linxin666/dsh-client-ui-task-board 0.3.2 → 0.3.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/README.i18n.yaml +2 -2
- package/README.md +6 -2
- package/README.zh.md +6 -2
- package/lib/client.js +437 -187
- package/lib/client.js.map +1 -1
- package/lib/index.js +45 -9
- package/lib/types/client/PluginSettingsCard.d.ts +2 -0
- package/lib/types/client/board/EditTaskModal.d.ts +8 -0
- package/lib/types/client/index.d.ts +1 -1
- package/lib/types/client/locales.d.ts +3 -0
- package/lib/types/client/settings-form.d.ts +1 -1
- package/lib/types/client/telemetry.d.ts +26 -0
- package/lib/types/core/controller.d.ts +9 -1
- package/lib/types/core/use-cases/task-update.d.ts +19 -3
- package/lib/types/http.d.ts +1 -1
- package/lib/types/index.d.ts +1 -1
- package/lib/types/loopback.d.ts +1 -1
- package/lib/types/mount-once.d.ts +1 -1
- package/package.json +2 -4
- package/src/client/PluginSettingsCard.tsx +4 -2
- package/src/client/board/EditTaskModal.tsx +94 -0
- package/src/client/board/TaskBoard.tsx +1 -1
- package/src/client/board/TaskDetail.tsx +19 -0
- package/src/client/index.ts +6 -1
- package/src/client/locales.ts +6 -0
- package/src/client/settings-card.module.css +5 -0
- package/src/client/settings-form.ts +1 -1
- package/src/client/telemetry.ts +105 -0
- package/src/core/controller.ts +11 -3
- package/src/core/use-cases/task-update.ts +33 -3
- package/src/dsh-home.ts +9 -3
- package/src/host-ledger.ts +9 -1
- package/src/http.ts +1 -1
- package/src/index.ts +1 -1
- package/src/loopback.ts +1 -1
- package/src/mount-once.ts +2 -2
package/lib/index.js
CHANGED
|
@@ -5,6 +5,7 @@ import { createHash, timingSafeEqual } from "node:crypto";
|
|
|
5
5
|
import { chmodSync, closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
6
6
|
import { dirname, isAbsolute, join, win32 } from "node:path";
|
|
7
7
|
import { homedir } from "node:os";
|
|
8
|
+
import { isAbsolute as isAbsolute$1, join as join$1 } from "node:path/posix";
|
|
8
9
|
//#region src/core/schedule.ts
|
|
9
10
|
/** Inclusive ranges per field, in cron order. */
|
|
10
11
|
const FIELD_RANGES = [
|
|
@@ -182,8 +183,9 @@ function isDigits(value) {
|
|
|
182
183
|
*/
|
|
183
184
|
/** Expand a leading ~ (or ~user) in a path, platform-style. */
|
|
184
185
|
function expandHome(path, home = homedir()) {
|
|
186
|
+
const j = home.startsWith("/") ? join$1 : join;
|
|
185
187
|
if (path === "~") return home;
|
|
186
|
-
if (path.startsWith("~/") || path.startsWith("~\\")) return
|
|
188
|
+
if (path.startsWith("~/") || path.startsWith("~\\")) return j(home, path.slice(2));
|
|
187
189
|
return path;
|
|
188
190
|
}
|
|
189
191
|
/**
|
|
@@ -193,12 +195,15 @@ function expandHome(path, home = homedir()) {
|
|
|
193
195
|
* @returns the absolute DSH home path.
|
|
194
196
|
*/
|
|
195
197
|
function resolveDshHome(env = process.env, home = homedir()) {
|
|
198
|
+
const isPosix = home.startsWith("/");
|
|
199
|
+
const j = isPosix ? join$1 : join;
|
|
200
|
+
const isAbs = isPosix ? isAbsolute$1 : isAbsolute;
|
|
196
201
|
const raw = env.DSH_HOME;
|
|
197
202
|
if (raw !== void 0 && raw.trim() !== "") {
|
|
198
203
|
const expanded = expandHome(raw.trim(), home);
|
|
199
|
-
return
|
|
204
|
+
return isAbs(expanded) ? expanded : j(process.cwd(), expanded);
|
|
200
205
|
}
|
|
201
|
-
return
|
|
206
|
+
return j(home, ".dsh");
|
|
202
207
|
}
|
|
203
208
|
/** Resolve the DSH home directory from the live environment. */
|
|
204
209
|
function dshHome() {
|
|
@@ -611,10 +616,34 @@ function applyScheduleNextRun(tasks, id, nextRunAt, lastTriggeredAt, now) {
|
|
|
611
616
|
* fresh updatedAt. Pure ledger transition (no persistence or notify — the
|
|
612
617
|
* controller orchestrates those).
|
|
613
618
|
*
|
|
614
|
-
* An explicit `undefined` in the patch clears
|
|
615
|
-
* back to the runtime default);
|
|
616
|
-
*
|
|
619
|
+
* An explicit `undefined` in the patch clears an execution-target field
|
|
620
|
+
* (the task falls back to the runtime default); content fields are trimmed
|
|
621
|
+
* and never cleared. An unknown permission string is ignored so stale UI
|
|
622
|
+
* can never persist a value the execution service rejects.
|
|
617
623
|
*/
|
|
624
|
+
/**
|
|
625
|
+
* The fields that edit the task's content (what the user reads and what the
|
|
626
|
+
* next execution sends). Unlike the execution targets they stay editable only
|
|
627
|
+
* while the task has never started executing — after the first run the
|
|
628
|
+
* recorded prompt is the record of what actually ran, so it becomes read-only.
|
|
629
|
+
*/
|
|
630
|
+
const TASK_CONTENT_FIELDS = [
|
|
631
|
+
"title",
|
|
632
|
+
"description",
|
|
633
|
+
"prompt"
|
|
634
|
+
];
|
|
635
|
+
/** Whether an update patch touches any task-content field. */
|
|
636
|
+
function hasContentPatch(patch) {
|
|
637
|
+
return TASK_CONTENT_FIELDS.some((field) => field in patch);
|
|
638
|
+
}
|
|
639
|
+
/**
|
|
640
|
+
* Whether a task's content may still be edited: the task must be on-board
|
|
641
|
+
* (not archived) and must never have started executing. Fail-closed: a
|
|
642
|
+
* running, settled, or cancelled-before-launch task keeps its content fixed.
|
|
643
|
+
*/
|
|
644
|
+
function canEditTaskContent(task) {
|
|
645
|
+
return task.archivedAt === void 0 && task.status !== "running" && task.executions.length === 0;
|
|
646
|
+
}
|
|
618
647
|
/** Keep an unknown permission string from entering the ledger. */
|
|
619
648
|
function normalizePermission(current, value) {
|
|
620
649
|
if (value === void 0) return void 0;
|
|
@@ -639,6 +668,11 @@ function applyUpdateTask(tasks, id, patch, now) {
|
|
|
639
668
|
...patch,
|
|
640
669
|
updatedAt: now
|
|
641
670
|
};
|
|
671
|
+
for (const field of TASK_CONTENT_FIELDS) {
|
|
672
|
+
if (!(field in patch)) continue;
|
|
673
|
+
const value = patch[field];
|
|
674
|
+
next[field] = value === void 0 ? task[field] : value.trim();
|
|
675
|
+
}
|
|
642
676
|
if (workspaceId !== void 0 || "workspaceId" in patch) next.workspaceId = workspaceId;
|
|
643
677
|
if (mode !== void 0 || "mode" in patch) next.mode = mode;
|
|
644
678
|
if (permission !== void 0 || "permission" in patch) next.permission = permission;
|
|
@@ -1266,6 +1300,8 @@ var HostTaskLedger = class {
|
|
|
1266
1300
|
const task = this.document.tasks.find((task) => task.id === action.taskId);
|
|
1267
1301
|
if (task === void 0) throw new Error("task not found");
|
|
1268
1302
|
if (task.archivedAt !== void 0) throw new Error("archived task is read-only");
|
|
1303
|
+
if (hasContentPatch(action.patch) && !canEditTaskContent(task)) throw new Error("task has already been executed");
|
|
1304
|
+
if ("title" in action.patch && action.patch.title?.trim() === "") throw new Error("title is required");
|
|
1269
1305
|
this.document.tasks = [...applyUpdateTask(this.document.tasks, action.taskId, action.patch, now)];
|
|
1270
1306
|
break;
|
|
1271
1307
|
}
|
|
@@ -2465,7 +2501,7 @@ function makeTaskBoardRoutes(service, access = {}) {
|
|
|
2465
2501
|
//#region src/mount-once.ts
|
|
2466
2502
|
/**
|
|
2467
2503
|
* Host single-instance guard shared by the plugin family. The family bundle
|
|
2468
|
-
* (dsh-web-
|
|
2504
|
+
* (dsh-web-all / dsh-skins) namespaces every child row id (web-ui-*), so
|
|
2469
2505
|
* the loader accepts a standalone install of the same package side by side;
|
|
2470
2506
|
* without this guard the second instance would still re-register the same
|
|
2471
2507
|
* webserver routes, tools, settings namespaces, and system-prompt sections
|
|
@@ -2478,7 +2514,7 @@ function makeTaskBoardRoutes(service, access = {}) {
|
|
|
2478
2514
|
* `ctx.effect` runs its callback immediately and treats the callback's
|
|
2479
2515
|
* return value as the fiber disposer, so the unmarker is returned, not run.
|
|
2480
2516
|
*/
|
|
2481
|
-
const MOUNTED = Symbol.for("dsh-web
|
|
2517
|
+
const MOUNTED = Symbol.for("dsh-web.mounted-plugins");
|
|
2482
2518
|
function mountedSet() {
|
|
2483
2519
|
const registry = globalThis;
|
|
2484
2520
|
return registry[MOUNTED] ??= /* @__PURE__ */ new Set();
|
|
@@ -2516,7 +2552,7 @@ const inject = [
|
|
|
2516
2552
|
"commands"
|
|
2517
2553
|
];
|
|
2518
2554
|
/** Model-facing announcement: plugin presence, capabilities, and limits. */
|
|
2519
|
-
const TASK_BOARD_GUIDANCE = "本机已安装 dsh-task-board 插件(DSH Web GUI 的任务看板):侧边栏「任务看板」入口;在 dsh-web
|
|
2555
|
+
const TASK_BOARD_GUIDANCE = "本机已安装 dsh-task-board 插件(DSH Web GUI 的任务看板):侧边栏「任务看板」入口;在 dsh-web 插件全家桶仓库(packages/dsh-task-board)统一维护,经聚合包 web-ui-all 一键安装。能力:多列看板管理任务;Host 权威账本;关闭浏览器后仍由 Host 执行和结算;任务可钉住工作区、agent 预设和权限;支持 Host 本地时区的 5 段 cron,错过的触发点不补跑;可选且默认关闭的空闲系统睡眠保护允许屏幕熄灭,但不承诺拦截合盖、手动睡眠、休眠、关机或唤醒已睡眠机器。执行消耗 API 额度。用户提到「任务看板 / 看板 / 定时任务」时即指本插件,请据此协作。若你同时用 todo_write 维护会话顶部的可见计划列表,最终回复前必须再次调用 todo_write 收尾:没有剩余工作时不要保留 in_progress,已完成的最后一步要标为 completed。";
|
|
2520
2556
|
/**
|
|
2521
2557
|
* Settings namespace of the board's announcement capability — the section the
|
|
2522
2558
|
* web settings surface edits. Spelled here rather than imported: the browser
|
|
@@ -22,6 +22,8 @@ export interface PluginSettingsCardProps<TKey extends string = string> {
|
|
|
22
22
|
titleKey: TKey;
|
|
23
23
|
/** Locale key of the line describing what this plugin's settings govern. */
|
|
24
24
|
descriptionKey: TKey;
|
|
25
|
+
/** Optional rich header description rendered instead of the plain t(descriptionKey) text (the plain text stays as the tooltip). */
|
|
26
|
+
descriptionNode?: ReactNode;
|
|
25
27
|
/** The card's form state: availability, writability, and what a save would do. */
|
|
26
28
|
state: CardShell;
|
|
27
29
|
/** Write every staged edit. */
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { BoardController } from '../../core/controller.ts';
|
|
2
|
+
import type { TaskRecord } from '../../core/tasks.ts';
|
|
3
|
+
/** Edit-task form overlay. */
|
|
4
|
+
export declare function EditTaskModal({ controller, task, onClose }: {
|
|
5
|
+
controller: BoardController;
|
|
6
|
+
task: TaskRecord;
|
|
7
|
+
onClose: () => void;
|
|
8
|
+
}): import("react").JSX.Element;
|
|
@@ -37,7 +37,7 @@ export interface SettingsPluginItemOwnerProps {
|
|
|
37
37
|
declare module '@deepseek-ai/cordis' {
|
|
38
38
|
interface Context {
|
|
39
39
|
/**
|
|
40
|
-
* Optional rc.6 compatibility binder provided by dsh-web-
|
|
40
|
+
* Optional rc.6 compatibility binder provided by dsh-web-settings;
|
|
41
41
|
* absent when that group plugin is not installed, so callers fall back to
|
|
42
42
|
* the official settings scope.
|
|
43
43
|
*/
|
|
@@ -40,7 +40,10 @@ export declare const zh: {
|
|
|
40
40
|
'new.cancel': string;
|
|
41
41
|
'new.required': string;
|
|
42
42
|
'detail.title': string;
|
|
43
|
+
'detail.edit': string;
|
|
43
44
|
'detail.close': string;
|
|
45
|
+
'edit.title': string;
|
|
46
|
+
'edit.save': string;
|
|
44
47
|
'detail.prompt': string;
|
|
45
48
|
'detail.description': string;
|
|
46
49
|
'detail.execution': string;
|
|
@@ -173,7 +173,7 @@ export declare class CardForm<T> {
|
|
|
173
173
|
/**
|
|
174
174
|
* Write every staged edit, then re-seed from what the Host accepted.
|
|
175
175
|
*
|
|
176
|
-
* When the scope carries the optional batch surface (the dsh-web
|
|
176
|
+
* When the scope carries the optional batch surface (the dsh-web
|
|
177
177
|
* bridge scope), every planned write rides one mutation so cross-field
|
|
178
178
|
* validate hooks (baseURL+model) judge the batch as a unit instead of
|
|
179
179
|
* deadlocking on per-field writes. Otherwise the per-field loop runs.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anonymous install telemetry shared by the DSH Web UI family plugins.
|
|
3
|
+
*
|
|
4
|
+
* Once per UTC day per browser, each wired plugin sends one heartbeat to
|
|
5
|
+
* the dsh-market edge API listing its package name (and version when known).
|
|
6
|
+
* The payload carries no conversation data and no identifiers beyond a random
|
|
7
|
+
* UUID generated in localStorage; the server hashes it with a deployment salt
|
|
8
|
+
* before storage and never persists IP addresses. Sends are fire-and-forget:
|
|
9
|
+
* failures stay silent and simply retry on a later mount or day. The system
|
|
10
|
+
* is documented in docs/telemetry.md.
|
|
11
|
+
*/
|
|
12
|
+
export type TelemetryChannel = 'market' | 'npm' | 'unknown';
|
|
13
|
+
export interface TelemetryItem {
|
|
14
|
+
/** npm package name or asset id, e.g. "@linxin666/dsh-pet" or "skin:harbor". */
|
|
15
|
+
name: string;
|
|
16
|
+
/** Installed version when known; omitted otherwise. */
|
|
17
|
+
version?: string;
|
|
18
|
+
/** Install channel when determinable (market = Workshop install). */
|
|
19
|
+
channel?: TelemetryChannel;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Fire the daily heartbeat for the given items at most once per UTC day per
|
|
23
|
+
* browser. Never throws and never blocks the caller. Items without an explicit
|
|
24
|
+
* version inherit the bundle's baked build version.
|
|
25
|
+
*/
|
|
26
|
+
export declare function reportDailyHeartbeat(items: readonly TelemetryItem[]): void;
|
|
@@ -125,7 +125,15 @@ export declare class BoardController {
|
|
|
125
125
|
createTask(input: NewTaskInput): TaskRecord | undefined;
|
|
126
126
|
/** Create through the Host and expose the task only after confirmation. */
|
|
127
127
|
createTaskConfirmed(input: NewTaskInput): Promise<TaskRecord | undefined>;
|
|
128
|
-
|
|
128
|
+
/**
|
|
129
|
+
* Apply an editable-field patch (task content + execution targets).
|
|
130
|
+
* Host-backed: the Host ledger owns the fail-closed checks (the content of
|
|
131
|
+
* an executed task is read-only) and confirms the mutation, so the resolved
|
|
132
|
+
* value reflects whether the Host accepted it; the legacy in-memory path
|
|
133
|
+
* applies and persists synchronously.
|
|
134
|
+
* @returns true when the patch was accepted by the authority.
|
|
135
|
+
*/
|
|
136
|
+
updateTask(id: string, patch: TaskUpdatePatch): Promise<boolean>;
|
|
129
137
|
/**
|
|
130
138
|
* Replace (a part of) the picker option sets the UI feeds (workspace list
|
|
131
139
|
* and agent-preset roster come from the runtime, not the ledger).
|
|
@@ -4,13 +4,29 @@
|
|
|
4
4
|
* fresh updatedAt. Pure ledger transition (no persistence or notify — the
|
|
5
5
|
* controller orchestrates those).
|
|
6
6
|
*
|
|
7
|
-
* An explicit `undefined` in the patch clears
|
|
8
|
-
* back to the runtime default);
|
|
9
|
-
*
|
|
7
|
+
* An explicit `undefined` in the patch clears an execution-target field
|
|
8
|
+
* (the task falls back to the runtime default); content fields are trimmed
|
|
9
|
+
* and never cleared. An unknown permission string is ignored so stale UI
|
|
10
|
+
* can never persist a value the execution service rejects.
|
|
10
11
|
*/
|
|
11
12
|
import { type TaskRecord } from '../tasks.ts';
|
|
12
13
|
/** Editable fields on a task (the update patch surface). */
|
|
13
14
|
export type TaskUpdatePatch = Partial<Pick<TaskRecord, 'title' | 'description' | 'prompt' | 'workspaceId' | 'mode' | 'permission'>>;
|
|
15
|
+
/**
|
|
16
|
+
* The fields that edit the task's content (what the user reads and what the
|
|
17
|
+
* next execution sends). Unlike the execution targets they stay editable only
|
|
18
|
+
* while the task has never started executing — after the first run the
|
|
19
|
+
* recorded prompt is the record of what actually ran, so it becomes read-only.
|
|
20
|
+
*/
|
|
21
|
+
export declare const TASK_CONTENT_FIELDS: readonly ["title", "description", "prompt"];
|
|
22
|
+
/** Whether an update patch touches any task-content field. */
|
|
23
|
+
export declare function hasContentPatch(patch: TaskUpdatePatch): boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Whether a task's content may still be edited: the task must be on-board
|
|
26
|
+
* (not archived) and must never have started executing. Fail-closed: a
|
|
27
|
+
* running, settled, or cancelled-before-launch task keeps its content fixed.
|
|
28
|
+
*/
|
|
29
|
+
export declare function canEditTaskContent(task: TaskRecord): boolean;
|
|
14
30
|
/**
|
|
15
31
|
* Apply an update across the ledger. Tasks that do not match the id are left
|
|
16
32
|
* untouched; the matched task receives the patch plus a fresh updatedAt.
|
package/lib/types/http.d.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* edit this shared source and re-run the sync instead of editing a copy.
|
|
11
11
|
* Consumer code is migrated onto it in follow-up waves; no call site changes
|
|
12
12
|
* belong in the same change as its introduction.
|
|
13
|
-
* @module dsh-web-
|
|
13
|
+
* @module dsh-web-shared/host/http
|
|
14
14
|
*/
|
|
15
15
|
import type { IncomingMessage, OutgoingHttpHeaders, ServerResponse } from 'node:http';
|
|
16
16
|
/**
|
package/lib/types/index.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ import z from 'schemastery';
|
|
|
11
11
|
export declare const DEFAULT_PROXY_TOKEN_ENV = "DSH_TASK_BOARD_PROXY_TOKEN";
|
|
12
12
|
export declare const inject: string[];
|
|
13
13
|
/** Model-facing announcement: plugin presence, capabilities, and limits. */
|
|
14
|
-
export declare const TASK_BOARD_GUIDANCE = "\u672C\u673A\u5DF2\u5B89\u88C5 dsh-task-board \u63D2\u4EF6\uFF08DSH Web GUI \u7684\u4EFB\u52A1\u770B\u677F\uFF09\uFF1A\u4FA7\u8FB9\u680F\u300C\u4EFB\u52A1\u770B\u677F\u300D\u5165\u53E3\uFF1B\u5728 dsh-web
|
|
14
|
+
export declare const TASK_BOARD_GUIDANCE = "\u672C\u673A\u5DF2\u5B89\u88C5 dsh-task-board \u63D2\u4EF6\uFF08DSH Web GUI \u7684\u4EFB\u52A1\u770B\u677F\uFF09\uFF1A\u4FA7\u8FB9\u680F\u300C\u4EFB\u52A1\u770B\u677F\u300D\u5165\u53E3\uFF1B\u5728 dsh-web \u63D2\u4EF6\u5168\u5BB6\u6876\u4ED3\u5E93\uFF08packages/dsh-task-board\uFF09\u7EDF\u4E00\u7EF4\u62A4\uFF0C\u7ECF\u805A\u5408\u5305 web-ui-all \u4E00\u952E\u5B89\u88C5\u3002\u80FD\u529B\uFF1A\u591A\u5217\u770B\u677F\u7BA1\u7406\u4EFB\u52A1\uFF1BHost \u6743\u5A01\u8D26\u672C\uFF1B\u5173\u95ED\u6D4F\u89C8\u5668\u540E\u4ECD\u7531 Host \u6267\u884C\u548C\u7ED3\u7B97\uFF1B\u4EFB\u52A1\u53EF\u9489\u4F4F\u5DE5\u4F5C\u533A\u3001agent \u9884\u8BBE\u548C\u6743\u9650\uFF1B\u652F\u6301 Host \u672C\u5730\u65F6\u533A\u7684 5 \u6BB5 cron\uFF0C\u9519\u8FC7\u7684\u89E6\u53D1\u70B9\u4E0D\u8865\u8DD1\uFF1B\u53EF\u9009\u4E14\u9ED8\u8BA4\u5173\u95ED\u7684\u7A7A\u95F2\u7CFB\u7EDF\u7761\u7720\u4FDD\u62A4\u5141\u8BB8\u5C4F\u5E55\u7184\u706D\uFF0C\u4F46\u4E0D\u627F\u8BFA\u62E6\u622A\u5408\u76D6\u3001\u624B\u52A8\u7761\u7720\u3001\u4F11\u7720\u3001\u5173\u673A\u6216\u5524\u9192\u5DF2\u7761\u7720\u673A\u5668\u3002\u6267\u884C\u6D88\u8017 API \u989D\u5EA6\u3002\u7528\u6237\u63D0\u5230\u300C\u4EFB\u52A1\u770B\u677F / \u770B\u677F / \u5B9A\u65F6\u4EFB\u52A1\u300D\u65F6\u5373\u6307\u672C\u63D2\u4EF6\uFF0C\u8BF7\u636E\u6B64\u534F\u4F5C\u3002\u82E5\u4F60\u540C\u65F6\u7528 todo_write \u7EF4\u62A4\u4F1A\u8BDD\u9876\u90E8\u7684\u53EF\u89C1\u8BA1\u5212\u5217\u8868\uFF0C\u6700\u7EC8\u56DE\u590D\u524D\u5FC5\u987B\u518D\u6B21\u8C03\u7528 todo_write \u6536\u5C3E\uFF1A\u6CA1\u6709\u5269\u4F59\u5DE5\u4F5C\u65F6\u4E0D\u8981\u4FDD\u7559 in_progress\uFF0C\u5DF2\u5B8C\u6210\u7684\u6700\u540E\u4E00\u6B65\u8981\u6807\u4E3A completed\u3002";
|
|
15
15
|
/**
|
|
16
16
|
* Settings namespace of the board's announcement capability — the section the
|
|
17
17
|
* web settings surface edits. Spelled here rather than imported: the browser
|
package/lib/types/loopback.d.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* Semantics: RFC 5735 IPv4 127/8, ::1, IPv4-mapped ::ffff:127/8 (matching the
|
|
8
8
|
* remote-web-ui gate), localhost hostnames, plus the browser same-origin
|
|
9
9
|
* markers (sec-fetch-site and Origin) for the request-level fence.
|
|
10
|
-
* @module dsh-web-
|
|
10
|
+
* @module dsh-web-shared/host/loopback
|
|
11
11
|
*/
|
|
12
12
|
import type { IncomingMessage } from 'node:http';
|
|
13
13
|
/** IPv4 127/8 predicate (four decimal octets, first == 127). */
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Host single-instance guard shared by the plugin family. The family bundle
|
|
3
|
-
* (dsh-web-
|
|
3
|
+
* (dsh-web-all / dsh-skins) namespaces every child row id (web-ui-*), so
|
|
4
4
|
* the loader accepts a standalone install of the same package side by side;
|
|
5
5
|
* without this guard the second instance would still re-register the same
|
|
6
6
|
* webserver routes, tools, settings namespaces, and system-prompt sections
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linxin666/dsh-client-ui-task-board",
|
|
3
3
|
"description": "Host-authoritative task board for the DSH Web GUI with real session execution, Host cron scheduling, and optional cross-platform idle-sleep protection; mounted without DSH source changes.",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.4",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "lib/types/index.d.ts",
|
|
@@ -38,8 +38,6 @@
|
|
|
38
38
|
}
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
|
-
"@deepseek-ai/dsh-agent": "^0.1.1-rc.2",
|
|
42
|
-
"@deepseek-ai/dsh-commands": "^0.1.1-rc.2",
|
|
43
41
|
"react": "^18.2.0"
|
|
44
42
|
},
|
|
45
43
|
"dependencies": {
|
|
@@ -79,7 +77,7 @@
|
|
|
79
77
|
"license": "Apache-2.0",
|
|
80
78
|
"repository": {
|
|
81
79
|
"type": "git",
|
|
82
|
-
"url": "https://github.com/zhu1090093659/dsh-web
|
|
80
|
+
"url": "https://github.com/zhu1090093659/dsh-web.git"
|
|
83
81
|
},
|
|
84
82
|
"scripts": {
|
|
85
83
|
"build": "tsc -p tsconfig.build.json && tsdown",
|
|
@@ -39,6 +39,8 @@ export interface PluginSettingsCardProps<TKey extends string = string> {
|
|
|
39
39
|
titleKey: TKey
|
|
40
40
|
/** Locale key of the line describing what this plugin's settings govern. */
|
|
41
41
|
descriptionKey: TKey
|
|
42
|
+
/** Optional rich header description rendered instead of the plain t(descriptionKey) text (the plain text stays as the tooltip). */
|
|
43
|
+
descriptionNode?: ReactNode
|
|
42
44
|
/** The card's form state: availability, writability, and what a save would do. */
|
|
43
45
|
state: CardShell
|
|
44
46
|
/** Write every staged edit. */
|
|
@@ -87,7 +89,7 @@ export function PluginSettingsCard<TKey extends string = string>(props: PluginSe
|
|
|
87
89
|
<div className={css.headerStatic}>
|
|
88
90
|
<span className={css.headText}>
|
|
89
91
|
<span className={css.name} title={title}>{title}</span>
|
|
90
|
-
<span className={css.description} title={description}>{description}</span>
|
|
92
|
+
<span className={css.description} title={description}>{props.descriptionNode ?? description}</span>
|
|
91
93
|
</span>
|
|
92
94
|
{state.dirty ? <span className={css.pending} title={props.t('settings.unsaved')}>{props.t('settings.unsaved')}</span> : null}
|
|
93
95
|
</div>
|
|
@@ -102,7 +104,7 @@ export function PluginSettingsCard<TKey extends string = string>(props: PluginSe
|
|
|
102
104
|
>
|
|
103
105
|
<span className={css.headText}>
|
|
104
106
|
<span className={css.name} title={title}>{title}</span>
|
|
105
|
-
<span className={css.description} title={description}>{description}</span>
|
|
107
|
+
<span className={css.description} title={description}>{props.descriptionNode ?? description}</span>
|
|
106
108
|
</span>
|
|
107
109
|
{state.dirty ? <span className={css.pending} title={props.t('settings.unsaved')}>{props.t('settings.unsaved')}</span> : null}
|
|
108
110
|
<svg
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Edit-task modal: title + description + the prompt the next execution will
|
|
3
|
+
* send, pre-filled from the task. Shown only for tasks that have never
|
|
4
|
+
* started executing (the detail view gates on canEditTaskContent); the Host
|
|
5
|
+
* still re-checks at submit, so a task that started running while the modal
|
|
6
|
+
* was open fails closed and the error surfaces here.
|
|
7
|
+
*/
|
|
8
|
+
import { useState } from 'react'
|
|
9
|
+
import type { BoardController } from '../../core/controller.ts'
|
|
10
|
+
import type { TaskRecord } from '../../core/tasks.ts'
|
|
11
|
+
import { t } from '../locales.ts'
|
|
12
|
+
import css from '../board.module.css'
|
|
13
|
+
|
|
14
|
+
/** Edit-task form overlay. */
|
|
15
|
+
export function EditTaskModal({ controller, task, onClose }: { controller: BoardController; task: TaskRecord; onClose: () => void }) {
|
|
16
|
+
const [title, setTitle] = useState(task.title)
|
|
17
|
+
const [description, setDescription] = useState(task.description)
|
|
18
|
+
const [prompt, setPrompt] = useState(task.prompt)
|
|
19
|
+
const [error, setError] = useState<string | undefined>(undefined)
|
|
20
|
+
const [pending, setPending] = useState(false)
|
|
21
|
+
|
|
22
|
+
const submit = async (): Promise<void> => {
|
|
23
|
+
if (title.trim() === '') {
|
|
24
|
+
setError(t('new.required'))
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
setPending(true)
|
|
28
|
+
// The Host confirms the mutation (and its fail-closed checks); only a
|
|
29
|
+
// confirmed save closes the modal.
|
|
30
|
+
if (await controller.updateTask(task.id, { title, description, prompt })) {
|
|
31
|
+
onClose()
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
setPending(false)
|
|
35
|
+
setError(controller.getSnapshot().transportError ?? t('new.required'))
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div className={css.modalBackdrop} onMouseDown={event => { if (event.target === event.currentTarget) onClose() }}>
|
|
40
|
+
<form
|
|
41
|
+
className={css.modal}
|
|
42
|
+
role="dialog"
|
|
43
|
+
aria-label={t('edit.title')}
|
|
44
|
+
onSubmit={event => { event.preventDefault(); void submit() }}
|
|
45
|
+
>
|
|
46
|
+
<h2 className={css.modalTitle}>{t('edit.title')}</h2>
|
|
47
|
+
|
|
48
|
+
<label className={css.field}>
|
|
49
|
+
<span className={css.fieldLabel}>{t('new.title')}</span>
|
|
50
|
+
<input
|
|
51
|
+
className={css.input}
|
|
52
|
+
value={title}
|
|
53
|
+
autoFocus
|
|
54
|
+
placeholder={t('new.titlePlaceholder')}
|
|
55
|
+
onChange={event => { setTitle(event.target.value); setError(undefined) }}
|
|
56
|
+
/>
|
|
57
|
+
</label>
|
|
58
|
+
|
|
59
|
+
<label className={css.field}>
|
|
60
|
+
<span className={css.fieldLabel}>{t('new.description')}</span>
|
|
61
|
+
<textarea
|
|
62
|
+
className={css.input}
|
|
63
|
+
rows={3}
|
|
64
|
+
value={description}
|
|
65
|
+
placeholder={t('new.descriptionPlaceholder')}
|
|
66
|
+
onChange={event => { setDescription(event.target.value) }}
|
|
67
|
+
/>
|
|
68
|
+
</label>
|
|
69
|
+
|
|
70
|
+
<label className={css.field}>
|
|
71
|
+
<span className={css.fieldLabel}>{t('new.prompt')}</span>
|
|
72
|
+
<textarea
|
|
73
|
+
className={css.input}
|
|
74
|
+
rows={4}
|
|
75
|
+
value={prompt}
|
|
76
|
+
placeholder={t('new.promptPlaceholder')}
|
|
77
|
+
onChange={event => { setPrompt(event.target.value) }}
|
|
78
|
+
/>
|
|
79
|
+
</label>
|
|
80
|
+
|
|
81
|
+
{error !== undefined && <p className={css.formError}>{error}</p>}
|
|
82
|
+
|
|
83
|
+
<footer className={css.modalFooter}>
|
|
84
|
+
<button type="button" className={css.ghostButton} onClick={onClose}>
|
|
85
|
+
{t('new.cancel')}
|
|
86
|
+
</button>
|
|
87
|
+
<button type="submit" className={css.primaryButton} disabled={pending}>
|
|
88
|
+
{t('edit.save')}
|
|
89
|
+
</button>
|
|
90
|
+
</footer>
|
|
91
|
+
</form>
|
|
92
|
+
</div>
|
|
93
|
+
)
|
|
94
|
+
}
|
|
@@ -52,7 +52,7 @@ export function TaskBoard({ controller }: { controller: BoardController }) {
|
|
|
52
52
|
return (
|
|
53
53
|
<div className={css.board} data-dsh-taskboard-board="" data-dsh-plugin="task-board">
|
|
54
54
|
<header className={css.boardHeader}>
|
|
55
|
-
{/* Shared hook: dsh-web-
|
|
55
|
+
{/* Shared hook: dsh-web-all offsets center-view back controls beside the collapsed mobile sidebar. */}
|
|
56
56
|
<button
|
|
57
57
|
type="button"
|
|
58
58
|
className={`${css.ghostButton} ${css.backButton}`}
|
|
@@ -8,10 +8,12 @@ import { useEffect, useState } from 'react'
|
|
|
8
8
|
import type { BoardController } from '../../core/controller.ts'
|
|
9
9
|
import { isValidCron } from '../../core/schedule.ts'
|
|
10
10
|
import { MANUAL_STATUSES, TASK_PERMISSIONS, type ExecutionRecord, type TaskPermission, type TaskRecord } from '../../core/tasks.ts'
|
|
11
|
+
import { canEditTaskContent } from '../../core/use-cases/task-update.ts'
|
|
11
12
|
import { t, type TaskBoardKey } from '../locales.ts'
|
|
12
13
|
import { SCHEDULE_PRESETS } from '../schedule-presets.ts'
|
|
13
14
|
import css from '../board.module.css'
|
|
14
15
|
import { ConfirmDialog } from './ConfirmDialog.tsx'
|
|
16
|
+
import { EditTaskModal } from './EditTaskModal.tsx'
|
|
15
17
|
import { formatHostTimestamp, formatTime } from './TaskCard.tsx'
|
|
16
18
|
import { STATUS_KEY } from './status-key.ts'
|
|
17
19
|
|
|
@@ -232,10 +234,13 @@ function ScheduleSection({ controller, task, pending }: { controller: BoardContr
|
|
|
232
234
|
/** Task detail overlay. */
|
|
233
235
|
export function TaskDetail({ controller, task }: { controller: BoardController; task: TaskRecord }) {
|
|
234
236
|
const [confirmDelete, setConfirmDelete] = useState(false)
|
|
237
|
+
const [showEdit, setShowEdit] = useState(false)
|
|
235
238
|
|
|
236
239
|
// Keep the overlay in sync if the task record changes underneath.
|
|
237
240
|
const [latest, setLatest] = useState(task)
|
|
238
241
|
useEffect(() => { setLatest(task) }, [task])
|
|
242
|
+
// A re-used overlay instance must not carry an edit session across tasks.
|
|
243
|
+
useEffect(() => { setShowEdit(false) }, [task.id])
|
|
239
244
|
const current = latest
|
|
240
245
|
const snapshot = controller.getSnapshot()
|
|
241
246
|
const running = current.status === 'running'
|
|
@@ -328,6 +333,16 @@ export function TaskDetail({ controller, task }: { controller: BoardController;
|
|
|
328
333
|
|
|
329
334
|
<footer className={css.detailFooter}>
|
|
330
335
|
{!archived && pending && <span className={css.detailMeta}>{t('board.pending')}…</span>}
|
|
336
|
+
{!archived && canEditTaskContent(current) && (
|
|
337
|
+
<button
|
|
338
|
+
type="button"
|
|
339
|
+
className={css.ghostButton}
|
|
340
|
+
disabled={pending}
|
|
341
|
+
onClick={() => { setShowEdit(true) }}
|
|
342
|
+
>
|
|
343
|
+
{t('detail.edit')}
|
|
344
|
+
</button>
|
|
345
|
+
)}
|
|
331
346
|
{!archived && (
|
|
332
347
|
<button
|
|
333
348
|
type="button"
|
|
@@ -395,6 +410,10 @@ export function TaskDetail({ controller, task }: { controller: BoardController;
|
|
|
395
410
|
}}
|
|
396
411
|
/>
|
|
397
412
|
)}
|
|
413
|
+
|
|
414
|
+
{showEdit && !archived && canEditTaskContent(current) && (
|
|
415
|
+
<EditTaskModal controller={controller} task={current} onClose={() => { setShowEdit(false) }} />
|
|
416
|
+
)}
|
|
398
417
|
</div>
|
|
399
418
|
)
|
|
400
419
|
}
|
package/src/client/index.ts
CHANGED
|
@@ -24,6 +24,7 @@ import { mountSidebarEntry } from './sidebar-entry.ts'
|
|
|
24
24
|
import { TaskBoardSettingsCard, TaskBoardSettingsCardController, type TaskBoardSettings } from './TaskBoardSettingsCard.tsx'
|
|
25
25
|
import { en, zh, type TaskBoardKey } from './locales.ts'
|
|
26
26
|
import { HttpTaskBoardHostTransport } from './host-api.ts'
|
|
27
|
+
import { reportDailyHeartbeat } from './telemetry.ts'
|
|
27
28
|
|
|
28
29
|
/** Locale namespace this plugin owns. */
|
|
29
30
|
const NS = 'task-board'
|
|
@@ -57,7 +58,7 @@ export interface SettingsPluginItemOwnerProps {
|
|
|
57
58
|
declare module '@deepseek-ai/cordis' {
|
|
58
59
|
interface Context {
|
|
59
60
|
/**
|
|
60
|
-
* Optional rc.6 compatibility binder provided by dsh-web-
|
|
61
|
+
* Optional rc.6 compatibility binder provided by dsh-web-settings;
|
|
61
62
|
* absent when that group plugin is not installed, so callers fall back to
|
|
62
63
|
* the official settings scope.
|
|
63
64
|
*/
|
|
@@ -74,6 +75,10 @@ export const inject = ['slots', 'sessions', 'workspaces', 'connection', 'setting
|
|
|
74
75
|
* @param ctx - client root context (services: sessions, workspaces).
|
|
75
76
|
*/
|
|
76
77
|
export function apply(ctx: ClientContext): void {
|
|
78
|
+
// Anonymous install heartbeat (docs/telemetry.md): one beat per browser per
|
|
79
|
+
// UTC day, package name only, silent failure.
|
|
80
|
+
reportDailyHeartbeat([{ name: '@linxin666/dsh-client-ui-task-board' }])
|
|
81
|
+
|
|
77
82
|
// A duplicated client injection (module factory executed twice in one page
|
|
78
83
|
// lifetime) would otherwise mount a second sidebar entry and board view.
|
|
79
84
|
// First application wins; later calls become no-ops (see apply-guard.ts).
|
package/src/client/locales.ts
CHANGED
|
@@ -41,7 +41,10 @@ export const zh = {
|
|
|
41
41
|
'new.cancel': '取消',
|
|
42
42
|
'new.required': '标题不能为空',
|
|
43
43
|
'detail.title': '任务详情',
|
|
44
|
+
'detail.edit': '编辑',
|
|
44
45
|
'detail.close': '关闭',
|
|
46
|
+
'edit.title': '编辑任务',
|
|
47
|
+
'edit.save': '保存',
|
|
45
48
|
'detail.prompt': '执行 Prompt',
|
|
46
49
|
'detail.description': '描述',
|
|
47
50
|
'detail.execution': '执行记录',
|
|
@@ -164,7 +167,10 @@ export const en: Record<keyof typeof zh, string> = {
|
|
|
164
167
|
'new.cancel': 'Cancel',
|
|
165
168
|
'new.required': 'Title is required',
|
|
166
169
|
'detail.title': 'Task Detail',
|
|
170
|
+
'detail.edit': 'Edit',
|
|
167
171
|
'detail.close': 'Close',
|
|
172
|
+
'edit.title': 'Edit Task',
|
|
173
|
+
'edit.save': 'Save',
|
|
168
174
|
'detail.prompt': 'Run Prompt',
|
|
169
175
|
'detail.description': 'Description',
|
|
170
176
|
'detail.execution': 'Execution History',
|
|
@@ -24,6 +24,10 @@
|
|
|
24
24
|
.header {
|
|
25
25
|
appearance: none;
|
|
26
26
|
width: 100%;
|
|
27
|
+
/* The suite does not force a global border-box, so width:100% + padding
|
|
28
|
+
would stretch the row 32px past the card and push the trailing pending
|
|
29
|
+
badge (last flex item) outside the card border. */
|
|
30
|
+
box-sizing: border-box;
|
|
27
31
|
font: inherit;
|
|
28
32
|
color: inherit;
|
|
29
33
|
text-align: left;
|
|
@@ -44,6 +48,7 @@
|
|
|
44
48
|
|
|
45
49
|
.headerStatic {
|
|
46
50
|
width: 100%;
|
|
51
|
+
box-sizing: border-box;
|
|
47
52
|
border-radius: 12px;
|
|
48
53
|
align-items: center;
|
|
49
54
|
gap: 12px;
|
|
@@ -320,7 +320,7 @@ export class CardForm<T> {
|
|
|
320
320
|
/**
|
|
321
321
|
* Write every staged edit, then re-seed from what the Host accepted.
|
|
322
322
|
*
|
|
323
|
-
* When the scope carries the optional batch surface (the dsh-web
|
|
323
|
+
* When the scope carries the optional batch surface (the dsh-web
|
|
324
324
|
* bridge scope), every planned write rides one mutation so cross-field
|
|
325
325
|
* validate hooks (baseURL+model) judge the batch as a unit instead of
|
|
326
326
|
* deadlocking on per-field writes. Otherwise the per-field loop runs.
|