@linxin666/dsh-client-ui-task-board 0.1.20 → 0.2.1
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 +83 -105
- package/README.zh.md +83 -105
- package/lib/client.js +902 -633
- package/lib/client.js.map +1 -1
- package/lib/index.js +2274 -5
- package/lib/types/client/PluginSettingsCard.d.ts +29 -1
- package/lib/types/client/TaskBoardSettingsCard.d.ts +7 -4
- package/lib/types/client/board/TaskCard.d.ts +5 -2
- package/lib/types/client/host-api.d.ts +18 -0
- package/lib/types/client/locales.d.ts +10 -0
- package/lib/types/client/sidebar-entry-core.d.ts +60 -0
- package/lib/types/client/sidebar-entry.d.ts +8 -10
- package/lib/types/core/controller.d.ts +47 -15
- package/lib/types/core/schedule.d.ts +11 -2
- package/lib/types/core/tasks.d.ts +6 -6
- package/lib/types/core/use-cases/task-archive.d.ts +5 -4
- package/lib/types/core/use-cases/task-schedule.d.ts +3 -3
- package/lib/types/dsh-home.d.ts +16 -0
- package/lib/types/host-ledger.d.ts +52 -0
- package/lib/types/host-routes.d.ts +26 -0
- package/lib/types/host-runner.d.ts +41 -0
- package/lib/types/host-service.d.ts +45 -0
- package/lib/types/index.d.ts +17 -9
- package/lib/types/loopback.d.ts +24 -0
- package/lib/types/power-inhibitor.d.ts +73 -0
- package/lib/types/protocol.d.ts +78 -0
- package/package.json +12 -9
- package/src/client/PluginSettingsCard.tsx +229 -18
- package/src/client/TaskBoardSettingsCard.tsx +51 -4
- package/src/client/board/NewTaskModal.tsx +9 -6
- package/src/client/board/TaskBoard.tsx +24 -7
- package/src/client/board/TaskCard.tsx +25 -8
- package/src/client/board/TaskDetail.tsx +83 -50
- package/src/client/board-mount.tsx +1 -0
- package/src/client/host-api.ts +104 -0
- package/src/client/index.ts +9 -78
- package/src/client/locales.ts +24 -4
- package/src/client/settings-card.module.css +90 -1
- package/src/client/sidebar-entry-core.ts +208 -0
- package/src/client/sidebar-entry.ts +24 -144
- package/src/core/controller.ts +222 -24
- package/src/core/schedule.ts +77 -13
- package/src/core/store.ts +4 -8
- package/src/core/tasks.ts +6 -6
- package/src/core/use-cases/task-archive.ts +14 -5
- package/src/core/use-cases/task-schedule.ts +6 -5
- package/src/dsh-home.ts +36 -0
- package/src/host-ledger.ts +539 -0
- package/src/host-routes.ts +196 -0
- package/src/host-runner.ts +152 -0
- package/src/host-service.ts +226 -0
- package/src/index.ts +54 -11
- package/src/loopback.ts +63 -0
- package/src/power-inhibitor.ts +321 -0
- package/src/protocol.ts +220 -0
- package/lib/types/core/scheduler.d.ts +0 -70
- package/src/core/scheduler.ts +0 -161
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { type ReactNode } from 'react';
|
|
10
10
|
import type { CardShell } from './settings-form.ts';
|
|
11
11
|
/** Copy keys the card chrome itself reads; every consumer locale carries this shared vocabulary. */
|
|
12
|
-
export declare const CARD_COPY_KEYS: readonly [
|
|
12
|
+
export declare const CARD_COPY_KEYS: readonly ["settings.collapse", "settings.expand", "settings.notExposed", "settings.unsaved", "settings.readOnly", "settings.saveFailed", "settings.discard", "settings.save", "settings.saving"];
|
|
13
13
|
/** Copy key the card chrome itself reads. */
|
|
14
14
|
export type CardCopyKey = (typeof CARD_COPY_KEYS)[number];
|
|
15
15
|
/** Key domain for the plugin's own copy (inferred per consumer). */
|
|
@@ -39,6 +39,12 @@ export interface PluginSettingsCardProps<TKey extends string = string> {
|
|
|
39
39
|
* already provides the selection.
|
|
40
40
|
*/
|
|
41
41
|
alwaysOpen?: boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Hide the save/discard footer: cards whose body applies its own changes
|
|
44
|
+
* immediately (an embedded external settings section) have no staged
|
|
45
|
+
* edits, so the footer would sit there permanently disabled.
|
|
46
|
+
*/
|
|
47
|
+
hideFooter?: boolean;
|
|
42
48
|
/** The plugin's controls. */
|
|
43
49
|
children: ReactNode;
|
|
44
50
|
}
|
|
@@ -82,6 +88,27 @@ export declare function ValueField(props: FieldProps & {
|
|
|
82
88
|
/** Placeholder shown while the draft is empty. */
|
|
83
89
|
placeholder?: string;
|
|
84
90
|
}): import("react").JSX.Element;
|
|
91
|
+
interface SelectOption {
|
|
92
|
+
value: string;
|
|
93
|
+
label: string;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* The shared dual-mode select control. While an appearance skin is active it
|
|
97
|
+
* renders the legacy native `<select>` untouched, so element-level skin
|
|
98
|
+
* selectors keep working; under the default appearance it renders a
|
|
99
|
+
* self-drawn `role="listbox"` popup whose open/close is transition-animated.
|
|
100
|
+
* Staged cards reach it through BooleanField/ChoiceField; immediate-apply
|
|
101
|
+
* editors (the side-card prefs) bind it directly through onEdit.
|
|
102
|
+
* 双模式下拉框:皮肤激活时用原生 select,默认外观用自绘动画弹层。
|
|
103
|
+
*/
|
|
104
|
+
export declare function SelectField(props: {
|
|
105
|
+
id: string;
|
|
106
|
+
options: ReadonlyArray<SelectOption>;
|
|
107
|
+
value: string;
|
|
108
|
+
disabled: boolean;
|
|
109
|
+
invalid: boolean;
|
|
110
|
+
onEdit: (text: string) => void;
|
|
111
|
+
}): import("react").JSX.Element;
|
|
85
112
|
/** A staged boolean field: 继承 / 开 / 关. */
|
|
86
113
|
export declare function BooleanField(props: FieldProps & {
|
|
87
114
|
/** Copy for the inherit option. */
|
|
@@ -101,3 +128,4 @@ export declare function ChoiceField(props: FieldProps & {
|
|
|
101
128
|
label: string;
|
|
102
129
|
}>;
|
|
103
130
|
}): import("react").JSX.Element;
|
|
131
|
+
export {};
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* plugin
|
|
5
|
-
* namespace.
|
|
2
|
+
* Task-board settings for availability, agent announcement, and optional Host
|
|
3
|
+
* idle-sleep protection. Registers into the `web-ui.plugin.item` child slot
|
|
4
|
+
* the Web UI plugin group renders, bound to the `task-board` namespace.
|
|
6
5
|
*/
|
|
7
6
|
import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
|
|
8
7
|
import type { SettingsScope, SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client';
|
|
@@ -13,6 +12,8 @@ export interface TaskBoardSettings {
|
|
|
13
12
|
enabled?: boolean;
|
|
14
13
|
/** Whether the board announces itself in every agent's system prompt. */
|
|
15
14
|
announceToAgent?: boolean;
|
|
15
|
+
/** Prevent host idle sleep while sessions run or schedules are armed. */
|
|
16
|
+
preventIdleSleep?: boolean;
|
|
16
17
|
}
|
|
17
18
|
/** What the task-board card renders. */
|
|
18
19
|
export interface TaskBoardSettingsCardState extends CardShell {
|
|
@@ -20,6 +21,8 @@ export interface TaskBoardSettingsCardState extends CardShell {
|
|
|
20
21
|
enabled: CardFieldState;
|
|
21
22
|
/** System-prompt announcement flag. */
|
|
22
23
|
announceToAgent: CardFieldState;
|
|
24
|
+
/** Idle-system-sleep protection flag. */
|
|
25
|
+
preventIdleSleep: CardFieldState;
|
|
23
26
|
}
|
|
24
27
|
/** The registration-side face the card's slot entry injects. */
|
|
25
28
|
export interface TaskBoardSettingsCardFace extends CardActions {
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import type { TaskRecord } from '../../core/tasks.ts';
|
|
2
2
|
/** Compact relative/absolute time label. */
|
|
3
|
-
export declare function
|
|
3
|
+
export declare function formatHostTimestamp(ms: number, timeZone?: string): string;
|
|
4
|
+
export declare function formatTime(ms: number, timeZone?: string): string;
|
|
4
5
|
/** One card in a column. */
|
|
5
|
-
declare function TaskCardInner({ task, onClick }: {
|
|
6
|
+
declare function TaskCardInner({ task, pending, timeZone, onClick }: {
|
|
6
7
|
task: TaskRecord;
|
|
8
|
+
pending: boolean;
|
|
9
|
+
timeZone?: string;
|
|
7
10
|
onClick: () => void;
|
|
8
11
|
}): import("react").JSX.Element;
|
|
9
12
|
/** Memoized card: re-renders only when the card's own task record changes. */
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { TaskRecord } from '../core/tasks.ts';
|
|
2
|
+
import { type TaskBoardAction, type TaskBoardEventPayload, type TaskBoardSnapshot } from '../protocol.ts';
|
|
3
|
+
export interface TaskBoardHostTransport {
|
|
4
|
+
bootstrap(legacy: readonly TaskRecord[]): Promise<TaskBoardSnapshot>;
|
|
5
|
+
state(): Promise<TaskBoardSnapshot>;
|
|
6
|
+
action(action: TaskBoardAction): Promise<TaskBoardSnapshot>;
|
|
7
|
+
subscribe(listener: (event?: TaskBoardEventPayload) => void): () => void;
|
|
8
|
+
}
|
|
9
|
+
export declare class HttpTaskBoardHostTransport implements TaskBoardHostTransport {
|
|
10
|
+
private readonly storage;
|
|
11
|
+
constructor(storage?: Pick<Storage, 'getItem' | 'setItem'> | undefined);
|
|
12
|
+
bootstrap(legacy: readonly TaskRecord[]): Promise<TaskBoardSnapshot>;
|
|
13
|
+
state(): Promise<TaskBoardSnapshot>;
|
|
14
|
+
action(action: TaskBoardAction): Promise<TaskBoardSnapshot>;
|
|
15
|
+
private post;
|
|
16
|
+
private request;
|
|
17
|
+
subscribe(listener: (event?: TaskBoardEventPayload) => void): () => void;
|
|
18
|
+
}
|
|
@@ -24,8 +24,12 @@ export declare const zh: {
|
|
|
24
24
|
'board.status.done': string;
|
|
25
25
|
'board.status.failed': string;
|
|
26
26
|
'board.runs': string;
|
|
27
|
+
'board.pending': string;
|
|
27
28
|
'board.updated': string;
|
|
28
29
|
'board.created': string;
|
|
30
|
+
'board.hostError': string;
|
|
31
|
+
'board.retryHost': string;
|
|
32
|
+
'board.hostMeta': string;
|
|
29
33
|
'new.title': string;
|
|
30
34
|
'new.titlePlaceholder': string;
|
|
31
35
|
'new.description': string;
|
|
@@ -99,6 +103,12 @@ export declare const zh: {
|
|
|
99
103
|
'settings.enabledHint': string;
|
|
100
104
|
'settings.announceToAgent': string;
|
|
101
105
|
'settings.announceToAgentHint': string;
|
|
106
|
+
'settings.preventIdleSleep': string;
|
|
107
|
+
'settings.preventIdleSleepHint': string;
|
|
108
|
+
'settings.powerStatus': string;
|
|
109
|
+
'settings.powerBoundary': string;
|
|
110
|
+
'settings.powerUnknown': string;
|
|
111
|
+
'settings.powerError': string;
|
|
102
112
|
'settings.inherit': string;
|
|
103
113
|
'settings.on': string;
|
|
104
114
|
'settings.off': string;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared sidebar entry injection core.
|
|
3
|
+
*
|
|
4
|
+
* dsh's sidebar shell exposes no slot an external plugin can register into,
|
|
5
|
+
* so the entry row is injected between the shell's New Session button and the
|
|
6
|
+
* workspace browser. The injection self-heals: a MutationObserver watches the
|
|
7
|
+
* sidebar root and re-inserts the row whenever a React re-render displaces it
|
|
8
|
+
* (re-insertion happens in the same frame, before paint, so no flicker).
|
|
9
|
+
*
|
|
10
|
+
* The row is plain DOM (no React tree) so it can never disturb the shell's
|
|
11
|
+
* reconciliation; the view it toggles is a separate root owned by the caller.
|
|
12
|
+
*
|
|
13
|
+
* Packages receive this file as a generated copy via scripts/sync-shared.mjs;
|
|
14
|
+
* edit the shared source and re-run the sync instead of editing a copy.
|
|
15
|
+
*/
|
|
16
|
+
/** Per-package configuration for one sidebar entry row. */
|
|
17
|
+
export interface SidebarEntryOptions {
|
|
18
|
+
/** Full attribute name identifying the injected row (idempotency key), e.g. 'data-dsh-ssh-entry'. */
|
|
19
|
+
rowAttribute: string;
|
|
20
|
+
/** CSS selector matching the injected row, e.g. '[data-dsh-ssh-entry]'. */
|
|
21
|
+
rowSelector: string;
|
|
22
|
+
/**
|
|
23
|
+
* L2 semantic-attribute plugin id (issue #506, enum table:
|
|
24
|
+
* skins/skin-center/contracts/semantic-attrs-v1.md). When set, the row also
|
|
25
|
+
* outputs data-dsh-plugin="<id>" and data-dsh-part="sidebar-entry"; unset
|
|
26
|
+
* leaves the row without semantic attributes.
|
|
27
|
+
*/
|
|
28
|
+
plugin?: string;
|
|
29
|
+
/** Inline icon markup (matches the shell's 16px nav-icon look). */
|
|
30
|
+
icon: string;
|
|
31
|
+
/** CSS module class names for the row and its two spans (entry / entryIcon / entryLabel). */
|
|
32
|
+
css: Record<string, string>;
|
|
33
|
+
/** Localized row label (aria-label + visible text). */
|
|
34
|
+
label(): string;
|
|
35
|
+
/** Optional localized tooltip (title attribute). */
|
|
36
|
+
tooltip?(): string;
|
|
37
|
+
/** Click action (open/toggle the owning panel). */
|
|
38
|
+
onToggle(): void;
|
|
39
|
+
/** Family-block position: 'before' inserts ahead of sibling plugin rows, 'after' behind them. */
|
|
40
|
+
position: 'before' | 'after';
|
|
41
|
+
/**
|
|
42
|
+
* Selectors of the sibling plugin entry rows this package orders against
|
|
43
|
+
* (its own row included — the placement guard excludes a row that is
|
|
44
|
+
* already inside the root). Each package passes the same list it used
|
|
45
|
+
* before the consolidation so the rendered order stays stable.
|
|
46
|
+
*/
|
|
47
|
+
familySelectors: readonly string[];
|
|
48
|
+
/** Optional active-state bridge; highlights the row while the panel is open. */
|
|
49
|
+
active?: {
|
|
50
|
+
subscribe(listener: () => void): () => void;
|
|
51
|
+
isOpen(): boolean;
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Mount the sidebar entry, waiting for the shell to render and self-healing
|
|
56
|
+
* on later React re-renders.
|
|
57
|
+
* @param options - the row's attribute/icon/copy/action/ordering configuration.
|
|
58
|
+
* @returns disposer removing the entry and its observers.
|
|
59
|
+
*/
|
|
60
|
+
export declare function mountSidebarEntry(options: SidebarEntryOptions): () => void;
|
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Sidebar entry injection.
|
|
2
|
+
* Sidebar entry injection — package-specific wiring over the shared core.
|
|
3
3
|
*
|
|
4
4
|
* dsh's sidebar shell exposes no slot an external plugin can register into
|
|
5
5
|
* (`sidebar.workspaces` / `sidebar.settings` are single-occupant and already
|
|
6
|
-
* taken), so
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* reconciliation; the board view it toggles is a separate React root mounted
|
|
14
|
-
* in the center column (see board-mount.ts).
|
|
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).
|
|
15
13
|
*/
|
|
16
14
|
import type { BoardController } from '../core/controller.ts';
|
|
17
15
|
/** Stable data attribute identifying the injected entry row. */
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Board controller: the single owner of task-ledger state and view state.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* navigates to a session (the sessions-list `current` selection changes).
|
|
4
|
+
* In production it projects the Host ledger and submits confirmed actions;
|
|
5
|
+
* the legacy store/execution seams remain for v1 migration tests. It also
|
|
6
|
+
* closes the board whenever the user navigates to a session.
|
|
8
7
|
* Framework-free (structural runtime faces) so the whole orchestration is
|
|
9
8
|
* unit-testable with fakes.
|
|
10
9
|
*
|
|
@@ -13,10 +12,17 @@
|
|
|
13
12
|
* owns only the orchestration seam (state, persistence, notify, execution,
|
|
14
13
|
* navigation, reconciliation).
|
|
15
14
|
*/
|
|
16
|
-
import { ExecutionService } from './execution.ts';
|
|
15
|
+
import type { ExecutionService } from './execution.ts';
|
|
17
16
|
import type { TaskStore } from './store.ts';
|
|
18
17
|
import { type NewTaskInput, type TaskRecord, type TaskStatus } from './tasks.ts';
|
|
19
18
|
import { type TaskUpdatePatch } from './use-cases/task-update.ts';
|
|
19
|
+
import type { TaskBoardAction, TaskBoardEventPayload, TaskBoardSnapshot } from '../protocol.ts';
|
|
20
|
+
export interface TaskBoardTransport {
|
|
21
|
+
bootstrap(legacy: readonly TaskRecord[]): Promise<TaskBoardSnapshot>;
|
|
22
|
+
state(): Promise<TaskBoardSnapshot>;
|
|
23
|
+
action(action: TaskBoardAction): Promise<TaskBoardSnapshot>;
|
|
24
|
+
subscribe(listener: (event?: TaskBoardEventPayload) => void): () => void;
|
|
25
|
+
}
|
|
20
26
|
/** The sessions face the controller needs for navigation awareness. */
|
|
21
27
|
export interface SessionsControllerFace {
|
|
22
28
|
list: {
|
|
@@ -31,7 +37,8 @@ export interface SessionsControllerFace {
|
|
|
31
37
|
/** Controller dependencies (all swappable in tests). */
|
|
32
38
|
export interface ControllerDeps {
|
|
33
39
|
store: TaskStore;
|
|
34
|
-
|
|
40
|
+
/** Legacy browser execution seam used only by isolated v1 tests. */
|
|
41
|
+
exec?: ExecutionService;
|
|
35
42
|
sessions: SessionsControllerFace;
|
|
36
43
|
/** Clock; defaults to Date.now. */
|
|
37
44
|
now?: () => number;
|
|
@@ -39,6 +46,8 @@ export interface ControllerDeps {
|
|
|
39
46
|
uuid?: () => string;
|
|
40
47
|
/** Debounce (ms) for session-list-changed reconciles; defaults to 350. */
|
|
41
48
|
reconcileDebounceMs?: number;
|
|
49
|
+
/** Host-authoritative transport; absent keeps the legacy in-memory test path. */
|
|
50
|
+
transport?: TaskBoardTransport;
|
|
42
51
|
}
|
|
43
52
|
/** One workspace option the execution-target pickers offer. */
|
|
44
53
|
export interface ExecutionWorkspaceOption {
|
|
@@ -69,6 +78,9 @@ export interface ControllerSnapshot {
|
|
|
69
78
|
selectedTaskId: string | undefined;
|
|
70
79
|
/** Picker option sets (workspace list + agent-preset roster). */
|
|
71
80
|
executionOptions: ExecutionOptionsSnapshot;
|
|
81
|
+
pendingTaskIds: readonly string[];
|
|
82
|
+
transportError?: string;
|
|
83
|
+
host?: Pick<TaskBoardSnapshot, 'revision' | 'scheduler' | 'power'>;
|
|
72
84
|
}
|
|
73
85
|
/** The selected task (resolved from the ledger), or undefined. */
|
|
74
86
|
export declare function selectedTaskOf(snapshot: ControllerSnapshot): TaskRecord | undefined;
|
|
@@ -87,6 +99,12 @@ export declare class BoardController {
|
|
|
87
99
|
private disposers;
|
|
88
100
|
private readonly now;
|
|
89
101
|
private readonly uuid;
|
|
102
|
+
private readonly pendingTaskIds;
|
|
103
|
+
private readonly taskQueues;
|
|
104
|
+
private transportError;
|
|
105
|
+
private hostState;
|
|
106
|
+
private remoteSubscribed;
|
|
107
|
+
private remoteInitialization;
|
|
90
108
|
/** @param deps - store, execution service, and the sessions navigation face. */
|
|
91
109
|
constructor(deps: ControllerDeps);
|
|
92
110
|
/** Load the persisted ledger and start the navigation/status subscriptions. */
|
|
@@ -95,6 +113,10 @@ export declare class BoardController {
|
|
|
95
113
|
dispose(): void;
|
|
96
114
|
getSnapshot(): ControllerSnapshot;
|
|
97
115
|
subscribe(fn: () => void): () => void;
|
|
116
|
+
/** Whether production mutations are confirmed by the Host transport. */
|
|
117
|
+
isHostBacked(): boolean;
|
|
118
|
+
/** Retry initial migration/state synchronization after an explicit Host error. */
|
|
119
|
+
retryHostSync(): Promise<boolean>;
|
|
98
120
|
openBoard(): void;
|
|
99
121
|
closeBoard(): void;
|
|
100
122
|
toggleBoard(): void;
|
|
@@ -107,6 +129,8 @@ export declare class BoardController {
|
|
|
107
129
|
openTask(id: string): void;
|
|
108
130
|
closeTask(): void;
|
|
109
131
|
createTask(input: NewTaskInput): TaskRecord | undefined;
|
|
132
|
+
/** Create through the Host and expose the task only after confirmation. */
|
|
133
|
+
createTaskConfirmed(input: NewTaskInput): Promise<TaskRecord | undefined>;
|
|
110
134
|
updateTask(id: string, patch: TaskUpdatePatch): void;
|
|
111
135
|
/**
|
|
112
136
|
* Replace (a part of) the picker option sets the UI feeds (workspace list
|
|
@@ -138,18 +162,13 @@ export declare class BoardController {
|
|
|
138
162
|
cron?: string;
|
|
139
163
|
}): boolean;
|
|
140
164
|
/**
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
* schedule rule (it was deleted mid-tick, for example).
|
|
165
|
+
* Legacy pure-controller seam retained for migration-focused tests. The
|
|
166
|
+
* production browser never rolls schedules; the Host ledger owns them.
|
|
144
167
|
*/
|
|
145
168
|
applyScheduleNextRun(id: string, nextRunAt: number | undefined, lastTriggeredAt: number | undefined): void;
|
|
146
169
|
/**
|
|
147
|
-
* Reload the
|
|
148
|
-
*
|
|
149
|
-
* tab (or a stale in-memory copy) can never be fired from this tab: the
|
|
150
|
-
* fire decision and the subsequent roll-forward both run on the freshest
|
|
151
|
-
* persisted truth. Deliberately silent — same-origin external changes still
|
|
152
|
-
* reach subscribers through the storage-event subscription.
|
|
170
|
+
* Reload the legacy v1 store without notifying subscribers. Production v2
|
|
171
|
+
* reads Host snapshots instead; this remains only for isolated legacy tests.
|
|
153
172
|
*/
|
|
154
173
|
reloadFromStore(): void;
|
|
155
174
|
/**
|
|
@@ -186,5 +205,18 @@ export declare class BoardController {
|
|
|
186
205
|
/** Settle tasks left 'running' whose sessions already finished. */
|
|
187
206
|
private reconcileRunningTasks;
|
|
188
207
|
private persistAndNotify;
|
|
208
|
+
private commitRemote;
|
|
209
|
+
private performRemote;
|
|
210
|
+
private initializeRemote;
|
|
211
|
+
private doInitializeRemote;
|
|
212
|
+
/**
|
|
213
|
+
* SSE frames carry revision/scheduler/power. When the revision matches the
|
|
214
|
+
* one already applied, apply the frame's scheduler/power in place and skip
|
|
215
|
+
* the full /state fetch; otherwise the 5 s heartbeat would re-clone and
|
|
216
|
+
* re-serialize the whole ledger per tab even while nothing changes.
|
|
217
|
+
*/
|
|
218
|
+
private onRemoteEvent;
|
|
219
|
+
private refreshRemote;
|
|
220
|
+
private acceptRemote;
|
|
189
221
|
private notify;
|
|
190
222
|
}
|
|
@@ -34,7 +34,16 @@ export declare function isValidCron(expr: string): boolean;
|
|
|
34
34
|
/**
|
|
35
35
|
* Compute the next matching instant after `fromMs` (ms epoch), in local time,
|
|
36
36
|
* at minute granularity, strictly greater than `fromMs`. Returns the ms epoch
|
|
37
|
-
* of the matching minute's start, or undefined when
|
|
38
|
-
*
|
|
37
|
+
* of the matching minute's start, or undefined when the calendar constraint
|
|
38
|
+
* can never match (for example `0 0 30 2 *`). The five-year horizon includes
|
|
39
|
+
* a full leap cycle, so a valid February 29 schedule remains reachable from
|
|
40
|
+
* every non-leap year.
|
|
41
|
+
*
|
|
42
|
+
* Walks candidate year/month/day/hour/minute values straight from the parsed
|
|
43
|
+
* field sets instead of scanning every minute: a sparse expression such as
|
|
44
|
+
* `0 0 29 2 *` used to iterate ~1.5M wall-clock minutes before reaching the
|
|
45
|
+
* next leap day. Wall-clock field construction + the final `matches` re-check
|
|
46
|
+
* preserve the old minute scan's DST semantics exactly (nonexistent spring
|
|
47
|
+
* minutes normalize forward and the repeated fall-back hour is never visited).
|
|
39
48
|
*/
|
|
40
49
|
export declare function nextRunAtMs(expr: string, fromMs: number): number | undefined;
|
|
@@ -26,9 +26,8 @@ export interface ExecutionRecord {
|
|
|
26
26
|
error: string | undefined;
|
|
27
27
|
}
|
|
28
28
|
/**
|
|
29
|
-
* A scheduled-run rule attached to a task. The
|
|
30
|
-
*
|
|
31
|
-
* persisted with the task (localStorage), so scheduling survives refreshes.
|
|
29
|
+
* A scheduled-run rule attached to a task. The Host scheduler triggers the
|
|
30
|
+
* task when `nextRunAt` is due and persists the rule in the Host ledger.
|
|
32
31
|
*/
|
|
33
32
|
export interface ScheduleRule {
|
|
34
33
|
/** Whether the schedule is armed. */
|
|
@@ -77,14 +76,15 @@ export interface TaskRecord {
|
|
|
77
76
|
permission?: TaskPermission;
|
|
78
77
|
/**
|
|
79
78
|
* When the task was archived (ms epoch). Archived tasks keep their status
|
|
80
|
-
* and execution history
|
|
79
|
+
* and execution history, leave the main board, and cannot run until restored;
|
|
80
|
+
* absent means on-board.
|
|
81
81
|
*/
|
|
82
82
|
archivedAt?: number;
|
|
83
83
|
}
|
|
84
84
|
/** Statuses a settled task may be archived from. */
|
|
85
85
|
export declare const ARCHIVABLE_STATUSES: readonly TaskStatus[];
|
|
86
86
|
/** Permission presets a task may pin on its execution session (the `/permission <id>` ids). */
|
|
87
|
-
export declare const TASK_PERMISSIONS: readonly [
|
|
87
|
+
export declare const TASK_PERMISSIONS: readonly ["read-only", "workspace-write", "danger-full-access"];
|
|
88
88
|
/** One permission preset id. */
|
|
89
89
|
export type TaskPermission = typeof TASK_PERMISSIONS[number];
|
|
90
90
|
/** Whether an unknown value is a known permission preset id. */
|
|
@@ -124,7 +124,7 @@ export declare const ALL_STATUSES: readonly TaskStatus[];
|
|
|
124
124
|
/** Brand an unknown string as a status; undefined when it is not one. */
|
|
125
125
|
export declare function isTaskStatus(value: unknown): value is TaskStatus;
|
|
126
126
|
/** Whether a manual move target is allowed from the given status. */
|
|
127
|
-
export declare function canMoveManually(
|
|
127
|
+
export declare function canMoveManually(from: TaskStatus, to: TaskStatus): boolean;
|
|
128
128
|
/** Normalize one optional execution-target string: trim; blank collapses to undefined. */
|
|
129
129
|
export declare function normalizeTargetId(value: string | undefined): string | undefined;
|
|
130
130
|
/** Create a task from user input. */
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Archive/restore task use case: move a settled (done/failed) task off the
|
|
3
|
-
* main board and back.
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* main board and back. The task keeps its status, execution history, and
|
|
4
|
+
* transcript references, while archiving disarms any schedule so it cannot
|
|
5
|
+
* create more execution records until the user restores and re-enables it.
|
|
6
6
|
*/
|
|
7
7
|
import type { TaskRecord } from '../tasks.ts';
|
|
8
8
|
/** Result of an archive transition. */
|
|
@@ -15,7 +15,8 @@ export interface ArchiveTaskResult {
|
|
|
15
15
|
/**
|
|
16
16
|
* Archive one task: only settled statuses (done/failed) can be archived;
|
|
17
17
|
* a running or not-yet-settled task stays on the board (its runner still
|
|
18
|
-
* owns its lifecycle).
|
|
18
|
+
* owns its lifecycle). Archiving disarms a schedule; already-archived tasks
|
|
19
|
+
* are a no-op.
|
|
19
20
|
*/
|
|
20
21
|
export declare function applyArchiveTask(tasks: readonly TaskRecord[], id: string, now: number): ArchiveTaskResult;
|
|
21
22
|
/** Restore one task back onto the main board (clears the archive marker). */
|
|
@@ -12,9 +12,9 @@ export interface SetScheduleResult {
|
|
|
12
12
|
applied: boolean;
|
|
13
13
|
}
|
|
14
14
|
/**
|
|
15
|
-
* Set
|
|
16
|
-
* untouched); an enabled rule computes the
|
|
17
|
-
*
|
|
15
|
+
* Set an on-board task's schedule rule. A blank or invalid cron, or an
|
|
16
|
+
* archived task, is rejected (state untouched); an enabled rule computes the
|
|
17
|
+
* next run instant immediately, a disabled one carries no next-run instant.
|
|
18
18
|
* @param tasks - current ledger.
|
|
19
19
|
* @param id - the task to schedule.
|
|
20
20
|
* @param patch - rule fields to change (absent fields keep their current value).
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DSH_HOME resolution shared by the plugin family's Host halves: the
|
|
3
|
+
* environment override wins, the platform home fallback follows. Mirrors
|
|
4
|
+
* what dsh-pet and dsh-liangshen each used to implement locally.
|
|
5
|
+
*/
|
|
6
|
+
/** Expand a leading ~ (or ~user) in a path, platform-style. */
|
|
7
|
+
export declare function expandHome(path: string, home?: string): string;
|
|
8
|
+
/**
|
|
9
|
+
* Resolve the DSH home directory.
|
|
10
|
+
* @param env - process environment to read DSH_HOME from.
|
|
11
|
+
* @param home - platform home directory fallback (test seam).
|
|
12
|
+
* @returns the absolute DSH home path.
|
|
13
|
+
*/
|
|
14
|
+
export declare function resolveDshHome(env?: NodeJS.ProcessEnv, home?: string): string;
|
|
15
|
+
/** Resolve the DSH home directory from the live environment. */
|
|
16
|
+
export declare function dshHome(): string;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { type ExecutionRecord, type TaskRecord } from './core/tasks.ts';
|
|
2
|
+
import { type TaskBoardAction, type TaskBoardSchedulerSnapshot } from './protocol.ts';
|
|
3
|
+
export interface LedgerState {
|
|
4
|
+
revision: number;
|
|
5
|
+
tasks: TaskRecord[];
|
|
6
|
+
scheduler: TaskBoardSchedulerSnapshot;
|
|
7
|
+
}
|
|
8
|
+
export interface OpenedRun {
|
|
9
|
+
task: TaskRecord;
|
|
10
|
+
execution: ExecutionRecord;
|
|
11
|
+
}
|
|
12
|
+
export declare class HostTaskLedger {
|
|
13
|
+
private readonly now;
|
|
14
|
+
private document;
|
|
15
|
+
private readonly listeners;
|
|
16
|
+
private readonly requestCache;
|
|
17
|
+
private readonly lockToken;
|
|
18
|
+
private lockFd;
|
|
19
|
+
readonly file: string;
|
|
20
|
+
readonly lockFile: string;
|
|
21
|
+
/** Small sidecar for the 30 s scheduler heartbeat (lastTickAt only). */
|
|
22
|
+
readonly schedulerFile: string;
|
|
23
|
+
constructor(dir?: string, now?: () => number);
|
|
24
|
+
/** Revision + scheduler without any task cloning; feeds the SSE event frame. */
|
|
25
|
+
summary(): {
|
|
26
|
+
revision: number;
|
|
27
|
+
scheduler: TaskBoardSchedulerSnapshot;
|
|
28
|
+
};
|
|
29
|
+
state(): LedgerState;
|
|
30
|
+
subscribe(listener: () => void): () => void;
|
|
31
|
+
dispose(): void;
|
|
32
|
+
applyRequest(requestId: string, action: TaskBoardAction): {
|
|
33
|
+
state: LedgerState;
|
|
34
|
+
run?: OpenedRun;
|
|
35
|
+
};
|
|
36
|
+
openScheduled(taskId: string, nextRunAt: number | undefined, triggeredAt: number): OpenedRun | undefined;
|
|
37
|
+
skipMissed(now: number): void;
|
|
38
|
+
setScheduler(patch: Partial<TaskBoardSchedulerSnapshot>): void;
|
|
39
|
+
attachSession(taskId: string, executionId: string, sessionId: string): void;
|
|
40
|
+
settle(taskId: string, executionId: string, outcome: 'succeeded' | 'failed' | 'cancelled', error?: string): void;
|
|
41
|
+
private apply;
|
|
42
|
+
private repairSchedules;
|
|
43
|
+
private reconcileInterruptedStarts;
|
|
44
|
+
private load;
|
|
45
|
+
private syncRecentRequests;
|
|
46
|
+
private readSchedulerSidecar;
|
|
47
|
+
/** Atomic write of the scheduler heartbeat sidecar (0600, tmp + rename + fsync). */
|
|
48
|
+
private writeSchedulerSidecar;
|
|
49
|
+
private commit;
|
|
50
|
+
private notify;
|
|
51
|
+
private acquireLock;
|
|
52
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { IncomingMessage } from 'node:http';
|
|
2
|
+
import type { WebRoute } from '@deepseek-ai/dsh-host-webserver';
|
|
3
|
+
import type { TaskBoardHostService } from './host-service.ts';
|
|
4
|
+
/** Header replaced by an authenticated same-host reverse proxy. */
|
|
5
|
+
export declare const TASK_BOARD_PROXY_TOKEN_HEADER = "x-dsh-task-board-proxy-token";
|
|
6
|
+
/** Optional authenticated reverse-proxy access layered over the loopback default. */
|
|
7
|
+
export interface TaskBoardRouteAccess {
|
|
8
|
+
trustedProxyHosts?: readonly string[];
|
|
9
|
+
proxyToken?: string;
|
|
10
|
+
}
|
|
11
|
+
interface ResolvedRouteAccess {
|
|
12
|
+
trustedProxyHosts: ReadonlySet<string>;
|
|
13
|
+
proxyToken?: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Task-board route fence. Direct desktop access uses the repository-wide
|
|
17
|
+
* loopback socket + Host guard and additionally requires a browser same-origin
|
|
18
|
+
* marker: a bare local curl without any browser signal cannot exercise the
|
|
19
|
+
* agent control plane (a forged Origin does pass the marker — it is a
|
|
20
|
+
* tripwire, the socket/Host/origin-equality checks carry the authority).
|
|
21
|
+
* Authenticated proxies must be explicitly allowlisted and replace the
|
|
22
|
+
* internal token header after their own authentication step.
|
|
23
|
+
*/
|
|
24
|
+
export declare function isTrustedTaskBoardRequest(req: IncomingMessage, access: ResolvedRouteAccess): boolean;
|
|
25
|
+
export declare function makeTaskBoardRoutes(service: TaskBoardHostService, access?: TaskBoardRouteAccess): WebRoute[];
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { ApiProxy } from '@deepseek-ai/dsh-host-apiproxy';
|
|
2
|
+
import type { TaskRecord } from './core/tasks.ts';
|
|
3
|
+
/** One session-list row, extracted from the sessions.list RPC result. */
|
|
4
|
+
export type SessionSummary = Extract<Awaited<ReturnType<ApiProxy['sessions']['list']>>['result'], {
|
|
5
|
+
ok: true;
|
|
6
|
+
}>['value']['items'][number];
|
|
7
|
+
export type ExecutionInspection = {
|
|
8
|
+
outcome: 'pending';
|
|
9
|
+
} | {
|
|
10
|
+
outcome: 'succeeded';
|
|
11
|
+
} | {
|
|
12
|
+
outcome: 'failed';
|
|
13
|
+
error: string;
|
|
14
|
+
} | {
|
|
15
|
+
outcome: 'cancelled';
|
|
16
|
+
error: string;
|
|
17
|
+
};
|
|
18
|
+
/** A post-create launch failure that still identifies the session to the ledger. */
|
|
19
|
+
export declare class SessionLaunchError extends Error {
|
|
20
|
+
readonly sessionId: string;
|
|
21
|
+
constructor(sessionId: string, cause: unknown);
|
|
22
|
+
}
|
|
23
|
+
export declare class HostExecutionRunner {
|
|
24
|
+
private readonly api;
|
|
25
|
+
constructor(api: ApiProxy);
|
|
26
|
+
launch(task: TaskRecord): Promise<string>;
|
|
27
|
+
listRunning(): Promise<{
|
|
28
|
+
known: true;
|
|
29
|
+
count: number;
|
|
30
|
+
items: SessionSummary[];
|
|
31
|
+
} | {
|
|
32
|
+
known: false;
|
|
33
|
+
}>;
|
|
34
|
+
/**
|
|
35
|
+
* Resolve one execution's outcome. The caller may pass the session list it
|
|
36
|
+
* already fetched this poll tick; otherwise inspect lists sessions itself.
|
|
37
|
+
* Sharing the list keeps a poll with E open executions at one list RPC
|
|
38
|
+
* instead of 1 + E.
|
|
39
|
+
*/
|
|
40
|
+
inspect(sessionId: string, startedAt?: number, sessions?: readonly SessionSummary[]): Promise<ExecutionInspection>;
|
|
41
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { ApiProxy } from '@deepseek-ai/dsh-host-apiproxy';
|
|
2
|
+
import { HostTaskLedger } from './host-ledger.ts';
|
|
3
|
+
import { HostExecutionRunner } from './host-runner.ts';
|
|
4
|
+
import { PowerInhibitor } from './power-inhibitor.ts';
|
|
5
|
+
import type { TaskBoardAction, TaskBoardEventPayload, TaskBoardSnapshot } from './protocol.ts';
|
|
6
|
+
export declare class TaskBoardHostService {
|
|
7
|
+
readonly ledger: HostTaskLedger;
|
|
8
|
+
readonly runner: HostExecutionRunner;
|
|
9
|
+
readonly power: PowerInhibitor;
|
|
10
|
+
private readonly listeners;
|
|
11
|
+
private timers;
|
|
12
|
+
private lastScheduleTick;
|
|
13
|
+
private disposed;
|
|
14
|
+
private pollInFlight;
|
|
15
|
+
private tickInFlight;
|
|
16
|
+
private active;
|
|
17
|
+
private preventIdleSleep;
|
|
18
|
+
private lastPowerJson;
|
|
19
|
+
private readonly now;
|
|
20
|
+
constructor(api: ApiProxy, options?: {
|
|
21
|
+
ledger?: HostTaskLedger;
|
|
22
|
+
power?: PowerInhibitor;
|
|
23
|
+
now?: () => number;
|
|
24
|
+
});
|
|
25
|
+
start(): void;
|
|
26
|
+
setConfiguration(active: boolean, preventIdleSleep: boolean): void;
|
|
27
|
+
snapshot(): TaskBoardSnapshot;
|
|
28
|
+
/** SSE frame payload; deliberately skips the tasks deep-clone of {@link snapshot}. */
|
|
29
|
+
eventPayload(): TaskBoardEventPayload;
|
|
30
|
+
subscribe(listener: () => void): () => void;
|
|
31
|
+
apply(requestId: string, action: TaskBoardAction): TaskBoardSnapshot;
|
|
32
|
+
dispose(): void;
|
|
33
|
+
private launch;
|
|
34
|
+
private pollSessions;
|
|
35
|
+
/** Reuse the session list this poll already fetched: one list RPC per tick, not 1 + E. */
|
|
36
|
+
private reconcileExecutions;
|
|
37
|
+
private tickSchedule;
|
|
38
|
+
private armedSchedules;
|
|
39
|
+
private hasOpenExecutions;
|
|
40
|
+
private scheduleLaunch;
|
|
41
|
+
private schedulePoll;
|
|
42
|
+
private scheduleTick;
|
|
43
|
+
private syncPowerReasons;
|
|
44
|
+
private emit;
|
|
45
|
+
}
|