@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.
Files changed (58) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +83 -105
  3. package/README.zh.md +83 -105
  4. package/lib/client.js +902 -633
  5. package/lib/client.js.map +1 -1
  6. package/lib/index.js +2274 -5
  7. package/lib/types/client/PluginSettingsCard.d.ts +29 -1
  8. package/lib/types/client/TaskBoardSettingsCard.d.ts +7 -4
  9. package/lib/types/client/board/TaskCard.d.ts +5 -2
  10. package/lib/types/client/host-api.d.ts +18 -0
  11. package/lib/types/client/locales.d.ts +10 -0
  12. package/lib/types/client/sidebar-entry-core.d.ts +60 -0
  13. package/lib/types/client/sidebar-entry.d.ts +8 -10
  14. package/lib/types/core/controller.d.ts +47 -15
  15. package/lib/types/core/schedule.d.ts +11 -2
  16. package/lib/types/core/tasks.d.ts +6 -6
  17. package/lib/types/core/use-cases/task-archive.d.ts +5 -4
  18. package/lib/types/core/use-cases/task-schedule.d.ts +3 -3
  19. package/lib/types/dsh-home.d.ts +16 -0
  20. package/lib/types/host-ledger.d.ts +52 -0
  21. package/lib/types/host-routes.d.ts +26 -0
  22. package/lib/types/host-runner.d.ts +41 -0
  23. package/lib/types/host-service.d.ts +45 -0
  24. package/lib/types/index.d.ts +17 -9
  25. package/lib/types/loopback.d.ts +24 -0
  26. package/lib/types/power-inhibitor.d.ts +73 -0
  27. package/lib/types/protocol.d.ts +78 -0
  28. package/package.json +12 -9
  29. package/src/client/PluginSettingsCard.tsx +229 -18
  30. package/src/client/TaskBoardSettingsCard.tsx +51 -4
  31. package/src/client/board/NewTaskModal.tsx +9 -6
  32. package/src/client/board/TaskBoard.tsx +24 -7
  33. package/src/client/board/TaskCard.tsx +25 -8
  34. package/src/client/board/TaskDetail.tsx +83 -50
  35. package/src/client/board-mount.tsx +1 -0
  36. package/src/client/host-api.ts +104 -0
  37. package/src/client/index.ts +9 -78
  38. package/src/client/locales.ts +24 -4
  39. package/src/client/settings-card.module.css +90 -1
  40. package/src/client/sidebar-entry-core.ts +208 -0
  41. package/src/client/sidebar-entry.ts +24 -144
  42. package/src/core/controller.ts +222 -24
  43. package/src/core/schedule.ts +77 -13
  44. package/src/core/store.ts +4 -8
  45. package/src/core/tasks.ts +6 -6
  46. package/src/core/use-cases/task-archive.ts +14 -5
  47. package/src/core/use-cases/task-schedule.ts +6 -5
  48. package/src/dsh-home.ts +36 -0
  49. package/src/host-ledger.ts +539 -0
  50. package/src/host-routes.ts +196 -0
  51. package/src/host-runner.ts +152 -0
  52. package/src/host-service.ts +226 -0
  53. package/src/index.ts +54 -11
  54. package/src/loopback.ts +63 -0
  55. package/src/power-inhibitor.ts +321 -0
  56. package/src/protocol.ts +220 -0
  57. package/lib/types/core/scheduler.d.ts +0 -70
  58. 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 ['settings.collapse', 'settings.expand', 'settings.notExposed', 'settings.unsaved', 'settings.readOnly', 'settings.saveFailed', 'settings.discard', 'settings.save', 'settings.saving'];
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
- * The task-board settings card: whether the board announces itself in every
3
- * agent's system prompt. Registers into the `settings.plugin.item` slot the
4
- * plugin-configuration section renders, bound to the `task-board` settings
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 formatTime(ms: number): string;
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 — following the skin precedent of DOM-level extension — the
7
- * entry row is injected between the shell's New Session button and the
8
- * workspace browser. The injection self-heals: a MutationObserver watches the
9
- * sidebar root and re-inserts the row whenever a React re-render displaces it
10
- * (re-insertion happens in the same frame, before paint, so no flicker).
11
- *
12
- * The row is plain DOM (no React tree) so it can never disturb the shell's
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
- * It keeps the ledger in memory, persists every mutation through the
5
- * {@link TaskStore}, drives real executions through the
6
- * {@link ExecutionService}, and closes the board view whenever the user
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
- exec: ExecutionService;
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
- * Roll a task's schedule forward (scheduler callback): persist the next due
142
- * instant and the trigger instant of this run. No-op when the task has no
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 ledger from the persisted store without notifying subscribers.
148
- * The scheduler calls this before every tick so a task deleted in another
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 nothing matches within
38
- * 366 days (e.g. `0 0 30 2 *`).
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 browser-side scheduler ticks
30
- * every minute and triggers the task when `nextRunAt` is due; the rule is
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 but leave the main board; absent means on-board.
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 ['read-only', 'workspace-write', 'danger-full-access'];
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(_from: TaskStatus, to: TaskStatus): boolean;
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. Purely a marker transition — the task keeps its
4
- * status, execution history and transcript references, so archiving never
5
- * loses traceability and restoring lands it in the exact column it left.
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). Already-archived tasks are a no-op.
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 a task's schedule rule. A blank or invalid cron is rejected (state
16
- * untouched); an enabled rule computes the next run instant immediately, a
17
- * disabled or invalid one carries no next-run instant.
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
+ }