@linxin666/dsh-client-ui-task-board 0.3.6 → 0.3.9
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 +688 -66
- 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/host-api.d.ts +2 -2
- package/lib/types/client/index.d.ts +10 -2
- package/lib/types/client/locales.d.ts +19 -0
- package/lib/types/client/settings-form.d.ts +35 -43
- 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/host-api.ts +5 -5
- package/src/client/index.ts +67 -7
- package/src/client/locales.ts +40 -2
- package/src/client/settings-form.ts +78 -92
- 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,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.9",
|
|
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>
|
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' },
|
package/src/client/index.ts
CHANGED
|
@@ -8,8 +8,13 @@
|
|
|
8
8
|
* shell fails the whole boot when a plugin apply throws, and an external
|
|
9
9
|
* plugin must not take the GUI down.
|
|
10
10
|
*/
|
|
11
|
-
import type {
|
|
12
|
-
import type {
|
|
11
|
+
import type { Context as ClientContext } from '@deepseek-ai/cordis'
|
|
12
|
+
import type { ClientRemote, SessionId } from '@deepseek-ai/dsh-api-remotes/client'
|
|
13
|
+
import type { ISessions } from '@deepseek-ai/dsh-api-session-controller/client'
|
|
14
|
+
import type { IWorkspaces } from '@deepseek-ai/dsh-api-workspace-controller/client'
|
|
15
|
+
import type { SettingsScope, SettingsScopeSpec } from '@deepseek-ai/dsh-client-ui-settings/client'
|
|
16
|
+
import type {} from '@deepseek-ai/dsh-api-remotes/client'
|
|
17
|
+
import type {} from '@deepseek-ai/dsh-client-ui-renderer/client'
|
|
13
18
|
import type {} from '@deepseek-ai/dsh-client-ui-slots'
|
|
14
19
|
// Type-only: pulls the locale plugin's Context merge (ctx.locale) and its
|
|
15
20
|
// LocaleNamespaceMap merge table.
|
|
@@ -67,9 +72,64 @@ declare module '@deepseek-ai/cordis' {
|
|
|
67
72
|
}
|
|
68
73
|
|
|
69
74
|
|
|
70
|
-
/**
|
|
75
|
+
/**
|
|
76
|
+
* Required services (fiber inject waiting — the runtime must be up first).
|
|
77
|
+
* The generated remote faces are probed at use time instead of injected:
|
|
78
|
+
* `remote.agentPresets` only registers on 0.1.2-alpha.2 hosts (the
|
|
79
|
+
* api-remotes contribution), so a hard wait would pend the entry forever
|
|
80
|
+
* on hosts below that cohort, which serve the same roster through the
|
|
81
|
+
* connection RPC face.
|
|
82
|
+
*/
|
|
71
83
|
export const inject = ['slots', 'sessions', 'workspaces', 'connection', 'settingsScope', 'locale', 'remote']
|
|
72
84
|
|
|
85
|
+
/** One agent-preset row the mode picker consumes (either face's wire shape). */
|
|
86
|
+
interface PresetRosterRow {
|
|
87
|
+
id: string
|
|
88
|
+
name?: string
|
|
89
|
+
description?: string
|
|
90
|
+
/** Why this preset cannot compose a session; absent when it can. */
|
|
91
|
+
broken?: string
|
|
92
|
+
isDefault: boolean
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Read the agent-preset roster through whichever face the running host
|
|
97
|
+
* serves: the generated api-remotes face (`remote.agentPresets`,
|
|
98
|
+
* 0.1.2-alpha.2) or the connection RPC face
|
|
99
|
+
* (`connection.api.agentPresets`, hosts below that cohort). Answers
|
|
100
|
+
* undefined when the host serves neither, so the caller leaves the picker
|
|
101
|
+
* options untouched instead of erroring.
|
|
102
|
+
*/
|
|
103
|
+
async function readPresetRoster(
|
|
104
|
+
ctx: ClientContext,
|
|
105
|
+
remote: ClientRemote,
|
|
106
|
+
): Promise<{ ok: boolean; presets: readonly PresetRosterRow[] } | undefined> {
|
|
107
|
+
// The cordis `remote` proxy throws on a property that was never injected
|
|
108
|
+
// ("cannot get property X without inject") rather than returning undefined,
|
|
109
|
+
// so the probe must guard the access — a hard read would abort mounting on
|
|
110
|
+
// hosts below the 0.1.2-alpha.2 cohort instead of degrading to the legacy
|
|
111
|
+
// connection RPC face below.
|
|
112
|
+
let remotes: ClientRemote['agentPresets'] | undefined
|
|
113
|
+
try {
|
|
114
|
+
remotes = (remote as Partial<ClientRemote>).agentPresets
|
|
115
|
+
} catch {
|
|
116
|
+
remotes = undefined
|
|
117
|
+
}
|
|
118
|
+
if (remotes !== undefined) {
|
|
119
|
+
const response = await remotes.list()
|
|
120
|
+
if (!response.ok) return { ok: false, presets: [] }
|
|
121
|
+
return { ok: true, presets: response.value.presets }
|
|
122
|
+
}
|
|
123
|
+
const connection = ctx.get('connection') as unknown as {
|
|
124
|
+
api?: { agentPresets?: { list(request: Record<string, never>): Promise<{ result: { ok: boolean; value?: { presets?: readonly PresetRosterRow[] } } }> } }
|
|
125
|
+
}
|
|
126
|
+
const legacy = connection.api?.agentPresets
|
|
127
|
+
if (legacy === undefined) return undefined
|
|
128
|
+
const response = await legacy.list({})
|
|
129
|
+
if (!response.result.ok || response.result.value === undefined) return { ok: false, presets: [] }
|
|
130
|
+
return { ok: true, presets: response.result.value.presets ?? [] }
|
|
131
|
+
}
|
|
132
|
+
|
|
73
133
|
/**
|
|
74
134
|
* Mount the task board.
|
|
75
135
|
* @param ctx - client root context (services: sessions, workspaces).
|
|
@@ -132,7 +192,7 @@ export function apply(ctx: ClientContext): void {
|
|
|
132
192
|
// narrow these two client services during a combined package build.
|
|
133
193
|
const sessions = ctx.get('sessions') as unknown as ISessions
|
|
134
194
|
const workspaces = ctx.get('workspaces') as unknown as IWorkspaces
|
|
135
|
-
const
|
|
195
|
+
const remote = ctx.get('remote') as unknown as ClientRemote
|
|
136
196
|
|
|
137
197
|
// Core wiring: real runtime faces into the framework-free services.
|
|
138
198
|
const store = new LocalStorageTaskStore()
|
|
@@ -166,10 +226,10 @@ export function apply(ctx: ClientContext): void {
|
|
|
166
226
|
disposers.push(workspaces.list.subscribe(pushWorkspaceOptions))
|
|
167
227
|
const pushPresetOptions = async (): Promise<void> => {
|
|
168
228
|
try {
|
|
169
|
-
const
|
|
170
|
-
if (!
|
|
229
|
+
const roster = await readPresetRoster(ctx, remote)
|
|
230
|
+
if (roster === undefined || !roster.ok) return
|
|
171
231
|
controller.setExecutionOptions({
|
|
172
|
-
presets:
|
|
232
|
+
presets: roster.presets.map(preset => ({
|
|
173
233
|
id: preset.id,
|
|
174
234
|
name: preset.name,
|
|
175
235
|
description: preset.description,
|