@fonlan/dsh-task-kanban 0.2.6 → 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 +139 -186
- 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 +0 -107
- package/src/client/board.tsx +5 -2
- package/src/client/icons.tsx +74 -0
- package/src/client/index.tsx +25 -10
- package/src/client/kanban-state.ts +49 -83
- package/src/client/sections.tsx +10 -136
- package/src/index.ts +7 -5
- package/src/server/settings.ts +62 -37
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/settings.ts
CHANGED
|
@@ -1,27 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* User-facing plugin settings (global parallel worker count, per-session-type
|
|
3
|
-
* model/effort/preset defaults),
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
3
|
+
* model/effort/preset defaults), carried by this bundle's entry config: dsh
|
|
4
|
+
* >= 0.1.7 owns plugin settings as entry config (the plugin's `Config`
|
|
5
|
+
* schema), so the face reads the config it was applied with and persists
|
|
6
|
+
* updates through `settings.update`.
|
|
7
7
|
*/
|
|
8
8
|
import type { Context } from '@deepseek-ai/cordis'
|
|
9
9
|
import z from '@deepseek-ai/schemastery'
|
|
10
10
|
import type { KanbanSessionKind, KanbanSettingsShape } from '../shared/card.js'
|
|
11
|
-
import type {} from '@deepseek-ai/dsh-settings'
|
|
12
11
|
|
|
13
12
|
export const KANBAN_SETTINGS_NS = 'task-kanban'
|
|
14
13
|
|
|
15
|
-
export const KanbanSettingsSchema = z.object({
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
14
|
+
export const KanbanSettingsSchema: z<KanbanSettingsShape> = z.object({
|
|
15
|
+
// Volatile: the settings plane (settings page / settings.update) only
|
|
16
|
+
// writes volatile-marked fields on dsh >= 0.1.7.
|
|
17
|
+
maxParallelWorkers: z.natural().min(1).default(1).volatile(),
|
|
18
|
+
refinementModel: z.string().default('').volatile(),
|
|
19
|
+
refinementProvider: z.string().default('').volatile(),
|
|
20
|
+
refinementReasoningEffort: z.string().default('').volatile(),
|
|
21
|
+
refinementPreset: z.string().default('').volatile(),
|
|
22
|
+
phaseModel: z.string().default('').volatile(),
|
|
23
|
+
phaseProvider: z.string().default('').volatile(),
|
|
24
|
+
phaseReasoningEffort: z.string().default('').volatile(),
|
|
25
|
+
phasePreset: z.string().default('').volatile(),
|
|
25
26
|
})
|
|
26
27
|
|
|
27
28
|
/** The per-session-type default slice a session kind resolves. */
|
|
@@ -47,38 +48,59 @@ export interface KanbanSettingsFace {
|
|
|
47
48
|
|
|
48
49
|
/** Structural settings-service subset; the real service resolves the schema. */
|
|
49
50
|
interface SettingsServiceLike {
|
|
50
|
-
register(ns: unknown, schema: unknown): unknown
|
|
51
51
|
update(ns: unknown, patch: Record<string, unknown>, expectedRevision?: number): Promise<unknown>
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
/** One config field as the loader hands it over: a stable volatile reference. */
|
|
55
|
+
interface VolatileField<T> {
|
|
56
|
+
get(): T | undefined
|
|
57
|
+
}
|
|
55
58
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
phaseReasoningEffort: '',
|
|
66
|
-
phasePreset: '',
|
|
59
|
+
/**
|
|
60
|
+
* Dereference one config field. `Config` fields marked `.volatile()` are handed
|
|
61
|
+
* to the plugin as references (`{ get() }`) instead of plain values, so reading
|
|
62
|
+
* them raw yields the reference object — which serializes to `{}` and makes
|
|
63
|
+
* every setting look empty. Non-volatile (plain) configs keep working as-is.
|
|
64
|
+
*/
|
|
65
|
+
function fieldValue<T>(value: unknown, fallback: T): T {
|
|
66
|
+
if (value !== null && typeof value === 'object' && typeof (value as VolatileField<T>).get === 'function') {
|
|
67
|
+
return (value as VolatileField<T>).get() ?? fallback
|
|
67
68
|
}
|
|
69
|
+
return (value as T | undefined) ?? fallback
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const EMPTY_DEFAULTS: KanbanSessionDefaults = { model: '', provider: '', reasoningEffort: '', preset: '' }
|
|
73
|
+
|
|
74
|
+
export function registerSettings(ctx: Context, config: KanbanSettingsShape): KanbanSettingsFace {
|
|
75
|
+
const fields = (config ?? {}) as unknown as Record<string, unknown>
|
|
76
|
+
// Read the references on EVERY access: the settings plane updates them in
|
|
77
|
+
// place (a volatile-only config change does not restart this entry).
|
|
78
|
+
const snapshot = (): KanbanSettingsShape => ({
|
|
79
|
+
maxParallelWorkers: fieldValue(fields.maxParallelWorkers, 1),
|
|
80
|
+
refinementModel: fieldValue(fields.refinementModel, ''),
|
|
81
|
+
refinementProvider: fieldValue(fields.refinementProvider, ''),
|
|
82
|
+
refinementReasoningEffort: fieldValue(fields.refinementReasoningEffort, ''),
|
|
83
|
+
refinementPreset: fieldValue(fields.refinementPreset, ''),
|
|
84
|
+
phaseModel: fieldValue(fields.phaseModel, ''),
|
|
85
|
+
phaseProvider: fieldValue(fields.phaseProvider, ''),
|
|
86
|
+
phaseReasoningEffort: fieldValue(fields.phaseReasoningEffort, ''),
|
|
87
|
+
phasePreset: fieldValue(fields.phasePreset, ''),
|
|
88
|
+
})
|
|
89
|
+
// Optimistic overlay: `settings.update` persists through the profile document
|
|
90
|
+
// and refreshes the references asynchronously, so the write's own caller must
|
|
91
|
+
// still observe the patch it just sent.
|
|
92
|
+
let pending: Partial<KanbanSettingsShape> = {}
|
|
93
|
+
const current = (): KanbanSettingsShape => ({ ...snapshot(), ...pending })
|
|
68
94
|
let service: SettingsServiceLike | undefined
|
|
95
|
+
// Optional settings service: without one the settings still resolve from
|
|
96
|
+
// the entry config; edits just cannot persist.
|
|
69
97
|
ctx.inject(['settings'], (sctx) => {
|
|
70
|
-
service = sctx
|
|
71
|
-
const scope = service.register(KANBAN_SETTINGS_NS, KanbanSettingsSchema) as {
|
|
72
|
-
get(): KanbanSettingsShape
|
|
73
|
-
watch(callback: (next: KanbanSettingsShape) => void): () => void
|
|
74
|
-
}
|
|
75
|
-
current = scope.get()
|
|
76
|
-
scope.watch((next) => { current = next })
|
|
98
|
+
service = (sctx as unknown as { settings?: SettingsServiceLike }).settings
|
|
77
99
|
})
|
|
78
100
|
return {
|
|
79
|
-
get: () => current,
|
|
101
|
+
get: () => current(),
|
|
80
102
|
sessionDefaults: (kind) => {
|
|
81
|
-
const s = current
|
|
103
|
+
const s = current()
|
|
82
104
|
switch (kind) {
|
|
83
105
|
case 'refine':
|
|
84
106
|
return { model: s.refinementModel, provider: s.refinementProvider, reasoningEffort: s.refinementReasoningEffort, preset: s.refinementPreset }
|
|
@@ -108,7 +130,10 @@ export function registerSettings(ctx: Context): KanbanSettingsFace {
|
|
|
108
130
|
if (service === undefined) {
|
|
109
131
|
throw new Error('@fonlan/dsh-task-kanban: settings service is not available in this profile')
|
|
110
132
|
}
|
|
133
|
+
// The settings plane keys plugin settings by PROFILE ENTRY id (the id in
|
|
134
|
+
// cordis.patch.yml), which is what this plugin ships.
|
|
111
135
|
await service.update(KANBAN_SETTINGS_NS, patch as Record<string, unknown>)
|
|
136
|
+
pending = { ...pending, ...patch }
|
|
112
137
|
},
|
|
113
138
|
}
|
|
114
139
|
}
|