@linxin666/dsh-client-ui-task-board 0.4.2 → 0.4.3
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.i18n.yaml +2 -2
- package/README.md +4 -3
- package/README.zh.md +4 -3
- package/lib/client.js +227 -388
- package/lib/client.js.map +1 -1
- package/lib/index.js +279 -42
- package/lib/types/client/index.d.ts +15 -1
- package/lib/types/client/native-panel.d.ts +62 -0
- package/lib/types/client/settings-entry-form.d.ts +49 -0
- package/lib/types/core/controller.d.ts +41 -2
- package/lib/types/host-ledger.d.ts +9 -0
- package/lib/types/host-service.d.ts +51 -5
- package/lib/types/index.d.ts +11 -1
- package/lib/types/mount-once.d.ts +38 -7
- package/package.json +4 -1
- package/src/client/board.module.css +16 -131
- package/src/client/index.ts +45 -17
- package/src/client/native-panel.tsx +124 -0
- package/src/client/settings-entry-form.ts +142 -0
- package/src/core/controller.ts +59 -2
- package/src/host-ledger.ts +20 -0
- package/src/host-routes.ts +37 -1
- package/src/host-service.ts +138 -22
- package/src/index.ts +23 -1
- package/src/mount-once.ts +107 -13
- package/lib/types/client/board-mount.d.ts +0 -23
- package/lib/types/client/body-mutations.d.ts +0 -45
- package/lib/types/client/panel-mount-core.d.ts +0 -56
- package/lib/types/client/sidebar-entry-core.d.ts +0 -55
- package/lib/types/client/sidebar-entry.d.ts +0 -29
- package/src/client/board-mount.tsx +0 -43
- package/src/client/body-mutations.ts +0 -133
- package/src/client/panel-mount-core.ts +0 -155
- package/src/client/sidebar-entry-core.ts +0 -237
- package/src/client/sidebar-entry.ts +0 -53
|
@@ -116,6 +116,15 @@ export declare class HostTaskLedger {
|
|
|
116
116
|
runtimeView(): LedgerRuntimeView;
|
|
117
117
|
/** Count armed, non-archived schedules without cloning task histories. */
|
|
118
118
|
armedScheduleCount(): number;
|
|
119
|
+
/**
|
|
120
|
+
* The earliest armed `nextRunAt` strictly after the supplied Host time, or
|
|
121
|
+
* undefined when no schedule is armed in the future. This is the instant the
|
|
122
|
+
* Host arms its native timer at, so the board wakes exactly when work is due
|
|
123
|
+
* instead of polling a fixed heartbeat.
|
|
124
|
+
* @param now - current Host time in ms epoch.
|
|
125
|
+
* @returns the nearest future trigger, or undefined when none is armed.
|
|
126
|
+
*/
|
|
127
|
+
nextArmedRunAt(now: number): number | undefined;
|
|
119
128
|
/** Return value-only references for schedules due at the supplied Host time. */
|
|
120
129
|
dueSchedules(now: number): DueScheduleReference[];
|
|
121
130
|
subscribe(listener: () => void): () => void;
|
|
@@ -31,16 +31,30 @@ export interface TeamSpawnResult {
|
|
|
31
31
|
export interface TaskBoardTeamDispatcher {
|
|
32
32
|
spawn(input: TeamSpawnInput): Promise<TeamSpawnResult>;
|
|
33
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* The native timer face the Host arms through. The cordis `timer` service
|
|
36
|
+
* (dsh-base's own `cordis-plugin-timer` row) provides it: its handles are
|
|
37
|
+
* registered on the owning fiber, so unloading the board clears every armed
|
|
38
|
+
* timer without this service tracking handles by hand. A composition that
|
|
39
|
+
* serves no timer service falls back to the process globals.
|
|
40
|
+
*/
|
|
41
|
+
export interface HostTimerFace {
|
|
42
|
+
timeout(callback: () => void, delay: number): () => void;
|
|
43
|
+
interval(callback: () => void, delay: number): () => void;
|
|
44
|
+
}
|
|
34
45
|
export declare class TaskBoardHostService {
|
|
35
46
|
readonly ledger: HostTaskLedger;
|
|
36
47
|
readonly runner: HostExecutionRunner;
|
|
37
48
|
readonly power: PowerInhibitor;
|
|
38
49
|
private readonly listeners;
|
|
39
|
-
|
|
40
|
-
private
|
|
50
|
+
/** The one recurring timer: the session-roster poll. */
|
|
51
|
+
private pollTimer;
|
|
52
|
+
/** The armed schedule timer, if a trigger is pending. */
|
|
53
|
+
private scheduleTimer;
|
|
54
|
+
/** The instant the armed schedule timer targets (ms epoch), for resume detection. */
|
|
55
|
+
private scheduleTarget;
|
|
41
56
|
private disposed;
|
|
42
57
|
private pollInFlight;
|
|
43
|
-
private tickInFlight;
|
|
44
58
|
private active;
|
|
45
59
|
/**
|
|
46
60
|
* Ids the last roster poll saw as present and idle; undefined while the
|
|
@@ -51,6 +65,7 @@ export declare class TaskBoardHostService {
|
|
|
51
65
|
private idleSessionIds;
|
|
52
66
|
private preventIdleSleep;
|
|
53
67
|
private readonly team;
|
|
68
|
+
private readonly timers;
|
|
54
69
|
private lastPowerJson;
|
|
55
70
|
private readonly now;
|
|
56
71
|
constructor(gateway: TypertGateway, options?: {
|
|
@@ -62,6 +77,7 @@ export declare class TaskBoardHostService {
|
|
|
62
77
|
sessionDefaultPermission?: TaskPermission;
|
|
63
78
|
maxSubtaskDepth?: number;
|
|
64
79
|
team?: TaskBoardTeamDispatcher;
|
|
80
|
+
timers?: HostTimerFace;
|
|
65
81
|
});
|
|
66
82
|
start(): void;
|
|
67
83
|
setConfiguration(active: boolean, preventIdleSleep: boolean): void;
|
|
@@ -84,7 +100,32 @@ export declare class TaskBoardHostService {
|
|
|
84
100
|
private pollSessions;
|
|
85
101
|
/** Reuse the session list this poll already fetched: one list RPC per tick, not 1 + E. */
|
|
86
102
|
private reconcileExecutions;
|
|
87
|
-
|
|
103
|
+
/** Drop the armed schedule timer and forget its target. */
|
|
104
|
+
private clearScheduleTimer;
|
|
105
|
+
/**
|
|
106
|
+
* Boot / resume recovery: skip every occurrence that came due while the
|
|
107
|
+
* board was not running and roll each schedule to its next future target,
|
|
108
|
+
* then arm the timer for the nearest one. Rendering the occurrence is
|
|
109
|
+
* deliberately not attempted: the ACL of a card that fired hours ago is
|
|
110
|
+
* stale, and the board's own recovery contract is "missed triggers are
|
|
111
|
+
* skipped, never replayed".
|
|
112
|
+
*/
|
|
113
|
+
private recoverSchedule;
|
|
114
|
+
/**
|
|
115
|
+
* Arm the native timer at the nearest armed future trigger. One timer serves
|
|
116
|
+
* every schedule: the ledger's next target is the only instant the Host has
|
|
117
|
+
* to wake for. A target beyond the platform's timer ceiling re-arms in
|
|
118
|
+
* segments, and a target already past (the wall clock jumped, or the process
|
|
119
|
+
* was suspended) is handled immediately as a recovery.
|
|
120
|
+
*/
|
|
121
|
+
private armSchedule;
|
|
122
|
+
/**
|
|
123
|
+
* One armed target became due. A fire landing well past its target is a
|
|
124
|
+
* resume (suspend, throttle, forward clock jump) rather than a normal
|
|
125
|
+
* occurrence, so it takes the recovery path instead of launching a run for a
|
|
126
|
+
* long-stale instant.
|
|
127
|
+
*/
|
|
128
|
+
private onScheduleFire;
|
|
88
129
|
private armedSchedules;
|
|
89
130
|
/**
|
|
90
131
|
* Launch one run set. The root goes first and receives the run shape in its
|
|
@@ -96,7 +137,12 @@ export declare class TaskBoardHostService {
|
|
|
96
137
|
private dispatchRuns;
|
|
97
138
|
private scheduleLaunch;
|
|
98
139
|
private schedulePoll;
|
|
99
|
-
|
|
140
|
+
/**
|
|
141
|
+
* Re-arm from the ledger's current targets. Callers that just changed a
|
|
142
|
+
* schedule (the host routes, the agent tools) invoke this after the write
|
|
143
|
+
* commits, so a new or edited trigger arms without waiting for the next fire.
|
|
144
|
+
*/
|
|
145
|
+
refreshSchedule(): void;
|
|
100
146
|
private syncPowerReasons;
|
|
101
147
|
private emit;
|
|
102
148
|
}
|
package/lib/types/index.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ import type { Context, Volatile } from '@deepseek-ai/cordis';
|
|
|
9
9
|
import z from '@deepseek-ai/schemastery';
|
|
10
10
|
import type { ToolDefinition } from '@deepseek-ai/dsh-tools';
|
|
11
11
|
import type { LlmRuntime } from '@deepseek-ai/dsh-llm';
|
|
12
|
-
import { type TaskBoardTeamDispatcher } from './host-service.ts';
|
|
12
|
+
import { type HostTimerFace, type TaskBoardTeamDispatcher } from './host-service.ts';
|
|
13
13
|
import { type TaskPermission } from './core/tasks.ts';
|
|
14
14
|
/** Default environment variable holding the authenticated proxy token. */
|
|
15
15
|
export declare const DEFAULT_PROXY_TOKEN_ENV = "DSH_TASK_BOARD_PROXY_TOKEN";
|
|
@@ -139,6 +139,16 @@ interface AgentToolRegistry {
|
|
|
139
139
|
* @returns the registry, or undefined when this deployment serves none.
|
|
140
140
|
*/
|
|
141
141
|
export declare function resolveToolRegistry(ctx: Context): AgentToolRegistry | undefined;
|
|
142
|
+
/**
|
|
143
|
+
* Resolve the native cordis timer service (dsh-base's own `cordis-plugin-timer`
|
|
144
|
+
* row). The board arms its schedule through it so unloading the row clears
|
|
145
|
+
* every pending timer with the owning fiber, instead of the row tracking raw
|
|
146
|
+
* process handles by hand. A composition that serves no timer service keeps
|
|
147
|
+
* working on the process globals.
|
|
148
|
+
* @param ctx - the plugin context.
|
|
149
|
+
* @returns the timer face, or undefined when this deployment serves none.
|
|
150
|
+
*/
|
|
151
|
+
export declare function resolveHostTimers(ctx: Context): HostTimerFace | undefined;
|
|
142
152
|
/** The subset of the Agent Teams service this plugin drives. */
|
|
143
153
|
interface AgentTeamsFace {
|
|
144
154
|
spawnTeammate(caller: unknown, request: {
|
|
@@ -4,19 +4,50 @@
|
|
|
4
4
|
* the loader accepts a standalone install of the same package side by side;
|
|
5
5
|
* without this guard the second instance would still re-register the same
|
|
6
6
|
* webserver routes, tools, settings namespaces, and system-prompt sections
|
|
7
|
-
* and fail the boot. mountOnce makes
|
|
8
|
-
*
|
|
9
|
-
* package name in the client module host).
|
|
7
|
+
* and fail the boot. mountOnce makes a mount of an already-mounted package a
|
|
8
|
+
* no-op for as long as the first instance lives (the browser half is already
|
|
9
|
+
* deduped by package name in the client module host).
|
|
10
|
+
*
|
|
11
|
+
* A no-op is only safe while the holder is ALIVE. The holder can be disposed
|
|
12
|
+
* long after the refused mount ran its course: the Host reloads a profile by
|
|
13
|
+
* creating the new loader entries before the old ones are torn down (a
|
|
14
|
+
* plugin-manager enable/disable/install write, a settings-driven row reload,
|
|
15
|
+
* HMR), so the new aggregate shell entry mounts its family plugin while the
|
|
16
|
+
* previous entry still owns the name. Dropping that refused mount lost the
|
|
17
|
+
* plugin for good - the previous entry then disposed its own mount, releasing
|
|
18
|
+
* the name with nobody left to take it, and the family row stayed listed as
|
|
19
|
+
* active while its host routes 404ed (the task-board panel showed
|
|
20
|
+
* "board.hostError.notMounted", no degraded record appeared, and only a Host
|
|
21
|
+
* restart recovered it).
|
|
22
|
+
*
|
|
23
|
+
* The refused mount is therefore QUEUED, not dropped, and replayed the moment
|
|
24
|
+
* the holder releases the name - if the waiting fiber is still alive then. The
|
|
25
|
+
* single-instance guarantee is unchanged: exactly one mount is live per
|
|
26
|
+
* package name, and the replay re-enters the guard so a later mount still
|
|
27
|
+
* dedupes against it.
|
|
10
28
|
*
|
|
11
29
|
* The registry rides a global symbol so two module instances of the same
|
|
12
|
-
* package (npm copy vs repository link) still share one verdict.
|
|
13
|
-
*
|
|
30
|
+
* package (npm copy vs repository link) still share one verdict. That symbol
|
|
31
|
+
* is a CROSS-REPOSITORY contract, not this file's private state: the four
|
|
32
|
+
* satellite packages (dsh-skins / dsh-pet / dsh-presets /
|
|
33
|
+
* dsh-community-plugins) are separate repositories carrying their own copy of
|
|
34
|
+
* this guard, rebuilt on their own schedule, so the value under `MOUNTED`
|
|
35
|
+
* must keep the shape every published copy reads (a `Set` of package names
|
|
36
|
+
* with `has`/`add`/`delete`). The wait queues this guard added therefore live
|
|
37
|
+
* under their own additive key, and the registry reads back a `Set` even when
|
|
38
|
+
* some other build left a different value there. Changing `MOUNTED`'s shape
|
|
39
|
+
* in place broke that contract once: a satellite's legacy copy created a
|
|
40
|
+
* `Set`, the family's new copy read it as a `Map`, and every family row
|
|
41
|
+
* mounted after it failed with "claims.get is not a function".
|
|
42
|
+
*
|
|
43
|
+
* cordis `ctx.effect` runs its callback immediately and treats the callback's
|
|
14
44
|
* return value as the fiber disposer, so the unmarker is returned, not run.
|
|
15
45
|
*/
|
|
16
46
|
/**
|
|
17
47
|
* Wrap a cordis plugin apply so the package runs at most once per process.
|
|
18
|
-
* The first mount registers normally and
|
|
19
|
-
*
|
|
48
|
+
* The first mount registers normally and releases the name when its fiber
|
|
49
|
+
* disposes; a mount refused while that name is held waits for the release and
|
|
50
|
+
* then runs, unless its own fiber disposes first.
|
|
20
51
|
* @param packageName - npm package identity shared by every install source.
|
|
21
52
|
* @param fn - the original plugin apply.
|
|
22
53
|
* @returns an apply of the same shape.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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.
|
|
4
|
+
"version": "0.4.3",
|
|
5
5
|
"icon": "icon.svg",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "lib/index.js",
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
"@deepseek-ai/dsh-client-connection",
|
|
35
35
|
"@deepseek-ai/dsh-client-ui-settings",
|
|
36
36
|
"@deepseek-ai/dsh-client-ui-renderer",
|
|
37
|
+
"@deepseek-ai/dsh-client-ui-layout",
|
|
37
38
|
"@deepseek-ai/dsh-api-session-controller",
|
|
38
39
|
"@deepseek-ai/dsh-api-workspace-controller",
|
|
39
40
|
"@deepseek-ai/dsh-api-remotes",
|
|
@@ -60,8 +61,10 @@
|
|
|
60
61
|
"@deepseek-ai/dsh-client-connection": "^0.1.7-rc.2",
|
|
61
62
|
"@deepseek-ai/dsh-client-locale": "^0.1.7-rc.2",
|
|
62
63
|
"@deepseek-ai/dsh-client-store": "^0.1.7-rc.2",
|
|
64
|
+
"@deepseek-ai/dsh-client-ui-layout": "^0.1.7-rc.2",
|
|
63
65
|
"@deepseek-ai/dsh-client-ui-renderer": "^0.1.7-rc.2",
|
|
64
66
|
"@deepseek-ai/dsh-client-ui-settings": "^0.1.7-rc.2",
|
|
67
|
+
"@deepseek-ai/dsh-client-ui-sidebar": "^0.1.7-rc.2",
|
|
65
68
|
"@deepseek-ai/dsh-client-ui-slots": "^0.1.7-rc.2",
|
|
66
69
|
"@deepseek-ai/dsh-client-ui-workspace": "^0.1.7-rc.2",
|
|
67
70
|
"@deepseek-ai/dsh-commands": "^0.1.7-rc.2",
|
|
@@ -4,135 +4,30 @@
|
|
|
4
4
|
* board follows the active theme (light/dark and skins).
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
/* ---
|
|
7
|
+
/* --- native panel page ------------------------------------------------------- */
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
/* The board is an official center-column panel: the layout mounts this element
|
|
10
|
+
as the selected keyed 'main' occupant, so the shell's own column already
|
|
11
|
+
owns position, sizing and the conversation underneath. Nothing here hides or
|
|
12
|
+
re-parents another plugin's surface — the previous DOM takeover (absolute
|
|
13
|
+
overlay over a hidden conversation subtree) is gone with it.
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
The container query context stays: the board's responsive rules measure the
|
|
16
|
+
PANEL rather than the viewport, so a narrow column or a resized window
|
|
17
|
+
narrows the board at the same threshold the shell uses for its columns. */
|
|
16
18
|
[data-dsh-taskboard-view] {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
display: flex;
|
|
20
|
+
flex-direction: column;
|
|
21
|
+
box-sizing: border-box;
|
|
22
|
+
width: 100%;
|
|
23
|
+
height: 100%;
|
|
24
|
+
min-width: 0;
|
|
25
|
+
min-height: 0;
|
|
21
26
|
container-name: task-board-view;
|
|
22
27
|
container-type: inline-size;
|
|
23
|
-
/* Opaque backdrop: the conversation subtree stays mounted under the view.
|
|
24
|
-
Even if the hide rule below were defeated by a stronger host inline
|
|
25
|
-
style, this background keeps sticky code-block banners (host z-index 6)
|
|
26
|
-
from showing through the active board (issue #100). */
|
|
27
28
|
background: var(--dsw-alias-bg-base);
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
/* The center column is single-occupant; the :not() guards keep the two
|
|
31
|
-
sibling panels (task board / ssh) from fighting over visibility if both
|
|
32
|
-
activation attributes ever coexist. */
|
|
33
|
-
html[data-dsh-taskboard-active]:not([data-dsh-ssh-active]) [data-dsh-taskboard-view] {
|
|
34
|
-
display: block;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/* While the board is active, the conversation content underneath is hidden
|
|
38
|
-
(it stays mounted and stateful). The !important is required: the dsh shell
|
|
39
|
-
(0.1.0-rc.6) wraps the conversation view in a node with an inline
|
|
40
|
-
`display: contents`, and inline styles beat a plain stylesheet rule. Without
|
|
41
|
-
it the composer (input card) stays visible at the bottom and paints over the
|
|
42
|
-
board modals' footer, hiding the run/delete buttons (issue #76).
|
|
43
|
-
The rule targets both the pane attribute and the center-column class so the
|
|
44
|
-
chat subtree (including the code-block copy banner, issue #100) stays hidden
|
|
45
|
-
whichever host container carries it. */
|
|
46
|
-
html[data-dsh-taskboard-active]:not([data-dsh-ssh-active]) [data-pane='conversation'] > :not([data-dsh-taskboard-view]),
|
|
47
|
-
html[data-dsh-taskboard-active]:not([data-dsh-ssh-active]) [class*='centerCol'] > :not([data-dsh-taskboard-view]) {
|
|
48
|
-
display: none !important;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/* --- sidebar entry row ------------------------------------------------------- */
|
|
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. */
|
|
57
|
-
.entry {
|
|
58
|
-
box-sizing: border-box;
|
|
59
|
-
display: flex;
|
|
60
|
-
align-items: center;
|
|
61
|
-
gap: 8px;
|
|
62
|
-
min-height: 36px;
|
|
63
|
-
padding: 7px 8px;
|
|
64
|
-
margin: 0 2px;
|
|
65
|
-
background: transparent;
|
|
66
|
-
border: none;
|
|
67
|
-
border-radius: 12px;
|
|
68
|
-
color: var(--dsw-alias-label-primary);
|
|
69
|
-
cursor: pointer;
|
|
70
|
-
font: inherit;
|
|
71
|
-
font-size: 14px;
|
|
72
|
-
line-height: 22px;
|
|
73
|
-
text-align: left;
|
|
74
|
-
white-space: nowrap;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
.entry:hover {
|
|
78
|
-
background: var(--dsw-alias-interactive-bg-hover);
|
|
79
|
-
color: var(--dsw-alias-label-primary);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
.entry[data-active] {
|
|
83
|
-
background: var(--dsw-alias-interactive-bg-active);
|
|
84
|
-
color: var(--dsw-alias-label-primary);
|
|
85
|
-
font-weight: 600;
|
|
86
|
-
}
|
|
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. */
|
|
90
|
-
.entryIcon {
|
|
91
|
-
display: inline-flex;
|
|
92
|
-
align-items: center;
|
|
93
|
-
justify-content: center;
|
|
94
|
-
width: 16px;
|
|
95
|
-
height: 16px;
|
|
96
|
-
flex: none;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
.entryIcon svg {
|
|
100
|
-
display: block;
|
|
101
|
-
width: 16px;
|
|
102
|
-
height: 16px;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
.entryLabel {
|
|
106
|
-
overflow: hidden;
|
|
107
|
-
text-overflow: ellipsis;
|
|
108
|
-
}
|
|
109
|
-
|
|
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. */
|
|
113
|
-
:global([data-dsh-frame][data-sidebar-collapsed]) .entry,
|
|
114
|
-
:global([data-sidebar-collapsed]) .entry {
|
|
115
|
-
justify-content: center;
|
|
116
|
-
padding: 0;
|
|
117
|
-
width: 36px;
|
|
118
|
-
height: 36px;
|
|
119
|
-
margin: 0 auto 12px;
|
|
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;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
:global([data-dsh-frame][data-sidebar-collapsed]) .entryLabel,
|
|
132
|
-
:global([data-sidebar-collapsed]) .entryLabel {
|
|
133
|
-
display: none;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
31
|
/* --- board frame -------------------------------------------------------------- */
|
|
137
32
|
|
|
138
33
|
.board {
|
|
@@ -924,7 +819,6 @@ html[data-dsh-taskboard-active]:not([data-dsh-ssh-active]) [class*='centerCol']
|
|
|
924
819
|
height: 100dvh;
|
|
925
820
|
}
|
|
926
821
|
|
|
927
|
-
.entry,
|
|
928
822
|
.card,
|
|
929
823
|
.primaryButton,
|
|
930
824
|
.ghostButton,
|
|
@@ -1038,7 +932,6 @@ html[data-dsh-taskboard-active]:not([data-dsh-ssh-active]) [class*='centerCol']
|
|
|
1038
932
|
* --------------------------------------------------------------------------- */
|
|
1039
933
|
|
|
1040
934
|
/* --- one themed focus-visible ring (approx 2px, 2px offset) --- */
|
|
1041
|
-
.entry:focus-visible,
|
|
1042
935
|
.card:focus-visible,
|
|
1043
936
|
.primaryButton:focus-visible,
|
|
1044
937
|
.ghostButton:focus-visible,
|
|
@@ -1055,7 +948,6 @@ html[data-dsh-taskboard-active]:not([data-dsh-ssh-active]) [class*='centerCol']
|
|
|
1055
948
|
}
|
|
1056
949
|
|
|
1057
950
|
/* --- unified 120ms motion across the board's interactive controls --- */
|
|
1058
|
-
.entry,
|
|
1059
951
|
.primaryButton,
|
|
1060
952
|
.ghostButton,
|
|
1061
953
|
.dangerButton,
|
|
@@ -1075,7 +967,6 @@ html[data-dsh-taskboard-active]:not([data-dsh-ssh-active]) [class*='centerCol']
|
|
|
1075
967
|
transform: translateY(0);
|
|
1076
968
|
}
|
|
1077
969
|
|
|
1078
|
-
.entry:active,
|
|
1079
970
|
.primaryButton:active:not(:disabled),
|
|
1080
971
|
.ghostButton:active:not(:disabled),
|
|
1081
972
|
.dangerButton:active:not(:disabled),
|
|
@@ -1084,11 +975,6 @@ html[data-dsh-taskboard-active]:not([data-dsh-ssh-active]) [class*='centerCol']
|
|
|
1084
975
|
transform: translateY(1px);
|
|
1085
976
|
}
|
|
1086
977
|
|
|
1087
|
-
/* --- entry row (sidebar) states --- */
|
|
1088
|
-
.entry[data-active]:hover {
|
|
1089
|
-
background: var(--dsw-specific-sidebar-nav-item-active);
|
|
1090
|
-
}
|
|
1091
|
-
|
|
1092
978
|
/* --- guarded hover for the remaining icon/link buttons (defensive: never
|
|
1093
979
|
disabled today, but a disabled element must not react to hover) --- */
|
|
1094
980
|
.iconButton:hover:not(:disabled) {
|
|
@@ -1121,7 +1007,6 @@ html[data-dsh-taskboard-active]:not([data-dsh-ssh-active]) [class*='centerCol']
|
|
|
1121
1007
|
|
|
1122
1008
|
/* --- reduced motion: strip transitions, stop the decorative spinner --- */
|
|
1123
1009
|
@media (prefers-reduced-motion: reduce) {
|
|
1124
|
-
.entry,
|
|
1125
1010
|
.card,
|
|
1126
1011
|
.primaryButton,
|
|
1127
1012
|
.ghostButton,
|
package/src/client/index.ts
CHANGED
|
@@ -24,17 +24,17 @@ import type {} from '@deepseek-ai/dsh-client-ui-settings/client'
|
|
|
24
24
|
// Type-only: pulls the workspace plugin's Context merge (ctx.uiWorkspace), the
|
|
25
25
|
// multi-instance navigation face that replaced ISessions.open().
|
|
26
26
|
import type {} from '@deepseek-ai/dsh-client-ui-workspace/client'
|
|
27
|
-
import { BoardController } from '../core/controller.ts'
|
|
27
|
+
import { BoardController, TASK_BOARD_PANEL_ID } from '../core/controller.ts'
|
|
28
28
|
import { mainViewSessionId } from './main-session.ts'
|
|
29
29
|
import { LocalStorageTaskStore } from '../core/store.ts'
|
|
30
30
|
import { claimTaskboardApply, releaseTaskboardApply } from './apply-guard.ts'
|
|
31
|
-
import {
|
|
32
|
-
import { mountSidebarEntry } from './sidebar-entry.ts'
|
|
31
|
+
import { registerTaskBoardPanel } from './native-panel.tsx'
|
|
33
32
|
import { TaskBoardSettingsCard, TaskBoardSettingsCardController, type TaskBoardSettings } from './TaskBoardSettingsCard.tsx'
|
|
34
33
|
import { en, zh, setRuntimeTranslate, type TaskBoardKey } from './locales.ts'
|
|
35
34
|
import { HttpTaskBoardHostTransport } from './host-api.ts'
|
|
36
35
|
import { reportDailyHeartbeat } from './telemetry.ts'
|
|
37
36
|
import { installPluginCard } from './plugin-card-seat.ts'
|
|
37
|
+
import { createServedEntryForm } from './settings-entry-form.ts'
|
|
38
38
|
|
|
39
39
|
/** Locale namespace this plugin owns. */
|
|
40
40
|
const NS = 'task-board'
|
|
@@ -124,7 +124,7 @@ declare module '@deepseek-ai/cordis' {
|
|
|
124
124
|
* on hosts below that cohort, which serve the same roster through the
|
|
125
125
|
* connection RPC face.
|
|
126
126
|
*/
|
|
127
|
-
export const inject = ['slots', 'sessions', 'workspaces', 'connection', 'configForms', 'locale', 'remote', 'remote.session', 'uiWorkspace']
|
|
127
|
+
export const inject = ['slots', 'sessions', 'workspaces', 'connection', 'configForms', 'locale', 'remote', 'remote.session', 'uiWorkspace', 'layout']
|
|
128
128
|
|
|
129
129
|
/** One agent-preset row the mode picker consumes (either face's wire shape). */
|
|
130
130
|
interface PresetRosterRow {
|
|
@@ -241,6 +241,18 @@ export function apply(ctx: ClientContext): void {
|
|
|
241
241
|
const controller = new BoardController({
|
|
242
242
|
store,
|
|
243
243
|
transport: new HttpTaskBoardHostTransport(),
|
|
244
|
+
// Panel navigation belongs to the layout service: the board asks it to
|
|
245
|
+
// select its own panel id (or the conversation) instead of taking over
|
|
246
|
+
// the center column at the DOM level. The lookup stays lazy so a
|
|
247
|
+
// selection issued before the service settles still lands once it does,
|
|
248
|
+
// and the face throws by contract before the layout root entry mounts,
|
|
249
|
+
// which the controller tolerates.
|
|
250
|
+
panel: {
|
|
251
|
+
select: panelId => {
|
|
252
|
+
const layout = ctx.get('layout') as { selectPanel?: (id: string | null) => void } | undefined
|
|
253
|
+
layout?.selectPanel?.(panelId)
|
|
254
|
+
},
|
|
255
|
+
},
|
|
244
256
|
sessions: {
|
|
245
257
|
// The main-view Session comes from the catalog's per-source ownership
|
|
246
258
|
// counts; navigation belongs to the workspace UI since the
|
|
@@ -362,12 +374,24 @@ export function apply(ctx: ClientContext): void {
|
|
|
362
374
|
}
|
|
363
375
|
void pushModelOptions()
|
|
364
376
|
disposers.push(ctx.on('connection/reset', () => { void pushModelOptions() }))
|
|
377
|
+
// Native panel surfaces: one sidebar row and one center-column page, both
|
|
378
|
+
// through the official slot seats the shell itself renders. The layout's
|
|
379
|
+
// panel selection is reconciled back into the controller so the board's own
|
|
380
|
+
// view state follows the user clicking another panel row.
|
|
365
381
|
try {
|
|
366
|
-
disposers.push(
|
|
367
|
-
|
|
382
|
+
disposers.push(registerTaskBoardPanel(ctx, controller))
|
|
383
|
+
const layoutFace = ctx.get('layout') as { panelInfo?: { subscribe(listener: () => void): () => void; getSnapshot(): { activePanelId: unknown } } } | undefined
|
|
384
|
+
if (layoutFace?.panelInfo !== undefined) {
|
|
385
|
+
const sync = (): void => {
|
|
386
|
+
const active = layoutFace.panelInfo!.getSnapshot().activePanelId
|
|
387
|
+
controller.syncPanelSelection(active === TASK_BOARD_PANEL_ID ? TASK_BOARD_PANEL_ID : null)
|
|
388
|
+
}
|
|
389
|
+
sync()
|
|
390
|
+
disposers.push(layoutFace.panelInfo.subscribe(sync))
|
|
391
|
+
}
|
|
368
392
|
} catch (error) {
|
|
369
|
-
//
|
|
370
|
-
console.error('[dsh-task-board]
|
|
393
|
+
// A shell that cannot serve the seats degrades the board, never the GUI.
|
|
394
|
+
console.error('[dsh-task-board] panel registration failed:', error)
|
|
371
395
|
}
|
|
372
396
|
|
|
373
397
|
uiDisposer = () => {
|
|
@@ -415,22 +439,26 @@ export function bindSettingsForm(ctx: ClientContext): ConfigForm<TaskBoardSettin
|
|
|
415
439
|
if (binder !== undefined && typeof binder.bind === 'function') {
|
|
416
440
|
return binder.bind<TaskBoardSettings>({ namespace: TASK_BOARD_NS })
|
|
417
441
|
}
|
|
418
|
-
return
|
|
442
|
+
return createServedEntryForm<TaskBoardSettings>({
|
|
443
|
+
forms: ctx.configForms,
|
|
444
|
+
entryIds: TASK_BOARD_ENTRY_IDS,
|
|
445
|
+
})
|
|
419
446
|
}
|
|
420
447
|
|
|
421
448
|
/**
|
|
422
|
-
* The profile entry id this package's own row carries
|
|
449
|
+
* The profile entry id this package's own row carries, for a page that serves
|
|
450
|
+
* no family binder.
|
|
423
451
|
*
|
|
424
452
|
* The shared describe mirror is the only local evidence of which row id this
|
|
425
|
-
* profile actually serves, but it answers asynchronously: at plugin
|
|
426
|
-
*
|
|
427
|
-
*
|
|
428
|
-
*
|
|
429
|
-
*
|
|
453
|
+
* profile actually serves, but it answers asynchronously: at plugin activation
|
|
454
|
+
* it usually holds nothing yet. An unanswered mirror therefore binds the
|
|
455
|
+
* aggregate row id rather than guessing among the candidates, and the binding
|
|
456
|
+
* is re-resolved once the mirror answers — see
|
|
457
|
+
* {@link createServedEntryForm}, which owns that rebinding.
|
|
430
458
|
* @param forms - the shared configuration forms service.
|
|
431
|
-
* @returns the entry id to bind.
|
|
459
|
+
* @returns the entry id to bind before the mirror answers.
|
|
432
460
|
*/
|
|
433
|
-
function servedEntryId(forms: ConfigForms): string {
|
|
461
|
+
export function servedEntryId(forms: ConfigForms): string {
|
|
434
462
|
let served: readonly string[] | undefined
|
|
435
463
|
try {
|
|
436
464
|
served = forms.describe().getSnapshot().view?.namespaces.map(view => view.ns)
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native panel registration for the task board.
|
|
3
|
+
*
|
|
4
|
+
* The board is an official-style center-column panel, not a DOM takeover: it
|
|
5
|
+
* contributes a row into the sidebar shell's own global panel list
|
|
6
|
+
* (`sidebar.panellist`) and its page into the layout's keyed `main` slot. The
|
|
7
|
+
* shell then owns the row box, the label, the active highlight, the collapsed
|
|
8
|
+
* rail, the panel switch and the window-chrome interplay exactly as it does for
|
|
9
|
+
* the shipped Plugins and Schedule pages, so the board renders through the same
|
|
10
|
+
* container the desktop application's Plugins page uses.
|
|
11
|
+
*
|
|
12
|
+
* Both registrations go through `ctx.slots.inject`, which fires only once the
|
|
13
|
+
* owning shell entry has declared the seat: load order between this plugin and
|
|
14
|
+
* ui-layout / ui-sidebar therefore does not matter, and neither does a shell
|
|
15
|
+
* that cannot serve the seats (the callback simply never runs).
|
|
16
|
+
*
|
|
17
|
+
* @module @linxin666/dsh-client-ui-task-board/client
|
|
18
|
+
*/
|
|
19
|
+
import type { Context as ClientContext } from '@deepseek-ai/cordis'
|
|
20
|
+
import type {} from '@deepseek-ai/dsh-client-ui-slots'
|
|
21
|
+
import type {} from '@deepseek-ai/dsh-client-ui-layout/client'
|
|
22
|
+
import type {} from '@deepseek-ai/dsh-client-ui-sidebar/client'
|
|
23
|
+
import { TASK_BOARD_PANEL_ID, type BoardController } from '../core/controller.ts'
|
|
24
|
+
import { TaskBoard } from './board/TaskBoard.tsx'
|
|
25
|
+
import { t } from './locales.ts'
|
|
26
|
+
import css from './board.module.css'
|
|
27
|
+
|
|
28
|
+
/** The panel id shared by the sidebar row and the main-slot occupant. */
|
|
29
|
+
export { TASK_BOARD_PANEL_ID }
|
|
30
|
+
|
|
31
|
+
/** Row order among the shell's global panel rows (Plugins is 0, Schedule 10). */
|
|
32
|
+
const PANEL_ORDER = 20
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The sidebar row glyph the shell asks for at its own size and active state.
|
|
36
|
+
* The shell owns the button, label, tooltip and rail geometry; this component
|
|
37
|
+
* draws only the glyph, like every other panel row.
|
|
38
|
+
*
|
|
39
|
+
* The glyph carries `data-dsh-panel-entry` because it is the only DOM the
|
|
40
|
+
* panel's own code owns inside that shell-owned row: the L2 contract (skins)
|
|
41
|
+
* resolves which row belongs to which plugin through it, since the shell
|
|
42
|
+
* stamps no per-entry hook of its own (see contracts/semantic-attrs-v1.md).
|
|
43
|
+
* @param props - the shell's icon share: square edge and selection state.
|
|
44
|
+
* @returns the decorative board glyph.
|
|
45
|
+
*/
|
|
46
|
+
export function TaskBoardPanelIcon({ size }: { size: number; active: boolean }): React.ReactElement {
|
|
47
|
+
return (
|
|
48
|
+
<svg
|
|
49
|
+
data-dsh-panel-entry={TASK_BOARD_PANEL_ID}
|
|
50
|
+
viewBox="0 0 16 16"
|
|
51
|
+
width={size}
|
|
52
|
+
height={size}
|
|
53
|
+
fill="none"
|
|
54
|
+
stroke="currentColor"
|
|
55
|
+
strokeWidth="1.3"
|
|
56
|
+
strokeLinecap="round"
|
|
57
|
+
strokeLinejoin="round"
|
|
58
|
+
aria-hidden="true"
|
|
59
|
+
>
|
|
60
|
+
<rect x="2" y="2.5" width="12" height="11" rx="1.5" />
|
|
61
|
+
<path d="M2 6.5h12M6.5 6.5v7" />
|
|
62
|
+
</svg>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The main-slot page. The layout mounts it only while the board is the selected
|
|
68
|
+
* panel, so the conversation keeps the center column untouched the rest of the
|
|
69
|
+
* time; the wrapper carries the pinned `data-dsh-taskboard-view` semantic
|
|
70
|
+
* anchor (L2 contract, skins) and the container query context the board's
|
|
71
|
+
* responsive rules read.
|
|
72
|
+
* @param props - the framework main-slot share plus this entry's injected face.
|
|
73
|
+
* @returns the board page.
|
|
74
|
+
*/
|
|
75
|
+
export function TaskBoardPanel({ controller }: { controller: BoardController }): React.ReactElement {
|
|
76
|
+
return (
|
|
77
|
+
<div className={css.panel} data-dsh-taskboard-view="" data-dsh-plugin="task-board">
|
|
78
|
+
<TaskBoard controller={controller} />
|
|
79
|
+
</div>
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** The injected face both registrations receive. */
|
|
84
|
+
interface PanelFace {
|
|
85
|
+
controller: BoardController
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Register the board's sidebar row and center-column page.
|
|
90
|
+
*
|
|
91
|
+
* Both seats are declared by shell plugins this package does not depend on at
|
|
92
|
+
* runtime, so each registration is wrapped in `ctx.slots.inject`: the callback
|
|
93
|
+
* runs only after the owning entry declares the seat, and a shell that never
|
|
94
|
+
* declares it leaves the board simply absent instead of failing boot.
|
|
95
|
+
* @param ctx - client root context (services: slots).
|
|
96
|
+
* @param controller - the board controller the page and the row drive.
|
|
97
|
+
* @returns disposer releasing both registrations.
|
|
98
|
+
*/
|
|
99
|
+
export function registerTaskBoardPanel(ctx: ClientContext, controller: BoardController): () => void {
|
|
100
|
+
const slots = ctx.slots as {
|
|
101
|
+
inject(key: string, callback: () => () => void): () => void
|
|
102
|
+
register(options: Record<string, unknown>, component: unknown): () => void
|
|
103
|
+
}
|
|
104
|
+
const disposers: Array<() => void> = []
|
|
105
|
+
|
|
106
|
+
disposers.push(slots.inject('sidebar.panellist', () => slots.register({
|
|
107
|
+
name: 'sidebar.panellist',
|
|
108
|
+
id: TASK_BOARD_PANEL_ID,
|
|
109
|
+
order: PANEL_ORDER,
|
|
110
|
+
// The shell resolves this through resolveSlotLabel on every locale change,
|
|
111
|
+
// so the module-level translate reads the active language at call time.
|
|
112
|
+
label: () => t('entry.label'),
|
|
113
|
+
}, TaskBoardPanelIcon)))
|
|
114
|
+
|
|
115
|
+
disposers.push(slots.inject('main', () => slots.register({
|
|
116
|
+
name: 'main',
|
|
117
|
+
key: TASK_BOARD_PANEL_ID,
|
|
118
|
+
inject: (): PanelFace => ({ controller }),
|
|
119
|
+
}, TaskBoardPanel as never)))
|
|
120
|
+
|
|
121
|
+
return () => {
|
|
122
|
+
for (const dispose of disposers.splice(0)) dispose()
|
|
123
|
+
}
|
|
124
|
+
}
|