@linxin666/dsh-client-ui-task-board 0.1.15 → 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/src/core/store.ts CHANGED
@@ -14,8 +14,7 @@
14
14
  * localStorage backend.
15
15
  */
16
16
  import { isValidCron } from './schedule.ts'
17
- import type { ScheduleRule, TaskRecord, TaskStatus } from './tasks.ts'
18
- import { isTaskStatus } from './tasks.ts'
17
+ import { isTaskPermission, isTaskStatus, type ScheduleRule, type TaskRecord, type TaskPermission, type TaskStatus } from './tasks.ts'
19
18
 
20
19
  /** Persistence seam for the task ledger. */
21
20
  export interface TaskStore {
@@ -64,6 +63,9 @@ function isTaskRecordShape(value: unknown): value is Omit<TaskRecord, 'status'>
64
63
  if (typeof record.prompt !== 'string') return false
65
64
  if (typeof record.createdAt !== 'number') return false
66
65
  if (typeof record.updatedAt !== 'number') return false
66
+ if (record.workspaceId !== undefined && typeof record.workspaceId !== 'string') return false
67
+ if (record.mode !== undefined && typeof record.mode !== 'string') return false
68
+ if (record.permission !== undefined && typeof record.permission !== 'string') return false
67
69
  if (!Array.isArray(record.executions)) return false
68
70
  for (const execution of record.executions) {
69
71
  if (typeof execution !== 'object' || execution === null) return false
@@ -78,6 +80,11 @@ function isTaskRecordShape(value: unknown): value is Omit<TaskRecord, 'status'>
78
80
  return true
79
81
  }
80
82
 
83
+ /** Collapse a blank persisted target string to undefined (clears the pin). */
84
+ function normalizeTargetId(value: string | undefined): string | undefined {
85
+ return value !== undefined && value.trim() === '' ? undefined : value
86
+ }
87
+
81
88
  /** A task record is structurally valid if it round-trips through the UI. */
82
89
  export function isTaskRecord(value: unknown): value is TaskRecord {
83
90
  return isTaskRecordShape(value) && isTaskStatus(value.status)
@@ -136,6 +143,12 @@ export function parseLedger(raw: string | null): TaskRecord[] {
136
143
  // clear a malformed persisted rule rather than leave it in the row.
137
144
  const task: TaskRecord = { ...row, status: normalizeStatus(row.status) }
138
145
  task.schedule = normalizeSchedule(row.schedule)
146
+ // Execution targets are normalized like the schedule: blank strings
147
+ // clear the pin and unknown permission strings from a future version
148
+ // fall back to the session default instead of dropping the row.
149
+ task.workspaceId = normalizeTargetId(row.workspaceId)
150
+ task.mode = normalizeTargetId(row.mode)
151
+ task.permission = isTaskPermission(row.permission) ? row.permission as TaskPermission : undefined
139
152
  tasks.push(task)
140
153
  }
141
154
  return tasks
package/src/core/tasks.ts CHANGED
@@ -64,6 +64,32 @@ export interface TaskRecord {
64
64
  executions: ExecutionRecord[]
65
65
  /** Optional scheduled-run rule (absent on tasks without a schedule). */
66
66
  schedule?: ScheduleRule
67
+ /**
68
+ * Workspace the execution must run in (a workspace-list id); absent means
69
+ * the recent-workspace fallback at execution time.
70
+ */
71
+ workspaceId?: string
72
+ /**
73
+ * Agent preset the execution session must be composed from (an
74
+ * `agentPreset.list` id); absent means the deployment default.
75
+ */
76
+ mode?: string
77
+ /**
78
+ * Permission preset applied to the execution session through the
79
+ * `/permission <id>` slash command; absent leaves the session default.
80
+ */
81
+ permission?: TaskPermission
82
+ }
83
+
84
+ /** Permission presets a task may pin on its execution session (the `/permission <id>` ids). */
85
+ export const TASK_PERMISSIONS = ['read-only', 'workspace-write', 'danger-full-access'] as const
86
+
87
+ /** One permission preset id. */
88
+ export type TaskPermission = typeof TASK_PERMISSIONS[number]
89
+
90
+ /** Whether an unknown value is a known permission preset id. */
91
+ export function isTaskPermission(value: unknown): value is TaskPermission {
92
+ return typeof value === 'string' && (TASK_PERMISSIONS as readonly string[]).includes(value)
67
93
  }
68
94
 
69
95
  /** Input for creating a task. */
@@ -71,6 +97,12 @@ export interface NewTaskInput {
71
97
  title: string
72
98
  description: string
73
99
  prompt: string
100
+ /** Workspace the execution must run in; empty/absent = the recent workspace. */
101
+ workspaceId?: string
102
+ /** Agent preset the execution session must be composed from; empty/absent = deployment default. */
103
+ mode?: string
104
+ /** Permission preset applied to the execution session; absent = session default. */
105
+ permission?: TaskPermission
74
106
  }
75
107
 
76
108
  /** The five kanban columns, in display order. */
@@ -103,6 +135,12 @@ export function canMoveManually(_from: TaskStatus, to: TaskStatus): boolean {
103
135
  return (MANUAL_STATUSES as readonly TaskStatus[]).includes(to)
104
136
  }
105
137
 
138
+ /** Normalize one optional execution-target string: trim; blank collapses to undefined. */
139
+ function normalizeTargetId(value: string | undefined): string | undefined {
140
+ const trimmed = value?.trim()
141
+ return trimmed === undefined || trimmed === '' ? undefined : trimmed
142
+ }
143
+
106
144
  /** Create a task from user input. */
107
145
  export function createTask(input: NewTaskInput, now: number, id: string): TaskRecord {
108
146
  return {
@@ -114,6 +152,9 @@ export function createTask(input: NewTaskInput, now: number, id: string): TaskRe
114
152
  createdAt: now,
115
153
  updatedAt: now,
116
154
  executions: [],
155
+ workspaceId: normalizeTargetId(input.workspaceId),
156
+ mode: normalizeTargetId(input.mode),
157
+ permission: isTaskPermission(input.permission) ? input.permission : undefined,
117
158
  }
118
159
  }
119
160
 
@@ -1,12 +1,31 @@
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 { isTaskPermission, type TaskRecord, type TaskPermission } from '../tasks.ts'
7
12
 
8
13
  /** Editable fields on a task (the update patch surface). */
9
- export type TaskUpdatePatch = Partial<Pick<TaskRecord, 'title' | 'description' | 'prompt'>>
14
+ export type TaskUpdatePatch = Partial<Pick<TaskRecord, 'title' | 'description' | 'prompt' | 'workspaceId' | 'mode' | 'permission'>>
15
+
16
+ /** Collapse a blank target string to undefined (clears the pin). */
17
+ function normalizeTargetId(value: string | undefined): string | undefined {
18
+ return value !== undefined && value.trim() === '' ? undefined : value
19
+ }
20
+
21
+ /** Keep an unknown permission string from entering the ledger. */
22
+ function normalizePermission(
23
+ current: TaskPermission | undefined,
24
+ value: TaskPermission | undefined,
25
+ ): TaskPermission | undefined {
26
+ if (value === undefined) return undefined
27
+ return isTaskPermission(value) ? value : current
28
+ }
10
29
 
11
30
  /**
12
31
  * Apply an update across the ledger. Tasks that do not match the id are left
@@ -22,5 +41,15 @@ export function applyUpdateTask(
22
41
  patch: TaskUpdatePatch,
23
42
  now: number,
24
43
  ): readonly TaskRecord[] {
25
- return tasks.map(task => task.id === id ? { ...task, ...patch, updatedAt: now } : task)
44
+ return tasks.map(task => {
45
+ if (task.id !== id) return task
46
+ const workspaceId = 'workspaceId' in patch ? normalizeTargetId(patch.workspaceId) : undefined
47
+ const mode = 'mode' in patch ? normalizeTargetId(patch.mode) : undefined
48
+ const permission = 'permission' in patch ? normalizePermission(task.permission, patch.permission) : undefined
49
+ const next: TaskRecord = { ...task, ...patch, updatedAt: now }
50
+ if (workspaceId !== undefined || 'workspaceId' in patch) next.workspaceId = workspaceId
51
+ if (mode !== undefined || 'mode' in patch) next.mode = mode
52
+ if (permission !== undefined || 'permission' in patch) next.permission = permission
53
+ return next
54
+ })
26
55
  }
package/src/index.ts CHANGED
@@ -22,7 +22,7 @@ const SECTION_ORDER = 200
22
22
  export const inject = ['systemPrompt']
23
23
 
24
24
  /** Model-facing announcement: plugin presence, capabilities, and limits. */
25
- export 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 额度。用户提到「任务看板 / 看板 / 定时任务」时即指本插件,请据此协作。'
25
+ export 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 额度。用户提到「任务看板 / 看板 / 定时任务」时即指本插件,请据此协作。'
26
26
 
27
27
  /**
28
28
  * Settings namespace of the board's announcement capability — the section the