@linxin666/dsh-client-ui-task-board 0.3.23 → 0.4.0

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/icon.svg +3 -0
  2. package/lib/client.js +351 -85
  3. package/lib/client.js.map +1 -1
  4. package/lib/index.js +174 -916
  5. package/lib/types/client/PluginSettingsCard.d.ts +4 -0
  6. package/lib/types/client/TaskBoardSettingsCard.d.ts +5 -5
  7. package/lib/types/client/board/TaskForm.d.ts +6 -1
  8. package/lib/types/client/board/parse-model-pref.d.ts +4 -0
  9. package/lib/types/client/index.d.ts +43 -10
  10. package/lib/types/client/locales.d.ts +1 -0
  11. package/lib/types/client/main-session.d.ts +32 -0
  12. package/lib/types/client/plugin-card-seat.d.ts +103 -0
  13. package/lib/types/client/settings-form.d.ts +17 -17
  14. package/lib/types/core/controller.d.ts +10 -8
  15. package/lib/types/core/session-reuse.d.ts +4 -2
  16. package/lib/types/host-ledger.d.ts +15 -0
  17. package/lib/types/http.d.ts +18 -1
  18. package/lib/types/index.d.ts +72 -17
  19. package/package.json +30 -24
  20. package/src/client/PluginSettingsCard.tsx +7 -2
  21. package/src/client/TaskBoardSettingsCard.tsx +7 -5
  22. package/src/client/board/NewTaskModal.tsx +33 -7
  23. package/src/client/board/TaskForm.tsx +13 -0
  24. package/src/client/board/parse-model-pref.ts +35 -0
  25. package/src/client/board.module.css +31 -12
  26. package/src/client/index.ts +121 -38
  27. package/src/client/locales.ts +2 -0
  28. package/src/client/main-session.ts +43 -0
  29. package/src/client/plugin-card-seat.ts +192 -0
  30. package/src/client/settings-form.ts +34 -31
  31. package/src/client/sidebar-entry-core.ts +3 -3
  32. package/src/client/sidebar-entry.ts +2 -2
  33. package/src/core/controller.ts +12 -8
  34. package/src/core/session-reuse.ts +14 -5
  35. package/src/host-ledger.ts +59 -11
  36. package/src/http.ts +23 -1
  37. package/src/index.ts +97 -52
package/src/index.ts CHANGED
@@ -6,11 +6,10 @@
6
6
  * a same-origin asynchronous view over that service.
7
7
  */
8
8
 
9
- import type { Context } from '@deepseek-ai/cordis'
9
+ import type { Context, Volatile } from '@deepseek-ai/cordis'
10
10
  import type {} from '@deepseek-ai/dsh-agent'
11
11
  import type {} from '@deepseek-ai/dsh-commands'
12
- import type { SettingsNamespace } from '@deepseek-ai/dsh-settings'
13
- import z from 'schemastery'
12
+ import z from '@deepseek-ai/schemastery'
14
13
  import type {} from '@deepseek-ai/dsh-system-prompt'
15
14
  import type {} from '@deepseek-ai/dsh-api-gateway'
16
15
  import type {} from '@deepseek-ai/dsh-workspace'
@@ -35,25 +34,30 @@ export const inject = ['systemPrompt', 'typertGateway', 'workspaceRegistry', 'we
35
34
  export const TASK_BOARD_GUIDANCE = '本机已安装 dsh-task-board 插件(DSH Web GUI 的任务看板):侧边栏「任务看板」入口;在 dsh-web 插件全家桶仓库(packages/dsh-task-board)统一维护,经聚合包 web-ui-all 一键安装。能力:多列看板管理任务;Host 权威账本;关闭浏览器后仍由 Host 执行和结算;任务可钉住工作区、agent 预设和权限;支持 Host 本地时区的 5 段 cron,错过的触发点不补跑;可选且默认关闭的空闲系统睡眠保护允许屏幕熄灭,但不承诺拦截合盖、手动睡眠、休眠、关机或唤醒已睡眠机器。执行消耗 API 额度。用户提到「任务看板 / 看板 / 定时任务」时即指本插件,请据此协作。若你同时用 todo_write 维护会话顶部的可见计划列表,最终回复前必须再次调用 todo_write 收尾:没有剩余工作时不要保留 in_progress,已完成的最后一步要标为 completed。'
36
35
 
37
36
  /**
38
- * Settings namespace of the board's announcement capability — the section the
39
- * web settings surface edits. Spelled here rather than imported: the browser
40
- * half spells the same value and must not depend on a Host package.
37
+ * Plugin config, validated by the same-named schemastery schema.
38
+ *
39
+ * This schema IS the board's settings page: the 0.1.7 Host serves one
40
+ * configuration form per profile entry from the entry's own Config, so the
41
+ * three fields the browser card edits are marked volatile — the Loader commits
42
+ * an edit into the running fiber's references without remounting the row (no
43
+ * settings document of the plugin's own exists any more).
41
44
  */
42
- export const TASK_BOARD_SETTINGS_NAMESPACE = 'task-board' as SettingsNamespace
43
-
44
- /** Plugin config, validated by the same-named schemastery schema. */
45
45
  export interface Config {
46
46
  /**
47
47
  * When true (default), a system-prompt section announces the board to every
48
48
  * agent. Set false to keep the board silent in prompts; agents then learn
49
49
  * about it only when the user mentions it.
50
50
  */
51
- announceToAgent?: boolean
51
+ announceToAgent?: Volatile<boolean>
52
52
  /** Master switch for the plugin (browser half + host announcement). */
53
- enabled?: boolean
53
+ enabled?: Volatile<boolean>
54
54
  /** Prevent idle system sleep while sessions run or schedules are armed. */
55
- preventIdleSleep?: boolean
56
- /** Canonical reverse-proxy Host authorities admitted with a server-side token. */
55
+ preventIdleSleep?: Volatile<boolean>
56
+ /**
57
+ * Canonical reverse-proxy Host authorities admitted with a server-side
58
+ * token. Deployment-level, so it stays an ordinary field: editing it reloads
59
+ * the row, which re-registers the routes with the new proxy access.
60
+ */
57
61
  trustedProxyHosts?: string[]
58
62
  /** Environment variable whose value the authenticated proxy injects upstream. */
59
63
  proxyTokenEnv?: string
@@ -65,15 +69,54 @@ export interface Config {
65
69
  sessionDefaultPermission?: TaskPermission
66
70
  }
67
71
 
68
- export const Config: z<Config> = z.object({
69
- announceToAgent: z.boolean().default(false),
70
- enabled: z.boolean().default(true),
71
- preventIdleSleep: z.boolean().default(false),
72
+ /**
73
+ * The schema is left to inference rather than annotated with `z<Config>`: a
74
+ * volatile field's parsed output is a `Volatile` reference while its accepted
75
+ * input stays the plain value, so the two sides no longer share one shape and
76
+ * the annotation would reject the schema the Host must be given.
77
+ */
78
+ export const Config = z.object({
79
+ announceToAgent: z.boolean().default(false).volatile(),
80
+ enabled: z.boolean().default(true).volatile(),
81
+ preventIdleSleep: z.boolean().default(false).volatile(),
72
82
  trustedProxyHosts: z.array(z.string()).default([]),
73
83
  proxyTokenEnv: z.string().min(1).default(DEFAULT_PROXY_TOKEN_ENV),
74
84
  sessionDefaultPermission: z.union(TASK_PERMISSIONS).default(DEFAULT_SESSION_PERMISSION),
75
85
  })
76
86
 
87
+ declare module '@deepseek-ai/cordis' {
88
+ interface Events {
89
+ /**
90
+ * Volatile config values were committed into the running fiber without a
91
+ * remount; dispatched to the owning fiber only. Spelled here because the
92
+ * Loader package is not a dependency of this plugin, with the Loader's own
93
+ * shape so the two declarations merge when a Host program carries both.
94
+ * @param paths - changed config paths as key arrays; every value is committed before dispatch.
95
+ * @mode emit
96
+ */
97
+ 'loader/volatile-update'(paths: readonly (readonly string[])[]): void
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Read one config field's current value.
103
+ *
104
+ * The Loader hands schema-volatile fields as stable references it commits in
105
+ * place, so a live value must be read at use time rather than captured when
106
+ * the plugin activates; a plain value (a programmatic mount, or a field the
107
+ * schema does not mark volatile) is returned as it stands.
108
+ * @param field - the config field as the Loader handed it.
109
+ * @param fallback - value to use when the field is absent.
110
+ * @returns the effective field value.
111
+ */
112
+ export function readConfigField<T>(field: Volatile<T> | T | undefined, fallback: T): T {
113
+ if (field === undefined) return fallback
114
+ if (typeof field === 'object' && field !== null && typeof (field as { get?: unknown }).get === 'function') {
115
+ return (field as Volatile<T>).get() as T
116
+ }
117
+ return field as T
118
+ }
119
+
77
120
  /** Resolve proxy access without ever placing the token value in plugin config. */
78
121
  export function resolveProxyAccess(config: Config | undefined, env: NodeJS.ProcessEnv = process.env): { trustedProxyHosts: string[]; proxyToken?: string } {
79
122
  const trustedProxyHosts = config?.trustedProxyHosts ?? []
@@ -107,16 +150,28 @@ export function resolveLlmRuntime(ctx: Context): LlmRuntime | undefined {
107
150
  }
108
151
 
109
152
  /**
110
- * Register the board's announcement section, gated on the composition entry's
111
- * `announceToAgent` (and the live settings value once the web settings
112
- * surface is served). The section is re-registered whenever the source
113
- * changes, so a settings edit takes effect without a restart.
153
+ * Activate the board's host half: ledger, routes, cron scheduler, power
154
+ * inhibitor, and the model-facing announcement.
155
+ *
156
+ * The effective settings are the config the Host hands this row. The three
157
+ * fields the browser card edits are schema-volatile, so the Loader commits an
158
+ * edit into the running fiber's references instead of remounting the row —
159
+ * `sync` therefore reads them at use time and follows
160
+ * `loader/volatile-update`, the event the Loader emits once it has committed
161
+ * them. Every other field reloads the row, which re-runs this activation.
114
162
  * @param ctx - the plugin context (systemPrompt injected).
115
163
  * @param config - resolved plugin config (schema defaults applied by the loader).
116
164
  */
117
165
  export const apply = mountOnce('@linxin666/dsh-client-ui-task-board', applyImpl)
118
166
 
119
167
  function applyImpl(ctx: Context, config?: Config): void {
168
+ /** Current master switch (the browser half reads the same field from its own form). */
169
+ const enabled = (): boolean => readConfigField(config?.enabled, true)
170
+ /** Current announcement flag. */
171
+ const announceToAgent = (): boolean => readConfigField(config?.announceToAgent, DEFAULT_ANNOUNCE)
172
+ /** Current idle-sleep protection flag. */
173
+ const preventIdleSleep = (): boolean => readConfigField(config?.preventIdleSleep, false)
174
+
120
175
  const host = new TaskBoardHostService(ctx.typertGateway, {
121
176
  workspaceRegistry: ctx.workspaceRegistry,
122
177
  sessionDefaultPermission: config?.sessionDefaultPermission ?? DEFAULT_SESSION_PERMISSION,
@@ -128,7 +183,9 @@ function applyImpl(ctx: Context, config?: Config): void {
128
183
  },
129
184
  },
130
185
  })
131
- host.setConfiguration(config?.enabled ?? true, config?.preventIdleSleep ?? false)
186
+ // Configuration before start(): a disabled row must not take the first
187
+ // scheduler tick, which would roll schedules the board is not running.
188
+ host.setConfiguration(enabled(), preventIdleSleep())
132
189
  host.start()
133
190
  ctx.effect(() => {
134
191
  const disposers: Array<() => void> = []
@@ -151,24 +208,27 @@ function applyImpl(ctx: Context, config?: Config): void {
151
208
  host.dispose()
152
209
  }
153
210
  }, 'task-board: host ledger, scheduler, and routes')
154
- // The live source the announcement reads: the settings section once the web
155
- // settings surface is served, the composition entry otherwise
156
- // (installSection swaps it when the namespace registers).
157
- let current: () => Config = () => config ?? {}
211
+
158
212
  let disposeSection: (() => void) | undefined
213
+ let applied: { enabled: boolean; announceToAgent: boolean; preventIdleSleep: boolean } | undefined
159
214
 
160
- // Register (or drop) the announcement to match the current source. The
161
- // section is kept under one disposer: re-registering first tears the old
162
- // one down so a duplicate-name registration never throws.
215
+ // Apply the current values to the host service and the announcement. A
216
+ // commit that changes nothing visible is a no-op, so following a coarse
217
+ // invalidation cannot churn the system-prompt registry. The section is kept
218
+ // under one disposer: re-registering first tears the old one down so a
219
+ // duplicate-name registration never throws.
163
220
  const sync = (): void => {
221
+ const next = { enabled: enabled(), announceToAgent: announceToAgent(), preventIdleSleep: preventIdleSleep() }
222
+ if (applied !== undefined && applied.enabled === next.enabled && applied.announceToAgent === next.announceToAgent && applied.preventIdleSleep === next.preventIdleSleep) {
223
+ return
224
+ }
225
+ applied = next
226
+ host.setConfiguration(next.enabled, next.preventIdleSleep)
164
227
  if (disposeSection !== undefined) {
165
228
  disposeSection()
166
229
  disposeSection = undefined
167
230
  }
168
- const active = current().enabled ?? true
169
- host.setConfiguration(active, current().preventIdleSleep ?? false)
170
- if (!active) return
171
- if ((current().announceToAgent ?? DEFAULT_ANNOUNCE) === false) return
231
+ if (!next.enabled || !next.announceToAgent) return
172
232
  disposeSection = ctx.systemPrompt.section({
173
233
  name: 'plugin:task-board',
174
234
  order: SECTION_ORDER,
@@ -176,24 +236,9 @@ function applyImpl(ctx: Context, config?: Config): void {
176
236
  })
177
237
  }
178
238
 
179
- ctx.inject(['settings'], (settingsCtx) => {
180
- try {
181
- if (typeof settingsCtx.settings?.installSection === 'function') {
182
- settingsCtx.settings.installSection(ctx, TASK_BOARD_SETTINGS_NAMESPACE, Config, config ?? {}, {
183
- setSource: (source) => { current = source },
184
- onChange: sync,
185
- })
186
- } else if (typeof settingsCtx.settings?.register === 'function') {
187
- const scope = settingsCtx.settings.register(TASK_BOARD_SETTINGS_NAMESPACE, Config, { base: config ?? {} })
188
- current = () => scope?.get?.() ?? (config ?? {})
189
- scope?.watch?.(() => { sync() })
190
- }
191
- } catch {
192
- // Defensive fallback against settings registration differences
193
- }
194
- })
195
-
196
- // Initial registration from the composition entry (covers deployments with
197
- // no settings service, whose installSection never fires its hooks).
239
+ // A settings edit of a volatile field is committed into the references this
240
+ // fiber already holds, with no remount and no second call to apply: the
241
+ // commit event is what makes the edit take effect without a restart.
242
+ ctx.on('loader/volatile-update', () => { sync() })
198
243
  sync()
199
244
  }