@fonlan/dsh-task-kanban 0.2.6 → 0.2.8
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.md +7 -5
- package/lib/client.js +146 -221
- package/lib/client.js.map +1 -1
- package/lib/index.js +65 -41
- package/lib/types/index.d.ts +23 -3
- package/lib/types/server/runner.d.ts +6 -5
- package/lib/types/server/settings.d.ts +34 -26
- package/package.json +39 -39
- package/src/client/board.css +0 -107
- package/src/client/board.tsx +5 -9
- package/src/client/icons.tsx +74 -0
- package/src/client/index.tsx +27 -11
- package/src/client/kanban-state.ts +79 -107
- package/src/client/sections.tsx +10 -136
- package/src/index.ts +7 -5
- package/src/server/runner.ts +33 -18
- package/src/server/settings.ts +69 -36
package/src/client/index.tsx
CHANGED
|
@@ -6,9 +6,9 @@ import type {} from '@deepseek-ai/dsh-client-ui-layout/client'
|
|
|
6
6
|
import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
|
|
7
7
|
import type {} from '@deepseek-ai/dsh-client-ui-settings/client'
|
|
8
8
|
import { BoardRoot } from './board'
|
|
9
|
-
import {
|
|
9
|
+
import { KanbanPanelIcon, KanbanSettingsSection } from './sections'
|
|
10
10
|
import { LOCALE_NS, zh, en } from './locales'
|
|
11
|
-
import {
|
|
11
|
+
import { KANBAN_PANEL_ID, setClient } from './kanban-state'
|
|
12
12
|
|
|
13
13
|
/** The settings sidebar entry this plugin's page owns (must stay stable). */
|
|
14
14
|
const SETTINGS_SECTION_ID = 'task-kanban'
|
|
@@ -19,9 +19,9 @@ export const inject = ['slots', 'sessions', 'workspaces', 'locale']
|
|
|
19
19
|
/** Client plugin body. */
|
|
20
20
|
export function apply(ctx: ClientContext): void {
|
|
21
21
|
setClient(ctx)
|
|
22
|
-
|
|
23
|
-
//
|
|
24
|
-
|
|
22
|
+
// Session opening goes through the shell's own view owner
|
|
23
|
+
// (`ctx.uiWorkspace.openSession`, see kanban-state.ts), which already returns
|
|
24
|
+
// the center column to the Conversation — no wrapper to install here.
|
|
25
25
|
|
|
26
26
|
// Live translate bound to the active locale (labels re-read it per call).
|
|
27
27
|
const t = ctx.locale.bind(LOCALE_NS)
|
|
@@ -30,14 +30,30 @@ export function apply(ctx: ClientContext): void {
|
|
|
30
30
|
return () => off()
|
|
31
31
|
}, 'task-kanban: dictionaries')
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
// The board is a GLOBAL MAIN PANEL: the shell renders one `main` entry per
|
|
34
|
+
// selected panel id, so keying it (instead of replacing `main.conversation`)
|
|
35
|
+
// keeps the Conversation mounted and selectable beside the board, and the
|
|
36
|
+
// sidebar's own panel row owns the button that selects it (the
|
|
37
|
+
// `sidebar.panellist` entry below). Nothing of this plugin sits in the
|
|
38
|
+
// sidebar foot any more, so the Settings trigger can never be squeezed.
|
|
39
|
+
ctx.slots.inject('main', () =>
|
|
34
40
|
ctx.slots.register({
|
|
35
|
-
name: '
|
|
36
|
-
|
|
37
|
-
|
|
41
|
+
name: 'main',
|
|
42
|
+
key: KANBAN_PANEL_ID,
|
|
43
|
+
locale: LOCALE_NS,
|
|
44
|
+
} as never, BoardRoot as never),
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
// Sidebar panel row: the shell renders the row (label, tooltip, active state)
|
|
48
|
+
// and calls `layout.selectPanel(id)` on click; the entry supplies the glyph.
|
|
49
|
+
ctx.slots.inject('sidebar.panellist', () =>
|
|
50
|
+
ctx.slots.register({
|
|
51
|
+
name: 'sidebar.panellist',
|
|
52
|
+
id: KANBAN_PANEL_ID,
|
|
53
|
+
order: 20,
|
|
38
54
|
label: () => t('kanban'),
|
|
39
55
|
locale: LOCALE_NS,
|
|
40
|
-
},
|
|
56
|
+
}, KanbanPanelIcon as never),
|
|
41
57
|
)
|
|
42
58
|
|
|
43
59
|
// The plugin's own settings page (设置 -> 侧栏「任务看板」) rides the
|
|
@@ -53,4 +69,4 @@ export function apply(ctx: ClientContext): void {
|
|
|
53
69
|
locale: LOCALE_NS,
|
|
54
70
|
}, KanbanSettingsSection as never),
|
|
55
71
|
)
|
|
56
|
-
}
|
|
72
|
+
}
|
|
@@ -1,44 +1,51 @@
|
|
|
1
|
-
import { useSyncExternalStore } from 'react'
|
|
2
1
|
import type { Context } from '@deepseek-ai/cordis'
|
|
3
2
|
type ClientContext = Context
|
|
4
3
|
|
|
5
4
|
/**
|
|
6
|
-
*
|
|
5
|
+
* Panel id shared by this plugin's main-panel key (`main`) and its sidebar
|
|
6
|
+
* entry (`sidebar.panellist`): the shell selects the panel by this id, so the
|
|
7
|
+
* two registrations must agree.
|
|
8
|
+
*/
|
|
9
|
+
export const KANBAN_PANEL_ID = 'task-kanban'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Structural view of the client shell's panel-navigation face (`ctx.layout`).
|
|
13
|
+
* Declared locally (no value import) so the plugin keeps working on a runtime
|
|
14
|
+
* whose layout types differ.
|
|
15
|
+
*/
|
|
16
|
+
interface LayoutLike {
|
|
17
|
+
selectPanel(panelId: string | null): void
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Structural view of the client shell's workspace-navigation face
|
|
22
|
+
* (`ctx.uiWorkspace`, dsh-client-ui-workspace). Declared locally (no value
|
|
23
|
+
* import) so the plugin keeps working on a runtime whose workspace types differ.
|
|
24
|
+
*/
|
|
25
|
+
interface UiWorkspaceLike {
|
|
26
|
+
/** Select a Session (id, or a direct-parent subagent address) and show it. */
|
|
27
|
+
openSession(target: string): void
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* HMR-safe owner context, keyed on the global symbol registry.
|
|
7
32
|
*
|
|
8
33
|
* The client-hmr hot swap re-evaluates this module (a fresh `lib/client.js`
|
|
9
|
-
* bundle) WITHOUT reloading the page
|
|
10
|
-
* duplicated per evaluation
|
|
11
|
-
*
|
|
12
|
-
* OLD copy's `boardOpen`/`boardDisposer` — while a board opened after the swap
|
|
13
|
-
* writes the NEW copy's state cell. The wrapper would then no-op and clicking
|
|
14
|
-
* a sidebar session would never leave the board.
|
|
15
|
-
*
|
|
16
|
-
* Keeping every field on one `globalThis`-backed record (via `Symbol.for`, so
|
|
17
|
-
* all bundle copies resolve the same key) makes every copy read/write the same
|
|
18
|
-
* state cell, so exit works regardless of which module copy performed the
|
|
19
|
-
* enter.
|
|
34
|
+
* bundle) WITHOUT reloading the page; a module-level cell would then be
|
|
35
|
+
* duplicated per evaluation and an old copy's helper would read a stale owner.
|
|
36
|
+
* `Symbol.for` makes every bundle copy share one cell.
|
|
20
37
|
*/
|
|
21
38
|
const STATE_KEY = Symbol.for('@fonlan/dsh-task-kanban/state')
|
|
22
39
|
|
|
23
40
|
interface KanbanState {
|
|
24
41
|
client: ClientContext | null
|
|
25
|
-
boardRoot: ((props: Record<string, unknown>) => React.ReactNode) | null
|
|
26
|
-
boardDisposer: (() => void) | null
|
|
27
|
-
boardOpen: boolean
|
|
28
|
-
listeners: Set<() => void>
|
|
29
42
|
}
|
|
30
43
|
|
|
31
44
|
function getState(): KanbanState {
|
|
32
45
|
const g = globalThis as unknown as Record<symbol, KanbanState | undefined>
|
|
33
46
|
let state = g[STATE_KEY]
|
|
34
47
|
if (state === undefined) {
|
|
35
|
-
state = {
|
|
36
|
-
client: null,
|
|
37
|
-
boardRoot: null,
|
|
38
|
-
boardDisposer: null,
|
|
39
|
-
boardOpen: false,
|
|
40
|
-
listeners: new Set(),
|
|
41
|
-
}
|
|
48
|
+
state = { client: null }
|
|
42
49
|
g[STATE_KEY] = state
|
|
43
50
|
}
|
|
44
51
|
return state
|
|
@@ -51,102 +58,67 @@ export function getClient(): ClientContext | null {
|
|
|
51
58
|
return getState().client
|
|
52
59
|
}
|
|
53
60
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
export function isBoardOpen(): boolean {
|
|
65
|
-
return getState().boardOpen
|
|
66
|
-
}
|
|
67
|
-
function subscribe(cb: () => void): () => void {
|
|
68
|
-
const listeners = getState().listeners
|
|
69
|
-
listeners.add(cb)
|
|
70
|
-
return () => { listeners.delete(cb) }
|
|
71
|
-
}
|
|
72
|
-
export function useBoardOpen(): boolean {
|
|
73
|
-
return useSyncExternalStore(subscribe, isBoardOpen)
|
|
61
|
+
/** The shell's panel face, when this runtime provides it. */
|
|
62
|
+
function layoutOf(ctx: ClientContext | null): LayoutLike | null {
|
|
63
|
+
if (ctx === null) return null
|
|
64
|
+
try {
|
|
65
|
+
const layout = (ctx as unknown as { layout?: LayoutLike }).layout
|
|
66
|
+
return typeof layout?.selectPanel === 'function' ? layout : null
|
|
67
|
+
} catch {
|
|
68
|
+
return null
|
|
69
|
+
}
|
|
74
70
|
}
|
|
75
71
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const ctx = state.client
|
|
79
|
-
const root = state.boardRoot
|
|
80
|
-
if (ctx === null || root === null || state.boardDisposer !== null) return
|
|
72
|
+
/** Select the kanban panel. The sidebar's own entry does this; kept for in-app callers. */
|
|
73
|
+
export function showKanbanPanel(ctx: ClientContext | null = getClient()): void {
|
|
81
74
|
try {
|
|
82
|
-
|
|
83
|
-
// seat to `main.conversation`. Register through `inject` so the board
|
|
84
|
-
// waits for that owner if the client plugin load order is reversed.
|
|
85
|
-
state.boardDisposer = ctx.slots.inject('main.conversation', () =>
|
|
86
|
-
ctx.slots.register({ name: 'main.conversation', priority: -1, locale: 'task-kanban' } as never, root as never),
|
|
87
|
-
)
|
|
88
|
-
state.boardOpen = true
|
|
89
|
-
notify()
|
|
75
|
+
layoutOf(ctx)?.selectPanel(KANBAN_PANEL_ID)
|
|
90
76
|
} catch (error) {
|
|
91
|
-
console.error('[@fonlan/dsh-task-kanban] cannot
|
|
77
|
+
console.error('[@fonlan/dsh-task-kanban] cannot select the kanban panel:', error)
|
|
92
78
|
}
|
|
93
79
|
}
|
|
94
80
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
if (state.boardOpen) {
|
|
102
|
-
state.boardOpen = false
|
|
103
|
-
notify()
|
|
81
|
+
/** Hand the center column back to the Conversation. */
|
|
82
|
+
export function leaveKanbanPanel(ctx: ClientContext | null = getClient()): void {
|
|
83
|
+
try {
|
|
84
|
+
layoutOf(ctx)?.selectPanel(null)
|
|
85
|
+
} catch (error) {
|
|
86
|
+
console.error('[@fonlan/dsh-task-kanban] cannot leave the kanban panel:', error)
|
|
104
87
|
}
|
|
105
88
|
}
|
|
106
89
|
|
|
107
|
-
export function toggleBoard(): void {
|
|
108
|
-
if (isBoardOpen()) exitBoard()
|
|
109
|
-
else enterBoard()
|
|
110
|
-
}
|
|
111
|
-
|
|
112
90
|
/**
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
* for the newly selected session.
|
|
91
|
+
* Open one session — or a durable direct-parent subagent address — in the
|
|
92
|
+
* center column, leaving the kanban panel.
|
|
116
93
|
*
|
|
117
|
-
*
|
|
118
|
-
* (
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
94
|
+
* Navigation is a view-owner concern, not a session-controller one: `ctx.sessions`
|
|
95
|
+
* (`ISessions`) exposes retention (`retain`/`using`/`retainInfo`), creation,
|
|
96
|
+
* fork, search and catalog reads, and has no `open`/`openSubagent` at all
|
|
97
|
+
* ("navigation belongs to view owners"). The view owner the shell itself uses is
|
|
98
|
+
* `ctx.uiWorkspace.openSession(target)` (dsh-client-ui-workspace): it retains the
|
|
99
|
+
* session AND reveals the Conversation panel as ONE navigation action. Routing
|
|
100
|
+
* every open through it therefore replaces the old wrapper around
|
|
101
|
+
* `sessions.open`/`openSubagent`, which existed only to hand the center column
|
|
102
|
+
* back when a sidebar click selected a session while the board was showing.
|
|
123
103
|
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
* a previous (HMR-replaced) module copy still closes the board correctly.
|
|
104
|
+
* @param target - session id (or subagent address) to show.
|
|
105
|
+
* @returns whether the runtime provided the navigation face; `false` means the
|
|
106
|
+
* session could not be shown (a client without ui-workspace).
|
|
128
107
|
*/
|
|
129
|
-
export function
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
exitBoard()
|
|
137
|
-
return open.call(sessions, id)
|
|
138
|
-
}) as typeof open
|
|
139
|
-
;(bound as unknown as { kbBound?: boolean }).kbBound = true
|
|
140
|
-
sessions.open = bound
|
|
108
|
+
export function openSessionView(target: string, ctx: ClientContext | null = getClient()): boolean {
|
|
109
|
+
if (ctx === null) return false
|
|
110
|
+
let uiWorkspace: UiWorkspaceLike | undefined
|
|
111
|
+
try {
|
|
112
|
+
uiWorkspace = (ctx as unknown as { uiWorkspace?: UiWorkspaceLike }).uiWorkspace
|
|
113
|
+
} catch {
|
|
114
|
+
return false
|
|
141
115
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
;(bound as unknown as { kbBound?: boolean }).kbBound = true
|
|
150
|
-
sessions.openSubagent = bound
|
|
116
|
+
if (typeof uiWorkspace?.openSession !== 'function') return false
|
|
117
|
+
try {
|
|
118
|
+
uiWorkspace.openSession(target)
|
|
119
|
+
return true
|
|
120
|
+
} catch (error) {
|
|
121
|
+
console.error('[@fonlan/dsh-task-kanban] cannot open the session view:', error)
|
|
122
|
+
return false
|
|
151
123
|
}
|
|
152
|
-
}
|
|
124
|
+
}
|
package/src/client/sections.tsx
CHANGED
|
@@ -1,143 +1,17 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
2
|
-
import {
|
|
2
|
+
import { ChecklistIcon } from './icons'
|
|
3
3
|
import { api, type PresetOption, type ReasoningOptions } from './api'
|
|
4
|
-
import { toggleBoard, useBoardOpen } from './kanban-state'
|
|
5
4
|
import type { KanbanSettingsShape, ModelOption } from '../shared/card'
|
|
6
5
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
export function KanbanFooterButton({ wide, t }: FooterActionProps): JSX.Element {
|
|
17
|
-
const open = useBoardOpen()
|
|
18
|
-
const ref = useRef<HTMLButtonElement | null>(null)
|
|
19
|
-
const label = t !== undefined ? t(open ? 'closeBoard' : 'openBoard') : 'Task Kanban'
|
|
20
|
-
|
|
21
|
-
useEffect(() => {
|
|
22
|
-
const btn = ref.current
|
|
23
|
-
if (btn === null) return
|
|
24
|
-
let foot: Element | null = null
|
|
25
|
-
let disposed = false
|
|
26
|
-
|
|
27
|
-
// Locate the sidebar foot area WITHOUT moving any DOM: it is the ancestor
|
|
28
|
-
// of the button whose LAST child contains the settings trigger and which
|
|
29
|
-
// is itself the last child of its parent (root > footArea > settingsArea).
|
|
30
|
-
// The slot machinery wraps entries in an anonymous div, so the search
|
|
31
|
-
// tolerates extra wrapper levels. Styling is pure CSS (see board.css
|
|
32
|
-
// .kb-foot-row), so a failed lookup only leaves the button in its natural
|
|
33
|
-
// slot row — it can never vanish.
|
|
34
|
-
const findFoot = (): Element | null => {
|
|
35
|
-
let el: Element | null = btn.parentElement
|
|
36
|
-
while (el !== null && el !== document.body && el.parentElement !== null) {
|
|
37
|
-
const last = el.lastElementChild
|
|
38
|
-
const hasTrigger =
|
|
39
|
-
last !== null &&
|
|
40
|
-
(last.querySelector?.('button[aria-haspopup="dialog"]') !== null ||
|
|
41
|
-
last.querySelector?.('button[aria-haspopup="menu"]') !== null)
|
|
42
|
-
const isLastChild = el.parentElement.lastElementChild === el
|
|
43
|
-
if (hasTrigger && isLastChild) return el
|
|
44
|
-
el = el.parentElement
|
|
45
|
-
}
|
|
46
|
-
return null
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const tryApply = (): boolean => {
|
|
50
|
-
if (foot === null) {
|
|
51
|
-
try {
|
|
52
|
-
foot = findFoot()
|
|
53
|
-
} catch {
|
|
54
|
-
foot = null
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
if (foot === null) return false
|
|
58
|
-
try {
|
|
59
|
-
foot.classList.add('kb-foot-row')
|
|
60
|
-
foot.classList.toggle('kb-foot-rail', wide !== true)
|
|
61
|
-
} catch {
|
|
62
|
-
// element already gone; retry will re-locate
|
|
63
|
-
}
|
|
64
|
-
return true
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
let applied = false
|
|
68
|
-
try {
|
|
69
|
-
applied = tryApply()
|
|
70
|
-
} catch (error) {
|
|
71
|
-
console.error('[@fonlan/dsh-task-kanban] foot placement error:', error)
|
|
72
|
-
}
|
|
73
|
-
if (!applied) {
|
|
74
|
-
// The settings entry may mount later than this entry (plugin load
|
|
75
|
-
// order); retry on DOM mutations AND on a short interval until the
|
|
76
|
-
// foot area is found (give up after 15s).
|
|
77
|
-
const observer = new MutationObserver(() => {
|
|
78
|
-
if (disposed) return
|
|
79
|
-
if (tryApply()) observer.disconnect()
|
|
80
|
-
})
|
|
81
|
-
observer.observe(document.body, { childList: true, subtree: true })
|
|
82
|
-
const timer = window.setInterval(() => {
|
|
83
|
-
if (disposed) {
|
|
84
|
-
window.clearInterval(timer)
|
|
85
|
-
return
|
|
86
|
-
}
|
|
87
|
-
if (tryApply()) {
|
|
88
|
-
observer.disconnect()
|
|
89
|
-
window.clearInterval(timer)
|
|
90
|
-
}
|
|
91
|
-
}, 1000)
|
|
92
|
-
window.setTimeout(() => {
|
|
93
|
-
observer.disconnect()
|
|
94
|
-
window.clearInterval(timer)
|
|
95
|
-
if (foot === null) {
|
|
96
|
-
// report the DOM chain so the next report carries evidence
|
|
97
|
-
const chain: string[] = []
|
|
98
|
-
let el: Element | null = btn.parentElement
|
|
99
|
-
while (el !== null && chain.length < 8) {
|
|
100
|
-
chain.push(el.tagName + '.' + String(el.className).slice(0, 60))
|
|
101
|
-
el = el.parentElement
|
|
102
|
-
}
|
|
103
|
-
console.warn('[@fonlan/dsh-task-kanban] could not locate the sidebar foot area:', chain)
|
|
104
|
-
}
|
|
105
|
-
}, 15000)
|
|
106
|
-
return () => {
|
|
107
|
-
disposed = true
|
|
108
|
-
observer.disconnect()
|
|
109
|
-
window.clearInterval(timer)
|
|
110
|
-
try {
|
|
111
|
-
foot?.classList.remove('kb-foot-row')
|
|
112
|
-
foot?.classList.remove('kb-foot-rail')
|
|
113
|
-
} catch {
|
|
114
|
-
// already gone
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
return () => {
|
|
119
|
-
try {
|
|
120
|
-
foot?.classList.remove('kb-foot-row')
|
|
121
|
-
foot?.classList.remove('kb-foot-rail')
|
|
122
|
-
} catch {
|
|
123
|
-
// already gone
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
}, [wide])
|
|
127
|
-
|
|
128
|
-
return (
|
|
129
|
-
<button
|
|
130
|
-
ref={ref}
|
|
131
|
-
type="button"
|
|
132
|
-
className={'kb-footer-action' + (wide === true ? ' kb-footer-action-wide' : '') + (open ? ' kb-footer-action-active' : '')}
|
|
133
|
-
aria-label={label}
|
|
134
|
-
title={label}
|
|
135
|
-
onClick={() => toggleBoard()}
|
|
136
|
-
>
|
|
137
|
-
<IconChecklistOutline14 size={wide === true ? 16 : 18} />
|
|
138
|
-
{wide === true && <span className="kb-footer-label">{t !== undefined ? t('kanban') : 'Kanban'}</span>}
|
|
139
|
-
</button>
|
|
140
|
-
)
|
|
6
|
+
/**
|
|
7
|
+
* Sidebar panel-row glyph for the board (`sidebar.panellist`).
|
|
8
|
+
*
|
|
9
|
+
* The shell owns the row: it renders the button, its label, tooltip and active
|
|
10
|
+
* state, and calls `layout.selectPanel(id)` on click. The entry contributes
|
|
11
|
+
* only the glyph, so this component must stay purely presentational.
|
|
12
|
+
*/
|
|
13
|
+
export function KanbanPanelIcon({ size }: { size: number }): JSX.Element {
|
|
14
|
+
return <ChecklistIcon size={size} />
|
|
141
15
|
}
|
|
142
16
|
|
|
143
17
|
interface SettingsSectionProps {
|
package/src/index.ts
CHANGED
|
@@ -7,20 +7,22 @@
|
|
|
7
7
|
* serves the fenced JSON API the board UI calls.
|
|
8
8
|
*/
|
|
9
9
|
import type { Context } from '@deepseek-ai/cordis'
|
|
10
|
-
import
|
|
11
|
-
import { registerSettings } from './server/settings.js'
|
|
10
|
+
import { KanbanSettingsSchema, registerSettings } from './server/settings.js'
|
|
12
11
|
import { TaskStore } from './server/task-store.js'
|
|
13
12
|
import { KanbanRunner } from './server/runner.js'
|
|
14
13
|
import { registerApiRoutes } from './server/rpc.js'
|
|
14
|
+
import type { KanbanSettingsShape } from './shared/card.js'
|
|
15
15
|
|
|
16
16
|
export const name = '@fonlan/dsh-task-kanban'
|
|
17
17
|
|
|
18
18
|
export const inject = ['webServer', 'agents', 'skills']
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
// The entry config is the settings document (worker count + per-session-type
|
|
21
|
+
// defaults); a changed config restarts this entry.
|
|
22
|
+
export const Config = KanbanSettingsSchema
|
|
21
23
|
|
|
22
|
-
export function apply(ctx: Context,
|
|
23
|
-
const settings = registerSettings(ctx)
|
|
24
|
+
export function apply(ctx: Context, config: KanbanSettingsShape): void {
|
|
25
|
+
const settings = registerSettings(ctx, config)
|
|
24
26
|
const store = new TaskStore()
|
|
25
27
|
const runner = new KanbanRunner(ctx, store, settings)
|
|
26
28
|
|
package/src/server/runner.ts
CHANGED
|
@@ -19,7 +19,14 @@ import { currentBranch, detectBaseRef, hasStashMessage, isGitRepo, isTreeDirty,
|
|
|
19
19
|
import { interactiveRefinementPrompt, mergePrompt, phasePrompt, refinementPrompt } from './prompts.js'
|
|
20
20
|
import { loadCardSkill, parseSkillGesture, skillInvocationMessage } from './skills.js'
|
|
21
21
|
import { registerKanbanTools, type KanbanToolResolver } from './tools.js'
|
|
22
|
-
|
|
22
|
+
// Declaration-merge only (erased at runtime): loads the `agentPresets` service
|
|
23
|
+
// augmentation for `ctx.get('agentPresets')` below. The 0.1.x name
|
|
24
|
+
// `@deepseek-ai/dsh-agent-presets` stopped shipping after 0.1.5-rc.3 and does
|
|
25
|
+
// not exist at 0.2.0-rc.1; `@deepseek-ai/dsh-agent-preset` ships on both live
|
|
26
|
+
// runtimes and carries the same `agentPresets` Context augmentation (it depends
|
|
27
|
+
// on `@deepseek-ai/dsh-agent-preset-registry`, which owns the service and the
|
|
28
|
+
// augmentation).
|
|
29
|
+
import type {} from '@deepseek-ai/dsh-agent-preset'
|
|
23
30
|
|
|
24
31
|
const WORKTREES_REL = ['.dsh', 'worktrees']
|
|
25
32
|
|
|
@@ -59,7 +66,7 @@ export class KanbanRunner implements KanbanToolResolver {
|
|
|
59
66
|
private recovered = false
|
|
60
67
|
/** Agents that already got their scoped kanban tools (event idempotency). */
|
|
61
68
|
private toolScopedAgents = new WeakSet<object>()
|
|
62
|
-
private
|
|
69
|
+
private agentCreatedOff: (() => void) | undefined
|
|
63
70
|
|
|
64
71
|
constructor(ctx: Context, store: TaskStore, settings: KanbanSettingsFace) {
|
|
65
72
|
this.ctx = ctx
|
|
@@ -73,21 +80,28 @@ export class KanbanRunner implements KanbanToolResolver {
|
|
|
73
80
|
|
|
74
81
|
start(): void {
|
|
75
82
|
this.pumpTimer = setInterval(() => { void this.pump() }, 2500)
|
|
76
|
-
// `agent/
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
|
|
81
|
-
|
|
83
|
+
// `agent/created` is the lifecycle edge for an agent that entered the
|
|
84
|
+
// registry — dsh announces every creation path through it, including
|
|
85
|
+
// `agents.resume` (the factory re-opens a persisted session's agent after a
|
|
86
|
+
// host restart), and its `source` records which path that was. The previous
|
|
87
|
+
// subscription used `agent/session-start`, which existed in dsh 0.1.5 only
|
|
88
|
+
// and was already gone by 0.1.7 — so resumed refinement/phase/merge
|
|
89
|
+
// sessions silently lost their scoped kanban tools. `agent/created` exists
|
|
90
|
+
// in 0.1.7-rc.2 and 0.2.0-rc.1 with the same `{ agent, source }` payload.
|
|
91
|
+
// It dispatches as a serial listener, so this callback must not reject:
|
|
92
|
+
// tool registration is best-effort and a lookup failure must not fail the
|
|
93
|
+
// host's agent creation (hence the floating promise).
|
|
94
|
+
this.agentCreatedOff = this.ctx.on('agent/created', (payload: { agent?: { id?: string; ctx?: Context } }): undefined => {
|
|
95
|
+
void this.onAgentCreated(payload.agent)
|
|
82
96
|
})
|
|
83
97
|
}
|
|
84
98
|
|
|
85
99
|
stop(): void {
|
|
86
100
|
if (this.pumpTimer !== undefined) clearInterval(this.pumpTimer)
|
|
87
101
|
this.pumpTimer = undefined
|
|
88
|
-
if (this.
|
|
89
|
-
this.
|
|
90
|
-
this.
|
|
102
|
+
if (this.agentCreatedOff !== undefined) {
|
|
103
|
+
this.agentCreatedOff()
|
|
104
|
+
this.agentCreatedOff = undefined
|
|
91
105
|
}
|
|
92
106
|
}
|
|
93
107
|
|
|
@@ -217,12 +231,13 @@ export class KanbanRunner implements KanbanToolResolver {
|
|
|
217
231
|
}
|
|
218
232
|
|
|
219
233
|
/**
|
|
220
|
-
* The single place kanban tools get registered.
|
|
221
|
-
* for BOTH `agents.create` (startup) and
|
|
222
|
-
* a restart)
|
|
234
|
+
* The single place kanban tools get registered. Subscribed to
|
|
235
|
+
* `agent/created`, which fires for BOTH `agents.create` (startup) and
|
|
236
|
+
* `agents.resume` (host re-open after a restart) and carries the path in its
|
|
237
|
+
* `source`, so a resumed refinement/phase/merge session keeps its scoped
|
|
223
238
|
* tools. Idempotent per agent object; non-kanban sessions are skipped.
|
|
224
239
|
*/
|
|
225
|
-
private async
|
|
240
|
+
private async onAgentCreated(agent: { id?: string; ctx?: Context } | undefined): Promise<void> {
|
|
226
241
|
if (agent === undefined || agent.id === undefined || agent.ctx === undefined) return
|
|
227
242
|
if (this.toolScopedAgents.has(agent)) return
|
|
228
243
|
let cardId = this.sessionCards.get(agent.id)
|
|
@@ -611,9 +626,9 @@ export class KanbanRunner implements KanbanToolResolver {
|
|
|
611
626
|
sessionId,
|
|
612
627
|
meta: { cwd, ...(presetId !== undefined ? { agentPreset: presetId } : {}) },
|
|
613
628
|
...(agentOptions !== undefined ? { agentOptions } : {}),
|
|
614
|
-
// Kanban tools are NOT registered here: `agent/
|
|
615
|
-
//
|
|
616
|
-
//
|
|
629
|
+
// Kanban tools are NOT registered here: `agent/created` (see start())
|
|
630
|
+
// fires for both create and resume, keeping them available even when the
|
|
631
|
+
// host re-opens a persisted kanban session.
|
|
617
632
|
setup: async (agentCtx) => {
|
|
618
633
|
const agentPresets = agentCtx.get('agentPresets') as
|
|
619
634
|
| { mount(agentCtx: unknown, id?: string): Promise<unknown> }
|