@linxin666/dsh-client-ui-task-board 0.1.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 (50) hide show
  1. package/LICENSE +29 -0
  2. package/README.md +211 -0
  3. package/cordis.patch.yml +12 -0
  4. package/lib/client.js +2592 -0
  5. package/lib/index.js +54 -0
  6. package/lib/invariant.js +6 -0
  7. package/lib/types/client/PluginSettingsCard.d.ts +77 -0
  8. package/lib/types/client/TaskBoardSettingsCard.d.ts +51 -0
  9. package/lib/types/client/board/ConfirmDialog.d.ts +12 -0
  10. package/lib/types/client/board/NewTaskModal.d.ts +6 -0
  11. package/lib/types/client/board/TaskBoard.d.ts +5 -0
  12. package/lib/types/client/board/TaskCard.d.ts +12 -0
  13. package/lib/types/client/board/TaskDetail.d.ts +7 -0
  14. package/lib/types/client/board-mount.d.ts +10 -0
  15. package/lib/types/client/index.d.ts +43 -0
  16. package/lib/types/client/locales.d.ts +105 -0
  17. package/lib/types/client/settings-form.d.ts +118 -0
  18. package/lib/types/client/sidebar-entry.d.ts +25 -0
  19. package/lib/types/core/controller.d.ts +129 -0
  20. package/lib/types/core/execution.d.ts +136 -0
  21. package/lib/types/core/schedule.d.ts +40 -0
  22. package/lib/types/core/scheduler.d.ts +49 -0
  23. package/lib/types/core/store.d.ts +36 -0
  24. package/lib/types/core/tasks.d.ts +111 -0
  25. package/lib/types/index.d.ts +44 -0
  26. package/lib/types/invariant.d.ts +3 -0
  27. package/package.json +85 -0
  28. package/src/client/PluginSettingsCard.tsx +207 -0
  29. package/src/client/TaskBoardSettingsCard.tsx +124 -0
  30. package/src/client/board/ConfirmDialog.tsx +40 -0
  31. package/src/client/board/NewTaskModal.tsx +82 -0
  32. package/src/client/board/TaskBoard.tsx +107 -0
  33. package/src/client/board/TaskCard.tsx +62 -0
  34. package/src/client/board/TaskDetail.tsx +286 -0
  35. package/src/client/board-mount.tsx +74 -0
  36. package/src/client/board.module.css +849 -0
  37. package/src/client/css-modules.d.ts +5 -0
  38. package/src/client/index.ts +167 -0
  39. package/src/client/locales.ts +211 -0
  40. package/src/client/settings-card.module.css +261 -0
  41. package/src/client/settings-form.ts +294 -0
  42. package/src/client/sidebar-entry.ts +118 -0
  43. package/src/core/controller.ts +379 -0
  44. package/src/core/execution.ts +259 -0
  45. package/src/core/schedule.ts +140 -0
  46. package/src/core/scheduler.ts +104 -0
  47. package/src/core/store.ts +183 -0
  48. package/src/core/tasks.ts +204 -0
  49. package/src/index.ts +94 -0
  50. package/src/invariant.ts +4 -0
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Board controller: the single owner of task-ledger state and view state.
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).
8
+ * Framework-free (structural runtime faces) so the whole orchestration is
9
+ * unit-testable with fakes.
10
+ */
11
+ import { ExecutionService } from './execution.ts';
12
+ import type { TaskStore } from './store.ts';
13
+ import { type NewTaskInput, type TaskRecord, type TaskStatus } from './tasks.ts';
14
+ /** The sessions face the controller needs for navigation awareness. */
15
+ export interface SessionsControllerFace {
16
+ list: {
17
+ getSnapshot(): {
18
+ current: string | undefined;
19
+ };
20
+ subscribe(fn: () => void): () => void;
21
+ };
22
+ /** Select a session as current (navigates the conversation view). */
23
+ open(id: string): void;
24
+ }
25
+ /** Controller dependencies (all swappable in tests). */
26
+ export interface ControllerDeps {
27
+ store: TaskStore;
28
+ exec: ExecutionService;
29
+ sessions: SessionsControllerFace;
30
+ /** Clock; defaults to Date.now. */
31
+ now?: () => number;
32
+ /** Id minting; defaults to a random-uuid. */
33
+ uuid?: () => string;
34
+ /** Debounce (ms) for session-list-changed reconciles; defaults to 350. */
35
+ reconcileDebounceMs?: number;
36
+ }
37
+ /** Immutable controller snapshot for UI subscriptions. */
38
+ export interface ControllerSnapshot {
39
+ tasks: readonly TaskRecord[];
40
+ boardOpen: boolean;
41
+ selectedTaskId: string | undefined;
42
+ }
43
+ /** The selected task (resolved from the ledger), or undefined. */
44
+ export declare function selectedTaskOf(snapshot: ControllerSnapshot): TaskRecord | undefined;
45
+ /**
46
+ * Board controller (see module doc). All mutations bump the snapshot and
47
+ * persist through the store; UI and DOM mounts subscribe and re-render.
48
+ */
49
+ export declare class BoardController {
50
+ private readonly deps;
51
+ private tasks;
52
+ private boardOpen;
53
+ private selectedTaskId;
54
+ private listeners;
55
+ private disposers;
56
+ private readonly now;
57
+ private readonly uuid;
58
+ /** @param deps - store, execution service, and the sessions navigation face. */
59
+ constructor(deps: ControllerDeps);
60
+ /** Load the persisted ledger and start the navigation/status subscriptions. */
61
+ start(): void;
62
+ /** Stop all subscriptions and drop retained state (idempotent). */
63
+ dispose(): void;
64
+ getSnapshot(): ControllerSnapshot;
65
+ subscribe(fn: () => void): () => void;
66
+ openBoard(): void;
67
+ closeBoard(): void;
68
+ toggleBoard(): void;
69
+ openTask(id: string): void;
70
+ closeTask(): void;
71
+ createTask(input: NewTaskInput): TaskRecord | undefined;
72
+ updateTask(id: string, patch: Partial<Pick<TaskRecord, 'title' | 'description' | 'prompt'>>): void;
73
+ moveTask(id: string, status: TaskStatus): void;
74
+ deleteTask(id: string): void;
75
+ /**
76
+ * Update a task's schedule rule. A blank or invalid cron expression is
77
+ * rejected (returns false, state untouched). When the rule ends up enabled
78
+ * the next run instant is computed immediately; a disabled rule carries no
79
+ * next-run instant.
80
+ * @param id - the task to schedule.
81
+ * @param patch - fields to change (absent fields keep their current value).
82
+ * @returns true when applied, false when rejected (invalid cron / unknown task).
83
+ */
84
+ setSchedule(id: string, patch: {
85
+ enabled?: boolean;
86
+ cron?: string;
87
+ }): boolean;
88
+ /**
89
+ * Roll a task's schedule forward (scheduler callback): persist the next due
90
+ * instant and the trigger instant of this run. No-op when the task has no
91
+ * schedule rule (it was deleted mid-tick, for example).
92
+ */
93
+ applyScheduleNextRun(id: string, nextRunAt: number | undefined, lastTriggeredAt: number | undefined): void;
94
+ /**
95
+ * Jump to an execution's session transcript. Selecting the session changes
96
+ * `current`, which closes the board (the conversation view takes over).
97
+ * @param sessionId - the execution session to open.
98
+ */
99
+ openSession(sessionId: string): void;
100
+ /**
101
+ * Execute a task for real: move it to 'running', open an execution record,
102
+ * and hand off to the ExecutionService. A second call while the task is
103
+ * already running is ignored.
104
+ */
105
+ runTask(id: string): Promise<boolean>;
106
+ /** Re-run a settled task: move it back to 'todo' first, then execute. */
107
+ rerunTask(id: string): Promise<void>;
108
+ private handleExecutionEvent;
109
+ /** Reconcile running tasks and close the board when the user navigates. */
110
+ private onSessionsChanged;
111
+ private lastCurrent;
112
+ /** Execution ids launched on this page; they settle via their live watch, never list reconciliation. */
113
+ private readonly activeExecutionIds;
114
+ /** Debounce timer for {@link reconcileRunningTasks}. */
115
+ private reconcileTimer;
116
+ /** Whether a reconcile pass is underway (single-flight guard). */
117
+ private reconcileInFlight;
118
+ /**
119
+ * Debounce + single-flight trigger for the running-task reconciliation.
120
+ * Session-list notifications arrive in bursts (one per session status
121
+ * change); both guards together keep a burst from reading the history API
122
+ * once per running task.
123
+ */
124
+ private scheduleReconcile;
125
+ /** Settle tasks left 'running' whose sessions already finished. */
126
+ private reconcileRunningTasks;
127
+ private persistAndNotify;
128
+ private notify;
129
+ }
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Execution service: runs a task through dsh's real session machinery.
3
+ *
4
+ * The board's "run" button must make dsh actually work, not fake a status:
5
+ * the service connects a real session (workspace blank-session reuse or
6
+ * `session.create` on the host via the workspaces service), renames it to
7
+ * the task title, sends the task prompt with `session.prompt`, and then
8
+ * watches the session's conversation snapshot until its turn settles. The
9
+ * task board controller consumes {@link ExecutionEvent}s to move the card
10
+ * running → done/failed and to keep the execution record.
11
+ *
12
+ * Deliberately framework-free: the runtime faces are declared structurally
13
+ * (a narrow slice of the real `ctx.sessions` / `ctx.workspaces` contracts)
14
+ * so tests drive it with plain fakes.
15
+ */
16
+ import type { ExecutionRecord, TaskRecord } from './tasks.ts';
17
+ /** The narrow sessions face the service needs. */
18
+ export interface SessionsExecutionFace {
19
+ list: {
20
+ getSnapshot(): {
21
+ /** Baseline arrival lifecycle — 'pending' until the host list has loaded. */
22
+ phase: 'pending' | 'ready';
23
+ byId: Record<string, {
24
+ running: boolean;
25
+ completed?: boolean;
26
+ }>;
27
+ };
28
+ subscribe(fn: () => void): () => void;
29
+ };
30
+ binding(id: string): {
31
+ session: SessionDriver;
32
+ } | undefined;
33
+ }
34
+ /** The narrow workspaces face the service needs. */
35
+ export interface WorkspacesExecutionFace {
36
+ list: {
37
+ getSnapshot(): {
38
+ items: readonly {
39
+ workspaceId: string;
40
+ }[];
41
+ recentWorkspaceId: string | undefined;
42
+ };
43
+ };
44
+ connectWorkspace(workspaceId: string): Promise<string>;
45
+ }
46
+ /** One raw session-history event narrowed to the failure signal reconcile needs. */
47
+ export interface ExecutionHistoryEvent {
48
+ type: string;
49
+ data?: unknown;
50
+ }
51
+ /** Optional raw-history face used to detect failures of never-opened sessions. */
52
+ export interface HistoryExecutionFace {
53
+ loadTail(sessionId: string): Promise<{
54
+ events: readonly ExecutionHistoryEvent[];
55
+ } | undefined>;
56
+ }
57
+ /** The behavior verbs the service invokes on an execution session. */
58
+ export interface SessionDriver {
59
+ rename(title: string): Promise<unknown>;
60
+ prompt(content: readonly unknown[], mode: 'queue'): Promise<{
61
+ ok: true;
62
+ } | {
63
+ ok: false;
64
+ error: unknown;
65
+ }>;
66
+ getSnapshot(): {
67
+ running: boolean;
68
+ lastAgentError: string | null;
69
+ turnEnds: ReadonlyMap<number, number>;
70
+ };
71
+ subscribe(fn: () => void): () => void;
72
+ }
73
+ /** Everything the service needs from the runtime. */
74
+ export interface ExecutionEnvironment {
75
+ sessions: SessionsExecutionFace;
76
+ workspaces: WorkspacesExecutionFace;
77
+ /** Raw-history reader for failure detection of never-opened sessions. */
78
+ history?: HistoryExecutionFace;
79
+ }
80
+ /** Outcome events the service emits to the controller. */
81
+ export type ExecutionEvent = {
82
+ kind: 'started';
83
+ taskId: string;
84
+ executionId: string;
85
+ sessionId: string;
86
+ } | {
87
+ kind: 'settled';
88
+ taskId: string;
89
+ executionId: string;
90
+ outcome: 'succeeded' | 'failed' | 'cancelled';
91
+ error?: string;
92
+ };
93
+ /**
94
+ * Run one task to completion (or to a settled failure).
95
+ *
96
+ * @param task - the task being executed.
97
+ * @param execution - the freshly opened execution record (id + start time).
98
+ * @param onEvent - callback for started/settled events.
99
+ * @returns resolves when the run settles (or fails to start); never rejects —
100
+ * every failure path is reported as a settled event.
101
+ */
102
+ export declare class ExecutionService {
103
+ private readonly env;
104
+ /** @param env - the runtime faces (real or fake). */
105
+ constructor(env: ExecutionEnvironment);
106
+ run(task: TaskRecord, execution: ExecutionRecord, onEvent: (event: ExecutionEvent) => void): Promise<void>;
107
+ /**
108
+ * Inspect a reloaded/background task that was left 'running' and emit a
109
+ * settled event when its session already finished.
110
+ *
111
+ * A session that was never opened keeps a cold conversation snapshot (the
112
+ * runtime only maintains the window for the staged/current session), so the
113
+ * settled outcome is decided by the strongest available signal, in order:
114
+ * 1. the list summary — missing session → cancelled; still running → pending;
115
+ * 2. a warm conversation snapshot → `lastAgentError` decides failed/succeeded;
116
+ * 3. the raw history tail (when a history face is wired) — a `turn/end`
117
+ * error reason proves failure;
118
+ * 4. otherwise a finished session counts as succeeded.
119
+ *
120
+ * @param task - a task whose latest execution has no endedAt.
121
+ * @returns a settled event when the session state proves completion, else undefined.
122
+ */
123
+ reconcile(task: TaskRecord): Promise<ExecutionEvent | undefined>;
124
+ /** Best-effort failure probe over the raw history tail (false when unavailable). */
125
+ private historyShowsFailure;
126
+ private connectSession;
127
+ private driverOf;
128
+ private sendPrompt;
129
+ /**
130
+ * Subscribe to the execution session and settle the run once the accepted
131
+ * turn completes (turn counter advanced past the acceptance baseline and
132
+ * the session is no longer running). Never settles while the session is
133
+ * still running; unsubscribes on settle.
134
+ */
135
+ private watchForSettlement;
136
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Minimal 5-field cron parsing and next-run computation for scheduled task
3
+ * runs. Framework-free and dependency-free so the scheduler and controller
4
+ * share one tiny pure module.
5
+ *
6
+ * Grammar: five whitespace-separated fields, 分 时 日 月 周. Every field
7
+ * supports the wildcard, step (wildcard or range + "/n"), single value,
8
+ * inclusive range a-b, and comma lists mixing any of those. Ranges: minutes
9
+ * 0-59, hours 0-23, days 1-31, months 1-12, weekdays 0-7 (0 and 7 both mean
10
+ * Sunday). When both the day and weekday fields are restricted they combine
11
+ * with OR semantics (standard cron). Invalid expressions parse to null and
12
+ * are rejected by the UI/controller.
13
+ */
14
+ /** The parsed match sets of one cron expression. */
15
+ export interface CronSchedule {
16
+ minutes: ReadonlySet<number>;
17
+ hours: ReadonlySet<number>;
18
+ days: ReadonlySet<number>;
19
+ months: ReadonlySet<number>;
20
+ /** Weekdays 0-6, 0 = Sunday (input 7 normalized to 0). */
21
+ weekdays: ReadonlySet<number>;
22
+ /** Whether the day-of-month field was the literal '*' (unrestricted). */
23
+ dayWildcard: boolean;
24
+ /** Whether the weekday field was the literal '*' (unrestricted). */
25
+ weekdayWildcard: boolean;
26
+ }
27
+ /**
28
+ * Parse a 5-field cron expression.
29
+ * @returns the match sets, or null when the expression is invalid.
30
+ */
31
+ export declare function parseCron(expr: string): CronSchedule | null;
32
+ /** Whether the expression parses. */
33
+ export declare function isValidCron(expr: string): boolean;
34
+ /**
35
+ * Compute the next matching instant after `fromMs` (ms epoch), in local time,
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 *`).
39
+ */
40
+ export declare function nextRunAtMs(expr: string, fromMs: number): number | undefined;
@@ -0,0 +1,49 @@
1
+ import type { TaskRecord } from './tasks.ts';
2
+ /** Everything the scheduler needs from its host (the board controller). */
3
+ export interface SchedulerDeps {
4
+ /** Read the current task ledger (the controller snapshot). */
5
+ tasks(): readonly TaskRecord[];
6
+ /** Clock; defaults to Date.now in the controller wiring. */
7
+ now(): number;
8
+ /** Trigger one task's real execution (the controller's runTask); resolves
9
+ * true when the run was accepted, false when rejected (e.g. the task is
10
+ * already running). */
11
+ runTask(id: string): Promise<boolean>;
12
+ /** Persist a rolled-forward schedule (next due + this trigger instant). */
13
+ applySchedule(id: string, nextRunAt: number | undefined, lastTriggeredAt: number | undefined): void;
14
+ /** Tick cadence; defaults to 60_000 ms. */
15
+ tickMs?: number;
16
+ /**
17
+ * Gate: while false the tick no-ops (e.g. the session list baseline has not
18
+ * arrived on page load, so executions would fail). Defaults to always ready.
19
+ */
20
+ ready?: () => boolean;
21
+ /** Environment listeners for tab-visibility recovery (browser only). */
22
+ environment?: {
23
+ addEventListener(type: 'visibilitychange', listener: () => void): void;
24
+ removeEventListener(type: 'visibilitychange', listener: () => void): void;
25
+ };
26
+ }
27
+ /**
28
+ * The schedule heartbeat (see module doc). `tick` is public so tests and
29
+ * callers can drive a check without waiting for the interval.
30
+ */
31
+ export declare class SchedulerService {
32
+ private readonly deps;
33
+ private timer;
34
+ private disposed;
35
+ /** @param deps - tasks/clock/trigger/apply faces (see {@link SchedulerDeps}). */
36
+ constructor(deps: SchedulerDeps);
37
+ /** Start ticking: one immediate check (catch-up after reload) + the interval. */
38
+ start(): void;
39
+ /** Stop ticking and drop listeners (idempotent). */
40
+ dispose(): void;
41
+ /**
42
+ * Check every enabled schedule and trigger the due ones. Idempotent per
43
+ * task per tick: the schedule is rolled forward only after runTask accepts
44
+ * the run, so a rejected run keeps its due slot and is retried on the next
45
+ * tick instead of being silently dropped.
46
+ */
47
+ tick(): Promise<void>;
48
+ private readonly onVisibility;
49
+ }
@@ -0,0 +1,36 @@
1
+ import type { TaskRecord } from './tasks.ts';
2
+ /** Persistence seam for the task ledger. */
3
+ export interface TaskStore {
4
+ /** Read the persisted ledger (empty when nothing is stored yet). */
5
+ load(): TaskRecord[];
6
+ /** Persist the whole ledger (replaces the stored document). */
7
+ save(tasks: readonly TaskRecord[]): void;
8
+ /** Drop the persisted ledger (leaves the in-memory state alone). */
9
+ clear(): void;
10
+ }
11
+ /** Storage key for the task ledger document. */
12
+ export declare const DEFAULT_STORAGE_KEY = "dsh.taskBoard.v1";
13
+ /** A task record is structurally valid if it round-trips through the UI. */
14
+ export declare function isTaskRecord(value: unknown): value is TaskRecord;
15
+ /** Parse + validate a persisted ledger document; invalid rows are dropped. */
16
+ export declare function parseLedger(raw: string | null): TaskRecord[];
17
+ /** localStorage-backed store (the browser backend). */
18
+ export declare class LocalStorageTaskStore implements TaskStore {
19
+ private readonly key;
20
+ private readonly storage;
21
+ /**
22
+ * @param key - storage key for the ledger document.
23
+ * @param storage - storage backend (defaults to the global localStorage; tests inject fakes).
24
+ */
25
+ constructor(key?: string, storage?: Pick<Storage, 'getItem' | 'setItem' | 'removeItem'> | undefined);
26
+ load(): TaskRecord[];
27
+ save(tasks: readonly TaskRecord[]): void;
28
+ clear(): void;
29
+ }
30
+ /** In-memory backend (tests, and a fallback when storage is unavailable). */
31
+ export declare class InMemoryTaskStore implements TaskStore {
32
+ private ledger;
33
+ load(): TaskRecord[];
34
+ save(tasks: readonly TaskRecord[]): void;
35
+ clear(): void;
36
+ }
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Task board domain model: task lifecycle statuses, the task record shape,
3
+ * and the pure transition functions the controller and tests share.
4
+ * Framework-free (no cordis, no runtime imports) so the state machine is
5
+ * unit-testable in isolation.
6
+ */
7
+ /** Task lifecycle status, one per kanban column. */
8
+ export type TaskStatus = 'backlog' | 'todo' | 'running' | 'done' | 'failed';
9
+ /**
10
+ * One real execution attempt: the run's own id, the dsh session that ran it
11
+ * (filled once the session is created), and the settled outcome once the
12
+ * session's turn ended.
13
+ */
14
+ export interface ExecutionRecord {
15
+ /** Execution attempt id (uuid). */
16
+ id: string;
17
+ /** The dsh session that ran this attempt; absent until creation resolves. */
18
+ sessionId: string | undefined;
19
+ /** When the run started (ms epoch). */
20
+ startedAt: number;
21
+ /** When the run settled; absent while still running. */
22
+ endedAt: number | undefined;
23
+ /** Outcome once settled. */
24
+ result: 'succeeded' | 'failed' | 'cancelled' | undefined;
25
+ /** Human failure text when the run failed (prompt rejection or agent error). */
26
+ error: string | undefined;
27
+ }
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.
32
+ */
33
+ export interface ScheduleRule {
34
+ /** Whether the schedule is armed. */
35
+ enabled: boolean;
36
+ /** 5-field cron expression: `分 时 日 月 周`. */
37
+ cron: string;
38
+ /** Next due instant (ms epoch); maintained by the scheduler/controller. */
39
+ nextRunAt: number | undefined;
40
+ /** Instant of the latest scheduled trigger (ms epoch). */
41
+ lastTriggeredAt: number | undefined;
42
+ }
43
+ /** One task on the board. */
44
+ export interface TaskRecord {
45
+ /** Stable task id (uuid). */
46
+ id: string;
47
+ /** Short display title. */
48
+ title: string;
49
+ /** Longer human description shown in the detail view. */
50
+ description: string;
51
+ /** The prompt sent to dsh when this task is executed. */
52
+ prompt: string;
53
+ /** Current column. */
54
+ status: TaskStatus;
55
+ /** Creation instant (ms epoch). */
56
+ createdAt: number;
57
+ /** Last mutation instant (ms epoch). */
58
+ updatedAt: number;
59
+ /** Every execution attempt, most recent last. */
60
+ executions: ExecutionRecord[];
61
+ /** Optional scheduled-run rule (absent on tasks without a schedule). */
62
+ schedule?: ScheduleRule;
63
+ }
64
+ /** Input for creating a task. */
65
+ export interface NewTaskInput {
66
+ title: string;
67
+ description: string;
68
+ prompt: string;
69
+ }
70
+ /** The five kanban columns, in display order. */
71
+ export declare const COLUMNS: readonly {
72
+ status: TaskStatus;
73
+ label: string;
74
+ }[];
75
+ /** Statuses a user may move a card to manually (execution states are owned by the runner). */
76
+ export declare const MANUAL_STATUSES: readonly TaskStatus[];
77
+ /** Statuses the runner may move a card to from 'running'. */
78
+ export declare const RUNNER_SETTLE_STATUSES: readonly TaskStatus[];
79
+ /** All valid statuses (closed union guard). */
80
+ export declare const ALL_STATUSES: readonly TaskStatus[];
81
+ /** Brand an unknown string as a status; undefined when it is not one. */
82
+ export declare function isTaskStatus(value: unknown): value is TaskStatus;
83
+ /** Whether a manual move target is allowed from the given status. */
84
+ export declare function canMoveManually(_from: TaskStatus, to: TaskStatus): boolean;
85
+ /** Create a task from user input. */
86
+ export declare function createTask(input: NewTaskInput, now: number, id: string): TaskRecord;
87
+ /** Clone a task with an updated status and a fresh updatedAt. */
88
+ export declare function withStatus(task: TaskRecord, status: TaskStatus, now: number): TaskRecord;
89
+ /**
90
+ * Merge a schedule patch into a task's schedule rule (creating it when
91
+ * absent), with a fresh updatedAt. Keys present in the patch overwrite the
92
+ * current value — including explicit `undefined`, which clears a field (used
93
+ * to disarm `nextRunAt`); absent keys keep their current value.
94
+ */
95
+ export declare function withSchedule(task: TaskRecord, patch: Partial<ScheduleRule>, now: number): TaskRecord;
96
+ /**
97
+ * Open a fresh execution on a task: move it to 'running' and append a
98
+ * running execution record. Returns the new task and the new execution.
99
+ */
100
+ export declare function startExecution(task: TaskRecord, now: number, executionId: string): {
101
+ task: TaskRecord;
102
+ execution: ExecutionRecord;
103
+ };
104
+ /**
105
+ * Settle a running execution: record the outcome and move the task into the
106
+ * matching column. No-op (returns the input task) when the execution is not
107
+ * the task's latest or is already settled.
108
+ */
109
+ export declare function settleExecution(task: TaskRecord, executionId: string, outcome: 'succeeded' | 'failed' | 'cancelled', now: number, error: string | undefined): TaskRecord;
110
+ /** A settled-execution summary string for the detail view. */
111
+ export declare function executionLabel(execution: ExecutionRecord): string;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Host loader entry for the task-board plugin.
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.
12
+ */
13
+ import type { Context } from '@deepseek-ai/cordis';
14
+ import z from 'schemastery';
15
+ export declare const inject: string[];
16
+ /** 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\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";
18
+ /**
19
+ * Settings namespace of the board's announcement capability — the section the
20
+ * web settings surface edits. Spelled here rather than imported: the browser
21
+ * half spells the same value and must not depend on a Host package.
22
+ */
23
+ export declare const TASK_BOARD_SETTINGS_NAMESPACE: import("@deepseek-ai/dsh-settings").SettingsNamespace;
24
+ /** Plugin config, validated by the same-named schemastery schema. */
25
+ export interface Config {
26
+ /**
27
+ * When true (default), a system-prompt section announces the board to every
28
+ * agent. Set false to keep the board silent in prompts; agents then learn
29
+ * about it only when the user mentions it.
30
+ */
31
+ announceToAgent?: boolean;
32
+ /** Master switch for the plugin (browser half + host announcement). */
33
+ enabled?: boolean;
34
+ }
35
+ export declare const Config: z<Config>;
36
+ /**
37
+ * Register the board's announcement section, gated on the composition entry's
38
+ * `announceToAgent` (and the live settings value once the web settings
39
+ * surface is served). The section is re-registered whenever the source
40
+ * changes, so a settings edit takes effect without a restart.
41
+ * @param ctx - the plugin context (systemPrompt injected).
42
+ * @param config - resolved plugin config (schema defaults applied by the loader).
43
+ */
44
+ export declare function apply(ctx: Context, config?: Config): void;
@@ -0,0 +1,3 @@
1
+ /** Invariant companion plugin (no assertions — nothing to check at runtime). */
2
+ /** Provides no assertions: the board owns no cross-package runtime invariants. */
3
+ export declare function apply(): void;
package/package.json ADDED
@@ -0,0 +1,85 @@
1
+ {
2
+ "name": "@linxin666/dsh-client-ui-task-board",
3
+ "description": "Task board for the dsh web GUI: a sidebar entry plus a multi-column kanban view with local persistence, task details, and real execution through dsh sessions (session.prompt). Hot-pluggable — mounted via ~/.dsh/cordis.patch.yml + a profile node_modules symlink, no dsh source changes.",
4
+ "version": "0.1.1",
5
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "types": "lib/types/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./lib/types/index.d.ts",
11
+ "default": "./lib/index.js"
12
+ },
13
+ "./invariant": {
14
+ "types": "./lib/types/invariant.d.ts",
15
+ "default": "./lib/invariant.js"
16
+ },
17
+ "./client": {
18
+ "types": "./lib/types/client/index.d.ts",
19
+ "default": "./lib/client.js"
20
+ },
21
+ "./src/*": "./src/*",
22
+ "./package.json": "./package.json"
23
+ },
24
+ "dsh": {
25
+ "bundle": {
26
+ "patch": "./cordis.patch.yml"
27
+ },
28
+ "client": {
29
+ "inject": [
30
+ "@deepseek-ai/dsh-client-runtime",
31
+ "@deepseek-ai/dsh-client-connection",
32
+ "@deepseek-ai/dsh-client-ui-settings"
33
+ ],
34
+ "platform": "web"
35
+ }
36
+ },
37
+ "peerDependencies": {
38
+ "@deepseek-ai/dsh-client-connection": "^0.1.0-rc.6",
39
+ "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.6",
40
+ "@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.6",
41
+ "@deepseek-ai/dsh-settings": "^0.1.0-rc.6",
42
+ "react": "^18.2.0"
43
+ },
44
+ "dependencies": {
45
+ "schemastery": "^3.18.0"
46
+ },
47
+ "devDependencies": {
48
+ "@deepseek-ai/cordis": "^4.0.1",
49
+ "@deepseek-ai/dsh-client-connection": "^0.1.0-rc.6",
50
+ "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.6",
51
+ "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.6",
52
+ "@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.6",
53
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.6",
54
+ "@deepseek-ai/dsh-settings": "^0.1.0-rc.6",
55
+ "@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.6",
56
+ "@types/react": "~18.3.1",
57
+ "@types/react-dom": "^18.3.5",
58
+ "jsdom": "^25.0.0",
59
+ "lightningcss": "1.32.0",
60
+ "react": "^18.3.1",
61
+ "react-dom": "^18.3.1",
62
+ "schemastery": "^3.18.0",
63
+ "tsdown": "0.22.2",
64
+ "typescript": "~5.7.2",
65
+ "vitest": "^3.0.0"
66
+ },
67
+ "files": [
68
+ "lib/index.js",
69
+ "lib/invariant.js",
70
+ "lib/client.js",
71
+ "lib/types/**/*.d.ts",
72
+ "lib/types/**/*.d.ts.map",
73
+ "src",
74
+ "cordis.patch.yml"
75
+ ],
76
+ "license": "BSD-3-Clause",
77
+ "scripts": {
78
+ "build": "tsc -p tsconfig.build.json && tsdown",
79
+ "bundle": "tsdown",
80
+ "watch": "tsdown --watch",
81
+ "typecheck": "tsc --noEmit",
82
+ "test": "vitest run",
83
+ "test:watch": "vitest"
84
+ }
85
+ }