@linxin666/dsh-client-ui-task-board 0.1.20 → 0.2.0

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 (52) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +82 -104
  3. package/README.zh.md +82 -104
  4. package/lib/client.js +574 -581
  5. package/lib/client.js.map +1 -1
  6. package/lib/index.js +2144 -5
  7. package/lib/types/client/PluginSettingsCard.d.ts +7 -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/core/controller.d.ts +40 -15
  13. package/lib/types/core/schedule.d.ts +4 -2
  14. package/lib/types/core/tasks.d.ts +6 -6
  15. package/lib/types/core/use-cases/task-archive.d.ts +5 -4
  16. package/lib/types/core/use-cases/task-schedule.d.ts +3 -3
  17. package/lib/types/dsh-home.d.ts +16 -0
  18. package/lib/types/host-ledger.d.ts +42 -0
  19. package/lib/types/host-routes.d.ts +24 -0
  20. package/lib/types/host-runner.d.ts +30 -0
  21. package/lib/types/host-service.d.ts +41 -0
  22. package/lib/types/index.d.ts +17 -9
  23. package/lib/types/loopback.d.ts +24 -0
  24. package/lib/types/power-inhibitor.d.ts +73 -0
  25. package/lib/types/protocol.d.ts +72 -0
  26. package/package.json +12 -9
  27. package/src/client/PluginSettingsCard.tsx +10 -0
  28. package/src/client/TaskBoardSettingsCard.tsx +51 -4
  29. package/src/client/board/NewTaskModal.tsx +9 -6
  30. package/src/client/board/TaskBoard.tsx +21 -4
  31. package/src/client/board/TaskCard.tsx +24 -8
  32. package/src/client/board/TaskDetail.tsx +83 -50
  33. package/src/client/host-api.ts +95 -0
  34. package/src/client/index.ts +9 -78
  35. package/src/client/locales.ts +24 -4
  36. package/src/core/controller.ts +205 -24
  37. package/src/core/schedule.ts +20 -3
  38. package/src/core/store.ts +4 -8
  39. package/src/core/tasks.ts +6 -6
  40. package/src/core/use-cases/task-archive.ts +14 -5
  41. package/src/core/use-cases/task-schedule.ts +6 -5
  42. package/src/dsh-home.ts +36 -0
  43. package/src/host-ledger.ts +478 -0
  44. package/src/host-routes.ts +188 -0
  45. package/src/host-runner.ts +134 -0
  46. package/src/host-service.ts +209 -0
  47. package/src/index.ts +54 -11
  48. package/src/loopback.ts +63 -0
  49. package/src/power-inhibitor.ts +321 -0
  50. package/src/protocol.ts +213 -0
  51. package/lib/types/core/scheduler.d.ts +0 -70
  52. 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
  }
@@ -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 `settings.plugin.item` slot the
4
+ * plugin-configuration section 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 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: () => 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: () => 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;
@@ -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, 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: () => 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,11 @@ 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
+ private refreshRemote;
213
+ private acceptRemote;
189
214
  private notify;
190
215
  }
@@ -34,7 +34,9 @@ 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.
39
41
  */
40
42
  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,42 @@
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
+ constructor(dir?: string, now?: () => number);
22
+ state(): LedgerState;
23
+ subscribe(listener: () => void): () => void;
24
+ dispose(): void;
25
+ applyRequest(requestId: string, action: TaskBoardAction): {
26
+ state: LedgerState;
27
+ run?: OpenedRun;
28
+ };
29
+ openScheduled(taskId: string, nextRunAt: number | undefined, triggeredAt: number): OpenedRun | undefined;
30
+ skipMissed(now: number): void;
31
+ setScheduler(patch: Partial<TaskBoardSchedulerSnapshot>): void;
32
+ attachSession(taskId: string, executionId: string, sessionId: string): void;
33
+ settle(taskId: string, executionId: string, outcome: 'succeeded' | 'failed' | 'cancelled', error?: string): void;
34
+ private apply;
35
+ private repairSchedules;
36
+ private reconcileInterruptedStarts;
37
+ private load;
38
+ private syncRecentRequests;
39
+ private commit;
40
+ private notify;
41
+ private acquireLock;
42
+ }
@@ -0,0 +1,24 @@
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, so a bare local curl cannot exercise the agent control plane.
19
+ * Authenticated proxies must be explicitly allowlisted and replace the
20
+ * internal token header after their own authentication step.
21
+ */
22
+ export declare function isTrustedTaskBoardRequest(req: IncomingMessage, access: ResolvedRouteAccess): boolean;
23
+ export declare function makeTaskBoardRoutes(service: TaskBoardHostService, access?: TaskBoardRouteAccess): WebRoute[];
24
+ export {};
@@ -0,0 +1,30 @@
1
+ import type { ApiProxy } from '@deepseek-ai/dsh-host-apiproxy';
2
+ import type { TaskRecord } from './core/tasks.ts';
3
+ export type ExecutionInspection = {
4
+ outcome: 'pending';
5
+ } | {
6
+ outcome: 'succeeded';
7
+ } | {
8
+ outcome: 'failed';
9
+ error: string;
10
+ } | {
11
+ outcome: 'cancelled';
12
+ error: string;
13
+ };
14
+ /** A post-create launch failure that still identifies the session to the ledger. */
15
+ export declare class SessionLaunchError extends Error {
16
+ readonly sessionId: string;
17
+ constructor(sessionId: string, cause: unknown);
18
+ }
19
+ export declare class HostExecutionRunner {
20
+ private readonly api;
21
+ constructor(api: ApiProxy);
22
+ launch(task: TaskRecord): Promise<string>;
23
+ listRunning(): Promise<{
24
+ known: true;
25
+ count: number;
26
+ } | {
27
+ known: false;
28
+ }>;
29
+ inspect(sessionId: string, startedAt?: number): Promise<ExecutionInspection>;
30
+ }
@@ -0,0 +1,41 @@
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, 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 readonly now;
19
+ constructor(api: ApiProxy, options?: {
20
+ ledger?: HostTaskLedger;
21
+ power?: PowerInhibitor;
22
+ now?: () => number;
23
+ });
24
+ start(): void;
25
+ setConfiguration(active: boolean, preventIdleSleep: boolean): void;
26
+ snapshot(): TaskBoardSnapshot;
27
+ subscribe(listener: () => void): () => void;
28
+ apply(requestId: string, action: TaskBoardAction): TaskBoardSnapshot;
29
+ dispose(): void;
30
+ private launch;
31
+ private pollSessions;
32
+ private reconcileExecutions;
33
+ private tickSchedule;
34
+ private armedSchedules;
35
+ private hasOpenExecutions;
36
+ private scheduleLaunch;
37
+ private schedulePoll;
38
+ private scheduleTick;
39
+ private syncPowerReasons;
40
+ private emit;
41
+ }
@@ -1,20 +1,17 @@
1
1
  /**
2
2
  * Host loader entry for the task-board plugin.
3
3
  *
4
- * Everything the board does is browser work (DOM, localStorage, driving the
5
- * client runtime's session services over the wire), so the host half's main
6
- * behavior is a system-prompt section announcing the plugin to every agent.
7
- * The section registers while this plugin is in the host composition (mount /
8
- * DSH restart) and disappears when the plugin leaves it (unmount / restart),
9
- * so agents always know the board exists and how to cooperate with it. The
10
- * announcement can be turned off through the web settings plugin-configuration
11
- * surface (`announceToAgent`); the section then disappears live.
4
+ * The Host owns the v2 ledger, action API, cron scheduler, session runner,
5
+ * execution reconciliation, and optional idle-sleep inhibitor. The browser is
6
+ * a same-origin asynchronous view over that service.
12
7
  */
13
8
  import type { Context } from '@deepseek-ai/cordis';
14
9
  import z from 'schemastery';
10
+ /** Default environment variable holding the authenticated proxy token. */
11
+ export declare const DEFAULT_PROXY_TOKEN_ENV = "DSH_TASK_BOARD_PROXY_TOKEN";
15
12
  export declare const inject: string[];
16
13
  /** Model-facing announcement: plugin presence, capabilities, and limits. */
17
- export declare const TASK_BOARD_GUIDANCE = "\u672C\u673A\u5DF2\u5B89\u88C5 dsh-task-board \u63D2\u4EF6\uFF08DSH Web GUI \u7684\u4EFB\u52A1\u770B\u677F\uFF09\uFF1A\u4FA7\u8FB9\u680F\u300C\u4EFB\u52A1\u770B\u677F\u300D\u5165\u53E3\uFF1B\u5728 dsh-web-ui \u63D2\u4EF6\u5168\u5BB6\u6876\u4ED3\u5E93\uFF08packages/dsh-task-board\uFF09\u7EDF\u4E00\u7EF4\u62A4\uFF0C\u7ECF\u805A\u5408\u5305 web-ui-all \u4E00\u952E\u5B89\u88C5\u3002\u80FD\u529B\uFF1A\u591A\u5217\u770B\u677F\u7BA1\u7406\u4EFB\u52A1\uFF1B\u4EFB\u52A1\u53EF\u771F\u5B9E\u6267\u884C\uFF08\u9A71\u52A8 agent \u4F1A\u8BDD\uFF09\uFF1B\u4EFB\u52A1\u53EF\u9489\u4F4F\u6267\u884C\u76EE\u6807\u2014\u2014\u5DE5\u4F5C\u533A / \u6A21\u5F0F\uFF08agent \u9884\u8BBE\uFF09/ \u6743\u9650\uFF08read-only / workspace-write / danger-full-access\uFF09\uFF0C\u7F3A\u7701\u7528\u8FD0\u884C\u65F6\u9ED8\u8BA4\uFF1B\u4EFB\u52A1\u652F\u6301 5 \u6BB5 cron \u5B9A\u65F6\u6267\u884C\uFF08\u5982 0 23 * * *\uFF09\uFF1B\u6570\u636E\u5B58\u6D4F\u89C8\u5668 localStorage\uFF08\u952E dsh.taskBoard.v1\uFF09\u3002\u9650\u5236\uFF1A\u5B9A\u65F6\u8C03\u5EA6\u5728\u6D4F\u89C8\u5668\u7AEF\uFF0C\u9700 GUI \u6807\u7B7E\u9875\u6253\u5F00\uFF0C\u9519\u8FC7\u5373\u8DF3\u8FC7\uFF1B\u6267\u884C\u6D88\u8017 API \u989D\u5EA6\u3002\u7528\u6237\u63D0\u5230\u300C\u4EFB\u52A1\u770B\u677F / \u770B\u677F / \u5B9A\u65F6\u4EFB\u52A1\u300D\u65F6\u5373\u6307\u672C\u63D2\u4EF6\uFF0C\u8BF7\u636E\u6B64\u534F\u4F5C\u3002";
14
+ export declare const TASK_BOARD_GUIDANCE = "\u672C\u673A\u5DF2\u5B89\u88C5 dsh-task-board \u63D2\u4EF6\uFF08DSH Web GUI \u7684\u4EFB\u52A1\u770B\u677F\uFF09\uFF1A\u4FA7\u8FB9\u680F\u300C\u4EFB\u52A1\u770B\u677F\u300D\u5165\u53E3\uFF1B\u5728 dsh-web-ui \u63D2\u4EF6\u5168\u5BB6\u6876\u4ED3\u5E93\uFF08packages/dsh-task-board\uFF09\u7EDF\u4E00\u7EF4\u62A4\uFF0C\u7ECF\u805A\u5408\u5305 web-ui-all \u4E00\u952E\u5B89\u88C5\u3002\u80FD\u529B\uFF1A\u591A\u5217\u770B\u677F\u7BA1\u7406\u4EFB\u52A1\uFF1BHost \u6743\u5A01\u8D26\u672C\uFF1B\u5173\u95ED\u6D4F\u89C8\u5668\u540E\u4ECD\u7531 Host \u6267\u884C\u548C\u7ED3\u7B97\uFF1B\u4EFB\u52A1\u53EF\u9489\u4F4F\u5DE5\u4F5C\u533A\u3001agent \u9884\u8BBE\u548C\u6743\u9650\uFF1B\u652F\u6301 Host \u672C\u5730\u65F6\u533A\u7684 5 \u6BB5 cron\uFF0C\u9519\u8FC7\u7684\u89E6\u53D1\u70B9\u4E0D\u8865\u8DD1\uFF1B\u53EF\u9009\u4E14\u9ED8\u8BA4\u5173\u95ED\u7684\u7A7A\u95F2\u7CFB\u7EDF\u7761\u7720\u4FDD\u62A4\u5141\u8BB8\u5C4F\u5E55\u7184\u706D\uFF0C\u4F46\u4E0D\u627F\u8BFA\u62E6\u622A\u5408\u76D6\u3001\u624B\u52A8\u7761\u7720\u3001\u4F11\u7720\u3001\u5173\u673A\u6216\u5524\u9192\u5DF2\u7761\u7720\u673A\u5668\u3002\u6267\u884C\u6D88\u8017 API \u989D\u5EA6\u3002\u7528\u6237\u63D0\u5230\u300C\u4EFB\u52A1\u770B\u677F / \u770B\u677F / \u5B9A\u65F6\u4EFB\u52A1\u300D\u65F6\u5373\u6307\u672C\u63D2\u4EF6\uFF0C\u8BF7\u636E\u6B64\u534F\u4F5C\u3002";
18
15
  /**
19
16
  * Settings namespace of the board's announcement capability — the section the
20
17
  * web settings surface edits. Spelled here rather than imported: the browser
@@ -31,8 +28,19 @@ export interface Config {
31
28
  announceToAgent?: boolean;
32
29
  /** Master switch for the plugin (browser half + host announcement). */
33
30
  enabled?: boolean;
31
+ /** Prevent idle system sleep while sessions run or schedules are armed. */
32
+ preventIdleSleep?: boolean;
33
+ /** Canonical reverse-proxy Host authorities admitted with a server-side token. */
34
+ trustedProxyHosts?: string[];
35
+ /** Environment variable whose value the authenticated proxy injects upstream. */
36
+ proxyTokenEnv?: string;
34
37
  }
35
38
  export declare const Config: z<Config>;
39
+ /** Resolve proxy access without ever placing the token value in plugin config. */
40
+ export declare function resolveProxyAccess(config: Config | undefined, env?: NodeJS.ProcessEnv): {
41
+ trustedProxyHosts: string[];
42
+ proxyToken?: string;
43
+ };
36
44
  /**
37
45
  * Register the board's announcement section, gated on the composition entry's
38
46
  * `announceToAgent` (and the live settings value once the web settings
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Loopback trust fence shared by the host route families: socket address,
3
+ * Host header, and browser same-origin markers. Packages receive this file as
4
+ * a generated copy via scripts/sync-shared.mjs; edit the shared source and
5
+ * re-run the sync instead of editing a copy.
6
+ *
7
+ * Semantics: RFC 5735 IPv4 127/8, ::1, IPv4-mapped ::ffff:127/8 (matching the
8
+ * remote-web-ui gate), localhost hostnames, plus the browser same-origin
9
+ * markers (sec-fetch-site and Origin) for the request-level fence.
10
+ * @module dsh-web-ui-shared/host/loopback
11
+ */
12
+ import type { IncomingMessage } from 'node:http';
13
+ /** IPv4 127/8 predicate (four decimal octets, first == 127). */
14
+ export declare function isIPv4Loopback(v4: string): boolean;
15
+ /** Whether a socket remote address names the loopback range (127/8, ::1, IPv4-mapped). */
16
+ export declare function isLoopbackAddress(address: string | undefined): boolean;
17
+ /** Whether a normalized URL hostname names the loopback authority (localhost, [::1], 127/8). */
18
+ export declare function isLoopbackHostname(hostname: string): boolean;
19
+ /**
20
+ * Request-level trust fence: a loopback socket address AND a loopback Host
21
+ * header, plus browser same-origin markers. The socket address is
22
+ * authoritative; X-Forwarded-For is never trusted.
23
+ */
24
+ export declare function isLoopbackRequest(request: IncomingMessage): boolean;
@@ -0,0 +1,73 @@
1
+ import { type ChildProcess, type SpawnSyncReturns } from 'node:child_process';
2
+ import { existsSync } from 'node:fs';
3
+ import type { TaskBoardPowerSnapshot } from './protocol.ts';
4
+ declare const WINDOWS_HELPER: string;
5
+ declare const LINUX_HELPER: string;
6
+ declare const LINUX_INHIBIT_PATHS: readonly ["/usr/bin/systemd-inhibit", "/bin/systemd-inhibit"];
7
+ export interface PowerReasons {
8
+ runningSessions: number;
9
+ armedSchedules: number;
10
+ sessionStateKnown: boolean;
11
+ }
12
+ export interface SpawnOptions {
13
+ shell: false;
14
+ windowsHide: boolean;
15
+ stdio: ['pipe', 'pipe', 'pipe'] | ['ignore', 'ignore', 'ignore'];
16
+ }
17
+ export type SpawnLike = (file: string, args: readonly string[], options: SpawnOptions) => ChildProcess;
18
+ export type SpawnSyncLike = (file: string, args: readonly string[], options: {
19
+ stdio: 'ignore';
20
+ timeout: number;
21
+ windowsHide: boolean;
22
+ }) => Pick<SpawnSyncReturns<Buffer>, 'status' | 'error'>;
23
+ export interface PowerInhibitorOptions {
24
+ platform?: NodeJS.Platform;
25
+ pid?: number;
26
+ env?: NodeJS.ProcessEnv;
27
+ spawn?: SpawnLike;
28
+ spawnSync?: SpawnSyncLike;
29
+ exists?: typeof existsSync;
30
+ execPath?: string;
31
+ setTimeout?: typeof globalThis.setTimeout;
32
+ clearTimeout?: typeof globalThis.clearTimeout;
33
+ }
34
+ export declare class PowerInhibitor {
35
+ private readonly listeners;
36
+ private enabled;
37
+ private reasons;
38
+ private phase;
39
+ private child;
40
+ private retry;
41
+ private retryReset;
42
+ private retryIndex;
43
+ private lastError;
44
+ private stopping;
45
+ private readonly platform;
46
+ private readonly pid;
47
+ private readonly env;
48
+ private readonly spawn;
49
+ private readonly spawnSync;
50
+ private readonly exists;
51
+ private readonly execPath;
52
+ private readonly timer;
53
+ private readonly clearTimer;
54
+ private linuxProbe;
55
+ constructor(options?: PowerInhibitorOptions);
56
+ setEnabled(enabled: boolean): void;
57
+ updateReasons(reasons: PowerReasons): void;
58
+ subscribe(listener: () => void): () => void;
59
+ snapshot(): TaskBoardPowerSnapshot;
60
+ dispose(): void;
61
+ private desired;
62
+ private sync;
63
+ private acquire;
64
+ private markReady;
65
+ private fail;
66
+ private release;
67
+ private clearRetryReset;
68
+ private windowsPowerShell;
69
+ private linuxSystemdInhibit;
70
+ private spawnCommand;
71
+ private emit;
72
+ }
73
+ export { LINUX_HELPER, LINUX_INHIBIT_PATHS, WINDOWS_HELPER };