@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.
- package/LICENSE +29 -0
- package/README.md +211 -0
- package/cordis.patch.yml +12 -0
- package/lib/client.js +2592 -0
- package/lib/index.js +54 -0
- package/lib/invariant.js +6 -0
- package/lib/types/client/PluginSettingsCard.d.ts +77 -0
- package/lib/types/client/TaskBoardSettingsCard.d.ts +51 -0
- package/lib/types/client/board/ConfirmDialog.d.ts +12 -0
- package/lib/types/client/board/NewTaskModal.d.ts +6 -0
- package/lib/types/client/board/TaskBoard.d.ts +5 -0
- package/lib/types/client/board/TaskCard.d.ts +12 -0
- package/lib/types/client/board/TaskDetail.d.ts +7 -0
- package/lib/types/client/board-mount.d.ts +10 -0
- package/lib/types/client/index.d.ts +43 -0
- package/lib/types/client/locales.d.ts +105 -0
- package/lib/types/client/settings-form.d.ts +118 -0
- package/lib/types/client/sidebar-entry.d.ts +25 -0
- package/lib/types/core/controller.d.ts +129 -0
- package/lib/types/core/execution.d.ts +136 -0
- package/lib/types/core/schedule.d.ts +40 -0
- package/lib/types/core/scheduler.d.ts +49 -0
- package/lib/types/core/store.d.ts +36 -0
- package/lib/types/core/tasks.d.ts +111 -0
- package/lib/types/index.d.ts +44 -0
- package/lib/types/invariant.d.ts +3 -0
- package/package.json +85 -0
- package/src/client/PluginSettingsCard.tsx +207 -0
- package/src/client/TaskBoardSettingsCard.tsx +124 -0
- package/src/client/board/ConfirmDialog.tsx +40 -0
- package/src/client/board/NewTaskModal.tsx +82 -0
- package/src/client/board/TaskBoard.tsx +107 -0
- package/src/client/board/TaskCard.tsx +62 -0
- package/src/client/board/TaskDetail.tsx +286 -0
- package/src/client/board-mount.tsx +74 -0
- package/src/client/board.module.css +849 -0
- package/src/client/css-modules.d.ts +5 -0
- package/src/client/index.ts +167 -0
- package/src/client/locales.ts +211 -0
- package/src/client/settings-card.module.css +261 -0
- package/src/client/settings-form.ts +294 -0
- package/src/client/sidebar-entry.ts +118 -0
- package/src/core/controller.ts +379 -0
- package/src/core/execution.ts +259 -0
- package/src/core/schedule.ts +140 -0
- package/src/core/scheduler.ts +104 -0
- package/src/core/store.ts +183 -0
- package/src/core/tasks.ts +204 -0
- package/src/index.ts +94 -0
- 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;
|
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
|
+
}
|