@linxin666/dsh-client-ui-task-board 0.3.24 → 0.4.1
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/icon.svg +3 -0
- package/lib/client.js +154 -115
- package/lib/client.js.map +1 -1
- package/lib/index.js +109 -898
- package/lib/types/client/PluginSettingsCard.d.ts +4 -0
- package/lib/types/client/TaskBoardSettingsCard.d.ts +5 -5
- package/lib/types/client/index.d.ts +43 -10
- package/lib/types/client/main-session.d.ts +32 -0
- package/lib/types/client/plugin-card-seat.d.ts +23 -16
- package/lib/types/client/settings-form.d.ts +17 -17
- package/lib/types/client/sidebar-entry-core.d.ts +0 -32
- package/lib/types/core/controller.d.ts +10 -8
- package/lib/types/http.d.ts +18 -1
- package/lib/types/index.d.ts +72 -17
- package/package.json +30 -25
- package/src/client/PluginSettingsCard.tsx +7 -2
- package/src/client/TaskBoardSettingsCard.tsx +7 -5
- package/src/client/board.module.css +31 -12
- package/src/client/index.ts +112 -23
- package/src/client/main-session.ts +43 -0
- package/src/client/plugin-card-seat.ts +24 -17
- package/src/client/settings-form.ts +34 -31
- package/src/client/sidebar-entry-core.ts +10 -94
- package/src/client/sidebar-entry.ts +2 -2
- package/src/core/controller.ts +12 -8
- package/src/http.ts +23 -1
- package/src/index.ts +97 -52
|
@@ -16,33 +16,6 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { subscribeBodyInvalidations } from './body-mutations.ts'
|
|
18
18
|
|
|
19
|
-
/**
|
|
20
|
-
* One trailing action button seated at the entry row's right edge (e.g. a
|
|
21
|
-
* refresh command or a collapse chevron). Rows with actions switch from the
|
|
22
|
-
* classic single-button structure to a container with a main button plus the
|
|
23
|
-
* action buttons (nested interactive elements are invalid HTML); rows without
|
|
24
|
-
* actions keep the classic structure untouched.
|
|
25
|
-
*/
|
|
26
|
-
export interface SidebarEntryAction {
|
|
27
|
-
/** Stable id stamped as data-dsh-entry-action="<id>" (CSS hooks and tests). */
|
|
28
|
-
id: string
|
|
29
|
-
/** Inline icon markup while the row is active (always, when inactiveIcon is unset). */
|
|
30
|
-
icon: string
|
|
31
|
-
/**
|
|
32
|
-
* Optional icon while the row is inactive. Setting it makes the action a
|
|
33
|
-
* state mirror (a collapse chevron): the core swaps the icon and stamps
|
|
34
|
-
* aria-expanded on every active-state change. Requires the active bridge.
|
|
35
|
-
*/
|
|
36
|
-
inactiveIcon?: string
|
|
37
|
-
/** Localized accessible label (aria-label + title); receives the active state. */
|
|
38
|
-
label(active: boolean): string
|
|
39
|
-
/**
|
|
40
|
-
* Click handler; receives the button so the caller can drive transient
|
|
41
|
-
* affordances (a refresh cooldown) without a re-render.
|
|
42
|
-
*/
|
|
43
|
-
onClick(button: HTMLButtonElement): void
|
|
44
|
-
}
|
|
45
|
-
|
|
46
19
|
/** Per-package configuration for one sidebar entry row. */
|
|
47
20
|
export interface SidebarEntryOptions {
|
|
48
21
|
/** Full attribute name identifying the injected row (idempotency key), e.g. 'data-dsh-ssh-entry'. */
|
|
@@ -74,12 +47,6 @@ export interface SidebarEntryOptions {
|
|
|
74
47
|
refresh?: { subscribe(listener: () => void): () => void }
|
|
75
48
|
/** Click action (open/toggle the owning panel). */
|
|
76
49
|
onToggle(): void
|
|
77
|
-
/**
|
|
78
|
-
* Optional trailing action buttons at the row's right edge. Requires the
|
|
79
|
-
* package CSS to seat entryMain / entryAction; empty keeps the classic
|
|
80
|
-
* single-button row.
|
|
81
|
-
*/
|
|
82
|
-
actions?: readonly SidebarEntryAction[]
|
|
83
50
|
/** Family-block position: 'before' inserts ahead of sibling plugin rows, 'after' behind them. */
|
|
84
51
|
position: 'before' | 'after'
|
|
85
52
|
/**
|
|
@@ -118,11 +85,9 @@ function newSessionButton(root: HTMLElement): HTMLButtonElement | undefined {
|
|
|
118
85
|
}
|
|
119
86
|
|
|
120
87
|
/** Build the entry row (detached; inserted once the shell is up). */
|
|
121
|
-
function createEntry(options: SidebarEntryOptions): { entry: HTMLElement; applyLabel: () => void
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
const entry = document.createElement(composite ? 'div' : 'button') as HTMLElement
|
|
125
|
-
if (!composite) (entry as HTMLButtonElement).type = 'button'
|
|
88
|
+
function createEntry(options: SidebarEntryOptions): { entry: HTMLElement; applyLabel: () => void } {
|
|
89
|
+
const entry = document.createElement('button')
|
|
90
|
+
entry.type = 'button'
|
|
126
91
|
entry.setAttribute(options.rowAttribute, '')
|
|
127
92
|
if (options.plugin !== undefined) {
|
|
128
93
|
entry.setAttribute('data-dsh-plugin', options.plugin)
|
|
@@ -134,62 +99,15 @@ function createEntry(options: SidebarEntryOptions): { entry: HTMLElement; applyL
|
|
|
134
99
|
const iconSpan = document.createElement('span')
|
|
135
100
|
iconSpan.className = options.css['entryIcon'] ?? ''
|
|
136
101
|
iconSpan.innerHTML = options.icon
|
|
137
|
-
|
|
138
|
-
// the row becomes a container and the main area its own button, so the row
|
|
139
|
-
// never nests interactive elements.
|
|
140
|
-
const main = composite ? document.createElement('button') : (entry as HTMLButtonElement)
|
|
141
|
-
if (composite) {
|
|
142
|
-
main.type = 'button'
|
|
143
|
-
main.className = options.css['entryMain'] ?? ''
|
|
144
|
-
main.append(iconSpan, labelSpan)
|
|
145
|
-
entry.append(main)
|
|
146
|
-
} else {
|
|
147
|
-
entry.append(iconSpan, labelSpan)
|
|
148
|
-
}
|
|
149
|
-
const actionButtons: { action: SidebarEntryAction; button: HTMLButtonElement }[] = []
|
|
150
|
-
for (const action of actions) {
|
|
151
|
-
const button = document.createElement('button')
|
|
152
|
-
button.type = 'button'
|
|
153
|
-
button.className = options.css['entryAction'] ?? ''
|
|
154
|
-
button.setAttribute('data-dsh-entry-action', action.id)
|
|
155
|
-
button.innerHTML = action.icon
|
|
156
|
-
button.addEventListener('click', () => { action.onClick(button) })
|
|
157
|
-
entry.append(button)
|
|
158
|
-
actionButtons.push({ action, button })
|
|
159
|
-
}
|
|
160
|
-
let open = false
|
|
161
|
-
const applyActions = (): void => {
|
|
162
|
-
for (const { action, button } of actionButtons) {
|
|
163
|
-
button.innerHTML = open || action.inactiveIcon === undefined ? action.icon : action.inactiveIcon
|
|
164
|
-
const text = action.label(open)
|
|
165
|
-
button.setAttribute('aria-label', text)
|
|
166
|
-
button.setAttribute('title', text)
|
|
167
|
-
if (action.inactiveIcon !== undefined) button.setAttribute('aria-expanded', String(open))
|
|
168
|
-
}
|
|
169
|
-
}
|
|
102
|
+
entry.append(iconSpan, labelSpan)
|
|
170
103
|
const applyLabel = (): void => {
|
|
171
|
-
|
|
172
|
-
if (options.tooltip !== undefined)
|
|
104
|
+
entry.setAttribute('aria-label', options.label())
|
|
105
|
+
if (options.tooltip !== undefined) entry.setAttribute('title', options.tooltip())
|
|
173
106
|
labelSpan.textContent = options.label()
|
|
174
|
-
applyActions()
|
|
175
107
|
}
|
|
176
108
|
applyLabel()
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
// The old row was clickable edge to edge; keep the padding between the
|
|
180
|
-
// main area and the action buttons a toggle target as well.
|
|
181
|
-
entry.addEventListener('click', (event) => {
|
|
182
|
-
if (event.target === entry) options.onToggle()
|
|
183
|
-
})
|
|
184
|
-
}
|
|
185
|
-
return {
|
|
186
|
-
entry,
|
|
187
|
-
applyLabel,
|
|
188
|
-
setOpen: (next: boolean) => {
|
|
189
|
-
open = next
|
|
190
|
-
applyActions()
|
|
191
|
-
},
|
|
192
|
-
}
|
|
109
|
+
entry.addEventListener('click', options.onToggle)
|
|
110
|
+
return { entry, applyLabel }
|
|
193
111
|
}
|
|
194
112
|
|
|
195
113
|
/** Re-insert the entry after the New Session row (before the browser region). */
|
|
@@ -231,7 +149,7 @@ export function mountSidebarEntry(options: SidebarEntryOptions): () => void {
|
|
|
231
149
|
if (typeof document !== 'undefined' && document.querySelector(options.rowSelector) !== null) {
|
|
232
150
|
return () => {}
|
|
233
151
|
}
|
|
234
|
-
const { entry, applyLabel
|
|
152
|
+
const { entry, applyLabel } = createEntry(options)
|
|
235
153
|
let root: HTMLElement | undefined
|
|
236
154
|
let placed = false
|
|
237
155
|
let unsubscribeRefresh: (() => void) | undefined
|
|
@@ -299,10 +217,8 @@ export function mountSidebarEntry(options: SidebarEntryOptions): () => void {
|
|
|
299
217
|
// row permanently highlighted — delete the attribute instead.
|
|
300
218
|
const unsubscribeActive = options.active === undefined ? undefined : (() => {
|
|
301
219
|
const syncActive = (): void => {
|
|
302
|
-
|
|
303
|
-
if (open) entry.dataset.active = 'true'
|
|
220
|
+
if (options.active!.isOpen()) entry.dataset.active = 'true'
|
|
304
221
|
else delete entry.dataset.active
|
|
305
|
-
setOpen(open)
|
|
306
222
|
}
|
|
307
223
|
const unsubscribe = options.active.subscribe(syncActive)
|
|
308
224
|
syncActive()
|
|
@@ -19,8 +19,8 @@ import { mountSidebarEntry as mountSharedSidebarEntry } from './sidebar-entry-co
|
|
|
19
19
|
/** Stable data attribute identifying the injected entry row. */
|
|
20
20
|
export const ENTRY_SELECTOR = '[data-dsh-taskboard-entry]'
|
|
21
21
|
|
|
22
|
-
/** Inline icon normalized to the shell's
|
|
23
|
-
const ICON = '<svg viewBox="0 0 16 16" width="
|
|
22
|
+
/** Inline icon normalized to the shell's 16px navigation glyph size. */
|
|
23
|
+
const ICON = '<svg viewBox="0 0 16 16" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2" y="2.5" width="12" height="11" rx="1.5"/><path d="M2 6.5h12M6.5 6.5v7"/></svg>'
|
|
24
24
|
|
|
25
25
|
/** Locale-change subscription the shared core asks for (ctx.locale.subscribe shape). */
|
|
26
26
|
export interface LocaleRefreshSource { subscribe(listener: () => void): () => void }
|
package/src/core/controller.ts
CHANGED
|
@@ -42,18 +42,22 @@ export interface TaskBoardTransport {
|
|
|
42
42
|
parseDraft?(request: TaskBoardParseRequest, signal?: AbortSignal): Promise<TaskBoardParseDraft>
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
/**
|
|
45
|
+
/**
|
|
46
|
+
* The sessions face the controller needs for navigation awareness. The Client
|
|
47
|
+
* Session Controller carries no global selection since 0.1.6-alpha.2, so the
|
|
48
|
+
* wiring resolves the main-view Session once and exposes it as current().
|
|
49
|
+
*/
|
|
46
50
|
export interface SessionsControllerFace {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
51
|
-
/** Select a session as current (navigates the conversation view). */
|
|
51
|
+
/** The Session the main view currently shows, when any. */
|
|
52
|
+
current(): string | undefined
|
|
53
|
+
/** Navigate the main view to a Session. */
|
|
52
54
|
open(id: string): void
|
|
55
|
+
/** Subscribe to catalog changes; selection ownership rides the same snapshot. */
|
|
56
|
+
subscribe(fn: () => void): () => void
|
|
53
57
|
}
|
|
54
58
|
|
|
55
59
|
function currentOf(sessions: SessionsControllerFace | undefined): string | undefined {
|
|
56
|
-
return sessions?.
|
|
60
|
+
return sessions?.current()
|
|
57
61
|
}
|
|
58
62
|
|
|
59
63
|
/** Controller dependencies (all swappable in tests). */
|
|
@@ -194,7 +198,7 @@ export class BoardController {
|
|
|
194
198
|
this.notify()
|
|
195
199
|
}) : undefined
|
|
196
200
|
if (unsubscribeExternal !== undefined) this.disposers.push(unsubscribeExternal)
|
|
197
|
-
this.disposers.push(this.deps.sessions.
|
|
201
|
+
this.disposers.push(this.deps.sessions.subscribe(() => {
|
|
198
202
|
this.onSessionsChanged()
|
|
199
203
|
}))
|
|
200
204
|
this.notify()
|
package/src/http.ts
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Shared JSON body/response helpers for the host route families: one strict
|
|
4
4
|
* bounded body reader, one lenient bounded body reader, one JSON object
|
|
5
|
-
* narrow,
|
|
5
|
+
* narrow, one JSON writer, and the outbound request-init helper that keeps a
|
|
6
|
+
* host fetch body decodable (withIdentityEncoding). Previously the readers
|
|
7
|
+
* were copy-pasted across the
|
|
6
8
|
* package route files (routes.ts, update-routes.ts, mobile-api.ts, and each
|
|
7
9
|
* family's route module) with drifting contracts: body caps ranging 4 KiB to
|
|
8
10
|
* 1 MiB and four distinct overflow behaviors (reject, undefined, null, throw).
|
|
@@ -88,6 +90,26 @@ export function asJsonObject(value: unknown): Record<string, unknown> | undefine
|
|
|
88
90
|
return isJsonObject(value) ? value : undefined
|
|
89
91
|
}
|
|
90
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Request init that asks a remote origin for an uncompressed body.
|
|
95
|
+
*
|
|
96
|
+
* The DSH host boots `@deepseek-ai/dsh-http-proxy`, whose top-level import of
|
|
97
|
+
* the npm `undici` copy replaces the legacy `undici.globalDispatcher.1` slot
|
|
98
|
+
* Node's built-in `fetch()` reads. That cross-major wrapper drops the
|
|
99
|
+
* `content-encoding` header and automatic decompression, so a plain fetch
|
|
100
|
+
* resolves to raw gzip/brotli/zstd bytes — compressed noise no JSON or text
|
|
101
|
+
* consumer can read. Every host fetch that parses a remote body must request
|
|
102
|
+
* identity encoding. Caller headers survive; `accept-encoding` is forced to
|
|
103
|
+
* `identity` because a caller value has no decoder behind it in this host.
|
|
104
|
+
* @param init - the caller's request init (signal, headers, method, ...).
|
|
105
|
+
* @returns a copy with `accept-encoding: identity` merged in.
|
|
106
|
+
*/
|
|
107
|
+
export function withIdentityEncoding(init: RequestInit = {}): RequestInit {
|
|
108
|
+
const headers = new Headers(init.headers)
|
|
109
|
+
headers.set('accept-encoding', 'identity')
|
|
110
|
+
return { ...init, headers }
|
|
111
|
+
}
|
|
112
|
+
|
|
91
113
|
/**
|
|
92
114
|
* Write one JSON response. Default headers are the family defaults
|
|
93
115
|
* (content-type and referrer-policy); caller headers are appended or
|
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
|
|
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
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
161
|
-
//
|
|
162
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
}
|