@fonlan/dsh-task-kanban 0.2.5 → 0.2.7
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 +207 -283
- package/lib/client.js.map +1 -1
- package/lib/index.js +46 -31
- package/lib/types/index.d.ts +3 -3
- package/lib/types/server/settings.d.ts +6 -26
- package/package.json +25 -25
- package/src/client/board.css +21 -147
- package/src/client/board.tsx +5 -2
- package/src/client/icons.tsx +74 -0
- package/src/client/index.tsx +38 -18
- package/src/client/kanban-state.ts +49 -83
- package/src/client/locales.ts +2 -6
- package/src/client/sections.tsx +57 -203
- package/src/index.ts +7 -5
- package/src/server/settings.ts +62 -37
|
@@ -1,44 +1,41 @@
|
|
|
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
|
+
* HMR-safe owner context, keyed on the global symbol registry.
|
|
7
22
|
*
|
|
8
23
|
* 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.
|
|
24
|
+
* bundle) WITHOUT reloading the page; a module-level cell would then be
|
|
25
|
+
* duplicated per evaluation and an old copy's helper would read a stale owner.
|
|
26
|
+
* `Symbol.for` makes every bundle copy share one cell.
|
|
20
27
|
*/
|
|
21
28
|
const STATE_KEY = Symbol.for('@fonlan/dsh-task-kanban/state')
|
|
22
29
|
|
|
23
30
|
interface KanbanState {
|
|
24
31
|
client: ClientContext | null
|
|
25
|
-
boardRoot: ((props: Record<string, unknown>) => React.ReactNode) | null
|
|
26
|
-
boardDisposer: (() => void) | null
|
|
27
|
-
boardOpen: boolean
|
|
28
|
-
listeners: Set<() => void>
|
|
29
32
|
}
|
|
30
33
|
|
|
31
34
|
function getState(): KanbanState {
|
|
32
35
|
const g = globalThis as unknown as Record<symbol, KanbanState | undefined>
|
|
33
36
|
let state = g[STATE_KEY]
|
|
34
37
|
if (state === undefined) {
|
|
35
|
-
state = {
|
|
36
|
-
client: null,
|
|
37
|
-
boardRoot: null,
|
|
38
|
-
boardDisposer: null,
|
|
39
|
-
boardOpen: false,
|
|
40
|
-
listeners: new Set(),
|
|
41
|
-
}
|
|
38
|
+
state = { client: null }
|
|
42
39
|
g[STATE_KEY] = state
|
|
43
40
|
}
|
|
44
41
|
return state
|
|
@@ -51,80 +48,49 @@ export function getClient(): ClientContext | null {
|
|
|
51
48
|
return getState().client
|
|
52
49
|
}
|
|
53
50
|
|
|
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)
|
|
51
|
+
/** The shell's panel face, when this runtime provides it. */
|
|
52
|
+
function layoutOf(ctx: ClientContext | null): LayoutLike | null {
|
|
53
|
+
if (ctx === null) return null
|
|
54
|
+
try {
|
|
55
|
+
const layout = (ctx as unknown as { layout?: LayoutLike }).layout
|
|
56
|
+
return typeof layout?.selectPanel === 'function' ? layout : null
|
|
57
|
+
} catch {
|
|
58
|
+
return null
|
|
59
|
+
}
|
|
74
60
|
}
|
|
75
61
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const ctx = state.client
|
|
79
|
-
const root = state.boardRoot
|
|
80
|
-
if (ctx === null || root === null || state.boardDisposer !== null) return
|
|
62
|
+
/** Select the kanban panel. The sidebar's own entry does this; kept for in-app callers. */
|
|
63
|
+
export function showKanbanPanel(ctx: ClientContext | null = getClient()): void {
|
|
81
64
|
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()
|
|
65
|
+
layoutOf(ctx)?.selectPanel(KANBAN_PANEL_ID)
|
|
90
66
|
} catch (error) {
|
|
91
|
-
console.error('[@fonlan/dsh-task-kanban] cannot
|
|
67
|
+
console.error('[@fonlan/dsh-task-kanban] cannot select the kanban panel:', error)
|
|
92
68
|
}
|
|
93
69
|
}
|
|
94
70
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
if (state.boardOpen) {
|
|
102
|
-
state.boardOpen = false
|
|
103
|
-
notify()
|
|
71
|
+
/** Hand the center column back to the Conversation. */
|
|
72
|
+
export function leaveKanbanPanel(ctx: ClientContext | null = getClient()): void {
|
|
73
|
+
try {
|
|
74
|
+
layoutOf(ctx)?.selectPanel(null)
|
|
75
|
+
} catch (error) {
|
|
76
|
+
console.error('[@fonlan/dsh-task-kanban] cannot leave the kanban panel:', error)
|
|
104
77
|
}
|
|
105
78
|
}
|
|
106
79
|
|
|
107
|
-
export function toggleBoard(): void {
|
|
108
|
-
if (isBoardOpen()) exitBoard()
|
|
109
|
-
else enterBoard()
|
|
110
|
-
}
|
|
111
|
-
|
|
112
80
|
/**
|
|
113
|
-
* Route session navigation through the
|
|
114
|
-
* board is
|
|
115
|
-
*
|
|
81
|
+
* Route session navigation through the panel selection: opening any session
|
|
82
|
+
* while the board is the active panel first returns the center column to the
|
|
83
|
+
* Conversation, so the session view renders.
|
|
116
84
|
*
|
|
117
85
|
* Every sidebar path that selects a session funnels through `sessions.open`
|
|
118
86
|
* (session rows, search results, fork results, New Session) or
|
|
119
87
|
* `sessions.openSubagent` (catalog children) — including re-clicking the
|
|
120
88
|
* already-current session, which never changes `list.current` and therefore
|
|
121
|
-
* cannot be caught by a list-store subscription. Wrapping the two entry
|
|
122
|
-
*
|
|
89
|
+
* cannot be caught by a list-store subscription. Wrapping the two entry points
|
|
90
|
+
* covers all of them in one place.
|
|
123
91
|
*
|
|
124
92
|
* The wrapper is idempotent: a `kbBound` marker on the wrapper prevents a
|
|
125
|
-
* second bind (plugin re-apply / HMR) from stacking another layer.
|
|
126
|
-
* wrapper's exit goes through the shared state store, a wrapper left over from
|
|
127
|
-
* a previous (HMR-replaced) module copy still closes the board correctly.
|
|
93
|
+
* second bind (plugin re-apply / HMR) from stacking another layer.
|
|
128
94
|
*/
|
|
129
95
|
export function bindSessionNavigation(ctx: ClientContext): void {
|
|
130
96
|
const sessions = ctx.sessions
|
|
@@ -133,7 +99,7 @@ export function bindSessionNavigation(ctx: ClientContext): void {
|
|
|
133
99
|
const open = sessions.open
|
|
134
100
|
if (typeof open === 'function' && (open as unknown as { kbBound?: boolean }).kbBound !== true) {
|
|
135
101
|
const bound = ((id: Parameters<typeof open>[0]) => {
|
|
136
|
-
|
|
102
|
+
leaveKanbanPanel(ctx)
|
|
137
103
|
return open.call(sessions, id)
|
|
138
104
|
}) as typeof open
|
|
139
105
|
;(bound as unknown as { kbBound?: boolean }).kbBound = true
|
|
@@ -143,10 +109,10 @@ export function bindSessionNavigation(ctx: ClientContext): void {
|
|
|
143
109
|
const openSubagent = sessions.openSubagent
|
|
144
110
|
if (typeof openSubagent === 'function' && (openSubagent as unknown as { kbBound?: boolean }).kbBound !== true) {
|
|
145
111
|
const bound = ((address: Parameters<typeof openSubagent>[0]) => {
|
|
146
|
-
|
|
112
|
+
leaveKanbanPanel(ctx)
|
|
147
113
|
return openSubagent.call(sessions, address)
|
|
148
114
|
}) as typeof openSubagent
|
|
149
115
|
;(bound as unknown as { kbBound?: boolean }).kbBound = true
|
|
150
116
|
sessions.openSubagent = bound
|
|
151
117
|
}
|
|
152
|
-
}
|
|
118
|
+
}
|
package/src/client/locales.ts
CHANGED
|
@@ -53,9 +53,7 @@ export const zh = {
|
|
|
53
53
|
noWorkspace: '没有可用工作区,请先在侧边栏添加',
|
|
54
54
|
refresh: '刷新',
|
|
55
55
|
settingsTitle: '任务看板',
|
|
56
|
-
|
|
57
|
-
settingsExpand: '展开设置',
|
|
58
|
-
settingsCollapse: '收起设置',
|
|
56
|
+
sectionSub: '全局并行 worker 数与各会话类型的默认模型、推理强度、Agent 预设',
|
|
59
57
|
maxParallelWorkers: '全局并行 worker 数',
|
|
60
58
|
settingsRefineGroup: '细化需求会话默认',
|
|
61
59
|
settingsPhaseGroup: 'Phase 实现会话默认',
|
|
@@ -129,9 +127,7 @@ export const en = {
|
|
|
129
127
|
noWorkspace: 'No workspaces available — add one in the sidebar first',
|
|
130
128
|
refresh: 'Refresh',
|
|
131
129
|
settingsTitle: 'Task Kanban',
|
|
132
|
-
|
|
133
|
-
settingsExpand: 'Show settings',
|
|
134
|
-
settingsCollapse: 'Hide settings',
|
|
130
|
+
sectionSub: 'Global parallel workers and per-session-type defaults for model, reasoning effort, and agent preset',
|
|
135
131
|
maxParallelWorkers: 'Global parallel workers',
|
|
136
132
|
settingsRefineGroup: 'Refinement session defaults',
|
|
137
133
|
settingsPhaseGroup: 'Phase implementation defaults',
|
package/src/client/sections.tsx
CHANGED
|
@@ -1,146 +1,20 @@
|
|
|
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
|
-
|
|
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} />
|
|
10
15
|
}
|
|
11
16
|
|
|
12
|
-
|
|
13
|
-
* The shell renders footer actions on their own row ABOVE the settings row,
|
|
14
|
-
* so the button is reparented into the settings area to sit on the same
|
|
15
|
-
* line, right of the Settings trigger (see board.css .kb-settings-row). */
|
|
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
|
-
)
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
interface SettingsCardProps {
|
|
17
|
+
interface SettingsSectionProps {
|
|
144
18
|
t?: (key: string) => string
|
|
145
19
|
}
|
|
146
20
|
|
|
@@ -247,11 +121,9 @@ function SessionDefaultsGroup({ kind, settings, models, presets, efforts, tr, on
|
|
|
247
121
|
)
|
|
248
122
|
}
|
|
249
123
|
|
|
250
|
-
/** Plugin settings
|
|
251
|
-
export function
|
|
124
|
+
/** Plugin settings page (设置 -> 侧栏「任务看板」): workers + default model + per-session-type defaults. */
|
|
125
|
+
export function KanbanSettingsSection({ t }: SettingsSectionProps): JSX.Element {
|
|
252
126
|
const tr = t ?? ((key: string) => key)
|
|
253
|
-
// Card-local disclosure: collapsed by default, like the built-in plugin cards.
|
|
254
|
-
const [open, setOpen] = useState(false)
|
|
255
127
|
const [settings, setSettings] = useState<KanbanSettingsShape>(EMPTY_SETTINGS)
|
|
256
128
|
const [models, setModels] = useState<ModelOption[]>([])
|
|
257
129
|
const [presets, setPresets] = useState<PresetOption[]>([])
|
|
@@ -339,67 +211,49 @@ export function KanbanSettingsCard({ t }: SettingsCardProps): JSX.Element {
|
|
|
339
211
|
}, [efforts])
|
|
340
212
|
|
|
341
213
|
return (
|
|
342
|
-
<
|
|
343
|
-
<
|
|
344
|
-
|
|
345
|
-
className="kb-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
onCommit={commit}
|
|
387
|
-
loadEfforts={loadEfforts}
|
|
388
|
-
/>
|
|
389
|
-
<SessionDefaultsGroup
|
|
390
|
-
kind="phase"
|
|
391
|
-
settings={settings}
|
|
392
|
-
models={models}
|
|
393
|
-
presets={presets}
|
|
394
|
-
efforts={efforts}
|
|
395
|
-
tr={tr}
|
|
396
|
-
onCommit={commit}
|
|
397
|
-
loadEfforts={loadEfforts}
|
|
398
|
-
/>
|
|
399
|
-
{saved && <div className="kb-settings-saved">{tr('saved')}</div>}
|
|
400
|
-
</div>
|
|
401
|
-
</div>
|
|
402
|
-
)}
|
|
403
|
-
</li>
|
|
214
|
+
<div className="kb-page">
|
|
215
|
+
<header className="kb-page-head">
|
|
216
|
+
<h3 className="kb-page-title">{tr('settingsTitle')}</h3>
|
|
217
|
+
<p className="kb-page-sub">{tr('sectionSub')}</p>
|
|
218
|
+
</header>
|
|
219
|
+
<div className="kb-settings">
|
|
220
|
+
<label className="kb-field kb-field-row">
|
|
221
|
+
<span>{tr('maxParallelWorkers')}</span>
|
|
222
|
+
<input
|
|
223
|
+
className="kb-input kb-input-number"
|
|
224
|
+
type="number"
|
|
225
|
+
min={1}
|
|
226
|
+
step={1}
|
|
227
|
+
value={settings.maxParallelWorkers}
|
|
228
|
+
onChange={(e) => {
|
|
229
|
+
const value = Math.max(1, Math.floor(Number(e.target.value) || 1))
|
|
230
|
+
setSettings((s) => ({ ...s, maxParallelWorkers: value }))
|
|
231
|
+
void commit({ maxParallelWorkers: value })
|
|
232
|
+
}}
|
|
233
|
+
/>
|
|
234
|
+
</label>
|
|
235
|
+
<SessionDefaultsGroup
|
|
236
|
+
kind="refinement"
|
|
237
|
+
settings={settings}
|
|
238
|
+
models={models}
|
|
239
|
+
presets={presets}
|
|
240
|
+
efforts={efforts}
|
|
241
|
+
tr={tr}
|
|
242
|
+
onCommit={commit}
|
|
243
|
+
loadEfforts={loadEfforts}
|
|
244
|
+
/>
|
|
245
|
+
<SessionDefaultsGroup
|
|
246
|
+
kind="phase"
|
|
247
|
+
settings={settings}
|
|
248
|
+
models={models}
|
|
249
|
+
presets={presets}
|
|
250
|
+
efforts={efforts}
|
|
251
|
+
tr={tr}
|
|
252
|
+
onCommit={commit}
|
|
253
|
+
loadEfforts={loadEfforts}
|
|
254
|
+
/>
|
|
255
|
+
{saved && <div className="kb-settings-saved">{tr('saved')}</div>}
|
|
256
|
+
</div>
|
|
257
|
+
</div>
|
|
404
258
|
)
|
|
405
259
|
}
|
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
|
|