@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
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* NewTaskModal and the EditTaskModal. State stays in the owning modal; these
|
|
5
5
|
* are controlled components.
|
|
6
6
|
*/
|
|
7
|
-
import type
|
|
7
|
+
import { type ReactNode } from 'react';
|
|
8
8
|
import { type TaskTag } from '../../core/tasks.ts';
|
|
9
9
|
/** Modal overlay: closes on backdrop press, submits through the form. */
|
|
10
10
|
export declare function ModalShell({ ariaLabel, title, error, pending, submitLabel, onSubmit, onClose, secondaryAction, children, }: {
|
|
@@ -22,6 +22,24 @@ export declare function ModalShell({ ariaLabel, title, error, pending, submitLab
|
|
|
22
22
|
};
|
|
23
23
|
children: ReactNode;
|
|
24
24
|
}): import("react").JSX.Element;
|
|
25
|
+
/**
|
|
26
|
+
* One collapsible region of a task form (create/edit dialog UX): the form
|
|
27
|
+
* groups its configuration into a few regions instead of stacking every field
|
|
28
|
+
* in one long column, so a normal-sized dialog needs no scrollbar. A collapsed
|
|
29
|
+
* region keeps a one-line summary of the values it holds, so nothing is hidden
|
|
30
|
+
* without a trace; a region holding a blocking error is forced open by its
|
|
31
|
+
* owner (forceOpen) rather than reporting the error out of sight.
|
|
32
|
+
*/
|
|
33
|
+
export declare function CollapsibleSection({ title, summary, defaultOpen, forceOpen, children, }: {
|
|
34
|
+
title: string;
|
|
35
|
+
/** One-line value preview rendered only while the region is collapsed. */
|
|
36
|
+
summary?: string | undefined;
|
|
37
|
+
/** Whether the region starts expanded (the owner's default layout). */
|
|
38
|
+
defaultOpen?: boolean;
|
|
39
|
+
/** Open regardless of the stored state: an error inside must stay visible. */
|
|
40
|
+
forceOpen?: boolean;
|
|
41
|
+
children: ReactNode;
|
|
42
|
+
}): import("react").JSX.Element;
|
|
25
43
|
/** Title + description + prompt fields shared by the new and edit task forms. */
|
|
26
44
|
export declare function TaskContentFields({ title, description, prompt, onTitleChange, onDescriptionChange, onPromptChange, }: {
|
|
27
45
|
title: string;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Display-only labels for the agent-preset roster, shared by the task form and
|
|
3
|
+
* the detail view.
|
|
4
|
+
*
|
|
5
|
+
* The deployment's four built-in presets (the `standard` / `ptc` / `minimal` /
|
|
6
|
+
* `cordis` rows the shipped web-app bundle declares) carry no display name in
|
|
7
|
+
* the registry roster, so a picker would list bare ids beside the user presets.
|
|
8
|
+
* The map below only supplies names for those four; any other id — a renamed
|
|
9
|
+
* built-in, a community preset, a user preset — falls back to the roster name
|
|
10
|
+
* and then to the id, so this table can never hide or mislabel a preset.
|
|
11
|
+
*/
|
|
12
|
+
import type { ExecutionPresetOption } from '../../core/controller.ts';
|
|
13
|
+
/** Whether one roster row is a shipped built-in preset. */
|
|
14
|
+
export declare function isBuiltinPreset(id: string): boolean;
|
|
15
|
+
/** Display label of one preset row: built-in label, roster name, or raw id. */
|
|
16
|
+
export declare function presetLabel(preset: Pick<ExecutionPresetOption, 'id' | 'name'>): string;
|
|
17
|
+
/**
|
|
18
|
+
* Label of the empty ("inherit") choice: names the preset a run actually lands
|
|
19
|
+
* on when the card pins nothing, which is the deployment default the roster
|
|
20
|
+
* marks with `isDefault`.
|
|
21
|
+
*/
|
|
22
|
+
export declare function inheritPresetLabel(presets: readonly ExecutionPresetOption[]): string;
|
|
@@ -7,13 +7,36 @@ import { type TaskBoardAction, type TaskBoardEventPayload, type TaskBoardParseDr
|
|
|
7
7
|
* "Unexpected token 'o', \"not found\" is not valid JSON" is what a missing
|
|
8
8
|
* Host half looks like today.
|
|
9
9
|
*/
|
|
10
|
-
export type HostApiFailure =
|
|
10
|
+
export type HostApiFailure =
|
|
11
|
+
/** The board's own routes are not registered and nothing explains why. */
|
|
12
|
+
'not-mounted'
|
|
13
|
+
/** Another live DSH process owns the ledger, so this instance registered no board routes (#1730). */
|
|
14
|
+
| 'ledger-locked'
|
|
15
|
+
/** This row degraded during start for some other reason the family health route reports (#1730). */
|
|
16
|
+
| 'degraded' | 'unauthorized' | 'forbidden' | 'locked' | 'rejected' | 'timeout' | 'unreachable' | 'unexpected';
|
|
11
17
|
/** Transport failure carrying a stable class next to its user-facing message. */
|
|
12
18
|
export declare class HostApiError extends Error {
|
|
13
19
|
readonly failure: HostApiFailure;
|
|
14
20
|
readonly status?: number | undefined;
|
|
15
21
|
constructor(failure: HostApiFailure, message: string, status?: number | undefined);
|
|
16
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* Name the real reason this board's routes are missing (issue #1730).
|
|
25
|
+
*
|
|
26
|
+
* The core webserver answers an unmounted route with a bare 404, which cannot
|
|
27
|
+
* distinguish "this deployment never loaded the plugin" from "this row
|
|
28
|
+
* degraded during start" — a second DSH process holding `ledger-v2.lock` is
|
|
29
|
+
* the case that matters, because the panel's advice ("restart the service")
|
|
30
|
+
* is then false: the lock belongs to the other process, and restarting cannot
|
|
31
|
+
* win it. The family health route knows the difference, so the 404 is
|
|
32
|
+
* disambiguated there before anything is shown.
|
|
33
|
+
* @param fallback - copy for the plain "routes are not there" failure, which
|
|
34
|
+
* is route-specific (the board's state route and the AI-parse route phrase
|
|
35
|
+
* their own absence); it is also what is shown when the ledger names nothing.
|
|
36
|
+
* @param fetchImpl - fetch implementation (injected for tests).
|
|
37
|
+
* @returns the HostApiError to throw for a missing board route.
|
|
38
|
+
*/
|
|
39
|
+
export declare function missingRouteFailure(fallback?: string, fetchImpl?: typeof fetch): Promise<HostApiError>;
|
|
17
40
|
export interface TaskBoardHostTransport {
|
|
18
41
|
bootstrap(legacy: readonly TaskRecord[]): Promise<TaskBoardSnapshot>;
|
|
19
42
|
state(): Promise<TaskBoardSnapshot>;
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* plugin must not take the GUI down.
|
|
10
10
|
*/
|
|
11
11
|
import type { Context as ClientContext } from '@deepseek-ai/cordis';
|
|
12
|
-
import type { ConfigForm } from '@deepseek-ai/dsh-client-ui-settings/client';
|
|
12
|
+
import type { ConfigForm, ConfigForms } from '@deepseek-ai/dsh-client-ui-settings/client';
|
|
13
13
|
import { type TaskBoardSettings } from './TaskBoardSettingsCard.tsx';
|
|
14
14
|
import { type TaskBoardKey } from './locales.ts';
|
|
15
15
|
/** Domain-owned description of one settings namespace a family card binds. */
|
|
@@ -94,3 +94,17 @@ export declare function apply(ctx: ClientContext): void;
|
|
|
94
94
|
* @returns the form the settings card reads and writes.
|
|
95
95
|
*/
|
|
96
96
|
export declare function bindSettingsForm(ctx: ClientContext): ConfigForm<TaskBoardSettings>;
|
|
97
|
+
/**
|
|
98
|
+
* The profile entry id this package's own row carries, for a page that serves
|
|
99
|
+
* no family binder.
|
|
100
|
+
*
|
|
101
|
+
* The shared describe mirror is the only local evidence of which row id this
|
|
102
|
+
* profile actually serves, but it answers asynchronously: at plugin activation
|
|
103
|
+
* it usually holds nothing yet. An unanswered mirror therefore binds the
|
|
104
|
+
* aggregate row id rather than guessing among the candidates, and the binding
|
|
105
|
+
* is re-resolved once the mirror answers — see
|
|
106
|
+
* {@link createServedEntryForm}, which owns that rebinding.
|
|
107
|
+
* @param forms - the shared configuration forms service.
|
|
108
|
+
* @returns the entry id to bind before the mirror answers.
|
|
109
|
+
*/
|
|
110
|
+
export declare function servedEntryId(forms: ConfigForms): string;
|
|
@@ -29,7 +29,10 @@ export declare const zh: {
|
|
|
29
29
|
'board.created': string;
|
|
30
30
|
'board.hostError': string;
|
|
31
31
|
'board.hostError.notMounted': string;
|
|
32
|
+
'board.hostError.ledgerLocked': string;
|
|
33
|
+
'board.hostError.degraded': string;
|
|
32
34
|
'board.hostError.unauthorized': string;
|
|
35
|
+
'board.hostError.forbidden': string;
|
|
33
36
|
'board.hostError.locked': string;
|
|
34
37
|
'board.hostError.timeout': string;
|
|
35
38
|
'board.hostError.unreachable': string;
|
|
@@ -54,6 +57,19 @@ export declare const zh: {
|
|
|
54
57
|
'new.aiParseFailed': string;
|
|
55
58
|
'board.retryHost': string;
|
|
56
59
|
'board.hostMeta': string;
|
|
60
|
+
'new.section.parse': string;
|
|
61
|
+
'new.section.content': string;
|
|
62
|
+
'new.section.labels': string;
|
|
63
|
+
'new.section.execution': string;
|
|
64
|
+
'new.section.run': string;
|
|
65
|
+
'new.section.handover': string;
|
|
66
|
+
'new.section.schedule': string;
|
|
67
|
+
'new.summary.none': string;
|
|
68
|
+
'new.summary.labelCount': string;
|
|
69
|
+
'new.summary.filled': string;
|
|
70
|
+
'new.summary.scheduleOff': string;
|
|
71
|
+
'new.summary.multiRound': string;
|
|
72
|
+
'new.summary.singleRound': string;
|
|
57
73
|
'new.title': string;
|
|
58
74
|
'new.titlePlaceholder': string;
|
|
59
75
|
'new.description': string;
|
|
@@ -145,13 +161,31 @@ export declare const zh: {
|
|
|
145
161
|
'detail.schedule.invalid': string;
|
|
146
162
|
'detail.schedule.notScheduled': string;
|
|
147
163
|
'detail.schedule.dueSoon': string;
|
|
164
|
+
'detail.schedule.timeZone': string;
|
|
165
|
+
'detail.schedule.timeZoneHost': string;
|
|
166
|
+
'detail.schedule.timeZoneHostUnknown': string;
|
|
167
|
+
'detail.schedule.timeZoneHint': string;
|
|
168
|
+
'detail.schedule.timeZoneSearch': string;
|
|
169
|
+
'detail.schedule.timeZoneNone': string;
|
|
170
|
+
'detail.schedule.nextRunOverdue': string;
|
|
171
|
+
'detail.schedule.duration.days': string;
|
|
172
|
+
'detail.schedule.duration.hours': string;
|
|
173
|
+
'detail.schedule.duration.minutes': string;
|
|
174
|
+
'detail.schedule.duration.seconds': string;
|
|
148
175
|
'card.scheduled': string;
|
|
149
176
|
'new.workspace': string;
|
|
150
|
-
'new.
|
|
177
|
+
'new.agentPreset': string;
|
|
151
178
|
'new.permission': string;
|
|
152
179
|
'new.model': string;
|
|
153
180
|
'exec.workspace.recent': string;
|
|
154
|
-
'exec.mode.
|
|
181
|
+
'exec.mode.inherit': string;
|
|
182
|
+
'exec.mode.inheritWith': string;
|
|
183
|
+
'exec.mode.builtinGroup': string;
|
|
184
|
+
'exec.mode.customGroup': string;
|
|
185
|
+
'preset.builtin.standard': string;
|
|
186
|
+
'preset.builtin.ptc': string;
|
|
187
|
+
'preset.builtin.minimal': string;
|
|
188
|
+
'preset.builtin.cordis': string;
|
|
155
189
|
'exec.mode.defaultSuffix': string;
|
|
156
190
|
'exec.mode.brokenSuffix': string;
|
|
157
191
|
'exec.mode.removed': string;
|
|
@@ -167,6 +201,8 @@ export declare const zh: {
|
|
|
167
201
|
'exec.teamRunHint': string;
|
|
168
202
|
'exec.teamRunUnavailable': string;
|
|
169
203
|
'exec.reuseSessionHint': string;
|
|
204
|
+
'exec.goalRun': string;
|
|
205
|
+
'exec.goalRunHint': string;
|
|
170
206
|
'detail.executionSettings': string;
|
|
171
207
|
'exec.hint': string;
|
|
172
208
|
'settings.title': string;
|
|
@@ -0,0 +1,62 @@
|
|
|
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 { TASK_BOARD_PANEL_ID, type BoardController } from '../core/controller.ts';
|
|
21
|
+
/** The panel id shared by the sidebar row and the main-slot occupant. */
|
|
22
|
+
export { TASK_BOARD_PANEL_ID };
|
|
23
|
+
/**
|
|
24
|
+
* The sidebar row glyph the shell asks for at its own size and active state.
|
|
25
|
+
* The shell owns the button, label, tooltip and rail geometry; this component
|
|
26
|
+
* draws only the glyph, like every other panel row.
|
|
27
|
+
*
|
|
28
|
+
* The glyph carries `data-dsh-panel-entry` because it is the only DOM the
|
|
29
|
+
* panel's own code owns inside that shell-owned row: the L2 contract (skins)
|
|
30
|
+
* resolves which row belongs to which plugin through it, since the shell
|
|
31
|
+
* stamps no per-entry hook of its own (see contracts/semantic-attrs-v1.md).
|
|
32
|
+
* @param props - the shell's icon share: square edge and selection state.
|
|
33
|
+
* @returns the decorative board glyph.
|
|
34
|
+
*/
|
|
35
|
+
export declare function TaskBoardPanelIcon({ size }: {
|
|
36
|
+
size: number;
|
|
37
|
+
active: boolean;
|
|
38
|
+
}): React.ReactElement;
|
|
39
|
+
/**
|
|
40
|
+
* The main-slot page. The layout mounts it only while the board is the selected
|
|
41
|
+
* panel, so the conversation keeps the center column untouched the rest of the
|
|
42
|
+
* time; the wrapper carries the pinned `data-dsh-taskboard-view` semantic
|
|
43
|
+
* anchor (L2 contract, skins) and the container query context the board's
|
|
44
|
+
* responsive rules read.
|
|
45
|
+
* @param props - the framework main-slot share plus this entry's injected face.
|
|
46
|
+
* @returns the board page.
|
|
47
|
+
*/
|
|
48
|
+
export declare function TaskBoardPanel({ controller }: {
|
|
49
|
+
controller: BoardController;
|
|
50
|
+
}): React.ReactElement;
|
|
51
|
+
/**
|
|
52
|
+
* Register the board's sidebar row and center-column page.
|
|
53
|
+
*
|
|
54
|
+
* Both seats are declared by shell plugins this package does not depend on at
|
|
55
|
+
* runtime, so each registration is wrapped in `ctx.slots.inject`: the callback
|
|
56
|
+
* runs only after the owning entry declares the seat, and a shell that never
|
|
57
|
+
* declares it leaves the board simply absent instead of failing boot.
|
|
58
|
+
* @param ctx - client root context (services: slots).
|
|
59
|
+
* @param controller - the board controller the page and the row drive.
|
|
60
|
+
* @returns disposer releasing both registrations.
|
|
61
|
+
*/
|
|
62
|
+
export declare function registerTaskBoardPanel(ctx: ClientContext, controller: BoardController): () => void;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/** One selectable zone, in the order the picker shows it. */
|
|
2
|
+
export interface ZoneChoice {
|
|
3
|
+
/** Canonical IANA id, or `''` for the Host-zone entry. */
|
|
4
|
+
id: string;
|
|
5
|
+
/** Display label. */
|
|
6
|
+
label: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Zones offered by the picker: the Host zone first (the default, and the entry
|
|
10
|
+
* a rule with no stored zone follows), then the runtime's own inventory.
|
|
11
|
+
*
|
|
12
|
+
* The Host entry is always present, even before the Host snapshot reports a
|
|
13
|
+
* zone: it is the entry that stores no zone, so dropping it would leave a rule
|
|
14
|
+
* unable to express "follow the Host" and would let the control silently
|
|
15
|
+
* display an unrelated zone.
|
|
16
|
+
*
|
|
17
|
+
* A stored zone the inventory does not list (a rule written on another machine
|
|
18
|
+
* or under a different ICU build) is appended, so opening the editor can never
|
|
19
|
+
* silently rewrite the rule's zone.
|
|
20
|
+
* @param hostTimeZone - zone the Host reported for this deployment, when known.
|
|
21
|
+
* @param storedTimeZone - zone the edited rule already stores, when any.
|
|
22
|
+
* @returns the de-duplicated choice list.
|
|
23
|
+
*/
|
|
24
|
+
export declare function zoneChoices(hostTimeZone: string | undefined, storedTimeZone?: string): ZoneChoice[];
|
|
25
|
+
/**
|
|
26
|
+
* The relative half of a next-run label: how far away the instant is, or how
|
|
27
|
+
* far past it the board is running. `now` is passed in rather than sampled so
|
|
28
|
+
* callers can share one clock reading across a row, and tests stay
|
|
29
|
+
* deterministic.
|
|
30
|
+
* @param targetMs - the instant being described.
|
|
31
|
+
* @param now - the reference instant.
|
|
32
|
+
* @returns localized relative wording.
|
|
33
|
+
*/
|
|
34
|
+
export declare function relativeTimeLabel(targetMs: number, now: number): string;
|
|
35
|
+
/**
|
|
36
|
+
* The full next-run label: the absolute wall clock in the schedule's own zone,
|
|
37
|
+
* plus the relative distance in parentheses. Showing both matters because a
|
|
38
|
+
* rule's zone can differ from the reader's, so the wall clock alone is
|
|
39
|
+
* ambiguous and the distance alone loses the actual moment.
|
|
40
|
+
* @param targetMs - the instant being described.
|
|
41
|
+
* @param timeZone - zone to render the absolute half in.
|
|
42
|
+
* @param formatAbsolute - formatter for the absolute half (keeps Intl caching in the caller).
|
|
43
|
+
* @param now - the reference instant.
|
|
44
|
+
* @param translate - translate function, bound so this module stays dependency-light.
|
|
45
|
+
* @returns the combined label.
|
|
46
|
+
*/
|
|
47
|
+
export declare function nextRunLabel(targetMs: number, timeZone: string | undefined, formatAbsolute: (ms: number, timeZone?: string) => string, now: number): string;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Family settings form bound to the profile entry id a Host actually serves.
|
|
3
|
+
*
|
|
4
|
+
* Under the 0.1.7 settings model a form is addressed by PROFILE ENTRY ID, while
|
|
5
|
+
* a family card knows only its own settings namespace. The family binder
|
|
6
|
+
* (`ctx.get('webUiSettings')`, published by dsh-web-settings) resolves one onto
|
|
7
|
+
* the other and is the preferred transport; this module is the fallback for a
|
|
8
|
+
* page that serves no binder — the group plugin is not installed, or its client
|
|
9
|
+
* half has not applied yet (the aggregate mounts its children in order, and a
|
|
10
|
+
* child still waiting on its own injects publishes nothing).
|
|
11
|
+
*
|
|
12
|
+
* The shared describe mirror is the only local evidence of which row id this
|
|
13
|
+
* profile serves, but it answers asynchronously: at plugin activation it usually
|
|
14
|
+
* holds nothing yet. A one-shot guess therefore cannot be right for every
|
|
15
|
+
* deployment, and the form is bound once per session:
|
|
16
|
+
*
|
|
17
|
+
* - binding the bare namespace made every save answer `No configurable plugin
|
|
18
|
+
* entry "<namespace>"` on an aggregate install (the namespace is not an entry
|
|
19
|
+
* id there), which the card reported as "the deployment did not accept these
|
|
20
|
+
* values";
|
|
21
|
+
* - binding the aggregate row id would break a standalone install, whose row id
|
|
22
|
+
* is the namespace or the package's own `ui-*` row.
|
|
23
|
+
*
|
|
24
|
+
* So the fallback starts on the most likely row and REBINDS the moment the
|
|
25
|
+
* mirror names one of this package's rows. Until the mirror answers, the form
|
|
26
|
+
* reports the binding it holds (usually `loading`), which the card renders as an
|
|
27
|
+
* unavailable form rather than a doomed one.
|
|
28
|
+
*/
|
|
29
|
+
import type { ConfigForm, ConfigForms } from '@deepseek-ai/dsh-client-ui-settings/client';
|
|
30
|
+
/** Options of {@link createServedEntryForm}. */
|
|
31
|
+
export interface ServedEntryFormOptions<T> {
|
|
32
|
+
/** The shared configuration forms service (`ctx.configForms`). */
|
|
33
|
+
forms: ConfigForms;
|
|
34
|
+
/**
|
|
35
|
+
* Profile entry ids this package's rows carry, most likely first: the
|
|
36
|
+
* aggregate's generated row, the package's own standalone row, then the bare
|
|
37
|
+
* namespace. The first is the fallback while the mirror has no answer; the
|
|
38
|
+
* last is the answer when the mirror serves none of them.
|
|
39
|
+
*/
|
|
40
|
+
entryIds: readonly string[];
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Create a settings form bound to the profile entry id this Host serves, and
|
|
44
|
+
* rebound whenever the shared describe mirror names a different one of this
|
|
45
|
+
* package's rows.
|
|
46
|
+
* @param options - the shared forms service and this package's candidate row ids.
|
|
47
|
+
* @returns a form delegating to the currently resolved entry's own form.
|
|
48
|
+
*/
|
|
49
|
+
export declare function createServedEntryForm<T>(options: ServedEntryFormOptions<T>): ConfigForm<T>;
|
|
@@ -43,6 +43,19 @@ export interface SessionsControllerFace {
|
|
|
43
43
|
/** Subscribe to catalog changes; selection ownership rides the same snapshot. */
|
|
44
44
|
subscribe(fn: () => void): () => void;
|
|
45
45
|
}
|
|
46
|
+
/** Stable id shared by the board's sidebar panel row and its main-slot page. */
|
|
47
|
+
export declare const TASK_BOARD_PANEL_ID = "task-board";
|
|
48
|
+
/**
|
|
49
|
+
* The layout's panel-navigation face. The board no longer owns the center
|
|
50
|
+
* column at the DOM level: selecting the panel is the layout's business, and
|
|
51
|
+
* this is the one call that asks for it (null returns to the conversation).
|
|
52
|
+
* Optional, so a composition without the layout service keeps the board's
|
|
53
|
+
* pure state transitions testable.
|
|
54
|
+
*/
|
|
55
|
+
export interface PanelNavigationFace {
|
|
56
|
+
/** Select the board panel, or null to hand the column back to the conversation. */
|
|
57
|
+
select(panelId: string | null): void;
|
|
58
|
+
}
|
|
46
59
|
/** Controller dependencies (all swappable in tests). */
|
|
47
60
|
export interface ControllerDeps {
|
|
48
61
|
store: TaskStore;
|
|
@@ -53,6 +66,8 @@ export interface ControllerDeps {
|
|
|
53
66
|
uuid?: () => string;
|
|
54
67
|
/** Host-authoritative transport; absent keeps the legacy in-memory test path. */
|
|
55
68
|
transport?: TaskBoardTransport;
|
|
69
|
+
/** Layout panel selection; absent keeps the board state-only (tests, shell-less hosts). */
|
|
70
|
+
panel?: PanelNavigationFace;
|
|
56
71
|
}
|
|
57
72
|
/**
|
|
58
73
|
* Register one host directory as a DSH project (workspace). Wired by the
|
|
@@ -149,9 +164,33 @@ export declare class BoardController {
|
|
|
149
164
|
isHostBacked(): boolean;
|
|
150
165
|
/** Retry initial migration/state synchronization after an explicit Host error. */
|
|
151
166
|
retryHostSync(): Promise<boolean>;
|
|
167
|
+
/**
|
|
168
|
+
* Show the board. The layout owns which panel the center column renders, so
|
|
169
|
+
* the state flip and the panel selection travel together here; a composition
|
|
170
|
+
* with no layout face (tests, a shell-less host) still flips the state.
|
|
171
|
+
*
|
|
172
|
+
* The selection is requested AFTER the snapshot flips so a subscriber
|
|
173
|
+
* rendering against `boardOpen` never observes "open" while the shell still
|
|
174
|
+
* shows the conversation.
|
|
175
|
+
*/
|
|
152
176
|
openBoard(): void;
|
|
177
|
+
/**
|
|
178
|
+
* Return the center column to the conversation. The layout's own selection is
|
|
179
|
+
* the source of truth for what the column renders, so this asks for the
|
|
180
|
+
* conversation explicitly rather than only clearing local state.
|
|
181
|
+
*/
|
|
153
182
|
closeBoard(): void;
|
|
154
183
|
toggleBoard(): void;
|
|
184
|
+
/**
|
|
185
|
+
* Reflect a panel selection that came from OUTSIDE this controller (the user
|
|
186
|
+
* clicked another sidebar row, or the layout dropped the panel id). Keeps
|
|
187
|
+
* `boardOpen` aligned with what the column actually shows without asking the
|
|
188
|
+
* layout to select anything back.
|
|
189
|
+
* @param panelId - the layout's current panel id, or null for the conversation.
|
|
190
|
+
*/
|
|
191
|
+
syncPanelSelection(panelId: string | null): void;
|
|
192
|
+
/** Ask the layout to select a panel; a shell that serves no layout face is a no-op. */
|
|
193
|
+
private selectPanel;
|
|
155
194
|
/**
|
|
156
195
|
* Switch between the kanban columns and the archive view. Leaving the
|
|
157
196
|
* archive view with an archived task still selected closes the selection —
|
|
@@ -223,6 +262,7 @@ export declare class BoardController {
|
|
|
223
262
|
setSchedule(id: string, patch: {
|
|
224
263
|
enabled?: boolean;
|
|
225
264
|
cron?: string;
|
|
265
|
+
timeZone?: string | null;
|
|
226
266
|
}): boolean;
|
|
227
267
|
/**
|
|
228
268
|
* Legacy pure-controller seam retained for migration-focused tests. The
|
|
@@ -259,8 +299,8 @@ export declare class BoardController {
|
|
|
259
299
|
* (background navigation, the Host runner creating and selecting a fresh
|
|
260
300
|
* execution session, settlement, other plugins), so closing on `current`
|
|
261
301
|
* changes would evict the board without the user asking. The board closes
|
|
262
|
-
* only on explicit user navigation:
|
|
263
|
-
*
|
|
302
|
+
* only on explicit user navigation: selecting another panel (the layout owns
|
|
303
|
+
* selection, and `syncPanelSelection` follows it) or the board's own actions
|
|
264
304
|
* (openSession / close). Keeping the hook preserves the subscription
|
|
265
305
|
* contract for future listeners.
|
|
266
306
|
*/
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The family health ledger's degraded-record contract (issues #1528, #1730).
|
|
3
|
+
*
|
|
4
|
+
* A family plugin row that fails during apply is recorded by the dsh-web-all
|
|
5
|
+
* shell as a degraded record; the shell serves the ledger through its
|
|
6
|
+
* loopback-only `GET /api/dsh-web-all/degraded` route because the failed
|
|
7
|
+
* plugin could not register any route of its own — the task board whose ledger
|
|
8
|
+
* lock is held by another DSH process is exactly that case, and without this
|
|
9
|
+
* the panel could only report "the Host API is not mounted".
|
|
10
|
+
*
|
|
11
|
+
* Both halves define this shape: the shell WRITES it (packages/dsh-web-all,
|
|
12
|
+
* src/degraded.ts — the two packages have no import edge, one is bundled into
|
|
13
|
+
* the other's profile) and a plugin's browser half PARSES it. The edits move
|
|
14
|
+
* together.
|
|
15
|
+
*/
|
|
16
|
+
/** Where a degraded row failed: module import, plugin shape, or fiber start. */
|
|
17
|
+
export type DegradedStage = 'import' | 'shape' | 'start';
|
|
18
|
+
/** One degraded family row as the shell's health route serves it. */
|
|
19
|
+
export interface DegradedRow {
|
|
20
|
+
/** Real plugin package name from the shell row config. */
|
|
21
|
+
plugin: string;
|
|
22
|
+
/** Where the failure happened. */
|
|
23
|
+
stage: DegradedStage;
|
|
24
|
+
/** The shell's one-line reason, for display. */
|
|
25
|
+
reason: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The one degraded record naming `plugin`, or undefined when that row is not
|
|
29
|
+
* degraded (or the payload does not come from the shell's own shape). A
|
|
30
|
+
* plugin may appear once: the shell keys the ledger by package name and
|
|
31
|
+
* refreshes the record on every new failure.
|
|
32
|
+
* @param body - parsed JSON body of the health route.
|
|
33
|
+
* @param plugin - the real plugin package name to look for.
|
|
34
|
+
* @returns the degraded row, or undefined when the ledger does not name it.
|
|
35
|
+
*/
|
|
36
|
+
export declare function degradedRowOf(body: unknown, plugin: string): DegradedRow | undefined;
|
|
37
|
+
/**
|
|
38
|
+
* Extract the pid of the LIVE process a ledger-lock failure blames, when the
|
|
39
|
+
* reason identifies one. The task board's lock refusal is the one degraded
|
|
40
|
+
* reason that carries an owner the user can act on, so the panel renders the
|
|
41
|
+
* pid in its own sentence and adds the guidance that actually applies.
|
|
42
|
+
* @param reason - the shell's one-line failure reason.
|
|
43
|
+
* @returns the confirmed owner pid, or undefined when the reason names none or
|
|
44
|
+
* warns that the PID may have been reused (its own text owns the recovery).
|
|
45
|
+
*/
|
|
46
|
+
export declare function ledgerOwnerPid(reason: string): number | undefined;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* npm identity of this plugin. The dsh-web-all degraded ledger is keyed by the
|
|
3
|
+
* real plugin package name of a family row, and this plugin's browser half
|
|
4
|
+
* must name its OWN row there to learn why its Host API is missing (issue
|
|
5
|
+
* #1730): the served bundle cannot derive the name, because a family row
|
|
6
|
+
* mounts the package from a versioned profile path. The host half spells the
|
|
7
|
+
* same name in its single-mount guard (src/index.ts); the two move together.
|
|
8
|
+
*/
|
|
9
|
+
export declare const TASK_BOARD_PACKAGE = "@linxin666/dsh-client-ui-task-board";
|
|
@@ -1,15 +1,36 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* runs. Framework-free and dependency-free so the scheduler
|
|
4
|
-
* share one tiny pure module.
|
|
2
|
+
* Zone-aware 5-field cron parsing and next-run computation for scheduled task
|
|
3
|
+
* runs. Framework-free and dependency-free (Intl only) so the Host scheduler
|
|
4
|
+
* and the browser next-run preview share one tiny pure module.
|
|
5
5
|
*
|
|
6
6
|
* Grammar: five whitespace-separated fields, 分 时 日 月 周. Every field
|
|
7
7
|
* supports the wildcard, step (wildcard or range + "/n"), single value,
|
|
8
8
|
* inclusive range a-b, and comma lists mixing any of those. Ranges: minutes
|
|
9
9
|
* 0-59, hours 0-23, days 1-31, months 1-12, weekdays 0-7 (0 and 7 both mean
|
|
10
|
-
* Sunday).
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* Sunday). Invalid expressions parse to null and are rejected by the UI and
|
|
11
|
+
* the Host.
|
|
12
|
+
*
|
|
13
|
+
* Day semantics follow Vixie cron: a field is *starred* when its field text
|
|
14
|
+
* starts with `*`. When day-of-month and day-of-week are both restricted the
|
|
15
|
+
* fields combine with OR; otherwise they combine with AND. The distinction
|
|
16
|
+
* matters for a stepped star such as `*/2`, which still restricts the dates it
|
|
17
|
+
* matches, so `0 0 */2 * 1` means "the Mondays that fall on an even day of
|
|
18
|
+
* month" rather than "every even day, or every Monday".
|
|
19
|
+
*
|
|
20
|
+
* Time zone: every candidate wall clock is interpreted in an explicit IANA
|
|
21
|
+
* zone (the schedule's stored zone, or the Host zone when none is given).
|
|
22
|
+
* A wall clock that does not exist (spring-forward gap) is skipped, and an
|
|
23
|
+
* ambiguous wall clock (fall-back overlap) resolves to the earlier instant, so
|
|
24
|
+
* a repeated local minute fires exactly once. The Host-scheduler semantics are
|
|
25
|
+
* byte-identical to `@deepseek-ai/dsh-schedule`'s resolver, which was verified
|
|
26
|
+
* against it across twelve IANA zones (including 30-minute and 45-minute DST
|
|
27
|
+
* offsets and a date-line jump) and 300k decision instants.
|
|
28
|
+
*
|
|
29
|
+
* That upstream is on the experimental track and is NOT a dependency here (no
|
|
30
|
+
* import, no inject, no peer range), so a change to it fails no test. Re-run the
|
|
31
|
+
* differential check on any `dsh-schedule` release or cohort bump that moves it,
|
|
32
|
+
* and keep this comment honest; the re-verification trigger is recorded in
|
|
33
|
+
* [the owning Agent Note](../../../../.agents/notes/implemented/feature/2026-09-29-task-board-schedule-time-zone.md).
|
|
13
34
|
*/
|
|
14
35
|
/** The parsed match sets of one cron expression. */
|
|
15
36
|
export interface CronSchedule {
|
|
@@ -19,11 +40,61 @@ export interface CronSchedule {
|
|
|
19
40
|
months: ReadonlySet<number>;
|
|
20
41
|
/** Weekdays 0-6, 0 = Sunday (input 7 normalized to 0). */
|
|
21
42
|
weekdays: ReadonlySet<number>;
|
|
22
|
-
/** Whether the day-of-month field
|
|
23
|
-
|
|
24
|
-
/** Whether the weekday field
|
|
25
|
-
|
|
43
|
+
/** Whether the day-of-month field text starts with `*` (Vixie starred). */
|
|
44
|
+
dayStarred: boolean;
|
|
45
|
+
/** Whether the weekday field text starts with `*` (Vixie starred). */
|
|
46
|
+
weekdayStarred: boolean;
|
|
47
|
+
}
|
|
48
|
+
/** Fallback zone when the process reports no usable IANA zone. */
|
|
49
|
+
export declare const HOST_TIME_ZONE_FALLBACK = "UTC";
|
|
50
|
+
/** One resolved wall-clock reading in some zone. */
|
|
51
|
+
export interface ZonedParts {
|
|
52
|
+
year: number;
|
|
53
|
+
month: number;
|
|
54
|
+
day: number;
|
|
55
|
+
hour: number;
|
|
56
|
+
minute: number;
|
|
26
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* The wall-clock reading of one instant in an explicit zone. Reading through
|
|
60
|
+
* Intl (rather than the process `Date` accessors) is what makes the result
|
|
61
|
+
* independent of the Host's own TZ.
|
|
62
|
+
* @param timeZone - explicit IANA zone.
|
|
63
|
+
* @param epochMs - instant to read.
|
|
64
|
+
* @returns the zone-local calendar and clock fields.
|
|
65
|
+
*/
|
|
66
|
+
export declare function partsInZone(timeZone: string, epochMs: number): ZonedParts;
|
|
67
|
+
/**
|
|
68
|
+
* Resolve a wall clock in an explicit zone to its instant.
|
|
69
|
+
*
|
|
70
|
+
* The offset is probed a day either side of the naive guess so both DST
|
|
71
|
+
* transitions are covered, then every candidate offset whose inverse maps back
|
|
72
|
+
* to the requested wall clock is kept. No survivor means the wall clock does
|
|
73
|
+
* not exist (a spring-forward gap); several means it is ambiguous (a fall-back
|
|
74
|
+
* overlap), and the earliest instant wins so the repeated clock is taken once.
|
|
75
|
+
* @param timeZone - explicit IANA zone.
|
|
76
|
+
* @param year - full calendar year.
|
|
77
|
+
* @param month - 1-12.
|
|
78
|
+
* @param day - day of month.
|
|
79
|
+
* @param hour - 0-23.
|
|
80
|
+
* @param minute - 0-59.
|
|
81
|
+
* @returns the instant in ms epoch, or undefined when the wall clock is skipped.
|
|
82
|
+
*/
|
|
83
|
+
export declare function zonedEpoch(timeZone: string, year: number, month: number, day: number, hour: number, minute: number): number | undefined;
|
|
84
|
+
/**
|
|
85
|
+
* The Host process's IANA zone, used for schedules that store no zone of their
|
|
86
|
+
* own (a ledger written before zones were persisted).
|
|
87
|
+
* @returns a zone name Intl accepts, never empty.
|
|
88
|
+
*/
|
|
89
|
+
export declare function resolveHostTimeZone(): string;
|
|
90
|
+
/**
|
|
91
|
+
* Whether a value names a zone this runtime can actually resolve. A stored
|
|
92
|
+
* zone is trusted only after it round-trips through Intl, so a typo or a zone
|
|
93
|
+
* that this ICU build does not know can never arm a schedule.
|
|
94
|
+
* @param value - candidate zone name.
|
|
95
|
+
* @returns true when the name is usable.
|
|
96
|
+
*/
|
|
97
|
+
export declare function isValidTimeZone(value: string): boolean;
|
|
27
98
|
/**
|
|
28
99
|
* Parse a 5-field cron expression.
|
|
29
100
|
* @returns the match sets, or null when the expression is invalid.
|
|
@@ -32,18 +103,22 @@ export declare function parseCron(expr: string): CronSchedule | null;
|
|
|
32
103
|
/** Whether the expression parses. */
|
|
33
104
|
export declare function isValidCron(expr: string): boolean;
|
|
34
105
|
/**
|
|
35
|
-
* Compute the next matching instant after `fromMs`
|
|
36
|
-
*
|
|
37
|
-
* of the matching minute's start, or undefined
|
|
38
|
-
* can never match (for example `0 0 30 2 *`).
|
|
39
|
-
* a full leap cycle, so a valid February 29
|
|
40
|
-
* every non-leap year.
|
|
106
|
+
* Compute the next matching instant after `fromMs` strictly greater than it,
|
|
107
|
+
* interpreting the expression's wall clock in `timeZone` (the Host zone when
|
|
108
|
+
* omitted). Returns the ms epoch of the matching minute's start, or undefined
|
|
109
|
+
* when the calendar constraint can never match (for example `0 0 30 2 *`).
|
|
110
|
+
* The five-year horizon includes a full leap cycle, so a valid February 29
|
|
111
|
+
* schedule remains reachable from every non-leap year.
|
|
41
112
|
*
|
|
42
113
|
* Walks candidate year/month/day/hour/minute values straight from the parsed
|
|
43
114
|
* field sets instead of scanning every minute: a sparse expression such as
|
|
44
115
|
* `0 0 29 2 *` used to iterate ~1.5M wall-clock minutes before reaching the
|
|
45
|
-
* next leap day.
|
|
46
|
-
*
|
|
47
|
-
*
|
|
116
|
+
* next leap day. A candidate wall clock that the zone skips (spring-forward
|
|
117
|
+
* gap) is passed over, which is what makes the gap semantics fall out of the
|
|
118
|
+
* candidate walk rather than needing a separate rule.
|
|
119
|
+
* @param expr - 5-field cron expression.
|
|
120
|
+
* @param fromMs - exclusive lower bound, ms epoch.
|
|
121
|
+
* @param timeZone - explicit IANA zone; defaults to the Host zone.
|
|
122
|
+
* @returns the next trigger instant, or undefined when unreachable.
|
|
48
123
|
*/
|
|
49
|
-
export declare function nextRunAtMs(expr: string, fromMs: number): number | undefined;
|
|
124
|
+
export declare function nextRunAtMs(expr: string, fromMs: number, timeZone?: string): number | undefined;
|