@linxin666/dsh-client-ui-task-board 0.3.23 → 0.4.0
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/icon.svg +3 -0
- package/lib/client.js +351 -85
- package/lib/client.js.map +1 -1
- package/lib/index.js +174 -916
- package/lib/types/client/PluginSettingsCard.d.ts +4 -0
- package/lib/types/client/TaskBoardSettingsCard.d.ts +5 -5
- package/lib/types/client/board/TaskForm.d.ts +6 -1
- package/lib/types/client/board/parse-model-pref.d.ts +4 -0
- package/lib/types/client/index.d.ts +43 -10
- package/lib/types/client/locales.d.ts +1 -0
- package/lib/types/client/main-session.d.ts +32 -0
- package/lib/types/client/plugin-card-seat.d.ts +103 -0
- package/lib/types/client/settings-form.d.ts +17 -17
- package/lib/types/core/controller.d.ts +10 -8
- package/lib/types/core/session-reuse.d.ts +4 -2
- package/lib/types/host-ledger.d.ts +15 -0
- package/lib/types/http.d.ts +18 -1
- package/lib/types/index.d.ts +72 -17
- package/package.json +30 -24
- package/src/client/PluginSettingsCard.tsx +7 -2
- package/src/client/TaskBoardSettingsCard.tsx +7 -5
- package/src/client/board/NewTaskModal.tsx +33 -7
- package/src/client/board/TaskForm.tsx +13 -0
- package/src/client/board/parse-model-pref.ts +35 -0
- package/src/client/board.module.css +31 -12
- package/src/client/index.ts +121 -38
- package/src/client/locales.ts +2 -0
- package/src/client/main-session.ts +43 -0
- package/src/client/plugin-card-seat.ts +192 -0
- package/src/client/settings-form.ts +34 -31
- package/src/client/sidebar-entry-core.ts +3 -3
- package/src/client/sidebar-entry.ts +2 -2
- package/src/core/controller.ts +12 -8
- package/src/core/session-reuse.ts +14 -5
- package/src/host-ledger.ts +59 -11
- package/src/http.ts +23 -1
- package/src/index.ts +97 -52
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linxin666/dsh-client-ui-task-board",
|
|
3
3
|
"description": "Host-authoritative task board for the DSH Web GUI with real session execution, Host cron scheduling, and optional cross-platform idle-sleep protection; mounted without DSH source changes.",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.4.0",
|
|
5
|
+
"icon": "icon.svg",
|
|
5
6
|
"type": "module",
|
|
6
7
|
"main": "lib/index.js",
|
|
7
8
|
"types": "lib/types/index.d.ts",
|
|
@@ -23,7 +24,7 @@
|
|
|
23
24
|
},
|
|
24
25
|
"dsh": {
|
|
25
26
|
"engines": {
|
|
26
|
-
"dsh": ">=0.1.
|
|
27
|
+
"dsh": ">=0.1.7-rc.1"
|
|
27
28
|
},
|
|
28
29
|
"bundle": {
|
|
29
30
|
"patch": "./cordis.patch.yml"
|
|
@@ -35,45 +36,49 @@
|
|
|
35
36
|
"@deepseek-ai/dsh-client-ui-renderer",
|
|
36
37
|
"@deepseek-ai/dsh-api-session-controller",
|
|
37
38
|
"@deepseek-ai/dsh-api-workspace-controller",
|
|
38
|
-
"@deepseek-ai/dsh-api-remotes"
|
|
39
|
+
"@deepseek-ai/dsh-api-remotes",
|
|
40
|
+
"@deepseek-ai/dsh-client-ui-workspace"
|
|
39
41
|
],
|
|
40
42
|
"platform": "web"
|
|
41
43
|
}
|
|
42
44
|
},
|
|
43
45
|
"peerDependencies": {
|
|
46
|
+
"@deepseek-ai/dsh": ">=0.1.7-rc.1",
|
|
44
47
|
"react": "^18.2.0"
|
|
45
48
|
},
|
|
46
49
|
"dependencies": {
|
|
47
|
-
"schemastery": "^3.18.
|
|
50
|
+
"@deepseek-ai/schemastery": "^3.18.4"
|
|
48
51
|
},
|
|
49
52
|
"devDependencies": {
|
|
50
|
-
"@deepseek-ai/cordis": "^4.0.
|
|
51
|
-
"@deepseek-ai/dsh-agent": "^0.1.
|
|
52
|
-
"@deepseek-ai/dsh-agent-
|
|
53
|
-
"@deepseek-ai/dsh-api-gateway": "^0.1.
|
|
54
|
-
"@deepseek-ai/dsh-api-remotes": "^0.1.
|
|
55
|
-
"@deepseek-ai/dsh-
|
|
56
|
-
"@deepseek-ai/dsh-api-
|
|
57
|
-
"@deepseek-ai/dsh-
|
|
58
|
-
"@deepseek-ai/dsh-client-
|
|
59
|
-
"@deepseek-ai/dsh-client-
|
|
60
|
-
"@deepseek-ai/dsh-client-
|
|
61
|
-
"@deepseek-ai/dsh-client-ui-
|
|
62
|
-
"@deepseek-ai/dsh-client-ui-
|
|
63
|
-
"@deepseek-ai/dsh-client-ui-
|
|
64
|
-
"@deepseek-ai/dsh-commands": "^0.1.
|
|
65
|
-
"@deepseek-ai/dsh-host-webserver": "^0.1.
|
|
66
|
-
"@deepseek-ai/dsh-settings": "^0.1.
|
|
67
|
-
"@deepseek-ai/dsh-system-prompt": "^0.1.
|
|
53
|
+
"@deepseek-ai/cordis": "^4.0.4",
|
|
54
|
+
"@deepseek-ai/dsh-agent": "^0.1.7-rc.1",
|
|
55
|
+
"@deepseek-ai/dsh-agent-preset-registry": "^0.1.7-rc.1",
|
|
56
|
+
"@deepseek-ai/dsh-api-gateway": "^0.1.7-rc.1",
|
|
57
|
+
"@deepseek-ai/dsh-api-remotes": "^0.1.7-rc.1",
|
|
58
|
+
"@deepseek-ai/dsh-api-session-controller": "^0.1.7-rc.1",
|
|
59
|
+
"@deepseek-ai/dsh-api-workspace-controller": "^0.1.7-rc.1",
|
|
60
|
+
"@deepseek-ai/dsh-client-connection": "^0.1.7-rc.1",
|
|
61
|
+
"@deepseek-ai/dsh-client-locale": "^0.1.7-rc.1",
|
|
62
|
+
"@deepseek-ai/dsh-client-store": "^0.1.7-rc.1",
|
|
63
|
+
"@deepseek-ai/dsh-client-ui-renderer": "^0.1.7-rc.1",
|
|
64
|
+
"@deepseek-ai/dsh-client-ui-settings": "^0.1.7-rc.1",
|
|
65
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.1.7-rc.1",
|
|
66
|
+
"@deepseek-ai/dsh-client-ui-workspace": "^0.1.7-rc.1",
|
|
67
|
+
"@deepseek-ai/dsh-commands": "^0.1.7-rc.1",
|
|
68
|
+
"@deepseek-ai/dsh-host-webserver": "^0.1.7-rc.1",
|
|
69
|
+
"@deepseek-ai/dsh-settings": "^0.1.7-rc.1",
|
|
70
|
+
"@deepseek-ai/dsh-system-prompt": "^0.1.7-rc.1",
|
|
71
|
+
"@deepseek-ai/dsh-workspace": "^0.1.7-rc.1",
|
|
68
72
|
"@types/node": "^22.20.0",
|
|
69
73
|
"@types/react": "~18.3.1",
|
|
70
74
|
"@types/react-dom": "^18.3.5",
|
|
75
|
+
"@vitest/coverage-v8": "^4.1.11",
|
|
71
76
|
"jsdom": "^25.0.0",
|
|
72
77
|
"react": "^18.3.1",
|
|
73
78
|
"react-dom": "^18.3.1",
|
|
74
79
|
"tsdown": "0.22.2",
|
|
75
80
|
"typescript": "~5.7.2",
|
|
76
|
-
"vitest": "^
|
|
81
|
+
"vitest": "^4.1.11"
|
|
77
82
|
},
|
|
78
83
|
"files": [
|
|
79
84
|
"lib/**/*.js",
|
|
@@ -81,7 +86,8 @@
|
|
|
81
86
|
"lib/**/*.d.ts",
|
|
82
87
|
"lib/**/*.d.ts.map",
|
|
83
88
|
"src",
|
|
84
|
-
"cordis.patch.yml"
|
|
89
|
+
"cordis.patch.yml",
|
|
90
|
+
"icon.svg"
|
|
85
91
|
],
|
|
86
92
|
"license": "Apache-2.0",
|
|
87
93
|
"repository": {
|
|
@@ -64,6 +64,10 @@ export interface PluginSettingsCardProps<TKey extends string = string> {
|
|
|
64
64
|
* edits, so the footer would sit there permanently disabled.
|
|
65
65
|
*/
|
|
66
66
|
hideFooter?: boolean
|
|
67
|
+
/** Render children even when the settings namespace is not exposed by the host. */
|
|
68
|
+
renderChildrenWhenNotExposed?: boolean
|
|
69
|
+
/** Hide the 'not exposed' warning banner when the settings namespace is not exposed. */
|
|
70
|
+
hideNotExposedNotice?: boolean
|
|
67
71
|
/** The plugin's controls. */
|
|
68
72
|
children: ReactNode
|
|
69
73
|
}
|
|
@@ -126,14 +130,15 @@ export function PluginSettingsCard<TKey extends string = string>(props: PluginSe
|
|
|
126
130
|
// official settings allowlist omits third-party namespaces): show a card
|
|
127
131
|
// that explains the gap instead of vanishing, so a missing card never
|
|
128
132
|
// reads as a missing plugin.
|
|
129
|
-
if (!state.exposed) {
|
|
133
|
+
if (!state.exposed && props.renderChildrenWhenNotExposed !== true) {
|
|
134
|
+
const showNotice = props.hideNotExposedNotice !== true
|
|
130
135
|
return (
|
|
131
136
|
<li className={cardClass}>
|
|
132
137
|
{header}
|
|
133
138
|
{expanded
|
|
134
139
|
? (
|
|
135
140
|
<div className={css.body}>
|
|
136
|
-
<p className={css.notExposed} role="status">{props.t('settings.notExposed')}</p>
|
|
141
|
+
{showNotice ? <p className={css.notExposed} role="status">{props.t('settings.notExposed')}</p> : null}
|
|
137
142
|
</div>
|
|
138
143
|
)
|
|
139
144
|
: null}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
|
|
8
8
|
import type { SnapshotStore } from '@deepseek-ai/dsh-client-store'
|
|
9
|
-
import type {
|
|
9
|
+
import type { ConfigForm } from '@deepseek-ai/dsh-client-ui-settings/client'
|
|
10
10
|
import { useEffect, useState } from 'react'
|
|
11
11
|
import type { TaskBoardPowerSnapshot } from '../protocol.ts'
|
|
12
12
|
import { PluginSettingsCard, BooleanField } from './PluginSettingsCard.tsx'
|
|
@@ -40,13 +40,13 @@ export interface TaskBoardSettingsCardFace extends CardActions {
|
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
/** Bridges the `task-board`
|
|
43
|
+
/** Bridges the `task-board` settings form onto the card's staged form. */
|
|
44
44
|
export class TaskBoardSettingsCardController {
|
|
45
45
|
private readonly form: CardForm<TaskBoardSettings>
|
|
46
46
|
private readonly store: SnapshotStore<TaskBoardSettingsCardState>
|
|
47
47
|
|
|
48
|
-
/** @param scope - the bound
|
|
49
|
-
constructor(scope:
|
|
48
|
+
/** @param scope - the bound configuration form for the `task-board` namespace. */
|
|
49
|
+
constructor(scope: ConfigForm<TaskBoardSettings>) {
|
|
50
50
|
this.form = new CardForm(scope, [
|
|
51
51
|
booleanField('enabled'),
|
|
52
52
|
booleanField('announceToAgent'),
|
|
@@ -73,7 +73,7 @@ export class TaskBoardSettingsCardController {
|
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
/**
|
|
76
|
-
* Release the card's
|
|
76
|
+
* Release the card's form subscription and bound stores; the slot
|
|
77
77
|
* disposer calls this on teardown.
|
|
78
78
|
*/
|
|
79
79
|
dispose(): void {
|
|
@@ -126,6 +126,8 @@ export function TaskBoardSettingsCard(props: TaskBoardSettingsCardProps) {
|
|
|
126
126
|
descriptionKey="settings.description"
|
|
127
127
|
defaultOpen={false}
|
|
128
128
|
state={state}
|
|
129
|
+
renderChildrenWhenNotExposed
|
|
130
|
+
hideNotExposedNotice
|
|
129
131
|
onSave={props.save}
|
|
130
132
|
onDiscard={props.discard}
|
|
131
133
|
>
|
|
@@ -10,6 +10,7 @@ import { collectKnownTags, TASK_PERMISSIONS, type TaskPermission, type TaskRecor
|
|
|
10
10
|
import { t, type TaskBoardKey } from '../locales.ts'
|
|
11
11
|
import { SCHEDULE_PRESETS } from '../schedule-presets.ts'
|
|
12
12
|
import { ModalShell, TaskContentFields, TaskTagFields, cleanTags } from './TaskForm.tsx'
|
|
13
|
+
import { readParseModelPreference, writeParseModelPreference } from './parse-model-pref.ts'
|
|
13
14
|
import css from '../board.module.css'
|
|
14
15
|
|
|
15
16
|
export interface NewTaskModalProps {
|
|
@@ -54,7 +55,9 @@ export function NewTaskModal({ controller, onClose, initialTask, defaultWorkspac
|
|
|
54
55
|
// parse face; the section stays hidden otherwise.
|
|
55
56
|
const [canParse] = useState(controller.getSnapshot().canParseTask === true)
|
|
56
57
|
const [parseText, setParseText] = useState('')
|
|
57
|
-
|
|
58
|
+
// Issue #1621: start from the model this browser used last, not from the
|
|
59
|
+
// roster's first entry; '' is the Host default and stays valid.
|
|
60
|
+
const [parseModel, setParseModel] = useState(() => readParseModelPreference())
|
|
58
61
|
const [parsePending, setParsePending] = useState(false)
|
|
59
62
|
const [parseError, setParseError] = useState<string | undefined>(undefined)
|
|
60
63
|
const parseAbort = useRef<AbortController | undefined>(undefined)
|
|
@@ -67,10 +70,14 @@ export function NewTaskModal({ controller, onClose, initialTask, defaultWorkspac
|
|
|
67
70
|
[controller],
|
|
68
71
|
)
|
|
69
72
|
|
|
70
|
-
// The model roster arrives asynchronously
|
|
71
|
-
// the
|
|
73
|
+
// The model roster arrives asynchronously. A remembered model the deployment
|
|
74
|
+
// no longer offers falls back to the Host default; an empty value is the
|
|
75
|
+
// Host-default choice and is never overwritten by the roster (issue #1621).
|
|
72
76
|
useEffect(() => {
|
|
73
|
-
if (parseModel === ''
|
|
77
|
+
if (parseModel === '' || parseModels.length === 0) return
|
|
78
|
+
if (parseModels.some(option => option.id === parseModel)) return
|
|
79
|
+
setParseModel('')
|
|
80
|
+
writeParseModelPreference('')
|
|
74
81
|
}, [parseModel, options.models])
|
|
75
82
|
|
|
76
83
|
const runParse = async (): Promise<void> => {
|
|
@@ -97,7 +104,13 @@ export function NewTaskModal({ controller, onClose, initialTask, defaultWorkspac
|
|
|
97
104
|
}
|
|
98
105
|
}
|
|
99
106
|
|
|
100
|
-
|
|
107
|
+
/**
|
|
108
|
+
* Create the task through the Host, then optionally start it.
|
|
109
|
+
* @param runAfterCreate - true for the "create and run" action: the task is
|
|
110
|
+
* committed either way, and a refused start opens the task instead of
|
|
111
|
+
* reporting the creation as failed.
|
|
112
|
+
*/
|
|
113
|
+
const submit = async (runAfterCreate: boolean): Promise<void> => {
|
|
101
114
|
if (scheduleEnabled) {
|
|
102
115
|
const cron = scheduleCron.trim()
|
|
103
116
|
if (cron === '' || !isValidCron(cron)) {
|
|
@@ -156,6 +169,14 @@ export function NewTaskModal({ controller, onClose, initialTask, defaultWorkspac
|
|
|
156
169
|
await controller.archiveTask(initialTask.id)
|
|
157
170
|
}
|
|
158
171
|
}
|
|
172
|
+
if (runAfterCreate) {
|
|
173
|
+
// The task exists from here on, so a refused start (a permission above
|
|
174
|
+
// the session default, a pinned target that went stale) must not read as
|
|
175
|
+
// a failed creation: open the task, whose detail view owns the
|
|
176
|
+
// confirmation step and the refusal message.
|
|
177
|
+
const started = await controller.runTask(task.id)
|
|
178
|
+
if (!started) controller.openTask(task.id)
|
|
179
|
+
}
|
|
159
180
|
onClose()
|
|
160
181
|
}
|
|
161
182
|
|
|
@@ -173,8 +194,9 @@ export function NewTaskModal({ controller, onClose, initialTask, defaultWorkspac
|
|
|
173
194
|
error={error}
|
|
174
195
|
pending={pending}
|
|
175
196
|
submitLabel={t('new.submit')}
|
|
176
|
-
onSubmit={() => { void submit() }}
|
|
197
|
+
onSubmit={() => { void submit(false) }}
|
|
177
198
|
onClose={onClose}
|
|
199
|
+
secondaryAction={{ label: t('new.createAndRun'), onSubmit: () => { void submit(true) } }}
|
|
178
200
|
>
|
|
179
201
|
{canParse && (
|
|
180
202
|
<section className={css.aiParse} data-dsh-part="ai-parse">
|
|
@@ -193,8 +215,12 @@ export function NewTaskModal({ controller, onClose, initialTask, defaultWorkspac
|
|
|
193
215
|
className={css.select}
|
|
194
216
|
value={parseModel}
|
|
195
217
|
aria-label={t('new.aiParseModel')}
|
|
196
|
-
onChange={event => {
|
|
218
|
+
onChange={event => {
|
|
219
|
+
setParseModel(event.target.value)
|
|
220
|
+
writeParseModelPreference(event.target.value)
|
|
221
|
+
}}
|
|
197
222
|
>
|
|
223
|
+
<option value="">{t('exec.model.default')}</option>
|
|
198
224
|
{parseModels.map(option => (
|
|
199
225
|
<option key={option.id} value={option.id}>{option.name ?? option.id}</option>
|
|
200
226
|
))}
|
|
@@ -21,6 +21,7 @@ export function ModalShell({
|
|
|
21
21
|
submitLabel,
|
|
22
22
|
onSubmit,
|
|
23
23
|
onClose,
|
|
24
|
+
secondaryAction,
|
|
24
25
|
children,
|
|
25
26
|
}: {
|
|
26
27
|
ariaLabel: string
|
|
@@ -30,6 +31,8 @@ export function ModalShell({
|
|
|
30
31
|
submitLabel: string
|
|
31
32
|
onSubmit: () => void
|
|
32
33
|
onClose: () => void
|
|
34
|
+
/** Optional second action beside the primary submit (e.g. create and run). */
|
|
35
|
+
secondaryAction?: { label: string; onSubmit: () => void }
|
|
33
36
|
children: ReactNode
|
|
34
37
|
}) {
|
|
35
38
|
return (
|
|
@@ -50,6 +53,16 @@ export function ModalShell({
|
|
|
50
53
|
<button type="button" className={css.ghostButton} onClick={onClose}>
|
|
51
54
|
{t('new.cancel')}
|
|
52
55
|
</button>
|
|
56
|
+
{secondaryAction !== undefined && (
|
|
57
|
+
<button
|
|
58
|
+
type="button"
|
|
59
|
+
className={css.ghostButton}
|
|
60
|
+
disabled={pending}
|
|
61
|
+
onClick={secondaryAction.onSubmit}
|
|
62
|
+
>
|
|
63
|
+
{secondaryAction.label}
|
|
64
|
+
</button>
|
|
65
|
+
)}
|
|
53
66
|
<button type="submit" className={css.primaryButton} disabled={pending}>
|
|
54
67
|
{submitLabel}
|
|
55
68
|
</button>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The model the user last picked for AI parsing, remembered per browser so the
|
|
3
|
+
* next new-task modal starts from it instead of the roster's first entry
|
|
4
|
+
* (issue #1621). An empty value is a real choice — "let the Host use its
|
|
5
|
+
* default model" — and is remembered as such.
|
|
6
|
+
*/
|
|
7
|
+
const PARSE_MODEL_KEY = 'dsh-task-board.parse-model'
|
|
8
|
+
|
|
9
|
+
/** The nearest browser storage, or undefined when the global itself throws. */
|
|
10
|
+
function defaultStorage(): Storage | undefined {
|
|
11
|
+
try {
|
|
12
|
+
return globalThis.localStorage
|
|
13
|
+
} catch {
|
|
14
|
+
return undefined
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** The remembered parse model; '' means the Host default (or no preference). */
|
|
19
|
+
export function readParseModelPreference(storage: Pick<Storage, 'getItem'> | undefined = defaultStorage()): string {
|
|
20
|
+
try {
|
|
21
|
+
return storage?.getItem(PARSE_MODEL_KEY) ?? ''
|
|
22
|
+
} catch {
|
|
23
|
+
// A blocked or full store must never break the form.
|
|
24
|
+
return ''
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Remember the picked parse model; '' records the Host-default choice. */
|
|
29
|
+
export function writeParseModelPreference(model: string, storage: Pick<Storage, 'setItem'> | undefined = defaultStorage()): void {
|
|
30
|
+
try {
|
|
31
|
+
storage?.setItem(PARSE_MODEL_KEY, model)
|
|
32
|
+
} catch {
|
|
33
|
+
// Storage is optional: the picker still works without it.
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -50,20 +50,27 @@ html[data-dsh-taskboard-active]:not([data-dsh-ssh-active]) [class*='centerCol']
|
|
|
50
50
|
|
|
51
51
|
/* --- sidebar entry row ------------------------------------------------------- */
|
|
52
52
|
|
|
53
|
+
/* Same geometry as the shell's own panel rows (SidebarRoot .panelRow): 2px
|
|
54
|
+
inset with a 12px radius, 8px content padding, 36px row, 14/22 type, and the
|
|
55
|
+
primary ink — so an injected row and an official row line up column for
|
|
56
|
+
column instead of each carrying its own box. */
|
|
53
57
|
.entry {
|
|
54
58
|
box-sizing: border-box;
|
|
55
59
|
display: flex;
|
|
56
60
|
align-items: center;
|
|
57
61
|
gap: 8px;
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
62
|
+
min-height: 36px;
|
|
63
|
+
padding: 7px 8px;
|
|
64
|
+
margin: 0 2px;
|
|
61
65
|
background: transparent;
|
|
62
66
|
border: none;
|
|
63
|
-
border-radius:
|
|
64
|
-
color: var(--dsw-alias-label-
|
|
67
|
+
border-radius: 12px;
|
|
68
|
+
color: var(--dsw-alias-label-primary);
|
|
65
69
|
cursor: pointer;
|
|
66
|
-
font
|
|
70
|
+
font: inherit;
|
|
71
|
+
font-size: 14px;
|
|
72
|
+
line-height: 22px;
|
|
73
|
+
text-align: left;
|
|
67
74
|
white-space: nowrap;
|
|
68
75
|
}
|
|
69
76
|
|
|
@@ -78,19 +85,21 @@ html[data-dsh-taskboard-active]:not([data-dsh-ssh-active]) [class*='centerCol']
|
|
|
78
85
|
font-weight: 600;
|
|
79
86
|
}
|
|
80
87
|
|
|
88
|
+
/* Icon box matches the shell's panel glyph exactly (16px), so the glyph column
|
|
89
|
+
and the label column land on the official rows' x positions. */
|
|
81
90
|
.entryIcon {
|
|
82
91
|
display: inline-flex;
|
|
83
92
|
align-items: center;
|
|
84
93
|
justify-content: center;
|
|
85
|
-
width:
|
|
86
|
-
height:
|
|
94
|
+
width: 16px;
|
|
95
|
+
height: 16px;
|
|
87
96
|
flex: none;
|
|
88
97
|
}
|
|
89
98
|
|
|
90
99
|
.entryIcon svg {
|
|
91
100
|
display: block;
|
|
92
|
-
width:
|
|
93
|
-
height:
|
|
101
|
+
width: 16px;
|
|
102
|
+
height: 16px;
|
|
94
103
|
}
|
|
95
104
|
|
|
96
105
|
.entryLabel {
|
|
@@ -98,7 +107,9 @@ html[data-dsh-taskboard-active]:not([data-dsh-ssh-active]) [class*='centerCol']
|
|
|
98
107
|
text-overflow: ellipsis;
|
|
99
108
|
}
|
|
100
109
|
|
|
101
|
-
/* Collapsed rail: icon-only, centered, matching the shell's 56px rail.
|
|
110
|
+
/* Collapsed rail: icon-only, centered, matching the shell's 56px rail. The
|
|
111
|
+
shell asks its own panel rows for an 18px glyph on the rail, so the icon box
|
|
112
|
+
and the svg step up together with the 12px rail radius. */
|
|
102
113
|
:global([data-dsh-frame][data-sidebar-collapsed]) .entry,
|
|
103
114
|
:global([data-sidebar-collapsed]) .entry {
|
|
104
115
|
justify-content: center;
|
|
@@ -106,7 +117,15 @@ html[data-dsh-taskboard-active]:not([data-dsh-ssh-active]) [class*='centerCol']
|
|
|
106
117
|
width: 36px;
|
|
107
118
|
height: 36px;
|
|
108
119
|
margin: 0 auto 12px;
|
|
109
|
-
border-radius:
|
|
120
|
+
border-radius: 12px;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
:global([data-dsh-frame][data-sidebar-collapsed]) .entryIcon,
|
|
124
|
+
:global([data-sidebar-collapsed]) .entryIcon,
|
|
125
|
+
:global([data-dsh-frame][data-sidebar-collapsed]) .entryIcon svg,
|
|
126
|
+
:global([data-sidebar-collapsed]) .entryIcon svg {
|
|
127
|
+
width: 18px;
|
|
128
|
+
height: 18px;
|
|
110
129
|
}
|
|
111
130
|
|
|
112
131
|
:global([data-dsh-frame][data-sidebar-collapsed]) .entryLabel,
|
package/src/client/index.ts
CHANGED
|
@@ -12,16 +12,20 @@ import type { Context as ClientContext } from '@deepseek-ai/cordis'
|
|
|
12
12
|
import type { ClientRemote, SessionId } from '@deepseek-ai/dsh-api-remotes/client'
|
|
13
13
|
import type { ISessions } from '@deepseek-ai/dsh-api-session-controller/client'
|
|
14
14
|
import type { IWorkspaces } from '@deepseek-ai/dsh-api-workspace-controller/client'
|
|
15
|
-
import type {
|
|
15
|
+
import type { ConfigForm, ConfigForms } from '@deepseek-ai/dsh-client-ui-settings/client'
|
|
16
16
|
import type {} from '@deepseek-ai/dsh-api-remotes/client'
|
|
17
17
|
import type {} from '@deepseek-ai/dsh-client-ui-renderer/client'
|
|
18
18
|
import type {} from '@deepseek-ai/dsh-client-ui-slots'
|
|
19
19
|
// Type-only: pulls the locale plugin's Context merge (ctx.locale) and its
|
|
20
20
|
// LocaleNamespaceMap merge table.
|
|
21
21
|
import type {} from '@deepseek-ai/dsh-client-locale/client'
|
|
22
|
-
// Type-only: pulls the
|
|
22
|
+
// Type-only: pulls the shared-forms Context merge (ctx.configForms).
|
|
23
23
|
import type {} from '@deepseek-ai/dsh-client-ui-settings/client'
|
|
24
|
+
// Type-only: pulls the workspace plugin's Context merge (ctx.uiWorkspace), the
|
|
25
|
+
// multi-instance navigation face that replaced ISessions.open().
|
|
26
|
+
import type {} from '@deepseek-ai/dsh-client-ui-workspace/client'
|
|
24
27
|
import { BoardController } from '../core/controller.ts'
|
|
28
|
+
import { mainViewSessionId } from './main-session.ts'
|
|
25
29
|
import { LocalStorageTaskStore } from '../core/store.ts'
|
|
26
30
|
import { claimTaskboardApply, releaseTaskboardApply } from './apply-guard.ts'
|
|
27
31
|
import { mountBoard } from './board-mount.tsx'
|
|
@@ -30,13 +34,53 @@ import { TaskBoardSettingsCard, TaskBoardSettingsCardController, type TaskBoardS
|
|
|
30
34
|
import { en, zh, setRuntimeTranslate, type TaskBoardKey } from './locales.ts'
|
|
31
35
|
import { HttpTaskBoardHostTransport } from './host-api.ts'
|
|
32
36
|
import { reportDailyHeartbeat } from './telemetry.ts'
|
|
37
|
+
import { installPluginCard } from './plugin-card-seat.ts'
|
|
33
38
|
|
|
34
39
|
/** Locale namespace this plugin owns. */
|
|
35
40
|
const NS = 'task-board'
|
|
36
41
|
|
|
37
|
-
/** Settings namespace
|
|
42
|
+
/** Settings namespace this card edits (the family identity of the plugin's own settings form). */
|
|
38
43
|
const TASK_BOARD_NS = 'task-board'
|
|
39
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Profile entry id the family aggregate's generated row carries — the
|
|
47
|
+
* deployment shape nearly every user runs. Under 0.1.7 a settings form is
|
|
48
|
+
* addressed by profile entry id, so the shared-forms fallback below has to
|
|
49
|
+
* name it; the family binder resolves the family namespace instead.
|
|
50
|
+
*/
|
|
51
|
+
const AGGREGATE_ENTRY_ID = 'web-ui-task-board'
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Profile entry ids this package's two patch rows carry: the aggregate's
|
|
55
|
+
* generated row and the standalone bundle patch's row (`ui-task-board`), plus
|
|
56
|
+
* the bare namespace as the last resort for a Host whose descriptor is keyed
|
|
57
|
+
* by the family namespace itself.
|
|
58
|
+
*/
|
|
59
|
+
const TASK_BOARD_ENTRY_IDS: readonly string[] = [AGGREGATE_ENTRY_ID, 'ui-task-board', TASK_BOARD_NS]
|
|
60
|
+
|
|
61
|
+
/** Domain-owned description of one settings namespace a family card binds. */
|
|
62
|
+
export interface SettingsFormSpec<T> {
|
|
63
|
+
/** Settings namespace the card edits. */
|
|
64
|
+
namespace: string
|
|
65
|
+
/**
|
|
66
|
+
* Narrow one wire section; undefined keeps the last accepted value. The
|
|
67
|
+
* shared form already resolves the namespace's own serialized wire schema,
|
|
68
|
+
* so a decoder exists only to narrow beyond that schema.
|
|
69
|
+
*/
|
|
70
|
+
decode?: (section: unknown) => T | undefined
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The family settings binder published by dsh-web-settings. Its `bind` resolves
|
|
75
|
+
* a family namespace to the profile entry id that owns it and hands back the
|
|
76
|
+
* shared configuration form, so it is the only seat that can reach this card's
|
|
77
|
+
* form on a Host whose row id is not the namespace.
|
|
78
|
+
*/
|
|
79
|
+
export interface SettingsFormBinder {
|
|
80
|
+
/** Bind one family settings namespace. */
|
|
81
|
+
bind<T>(spec: SettingsFormSpec<T>): ConfigForm<T>
|
|
82
|
+
}
|
|
83
|
+
|
|
40
84
|
declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
41
85
|
interface LocaleNamespaceMap {
|
|
42
86
|
/** Task-board surface copy. */
|
|
@@ -46,9 +90,9 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
|
46
90
|
interface SlotMap {
|
|
47
91
|
/**
|
|
48
92
|
* The child slot the Web UI plugin group declares; this card registers
|
|
49
|
-
* into the group
|
|
50
|
-
* Spelled here with the same shape so this
|
|
51
|
-
* depending on the sibling UI package.
|
|
93
|
+
* into the group's list seat rather than the official
|
|
94
|
+
* bundle-configuration seat. Spelled here with the same shape so this
|
|
95
|
+
* package can register without depending on the sibling UI package.
|
|
52
96
|
*/
|
|
53
97
|
'web-ui.plugin.item': { kind: 'list'; scope: 'root'; owner: SettingsPluginItemOwnerProps }
|
|
54
98
|
}
|
|
@@ -63,11 +107,11 @@ export interface SettingsPluginItemOwnerProps {
|
|
|
63
107
|
declare module '@deepseek-ai/cordis' {
|
|
64
108
|
interface Context {
|
|
65
109
|
/**
|
|
66
|
-
* Optional
|
|
67
|
-
*
|
|
68
|
-
*
|
|
110
|
+
* Optional family settings binder provided by dsh-web-settings; absent
|
|
111
|
+
* when that group plugin is not installed, so callers fall back to the
|
|
112
|
+
* shared configuration forms service.
|
|
69
113
|
*/
|
|
70
|
-
webUiSettings?:
|
|
114
|
+
webUiSettings?: SettingsFormBinder
|
|
71
115
|
}
|
|
72
116
|
}
|
|
73
117
|
|
|
@@ -80,7 +124,7 @@ declare module '@deepseek-ai/cordis' {
|
|
|
80
124
|
* on hosts below that cohort, which serve the same roster through the
|
|
81
125
|
* connection RPC face.
|
|
82
126
|
*/
|
|
83
|
-
export const inject = ['slots', 'sessions', 'workspaces', 'connection', '
|
|
127
|
+
export const inject = ['slots', 'sessions', 'workspaces', 'connection', 'configForms', 'locale', 'remote', 'remote.session', 'uiWorkspace']
|
|
84
128
|
|
|
85
129
|
/** One agent-preset row the mode picker consumes (either face's wire shape). */
|
|
86
130
|
interface PresetRosterRow {
|
|
@@ -164,31 +208,23 @@ export function apply(ctx: ClientContext): void {
|
|
|
164
208
|
try { setRuntimeTranslate(ctx.locale.bind(NS)) } catch { /* locale missing: document-language fallback stays */ }
|
|
165
209
|
|
|
166
210
|
// Plugin configuration card: one staged form over the `task-board` settings
|
|
167
|
-
// namespace, contributed to
|
|
168
|
-
|
|
169
|
-
const
|
|
170
|
-
const settingsCard = new TaskBoardSettingsCardController(
|
|
171
|
-
ctx
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
inject: () => settingsCard.inject(),
|
|
179
|
-
}, TaskBoardSettingsCard)
|
|
180
|
-
return () => {
|
|
181
|
-
settingsCard.dispose()
|
|
182
|
-
unregister()
|
|
183
|
-
}
|
|
184
|
-
} catch {
|
|
185
|
-
return () => {}
|
|
186
|
-
}
|
|
211
|
+
// namespace, contributed to whichever plugin-card seat this host declares
|
|
212
|
+
// (the family group's list seat, or the official bundle-configuration seat).
|
|
213
|
+
const settingsForm = bindSettingsForm(ctx)
|
|
214
|
+
const settingsCard = new TaskBoardSettingsCardController(settingsForm)
|
|
215
|
+
installPluginCard(ctx, {
|
|
216
|
+
bundle: '@linxin666/dsh-client-ui-task-board',
|
|
217
|
+
id: 'task-board',
|
|
218
|
+
order: 110,
|
|
219
|
+
locale: NS,
|
|
220
|
+
inject: () => settingsCard.inject(),
|
|
221
|
+
component: TaskBoardSettingsCard,
|
|
187
222
|
})
|
|
223
|
+
ctx.effect(() => () => { settingsCard.dispose() }, 'task-board: settings card')
|
|
188
224
|
|
|
189
|
-
// The sidebar entry and board view mount once the settings
|
|
190
|
-
// while the
|
|
191
|
-
// nothing mounts yet. Only an unavailable
|
|
225
|
+
// The sidebar entry and board view mount once the settings form settles;
|
|
226
|
+
// while the form is still loading, the composition default is unknown, so
|
|
227
|
+
// nothing mounts yet. Only an unavailable form (no settings surface served)
|
|
192
228
|
// falls back to the composition default (enabled).
|
|
193
229
|
let uiDisposer: (() => void) | undefined
|
|
194
230
|
const mountUi = (): void => {
|
|
@@ -206,8 +242,12 @@ export function apply(ctx: ClientContext): void {
|
|
|
206
242
|
store,
|
|
207
243
|
transport: new HttpTaskBoardHostTransport(),
|
|
208
244
|
sessions: {
|
|
209
|
-
|
|
210
|
-
|
|
245
|
+
// The main-view Session comes from the catalog's per-source ownership
|
|
246
|
+
// counts; navigation belongs to the workspace UI since the
|
|
247
|
+
// multi-instance Client Session model.
|
|
248
|
+
current: () => mainViewSessionId(sessions.list.getSnapshot().byId),
|
|
249
|
+
open: id => ctx.uiWorkspace.openSession(id as never),
|
|
250
|
+
subscribe: fn => sessions.list.subscribe(fn),
|
|
211
251
|
},
|
|
212
252
|
})
|
|
213
253
|
controller.start()
|
|
@@ -337,13 +377,56 @@ export function apply(ctx: ClientContext): void {
|
|
|
337
377
|
}
|
|
338
378
|
}
|
|
339
379
|
const syncEnabled = (): void => {
|
|
340
|
-
const snapshot =
|
|
380
|
+
const snapshot = settingsForm.getSnapshot()
|
|
341
381
|
const enabled = snapshot.status === 'ready'
|
|
342
382
|
? snapshot.value?.enabled ?? true
|
|
343
383
|
: snapshot.status === 'unavailable'
|
|
344
384
|
if (enabled) mountUi()
|
|
345
385
|
else uiDisposer?.()
|
|
346
386
|
}
|
|
347
|
-
|
|
387
|
+
settingsForm.subscribe(syncEnabled)
|
|
348
388
|
syncEnabled()
|
|
349
389
|
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Bind the settings form this card stages over.
|
|
393
|
+
*
|
|
394
|
+
* The family binder (`ctx.get('webUiSettings')`, published by dsh-web-settings)
|
|
395
|
+
* comes first: it is what traces this package's family namespace onto the
|
|
396
|
+
* profile entry id the Host serves the form under, and it keeps the loopback
|
|
397
|
+
* bridge as its own fallback. A page without that group falls back to the
|
|
398
|
+
* shared configuration forms service bound directly at one of this package's
|
|
399
|
+
* own profile entry ids.
|
|
400
|
+
* @param ctx - client root context.
|
|
401
|
+
* @returns the form the settings card reads and writes.
|
|
402
|
+
*/
|
|
403
|
+
export function bindSettingsForm(ctx: ClientContext): ConfigForm<TaskBoardSettings> {
|
|
404
|
+
const binder = ctx.get('webUiSettings')
|
|
405
|
+
if (binder !== undefined && typeof binder.bind === 'function') {
|
|
406
|
+
return binder.bind<TaskBoardSettings>({ namespace: TASK_BOARD_NS })
|
|
407
|
+
}
|
|
408
|
+
return ctx.configForms.get<TaskBoardSettings>(servedEntryId(ctx.configForms))
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* The profile entry id this package's own row carries.
|
|
413
|
+
*
|
|
414
|
+
* 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
|
+
* activation it usually holds nothing yet. An unanswered mirror therefore
|
|
417
|
+
* binds the aggregate row id rather than guessing among the candidates —
|
|
418
|
+
* the form is bound once for the session, so a wrong guess would leave the
|
|
419
|
+
* card reporting an unserved namespace even after the mirror settles.
|
|
420
|
+
* @param forms - the shared configuration forms service.
|
|
421
|
+
* @returns the entry id to bind.
|
|
422
|
+
*/
|
|
423
|
+
function servedEntryId(forms: ConfigForms): string {
|
|
424
|
+
let served: readonly string[] | undefined
|
|
425
|
+
try {
|
|
426
|
+
served = forms.describe().getSnapshot().view?.namespaces.map(view => view.ns)
|
|
427
|
+
} catch {
|
|
428
|
+
served = undefined
|
|
429
|
+
}
|
|
430
|
+
if (served === undefined) return AGGREGATE_ENTRY_ID
|
|
431
|
+
return TASK_BOARD_ENTRY_IDS.find(id => served.includes(id)) ?? TASK_BOARD_NS
|
|
432
|
+
}
|