@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/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 会话);任务支持 5 段 cron 定时执行(如 0 23 * * *);数据存浏览器 localStorage(键 dsh.taskBoard.v1)。限制:定时调度在浏览器端,需 GUI 标签页打开,错过即跳过;执行消耗 API 额度。用户提到「任务看板 / 看板 / 定时任务」时即指本插件,请据此协作。";
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: Partial<Pick<TaskRecord, 'title' | 'description' | 'prompt'>>): void;
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.
@@ -1,4 +1,4 @@
1
- import type { TaskRecord } from './tasks.ts';
1
+ import { type TaskRecord } from './tasks.ts';
2
2
  /** Persistence seam for the task ledger. */
3
3
  export interface TaskStore {
4
4
  /** Read the persisted ledger (empty when nothing is stored yet). */
@@ -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) with a fresh updatedAt. Pure ledger transition (no persistence or
4
- * notify — the controller orchestrates those).
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 { TaskRecord } from '../tasks.ts';
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.
@@ -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.16",
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 { t } from '../locales.ts'
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({ title, description, prompt })
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;
@@ -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 => sessions.binding(id as SessionId),
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))
@@ -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.',