@linxin666/dsh-client-ui-task-board 0.4.2 → 0.4.4
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 +21 -13
- package/README.zh.md +21 -13
- package/lib/client.js +1566 -929
- package/lib/client.js.map +1 -1
- package/lib/index.js +934 -176
- package/lib/types/client/board/TaskForm.d.ts +19 -1
- package/lib/types/client/board/preset-label.d.ts +22 -0
- package/lib/types/client/host-api.d.ts +24 -1
- package/lib/types/client/index.d.ts +15 -1
- package/lib/types/client/locales.d.ts +38 -2
- package/lib/types/client/native-panel.d.ts +62 -0
- package/lib/types/client/schedule-zone.d.ts +47 -0
- package/lib/types/client/settings-entry-form.d.ts +49 -0
- package/lib/types/core/controller.d.ts +42 -2
- package/lib/types/core/degraded-reason.d.ts +46 -0
- package/lib/types/core/package-name.d.ts +9 -0
- package/lib/types/core/schedule.d.ts +95 -20
- package/lib/types/core/subtask.d.ts +30 -11
- package/lib/types/core/tasks.d.ts +26 -2
- package/lib/types/core/use-cases/task-create.d.ts +2 -1
- package/lib/types/core/use-cases/task-schedule.d.ts +8 -4
- package/lib/types/core/use-cases/task-update.d.ts +1 -1
- package/lib/types/host-ledger.d.ts +65 -4
- package/lib/types/host-runner.d.ts +76 -3
- package/lib/types/host-service.d.ts +77 -5
- package/lib/types/index.d.ts +11 -1
- package/lib/types/mount-once.d.ts +38 -7
- package/lib/types/protocol.d.ts +9 -3
- package/package.json +26 -23
- package/src/client/board/LinkSubtaskModal.tsx +21 -11
- package/src/client/board/NewTaskModal.tsx +271 -150
- package/src/client/board/TaskCard.tsx +24 -5
- package/src/client/board/TaskDetail.tsx +83 -33
- package/src/client/board/TaskForm.tsx +48 -3
- package/src/client/board/preset-label.ts +44 -0
- package/src/client/board.module.css +130 -136
- package/src/client/host-api.ts +109 -6
- package/src/client/index.ts +50 -18
- package/src/client/locales.ts +82 -8
- package/src/client/native-panel.tsx +124 -0
- package/src/client/schedule-zone.ts +135 -0
- package/src/client/settings-entry-form.ts +142 -0
- package/src/core/controller.ts +66 -4
- package/src/core/degraded-reason.ts +81 -0
- package/src/core/package-name.ts +9 -0
- package/src/core/schedule.ts +211 -55
- package/src/core/store.ts +10 -1
- package/src/core/subtask.ts +77 -22
- package/src/core/tasks.ts +38 -4
- package/src/core/use-cases/task-create.ts +14 -2
- package/src/core/use-cases/task-schedule.ts +17 -6
- package/src/core/use-cases/task-update.ts +4 -1
- package/src/host/agent-tools.ts +40 -8
- package/src/host-ledger.ts +223 -23
- package/src/host-routes.ts +37 -1
- package/src/host-runner.ts +164 -14
- package/src/host-service.ts +232 -39
- package/src/index.ts +23 -1
- package/src/mount-once.ts +107 -13
- package/src/protocol.ts +27 -8
- 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/lib/types/host/run-guarded.d.ts +0 -39
- 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
- package/src/host/run-guarded.ts +0 -76
package/src/protocol.ts
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import type { TaskUpdatePatch } from './core/use-cases/task-update.ts'
|
|
2
2
|
import { isTaskPermission, isTaskStatus, isTaskTagList, type NewTaskInput, type TaskPermission, type TaskRecord, type TaskStatus } from './core/tasks.ts'
|
|
3
3
|
import { parseLedger } from './core/store.ts'
|
|
4
|
+
import { isValidTimeZone } from './core/schedule.ts'
|
|
4
5
|
import { sanitizeFreezeSnapshot, type FreezeSnapshot } from './core/freeze-snapshot.ts'
|
|
5
6
|
import { sanitizeHandover, type TaskHandoverInput } from './core/handover.ts'
|
|
6
7
|
|
|
7
8
|
/** Freeze payload carried by create/update actions after the gate (redacted in place). */
|
|
8
9
|
type FreezePayload = FreezeSnapshot & { redacted?: boolean; frozenBy?: string }
|
|
9
10
|
|
|
10
|
-
export const TASK_BOARD_SCHEMA_VERSION =
|
|
11
|
-
/** Ledger documents written before
|
|
12
|
-
export const TASK_BOARD_LEGACY_SCHEMA_VERSION =
|
|
11
|
+
export const TASK_BOARD_SCHEMA_VERSION = 4 as const
|
|
12
|
+
/** Ledger documents written before v4; loaded once and migrated on startup. */
|
|
13
|
+
export const TASK_BOARD_LEGACY_SCHEMA_VERSION = 3 as const
|
|
14
|
+
/** Ledger documents written before v3; migrated through the v3 normalization too. */
|
|
15
|
+
export const TASK_BOARD_OLDER_SCHEMA_VERSION = 2 as const
|
|
13
16
|
export const TASK_BOARD_API_PREFIX = '/api/task-board'
|
|
14
17
|
|
|
15
18
|
export type PowerPhase = 'disabled' | 'idle' | 'acquiring' | 'active' | 'error' | 'unsupported'
|
|
@@ -101,7 +104,8 @@ export type TaskBoardAction =
|
|
|
101
104
|
| { kind: 'move'; taskId: string; status: TaskStatus }
|
|
102
105
|
| { kind: 'archive'; taskId: string }
|
|
103
106
|
| { kind: 'restore'; taskId: string }
|
|
104
|
-
| { kind: '
|
|
107
|
+
| { kind: 'settle'; taskId: string }
|
|
108
|
+
| { kind: 'set-schedule'; taskId: string; patch: { enabled?: boolean; cron?: string; timeZone?: string | null } }
|
|
105
109
|
| { kind: 'run'; taskId: string }
|
|
106
110
|
| { kind: 'rerun'; taskId: string }
|
|
107
111
|
| { kind: 'confirm-permission'; taskId: string }
|
|
@@ -154,6 +158,8 @@ function validImportedKnownFields(value: Record<string, unknown>): boolean {
|
|
|
154
158
|
const schedule = record(value.schedule)
|
|
155
159
|
if (schedule === undefined || typeof schedule.enabled !== 'boolean' || typeof schedule.cron !== 'string') return false
|
|
156
160
|
if (!optionalFiniteNumber(schedule.nextRunAt) || !optionalFiniteNumber(schedule.lastTriggeredAt)) return false
|
|
161
|
+
if (schedule.timeZone !== undefined
|
|
162
|
+
&& (typeof schedule.timeZone !== 'string' || !isValidTimeZone(schedule.timeZone))) return false
|
|
157
163
|
}
|
|
158
164
|
if (value.executions !== undefined) {
|
|
159
165
|
if (!Array.isArray(value.executions)) return false
|
|
@@ -199,6 +205,7 @@ function importedTask(value: unknown): TaskRecord | undefined {
|
|
|
199
205
|
schedule: {
|
|
200
206
|
enabled: task.schedule.enabled,
|
|
201
207
|
cron: task.schedule.cron,
|
|
208
|
+
...(task.schedule.timeZone === undefined ? {} : { timeZone: task.schedule.timeZone }),
|
|
202
209
|
nextRunAt: task.schedule.nextRunAt,
|
|
203
210
|
lastTriggeredAt: task.schedule.lastTriggeredAt,
|
|
204
211
|
},
|
|
@@ -210,6 +217,7 @@ function importedTask(value: unknown): TaskRecord | undefined {
|
|
|
210
217
|
...(task.mode === undefined ? {} : { mode: task.mode }),
|
|
211
218
|
...(task.permission === undefined ? {} : { permission: task.permission }),
|
|
212
219
|
...(task.reuseSession === undefined ? {} : { reuseSession: task.reuseSession }),
|
|
220
|
+
...(task.goalRun === undefined ? {} : { goalRun: task.goalRun }),
|
|
213
221
|
...(task.archivedAt === undefined ? {} : { archivedAt: task.archivedAt }),
|
|
214
222
|
...(task.freeze === undefined ? {} : { freeze: task.freeze }),
|
|
215
223
|
...(task.handover === undefined ? {} : { handover: task.handover }),
|
|
@@ -247,30 +255,36 @@ function handoverPayload(value: unknown): TaskHandoverInput | undefined {
|
|
|
247
255
|
|
|
248
256
|
function createInput(value: unknown): value is NewTaskInput {
|
|
249
257
|
const input = record(value)
|
|
250
|
-
if (input === undefined || !exactKeys(input, ['title', 'description', 'prompt', 'parentId', 'workspaceId', 'mode', 'permission', 'schedule', 'freeze', 'handover', 'model', 'reuseSession', 'teamRun', 'tags'])) return false
|
|
258
|
+
if (input === undefined || !exactKeys(input, ['title', 'description', 'prompt', 'parentId', 'workspaceId', 'mode', 'permission', 'schedule', 'freeze', 'handover', 'model', 'reuseSession', 'teamRun', 'goalRun', 'tags'])) return false
|
|
251
259
|
if (input.parentId !== undefined && (typeof input.parentId !== 'string' || input.parentId.trim() === '')) return false
|
|
252
260
|
if (typeof input.title !== 'string' || typeof input.description !== 'string' || typeof input.prompt !== 'string') return false
|
|
253
261
|
if (!optionalString(input.workspaceId) || !optionalString(input.mode) || !optionalString(input.model)) return false
|
|
254
262
|
if (input.reuseSession !== undefined && typeof input.reuseSession !== 'boolean') return false
|
|
255
263
|
if (input.teamRun !== undefined && typeof input.teamRun !== 'boolean') return false
|
|
264
|
+
if (input.goalRun !== undefined && typeof input.goalRun !== 'boolean') return false
|
|
256
265
|
if (input.permission !== undefined && !isTaskPermission(input.permission)) return false
|
|
257
266
|
if (input.tags !== undefined && !isTaskTagList(input.tags)) return false
|
|
258
267
|
if (input.freeze !== undefined && freezePayload(input.freeze) === undefined) return false
|
|
259
268
|
if (input.handover !== undefined && handoverPayload(input.handover) === undefined) return false
|
|
260
269
|
if (input.schedule !== undefined) {
|
|
261
270
|
const schedule = record(input.schedule)
|
|
262
|
-
if (schedule === undefined || !exactKeys(schedule, ['enabled', 'cron'])) return false
|
|
271
|
+
if (schedule === undefined || !exactKeys(schedule, ['enabled', 'cron', 'timeZone'])) return false
|
|
263
272
|
if (typeof schedule.enabled !== 'boolean' || typeof schedule.cron !== 'string') return false
|
|
273
|
+
if (schedule.timeZone !== undefined
|
|
274
|
+
&& (typeof schedule.timeZone !== 'string' || !isValidTimeZone(schedule.timeZone))) return false
|
|
264
275
|
}
|
|
265
276
|
return true
|
|
266
277
|
}
|
|
267
278
|
|
|
268
279
|
function updatePatch(value: unknown): boolean {
|
|
269
280
|
const patch = record(value)
|
|
270
|
-
if (patch === undefined || !exactKeys(patch, ['title', 'description', 'prompt', 'workspaceId', 'mode', 'permission', 'freeze', 'handover', 'model', 'reuseSession', 'teamRun', 'tags'])) return false
|
|
281
|
+
if (patch === undefined || !exactKeys(patch, ['title', 'description', 'prompt', 'workspaceId', 'mode', 'permission', 'freeze', 'handover', 'model', 'reuseSession', 'teamRun', 'goalRun', 'tags'])) return false
|
|
271
282
|
// null (or false) clears the reuse opt-in; only a real boolean is accepted.
|
|
272
283
|
if (patch.reuseSession !== undefined && patch.reuseSession !== null && typeof patch.reuseSession !== 'boolean') return false
|
|
273
284
|
if (patch.teamRun !== undefined && patch.teamRun !== null && typeof patch.teamRun !== 'boolean') return false
|
|
285
|
+
// The goal opt-in is tri-state: null/true return the card to its default
|
|
286
|
+
// (goal run), false pins a single plain turn.
|
|
287
|
+
if (patch.goalRun !== undefined && patch.goalRun !== null && typeof patch.goalRun !== 'boolean') return false
|
|
274
288
|
for (const key of ['title', 'description', 'prompt', 'workspaceId', 'mode', 'model'] as const) {
|
|
275
289
|
if (!optionalString(patch[key])) return false
|
|
276
290
|
}
|
|
@@ -286,9 +300,13 @@ function updatePatch(value: unknown): boolean {
|
|
|
286
300
|
function schedulePatch(value: unknown): boolean {
|
|
287
301
|
const patch = record(value)
|
|
288
302
|
return patch !== undefined
|
|
289
|
-
&& exactKeys(patch, ['enabled', 'cron'])
|
|
303
|
+
&& exactKeys(patch, ['enabled', 'cron', 'timeZone'])
|
|
290
304
|
&& (patch.enabled === undefined || typeof patch.enabled === 'boolean')
|
|
291
305
|
&& (patch.cron === undefined || typeof patch.cron === 'string')
|
|
306
|
+
// `null` clears the stored zone back to the Host zone; an unknown name is
|
|
307
|
+
// rejected here so the Host never has to guess at an unusable zone.
|
|
308
|
+
&& (patch.timeZone === undefined || patch.timeZone === null
|
|
309
|
+
|| (typeof patch.timeZone === 'string' && isValidTimeZone(patch.timeZone)))
|
|
292
310
|
}
|
|
293
311
|
|
|
294
312
|
export function parseActionEnvelope(value: unknown): TaskBoardActionEnvelope | undefined {
|
|
@@ -364,6 +382,7 @@ function parseEnvelopeAction(value: unknown): TaskBoardActionEnvelope | undefine
|
|
|
364
382
|
case 'delete':
|
|
365
383
|
case 'archive':
|
|
366
384
|
case 'restore':
|
|
385
|
+
case 'settle':
|
|
367
386
|
case 'run':
|
|
368
387
|
case 'rerun':
|
|
369
388
|
if (!exactKeys(action, ['kind', 'taskId'])) return undefined
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Board view mounting (dsh-task-board wrapper).
|
|
3
|
-
*
|
|
4
|
-
* The single-occupant takeover lifecycle — container injection into the
|
|
5
|
-
* center column, sibling eviction, remount resilience, sidebar click-out —
|
|
6
|
-
* lives exactly once in shared/client/panel-mount-core.ts (synced copy); this
|
|
7
|
-
* wrapper supplies the board tree, view dataset key, CSS module, and the html
|
|
8
|
-
* attribute names. Those names are pinned by board.module.css, skins, and the
|
|
9
|
-
* semantic attributes contract.
|
|
10
|
-
*/
|
|
11
|
-
import type { BoardController } from '../core/controller.ts';
|
|
12
|
-
import type { LocaleRefreshSource } from './sidebar-entry.ts';
|
|
13
|
-
/** The injected board container (kept in the DOM, hidden when inactive). */
|
|
14
|
-
export declare const BOARD_VIEW_SELECTOR = "[data-dsh-taskboard-view]";
|
|
15
|
-
/**
|
|
16
|
-
* Mount the board React tree into the center column and bind its visibility
|
|
17
|
-
* to the controller's boardOpen state.
|
|
18
|
-
* @param controller - the board controller driving the view.
|
|
19
|
-
* @param locale - locale-change source; when given, re-renders a mounted board
|
|
20
|
-
* on a Language switch.
|
|
21
|
-
* @returns disposer unmounting the tree and restoring the column.
|
|
22
|
-
*/
|
|
23
|
-
export declare function mountBoard(controller: BoardController, locale?: LocaleRefreshSource): () => void;
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* One `document.body` MutationObserver per page, shared by every family
|
|
3
|
-
* plugin bundle.
|
|
4
|
-
*
|
|
5
|
-
* Family plugins that inject at the DOM level must notice when the shell
|
|
6
|
-
* re-renders around their injected node: the sidebar entry rows
|
|
7
|
-
* (sidebar-entry-core), the center-column panel containers
|
|
8
|
-
* (panel-mount-core), and the aggregate shell's column shims. Each consumer
|
|
9
|
-
* used to install its OWN `MutationObserver` on `document.body` with
|
|
10
|
-
* `{ childList: true, subtree: true }`, so a page carrying N family plugins
|
|
11
|
-
* paid N native observers and N callback invocations for EVERY mutation batch
|
|
12
|
-
* the app produced — chat token streaming alone produces many per second, and
|
|
13
|
-
* the count grows with the number of installed plugins.
|
|
14
|
-
*
|
|
15
|
-
* This module keeps exactly ONE body observer per page. The registry lives on
|
|
16
|
-
* `globalThis` under a `Symbol.for` key, so the per-package generated copies
|
|
17
|
-
* of this file (scripts/sync-shared.mjs) and separately bundled plugins all
|
|
18
|
-
* reach the same hub at runtime instead of one hub per module instance.
|
|
19
|
-
* Record subscribers receive the mutations accumulated since the last flush.
|
|
20
|
-
* Consumers that only re-check their DOM use subscribeBodyInvalidations:
|
|
21
|
-
* when all subscribers use that path, the hub retains no mutation records
|
|
22
|
-
* (or detached subtrees) while a background page's animation frames pause.
|
|
23
|
-
* Both paths run at most once per frame. The last subscriber disconnects the
|
|
24
|
-
* observer, cancels the pending frame and releases its records.
|
|
25
|
-
*
|
|
26
|
-
* Failure policy: without a DOM or `MutationObserver` the subscription is a
|
|
27
|
-
* no-op disposer (the same silence the per-consumer observers had), and when
|
|
28
|
-
* `requestAnimationFrame` is unavailable the flush runs synchronously so a
|
|
29
|
-
* subscriber is never silently dropped. A throwing subscriber cannot stop the
|
|
30
|
-
* others.
|
|
31
|
-
*/
|
|
32
|
-
/**
|
|
33
|
-
* Subscribe to a coalesced DOM re-check without retaining mutation records.
|
|
34
|
-
* The marked wrapper also works with an older hub, which delivers records
|
|
35
|
-
* that it simply ignores until a page reload picks up the updated hub.
|
|
36
|
-
*/
|
|
37
|
-
export declare function subscribeBodyInvalidations(subscriber: () => void): () => void;
|
|
38
|
-
/**
|
|
39
|
-
* Subscribe to body-level childList mutations.
|
|
40
|
-
* @param subscriber - called at most once per animation frame with the records
|
|
41
|
-
* collected since the previous flush; must be safe to run repeatedly.
|
|
42
|
-
* @returns the disposer removing this subscriber (and the observer when it was
|
|
43
|
-
* the last one).
|
|
44
|
-
*/
|
|
45
|
-
export declare function subscribeBodyMutations(subscriber: (records: MutationRecord[]) => void): () => void;
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Center-column panel takeover lifecycle.
|
|
3
|
-
*
|
|
4
|
-
* The `conversation` slot is single-occupant (ui-conversation) and external
|
|
5
|
-
* plugins cannot declare slots, so a family panel takes over the center
|
|
6
|
-
* column at the DOM level: a container is appended inside the center column
|
|
7
|
-
* (`[class*="centerCol"]`, the 0.1.0-rc.6+ AppFrame layout; previously
|
|
8
|
-
* `[data-pane="conversation"]` on older shells — the mount selector keeps
|
|
9
|
-
* both, ssh #243 / task-board #107) as an extra trailing child React never
|
|
10
|
-
* manages, and a stylesheet rule hides the conversation content while the
|
|
11
|
-
* panel is active. Toggling is a data attribute on <html> — no React
|
|
12
|
-
* involvement, so the conversation subtree underneath stays mounted and
|
|
13
|
-
* stateful.
|
|
14
|
-
*
|
|
15
|
-
* Consuming plugins keep a thin wrapper that supplies the panel tree,
|
|
16
|
-
* container attribute names, and stylesheet class; those names are pinned by
|
|
17
|
-
* each package's CSS, skins, and the semantic-attributes contract. The
|
|
18
|
-
* sidebar row toggling the panel shares its core the same way
|
|
19
|
-
* (shared/client/sidebar-entry-core.ts, synced copy).
|
|
20
|
-
*/
|
|
21
|
-
import { type Root } from 'react-dom/client';
|
|
22
|
-
/** Options for mountCenterPanel; dsh-ssh mount.tsx and dsh-task-board board-mount.tsx are the canonical consumers. */
|
|
23
|
-
export interface CenterPanelMountOptions {
|
|
24
|
-
/** Render the panel tree (first open, remount while open, locale refresh). */
|
|
25
|
-
render: (root: Root) => void;
|
|
26
|
-
/** dataset key of the injected container's view attribute, e.g. `dshSshView` for `data-dsh-ssh-view`. */
|
|
27
|
-
viewDatasetKey: string;
|
|
28
|
-
/** value of the container's L2 `data-dsh-plugin` semantic attribute. */
|
|
29
|
-
pluginName: string;
|
|
30
|
-
/** stylesheet class applied to the injected container. */
|
|
31
|
-
viewClassName: string;
|
|
32
|
-
/** <html> attribute set while this panel is active. */
|
|
33
|
-
activeAttribute: string;
|
|
34
|
-
/** the sibling panel's active attribute, removed from <html> when this panel opens. */
|
|
35
|
-
siblingActiveAttribute: string;
|
|
36
|
-
/** detail value this panel broadcasts on the cross-plugin activation event. */
|
|
37
|
-
panelName: string;
|
|
38
|
-
/** sibling detail value whose activation closes this panel. */
|
|
39
|
-
siblingPanelName: string;
|
|
40
|
-
/** open flag of the owning controller. */
|
|
41
|
-
isOpen: () => boolean;
|
|
42
|
-
/** close the panel, handing the center column back to the conversation. */
|
|
43
|
-
close: () => void;
|
|
44
|
-
/** subscribe to the owning controller's open-state changes; returns an unsubscriber. */
|
|
45
|
-
subscribe: (listener: () => void) => () => void;
|
|
46
|
-
/** locale-change source; when given, re-renders an open panel on a Language switch. */
|
|
47
|
-
locale?: {
|
|
48
|
-
subscribe(listener: () => void): () => void;
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
/**
|
|
52
|
-
* Mount a family panel into the center column and bind its visibility to the
|
|
53
|
-
* owning controller's open state.
|
|
54
|
-
* @returns disposer unmounting the tree and restoring the column.
|
|
55
|
-
*/
|
|
56
|
-
export declare function mountCenterPanel(options: CenterPanelMountOptions): () => void;
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
/** Per-package configuration for one sidebar entry row. */
|
|
2
|
-
export interface SidebarEntryOptions {
|
|
3
|
-
/** Full attribute name identifying the injected row (idempotency key), e.g. 'data-dsh-ssh-entry'. */
|
|
4
|
-
rowAttribute: string;
|
|
5
|
-
/** CSS selector matching the injected row, e.g. '[data-dsh-ssh-entry]'. */
|
|
6
|
-
rowSelector: string;
|
|
7
|
-
/**
|
|
8
|
-
* L2 semantic-attribute plugin id (issue #506, enum table:
|
|
9
|
-
* skins/skin-center/contracts/semantic-attrs-v1.md). When set, the row also
|
|
10
|
-
* outputs data-dsh-plugin="<id>" and data-dsh-part="sidebar-entry"; unset
|
|
11
|
-
* leaves the row without semantic attributes.
|
|
12
|
-
*/
|
|
13
|
-
plugin?: string;
|
|
14
|
-
/** Inline icon markup (matches the shell's 16px nav-icon look). */
|
|
15
|
-
icon: string;
|
|
16
|
-
/** CSS module class names for the row and its two spans (entry / entryIcon / entryLabel). */
|
|
17
|
-
css: Record<string, string>;
|
|
18
|
-
/** Localized row label (aria-label + visible text). */
|
|
19
|
-
label(): string;
|
|
20
|
-
/** Optional localized tooltip (title attribute). */
|
|
21
|
-
tooltip?(): string;
|
|
22
|
-
/**
|
|
23
|
-
* Optional locale-change subscription: re-applies label / aria-label /
|
|
24
|
-
* tooltip whenever the active locale changes. Plain-DOM rows would
|
|
25
|
-
* otherwise keep the label captured at mount; pass the SDK locale runtime
|
|
26
|
-
* subscription (ctx.locale.subscribe) so the row follows the language
|
|
27
|
-
* switch without a reload.
|
|
28
|
-
*/
|
|
29
|
-
refresh?: {
|
|
30
|
-
subscribe(listener: () => void): () => void;
|
|
31
|
-
};
|
|
32
|
-
/** Click action (open/toggle the owning panel). */
|
|
33
|
-
onToggle(): void;
|
|
34
|
-
/** Family-block position: 'before' inserts ahead of sibling plugin rows, 'after' behind them. */
|
|
35
|
-
position: 'before' | 'after';
|
|
36
|
-
/**
|
|
37
|
-
* Selectors of the sibling plugin entry rows this package orders against
|
|
38
|
-
* (its own row included — the placement guard excludes a row that is
|
|
39
|
-
* already inside the root). Each package passes the same list it used
|
|
40
|
-
* before the consolidation so the rendered order stays stable.
|
|
41
|
-
*/
|
|
42
|
-
familySelectors: readonly string[];
|
|
43
|
-
/** Optional active-state bridge; highlights the row while the panel is open. */
|
|
44
|
-
active?: {
|
|
45
|
-
subscribe(listener: () => void): () => void;
|
|
46
|
-
isOpen(): boolean;
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
/**
|
|
50
|
-
* Mount the sidebar entry, waiting for the shell to render and self-healing
|
|
51
|
-
* on later React re-renders.
|
|
52
|
-
* @param options - the row's attribute/icon/copy/action/ordering configuration.
|
|
53
|
-
* @returns disposer removing the entry and its observers.
|
|
54
|
-
*/
|
|
55
|
-
export declare function mountSidebarEntry(options: SidebarEntryOptions): () => void;
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Sidebar entry injection — package-specific wiring over the shared core.
|
|
3
|
-
*
|
|
4
|
-
* dsh's sidebar shell exposes no slot an external plugin can register into
|
|
5
|
-
* (`sidebar.workspaces` / `sidebar.settings` are single-occupant and already
|
|
6
|
-
* taken), so the entry row is injected between the shell's New Session button
|
|
7
|
-
* and the workspace browser. The DOM injection / self-healing / idempotency
|
|
8
|
-
* logic lives exactly once in shared/client/sidebar-entry-core.ts (synced
|
|
9
|
-
* copy); this wrapper supplies the task-board icon, copy, CSS module, and the
|
|
10
|
-
* board toggle. The row is plain DOM (no React tree); the board view it
|
|
11
|
-
* toggles is a separate React root mounted in the center column
|
|
12
|
-
* (see board-mount.ts).
|
|
13
|
-
*/
|
|
14
|
-
import type { BoardController } from '../core/controller.ts';
|
|
15
|
-
/** Stable data attribute identifying the injected entry row. */
|
|
16
|
-
export declare const ENTRY_SELECTOR = "[data-dsh-taskboard-entry]";
|
|
17
|
-
/** Locale-change subscription the shared core asks for (ctx.locale.subscribe shape). */
|
|
18
|
-
export interface LocaleRefreshSource {
|
|
19
|
-
subscribe(listener: () => void): () => void;
|
|
20
|
-
}
|
|
21
|
-
/**
|
|
22
|
-
* Mount the sidebar entry, waiting for the shell to render and self-healing
|
|
23
|
-
* on later React re-renders.
|
|
24
|
-
* @param controller - the board controller the entry toggles.
|
|
25
|
-
* @param locale - locale-change source; when given, re-applies the label on
|
|
26
|
-
* a Language switch (the plain-DOM row otherwise keeps the mount-time copy).
|
|
27
|
-
* @returns disposer removing the entry and its observers.
|
|
28
|
-
*/
|
|
29
|
-
export declare function mountSidebarEntry(controller: BoardController, locale?: LocaleRefreshSource): () => void;
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Async-boundary guard shared by the plugin family's Host halves: every
|
|
3
|
-
* fire-and-forget promise chain and callback the host runtime does not own
|
|
4
|
-
* (route handlers, timers, event listeners, spawned work) funnels through
|
|
5
|
-
* these helpers. The dsh host installs a process-level fail-loud guard that
|
|
6
|
-
* turns ANY unhandled promise rejection into a whole-process exit — one
|
|
7
|
-
* plugin's stray rejection would otherwise take every plugin down. These
|
|
8
|
-
* helpers exist so that failure mode is structurally impossible in family
|
|
9
|
-
* code: the rejection becomes a logged error at the plugin boundary instead.
|
|
10
|
-
*
|
|
11
|
-
* Complements the aggregate's shell isolation (packages/dsh-web-all): the
|
|
12
|
-
* shell contains import/activation failures at boot; runGuarded contains
|
|
13
|
-
* run-time failures after activation.
|
|
14
|
-
* @module dsh-web-shared/host/run-guarded
|
|
15
|
-
*/
|
|
16
|
-
/**
|
|
17
|
-
* Run one async operation, logging (never propagating) a rejection. Use for
|
|
18
|
-
* promises whose lifecycle the caller does not await: spawned work, retry
|
|
19
|
-
* loops, background flushes. Returns the original promise so callers can
|
|
20
|
-
* still chain when they want to.
|
|
21
|
-
* @param promise - the work to shield; any thenable.
|
|
22
|
-
* @param label - log prefix naming the work site.
|
|
23
|
-
* @param log - error sink; defaults to console.error.
|
|
24
|
-
* @returns the input promise (rejection already consumed).
|
|
25
|
-
*/
|
|
26
|
-
export declare function runGuarded<T>(promise: PromiseLike<T>, label: string, log?: (error: unknown) => void): PromiseLike<T>;
|
|
27
|
-
/**
|
|
28
|
-
* Wrap one callback so every invocation is individually guarded: a rejection
|
|
29
|
-
* inside one call is logged and swallowed instead of escaping into whatever
|
|
30
|
-
* infrastructure invoked the callback (HTTP server, EventEmitter, interval).
|
|
31
|
-
* Sync throws are caught identically; a returned promise is replaced by
|
|
32
|
-
* `undefined` after guarding (callers that need the original rejection should
|
|
33
|
-
* await inside their own try/catch instead).
|
|
34
|
-
* @param label - log prefix naming the callback site.
|
|
35
|
-
* @param handler - the work to guard.
|
|
36
|
-
* @param log - error sink; defaults to console.error.
|
|
37
|
-
* @returns a wrapped callback with the same parameter list.
|
|
38
|
-
*/
|
|
39
|
-
export declare function guardedHandler<A extends unknown[]>(label: string, handler: (...args: A) => unknown, log?: (error: unknown) => void): (...args: A) => unknown;
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Board view mounting (dsh-task-board wrapper).
|
|
3
|
-
*
|
|
4
|
-
* The single-occupant takeover lifecycle — container injection into the
|
|
5
|
-
* center column, sibling eviction, remount resilience, sidebar click-out —
|
|
6
|
-
* lives exactly once in shared/client/panel-mount-core.ts (synced copy); this
|
|
7
|
-
* wrapper supplies the board tree, view dataset key, CSS module, and the html
|
|
8
|
-
* attribute names. Those names are pinned by board.module.css, skins, and the
|
|
9
|
-
* semantic attributes contract.
|
|
10
|
-
*/
|
|
11
|
-
import type { BoardController } from '../core/controller.ts'
|
|
12
|
-
import { TaskBoard } from './board/TaskBoard.tsx'
|
|
13
|
-
import { mountCenterPanel } from './panel-mount-core.ts'
|
|
14
|
-
import type { LocaleRefreshSource } from './sidebar-entry.ts'
|
|
15
|
-
import css from './board.module.css'
|
|
16
|
-
|
|
17
|
-
/** The injected board container (kept in the DOM, hidden when inactive). */
|
|
18
|
-
export const BOARD_VIEW_SELECTOR = '[data-dsh-taskboard-view]'
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Mount the board React tree into the center column and bind its visibility
|
|
22
|
-
* to the controller's boardOpen state.
|
|
23
|
-
* @param controller - the board controller driving the view.
|
|
24
|
-
* @param locale - locale-change source; when given, re-renders a mounted board
|
|
25
|
-
* on a Language switch.
|
|
26
|
-
* @returns disposer unmounting the tree and restoring the column.
|
|
27
|
-
*/
|
|
28
|
-
export function mountBoard(controller: BoardController, locale?: LocaleRefreshSource): () => void {
|
|
29
|
-
return mountCenterPanel({
|
|
30
|
-
render: root => root.render(<TaskBoard controller={controller} />),
|
|
31
|
-
viewDatasetKey: 'dshTaskboardView',
|
|
32
|
-
pluginName: 'task-board',
|
|
33
|
-
viewClassName: css.boardView,
|
|
34
|
-
activeAttribute: 'data-dsh-taskboard-active',
|
|
35
|
-
siblingActiveAttribute: 'data-dsh-ssh-active',
|
|
36
|
-
panelName: 'taskboard',
|
|
37
|
-
siblingPanelName: 'ssh',
|
|
38
|
-
isOpen: () => controller.getSnapshot().boardOpen,
|
|
39
|
-
close: () => controller.closeBoard(),
|
|
40
|
-
subscribe: listener => controller.subscribe(listener),
|
|
41
|
-
locale,
|
|
42
|
-
})
|
|
43
|
-
}
|
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
// Generated by scripts/sync-shared.mjs from shared/client/body-mutations.ts. Do not edit this copy; edit the shared source and run "node scripts/sync-shared.mjs".
|
|
2
|
-
/**
|
|
3
|
-
* One `document.body` MutationObserver per page, shared by every family
|
|
4
|
-
* plugin bundle.
|
|
5
|
-
*
|
|
6
|
-
* Family plugins that inject at the DOM level must notice when the shell
|
|
7
|
-
* re-renders around their injected node: the sidebar entry rows
|
|
8
|
-
* (sidebar-entry-core), the center-column panel containers
|
|
9
|
-
* (panel-mount-core), and the aggregate shell's column shims. Each consumer
|
|
10
|
-
* used to install its OWN `MutationObserver` on `document.body` with
|
|
11
|
-
* `{ childList: true, subtree: true }`, so a page carrying N family plugins
|
|
12
|
-
* paid N native observers and N callback invocations for EVERY mutation batch
|
|
13
|
-
* the app produced — chat token streaming alone produces many per second, and
|
|
14
|
-
* the count grows with the number of installed plugins.
|
|
15
|
-
*
|
|
16
|
-
* This module keeps exactly ONE body observer per page. The registry lives on
|
|
17
|
-
* `globalThis` under a `Symbol.for` key, so the per-package generated copies
|
|
18
|
-
* of this file (scripts/sync-shared.mjs) and separately bundled plugins all
|
|
19
|
-
* reach the same hub at runtime instead of one hub per module instance.
|
|
20
|
-
* Record subscribers receive the mutations accumulated since the last flush.
|
|
21
|
-
* Consumers that only re-check their DOM use subscribeBodyInvalidations:
|
|
22
|
-
* when all subscribers use that path, the hub retains no mutation records
|
|
23
|
-
* (or detached subtrees) while a background page's animation frames pause.
|
|
24
|
-
* Both paths run at most once per frame. The last subscriber disconnects the
|
|
25
|
-
* observer, cancels the pending frame and releases its records.
|
|
26
|
-
*
|
|
27
|
-
* Failure policy: without a DOM or `MutationObserver` the subscription is a
|
|
28
|
-
* no-op disposer (the same silence the per-consumer observers had), and when
|
|
29
|
-
* `requestAnimationFrame` is unavailable the flush runs synchronously so a
|
|
30
|
-
* subscriber is never silently dropped. A throwing subscriber cannot stop the
|
|
31
|
-
* others.
|
|
32
|
-
*/
|
|
33
|
-
|
|
34
|
-
/** The page-wide hub: one observer plus its subscribers and pending records. */
|
|
35
|
-
interface BodyMutationHub {
|
|
36
|
-
observer: MutationObserver
|
|
37
|
-
subscribers: Set<(records: MutationRecord[]) => void>
|
|
38
|
-
pending: MutationRecord[]
|
|
39
|
-
scheduled: boolean
|
|
40
|
-
frame?: number
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/** Cross-bundle registry key; `Symbol.for` so every module copy agrees. */
|
|
44
|
-
const HUB_KEY = Symbol.for('dsh-web.body-mutation-hub')
|
|
45
|
-
const INVALIDATION_ONLY = Symbol.for('dsh-web.body-mutation-invalidation')
|
|
46
|
-
type Subscriber = ((records: MutationRecord[]) => void) & { [INVALIDATION_ONLY]?: true }
|
|
47
|
-
|
|
48
|
-
function needsRecords(subscribers: Set<(records: MutationRecord[]) => void>): boolean {
|
|
49
|
-
for (const listener of subscribers) {
|
|
50
|
-
if (!(listener as Subscriber)[INVALIDATION_ONLY]) return true
|
|
51
|
-
}
|
|
52
|
-
return false
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Subscribe to a coalesced DOM re-check without retaining mutation records.
|
|
57
|
-
* The marked wrapper also works with an older hub, which delivers records
|
|
58
|
-
* that it simply ignores until a page reload picks up the updated hub.
|
|
59
|
-
*/
|
|
60
|
-
export function subscribeBodyInvalidations(subscriber: () => void): () => void {
|
|
61
|
-
const listener: Subscriber = () => { subscriber() }
|
|
62
|
-
listener[INVALIDATION_ONLY] = true
|
|
63
|
-
return subscribeBodyMutations(listener)
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Subscribe to body-level childList mutations.
|
|
68
|
-
* @param subscriber - called at most once per animation frame with the records
|
|
69
|
-
* collected since the previous flush; must be safe to run repeatedly.
|
|
70
|
-
* @returns the disposer removing this subscriber (and the observer when it was
|
|
71
|
-
* the last one).
|
|
72
|
-
*/
|
|
73
|
-
export function subscribeBodyMutations(subscriber: (records: MutationRecord[]) => void): () => void {
|
|
74
|
-
if (typeof globalThis === 'undefined' || typeof document === 'undefined') return () => {}
|
|
75
|
-
if (typeof MutationObserver !== 'function') return () => {}
|
|
76
|
-
const registry = globalThis as unknown as Record<symbol, unknown>
|
|
77
|
-
let hub = registry[HUB_KEY] as BodyMutationHub | undefined
|
|
78
|
-
if (hub === undefined) {
|
|
79
|
-
const subscribers = new Set<(records: MutationRecord[]) => void>()
|
|
80
|
-
const created: BodyMutationHub = {
|
|
81
|
-
observer: undefined as unknown as MutationObserver,
|
|
82
|
-
subscribers,
|
|
83
|
-
pending: [],
|
|
84
|
-
scheduled: false,
|
|
85
|
-
}
|
|
86
|
-
const flush = (): void => {
|
|
87
|
-
created.frame = undefined
|
|
88
|
-
created.scheduled = false
|
|
89
|
-
const batch = created.pending
|
|
90
|
-
created.pending = []
|
|
91
|
-
for (const listener of [...subscribers]) {
|
|
92
|
-
if (!subscribers.has(listener)) continue
|
|
93
|
-
try {
|
|
94
|
-
listener(batch)
|
|
95
|
-
} catch {
|
|
96
|
-
// One subscriber must never break the others; the next flush retries it.
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
const schedule = (): void => {
|
|
101
|
-
if (created.scheduled) return
|
|
102
|
-
created.scheduled = true
|
|
103
|
-
if (typeof requestAnimationFrame === 'function') created.frame = requestAnimationFrame(flush)
|
|
104
|
-
else flush()
|
|
105
|
-
}
|
|
106
|
-
created.observer = new MutationObserver((records) => {
|
|
107
|
-
if (needsRecords(subscribers)) {
|
|
108
|
-
for (const record of records) created.pending.push(record)
|
|
109
|
-
}
|
|
110
|
-
schedule()
|
|
111
|
-
})
|
|
112
|
-
created.observer.observe(document.body ?? document.documentElement, { childList: true, subtree: true })
|
|
113
|
-
registry[HUB_KEY] = created
|
|
114
|
-
hub = created
|
|
115
|
-
}
|
|
116
|
-
const active = hub
|
|
117
|
-
active.subscribers.add(subscriber)
|
|
118
|
-
let subscribed = true
|
|
119
|
-
return () => {
|
|
120
|
-
if (!subscribed) return
|
|
121
|
-
subscribed = false
|
|
122
|
-
active.subscribers.delete(subscriber)
|
|
123
|
-
if (!needsRecords(active.subscribers)) active.pending = []
|
|
124
|
-
if (active.subscribers.size === 0 && registry[HUB_KEY] === active) {
|
|
125
|
-
active.observer.disconnect()
|
|
126
|
-
if (active.frame !== undefined && typeof cancelAnimationFrame === 'function') cancelAnimationFrame(active.frame)
|
|
127
|
-
active.frame = undefined
|
|
128
|
-
active.pending = []
|
|
129
|
-
active.scheduled = false
|
|
130
|
-
delete registry[HUB_KEY]
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
}
|