@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
|
@@ -1,35 +1,15 @@
|
|
|
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
11
|
export declare const KANBAN_SETTINGS_NS = "task-kanban";
|
|
12
|
-
export declare const KanbanSettingsSchema: z<
|
|
13
|
-
maxParallelWorkers: z<number, number>;
|
|
14
|
-
refinementModel: z<string, string>;
|
|
15
|
-
refinementProvider: z<string, string>;
|
|
16
|
-
refinementReasoningEffort: z<string, string>;
|
|
17
|
-
refinementPreset: z<string, string>;
|
|
18
|
-
phaseModel: z<string, string>;
|
|
19
|
-
phaseProvider: z<string, string>;
|
|
20
|
-
phaseReasoningEffort: z<string, string>;
|
|
21
|
-
phasePreset: z<string, string>;
|
|
22
|
-
}>, Schemastery.ObjectT<{
|
|
23
|
-
maxParallelWorkers: z<number, number>;
|
|
24
|
-
refinementModel: z<string, string>;
|
|
25
|
-
refinementProvider: z<string, string>;
|
|
26
|
-
refinementReasoningEffort: z<string, string>;
|
|
27
|
-
refinementPreset: z<string, string>;
|
|
28
|
-
phaseModel: z<string, string>;
|
|
29
|
-
phaseProvider: z<string, string>;
|
|
30
|
-
phaseReasoningEffort: z<string, string>;
|
|
31
|
-
phasePreset: z<string, string>;
|
|
32
|
-
}>>;
|
|
12
|
+
export declare const KanbanSettingsSchema: z<KanbanSettingsShape>;
|
|
33
13
|
/** The per-session-type default slice a session kind resolves. */
|
|
34
14
|
export interface KanbanSessionDefaults {
|
|
35
15
|
model: string;
|
|
@@ -52,4 +32,4 @@ export interface KanbanSettingsFace {
|
|
|
52
32
|
*/
|
|
53
33
|
sessionDefaults(kind: KanbanSessionKind): KanbanSessionDefaults;
|
|
54
34
|
}
|
|
55
|
-
export declare function registerSettings(ctx: Context): KanbanSettingsFace;
|
|
35
|
+
export declare function registerSettings(ctx: Context, config: KanbanSettingsShape): KanbanSettingsFace;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fonlan/dsh-task-kanban",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.7",
|
|
4
4
|
"description": "DSH web plugin: a task kanban that refines requirements into phased plans, auto-executes them in git worktrees (one session per phase), and auto-merges into the main branch on completion.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -80,52 +80,52 @@
|
|
|
80
80
|
"@deepseek-ai/cordis": "^4.0.2",
|
|
81
81
|
"@deepseek-ai/dsh-agent": "^0.1.5-rc.2",
|
|
82
82
|
"@deepseek-ai/dsh-agent-presets": "^0.1.5-rc.2",
|
|
83
|
-
"@deepseek-ai/dsh-session": "^0.1.5-rc.2",
|
|
84
|
-
"@deepseek-ai/dsh-tools": "^0.1.5-rc.2",
|
|
85
|
-
"@deepseek-ai/dsh-llm": "^0.1.5-rc.2",
|
|
86
|
-
"@deepseek-ai/dsh-settings": "^0.1.5-rc.2",
|
|
87
|
-
"@deepseek-ai/dsh-skill": "^0.1.5-rc.2",
|
|
88
|
-
"@deepseek-ai/schemastery": "^3.18.2",
|
|
89
83
|
"@deepseek-ai/dsh-api-session-controller": "^0.1.5-rc.2",
|
|
90
84
|
"@deepseek-ai/dsh-client-locale": "^0.1.5-rc.2",
|
|
91
85
|
"@deepseek-ai/dsh-client-ui-conversation": "^0.1.5-rc.2",
|
|
92
86
|
"@deepseek-ai/dsh-client-ui-layout": "^0.1.5-rc.2",
|
|
87
|
+
"@deepseek-ai/dsh-client-ui-primitives": "^0.1.5-rc.2",
|
|
93
88
|
"@deepseek-ai/dsh-client-ui-renderer": "^0.1.5-rc.2",
|
|
94
89
|
"@deepseek-ai/dsh-client-ui-session": "^0.1.5-rc.2",
|
|
90
|
+
"@deepseek-ai/dsh-client-ui-settings-plugins": "^0.1.5-rc.2",
|
|
95
91
|
"@deepseek-ai/dsh-client-ui-slots": "^0.1.5-rc.2",
|
|
96
92
|
"@deepseek-ai/dsh-client-ui-workspace": "^0.1.5-rc.2",
|
|
97
|
-
"@deepseek-ai/dsh-client-ui-primitives": "^0.1.5-rc.2",
|
|
98
|
-
"@deepseek-ai/dsh-client-ui-settings-plugins": "^0.1.5-rc.2",
|
|
99
|
-
"react": "^18.2.0",
|
|
100
|
-
"react-dom": "^18.2.0"
|
|
101
|
-
},
|
|
102
|
-
"devDependencies": {
|
|
103
|
-
"@types/node": "^22.0.0",
|
|
104
|
-
"@types/react": "~18.3.1",
|
|
105
|
-
"@types/react-dom": "~18.3.0",
|
|
106
|
-
"tsdown": "^0.22.14",
|
|
107
|
-
"typescript": "^5.9.0",
|
|
108
|
-
"@deepseek-ai/dsh-host-webserver": "^0.1.5-rc.2",
|
|
109
|
-
"@deepseek-ai/dsh-agent-presets": "^0.1.5-rc.2",
|
|
110
|
-
"@deepseek-ai/dsh-agent": "^0.1.5-rc.2",
|
|
111
|
-
"@deepseek-ai/dsh-tools": "^0.1.5-rc.2",
|
|
112
93
|
"@deepseek-ai/dsh-llm": "^0.1.5-rc.2",
|
|
113
94
|
"@deepseek-ai/dsh-session": "^0.1.5-rc.2",
|
|
114
95
|
"@deepseek-ai/dsh-settings": "^0.1.5-rc.2",
|
|
115
96
|
"@deepseek-ai/dsh-skill": "^0.1.5-rc.2",
|
|
97
|
+
"@deepseek-ai/dsh-tools": "^0.1.5-rc.2",
|
|
116
98
|
"@deepseek-ai/schemastery": "^3.18.2",
|
|
99
|
+
"react": "^18.2.0",
|
|
100
|
+
"react-dom": "^18.2.0"
|
|
101
|
+
},
|
|
102
|
+
"devDependencies": {
|
|
103
|
+
"@deepseek-ai/dsh-agent": "^0.1.5-rc.2",
|
|
104
|
+
"@deepseek-ai/dsh-agent-presets": "^0.1.5-rc.2",
|
|
117
105
|
"@deepseek-ai/dsh-api-session-controller": "^0.1.5-rc.2",
|
|
118
106
|
"@deepseek-ai/dsh-client-locale": "^0.1.5-rc.2",
|
|
119
107
|
"@deepseek-ai/dsh-client-ui-conversation": "^0.1.5-rc.2",
|
|
120
108
|
"@deepseek-ai/dsh-client-ui-layout": "^0.1.5-rc.2",
|
|
109
|
+
"@deepseek-ai/dsh-client-ui-primitives": "^0.1.5-rc.2",
|
|
121
110
|
"@deepseek-ai/dsh-client-ui-renderer": "^0.1.5-rc.2",
|
|
122
111
|
"@deepseek-ai/dsh-client-ui-session": "^0.1.5-rc.2",
|
|
112
|
+
"@deepseek-ai/dsh-client-ui-settings": "^0.1.5-rc.2",
|
|
113
|
+
"@deepseek-ai/dsh-client-ui-settings-plugins": "^0.1.5-rc.2",
|
|
123
114
|
"@deepseek-ai/dsh-client-ui-sidebar": "^0.1.5-rc.2",
|
|
124
115
|
"@deepseek-ai/dsh-client-ui-slots": "^0.1.5-rc.2",
|
|
125
116
|
"@deepseek-ai/dsh-client-ui-workspace": "^0.1.5-rc.2",
|
|
126
|
-
"@deepseek-ai/dsh-
|
|
127
|
-
"@deepseek-ai/dsh-
|
|
128
|
-
"@deepseek-ai/dsh-
|
|
117
|
+
"@deepseek-ai/dsh-host-webserver": "^0.1.5-rc.2",
|
|
118
|
+
"@deepseek-ai/dsh-llm": "^0.1.5-rc.2",
|
|
119
|
+
"@deepseek-ai/dsh-session": "^0.1.5-rc.2",
|
|
120
|
+
"@deepseek-ai/dsh-settings": "^0.1.5-rc.2",
|
|
121
|
+
"@deepseek-ai/dsh-skill": "^0.1.5-rc.2",
|
|
122
|
+
"@deepseek-ai/dsh-tools": "^0.1.5-rc.2",
|
|
123
|
+
"@deepseek-ai/schemastery": "^3.18.3",
|
|
124
|
+
"@types/node": "^22.0.0",
|
|
125
|
+
"@types/react": "~18.3.1",
|
|
126
|
+
"@types/react-dom": "~18.3.0",
|
|
127
|
+
"tsdown": "^0.22.14",
|
|
128
|
+
"typescript": "^5.9.0",
|
|
129
129
|
"vitest": "^3.2.0"
|
|
130
130
|
},
|
|
131
131
|
"packageManager": "pnpm@11.21.0"
|
package/src/client/board.css
CHANGED
|
@@ -386,110 +386,3 @@
|
|
|
386
386
|
font-size: 13px;
|
|
387
387
|
line-height: 1.5;
|
|
388
388
|
}
|
|
389
|
-
.kb-footer-action {
|
|
390
|
-
display: inline-flex;
|
|
391
|
-
align-items: center;
|
|
392
|
-
justify-content: center;
|
|
393
|
-
gap: 6px;
|
|
394
|
-
width: 32px;
|
|
395
|
-
height: 32px;
|
|
396
|
-
border-radius: 6px;
|
|
397
|
-
border: none;
|
|
398
|
-
background: transparent;
|
|
399
|
-
/* same foreground as the settings trigger (its trigger css uses this var) */
|
|
400
|
-
color: var(--dsw-alias-label-primary, inherit);
|
|
401
|
-
cursor: pointer;
|
|
402
|
-
}
|
|
403
|
-
.kb-footer-action:hover { background: rgba(128, 128, 128, 0.18); }
|
|
404
|
-
.kb-footer-action-active { background: rgba(59, 130, 246, 0.25); }
|
|
405
|
-
/* Wide mode borrows the settings trigger's metrics (34px tall, 14px label,
|
|
406
|
-
18px icon) so both controls read as siblings on the same line. */
|
|
407
|
-
.kb-footer-action-wide {
|
|
408
|
-
width: auto;
|
|
409
|
-
height: 34px;
|
|
410
|
-
padding: 0 8px;
|
|
411
|
-
border-radius: 12px;
|
|
412
|
-
font-size: 14px;
|
|
413
|
-
}
|
|
414
|
-
.kb-footer-action-wide .kb-footer-label {
|
|
415
|
-
font-size: 14px;
|
|
416
|
-
line-height: 22px;
|
|
417
|
-
}
|
|
418
|
-
/* Theme following is handled by the --dsw-alias-* tokens above: they change
|
|
419
|
-
value with the shell's selected light/dark appearance, so no OS-level
|
|
420
|
-
prefers-color-scheme override is needed (and one would fight the app
|
|
421
|
-
theme when the two disagree). */
|
|
422
|
-
/* Same-line placement WITHOUT moving any DOM: the sidebar foot area becomes
|
|
423
|
-
a reversed flex row, so its LAST child (the settings area) renders on the
|
|
424
|
-
LEFT and the FIRST child (footer actions = the kanban button) on the RIGHT
|
|
425
|
-
— exactly "settings, then kanban". The settings trigger flexes to fill the
|
|
426
|
-
remaining width; the button keeps its content width. */
|
|
427
|
-
.kb-foot-row {
|
|
428
|
-
display: flex;
|
|
429
|
-
flex-direction: row-reverse;
|
|
430
|
-
align-items: stretch;
|
|
431
|
-
gap: 4px;
|
|
432
|
-
}
|
|
433
|
-
.kb-foot-row > div:first-child {
|
|
434
|
-
flex: none;
|
|
435
|
-
width: auto;
|
|
436
|
-
min-width: 0;
|
|
437
|
-
display: flex;
|
|
438
|
-
align-items: center;
|
|
439
|
-
}
|
|
440
|
-
.kb-foot-row > div:last-child {
|
|
441
|
-
flex: 1;
|
|
442
|
-
min-width: 0;
|
|
443
|
-
}
|
|
444
|
-
/* Rail (collapsed sidebar): column-reverse stacks the settings circle on top
|
|
445
|
-
and the kanban icon below it, both centered; the kanban button borrows the
|
|
446
|
-
settings trigger's rail chrome (36px circle) so the two match. */
|
|
447
|
-
.kb-foot-rail {
|
|
448
|
-
flex-direction: column-reverse;
|
|
449
|
-
align-items: center;
|
|
450
|
-
}
|
|
451
|
-
.kb-foot-rail .kb-footer-action {
|
|
452
|
-
width: 36px;
|
|
453
|
-
height: 36px;
|
|
454
|
-
border-radius: 50%;
|
|
455
|
-
margin: 8px 0 10px;
|
|
456
|
-
}
|
|
457
|
-
.kb-footer-label {
|
|
458
|
-
max-width: 110px;
|
|
459
|
-
overflow: hidden;
|
|
460
|
-
text-overflow: ellipsis;
|
|
461
|
-
white-space: nowrap;
|
|
462
|
-
}
|
|
463
|
-
/* Narrow screens (mobile drawer / auto-collapse breakpoint): the drawer's
|
|
464
|
-
foot area is ~256-280px but the footer actions row carries the kanban
|
|
465
|
-
button PLUS mobile-nav's drawer actions (Files / session log), which
|
|
466
|
-
together exceed the row and squeeze the settings trigger to zero width
|
|
467
|
-
(row-reverse overflows left, clipping the button). Below the shell's
|
|
468
|
-
auto-collapse breakpoint, stack the settings row above the actions and
|
|
469
|
-
let the actions wrap so nothing is squeezed or clipped. */
|
|
470
|
-
@media (max-width: 1023px) {
|
|
471
|
-
.kb-foot-row {
|
|
472
|
-
flex-direction: column-reverse;
|
|
473
|
-
align-items: stretch;
|
|
474
|
-
}
|
|
475
|
-
.kb-foot-row > div:first-child {
|
|
476
|
-
flex: none;
|
|
477
|
-
width: 100%;
|
|
478
|
-
display: flex;
|
|
479
|
-
flex-wrap: wrap;
|
|
480
|
-
justify-content: flex-start;
|
|
481
|
-
gap: 4px;
|
|
482
|
-
padding-top: 4px;
|
|
483
|
-
}
|
|
484
|
-
.kb-foot-row > div:last-child {
|
|
485
|
-
flex: none;
|
|
486
|
-
width: 100%;
|
|
487
|
-
}
|
|
488
|
-
/* The footer-actions entries are display:contents wrappers, so the kanban
|
|
489
|
-
button itself is a flex item of the wrapping actions row; pull it to the
|
|
490
|
-
first wrap position so it sits directly below the settings row (the
|
|
491
|
-
mobile-nav drawer actions follow on their own wrapped line). */
|
|
492
|
-
.kb-foot-row .kb-footer-action {
|
|
493
|
-
order: -1;
|
|
494
|
-
}
|
|
495
|
-
}
|
package/src/client/board.tsx
CHANGED
|
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
|
2
2
|
import type { KanbanCard, Lane, PhaseAttempt, PlanPhase } from '../shared/card'
|
|
3
3
|
import { DROP_RULES, laneOf } from '../shared/lanes'
|
|
4
4
|
import { api, gatewaySkillList, type SkillOption } from './api'
|
|
5
|
-
import {
|
|
5
|
+
import { getClient, leaveKanbanPanel } from './kanban-state'
|
|
6
6
|
import { en, zh } from './locales'
|
|
7
7
|
import './board.css'
|
|
8
8
|
|
|
@@ -158,7 +158,10 @@ export function BoardRoot(props: BoardProps): JSX.Element {
|
|
|
158
158
|
|
|
159
159
|
const openSession = useCallback((sessionId: string) => {
|
|
160
160
|
const ctx = getClient()
|
|
161
|
-
|
|
161
|
+
// Return the center column to the Conversation first: `sessions.open` is
|
|
162
|
+
// wrapped by bindSessionNavigation as well, but a runtime without that
|
|
163
|
+
// binding must still leave the board before the session renders.
|
|
164
|
+
leaveKanbanPanel(ctx)
|
|
162
165
|
if (ctx === null) return
|
|
163
166
|
try {
|
|
164
167
|
;(ctx.sessions as { open(id: string): void }).open(sessionId)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import * as primitives from '@deepseek-ai/dsh-client-ui-primitives'
|
|
2
|
+
|
|
3
|
+
/** Props every shell icon accepts: square edge in px, color rides currentColor. */
|
|
4
|
+
export interface IconProps {
|
|
5
|
+
size?: number
|
|
6
|
+
className?: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** One shell icon component. */
|
|
10
|
+
export type IconComponent = (props: IconProps) => JSX.Element
|
|
11
|
+
|
|
12
|
+
/** The runtime export table, read by capability instead of by one hard-coded name. */
|
|
13
|
+
const table = primitives as unknown as Record<string, IconComponent | undefined>
|
|
14
|
+
|
|
15
|
+
/** First name the runtime actually provides (a missing export is `undefined`). */
|
|
16
|
+
function firstDefined(...names: string[]): IconComponent | undefined {
|
|
17
|
+
for (const name of names) {
|
|
18
|
+
let candidate: IconComponent | undefined
|
|
19
|
+
try {
|
|
20
|
+
candidate = table[name]
|
|
21
|
+
} catch {
|
|
22
|
+
// Some module namespaces reject unknown names instead of yielding
|
|
23
|
+
// `undefined`; a rename must never throw on its way to the fallback.
|
|
24
|
+
continue
|
|
25
|
+
}
|
|
26
|
+
if (typeof candidate === 'function') return candidate
|
|
27
|
+
}
|
|
28
|
+
return undefined
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Self-contained 16×16 checklist glyph (the 0.1.7 artwork) used when the shell
|
|
33
|
+
* primitives expose none of the names above: the footer entry must render
|
|
34
|
+
* something even on a runtime whose icon exports we do not know.
|
|
35
|
+
*/
|
|
36
|
+
function ChecklistFallback({ size = 14, className }: IconProps): JSX.Element {
|
|
37
|
+
return (
|
|
38
|
+
<svg
|
|
39
|
+
width={size}
|
|
40
|
+
height={size}
|
|
41
|
+
className={className}
|
|
42
|
+
viewBox="0 0 16 16"
|
|
43
|
+
fill="none"
|
|
44
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
45
|
+
aria-hidden="true"
|
|
46
|
+
strokeWidth={1}
|
|
47
|
+
>
|
|
48
|
+
<path d="M3.75 6.25C4.7165 6.25 5.5 5.4665 5.5 4.5C5.5 3.5335 4.7165 2.75 3.75 2.75C2.7835 2.75 2 3.5335 2 4.5C2 5.4665 2.7835 6.25 3.75 6.25Z" stroke="currentColor" />
|
|
49
|
+
<path d="M7.5 4.5H13.5" stroke="currentColor" />
|
|
50
|
+
<path d="M3.75 13.25C4.7165 13.25 5.5 12.4665 5.5 11.5C5.5 10.5335 4.7165 9.75 3.75 9.75C2.7835 9.75 2 10.5335 2 11.5C2 12.4665 2.7835 13.25 3.75 13.25Z" stroke="currentColor" />
|
|
51
|
+
<path d="M7.5 11.5H13.5" stroke="currentColor" />
|
|
52
|
+
</svg>
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The checklist icon used by the sidebar panel row entry.
|
|
58
|
+
*
|
|
59
|
+
* DSH 0.1.7 renamed every size-suffixed shell icon (`IconChecklistOutline14`)
|
|
60
|
+
* into stroke-weight variants (`…Regular` 1px / `…Medium` 1.3px) inside
|
|
61
|
+
* `@deepseek-ai/dsh-client-ui-primitives`. A stale named import resolves to
|
|
62
|
+
* `undefined` and React then throws "Element type is invalid" while rendering
|
|
63
|
+
* the slot entry — the entry is dropped and the button silently disappears
|
|
64
|
+
* (exactly what upgrading to 0.1.7 did to this plugin). Resolve the name at
|
|
65
|
+
* runtime, newest first, so the next rename degrades to another shipped
|
|
66
|
+
* variant instead of taking the button down with it.
|
|
67
|
+
*/
|
|
68
|
+
export const ChecklistIcon: IconComponent =
|
|
69
|
+
firstDefined(
|
|
70
|
+
'IconChecklistOutlineRegular',
|
|
71
|
+
'IconChecklistOutlineMedium',
|
|
72
|
+
'IconChecklistOutline',
|
|
73
|
+
'IconChecklistOutline14',
|
|
74
|
+
) ?? ChecklistFallback
|
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, bindSessionNavigation } 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,8 +19,7 @@ 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
|
-
// Selecting a session while the board is open must leave the board first.
|
|
22
|
+
// Selecting a session while the board is open must return to the Conversation.
|
|
24
23
|
bindSessionNavigation(ctx)
|
|
25
24
|
|
|
26
25
|
// Live translate bound to the active locale (labels re-read it per call).
|
|
@@ -30,14 +29,30 @@ export function apply(ctx: ClientContext): void {
|
|
|
30
29
|
return () => off()
|
|
31
30
|
}, 'task-kanban: dictionaries')
|
|
32
31
|
|
|
33
|
-
|
|
32
|
+
// The board is a GLOBAL MAIN PANEL: the shell renders one `main` entry per
|
|
33
|
+
// selected panel id, so keying it (instead of replacing `main.conversation`)
|
|
34
|
+
// keeps the Conversation mounted and selectable beside the board, and the
|
|
35
|
+
// sidebar's own panel row owns the button that selects it (the
|
|
36
|
+
// `sidebar.panellist` entry below). Nothing of this plugin sits in the
|
|
37
|
+
// sidebar foot any more, so the Settings trigger can never be squeezed.
|
|
38
|
+
ctx.slots.inject('main', () =>
|
|
34
39
|
ctx.slots.register({
|
|
35
|
-
name: '
|
|
36
|
-
|
|
37
|
-
|
|
40
|
+
name: 'main',
|
|
41
|
+
key: KANBAN_PANEL_ID,
|
|
42
|
+
locale: LOCALE_NS,
|
|
43
|
+
} as never, BoardRoot as never),
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
// Sidebar panel row: the shell renders the row (label, tooltip, active state)
|
|
47
|
+
// and calls `layout.selectPanel(id)` on click; the entry supplies the glyph.
|
|
48
|
+
ctx.slots.inject('sidebar.panellist', () =>
|
|
49
|
+
ctx.slots.register({
|
|
50
|
+
name: 'sidebar.panellist',
|
|
51
|
+
id: KANBAN_PANEL_ID,
|
|
52
|
+
order: 20,
|
|
38
53
|
label: () => t('kanban'),
|
|
39
54
|
locale: LOCALE_NS,
|
|
40
|
-
},
|
|
55
|
+
}, KanbanPanelIcon as never),
|
|
41
56
|
)
|
|
42
57
|
|
|
43
58
|
// The plugin's own settings page (设置 -> 侧栏「任务看板」) rides the
|
|
@@ -53,4 +68,4 @@ export function apply(ctx: ClientContext): void {
|
|
|
53
68
|
locale: LOCALE_NS,
|
|
54
69
|
}, KanbanSettingsSection as never),
|
|
55
70
|
)
|
|
56
|
-
}
|
|
71
|
+
}
|
|
@@ -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
|
+
}
|