@linxin666/dsh-client-ui-task-board 0.1.16 → 0.1.17
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 +8 -4
- package/README.zh.md +8 -4
- package/lib/client.js +447 -33
- package/lib/client.js.map +1 -1
- package/lib/index.js +1 -1
- package/lib/types/client/locales.d.ts +14 -0
- package/lib/types/core/controller.d.ts +30 -1
- package/lib/types/core/execution.d.ts +49 -4
- package/lib/types/core/store.d.ts +1 -1
- package/lib/types/core/tasks.d.ts +27 -0
- package/lib/types/core/use-cases/task-update.d.ts +9 -4
- package/lib/types/index.d.ts +1 -1
- package/package.json +1 -1
- package/src/client/board/NewTaskModal.tsx +68 -3
- package/src/client/board/TaskDetail.tsx +71 -1
- package/src/client/board.module.css +16 -0
- package/src/client/index.ts +68 -2
- package/src/client/locales.ts +28 -0
- package/src/core/controller.ts +38 -2
- package/src/core/execution.ts +140 -12
- package/src/core/store.ts +15 -2
- package/src/core/tasks.ts +41 -0
- package/src/core/use-cases/task-update.ts +34 -5
- package/src/index.ts +1 -1
package/lib/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import z from "schemastery";
|
|
|
5
5
|
const SECTION_ORDER = 200;
|
|
6
6
|
const inject = ["systemPrompt"];
|
|
7
7
|
/** Model-facing announcement: plugin presence, capabilities, and limits. */
|
|
8
|
-
const TASK_BOARD_GUIDANCE = "本机已安装 dsh-task-board 插件(DSH Web GUI 的任务看板):侧边栏「任务看板」入口;在 dsh-web-ui 插件全家桶仓库(packages/dsh-task-board)统一维护,经聚合包 web-ui-all 一键安装。能力:多列看板管理任务;任务可真实执行(驱动 agent
|
|
8
|
+
const TASK_BOARD_GUIDANCE = "本机已安装 dsh-task-board 插件(DSH Web GUI 的任务看板):侧边栏「任务看板」入口;在 dsh-web-ui 插件全家桶仓库(packages/dsh-task-board)统一维护,经聚合包 web-ui-all 一键安装。能力:多列看板管理任务;任务可真实执行(驱动 agent 会话);任务可钉住执行目标——工作区 / 模式(agent 预设)/ 权限(read-only / workspace-write / danger-full-access),缺省用运行时默认;任务支持 5 段 cron 定时执行(如 0 23 * * *);数据存浏览器 localStorage(键 dsh.taskBoard.v1)。限制:定时调度在浏览器端,需 GUI 标签页打开,错过即跳过;执行消耗 API 额度。用户提到「任务看板 / 看板 / 定时任务」时即指本插件,请据此协作。";
|
|
9
9
|
/**
|
|
10
10
|
* Settings namespace of the board's announcement capability — the section the
|
|
11
11
|
* web settings surface edits. Spelled here rather than imported: the browser
|
|
@@ -72,6 +72,20 @@ export declare const zh: {
|
|
|
72
72
|
'detail.schedule.notScheduled': string;
|
|
73
73
|
'detail.schedule.dueSoon': string;
|
|
74
74
|
'card.scheduled': string;
|
|
75
|
+
'new.workspace': string;
|
|
76
|
+
'new.mode': string;
|
|
77
|
+
'new.permission': string;
|
|
78
|
+
'exec.workspace.recent': string;
|
|
79
|
+
'exec.mode.default': string;
|
|
80
|
+
'exec.mode.defaultSuffix': string;
|
|
81
|
+
'exec.mode.brokenSuffix': string;
|
|
82
|
+
'exec.mode.removed': string;
|
|
83
|
+
'exec.permission.default': string;
|
|
84
|
+
'exec.permission.read-only': string;
|
|
85
|
+
'exec.permission.workspace-write': string;
|
|
86
|
+
'exec.permission.danger-full-access': string;
|
|
87
|
+
'detail.executionSettings': string;
|
|
88
|
+
'exec.hint': string;
|
|
75
89
|
'settings.title': string;
|
|
76
90
|
'settings.description': string;
|
|
77
91
|
'settings.enabled': string;
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
import { ExecutionService } from './execution.ts';
|
|
17
17
|
import type { TaskStore } from './store.ts';
|
|
18
18
|
import { type NewTaskInput, type TaskRecord, type TaskStatus } from './tasks.ts';
|
|
19
|
+
import { type TaskUpdatePatch } from './use-cases/task-update.ts';
|
|
19
20
|
/** The sessions face the controller needs for navigation awareness. */
|
|
20
21
|
export interface SessionsControllerFace {
|
|
21
22
|
list: {
|
|
@@ -39,11 +40,33 @@ export interface ControllerDeps {
|
|
|
39
40
|
/** Debounce (ms) for session-list-changed reconciles; defaults to 350. */
|
|
40
41
|
reconcileDebounceMs?: number;
|
|
41
42
|
}
|
|
43
|
+
/** One workspace option the execution-target pickers offer. */
|
|
44
|
+
export interface ExecutionWorkspaceOption {
|
|
45
|
+
workspaceId: string;
|
|
46
|
+
/** Display label (workspace title; the wiring falls back to the path). */
|
|
47
|
+
title: string;
|
|
48
|
+
}
|
|
49
|
+
/** One agent-preset option the execution-target pickers offer. */
|
|
50
|
+
export interface ExecutionPresetOption {
|
|
51
|
+
id: string;
|
|
52
|
+
name?: string;
|
|
53
|
+
description?: string;
|
|
54
|
+
/** Why this preset cannot compose a session; the pickers disable it. */
|
|
55
|
+
broken?: string;
|
|
56
|
+
isDefault: boolean;
|
|
57
|
+
}
|
|
58
|
+
/** The execution-target option sets the UI feeds into the controller. */
|
|
59
|
+
export interface ExecutionOptionsSnapshot {
|
|
60
|
+
workspaces: readonly ExecutionWorkspaceOption[];
|
|
61
|
+
presets: readonly ExecutionPresetOption[];
|
|
62
|
+
}
|
|
42
63
|
/** Immutable controller snapshot for UI subscriptions. */
|
|
43
64
|
export interface ControllerSnapshot {
|
|
44
65
|
tasks: readonly TaskRecord[];
|
|
45
66
|
boardOpen: boolean;
|
|
46
67
|
selectedTaskId: string | undefined;
|
|
68
|
+
/** Picker option sets (workspace list + agent-preset roster). */
|
|
69
|
+
executionOptions: ExecutionOptionsSnapshot;
|
|
47
70
|
}
|
|
48
71
|
/** The selected task (resolved from the ledger), or undefined. */
|
|
49
72
|
export declare function selectedTaskOf(snapshot: ControllerSnapshot): TaskRecord | undefined;
|
|
@@ -56,6 +79,7 @@ export declare class BoardController {
|
|
|
56
79
|
private tasks;
|
|
57
80
|
private boardOpen;
|
|
58
81
|
private selectedTaskId;
|
|
82
|
+
private executionOptions;
|
|
59
83
|
private listeners;
|
|
60
84
|
private disposers;
|
|
61
85
|
private readonly now;
|
|
@@ -74,7 +98,12 @@ export declare class BoardController {
|
|
|
74
98
|
openTask(id: string): void;
|
|
75
99
|
closeTask(): void;
|
|
76
100
|
createTask(input: NewTaskInput): TaskRecord | undefined;
|
|
77
|
-
updateTask(id: string, patch:
|
|
101
|
+
updateTask(id: string, patch: TaskUpdatePatch): void;
|
|
102
|
+
/**
|
|
103
|
+
* Replace (a part of) the picker option sets the UI feeds (workspace list
|
|
104
|
+
* and agent-preset roster come from the runtime, not the ledger).
|
|
105
|
+
*/
|
|
106
|
+
setExecutionOptions(patch: Partial<ExecutionOptionsSnapshot>): void;
|
|
78
107
|
moveTask(id: string, status: TaskStatus): void;
|
|
79
108
|
deleteTask(id: string): void;
|
|
80
109
|
/**
|
|
@@ -14,22 +14,40 @@
|
|
|
14
14
|
* so tests drive it with plain fakes.
|
|
15
15
|
*/
|
|
16
16
|
import type { ExecutionRecord, TaskRecord } from './tasks.ts';
|
|
17
|
+
/** One list-row summary the execution service reads (the narrow slice of a SessionSummary). */
|
|
18
|
+
export interface ExecutionSessionSummary {
|
|
19
|
+
running: boolean;
|
|
20
|
+
completed?: boolean;
|
|
21
|
+
/** Empty-log bit: the preset can only be recomposed while the session is blank. */
|
|
22
|
+
blank?: boolean;
|
|
23
|
+
/** The preset the session currently runs (absent on deployments without presets). */
|
|
24
|
+
agentPreset?: string;
|
|
25
|
+
}
|
|
17
26
|
/** The narrow sessions face the service needs. */
|
|
18
27
|
export interface SessionsExecutionFace {
|
|
19
28
|
list: {
|
|
20
29
|
getSnapshot(): {
|
|
21
30
|
/** Baseline arrival lifecycle — 'pending' until the host list has loaded. */
|
|
22
31
|
phase: 'pending' | 'ready';
|
|
23
|
-
byId: Record<string,
|
|
24
|
-
running: boolean;
|
|
25
|
-
completed?: boolean;
|
|
26
|
-
}>;
|
|
32
|
+
byId: Record<string, ExecutionSessionSummary>;
|
|
27
33
|
};
|
|
28
34
|
subscribe(fn: () => void): () => void;
|
|
29
35
|
};
|
|
30
36
|
binding(id: string): {
|
|
31
37
|
session: SessionDriver;
|
|
32
38
|
} | undefined;
|
|
39
|
+
/** Record a host-confirmed preset switch so the list label moves immediately. */
|
|
40
|
+
noteAgentPreset?(sessionId: string, agentPreset: string): void;
|
|
41
|
+
}
|
|
42
|
+
/** The narrow agent-preset wire face the service needs (`agentPreset.select`). */
|
|
43
|
+
export interface PresetsExecutionFace {
|
|
44
|
+
/** Recompose a blank session's agent from a preset. */
|
|
45
|
+
select(sessionId: string, agentPreset: string): Promise<{
|
|
46
|
+
ok: true;
|
|
47
|
+
} | {
|
|
48
|
+
ok: false;
|
|
49
|
+
error: unknown;
|
|
50
|
+
}>;
|
|
33
51
|
}
|
|
34
52
|
/** The narrow workspaces face the service needs. */
|
|
35
53
|
export interface WorkspacesExecutionFace {
|
|
@@ -63,6 +81,18 @@ export interface SessionDriver {
|
|
|
63
81
|
ok: false;
|
|
64
82
|
error: unknown;
|
|
65
83
|
}>;
|
|
84
|
+
/**
|
|
85
|
+
* Admit one slash-command line against the session's agent (the
|
|
86
|
+
* `/permission <id>` mechanism). `matched` reports whether a command
|
|
87
|
+
* claimed the line.
|
|
88
|
+
*/
|
|
89
|
+
command(line: string): Promise<{
|
|
90
|
+
ok: true;
|
|
91
|
+
matched: boolean;
|
|
92
|
+
} | {
|
|
93
|
+
ok: false;
|
|
94
|
+
error: unknown;
|
|
95
|
+
}>;
|
|
66
96
|
getSnapshot(): {
|
|
67
97
|
running: boolean;
|
|
68
98
|
lastAgentError: string | null;
|
|
@@ -74,6 +104,8 @@ export interface SessionDriver {
|
|
|
74
104
|
export interface ExecutionEnvironment {
|
|
75
105
|
sessions: SessionsExecutionFace;
|
|
76
106
|
workspaces: WorkspacesExecutionFace;
|
|
107
|
+
/** Agent-preset wire face; absent on deployments without preset support. */
|
|
108
|
+
presets?: PresetsExecutionFace;
|
|
77
109
|
/** Raw-history reader for failure detection of never-opened sessions. */
|
|
78
110
|
history?: HistoryExecutionFace;
|
|
79
111
|
}
|
|
@@ -104,6 +136,19 @@ export declare class ExecutionService {
|
|
|
104
136
|
/** @param env - the runtime faces (real or fake). */
|
|
105
137
|
constructor(env: ExecutionEnvironment);
|
|
106
138
|
run(task: TaskRecord, execution: ExecutionRecord, onEvent: (event: ExecutionEvent) => void): Promise<void>;
|
|
139
|
+
/**
|
|
140
|
+
* Recompose the execution session's agent from the task-pinned preset.
|
|
141
|
+
* No-op when the task pins none or the session already runs it; fails the
|
|
142
|
+
* run when the session is no longer blank, the preset face is missing, or
|
|
143
|
+
* the wire refuses.
|
|
144
|
+
*/
|
|
145
|
+
private applyMode;
|
|
146
|
+
/**
|
|
147
|
+
* Apply the task-pinned permission preset through the `/permission <id>`
|
|
148
|
+
* slash command. No-op when the task pins none; fails the run when the
|
|
149
|
+
* admission is rejected or no command claimed the line.
|
|
150
|
+
*/
|
|
151
|
+
private applyPermission;
|
|
107
152
|
/**
|
|
108
153
|
* Inspect a reloaded/background task that was left 'running' and emit a
|
|
109
154
|
* settled event when its session already finished.
|
|
@@ -60,12 +60,39 @@ export interface TaskRecord {
|
|
|
60
60
|
executions: ExecutionRecord[];
|
|
61
61
|
/** Optional scheduled-run rule (absent on tasks without a schedule). */
|
|
62
62
|
schedule?: ScheduleRule;
|
|
63
|
+
/**
|
|
64
|
+
* Workspace the execution must run in (a workspace-list id); absent means
|
|
65
|
+
* the recent-workspace fallback at execution time.
|
|
66
|
+
*/
|
|
67
|
+
workspaceId?: string;
|
|
68
|
+
/**
|
|
69
|
+
* Agent preset the execution session must be composed from (an
|
|
70
|
+
* `agentPreset.list` id); absent means the deployment default.
|
|
71
|
+
*/
|
|
72
|
+
mode?: string;
|
|
73
|
+
/**
|
|
74
|
+
* Permission preset applied to the execution session through the
|
|
75
|
+
* `/permission <id>` slash command; absent leaves the session default.
|
|
76
|
+
*/
|
|
77
|
+
permission?: TaskPermission;
|
|
63
78
|
}
|
|
79
|
+
/** Permission presets a task may pin on its execution session (the `/permission <id>` ids). */
|
|
80
|
+
export declare const TASK_PERMISSIONS: readonly ["read-only", "workspace-write", "danger-full-access"];
|
|
81
|
+
/** One permission preset id. */
|
|
82
|
+
export type TaskPermission = typeof TASK_PERMISSIONS[number];
|
|
83
|
+
/** Whether an unknown value is a known permission preset id. */
|
|
84
|
+
export declare function isTaskPermission(value: unknown): value is TaskPermission;
|
|
64
85
|
/** Input for creating a task. */
|
|
65
86
|
export interface NewTaskInput {
|
|
66
87
|
title: string;
|
|
67
88
|
description: string;
|
|
68
89
|
prompt: string;
|
|
90
|
+
/** Workspace the execution must run in; empty/absent = the recent workspace. */
|
|
91
|
+
workspaceId?: string;
|
|
92
|
+
/** Agent preset the execution session must be composed from; empty/absent = deployment default. */
|
|
93
|
+
mode?: string;
|
|
94
|
+
/** Permission preset applied to the execution session; absent = session default. */
|
|
95
|
+
permission?: TaskPermission;
|
|
69
96
|
}
|
|
70
97
|
/** The five kanban columns, in display order. */
|
|
71
98
|
export declare const COLUMNS: readonly {
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Update-task use case: apply an editable-field patch (title/description/
|
|
3
|
-
* prompt
|
|
4
|
-
* notify — the
|
|
3
|
+
* prompt plus the execution targets workspaceId/mode/permission) with a
|
|
4
|
+
* fresh updatedAt. Pure ledger transition (no persistence or notify — the
|
|
5
|
+
* controller orchestrates those).
|
|
6
|
+
*
|
|
7
|
+
* An explicit `undefined` in the patch clears the field (the task falls
|
|
8
|
+
* back to the runtime default); an unknown permission string is ignored so
|
|
9
|
+
* stale UI can never persist a value the execution service rejects.
|
|
5
10
|
*/
|
|
6
|
-
import type
|
|
11
|
+
import { type TaskRecord } from '../tasks.ts';
|
|
7
12
|
/** Editable fields on a task (the update patch surface). */
|
|
8
|
-
export type TaskUpdatePatch = Partial<Pick<TaskRecord, 'title' | 'description' | 'prompt'>>;
|
|
13
|
+
export type TaskUpdatePatch = Partial<Pick<TaskRecord, 'title' | 'description' | 'prompt' | 'workspaceId' | 'mode' | 'permission'>>;
|
|
9
14
|
/**
|
|
10
15
|
* Apply an update across the ledger. Tasks that do not match the id are left
|
|
11
16
|
* untouched; the matched task receives the patch plus a fresh updatedAt.
|
package/lib/types/index.d.ts
CHANGED
|
@@ -14,7 +14,7 @@ import type { Context } from '@deepseek-ai/cordis';
|
|
|
14
14
|
import z from 'schemastery';
|
|
15
15
|
export declare const inject: string[];
|
|
16
16
|
/** Model-facing announcement: plugin presence, capabilities, and limits. */
|
|
17
|
-
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-ui \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\uFF1B\u4EFB\u52A1\u53EF\u771F\u5B9E\u6267\u884C\uFF08\u9A71\u52A8 agent \u4F1A\u8BDD\uFF09\uFF1B\u4EFB\u52A1\u652F\u6301 5 \u6BB5 cron \u5B9A\u65F6\u6267\u884C\uFF08\u5982 0 23 * * *\uFF09\uFF1B\u6570\u636E\u5B58\u6D4F\u89C8\u5668 localStorage\uFF08\u952E dsh.taskBoard.v1\uFF09\u3002\u9650\u5236\uFF1A\u5B9A\u65F6\u8C03\u5EA6\u5728\u6D4F\u89C8\u5668\u7AEF\uFF0C\u9700 GUI \u6807\u7B7E\u9875\u6253\u5F00\uFF0C\u9519\u8FC7\u5373\u8DF3\u8FC7\uFF1B\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";
|
|
17
|
+
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-ui \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\uFF1B\u4EFB\u52A1\u53EF\u771F\u5B9E\u6267\u884C\uFF08\u9A71\u52A8 agent \u4F1A\u8BDD\uFF09\uFF1B\u4EFB\u52A1\u53EF\u9489\u4F4F\u6267\u884C\u76EE\u6807\u2014\u2014\u5DE5\u4F5C\u533A / \u6A21\u5F0F\uFF08agent \u9884\u8BBE\uFF09/ \u6743\u9650\uFF08read-only / workspace-write / danger-full-access\uFF09\uFF0C\u7F3A\u7701\u7528\u8FD0\u884C\u65F6\u9ED8\u8BA4\uFF1B\u4EFB\u52A1\u652F\u6301 5 \u6BB5 cron \u5B9A\u65F6\u6267\u884C\uFF08\u5982 0 23 * * *\uFF09\uFF1B\u6570\u636E\u5B58\u6D4F\u89C8\u5668 localStorage\uFF08\u952E dsh.taskBoard.v1\uFF09\u3002\u9650\u5236\uFF1A\u5B9A\u65F6\u8C03\u5EA6\u5728\u6D4F\u89C8\u5668\u7AEF\uFF0C\u9700 GUI \u6807\u7B7E\u9875\u6253\u5F00\uFF0C\u9519\u8FC7\u5373\u8DF3\u8FC7\uFF1B\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";
|
|
18
18
|
/**
|
|
19
19
|
* Settings namespace of the board's announcement capability — the section the
|
|
20
20
|
* web settings surface edits. Spelled here rather than imported: the browser
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linxin666/dsh-client-ui-task-board",
|
|
3
3
|
"description": "Task board for the dsh web GUI: a sidebar entry plus a multi-column kanban view with local persistence, task details, and real execution through dsh sessions (session.prompt). Hot-pluggable — mounted via ~/.dsh/cordis.patch.yml + a profile node_modules symlink, no dsh source changes.",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.17",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "lib/types/index.d.ts",
|
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
* New-task modal: title + description + the prompt that execution will send.
|
|
3
3
|
* Creates through the controller (which persists immediately).
|
|
4
4
|
*/
|
|
5
|
-
import { useState } from 'react'
|
|
5
|
+
import { useEffect, useState } from 'react'
|
|
6
6
|
import type { BoardController } from '../../core/controller.ts'
|
|
7
|
-
import {
|
|
7
|
+
import { TASK_PERMISSIONS, type TaskPermission } from '../../core/tasks.ts'
|
|
8
|
+
import { t, type TaskBoardKey } from '../locales.ts'
|
|
8
9
|
import css from '../board.module.css'
|
|
9
10
|
|
|
10
11
|
/** New-task form overlay. */
|
|
@@ -12,10 +13,28 @@ export function NewTaskModal({ controller, onClose }: { controller: BoardControl
|
|
|
12
13
|
const [title, setTitle] = useState('')
|
|
13
14
|
const [description, setDescription] = useState('')
|
|
14
15
|
const [prompt, setPrompt] = useState('')
|
|
16
|
+
const [workspaceId, setWorkspaceId] = useState('')
|
|
17
|
+
const [mode, setMode] = useState('')
|
|
18
|
+
const [permission, setPermission] = useState('')
|
|
15
19
|
const [error, setError] = useState<string | undefined>(undefined)
|
|
20
|
+
const [options, setOptions] = useState(controller.getSnapshot().executionOptions)
|
|
21
|
+
|
|
22
|
+
// The workspace list and preset roster arrive from the runtime after mount;
|
|
23
|
+
// follow them so the pickers never freeze on an empty snapshot.
|
|
24
|
+
useEffect(
|
|
25
|
+
() => controller.subscribe(() => setOptions(controller.getSnapshot().executionOptions)),
|
|
26
|
+
[controller],
|
|
27
|
+
)
|
|
16
28
|
|
|
17
29
|
const submit = (): void => {
|
|
18
|
-
const task = controller.createTask({
|
|
30
|
+
const task = controller.createTask({
|
|
31
|
+
title,
|
|
32
|
+
description,
|
|
33
|
+
prompt,
|
|
34
|
+
workspaceId: workspaceId === '' ? undefined : workspaceId,
|
|
35
|
+
mode: mode === '' ? undefined : mode,
|
|
36
|
+
permission: permission === '' ? undefined : permission as TaskPermission,
|
|
37
|
+
})
|
|
19
38
|
if (task === undefined) {
|
|
20
39
|
setError(t('new.required'))
|
|
21
40
|
return
|
|
@@ -66,6 +85,52 @@ export function NewTaskModal({ controller, onClose }: { controller: BoardControl
|
|
|
66
85
|
/>
|
|
67
86
|
</label>
|
|
68
87
|
|
|
88
|
+
<label className={css.field}>
|
|
89
|
+
<span className={css.fieldLabel}>{t('new.workspace')}</span>
|
|
90
|
+
<select
|
|
91
|
+
className={css.select}
|
|
92
|
+
value={workspaceId}
|
|
93
|
+
onChange={event => { setWorkspaceId(event.target.value) }}
|
|
94
|
+
>
|
|
95
|
+
<option value="">{t('exec.workspace.recent')}</option>
|
|
96
|
+
{options.workspaces.map(workspace => (
|
|
97
|
+
<option key={workspace.workspaceId} value={workspace.workspaceId}>{workspace.title}</option>
|
|
98
|
+
))}
|
|
99
|
+
</select>
|
|
100
|
+
</label>
|
|
101
|
+
|
|
102
|
+
<label className={css.field}>
|
|
103
|
+
<span className={css.fieldLabel}>{t('new.mode')}</span>
|
|
104
|
+
<select
|
|
105
|
+
className={css.select}
|
|
106
|
+
value={mode}
|
|
107
|
+
onChange={event => { setMode(event.target.value) }}
|
|
108
|
+
>
|
|
109
|
+
<option value="">{t('exec.mode.default')}</option>
|
|
110
|
+
{options.presets.map(preset => (
|
|
111
|
+
<option key={preset.id} value={preset.id} disabled={preset.broken !== undefined}>
|
|
112
|
+
{preset.name ?? preset.id}
|
|
113
|
+
{preset.isDefault ? t('exec.mode.defaultSuffix') : ''}
|
|
114
|
+
{preset.broken !== undefined ? t('exec.mode.brokenSuffix') : ''}
|
|
115
|
+
</option>
|
|
116
|
+
))}
|
|
117
|
+
</select>
|
|
118
|
+
</label>
|
|
119
|
+
|
|
120
|
+
<label className={css.field}>
|
|
121
|
+
<span className={css.fieldLabel}>{t('new.permission')}</span>
|
|
122
|
+
<select
|
|
123
|
+
className={css.select}
|
|
124
|
+
value={permission}
|
|
125
|
+
onChange={event => { setPermission(event.target.value) }}
|
|
126
|
+
>
|
|
127
|
+
<option value="">{t('exec.permission.default')}</option>
|
|
128
|
+
{TASK_PERMISSIONS.map(id => (
|
|
129
|
+
<option key={id} value={id}>{t(`exec.permission.${id}` as TaskBoardKey)}</option>
|
|
130
|
+
))}
|
|
131
|
+
</select>
|
|
132
|
+
</label>
|
|
133
|
+
|
|
69
134
|
{error !== undefined && <p className={css.formError}>{error}</p>}
|
|
70
135
|
|
|
71
136
|
<footer className={css.modalFooter}>
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { useEffect, useState } from 'react'
|
|
8
8
|
import type { BoardController } from '../../core/controller.ts'
|
|
9
9
|
import { isValidCron } from '../../core/schedule.ts'
|
|
10
|
-
import { MANUAL_STATUSES, type ExecutionRecord, type TaskRecord, type TaskStatus } from '../../core/tasks.ts'
|
|
10
|
+
import { MANUAL_STATUSES, TASK_PERMISSIONS, type ExecutionRecord, type TaskPermission, type TaskRecord, type TaskStatus } from '../../core/tasks.ts'
|
|
11
11
|
import { t, type TaskBoardKey } from '../locales.ts'
|
|
12
12
|
import css from '../board.module.css'
|
|
13
13
|
import { ConfirmDialog } from './ConfirmDialog.tsx'
|
|
@@ -66,6 +66,74 @@ const SCHEDULE_PRESETS: ReadonlyArray<{ cron: string; label: TaskBoardKey }> = [
|
|
|
66
66
|
{ cron: '0 9 * * 1', label: 'detail.schedule.preset.weeklyMon9' },
|
|
67
67
|
]
|
|
68
68
|
|
|
69
|
+
/** The execution-target editor: workspace / mode / permission pickers. */
|
|
70
|
+
function ExecutionSettingsSection({ controller, task }: { controller: BoardController; task: TaskRecord }) {
|
|
71
|
+
const [options, setOptions] = useState(controller.getSnapshot().executionOptions)
|
|
72
|
+
useEffect(
|
|
73
|
+
() => controller.subscribe(() => setOptions(controller.getSnapshot().executionOptions)),
|
|
74
|
+
[controller],
|
|
75
|
+
)
|
|
76
|
+
const workspaceId = task.workspaceId ?? ''
|
|
77
|
+
const mode = task.mode ?? ''
|
|
78
|
+
const permission = task.permission ?? ''
|
|
79
|
+
// A pinned target may disappear from the runtime (workspace deleted,
|
|
80
|
+
// preset removed); keep it selectable as a stale row instead of silently
|
|
81
|
+
// dropping it, so the user sees exactly what the task will ask for.
|
|
82
|
+
const workspaceKnown = workspaceId === '' || options.workspaces.some(item => item.workspaceId === workspaceId)
|
|
83
|
+
const modeKnown = mode === '' || options.presets.some(item => item.id === mode)
|
|
84
|
+
return (
|
|
85
|
+
<section className={css.detailSection}>
|
|
86
|
+
<h4>{t('detail.executionSettings')}</h4>
|
|
87
|
+
<p className={css.detailText}>{t('exec.hint')}</p>
|
|
88
|
+
<label className={css.field}>
|
|
89
|
+
<span className={css.fieldLabel}>{t('new.workspace')}</span>
|
|
90
|
+
<select
|
|
91
|
+
className={css.select}
|
|
92
|
+
value={workspaceId}
|
|
93
|
+
onChange={event => { controller.updateTask(task.id, { workspaceId: event.target.value }) }}
|
|
94
|
+
>
|
|
95
|
+
<option value="">{t('exec.workspace.recent')}</option>
|
|
96
|
+
{!workspaceKnown && <option value={workspaceId}>{workspaceId}{t('exec.mode.removed')}</option>}
|
|
97
|
+
{options.workspaces.map(workspace => (
|
|
98
|
+
<option key={workspace.workspaceId} value={workspace.workspaceId}>{workspace.title}</option>
|
|
99
|
+
))}
|
|
100
|
+
</select>
|
|
101
|
+
</label>
|
|
102
|
+
<label className={css.field}>
|
|
103
|
+
<span className={css.fieldLabel}>{t('new.mode')}</span>
|
|
104
|
+
<select
|
|
105
|
+
className={css.select}
|
|
106
|
+
value={mode}
|
|
107
|
+
onChange={event => { controller.updateTask(task.id, { mode: event.target.value }) }}
|
|
108
|
+
>
|
|
109
|
+
<option value="">{t('exec.mode.default')}</option>
|
|
110
|
+
{!modeKnown && <option value={mode}>{mode}{t('exec.mode.removed')}</option>}
|
|
111
|
+
{options.presets.map(preset => (
|
|
112
|
+
<option key={preset.id} value={preset.id} disabled={preset.broken !== undefined}>
|
|
113
|
+
{preset.name ?? preset.id}
|
|
114
|
+
{preset.isDefault ? t('exec.mode.defaultSuffix') : ''}
|
|
115
|
+
{preset.broken !== undefined ? t('exec.mode.brokenSuffix') : ''}
|
|
116
|
+
</option>
|
|
117
|
+
))}
|
|
118
|
+
</select>
|
|
119
|
+
</label>
|
|
120
|
+
<label className={css.field}>
|
|
121
|
+
<span className={css.fieldLabel}>{t('new.permission')}</span>
|
|
122
|
+
<select
|
|
123
|
+
className={css.select}
|
|
124
|
+
value={permission}
|
|
125
|
+
onChange={event => { controller.updateTask(task.id, { permission: event.target.value === '' ? undefined : event.target.value as TaskPermission }) }}
|
|
126
|
+
>
|
|
127
|
+
<option value="">{t('exec.permission.default')}</option>
|
|
128
|
+
{TASK_PERMISSIONS.map(id => (
|
|
129
|
+
<option key={id} value={id}>{t(`exec.permission.${id}` as TaskBoardKey)}</option>
|
|
130
|
+
))}
|
|
131
|
+
</select>
|
|
132
|
+
</label>
|
|
133
|
+
</section>
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
|
|
69
137
|
/** The scheduled-runs editor: enable toggle, cron input + presets, next-run info. */
|
|
70
138
|
function ScheduleSection({ controller, task }: { controller: BoardController; task: TaskRecord }) {
|
|
71
139
|
const schedule = task.schedule
|
|
@@ -203,6 +271,8 @@ export function TaskDetail({ controller, task }: { controller: BoardController;
|
|
|
203
271
|
<pre className={css.promptBlock}>{current.prompt !== '' ? current.prompt : current.title}</pre>
|
|
204
272
|
</section>
|
|
205
273
|
|
|
274
|
+
<ExecutionSettingsSection controller={controller} task={current} />
|
|
275
|
+
|
|
206
276
|
<ScheduleSection controller={controller} task={current} />
|
|
207
277
|
|
|
208
278
|
<section className={css.detailSection}>
|
|
@@ -512,6 +512,18 @@ html[data-dsh-taskboard-active]:not([data-dsh-ssh-active]) [class*='centerCol']
|
|
|
512
512
|
border-color: var(--dsw-alias-state-business-primary);
|
|
513
513
|
}
|
|
514
514
|
|
|
515
|
+
.select {
|
|
516
|
+
padding: 7px 10px;
|
|
517
|
+
font-size: 13px;
|
|
518
|
+
color: var(--dsw-alias-label-primary);
|
|
519
|
+
background: var(--dsw-specific-input-major);
|
|
520
|
+
border: 1px solid var(--dsw-alias-border-l2);
|
|
521
|
+
border-radius: 8px;
|
|
522
|
+
outline: none;
|
|
523
|
+
font-family: inherit;
|
|
524
|
+
max-width: 100%;
|
|
525
|
+
}
|
|
526
|
+
|
|
515
527
|
.input::placeholder {
|
|
516
528
|
color: var(--dsw-alias-label-tertiary);
|
|
517
529
|
}
|
|
@@ -775,6 +787,7 @@ html[data-dsh-taskboard-active]:not([data-dsh-ssh-active]) [class*='centerCol']
|
|
|
775
787
|
.linkButton:focus-visible,
|
|
776
788
|
.search:focus-visible,
|
|
777
789
|
.input:focus-visible,
|
|
790
|
+
.select:focus-visible,
|
|
778
791
|
.schedulePreset:focus-visible,
|
|
779
792
|
.scheduleToggle input:focus-visible {
|
|
780
793
|
outline: 2px solid var(--dsw-alias-state-business-primary);
|
|
@@ -790,6 +803,7 @@ html[data-dsh-taskboard-active]:not([data-dsh-ssh-active]) [class*='centerCol']
|
|
|
790
803
|
.linkButton,
|
|
791
804
|
.search,
|
|
792
805
|
.input,
|
|
806
|
+
.select,
|
|
793
807
|
.schedulePreset,
|
|
794
808
|
.scheduleToggle input {
|
|
795
809
|
transition: background-color 120ms ease, color 120ms ease, border-color 120ms ease, outline-color 120ms ease, box-shadow 120ms ease, transform 120ms ease;
|
|
@@ -834,6 +848,7 @@ html[data-dsh-taskboard-active]:not([data-dsh-ssh-active]) [class*='centerCol']
|
|
|
834
848
|
|
|
835
849
|
/* --- search box focus (border tint + themed ring above) --- */
|
|
836
850
|
.search:focus,
|
|
851
|
+
.select:focus,
|
|
837
852
|
.schedulePreset:focus {
|
|
838
853
|
border-color: var(--dsw-alias-state-business-primary);
|
|
839
854
|
}
|
|
@@ -855,6 +870,7 @@ html[data-dsh-taskboard-active]:not([data-dsh-ssh-active]) [class*='centerCol']
|
|
|
855
870
|
.linkButton,
|
|
856
871
|
.search,
|
|
857
872
|
.input,
|
|
873
|
+
.select,
|
|
858
874
|
.schedulePreset,
|
|
859
875
|
.scheduleToggle input {
|
|
860
876
|
transition: none;
|
package/src/client/index.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* plugin must not take the GUI down.
|
|
10
10
|
*/
|
|
11
11
|
import type { ClientContext, SessionId, SettingsScope, SettingsScopeSpec, WorkspaceId } from '@deepseek-ai/dsh-client-runtime/client'
|
|
12
|
-
import type { ConnectionHandle } from '@deepseek-ai/dsh-client-connection/client'
|
|
12
|
+
import type { ConnectionHandle, PromptContentPart } from '@deepseek-ai/dsh-client-connection/client'
|
|
13
13
|
import type {} from '@deepseek-ai/dsh-client-ui-slots'
|
|
14
14
|
// Type-only: pulls the locale plugin's Context merge (ctx.locale) and its
|
|
15
15
|
// LocaleNamespaceMap merge table.
|
|
@@ -116,12 +116,40 @@ export function apply(ctx: ClientContext): void {
|
|
|
116
116
|
const exec = new ExecutionService({
|
|
117
117
|
sessions: {
|
|
118
118
|
list: sessions.list,
|
|
119
|
-
binding: id =>
|
|
119
|
+
binding: id => {
|
|
120
|
+
const binding = sessions.binding(id as SessionId)
|
|
121
|
+
if (binding === undefined) return undefined
|
|
122
|
+
const { session } = binding
|
|
123
|
+
return {
|
|
124
|
+
session: {
|
|
125
|
+
rename: title => session.rename(title),
|
|
126
|
+
prompt: (content, mode) =>
|
|
127
|
+
session.prompt(content as PromptContentPart[], mode).then(result =>
|
|
128
|
+
result.ok ? { ok: true as const } : { ok: false as const, error: result.error }),
|
|
129
|
+
command: line =>
|
|
130
|
+
session.command(line).then(result =>
|
|
131
|
+
result.ok ? { ok: true as const, matched: result.value.matched } : { ok: false as const, error: result.error }),
|
|
132
|
+
getSnapshot: () => session.getSnapshot(),
|
|
133
|
+
subscribe: fn => session.subscribe(fn),
|
|
134
|
+
},
|
|
135
|
+
}
|
|
136
|
+
},
|
|
137
|
+
noteAgentPreset: (sessionId, agentPreset) => sessions.noteAgentPreset(sessionId as SessionId, agentPreset),
|
|
120
138
|
},
|
|
121
139
|
workspaces: {
|
|
122
140
|
list: workspaces.list,
|
|
123
141
|
connectWorkspace: id => workspaces.connectWorkspace(id as WorkspaceId),
|
|
124
142
|
},
|
|
143
|
+
presets: {
|
|
144
|
+
select: async (sessionId, agentPreset) => {
|
|
145
|
+
try {
|
|
146
|
+
const response = await connection.api.agentPresets.select({ sessionId: sessionId as SessionId, agentPreset })
|
|
147
|
+
return response.result.ok ? { ok: true as const } : { ok: false as const, error: response.result.error }
|
|
148
|
+
} catch (error) {
|
|
149
|
+
return { ok: false as const, error }
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
},
|
|
125
153
|
history: {
|
|
126
154
|
loadTail: async sessionId => {
|
|
127
155
|
const response = await connection.api.sessions.history({
|
|
@@ -163,6 +191,44 @@ export function apply(ctx: ClientContext): void {
|
|
|
163
191
|
scheduler.start()
|
|
164
192
|
|
|
165
193
|
const disposers: Array<() => void> = []
|
|
194
|
+
|
|
195
|
+
// Execution-target option feeds: the workspace list drives the workspace
|
|
196
|
+
// picker, and the agent-preset roster drives the mode picker. Both are
|
|
197
|
+
// runtime facts (not ledger state), so the wiring pushes them into the
|
|
198
|
+
// controller on change; the preset roster is re-read after reconnects
|
|
199
|
+
// because a reconnect may serve a different deployment.
|
|
200
|
+
const pushWorkspaceOptions = (): void => {
|
|
201
|
+
const snapshot = workspaces.list.getSnapshot()
|
|
202
|
+
controller.setExecutionOptions({
|
|
203
|
+
workspaces: snapshot.items.map(item => ({
|
|
204
|
+
workspaceId: item.workspaceId,
|
|
205
|
+
title: item.title !== '' ? item.title : item.path,
|
|
206
|
+
})),
|
|
207
|
+
})
|
|
208
|
+
}
|
|
209
|
+
pushWorkspaceOptions()
|
|
210
|
+
disposers.push(workspaces.list.subscribe(pushWorkspaceOptions))
|
|
211
|
+
const pushPresetOptions = async (): Promise<void> => {
|
|
212
|
+
try {
|
|
213
|
+
const response = await connection.api.agentPresets.list({})
|
|
214
|
+
if (!response.result.ok) return
|
|
215
|
+
controller.setExecutionOptions({
|
|
216
|
+
presets: response.result.value.presets.map(preset => ({
|
|
217
|
+
id: preset.id,
|
|
218
|
+
name: preset.name,
|
|
219
|
+
description: preset.description,
|
|
220
|
+
broken: preset.broken,
|
|
221
|
+
isDefault: preset.isDefault,
|
|
222
|
+
})),
|
|
223
|
+
})
|
|
224
|
+
} catch (error) {
|
|
225
|
+
// A failed roster read leaves the previous options in place; the
|
|
226
|
+
// picker stays usable and the next reconnect retries the read.
|
|
227
|
+
console.error('[dsh-task-board] agent preset roster read failed', error)
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
void pushPresetOptions()
|
|
231
|
+
disposers.push(ctx.on('connection/reset', () => { void pushPresetOptions() }))
|
|
166
232
|
try {
|
|
167
233
|
disposers.push(mountSidebarEntry(controller))
|
|
168
234
|
disposers.push(mountBoard(controller))
|
package/src/client/locales.ts
CHANGED
|
@@ -73,6 +73,20 @@ export const zh = {
|
|
|
73
73
|
'detail.schedule.notScheduled': '尚未排程',
|
|
74
74
|
'detail.schedule.dueSoon': '即将运行',
|
|
75
75
|
'card.scheduled': '定时',
|
|
76
|
+
'new.workspace': '工作区',
|
|
77
|
+
'new.mode': '模式',
|
|
78
|
+
'new.permission': '权限',
|
|
79
|
+
'exec.workspace.recent': '最近使用(默认)',
|
|
80
|
+
'exec.mode.default': '部署默认',
|
|
81
|
+
'exec.mode.defaultSuffix': '(默认)',
|
|
82
|
+
'exec.mode.brokenSuffix': '(不可用)',
|
|
83
|
+
'exec.mode.removed': '(已移除)',
|
|
84
|
+
'exec.permission.default': '会话默认',
|
|
85
|
+
'exec.permission.read-only': '只读',
|
|
86
|
+
'exec.permission.workspace-write': '工作区可写',
|
|
87
|
+
'exec.permission.danger-full-access': '完全访问',
|
|
88
|
+
'detail.executionSettings': '执行设置',
|
|
89
|
+
'exec.hint': '执行时生效:工作区决定执行会话落在哪个工作区;模式决定会话的 agent 预设;权限经 /permission 命令应用到会话。留空则使用运行时默认。',
|
|
76
90
|
// 插件设置卡片(settings.plugin.item 席位)。
|
|
77
91
|
'settings.title': '任务看板',
|
|
78
92
|
'settings.description': '控制看板在 agent 系统提示中的播报行为。',
|
|
@@ -165,6 +179,20 @@ export const en: Record<keyof typeof zh, string> = {
|
|
|
165
179
|
'detail.schedule.notScheduled': 'Not scheduled yet',
|
|
166
180
|
'detail.schedule.dueSoon': 'Due soon',
|
|
167
181
|
'card.scheduled': 'scheduled',
|
|
182
|
+
'new.workspace': 'Workspace',
|
|
183
|
+
'new.mode': 'Mode',
|
|
184
|
+
'new.permission': 'Permission',
|
|
185
|
+
'exec.workspace.recent': 'Most recent (default)',
|
|
186
|
+
'exec.mode.default': 'Deployment default',
|
|
187
|
+
'exec.mode.defaultSuffix': ' (default)',
|
|
188
|
+
'exec.mode.brokenSuffix': ' (unavailable)',
|
|
189
|
+
'exec.mode.removed': ' (removed)',
|
|
190
|
+
'exec.permission.default': 'Session default',
|
|
191
|
+
'exec.permission.read-only': 'Read-only',
|
|
192
|
+
'exec.permission.workspace-write': 'Workspace Write',
|
|
193
|
+
'exec.permission.danger-full-access': 'Full Access',
|
|
194
|
+
'detail.executionSettings': 'Execution Settings',
|
|
195
|
+
'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.',
|
|
168
196
|
// Plugin settings card (the `settings.plugin.item` seat).
|
|
169
197
|
'settings.title': 'Task Board',
|
|
170
198
|
'settings.description': 'How the board announces itself in each agent system prompt.',
|