@linxin666/dsh-client-ui-task-board 0.4.1 → 0.4.3
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/README.i18n.yaml +2 -2
- package/README.md +28 -3
- package/README.zh.md +28 -3
- package/lib/client.js +1142 -467
- package/lib/client.js.map +1 -1
- package/lib/index.js +2066 -1475
- package/lib/types/client/TaskBoardSettingsCard.d.ts +4 -0
- package/lib/types/client/board/LinkSubtaskModal.d.ts +11 -0
- package/lib/types/client/board/NewTaskModal.d.ts +7 -1
- package/lib/types/client/board/TaskCard.d.ts +11 -1
- package/lib/types/client/index.d.ts +15 -1
- package/lib/types/client/locales.d.ts +33 -0
- package/lib/types/client/native-panel.d.ts +62 -0
- package/lib/types/client/settings-entry-form.d.ts +49 -0
- package/lib/types/core/controller.d.ts +71 -3
- package/lib/types/core/subtask.d.ts +122 -0
- package/lib/types/core/tasks.d.ts +46 -3
- package/lib/types/core/use-cases/task-archive.d.ts +10 -13
- package/lib/types/core/use-cases/task-create.d.ts +13 -4
- package/lib/types/core/use-cases/task-delete.d.ts +7 -1
- package/lib/types/core/use-cases/task-parent.d.ts +21 -0
- package/lib/types/core/use-cases/task-update.d.ts +1 -1
- package/lib/types/host/agent-tools.d.ts +27 -0
- package/lib/types/host-ledger.d.ts +97 -4
- package/lib/types/host-runner.d.ts +39 -1
- package/lib/types/host-service.d.ts +98 -5
- package/lib/types/index.d.ts +93 -5
- package/lib/types/mount-once.d.ts +38 -7
- package/lib/types/protocol.d.ts +14 -0
- package/package.json +26 -21
- package/src/client/TaskBoardSettingsCard.tsx +48 -3
- package/src/client/board/LinkSubtaskModal.tsx +82 -0
- package/src/client/board/NewTaskModal.tsx +40 -6
- package/src/client/board/TaskBoard.tsx +88 -8
- package/src/client/board/TaskCard.tsx +52 -2
- package/src/client/board/TaskDetail.tsx +124 -1
- package/src/client/board.module.css +112 -131
- package/src/client/host-api.ts +14 -4
- package/src/client/index.ts +56 -18
- package/src/client/locales.ts +70 -2
- package/src/client/native-panel.tsx +124 -0
- package/src/client/settings-entry-form.ts +142 -0
- package/src/core/controller.ts +128 -10
- package/src/core/store.ts +15 -0
- package/src/core/subtask.ts +286 -0
- package/src/core/tasks.ts +50 -2
- package/src/core/use-cases/task-archive.ts +46 -12
- package/src/core/use-cases/task-create.ts +42 -7
- package/src/core/use-cases/task-delete.ts +11 -1
- package/src/core/use-cases/task-parent.ts +64 -0
- package/src/core/use-cases/task-update.ts +3 -1
- package/src/host/agent-tools.ts +643 -0
- package/src/host-ledger.ts +343 -45
- package/src/host-routes.ts +37 -1
- package/src/host-runner.ts +141 -12
- package/src/host-service.ts +265 -32
- package/src/index.ts +203 -9
- package/src/mount-once.ts +107 -13
- package/src/protocol.ts +26 -2
- package/lib/types/client/board-mount.d.ts +0 -23
- package/lib/types/client/body-mutations.d.ts +0 -45
- package/lib/types/client/panel-mount-core.d.ts +0 -56
- package/lib/types/client/sidebar-entry-core.d.ts +0 -55
- package/lib/types/client/sidebar-entry.d.ts +0 -29
- package/src/client/board-mount.tsx +0 -43
- package/src/client/body-mutations.ts +0 -133
- package/src/client/panel-mount-core.ts +0 -155
- package/src/client/sidebar-entry-core.ts +0 -237
- package/src/client/sidebar-entry.ts +0 -53
package/src/client/index.ts
CHANGED
|
@@ -24,17 +24,17 @@ import type {} from '@deepseek-ai/dsh-client-ui-settings/client'
|
|
|
24
24
|
// Type-only: pulls the workspace plugin's Context merge (ctx.uiWorkspace), the
|
|
25
25
|
// multi-instance navigation face that replaced ISessions.open().
|
|
26
26
|
import type {} from '@deepseek-ai/dsh-client-ui-workspace/client'
|
|
27
|
-
import { BoardController } from '../core/controller.ts'
|
|
27
|
+
import { BoardController, TASK_BOARD_PANEL_ID } from '../core/controller.ts'
|
|
28
28
|
import { mainViewSessionId } from './main-session.ts'
|
|
29
29
|
import { LocalStorageTaskStore } from '../core/store.ts'
|
|
30
30
|
import { claimTaskboardApply, releaseTaskboardApply } from './apply-guard.ts'
|
|
31
|
-
import {
|
|
32
|
-
import { mountSidebarEntry } from './sidebar-entry.ts'
|
|
31
|
+
import { registerTaskBoardPanel } from './native-panel.tsx'
|
|
33
32
|
import { TaskBoardSettingsCard, TaskBoardSettingsCardController, type TaskBoardSettings } from './TaskBoardSettingsCard.tsx'
|
|
34
33
|
import { en, zh, setRuntimeTranslate, type TaskBoardKey } from './locales.ts'
|
|
35
34
|
import { HttpTaskBoardHostTransport } from './host-api.ts'
|
|
36
35
|
import { reportDailyHeartbeat } from './telemetry.ts'
|
|
37
36
|
import { installPluginCard } from './plugin-card-seat.ts'
|
|
37
|
+
import { createServedEntryForm } from './settings-entry-form.ts'
|
|
38
38
|
|
|
39
39
|
/** Locale namespace this plugin owns. */
|
|
40
40
|
const NS = 'task-board'
|
|
@@ -124,7 +124,7 @@ declare module '@deepseek-ai/cordis' {
|
|
|
124
124
|
* on hosts below that cohort, which serve the same roster through the
|
|
125
125
|
* connection RPC face.
|
|
126
126
|
*/
|
|
127
|
-
export const inject = ['slots', 'sessions', 'workspaces', 'connection', 'configForms', 'locale', 'remote', 'remote.session', 'uiWorkspace']
|
|
127
|
+
export const inject = ['slots', 'sessions', 'workspaces', 'connection', 'configForms', 'locale', 'remote', 'remote.session', 'uiWorkspace', 'layout']
|
|
128
128
|
|
|
129
129
|
/** One agent-preset row the mode picker consumes (either face's wire shape). */
|
|
130
130
|
interface PresetRosterRow {
|
|
@@ -241,6 +241,18 @@ export function apply(ctx: ClientContext): void {
|
|
|
241
241
|
const controller = new BoardController({
|
|
242
242
|
store,
|
|
243
243
|
transport: new HttpTaskBoardHostTransport(),
|
|
244
|
+
// Panel navigation belongs to the layout service: the board asks it to
|
|
245
|
+
// select its own panel id (or the conversation) instead of taking over
|
|
246
|
+
// the center column at the DOM level. The lookup stays lazy so a
|
|
247
|
+
// selection issued before the service settles still lands once it does,
|
|
248
|
+
// and the face throws by contract before the layout root entry mounts,
|
|
249
|
+
// which the controller tolerates.
|
|
250
|
+
panel: {
|
|
251
|
+
select: panelId => {
|
|
252
|
+
const layout = ctx.get('layout') as { selectPanel?: (id: string | null) => void } | undefined
|
|
253
|
+
layout?.selectPanel?.(panelId)
|
|
254
|
+
},
|
|
255
|
+
},
|
|
244
256
|
sessions: {
|
|
245
257
|
// The main-view Session comes from the catalog's per-source ownership
|
|
246
258
|
// counts; navigation belongs to the workspace UI since the
|
|
@@ -362,12 +374,24 @@ export function apply(ctx: ClientContext): void {
|
|
|
362
374
|
}
|
|
363
375
|
void pushModelOptions()
|
|
364
376
|
disposers.push(ctx.on('connection/reset', () => { void pushModelOptions() }))
|
|
377
|
+
// Native panel surfaces: one sidebar row and one center-column page, both
|
|
378
|
+
// through the official slot seats the shell itself renders. The layout's
|
|
379
|
+
// panel selection is reconciled back into the controller so the board's own
|
|
380
|
+
// view state follows the user clicking another panel row.
|
|
365
381
|
try {
|
|
366
|
-
disposers.push(
|
|
367
|
-
|
|
382
|
+
disposers.push(registerTaskBoardPanel(ctx, controller))
|
|
383
|
+
const layoutFace = ctx.get('layout') as { panelInfo?: { subscribe(listener: () => void): () => void; getSnapshot(): { activePanelId: unknown } } } | undefined
|
|
384
|
+
if (layoutFace?.panelInfo !== undefined) {
|
|
385
|
+
const sync = (): void => {
|
|
386
|
+
const active = layoutFace.panelInfo!.getSnapshot().activePanelId
|
|
387
|
+
controller.syncPanelSelection(active === TASK_BOARD_PANEL_ID ? TASK_BOARD_PANEL_ID : null)
|
|
388
|
+
}
|
|
389
|
+
sync()
|
|
390
|
+
disposers.push(layoutFace.panelInfo.subscribe(sync))
|
|
391
|
+
}
|
|
368
392
|
} catch (error) {
|
|
369
|
-
//
|
|
370
|
-
console.error('[dsh-task-board]
|
|
393
|
+
// A shell that cannot serve the seats degrades the board, never the GUI.
|
|
394
|
+
console.error('[dsh-task-board] panel registration failed:', error)
|
|
371
395
|
}
|
|
372
396
|
|
|
373
397
|
uiDisposer = () => {
|
|
@@ -384,7 +408,17 @@ export function apply(ctx: ClientContext): void {
|
|
|
384
408
|
if (enabled) mountUi()
|
|
385
409
|
else uiDisposer?.()
|
|
386
410
|
}
|
|
387
|
-
settingsForm.subscribe(syncEnabled)
|
|
411
|
+
const unsubscribeSettings = settingsForm.subscribe(syncEnabled)
|
|
412
|
+
// The mounted surfaces and the settings subscription belong to this fiber.
|
|
413
|
+
// The loader replaces a rebuilt bundle IN PLACE (fiber teardown and re-apply
|
|
414
|
+
// without a page reload), and that same teardown removes the dictionaries
|
|
415
|
+
// this row and board render through — a row left behind would stay visible
|
|
416
|
+
// showing raw dictionary keys, and a surviving subscription could mount a
|
|
417
|
+
// second one from a dead instance.
|
|
418
|
+
ctx.effect(() => () => {
|
|
419
|
+
unsubscribeSettings()
|
|
420
|
+
uiDisposer?.()
|
|
421
|
+
}, 'task-board: DOM surfaces')
|
|
388
422
|
syncEnabled()
|
|
389
423
|
}
|
|
390
424
|
|
|
@@ -405,22 +439,26 @@ export function bindSettingsForm(ctx: ClientContext): ConfigForm<TaskBoardSettin
|
|
|
405
439
|
if (binder !== undefined && typeof binder.bind === 'function') {
|
|
406
440
|
return binder.bind<TaskBoardSettings>({ namespace: TASK_BOARD_NS })
|
|
407
441
|
}
|
|
408
|
-
return
|
|
442
|
+
return createServedEntryForm<TaskBoardSettings>({
|
|
443
|
+
forms: ctx.configForms,
|
|
444
|
+
entryIds: TASK_BOARD_ENTRY_IDS,
|
|
445
|
+
})
|
|
409
446
|
}
|
|
410
447
|
|
|
411
448
|
/**
|
|
412
|
-
* The profile entry id this package's own row carries
|
|
449
|
+
* The profile entry id this package's own row carries, for a page that serves
|
|
450
|
+
* no family binder.
|
|
413
451
|
*
|
|
414
452
|
* The shared describe mirror is the only local evidence of which row id this
|
|
415
|
-
* profile actually serves, but it answers asynchronously: at plugin
|
|
416
|
-
*
|
|
417
|
-
*
|
|
418
|
-
*
|
|
419
|
-
*
|
|
453
|
+
* profile actually serves, but it answers asynchronously: at plugin activation
|
|
454
|
+
* it usually holds nothing yet. An unanswered mirror therefore binds the
|
|
455
|
+
* aggregate row id rather than guessing among the candidates, and the binding
|
|
456
|
+
* is re-resolved once the mirror answers — see
|
|
457
|
+
* {@link createServedEntryForm}, which owns that rebinding.
|
|
420
458
|
* @param forms - the shared configuration forms service.
|
|
421
|
-
* @returns the entry id to bind.
|
|
459
|
+
* @returns the entry id to bind before the mirror answers.
|
|
422
460
|
*/
|
|
423
|
-
function servedEntryId(forms: ConfigForms): string {
|
|
461
|
+
export function servedEntryId(forms: ConfigForms): string {
|
|
424
462
|
let served: readonly string[] | undefined
|
|
425
463
|
try {
|
|
426
464
|
served = forms.describe().getSnapshot().view?.namespaces.map(view => view.ns)
|
package/src/client/locales.ts
CHANGED
|
@@ -157,12 +157,16 @@ export const zh = {
|
|
|
157
157
|
'exec.mode.brokenSuffix': '(不可用)',
|
|
158
158
|
'exec.mode.removed': '(已移除)',
|
|
159
159
|
'exec.permission.default': '会话默认',
|
|
160
|
+
'exec.permission.inheritParent': '继承父任务({permission})',
|
|
160
161
|
'exec.permission.read-only': '只读',
|
|
161
162
|
'exec.permission.workspace-write': '工作区可写',
|
|
162
163
|
'exec.permission.danger-full-access': '完全访问',
|
|
163
164
|
'exec.model.default': '宿主默认(agent-default-model)',
|
|
164
165
|
'exec.model.unknown': '(未知模型/回退默认)',
|
|
165
166
|
'exec.reuseSession': '在同一对话继续',
|
|
167
|
+
'exec.teamRun': '用 Agent Team 执行(Leader/teammate)',
|
|
168
|
+
'exec.teamRunHint': '开启后:执行本任务只启动一个 Leader 会话,每个子任务由它派生为 teammate 并在该会话内并行工作(子任务自身的权限钉住不适用)。关闭则每个成员各开独立会话。',
|
|
169
|
+
'exec.teamRunUnavailable': '当前部署未提供 Agent Teams 服务,无法启用。',
|
|
166
170
|
'exec.reuseSessionHint': '开启后,本任务的后续执行在上一次会话里继续(该会话空闲且仍存在时),不再每次新建对话;每次复用时都会重新应用上面钉住的权限与模型。',
|
|
167
171
|
'detail.executionSettings': '执行设置',
|
|
168
172
|
'exec.hint': '执行时生效:工作区决定执行会话落在哪个工作区;模式决定会话的 agent 预设;权限经 /permission 命令应用到会话。留空则使用运行时默认。',
|
|
@@ -170,7 +174,7 @@ export const zh = {
|
|
|
170
174
|
'settings.title': '任务看板',
|
|
171
175
|
'settings.description': '控制 Host 任务看板、agent 播报与运行期间的系统空闲睡眠保护。',
|
|
172
176
|
'settings.enabled': '启用任务看板',
|
|
173
|
-
'settings.enabledHint': '
|
|
177
|
+
'settings.enabledHint': '关闭后隐藏侧边栏入口与看板视图,并注销 task_board_* agent 工具;开启时任何会话都可以通过这些工具读写看板、子任务与定时计划。',
|
|
174
178
|
'settings.announceToAgent': '向 agent 播报任务看板',
|
|
175
179
|
'settings.announceToAgentHint': '开启:每条 agent 系统提示都会包含本看板的说明;关闭:不播报,agent 仅在用户主动提及时了解看板。',
|
|
176
180
|
'settings.preventIdleSleep': '阻止系统空闲睡眠',
|
|
@@ -194,6 +198,36 @@ export const zh = {
|
|
|
194
198
|
'settings.unsaved': '未保存',
|
|
195
199
|
'settings.saveFailed': '部署未接受这些值,已保留供你修改。',
|
|
196
200
|
'settings.invalidNumber': '请输入数字,留空则使用默认值。',
|
|
201
|
+
'settings.maxSubtaskDepth': '子任务深度上限',
|
|
202
|
+
'settings.maxSubtaskDepthHint': '默认 1:一个任务只允许一层子任务,子任务不能再创建或关联子任务。最大 3。执行父任务会并发执行它的整棵子任务树,层级越深,一次执行开启的会话越多。',
|
|
203
|
+
'settings.maxSubtaskDepthOption': '{depth} 层',
|
|
204
|
+
// 子任务(父子关系、级联执行、关联/解除关联)。
|
|
205
|
+
'detail.parent': '父任务',
|
|
206
|
+
'detail.parent.open': '打开父任务',
|
|
207
|
+
'detail.subtasks': '子任务',
|
|
208
|
+
'detail.subtasks.empty': '还没有子任务',
|
|
209
|
+
'detail.subtasks.add': '添加子任务',
|
|
210
|
+
'detail.subtasks.link': '关联现有任务',
|
|
211
|
+
'detail.subtasks.detach': '解除关联',
|
|
212
|
+
'detail.subtasks.runningLock': '子任务正在执行,结算后才能解除关联或改挂父任务',
|
|
213
|
+
'detail.subtasks.runHint': '执行本任务会同时并发执行它的 {count} 个子任务;子任务失败不影响其它子任务继续执行。',
|
|
214
|
+
'detail.subtasks.waiting': '等待子任务结算',
|
|
215
|
+
'detail.subtasks.depthLimit': '当前子任务深度上限为 {depth} 层,子任务不能再创建或关联子任务。',
|
|
216
|
+
'detail.subtasks.hostOnly': '连接 Host 后才能创建或关联子任务。',
|
|
217
|
+
'new.subtaskTitle': '新建子任务',
|
|
218
|
+
'new.subtaskInherit': '未单独选择的执行设置继承父任务(工作区、模式、权限、模型),可在此覆盖。',
|
|
219
|
+
'link.subtask.title': '关联现有任务',
|
|
220
|
+
'link.subtask.hint': '只能关联没有父任务的在看板任务;关联后它成为本任务的子任务。',
|
|
221
|
+
'link.subtask.empty': '没有可关联的任务',
|
|
222
|
+
'link.subtask.confirm': '关联',
|
|
223
|
+
'card.subtask': '子任务',
|
|
224
|
+
'card.subtasks': '{count} 子任务',
|
|
225
|
+
'card.subtasksFailed': '{count} 失败',
|
|
226
|
+
'card.subtasksRunning': '{count} 运行中',
|
|
227
|
+
'card.subtasksBreakdown': '子任务 {total}:已完成 {done},运行中 {running},失败 {failed}',
|
|
228
|
+
'board.hideSubtasks': '隐藏子任务',
|
|
229
|
+
'board.showSubtasks': '显示子任务',
|
|
230
|
+
'board.subtaskFilterHint': '看板默认只显示父任务;搜索或按标签筛选时会自动展开子任务。',
|
|
197
231
|
} satisfies Record<string, string>
|
|
198
232
|
|
|
199
233
|
/** en dictionary, complete against the zh key set. */
|
|
@@ -348,12 +382,16 @@ export const en: Record<keyof typeof zh, string> = {
|
|
|
348
382
|
'exec.mode.brokenSuffix': ' (unavailable)',
|
|
349
383
|
'exec.mode.removed': ' (removed)',
|
|
350
384
|
'exec.permission.default': 'Session default',
|
|
385
|
+
'exec.permission.inheritParent': 'Inherit parent ({permission})',
|
|
351
386
|
'exec.permission.read-only': 'Read-only',
|
|
352
387
|
'exec.permission.workspace-write': 'Workspace Write',
|
|
353
388
|
'exec.permission.danger-full-access': 'Full Access',
|
|
354
389
|
'exec.model.default': 'Host default (agent-default-model)',
|
|
355
390
|
'exec.model.unknown': ' (unknown / fallback to default)',
|
|
356
391
|
'exec.reuseSession': 'Continue in the same conversation',
|
|
392
|
+
'exec.teamRun': 'Run with an Agent Team (Lead + teammates)',
|
|
393
|
+
'exec.teamRunHint': 'On: running this task starts one Lead session and spawns a teammate per subtask inside it, working in parallel (a subtask permission pin does not apply). Off: every member gets its own independent session.',
|
|
394
|
+
'exec.teamRunUnavailable': 'This deployment serves no Agent Teams service, so team runs cannot be enabled.',
|
|
357
395
|
'exec.reuseSessionHint': 'When on, later runs continue in the previous session (when that session is idle and still exists) instead of starting a new conversation each time; the pinned permission and model above are re-applied on every reuse.',
|
|
358
396
|
'detail.executionSettings': 'Execution Settings',
|
|
359
397
|
'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.',
|
|
@@ -361,7 +399,7 @@ export const en: Record<keyof typeof zh, string> = {
|
|
|
361
399
|
'settings.title': 'Task Board',
|
|
362
400
|
'settings.description': 'Configure the Host task board, agent announcement, and idle-system-sleep protection while work is pending.',
|
|
363
401
|
'settings.enabled': 'Enable the task board',
|
|
364
|
-
'settings.enabledHint': 'When off, the sidebar entry and board view are hidden.',
|
|
402
|
+
'settings.enabledHint': 'When off, the sidebar entry and board view are hidden and the task_board_* agent tools are unregistered; while on, any session can read and drive the board, its subtasks and its schedules through those tools.',
|
|
365
403
|
'settings.announceToAgent': 'Announce the task board to agents',
|
|
366
404
|
'settings.announceToAgentHint': 'On: every agent system prompt includes a note about this board. Off: no announcement; agents learn about the board only when you mention it.',
|
|
367
405
|
'settings.preventIdleSleep': 'Prevent idle system sleep',
|
|
@@ -385,6 +423,36 @@ export const en: Record<keyof typeof zh, string> = {
|
|
|
385
423
|
'settings.unsaved': 'Unsaved',
|
|
386
424
|
'settings.saveFailed': 'The deployment did not accept these values; they were left for you to correct.',
|
|
387
425
|
'settings.invalidNumber': 'Enter a number, or leave blank to use the default.',
|
|
426
|
+
'settings.maxSubtaskDepth': 'Subtask depth limit',
|
|
427
|
+
'settings.maxSubtaskDepthHint': 'Default 1: a task may carry one level of subtasks, and a subtask cannot create or link further subtasks. Maximum 3. Running a task also runs its whole subtask tree concurrently, and every extra level opens more sessions per run.',
|
|
428
|
+
'settings.maxSubtaskDepthOption': '{depth} levels',
|
|
429
|
+
// Subtasks (parent/child links, cascade runs, link/detach).
|
|
430
|
+
'detail.parent': 'Parent task',
|
|
431
|
+
'detail.parent.open': 'Open parent task',
|
|
432
|
+
'detail.subtasks': 'Subtasks',
|
|
433
|
+
'detail.subtasks.empty': 'No subtasks yet',
|
|
434
|
+
'detail.subtasks.add': 'Add subtask',
|
|
435
|
+
'detail.subtasks.link': 'Link existing task',
|
|
436
|
+
'detail.subtasks.detach': 'Detach',
|
|
437
|
+
'detail.subtasks.runningLock': 'This subtask is running; it can be detached or re-linked once it settles',
|
|
438
|
+
'detail.subtasks.runHint': 'Running this task also runs its {count} subtasks concurrently; one subtask failing does not stop the others.',
|
|
439
|
+
'detail.subtasks.waiting': 'Waiting for subtasks',
|
|
440
|
+
'detail.subtasks.depthLimit': 'The subtask depth limit is {depth}; a subtask cannot create or link further subtasks.',
|
|
441
|
+
'detail.subtasks.hostOnly': 'Connect to the Host to create or link subtasks.',
|
|
442
|
+
'new.subtaskTitle': 'New Subtask',
|
|
443
|
+
'new.subtaskInherit': 'Execution settings left unset inherit the parent (workspace, mode, permission, model); override any of them here.',
|
|
444
|
+
'link.subtask.title': 'Link an existing task',
|
|
445
|
+
'link.subtask.hint': 'Only an on-board task without a parent can be linked; it then becomes a subtask of this task.',
|
|
446
|
+
'link.subtask.empty': 'No task can be linked',
|
|
447
|
+
'link.subtask.confirm': 'Link',
|
|
448
|
+
'card.subtask': 'subtask',
|
|
449
|
+
'card.subtasks': '{count} subtasks',
|
|
450
|
+
'card.subtasksFailed': '{count} failed',
|
|
451
|
+
'card.subtasksRunning': '{count} running',
|
|
452
|
+
'card.subtasksBreakdown': 'Subtasks {total}: {done} done, {running} running, {failed} failed',
|
|
453
|
+
'board.hideSubtasks': 'Hide subtasks',
|
|
454
|
+
'board.showSubtasks': 'Show subtasks',
|
|
455
|
+
'board.subtaskFilterHint': 'The board shows parent tasks only; a text or label filter reveals the subtasks automatically.',
|
|
388
456
|
}
|
|
389
457
|
|
|
390
458
|
/** The dictionary key union. */
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native panel registration for the task board.
|
|
3
|
+
*
|
|
4
|
+
* The board is an official-style center-column panel, not a DOM takeover: it
|
|
5
|
+
* contributes a row into the sidebar shell's own global panel list
|
|
6
|
+
* (`sidebar.panellist`) and its page into the layout's keyed `main` slot. The
|
|
7
|
+
* shell then owns the row box, the label, the active highlight, the collapsed
|
|
8
|
+
* rail, the panel switch and the window-chrome interplay exactly as it does for
|
|
9
|
+
* the shipped Plugins and Schedule pages, so the board renders through the same
|
|
10
|
+
* container the desktop application's Plugins page uses.
|
|
11
|
+
*
|
|
12
|
+
* Both registrations go through `ctx.slots.inject`, which fires only once the
|
|
13
|
+
* owning shell entry has declared the seat: load order between this plugin and
|
|
14
|
+
* ui-layout / ui-sidebar therefore does not matter, and neither does a shell
|
|
15
|
+
* that cannot serve the seats (the callback simply never runs).
|
|
16
|
+
*
|
|
17
|
+
* @module @linxin666/dsh-client-ui-task-board/client
|
|
18
|
+
*/
|
|
19
|
+
import type { Context as ClientContext } from '@deepseek-ai/cordis'
|
|
20
|
+
import type {} from '@deepseek-ai/dsh-client-ui-slots'
|
|
21
|
+
import type {} from '@deepseek-ai/dsh-client-ui-layout/client'
|
|
22
|
+
import type {} from '@deepseek-ai/dsh-client-ui-sidebar/client'
|
|
23
|
+
import { TASK_BOARD_PANEL_ID, type BoardController } from '../core/controller.ts'
|
|
24
|
+
import { TaskBoard } from './board/TaskBoard.tsx'
|
|
25
|
+
import { t } from './locales.ts'
|
|
26
|
+
import css from './board.module.css'
|
|
27
|
+
|
|
28
|
+
/** The panel id shared by the sidebar row and the main-slot occupant. */
|
|
29
|
+
export { TASK_BOARD_PANEL_ID }
|
|
30
|
+
|
|
31
|
+
/** Row order among the shell's global panel rows (Plugins is 0, Schedule 10). */
|
|
32
|
+
const PANEL_ORDER = 20
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The sidebar row glyph the shell asks for at its own size and active state.
|
|
36
|
+
* The shell owns the button, label, tooltip and rail geometry; this component
|
|
37
|
+
* draws only the glyph, like every other panel row.
|
|
38
|
+
*
|
|
39
|
+
* The glyph carries `data-dsh-panel-entry` because it is the only DOM the
|
|
40
|
+
* panel's own code owns inside that shell-owned row: the L2 contract (skins)
|
|
41
|
+
* resolves which row belongs to which plugin through it, since the shell
|
|
42
|
+
* stamps no per-entry hook of its own (see contracts/semantic-attrs-v1.md).
|
|
43
|
+
* @param props - the shell's icon share: square edge and selection state.
|
|
44
|
+
* @returns the decorative board glyph.
|
|
45
|
+
*/
|
|
46
|
+
export function TaskBoardPanelIcon({ size }: { size: number; active: boolean }): React.ReactElement {
|
|
47
|
+
return (
|
|
48
|
+
<svg
|
|
49
|
+
data-dsh-panel-entry={TASK_BOARD_PANEL_ID}
|
|
50
|
+
viewBox="0 0 16 16"
|
|
51
|
+
width={size}
|
|
52
|
+
height={size}
|
|
53
|
+
fill="none"
|
|
54
|
+
stroke="currentColor"
|
|
55
|
+
strokeWidth="1.3"
|
|
56
|
+
strokeLinecap="round"
|
|
57
|
+
strokeLinejoin="round"
|
|
58
|
+
aria-hidden="true"
|
|
59
|
+
>
|
|
60
|
+
<rect x="2" y="2.5" width="12" height="11" rx="1.5" />
|
|
61
|
+
<path d="M2 6.5h12M6.5 6.5v7" />
|
|
62
|
+
</svg>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The main-slot page. The layout mounts it only while the board is the selected
|
|
68
|
+
* panel, so the conversation keeps the center column untouched the rest of the
|
|
69
|
+
* time; the wrapper carries the pinned `data-dsh-taskboard-view` semantic
|
|
70
|
+
* anchor (L2 contract, skins) and the container query context the board's
|
|
71
|
+
* responsive rules read.
|
|
72
|
+
* @param props - the framework main-slot share plus this entry's injected face.
|
|
73
|
+
* @returns the board page.
|
|
74
|
+
*/
|
|
75
|
+
export function TaskBoardPanel({ controller }: { controller: BoardController }): React.ReactElement {
|
|
76
|
+
return (
|
|
77
|
+
<div className={css.panel} data-dsh-taskboard-view="" data-dsh-plugin="task-board">
|
|
78
|
+
<TaskBoard controller={controller} />
|
|
79
|
+
</div>
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** The injected face both registrations receive. */
|
|
84
|
+
interface PanelFace {
|
|
85
|
+
controller: BoardController
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Register the board's sidebar row and center-column page.
|
|
90
|
+
*
|
|
91
|
+
* Both seats are declared by shell plugins this package does not depend on at
|
|
92
|
+
* runtime, so each registration is wrapped in `ctx.slots.inject`: the callback
|
|
93
|
+
* runs only after the owning entry declares the seat, and a shell that never
|
|
94
|
+
* declares it leaves the board simply absent instead of failing boot.
|
|
95
|
+
* @param ctx - client root context (services: slots).
|
|
96
|
+
* @param controller - the board controller the page and the row drive.
|
|
97
|
+
* @returns disposer releasing both registrations.
|
|
98
|
+
*/
|
|
99
|
+
export function registerTaskBoardPanel(ctx: ClientContext, controller: BoardController): () => void {
|
|
100
|
+
const slots = ctx.slots as {
|
|
101
|
+
inject(key: string, callback: () => () => void): () => void
|
|
102
|
+
register(options: Record<string, unknown>, component: unknown): () => void
|
|
103
|
+
}
|
|
104
|
+
const disposers: Array<() => void> = []
|
|
105
|
+
|
|
106
|
+
disposers.push(slots.inject('sidebar.panellist', () => slots.register({
|
|
107
|
+
name: 'sidebar.panellist',
|
|
108
|
+
id: TASK_BOARD_PANEL_ID,
|
|
109
|
+
order: PANEL_ORDER,
|
|
110
|
+
// The shell resolves this through resolveSlotLabel on every locale change,
|
|
111
|
+
// so the module-level translate reads the active language at call time.
|
|
112
|
+
label: () => t('entry.label'),
|
|
113
|
+
}, TaskBoardPanelIcon)))
|
|
114
|
+
|
|
115
|
+
disposers.push(slots.inject('main', () => slots.register({
|
|
116
|
+
name: 'main',
|
|
117
|
+
key: TASK_BOARD_PANEL_ID,
|
|
118
|
+
inject: (): PanelFace => ({ controller }),
|
|
119
|
+
}, TaskBoardPanel as never)))
|
|
120
|
+
|
|
121
|
+
return () => {
|
|
122
|
+
for (const dispose of disposers.splice(0)) dispose()
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// Generated by scripts/sync-shared.mjs from shared/client/settings/settings-entry-form.ts. Do not edit this copy; edit the shared source and run "node scripts/sync-shared.mjs".
|
|
2
|
+
// Generated by scripts/sync-shared.mjs from shared/client/settings/settings-entry-form.ts. Do not edit this copy; edit the shared source and run "node scripts/sync-shared.mjs".
|
|
3
|
+
/**
|
|
4
|
+
* Family settings form bound to the profile entry id a Host actually serves.
|
|
5
|
+
*
|
|
6
|
+
* Under the 0.1.7 settings model a form is addressed by PROFILE ENTRY ID, while
|
|
7
|
+
* a family card knows only its own settings namespace. The family binder
|
|
8
|
+
* (`ctx.get('webUiSettings')`, published by dsh-web-settings) resolves one onto
|
|
9
|
+
* the other and is the preferred transport; this module is the fallback for a
|
|
10
|
+
* page that serves no binder — the group plugin is not installed, or its client
|
|
11
|
+
* half has not applied yet (the aggregate mounts its children in order, and a
|
|
12
|
+
* child still waiting on its own injects publishes nothing).
|
|
13
|
+
*
|
|
14
|
+
* The shared describe mirror is the only local evidence of which row id this
|
|
15
|
+
* profile serves, but it answers asynchronously: at plugin activation it usually
|
|
16
|
+
* holds nothing yet. A one-shot guess therefore cannot be right for every
|
|
17
|
+
* deployment, and the form is bound once per session:
|
|
18
|
+
*
|
|
19
|
+
* - binding the bare namespace made every save answer `No configurable plugin
|
|
20
|
+
* entry "<namespace>"` on an aggregate install (the namespace is not an entry
|
|
21
|
+
* id there), which the card reported as "the deployment did not accept these
|
|
22
|
+
* values";
|
|
23
|
+
* - binding the aggregate row id would break a standalone install, whose row id
|
|
24
|
+
* is the namespace or the package's own `ui-*` row.
|
|
25
|
+
*
|
|
26
|
+
* So the fallback starts on the most likely row and REBINDS the moment the
|
|
27
|
+
* mirror names one of this package's rows. Until the mirror answers, the form
|
|
28
|
+
* reports the binding it holds (usually `loading`), which the card renders as an
|
|
29
|
+
* unavailable form rather than a doomed one.
|
|
30
|
+
*/
|
|
31
|
+
import type { ConfigForm, ConfigFormSnapshot, ConfigForms } from '@deepseek-ai/dsh-client-ui-settings/client'
|
|
32
|
+
|
|
33
|
+
/** Options of {@link createServedEntryForm}. */
|
|
34
|
+
export interface ServedEntryFormOptions<T> {
|
|
35
|
+
/** The shared configuration forms service (`ctx.configForms`). */
|
|
36
|
+
forms: ConfigForms
|
|
37
|
+
/**
|
|
38
|
+
* Profile entry ids this package's rows carry, most likely first: the
|
|
39
|
+
* aggregate's generated row, the package's own standalone row, then the bare
|
|
40
|
+
* namespace. The first is the fallback while the mirror has no answer; the
|
|
41
|
+
* last is the answer when the mirror serves none of them.
|
|
42
|
+
*/
|
|
43
|
+
entryIds: readonly string[]
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The snapshot a form reports before the Host has answered with this entry. */
|
|
47
|
+
function pendingSnapshot<T>(): ConfigFormSnapshot<T> {
|
|
48
|
+
return {
|
|
49
|
+
status: 'loading',
|
|
50
|
+
value: undefined,
|
|
51
|
+
base: undefined,
|
|
52
|
+
user: undefined,
|
|
53
|
+
revision: undefined,
|
|
54
|
+
writable: false,
|
|
55
|
+
mode: 'host',
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Create a settings form bound to the profile entry id this Host serves, and
|
|
61
|
+
* rebound whenever the shared describe mirror names a different one of this
|
|
62
|
+
* package's rows.
|
|
63
|
+
* @param options - the shared forms service and this package's candidate row ids.
|
|
64
|
+
* @returns a form delegating to the currently resolved entry's own form.
|
|
65
|
+
*/
|
|
66
|
+
export function createServedEntryForm<T>(options: ServedEntryFormOptions<T>): ConfigForm<T> {
|
|
67
|
+
const { forms, entryIds } = options
|
|
68
|
+
const fallbackId = entryIds[0]
|
|
69
|
+
const namespaceId = entryIds[entryIds.length - 1]
|
|
70
|
+
const listeners = new Set<() => void>()
|
|
71
|
+
let boundId: string | undefined
|
|
72
|
+
let bound: ConfigForm<T> | undefined
|
|
73
|
+
let offBound: (() => void) | undefined
|
|
74
|
+
let snapshot: ConfigFormSnapshot<T> = pendingSnapshot<T>()
|
|
75
|
+
|
|
76
|
+
/** Republish: the delegated snapshot when one is bound, the pending one otherwise. */
|
|
77
|
+
const publish = (): void => {
|
|
78
|
+
if (bound !== undefined) snapshot = bound.getSnapshot()
|
|
79
|
+
for (const listener of [...listeners]) listener()
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The entry id the mirror currently justifies. An unanswered or empty mirror
|
|
84
|
+
* is not evidence of absence, so it keeps the first candidate; a mirror that
|
|
85
|
+
* answers without any of this package's rows leaves the namespace itself.
|
|
86
|
+
* @returns the entry id to bind.
|
|
87
|
+
*/
|
|
88
|
+
const resolve = (): string => {
|
|
89
|
+
let served: readonly string[] | undefined
|
|
90
|
+
try {
|
|
91
|
+
served = forms.describe().getSnapshot().view?.namespaces.map(row => row.ns)
|
|
92
|
+
} catch {
|
|
93
|
+
// A mirror that refuses the read is unanswered too, not absent.
|
|
94
|
+
served = undefined
|
|
95
|
+
}
|
|
96
|
+
if (served === undefined || served.length === 0) return fallbackId
|
|
97
|
+
return entryIds.find(id => served.includes(id)) ?? namespaceId
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Bind (or rebind) the resolved entry's form; a no-op while it is unchanged. */
|
|
101
|
+
const bind = (): void => {
|
|
102
|
+
const target = resolve()
|
|
103
|
+
if (target === boundId) return
|
|
104
|
+
offBound?.()
|
|
105
|
+
offBound = undefined
|
|
106
|
+
let form: ConfigForm<T>
|
|
107
|
+
try {
|
|
108
|
+
form = forms.get<T>(target)
|
|
109
|
+
} catch {
|
|
110
|
+
// A service that refuses the id stays unbound; the next mirror answer
|
|
111
|
+
// retries rather than pinning a wrong entry for the session.
|
|
112
|
+
boundId = undefined
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
boundId = target
|
|
116
|
+
bound = form
|
|
117
|
+
offBound = form.subscribe(() => { publish() })
|
|
118
|
+
publish()
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// The mirror subscription lives for the page: the card owns its form for the
|
|
122
|
+
// session, and the ConfigForm contract carries no disposer to hang it on. The
|
|
123
|
+
// callback is cheap (a resolve plus a no-op while the id is unchanged). A
|
|
124
|
+
// mirror without a subscription seat keeps the initial binding.
|
|
125
|
+
try {
|
|
126
|
+
forms.describe().subscribe(() => { bind() })
|
|
127
|
+
} catch {
|
|
128
|
+
// A describe face that refuses the subscription: the initial binding stands.
|
|
129
|
+
}
|
|
130
|
+
bind()
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
getSnapshot: () => snapshot,
|
|
134
|
+
subscribe: (listener) => {
|
|
135
|
+
listeners.add(listener)
|
|
136
|
+
return () => { listeners.delete(listener) }
|
|
137
|
+
},
|
|
138
|
+
set: (field, value) => bound?.set(field, value) ?? Promise.resolve(false),
|
|
139
|
+
unset: (field) => bound?.unset(field) ?? Promise.resolve(false),
|
|
140
|
+
mutate: (ops, expectedRevision) => bound?.mutate(ops, expectedRevision) ?? Promise.resolve(false),
|
|
141
|
+
}
|
|
142
|
+
}
|