@linxin666/dsh-client-ui-task-board 0.1.16 → 0.1.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +11 -7
  3. package/README.zh.md +11 -7
  4. package/lib/client.js +821 -145
  5. package/lib/client.js.map +1 -1
  6. package/lib/index.js +1 -1
  7. package/lib/types/client/PluginSettingsCard.d.ts +11 -0
  8. package/lib/types/client/locales.d.ts +21 -0
  9. package/lib/types/client/schedule-presets.d.ts +10 -0
  10. package/lib/types/core/controller.d.ts +57 -1
  11. package/lib/types/core/execution.d.ts +65 -4
  12. package/lib/types/core/scheduler.d.ts +7 -0
  13. package/lib/types/core/store.d.ts +1 -1
  14. package/lib/types/core/tasks.d.ts +43 -0
  15. package/lib/types/core/use-cases/task-archive.d.ts +22 -0
  16. package/lib/types/core/use-cases/task-create.d.ts +1 -6
  17. package/lib/types/core/use-cases/task-update.d.ts +9 -4
  18. package/lib/types/index.d.ts +1 -1
  19. package/package.json +1 -1
  20. package/src/client/PluginSettingsCard.tsx +50 -47
  21. package/src/client/board/NewTaskModal.tsx +136 -3
  22. package/src/client/board/TaskBoard.tsx +48 -19
  23. package/src/client/board/TaskDetail.tsx +97 -8
  24. package/src/client/board.module.css +16 -0
  25. package/src/client/index.ts +71 -2
  26. package/src/client/locales.ts +42 -0
  27. package/src/client/schedule-presets.ts +13 -0
  28. package/src/client/settings-card.module.css +9 -0
  29. package/src/core/controller.ts +92 -2
  30. package/src/core/execution.ts +179 -12
  31. package/src/core/scheduler.ts +15 -0
  32. package/src/core/store.ts +16 -2
  33. package/src/core/tasks.ts +56 -0
  34. package/src/core/use-cases/task-archive.ts +48 -0
  35. package/src/core/use-cases/task-create.ts +12 -3
  36. package/src/core/use-cases/task-update.ts +34 -5
  37. 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 会话);任务支持 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
@@ -28,6 +28,17 @@ export interface PluginSettingsCardProps<TKey extends string = string> {
28
28
  onSave: () => void;
29
29
  /** Drop every staged edit. */
30
30
  onDiscard: () => void;
31
+ /**
32
+ * Render the controls expanded on arrival (still collapsible). Defaults to
33
+ * true: promoted first-level sections show their settings immediately.
34
+ */
35
+ defaultOpen?: boolean;
36
+ /**
37
+ * Render without the collapse affordance: a static header and an always
38
+ * visible body. Used by first-level settings sections whose nav entry
39
+ * already provides the selection.
40
+ */
41
+ alwaysOpen?: boolean;
31
42
  /** The plugin's controls. */
32
43
  children: ReactNode;
33
44
  }
@@ -13,6 +13,10 @@ export declare const zh: {
13
13
  'board.search': string;
14
14
  'board.empty': string;
15
15
  'board.filterAll': string;
16
+ 'board.archive': string;
17
+ 'board.archiveView': string;
18
+ 'board.backToBoard': string;
19
+ 'archive.empty': string;
16
20
  'board.status': string;
17
21
  'board.status.backlog': string;
18
22
  'board.status.todo': string;
@@ -40,6 +44,9 @@ export declare const zh: {
40
44
  'detail.run': string;
41
45
  'detail.rerun': string;
42
46
  'detail.delete': string;
47
+ 'detail.archive': string;
48
+ 'detail.restore': string;
49
+ 'detail.archivedAt': string;
43
50
  'detail.viewSession': string;
44
51
  'detail.noSession': string;
45
52
  'detail.executionStarted': string;
@@ -72,6 +79,20 @@ export declare const zh: {
72
79
  'detail.schedule.notScheduled': string;
73
80
  'detail.schedule.dueSoon': string;
74
81
  'card.scheduled': string;
82
+ 'new.workspace': string;
83
+ 'new.mode': string;
84
+ 'new.permission': string;
85
+ 'exec.workspace.recent': string;
86
+ 'exec.mode.default': string;
87
+ 'exec.mode.defaultSuffix': string;
88
+ 'exec.mode.brokenSuffix': string;
89
+ 'exec.mode.removed': string;
90
+ 'exec.permission.default': string;
91
+ 'exec.permission.read-only': string;
92
+ 'exec.permission.workspace-write': string;
93
+ 'exec.permission.danger-full-access': string;
94
+ 'detail.executionSettings': string;
95
+ 'exec.hint': string;
75
96
  'settings.title': string;
76
97
  'settings.description': string;
77
98
  'settings.enabled': string;
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Shared scheduled-run presets for the schedule editors (the new-task dialog
3
+ * and the task detail panel): cron expression → locale label.
4
+ */
5
+ import type { TaskBoardKey } from './locales.ts';
6
+ /** Common scheduled-run presets (cron → locale label). */
7
+ export declare const SCHEDULE_PRESETS: ReadonlyArray<{
8
+ cron: string;
9
+ label: TaskBoardKey;
10
+ }>;
@@ -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,35 @@ 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;
67
+ /** True when the board shows the archive view instead of the columns. */
68
+ archiveView: boolean;
46
69
  selectedTaskId: string | undefined;
70
+ /** Picker option sets (workspace list + agent-preset roster). */
71
+ executionOptions: ExecutionOptionsSnapshot;
47
72
  }
48
73
  /** The selected task (resolved from the ledger), or undefined. */
49
74
  export declare function selectedTaskOf(snapshot: ControllerSnapshot): TaskRecord | undefined;
@@ -55,7 +80,9 @@ export declare class BoardController {
55
80
  private readonly deps;
56
81
  private tasks;
57
82
  private boardOpen;
83
+ private archiveView;
58
84
  private selectedTaskId;
85
+ private executionOptions;
59
86
  private listeners;
60
87
  private disposers;
61
88
  private readonly now;
@@ -71,12 +98,32 @@ export declare class BoardController {
71
98
  openBoard(): void;
72
99
  closeBoard(): void;
73
100
  toggleBoard(): void;
101
+ /**
102
+ * Switch between the kanban columns and the archive view. Leaving the
103
+ * archive view with an archived task still selected closes the selection —
104
+ * the detail overlay must not linger over a task that is off-board.
105
+ */
106
+ toggleArchiveView(): void;
74
107
  openTask(id: string): void;
75
108
  closeTask(): void;
76
109
  createTask(input: NewTaskInput): TaskRecord | undefined;
77
- updateTask(id: string, patch: Partial<Pick<TaskRecord, 'title' | 'description' | 'prompt'>>): void;
110
+ updateTask(id: string, patch: TaskUpdatePatch): void;
111
+ /**
112
+ * Replace (a part of) the picker option sets the UI feeds (workspace list
113
+ * and agent-preset roster come from the runtime, not the ledger).
114
+ */
115
+ setExecutionOptions(patch: Partial<ExecutionOptionsSnapshot>): void;
78
116
  moveTask(id: string, status: TaskStatus): void;
79
117
  deleteTask(id: string): void;
118
+ /**
119
+ * Archive a settled task (done/failed). Running or on-board-unsettled
120
+ * tasks are refused so the runner keeps exclusive ownership of their
121
+ * lifecycle.
122
+ * @returns true when applied.
123
+ */
124
+ archiveTask(id: string): boolean;
125
+ /** Restore an archived task back onto the board (same status column). */
126
+ restoreTask(id: string): boolean;
80
127
  /**
81
128
  * Update a task's schedule rule. A blank or invalid cron expression is
82
129
  * rejected (returns false, state untouched). When the rule ends up enabled
@@ -96,6 +143,15 @@ export declare class BoardController {
96
143
  * schedule rule (it was deleted mid-tick, for example).
97
144
  */
98
145
  applyScheduleNextRun(id: string, nextRunAt: number | undefined, lastTriggeredAt: number | undefined): void;
146
+ /**
147
+ * Reload the ledger from the persisted store without notifying subscribers.
148
+ * The scheduler calls this before every tick so a task deleted in another
149
+ * tab (or a stale in-memory copy) can never be fired from this tab: the
150
+ * fire decision and the subsequent roll-forward both run on the freshest
151
+ * persisted truth. Deliberately silent — same-origin external changes still
152
+ * reach subscribers through the storage-event subscription.
153
+ */
154
+ reloadFromStore(): void;
99
155
  /**
100
156
  * Jump to an execution's session transcript. Selecting the session changes
101
157
  * `current`, which closes the board (the conversation view takes over).
@@ -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
  }
@@ -101,9 +133,38 @@ export type ExecutionEvent = {
101
133
  */
102
134
  export declare class ExecutionService {
103
135
  private readonly env;
136
+ /**
137
+ * In-flight preset switches keyed by `sessionId\u0000mode`. The session
138
+ * list mirror can lag behind a just-applied switch, so concurrent runs
139
+ * against one blank session would otherwise each issue the same select
140
+ * RPC (an amplified storm); sharing one in-flight call keeps the wire to
141
+ * a single request. The settled entry is forgotten so a later run can
142
+ * switch again.
143
+ */
144
+ private readonly presetSwitches;
104
145
  /** @param env - the runtime faces (real or fake). */
105
146
  constructor(env: ExecutionEnvironment);
106
147
  run(task: TaskRecord, execution: ExecutionRecord, onEvent: (event: ExecutionEvent) => void): Promise<void>;
148
+ /**
149
+ * Recompose the execution session's agent from the task-pinned preset.
150
+ * No-op when the task pins none or the session already runs it; fails the
151
+ * run when the session is no longer blank, the preset face is missing, or
152
+ * the wire refuses.
153
+ */
154
+ private applyMode;
155
+ /**
156
+ * One in-flight `select` per (session, preset): concurrent runs against
157
+ * the same blank session share the same wire call instead of each issuing
158
+ * a duplicate RPC. The entry is removed once the call settles, so a later
159
+ * run (after a shared failure, say) issues a fresh switch.
160
+ */
161
+ private switchPreset;
162
+ /**
163
+ * Apply the task-pinned permission preset through the `/permission <id>`
164
+ * slash command. No-op when the task pins none; fails the run when the
165
+ * admission is rejected or no command claimed the line.
166
+ */
167
+ private applyPermission;
107
168
  /**
108
169
  * Inspect a reloaded/background task that was left 'running' and emit a
109
170
  * settled event when its session already finished.
@@ -3,6 +3,13 @@ import type { TaskRecord } from './tasks.ts';
3
3
  export interface SchedulerDeps {
4
4
  /** Read the current task ledger (the controller snapshot). */
5
5
  tasks(): readonly TaskRecord[];
6
+ /**
7
+ * Re-sync the task ledger from its persisted source before the tick decides
8
+ * what is due. The wiring passes the controller's store reload so a task
9
+ * deleted elsewhere can never be fired from a stale in-memory copy.
10
+ * Optional: absent in pure in-memory harnesses.
11
+ */
12
+ refresh?: () => void;
6
13
  /** Clock; defaults to Date.now in the controller wiring. */
7
14
  now(): number;
8
15
  /** Trigger one task's real execution (the controller's runTask); resolves
@@ -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,55 @@ 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;
78
+ /**
79
+ * When the task was archived (ms epoch). Archived tasks keep their status
80
+ * and execution history but leave the main board; absent means on-board.
81
+ */
82
+ archivedAt?: number;
63
83
  }
84
+ /** Statuses a settled task may be archived from. */
85
+ export declare const ARCHIVABLE_STATUSES: readonly TaskStatus[];
86
+ /** Permission presets a task may pin on its execution session (the `/permission <id>` ids). */
87
+ export declare const TASK_PERMISSIONS: readonly ["read-only", "workspace-write", "danger-full-access"];
88
+ /** One permission preset id. */
89
+ export type TaskPermission = typeof TASK_PERMISSIONS[number];
90
+ /** Whether an unknown value is a known permission preset id. */
91
+ export declare function isTaskPermission(value: unknown): value is TaskPermission;
64
92
  /** Input for creating a task. */
65
93
  export interface NewTaskInput {
66
94
  title: string;
67
95
  description: string;
68
96
  prompt: string;
97
+ /** Workspace the execution must run in; empty/absent = the recent workspace. */
98
+ workspaceId?: string;
99
+ /** Agent preset the execution session must be composed from; empty/absent = deployment default. */
100
+ mode?: string;
101
+ /** Permission preset applied to the execution session; absent = session default. */
102
+ permission?: TaskPermission;
103
+ /**
104
+ * Optional scheduled-run rule requested at creation time (the new-task
105
+ * dialog): an enable flag plus a 5-field cron expression. The create use
106
+ * case arms it only when enabled and the expression is valid.
107
+ */
108
+ schedule?: {
109
+ enabled: boolean;
110
+ cron: string;
111
+ };
69
112
  }
70
113
  /** The five kanban columns, in display order. */
71
114
  export declare const COLUMNS: readonly {
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Archive/restore task use case: move a settled (done/failed) task off the
3
+ * main board and back. Purely a marker transition — the task keeps its
4
+ * status, execution history and transcript references, so archiving never
5
+ * loses traceability and restoring lands it in the exact column it left.
6
+ */
7
+ import type { TaskRecord } from '../tasks.ts';
8
+ /** Result of an archive transition. */
9
+ export interface ArchiveTaskResult {
10
+ /** The next ledger. */
11
+ tasks: readonly TaskRecord[];
12
+ /** Whether the archive was applied (false = unknown task / not archivable). */
13
+ archived: boolean;
14
+ }
15
+ /**
16
+ * Archive one task: only settled statuses (done/failed) can be archived;
17
+ * a running or not-yet-settled task stays on the board (its runner still
18
+ * owns its lifecycle). Already-archived tasks are a no-op.
19
+ */
20
+ export declare function applyArchiveTask(tasks: readonly TaskRecord[], id: string, now: number): ArchiveTaskResult;
21
+ /** Restore one task back onto the main board (clears the archive marker). */
22
+ export declare function applyRestoreTask(tasks: readonly TaskRecord[], id: string, now: number): ArchiveTaskResult;
@@ -1,8 +1,3 @@
1
- /**
2
- * Create-task use case: mint a new task from user input, rejecting a blank
3
- * title. Pure ledger transition (no persistence or notify — the controller
4
- * orchestrates those), so it is unit-testable without any runtime face.
5
- */
6
1
  import { type NewTaskInput, type TaskRecord } from '../tasks.ts';
7
2
  /** Result of a create transition: the new task (when accepted) + the next ledger. */
8
3
  export interface CreateTaskResult {
@@ -15,7 +10,7 @@ export interface CreateTaskResult {
15
10
  * Apply a create against the current ledger. Returns the new task and the
16
11
  * appended ledger, or the unchanged ledger when the title is blank.
17
12
  * @param tasks - current ledger.
18
- * @param input - raw user input (title/description/prompt).
13
+ * @param input - raw user input (title/description/prompt + optional schedule).
19
14
  * @param now - clock instant (ms epoch).
20
15
  * @param id - minted task id.
21
16
  */
@@ -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.18",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "types": "lib/types/index.d.ts",
@@ -45,6 +45,17 @@ export interface PluginSettingsCardProps<TKey extends string = string> {
45
45
  onSave: () => void
46
46
  /** Drop every staged edit. */
47
47
  onDiscard: () => void
48
+ /**
49
+ * Render the controls expanded on arrival (still collapsible). Defaults to
50
+ * true: promoted first-level sections show their settings immediately.
51
+ */
52
+ defaultOpen?: boolean
53
+ /**
54
+ * Render without the collapse affordance: a static header and an always
55
+ * visible body. Used by first-level settings sections whose nav entry
56
+ * already provides the selection.
57
+ */
58
+ alwaysOpen?: boolean
48
59
  /** The plugin's controls. */
49
60
  children: ReactNode
50
61
  }
@@ -55,57 +66,27 @@ export interface PluginSettingsCardProps<TKey extends string = string> {
55
66
  * @returns the card, or nothing while the namespace is still loading.
56
67
  */
57
68
  export function PluginSettingsCard<TKey extends string = string>(props: PluginSettingsCardProps<TKey>) {
58
- const [open, setOpen] = useState(false)
59
- const { state } = props
69
+ const [open, setOpen] = useState(props.defaultOpen ?? true)
70
+ const { state, alwaysOpen } = props
60
71
  if (!state.available) return null
61
72
  const title = props.t(props.titleKey)
62
73
  const description = props.t(props.descriptionKey)
63
74
  const blocked = !state.dirty || state.invalid || state.saving
64
- const cardClass = open ? `${css.cardOpen} ${css.card}` : css.card
65
- // The namespace exists but the Host does not serve it to this client (the
66
- // official settings allowlist omits third-party namespaces): show a card
67
- // that explains the gap instead of vanishing, so a missing card never
68
- // reads as a missing plugin.
69
- if (!state.exposed) {
70
- return (
71
- <li className={cardClass}>
72
- <button
73
- type="button"
74
- className={css.header}
75
- aria-expanded={open}
76
- aria-label={`${props.t(open ? 'settings.collapse' : 'settings.expand')}: ${title}`}
77
- onClick={() => { setOpen(!open) }}
78
- >
79
- <span className={css.headText}>
80
- <span className={css.name} title={title}>{title}</span>
81
- <span className={css.description} title={description}>{description}</span>
82
- </span>
83
- <svg
84
- width="14"
85
- height="14"
86
- viewBox="0 0 14 14"
87
- fill="none"
88
- xmlns="http://www.w3.org/2000/svg"
89
- className={open ? `${css.chevron} ${css.chevronOpen}` : css.chevron}
90
- >
91
- <path
92
- d="M11.8486 5.5L11.4238 5.92383L8.69727 8.65137C8.44157 8.90706 8.21562 9.13382 8.01172 9.29785C7.79912 9.46883 7.55595 9.61756 7.25 9.66602C7.08435 9.69222 6.91565 9.69222 6.75 9.66602C6.44405 9.61756 6.20088 9.46883 5.98828 9.29785C5.78438 9.13382 5.55843 8.90706 5.30273 8.65137L2.57617 5.92383L2.15137 5.5L3 4.65137L3.42383 5.07617L6.15137 7.80273C6.42595 8.07732 6.59876 8.24849 6.74023 8.3623C6.87291 8.46904 6.92272 8.47813 6.9375 8.48047C6.97895 8.48703 7.02105 8.48703 7.0625 8.48047C7.07728 8.47813 7.12709 8.46904 7.25977 8.3623C7.40124 8.24849 7.57405 8.07732 7.84863 7.80273L10.5762 5.07617L11 4.65137L11.8486 5.5Z"
93
- fill="currentColor"
94
- />
95
- </svg>
96
- </button>
97
- {open
98
- ? (
99
- <div className={css.body}>
100
- <p className={css.notExposed} role="status">{props.t('settings.notExposed')}</p>
101
- </div>
102
- )
103
- : null}
104
- </li>
75
+ const expanded = alwaysOpen === true || open
76
+ const cardClass = expanded ? `${css.cardOpen} ${css.card}` : css.card
77
+ // With alwaysOpen the nav entry already provides the selection, so the
78
+ // header is a static title row instead of a disclosure button.
79
+ const header = alwaysOpen === true
80
+ ? (
81
+ <div className={css.headerStatic}>
82
+ <span className={css.headText}>
83
+ <span className={css.name} title={title}>{title}</span>
84
+ <span className={css.description} title={description}>{description}</span>
85
+ </span>
86
+ {state.dirty ? <span className={css.pending} title={props.t('settings.unsaved')}>{props.t('settings.unsaved')}</span> : null}
87
+ </div>
105
88
  )
106
- }
107
- return (
108
- <li className={cardClass}>
89
+ : (
109
90
  <button
110
91
  type="button"
111
92
  className={css.header}
@@ -132,7 +113,29 @@ export function PluginSettingsCard<TKey extends string = string>(props: PluginSe
132
113
  />
133
114
  </svg>
134
115
  </button>
135
- {open
116
+ )
117
+ // The namespace exists but the Host does not serve it to this client (the
118
+ // official settings allowlist omits third-party namespaces): show a card
119
+ // that explains the gap instead of vanishing, so a missing card never
120
+ // reads as a missing plugin.
121
+ if (!state.exposed) {
122
+ return (
123
+ <li className={cardClass}>
124
+ {header}
125
+ {expanded
126
+ ? (
127
+ <div className={css.body}>
128
+ <p className={css.notExposed} role="status">{props.t('settings.notExposed')}</p>
129
+ </div>
130
+ )
131
+ : null}
132
+ </li>
133
+ )
134
+ }
135
+ return (
136
+ <li className={cardClass}>
137
+ {header}
138
+ {expanded
136
139
  ? (
137
140
  <div className={css.body}>
138
141
  {!state.writable ? <p className={css.readOnly} role="status">{props.t('settings.readOnly')}</p> : null}