@linxin666/dsh-client-ui-task-board 0.3.6 → 0.3.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.i18n.yaml +2 -2
- package/README.md +9 -4
- package/README.zh.md +9 -4
- package/lib/client.js +742 -75
- package/lib/client.js.map +1 -1
- package/lib/index.js +698 -190
- package/lib/types/client/TaskBoardSettingsCard.d.ts +2 -1
- package/lib/types/client/board/TaskBoard.d.ts +3 -0
- package/lib/types/client/board-mount.d.ts +4 -1
- package/lib/types/client/host-api.d.ts +2 -2
- package/lib/types/client/index.d.ts +10 -2
- package/lib/types/client/locales.d.ts +21 -0
- package/lib/types/client/settings-form.d.ts +35 -43
- package/lib/types/client/sidebar-entry-core.d.ts +10 -0
- package/lib/types/client/sidebar-entry.d.ts +7 -1
- package/lib/types/core/controller.d.ts +11 -2
- package/lib/types/core/freeze-snapshot.d.ts +71 -0
- package/lib/types/core/handover.d.ts +58 -0
- package/lib/types/core/tasks.d.ts +70 -1
- package/lib/types/core/use-cases/task-update.d.ts +16 -7
- package/lib/types/host-ledger.d.ts +17 -2
- package/lib/types/host-runner.d.ts +43 -23
- package/lib/types/host-service.d.ts +8 -5
- package/lib/types/index.d.ts +9 -1
- package/lib/types/protocol.d.ts +15 -2
- package/package.json +26 -17
- package/src/client/TaskBoardSettingsCard.tsx +2 -1
- package/src/client/board/NewTaskModal.tsx +52 -0
- package/src/client/board/TaskBoard.tsx +5 -3
- package/src/client/board/TaskCard.tsx +3 -0
- package/src/client/board/TaskDetail.tsx +54 -0
- package/src/client/board-mount.tsx +11 -1
- package/src/client/host-api.ts +5 -5
- package/src/client/index.ts +76 -10
- package/src/client/locales.ts +55 -2
- package/src/client/settings-form.ts +78 -92
- package/src/client/sidebar-entry-core.ts +33 -7
- package/src/client/sidebar-entry.ts +7 -1
- package/src/core/controller.ts +35 -10
- package/src/core/freeze-snapshot.ts +213 -0
- package/src/core/handover.ts +117 -0
- package/src/core/store.ts +48 -1
- package/src/core/tasks.ts +87 -0
- package/src/core/use-cases/task-update.ts +30 -10
- package/src/dsh-home.ts +1 -1
- package/src/host-ledger.ts +137 -59
- package/src/host-routes.ts +1 -1
- package/src/host-runner.ts +211 -97
- package/src/host-service.ts +14 -10
- package/src/index.ts +24 -10
- package/src/protocol.ts +98 -14
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type ExecutionRecord, type TaskRecord } from './core/tasks.ts';
|
|
2
2
|
import { type TaskBoardAction, type TaskBoardSchedulerSnapshot } from './protocol.ts';
|
|
3
|
+
import { type TaskPermission } from './core/handover.ts';
|
|
3
4
|
export interface LedgerState {
|
|
4
5
|
revision: number;
|
|
5
6
|
tasks: TaskRecord[];
|
|
@@ -47,7 +48,11 @@ export declare class HostTaskLedger {
|
|
|
47
48
|
readonly lockFile: string;
|
|
48
49
|
/** Small sidecar for the 30 s scheduler heartbeat (lastTickAt only). */
|
|
49
50
|
readonly schedulerFile: string;
|
|
50
|
-
|
|
51
|
+
/** Session-default permission the confirmation gate compares against. */
|
|
52
|
+
readonly sessionDefaultPermission: TaskPermission;
|
|
53
|
+
constructor(dir?: string, now?: () => number, options?: {
|
|
54
|
+
sessionDefaultPermission?: TaskPermission;
|
|
55
|
+
});
|
|
51
56
|
/** Revision + scheduler without any task cloning; feeds the SSE event frame. */
|
|
52
57
|
summary(): {
|
|
53
58
|
revision: number;
|
|
@@ -66,7 +71,7 @@ export declare class HostTaskLedger {
|
|
|
66
71
|
dueSchedules(now: number): DueScheduleReference[];
|
|
67
72
|
subscribe(listener: () => void): () => void;
|
|
68
73
|
dispose(): void;
|
|
69
|
-
applyRequest(requestId: string, action: TaskBoardAction): {
|
|
74
|
+
applyRequest(requestId: string, action: TaskBoardAction, initiator?: string): {
|
|
70
75
|
state: LedgerState;
|
|
71
76
|
run?: OpenedRun;
|
|
72
77
|
};
|
|
@@ -78,7 +83,17 @@ export declare class HostTaskLedger {
|
|
|
78
83
|
private apply;
|
|
79
84
|
private repairSchedules;
|
|
80
85
|
private reconcileInterruptedStarts;
|
|
86
|
+
/**
|
|
87
|
+
* Field-preserving v2 to v3 migration. v3 adds no fields yet, so the
|
|
88
|
+
* migration reuses the v3 normalization, but it first proves every task
|
|
89
|
+
* row is structurally valid: a v2 document that would silently drop or
|
|
90
|
+
* coerce rows fails loudly instead (no quarantined-empty restart).
|
|
91
|
+
*/
|
|
92
|
+
private migrateLegacyDocument;
|
|
81
93
|
private load;
|
|
94
|
+
private normalizeDocument;
|
|
95
|
+
/** Quarantine an unreadable document and start from an empty ledger. */
|
|
96
|
+
private recoverCorrupt;
|
|
82
97
|
private syncRecentRequests;
|
|
83
98
|
private readSchedulerSidecar;
|
|
84
99
|
/** Atomic write of the scheduler heartbeat sidecar (0600, tmp + rename + fsync). */
|
|
@@ -1,13 +1,25 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { TypertGateway } from '@deepseek-ai/dsh-api-gateway';
|
|
2
|
+
import type { SessionSummary } from '@deepseek-ai/dsh-api-session-controller/types';
|
|
2
3
|
import type { CommandResult } from '@deepseek-ai/dsh-commands/types';
|
|
4
|
+
import type { Workspace } from '@deepseek-ai/dsh-workspace/types';
|
|
3
5
|
import type { TaskRecord } from './core/tasks.ts';
|
|
4
|
-
/**
|
|
5
|
-
export
|
|
6
|
-
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
/** Host services needed to validate a task's workspace before creating a session. */
|
|
7
|
+
export interface TaskBoardWorkspaceRegistry {
|
|
8
|
+
list(): readonly Workspace[];
|
|
9
|
+
}
|
|
10
|
+
interface GatewayRequest {
|
|
11
|
+
namespace: string;
|
|
12
|
+
method: string;
|
|
13
|
+
args: Record<string, unknown>;
|
|
14
|
+
signal?: AbortSignal;
|
|
15
|
+
}
|
|
16
|
+
interface SessionGateway {
|
|
17
|
+
invoke(request: GatewayRequest): Promise<unknown>;
|
|
18
|
+
stream?(request: GatewayRequest): Promise<AsyncIterable<unknown>>;
|
|
19
|
+
}
|
|
20
|
+
/** One session-list row consumed by task-board reconciliation. */
|
|
21
|
+
export type { SessionSummary };
|
|
22
|
+
type ExecutionSessionId = SessionSummary['sessionId'];
|
|
11
23
|
export interface SessionCommandDispatcher {
|
|
12
24
|
execute(sessionId: ExecutionSessionId, line: string, signal: AbortSignal): Promise<CommandResult | undefined>;
|
|
13
25
|
}
|
|
@@ -27,17 +39,31 @@ export declare class SessionLaunchError extends Error {
|
|
|
27
39
|
readonly sessionId: string;
|
|
28
40
|
constructor(sessionId: string, cause: unknown);
|
|
29
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* Compose the execution prompt (issue #6): a continuation card (one carrying
|
|
44
|
+
* a frozen snapshot) has its instruction mandatorily wrapped in a source
|
|
45
|
+
* declaration (freeze instant, source session, unreviewed-content warning)
|
|
46
|
+
* templated by the board, so the picking-up agent stays wary of stored
|
|
47
|
+
* prompt-instruction injection in card text (adversarial scenario c). The
|
|
48
|
+
* wrap composes with the T4 handover preamble: the reference preamble comes
|
|
49
|
+
* first, the provenance wrap then encloses the instruction. Plain tasks (no
|
|
50
|
+
* freeze) keep the bare handover preamble + prompt.
|
|
51
|
+
*/
|
|
52
|
+
export declare function promptText(task: TaskRecord): string;
|
|
30
53
|
export declare class HostExecutionRunner {
|
|
31
|
-
private readonly
|
|
54
|
+
private readonly gateway;
|
|
32
55
|
private readonly commands?;
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
* no matching turn/end. While a session's newest seq is unchanged a
|
|
36
|
-
* re-scan cannot change the outcome, so only a one-message probe runs per
|
|
37
|
-
* poll tick instead of up to 100 history pages.
|
|
38
|
-
*/
|
|
56
|
+
private readonly workspaceRegistry?;
|
|
57
|
+
/** Newest scanned event sequence per session with no matching execution end. */
|
|
39
58
|
private readonly scanMemos;
|
|
40
|
-
|
|
59
|
+
private readonly unavailableAttempts;
|
|
60
|
+
private readonly unavailableBackoffMs;
|
|
61
|
+
constructor(gateway: SessionGateway | TypertGateway, commands?: SessionCommandDispatcher | undefined, workspaceRegistry?: TaskBoardWorkspaceRegistry | undefined, unavailableRetry?: {
|
|
62
|
+
attempts?: number;
|
|
63
|
+
backoffMs?: number;
|
|
64
|
+
});
|
|
65
|
+
private invoke;
|
|
66
|
+
private stream;
|
|
41
67
|
launch(task: TaskRecord): Promise<string>;
|
|
42
68
|
listRunning(): Promise<{
|
|
43
69
|
known: true;
|
|
@@ -46,12 +72,6 @@ export declare class HostExecutionRunner {
|
|
|
46
72
|
} | {
|
|
47
73
|
known: false;
|
|
48
74
|
}>;
|
|
49
|
-
/**
|
|
50
|
-
* Resolve one execution's outcome. The caller may pass the session list it
|
|
51
|
-
* already fetched this poll tick; otherwise inspect lists sessions itself.
|
|
52
|
-
* Sharing the list keeps a poll with E open executions at one list RPC
|
|
53
|
-
* instead of 1 + E.
|
|
54
|
-
*/
|
|
75
|
+
/** Resolve an execution outcome from the session list and bounded history pages. */
|
|
55
76
|
inspect(sessionId: string, startedAt?: number, sessions?: readonly SessionSummary[]): Promise<ExecutionInspection>;
|
|
56
77
|
}
|
|
57
|
-
export {};
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { TypertGateway } from '@deepseek-ai/dsh-api-gateway';
|
|
2
2
|
import { HostTaskLedger } from './host-ledger.ts';
|
|
3
|
-
import { HostExecutionRunner, type SessionCommandDispatcher } from './host-runner.ts';
|
|
3
|
+
import { HostExecutionRunner, type SessionCommandDispatcher, type TaskBoardWorkspaceRegistry } from './host-runner.ts';
|
|
4
4
|
import { PowerInhibitor } from './power-inhibitor.ts';
|
|
5
|
-
import type
|
|
5
|
+
import { type TaskBoardAction, type TaskBoardEventPayload, type TaskBoardSnapshot } from './protocol.ts';
|
|
6
|
+
import type { TaskPermission } from './core/handover.ts';
|
|
6
7
|
export declare class TaskBoardHostService {
|
|
7
8
|
readonly ledger: HostTaskLedger;
|
|
8
9
|
readonly runner: HostExecutionRunner;
|
|
@@ -17,11 +18,13 @@ export declare class TaskBoardHostService {
|
|
|
17
18
|
private preventIdleSleep;
|
|
18
19
|
private lastPowerJson;
|
|
19
20
|
private readonly now;
|
|
20
|
-
constructor(
|
|
21
|
+
constructor(gateway: TypertGateway, options?: {
|
|
21
22
|
ledger?: HostTaskLedger;
|
|
22
23
|
power?: PowerInhibitor;
|
|
23
24
|
now?: () => number;
|
|
24
25
|
commandDispatcher?: SessionCommandDispatcher;
|
|
26
|
+
workspaceRegistry?: TaskBoardWorkspaceRegistry;
|
|
27
|
+
sessionDefaultPermission?: TaskPermission;
|
|
25
28
|
});
|
|
26
29
|
start(): void;
|
|
27
30
|
setConfiguration(active: boolean, preventIdleSleep: boolean): void;
|
|
@@ -29,7 +32,7 @@ export declare class TaskBoardHostService {
|
|
|
29
32
|
/** SSE frame payload; deliberately skips the tasks deep-clone of {@link snapshot}. */
|
|
30
33
|
eventPayload(): TaskBoardEventPayload;
|
|
31
34
|
subscribe(listener: () => void): () => void;
|
|
32
|
-
apply(requestId: string, action: TaskBoardAction): TaskBoardSnapshot;
|
|
35
|
+
apply(requestId: string, action: TaskBoardAction, initiator?: string): TaskBoardSnapshot;
|
|
33
36
|
dispose(): void;
|
|
34
37
|
private launch;
|
|
35
38
|
private pollSessions;
|
package/lib/types/index.d.ts
CHANGED
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
* a same-origin asynchronous view over that service.
|
|
7
7
|
*/
|
|
8
8
|
import type { Context } from '@deepseek-ai/cordis';
|
|
9
|
+
import type { SettingsNamespace } from '@deepseek-ai/dsh-settings';
|
|
9
10
|
import z from 'schemastery';
|
|
11
|
+
import { type TaskPermission } from './core/tasks.ts';
|
|
10
12
|
/** Default environment variable holding the authenticated proxy token. */
|
|
11
13
|
export declare const DEFAULT_PROXY_TOKEN_ENV = "DSH_TASK_BOARD_PROXY_TOKEN";
|
|
12
14
|
export declare const inject: string[];
|
|
@@ -17,7 +19,7 @@ export declare const TASK_BOARD_GUIDANCE = "\u672C\u673A\u5DF2\u5B89\u88C5 dsh-t
|
|
|
17
19
|
* web settings surface edits. Spelled here rather than imported: the browser
|
|
18
20
|
* half spells the same value and must not depend on a Host package.
|
|
19
21
|
*/
|
|
20
|
-
export declare const TASK_BOARD_SETTINGS_NAMESPACE:
|
|
22
|
+
export declare const TASK_BOARD_SETTINGS_NAMESPACE: SettingsNamespace;
|
|
21
23
|
/** Plugin config, validated by the same-named schemastery schema. */
|
|
22
24
|
export interface Config {
|
|
23
25
|
/**
|
|
@@ -34,6 +36,12 @@ export interface Config {
|
|
|
34
36
|
trustedProxyHosts?: string[];
|
|
35
37
|
/** Environment variable whose value the authenticated proxy injects upstream. */
|
|
36
38
|
proxyTokenEnv?: string;
|
|
39
|
+
/**
|
|
40
|
+
* The deployment's session-default permission. A card whose effective
|
|
41
|
+
* permission (handover bundle or pin) is above this value requires a human
|
|
42
|
+
* confirmation before it may run; cron refuses unconfirmed cards.
|
|
43
|
+
*/
|
|
44
|
+
sessionDefaultPermission?: TaskPermission;
|
|
37
45
|
}
|
|
38
46
|
export declare const Config: z<Config>;
|
|
39
47
|
/** Resolve proxy access without ever placing the token value in plugin config. */
|
package/lib/types/protocol.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { TaskUpdatePatch } from './core/use-cases/task-update.ts';
|
|
2
|
-
import { type NewTaskInput, type TaskRecord, type TaskStatus } from './core/tasks.ts';
|
|
3
|
-
export declare const TASK_BOARD_SCHEMA_VERSION:
|
|
2
|
+
import { type NewTaskInput, type TaskPermission, type TaskRecord, type TaskStatus } from './core/tasks.ts';
|
|
3
|
+
export declare const TASK_BOARD_SCHEMA_VERSION: 3;
|
|
4
|
+
/** Ledger documents written before v3; loaded once and migrated on startup. */
|
|
5
|
+
export declare const TASK_BOARD_LEGACY_SCHEMA_VERSION: 2;
|
|
4
6
|
export declare const TASK_BOARD_API_PREFIX = "/api/task-board";
|
|
5
7
|
export type PowerPhase = 'disabled' | 'idle' | 'acquiring' | 'active' | 'error' | 'unsupported';
|
|
6
8
|
export interface TaskBoardPowerSnapshot {
|
|
@@ -25,6 +27,8 @@ export interface TaskBoardSnapshot {
|
|
|
25
27
|
tasks: TaskRecord[];
|
|
26
28
|
scheduler: TaskBoardSchedulerSnapshot;
|
|
27
29
|
power: TaskBoardPowerSnapshot;
|
|
30
|
+
/** Session-default permission the confirmation gate compares against. */
|
|
31
|
+
sessionDefaultPermission?: TaskPermission;
|
|
28
32
|
}
|
|
29
33
|
/** SSE event frame: revision/scheduler/power only, never the task list. */
|
|
30
34
|
export interface TaskBoardEventPayload {
|
|
@@ -70,9 +74,18 @@ export type TaskBoardAction = {
|
|
|
70
74
|
} | {
|
|
71
75
|
kind: 'rerun';
|
|
72
76
|
taskId: string;
|
|
77
|
+
} | {
|
|
78
|
+
kind: 'confirm-permission';
|
|
79
|
+
taskId: string;
|
|
73
80
|
};
|
|
74
81
|
export interface TaskBoardActionEnvelope {
|
|
75
82
|
requestId: string;
|
|
76
83
|
action: TaskBoardAction;
|
|
84
|
+
/**
|
|
85
|
+
* Session id of the DSH session issuing the action, for the execution audit
|
|
86
|
+
* trail (issue #6). Client-asserted, not a trust boundary; parsed only as a
|
|
87
|
+
* bounded non-empty string and recorded on opened executions / freeze stamps.
|
|
88
|
+
*/
|
|
89
|
+
initiator?: string;
|
|
77
90
|
}
|
|
78
91
|
export declare function parseActionEnvelope(value: unknown): TaskBoardActionEnvelope | undefined;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linxin666/dsh-client-ui-task-board",
|
|
3
3
|
"description": "Host-authoritative task board for the DSH Web GUI with real session execution, Host cron scheduling, and optional cross-platform idle-sleep protection; mounted without DSH source changes.",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.10",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "lib/types/index.d.ts",
|
|
@@ -23,16 +23,19 @@
|
|
|
23
23
|
},
|
|
24
24
|
"dsh": {
|
|
25
25
|
"engines": {
|
|
26
|
-
"dsh": ">=0.1.
|
|
26
|
+
"dsh": ">=0.1.2-alpha.1"
|
|
27
27
|
},
|
|
28
28
|
"bundle": {
|
|
29
29
|
"patch": "./cordis.patch.yml"
|
|
30
30
|
},
|
|
31
31
|
"client": {
|
|
32
32
|
"inject": [
|
|
33
|
-
"@deepseek-ai/dsh-client-runtime",
|
|
34
33
|
"@deepseek-ai/dsh-client-connection",
|
|
35
|
-
"@deepseek-ai/dsh-client-ui-settings"
|
|
34
|
+
"@deepseek-ai/dsh-client-ui-settings",
|
|
35
|
+
"@deepseek-ai/dsh-client-ui-renderer",
|
|
36
|
+
"@deepseek-ai/dsh-api-session-controller",
|
|
37
|
+
"@deepseek-ai/dsh-api-workspace-controller",
|
|
38
|
+
"@deepseek-ai/dsh-api-remotes"
|
|
36
39
|
],
|
|
37
40
|
"platform": "web"
|
|
38
41
|
}
|
|
@@ -44,21 +47,27 @@
|
|
|
44
47
|
"schemastery": "^3.18.0"
|
|
45
48
|
},
|
|
46
49
|
"devDependencies": {
|
|
47
|
-
"@deepseek-ai/cordis": "^4.0.
|
|
48
|
-
"@deepseek-ai/dsh-agent": "^0.1.
|
|
49
|
-
"@deepseek-ai/dsh-
|
|
50
|
-
"@deepseek-ai/dsh-
|
|
51
|
-
"@deepseek-ai/dsh-
|
|
52
|
-
"@deepseek-ai/dsh-
|
|
53
|
-
"@deepseek-ai/dsh-
|
|
54
|
-
"@deepseek-ai/dsh-
|
|
55
|
-
"@deepseek-ai/dsh-
|
|
56
|
-
"@deepseek-ai/dsh-
|
|
57
|
-
"@deepseek-ai/dsh-
|
|
58
|
-
"@deepseek-ai/dsh-
|
|
50
|
+
"@deepseek-ai/cordis": "^4.0.2",
|
|
51
|
+
"@deepseek-ai/dsh-agent": "^0.1.2-alpha.2",
|
|
52
|
+
"@deepseek-ai/dsh-agent-presets": "^0.1.2-alpha.2",
|
|
53
|
+
"@deepseek-ai/dsh-api-gateway": "^0.1.2-alpha.2",
|
|
54
|
+
"@deepseek-ai/dsh-api-remotes": "^0.1.2-alpha.2",
|
|
55
|
+
"@deepseek-ai/dsh-workspace": "^0.1.2-alpha.2",
|
|
56
|
+
"@deepseek-ai/dsh-api-session-controller": "^0.1.2-alpha.2",
|
|
57
|
+
"@deepseek-ai/dsh-api-workspace-controller": "^0.1.2-alpha.2",
|
|
58
|
+
"@deepseek-ai/dsh-client-connection": "^0.1.2-alpha.2",
|
|
59
|
+
"@deepseek-ai/dsh-client-locale": "^0.1.2-alpha.2",
|
|
60
|
+
"@deepseek-ai/dsh-client-store": "^0.1.2-alpha.2",
|
|
61
|
+
"@deepseek-ai/dsh-client-ui-renderer": "^0.1.2-alpha.2",
|
|
62
|
+
"@deepseek-ai/dsh-client-ui-settings": "^0.1.2-alpha.2",
|
|
63
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.1.2-alpha.2",
|
|
64
|
+
"@deepseek-ai/dsh-commands": "^0.1.2-alpha.2",
|
|
65
|
+
"@deepseek-ai/dsh-host-webserver": "^0.1.2-alpha.2",
|
|
66
|
+
"@deepseek-ai/dsh-settings": "^0.1.2-alpha.2",
|
|
67
|
+
"@deepseek-ai/dsh-system-prompt": "^0.1.2-alpha.2",
|
|
68
|
+
"@types/node": "^22.20.0",
|
|
59
69
|
"@types/react": "~18.3.1",
|
|
60
70
|
"@types/react-dom": "^18.3.5",
|
|
61
|
-
"@types/node": "^22.20.0",
|
|
62
71
|
"jsdom": "^25.0.0",
|
|
63
72
|
"react": "^18.3.1",
|
|
64
73
|
"react-dom": "^18.3.1",
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
|
|
8
|
-
import type {
|
|
8
|
+
import type { SnapshotStore } from '@deepseek-ai/dsh-client-store'
|
|
9
|
+
import type { SettingsScope } from '@deepseek-ai/dsh-client-ui-settings/client'
|
|
9
10
|
import { useEffect, useState } from 'react'
|
|
10
11
|
import type { TaskBoardPowerSnapshot } from '../protocol.ts'
|
|
11
12
|
import { PluginSettingsCard, BooleanField } from './PluginSettingsCard.tsx'
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { useEffect, useState } from 'react'
|
|
6
6
|
import type { BoardController } from '../../core/controller.ts'
|
|
7
7
|
import { isValidCron, nextRunAtMs } from '../../core/schedule.ts'
|
|
8
|
+
import { parseFreezeRequest } from '../../core/freeze-snapshot.ts'
|
|
8
9
|
import { TASK_PERMISSIONS, type TaskPermission } from '../../core/tasks.ts'
|
|
9
10
|
import { t, type TaskBoardKey } from '../locales.ts'
|
|
10
11
|
import { SCHEDULE_PRESETS } from '../schedule-presets.ts'
|
|
@@ -22,6 +23,9 @@ export function NewTaskModal({ controller, onClose }: { controller: BoardControl
|
|
|
22
23
|
const [scheduleEnabled, setScheduleEnabled] = useState(false)
|
|
23
24
|
const [scheduleCron, setScheduleCron] = useState('')
|
|
24
25
|
const [scheduleError, setScheduleError] = useState<string | undefined>(undefined)
|
|
26
|
+
const [freezeText, setFreezeText] = useState('')
|
|
27
|
+
const [freezeError, setFreezeError] = useState<string | undefined>(undefined)
|
|
28
|
+
const [handoverText, setHandoverText] = useState('')
|
|
25
29
|
const [error, setError] = useState<string | undefined>(undefined)
|
|
26
30
|
const [pending, setPending] = useState(false)
|
|
27
31
|
const [options, setOptions] = useState(controller.getSnapshot().executionOptions)
|
|
@@ -41,11 +45,34 @@ export function NewTaskModal({ controller, onClose }: { controller: BoardControl
|
|
|
41
45
|
return
|
|
42
46
|
}
|
|
43
47
|
}
|
|
48
|
+
// Optional continuation-card snapshot: parse the freeze block through the
|
|
49
|
+
// T2 gate (structure + redaction + taint + size); a malformed block stops
|
|
50
|
+
// submission with the parser's error instead of creating a plain task.
|
|
51
|
+
let freeze: Parameters<typeof controller.createTaskConfirmed>[0]['freeze'] = undefined
|
|
52
|
+
if (freezeText.trim() !== '') {
|
|
53
|
+
const parsed = parseFreezeRequest(freezeText)
|
|
54
|
+
if (!parsed.ok) {
|
|
55
|
+
setFreezeError(parsed.error.message)
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
freeze = { ...parsed.snapshot, ...(parsed.warnings.includes('redacted') ? { redacted: true } : {}) }
|
|
59
|
+
}
|
|
60
|
+
// Optional handover bundle: non-empty reference lines attach the picked
|
|
61
|
+
// triplet (workspace/mode/permission above) plus the references.
|
|
62
|
+
const references = handoverText.split('\n').map(line => line.trim()).filter(line => line !== '')
|
|
63
|
+
const handover = references.length === 0 ? undefined : {
|
|
64
|
+
references,
|
|
65
|
+
workspaceId: workspaceId === '' ? undefined : workspaceId,
|
|
66
|
+
mode: mode === '' ? undefined : mode,
|
|
67
|
+
permission: permission === '' ? undefined : permission as TaskPermission,
|
|
68
|
+
}
|
|
44
69
|
setPending(true)
|
|
45
70
|
const task = await controller.createTaskConfirmed({
|
|
46
71
|
title,
|
|
47
72
|
description,
|
|
48
73
|
prompt,
|
|
74
|
+
freeze,
|
|
75
|
+
handover,
|
|
49
76
|
workspaceId: workspaceId === '' ? undefined : workspaceId,
|
|
50
77
|
mode: mode === '' ? undefined : mode,
|
|
51
78
|
permission: permission === '' ? undefined : permission as TaskPermission,
|
|
@@ -83,6 +110,31 @@ export function NewTaskModal({ controller, onClose }: { controller: BoardControl
|
|
|
83
110
|
onPromptChange={setPrompt}
|
|
84
111
|
/>
|
|
85
112
|
|
|
113
|
+
<label className={css.field}>
|
|
114
|
+
<span className={css.fieldLabel}>{t('new.freeze')}</span>
|
|
115
|
+
<textarea
|
|
116
|
+
className={css.input}
|
|
117
|
+
rows={4}
|
|
118
|
+
value={freezeText}
|
|
119
|
+
placeholder={t('new.freezePlaceholder')}
|
|
120
|
+
spellCheck={false}
|
|
121
|
+
onChange={event => { setFreezeText(event.target.value); setFreezeError(undefined) }}
|
|
122
|
+
/>
|
|
123
|
+
</label>
|
|
124
|
+
{freezeError !== undefined && <p className={css.formError}>{freezeError}</p>}
|
|
125
|
+
|
|
126
|
+
<label className={css.field}>
|
|
127
|
+
<span className={css.fieldLabel}>{t('new.handover')}</span>
|
|
128
|
+
<textarea
|
|
129
|
+
className={css.input}
|
|
130
|
+
rows={3}
|
|
131
|
+
value={handoverText}
|
|
132
|
+
placeholder={t('new.handoverPlaceholder')}
|
|
133
|
+
spellCheck={false}
|
|
134
|
+
onChange={event => { setHandoverText(event.target.value) }}
|
|
135
|
+
/>
|
|
136
|
+
</label>
|
|
137
|
+
|
|
86
138
|
<label className={css.field}>
|
|
87
139
|
<span className={css.fieldLabel}>{t('new.workspace')}</span>
|
|
88
140
|
<select
|
|
@@ -13,11 +13,13 @@ import { STATUS_KEY } from './status-key.ts'
|
|
|
13
13
|
import { TaskCard } from './TaskCard.tsx'
|
|
14
14
|
import { TaskDetail } from './TaskDetail.tsx'
|
|
15
15
|
|
|
16
|
-
/** Case-insensitive title/description match. */
|
|
17
|
-
function matchesFilter(task: TaskRecord, filter: string): boolean {
|
|
16
|
+
/** Case-insensitive title/description/freeze-snapshot match. */
|
|
17
|
+
export function matchesFilter(task: TaskRecord, filter: string): boolean {
|
|
18
18
|
if (filter.trim() === '') return true
|
|
19
19
|
const needle = filter.trim().toLowerCase()
|
|
20
|
-
|
|
20
|
+
const haystacks = [task.title, task.description]
|
|
21
|
+
if (task.freeze !== undefined) haystacks.push(task.freeze.goal, task.freeze.progress, task.freeze.next)
|
|
22
|
+
return haystacks.some(text => text.toLowerCase().includes(needle))
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
/**
|
|
@@ -62,6 +62,9 @@ function TaskCardInner({ task, pending, timeZone, onClick }: { task: TaskRecord;
|
|
|
62
62
|
{task.description !== '' && <span className={css.cardExcerpt}>{task.description}</span>}
|
|
63
63
|
<span className={css.cardMeta}>
|
|
64
64
|
<span className={css.cardTime}>{t('board.updated')} {formatTime(task.updatedAt)}</span>
|
|
65
|
+
{task.freeze !== undefined && (
|
|
66
|
+
<span className={css.cardSchedule} title={task.freeze.goal}>{t('card.frozen')}</span>
|
|
67
|
+
)}
|
|
65
68
|
{!archived && task.schedule?.enabled === true && (
|
|
66
69
|
<span
|
|
67
70
|
className={css.cardSchedule}
|
|
@@ -9,6 +9,7 @@ import type { BoardController } from '../../core/controller.ts'
|
|
|
9
9
|
import { isValidCron } from '../../core/schedule.ts'
|
|
10
10
|
import { MANUAL_STATUSES, TASK_PERMISSIONS, type ExecutionRecord, type TaskPermission, type TaskRecord } from '../../core/tasks.ts'
|
|
11
11
|
import { canEditTaskContent } from '../../core/use-cases/task-update.ts'
|
|
12
|
+
import { requiresPermissionConfirmation } from '../../core/handover.ts'
|
|
12
13
|
import { t, type TaskBoardKey } from '../locales.ts'
|
|
13
14
|
import { SCHEDULE_PRESETS } from '../schedule-presets.ts'
|
|
14
15
|
import css from '../board.module.css'
|
|
@@ -36,6 +37,11 @@ function ExecutionRow({ execution, timeZone, onOpen }: { execution: ExecutionRec
|
|
|
36
37
|
{t('detail.executionStarted')} {formatTime(execution.startedAt, timeZone)}
|
|
37
38
|
{execution.endedAt !== undefined && ` · ${t('detail.executionEnded')} ${formatTime(execution.endedAt, timeZone)}`}
|
|
38
39
|
</span>
|
|
40
|
+
{execution.initiatedBy !== undefined && (
|
|
41
|
+
<span className={css.executionTimes} title={execution.initiatedBy}>
|
|
42
|
+
{t('detail.execution.initiator', { session: execution.initiatedBy })}
|
|
43
|
+
</span>
|
|
44
|
+
)}
|
|
39
45
|
{execution.sessionId !== undefined && (
|
|
40
46
|
<button
|
|
41
47
|
type="button"
|
|
@@ -248,6 +254,7 @@ export function TaskDetail({ controller, task }: { controller: BoardController;
|
|
|
248
254
|
const pending = snapshot.pendingTaskIds.includes(current.id)
|
|
249
255
|
const transportError = snapshot.transportError
|
|
250
256
|
const timeZone = snapshot.host?.scheduler.timeZone
|
|
257
|
+
const permissionPending = requiresPermissionConfirmation(current, snapshot.host?.sessionDefaultPermission)
|
|
251
258
|
|
|
252
259
|
return (
|
|
253
260
|
<div className={css.modalBackdrop} onMouseDown={event => { if (event.target === event.currentTarget) controller.closeTask() }}>
|
|
@@ -281,6 +288,53 @@ export function TaskDetail({ controller, task }: { controller: BoardController;
|
|
|
281
288
|
<p className={css.detailText}>{current.description !== '' ? current.description : '—'}</p>
|
|
282
289
|
</section>
|
|
283
290
|
|
|
291
|
+
{current.freeze !== undefined && (
|
|
292
|
+
<section className={css.detailSection} data-dsh-part="freeze">
|
|
293
|
+
<h4>{t('detail.freeze')}</h4>
|
|
294
|
+
{current.freeze.redacted === true && <p className={css.formError}>{t('detail.freeze.redacted')}</p>}
|
|
295
|
+
<p className={css.detailText}><strong>{t('detail.freeze.goal')}</strong></p>
|
|
296
|
+
<pre className={css.promptBlock}>{current.freeze.goal}</pre>
|
|
297
|
+
<p className={css.detailText}><strong>{t('detail.freeze.progress')}</strong></p>
|
|
298
|
+
<pre className={css.promptBlock}>{current.freeze.progress}</pre>
|
|
299
|
+
<p className={css.detailText}><strong>{t('detail.freeze.next')}</strong></p>
|
|
300
|
+
<pre className={css.promptBlock}>{current.freeze.next}</pre>
|
|
301
|
+
<p className={css.detailMeta}>{t('detail.freeze.frozenAt', { time: formatHostTimestamp(current.freeze.frozenAt, timeZone) })}</p>
|
|
302
|
+
{current.freeze.frozenBy !== undefined && (
|
|
303
|
+
<p className={css.detailMeta}>{t('detail.freeze.frozenBy', { session: current.freeze.frozenBy })}</p>
|
|
304
|
+
)}
|
|
305
|
+
</section>
|
|
306
|
+
)}
|
|
307
|
+
|
|
308
|
+
{current.handover !== undefined && (
|
|
309
|
+
<section className={css.detailSection} data-dsh-part="handover">
|
|
310
|
+
<h4>{t('detail.handover')}</h4>
|
|
311
|
+
<p className={css.detailText}>
|
|
312
|
+
{t('new.workspace')}: {current.handover.workspaceId ?? t('exec.workspace.recent')}
|
|
313
|
+
{' · '}{t('new.mode')}: {current.handover.mode ?? t('exec.mode.default')}
|
|
314
|
+
{' · '}{t('new.permission')}: {current.handover.permission === undefined ? t('exec.permission.default') : t(`exec.permission.${current.handover.permission}` as TaskBoardKey)}
|
|
315
|
+
</p>
|
|
316
|
+
<p className={css.detailText}><strong>{t('detail.handover.references')}</strong></p>
|
|
317
|
+
<ul className={css.executionList}>
|
|
318
|
+
{current.handover.references.map(reference => (
|
|
319
|
+
<li key={reference} className={css.executionRow}><code>{reference}</code></li>
|
|
320
|
+
))}
|
|
321
|
+
</ul>
|
|
322
|
+
<p className={css.detailMeta}>{t('detail.handover.bundledAt', { time: formatHostTimestamp(current.handover.bundledAt, timeZone) })}</p>
|
|
323
|
+
</section>
|
|
324
|
+
)}
|
|
325
|
+
|
|
326
|
+
{permissionPending && (
|
|
327
|
+
<section className={css.detailSection} data-dsh-part="permission-gate">
|
|
328
|
+
<p className={css.formError}>{t('detail.permissionPending', { permission: t(`exec.permission.${current.handover?.permission ?? current.permission}` as TaskBoardKey) })}</p>
|
|
329
|
+
<button type="button" className={css.primaryButton} disabled={pending} onClick={() => { void controller.confirmPermission(current.id) }}>
|
|
330
|
+
{t('detail.permissionConfirm')}
|
|
331
|
+
</button>
|
|
332
|
+
</section>
|
|
333
|
+
)}
|
|
334
|
+
{current.permissionConfirmedAt !== undefined && (
|
|
335
|
+
<p className={css.detailMeta}>{t('detail.permissionConfirmed', { time: formatHostTimestamp(current.permissionConfirmedAt, timeZone) })}</p>
|
|
336
|
+
)}
|
|
337
|
+
|
|
284
338
|
<section className={css.detailSection}>
|
|
285
339
|
<h4>{t('detail.prompt')}</h4>
|
|
286
340
|
<pre className={css.promptBlock}>{current.prompt !== '' ? current.prompt : current.title}</pre>
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import { createRoot, type Root } from 'react-dom/client'
|
|
16
16
|
import type { BoardController } from '../core/controller.ts'
|
|
17
17
|
import { TaskBoard } from './board/TaskBoard.tsx'
|
|
18
|
+
import type { LocaleRefreshSource } from './sidebar-entry.ts'
|
|
18
19
|
import css from './board.module.css'
|
|
19
20
|
|
|
20
21
|
/** The injected board container (kept in the DOM, hidden when inactive). */
|
|
@@ -37,11 +38,19 @@ function conversationColumn(): HTMLElement | undefined {
|
|
|
37
38
|
* Mount the board React tree into the center column and bind its visibility
|
|
38
39
|
* to the controller's boardOpen state.
|
|
39
40
|
* @param controller - the board controller driving the view.
|
|
41
|
+
* @param locale - locale-change source; when given, re-renders a mounted board
|
|
42
|
+
* on a Language switch.
|
|
40
43
|
* @returns disposer unmounting the tree and restoring the column.
|
|
41
44
|
*/
|
|
42
|
-
export function mountBoard(controller: BoardController): () => void {
|
|
45
|
+
export function mountBoard(controller: BoardController, locale?: LocaleRefreshSource): () => void {
|
|
43
46
|
let root: Root | undefined
|
|
44
47
|
let container: HTMLDivElement | undefined
|
|
48
|
+
let unsubscribeLocale: (() => void) | undefined
|
|
49
|
+
try {
|
|
50
|
+
unsubscribeLocale = locale?.subscribe(() => {
|
|
51
|
+
if (root !== undefined) root.render(<TaskBoard controller={controller} />)
|
|
52
|
+
})
|
|
53
|
+
} catch { /* locale service absent: the board follows its next natural re-render */ }
|
|
45
54
|
|
|
46
55
|
const ensure = (): void => {
|
|
47
56
|
if (container !== undefined) {
|
|
@@ -106,6 +115,7 @@ export function mountBoard(controller: BoardController): () => void {
|
|
|
106
115
|
document.removeEventListener(ACTIVATE_EVENT, onOtherActivate)
|
|
107
116
|
waitObserver.disconnect()
|
|
108
117
|
unsubscribe()
|
|
118
|
+
unsubscribeLocale?.()
|
|
109
119
|
document.documentElement.removeAttribute(ACTIVE_ATTR)
|
|
110
120
|
root?.unmount()
|
|
111
121
|
root = undefined
|
package/src/client/host-api.ts
CHANGED
|
@@ -25,7 +25,7 @@ async function readJson<T>(response: Response): Promise<T> {
|
|
|
25
25
|
export interface TaskBoardHostTransport {
|
|
26
26
|
bootstrap(legacy: readonly TaskRecord[]): Promise<TaskBoardSnapshot>
|
|
27
27
|
state(): Promise<TaskBoardSnapshot>
|
|
28
|
-
action(action: TaskBoardAction): Promise<TaskBoardSnapshot>
|
|
28
|
+
action(action: TaskBoardAction, initiator?: string): Promise<TaskBoardSnapshot>
|
|
29
29
|
subscribe(listener: (event?: TaskBoardEventPayload) => void): () => void
|
|
30
30
|
}
|
|
31
31
|
|
|
@@ -57,12 +57,12 @@ export class HttpTaskBoardHostTransport implements TaskBoardHostTransport {
|
|
|
57
57
|
return await this.request(`${TASK_BOARD_API_PREFIX}/state`, { cache: 'no-store' })
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
async action(action: TaskBoardAction): Promise<TaskBoardSnapshot> {
|
|
61
|
-
return await this.post(uuid(), action)
|
|
60
|
+
async action(action: TaskBoardAction, initiator?: string): Promise<TaskBoardSnapshot> {
|
|
61
|
+
return await this.post(uuid(), action, initiator)
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
private async post(requestId: string, action: TaskBoardAction): Promise<TaskBoardSnapshot> {
|
|
65
|
-
const envelope: TaskBoardActionEnvelope = { requestId, action }
|
|
64
|
+
private async post(requestId: string, action: TaskBoardAction, initiator?: string): Promise<TaskBoardSnapshot> {
|
|
65
|
+
const envelope: TaskBoardActionEnvelope = { requestId, action, ...(initiator === undefined || initiator === '' ? {} : { initiator }) }
|
|
66
66
|
return await this.request(`${TASK_BOARD_API_PREFIX}/action`, {
|
|
67
67
|
method: 'POST',
|
|
68
68
|
headers: { 'content-type': 'application/json' },
|