@linxin666/dsh-client-ui-task-board 0.3.21 → 0.3.23
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 +5 -0
- package/README.zh.md +5 -0
- package/lib/client.js +723 -29
- package/lib/client.js.map +1 -1
- package/lib/index.js +2629 -158
- package/lib/types/client/board/EditTaskModal.d.ts +7 -1
- package/lib/types/client/board/NewTaskModal.d.ts +6 -1
- package/lib/types/client/board/TaskBoard.d.ts +10 -1
- package/lib/types/client/board/TaskForm.d.ts +18 -0
- package/lib/types/client/host-api.d.ts +23 -1
- package/lib/types/client/locales.d.ts +36 -0
- package/lib/types/core/controller.d.ts +35 -1
- package/lib/types/core/tasks.d.ts +60 -0
- package/lib/types/core/use-cases/task-update.d.ts +8 -1
- package/lib/types/host-ai.d.ts +51 -0
- package/lib/types/host-routes.d.ts +11 -1
- package/lib/types/index.d.ts +9 -0
- package/lib/types/protocol.d.ts +19 -0
- package/package.json +1 -1
- package/src/client/board/EditTaskModal.tsx +52 -3
- package/src/client/board/NewTaskModal.tsx +100 -5
- package/src/client/board/TaskBoard.tsx +150 -6
- package/src/client/board/TaskCard.tsx +17 -1
- package/src/client/board/TaskDetail.tsx +38 -2
- package/src/client/board/TaskForm.tsx +85 -0
- package/src/client/board.module.css +180 -0
- package/src/client/host-api.ts +124 -5
- package/src/client/index.ts +7 -0
- package/src/client/locales.ts +72 -0
- package/src/core/controller.ts +57 -1
- package/src/core/store.ts +5 -1
- package/src/core/tasks.ts +112 -0
- package/src/core/use-cases/task-update.ts +12 -2
- package/src/host-ai.ts +152 -0
- package/src/host-ledger.ts +25 -3
- package/src/host-routes.ts +68 -3
- package/src/host-runner.ts +24 -1
- package/src/index.ts +26 -1
- package/src/protocol.ts +45 -3
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import type { BoardController } from '../../core/controller.ts';
|
|
2
|
-
import type
|
|
2
|
+
import { type TaskRecord } from '../../core/tasks.ts';
|
|
3
3
|
/** Edit-task form overlay. */
|
|
4
4
|
export declare function EditTaskModal({ controller, task, onClose }: {
|
|
5
5
|
controller: BoardController;
|
|
6
6
|
task: TaskRecord;
|
|
7
7
|
onClose: () => void;
|
|
8
8
|
}): import("react").JSX.Element;
|
|
9
|
+
/** Edit-tags modal: edit labels only, shown for tasks after first execution. */
|
|
10
|
+
export declare function EditTagsModal({ controller, task, onClose }: {
|
|
11
|
+
controller: BoardController;
|
|
12
|
+
task: TaskRecord;
|
|
13
|
+
onClose: () => void;
|
|
14
|
+
}): import("react").JSX.Element;
|
|
@@ -5,8 +5,13 @@ export interface NewTaskModalProps {
|
|
|
5
5
|
onClose: () => void;
|
|
6
6
|
/** Optional task template to clone/duplicate from. */
|
|
7
7
|
initialTask?: TaskRecord;
|
|
8
|
+
/**
|
|
9
|
+
* Workspace the board's project filter has selected (#1536): a task created
|
|
10
|
+
* while a project is open belongs to that project unless the user changes it.
|
|
11
|
+
*/
|
|
12
|
+
defaultWorkspaceId?: string;
|
|
8
13
|
/** Optional callback after successful duplication (e.g. to archive source). */
|
|
9
14
|
onDuplicateSuccess?: (sourceTaskId: string) => Promise<void>;
|
|
10
15
|
}
|
|
11
16
|
/** New-task form overlay. */
|
|
12
|
-
export declare function NewTaskModal({ controller, onClose, initialTask, onDuplicateSuccess }: NewTaskModalProps): import("react").JSX.Element;
|
|
17
|
+
export declare function NewTaskModal({ controller, onClose, initialTask, defaultWorkspaceId, onDuplicateSuccess }: NewTaskModalProps): import("react").JSX.Element;
|
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
import { type BoardController } from '../../core/controller.ts';
|
|
2
2
|
import { type TaskRecord } from '../../core/tasks.ts';
|
|
3
|
-
/**
|
|
3
|
+
/** Sentinel option value of the project row's "register a new project" entry. */
|
|
4
|
+
export declare const NEW_PROJECT_VALUE = "__dsh_new_project__";
|
|
5
|
+
/** Case-insensitive title/description/tag/freeze-snapshot match. */
|
|
4
6
|
export declare function matchesFilter(task: TaskRecord, filter: string): boolean;
|
|
7
|
+
/**
|
|
8
|
+
* Whether a task carries every selected label (issue #1521). Multi-select is
|
|
9
|
+
* conjunctive: adding a label narrows the board instead of widening it, which
|
|
10
|
+
* is the only reading that keeps "工作" selected from dragging unrelated cards
|
|
11
|
+
* back in when a second label is added.
|
|
12
|
+
*/
|
|
13
|
+
export declare function matchesTagFilter(task: TaskRecord, selected: readonly string[]): boolean;
|
|
5
14
|
/** Board component; subscribes to the controller snapshot. */
|
|
6
15
|
export declare function TaskBoard({ controller }: {
|
|
7
16
|
controller: BoardController;
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* are controlled components.
|
|
6
6
|
*/
|
|
7
7
|
import type { ReactNode } from 'react';
|
|
8
|
+
import { type TaskTag } from '../../core/tasks.ts';
|
|
8
9
|
/** Modal overlay: closes on backdrop press, submits through the form. */
|
|
9
10
|
export declare function ModalShell({ ariaLabel, title, error, pending, submitLabel, onSubmit, onClose, children, }: {
|
|
10
11
|
ariaLabel: string;
|
|
@@ -26,3 +27,20 @@ export declare function TaskContentFields({ title, description, prompt, onTitleC
|
|
|
26
27
|
onDescriptionChange: (value: string) => void;
|
|
27
28
|
onPromptChange: (value: string) => void;
|
|
28
29
|
}): import("react").JSX.Element;
|
|
30
|
+
/**
|
|
31
|
+
* Task labels (issue #1521): one row per label holding the badge name and an
|
|
32
|
+
* optional execution hint. The name inputs offer the labels already used on the
|
|
33
|
+
* board through a datalist, and picking one adopts its hint when the row has
|
|
34
|
+
* none — so a business line is defined once and reused by every later task.
|
|
35
|
+
*/
|
|
36
|
+
export declare function TaskTagFields({ tags, knownTags, onChange, }: {
|
|
37
|
+
tags: TaskTag[];
|
|
38
|
+
/** Labels already carried elsewhere on the board. */
|
|
39
|
+
knownTags: TaskTag[];
|
|
40
|
+
onChange: (tags: TaskTag[]) => void;
|
|
41
|
+
}): import("react").JSX.Element;
|
|
42
|
+
/**
|
|
43
|
+
* Clean a tag list via normalizeTags: trim, drop blanks and duplicates, cap
|
|
44
|
+
* lengths and count, so the wire always carries a valid tag list.
|
|
45
|
+
*/
|
|
46
|
+
export declare function cleanTags(tags: readonly TaskTag[]): TaskTag[];
|
|
@@ -1,10 +1,26 @@
|
|
|
1
1
|
import type { TaskRecord } from '../core/tasks.ts';
|
|
2
|
-
import { type TaskBoardAction, type TaskBoardEventPayload, type TaskBoardSnapshot } from '../protocol.ts';
|
|
2
|
+
import { type TaskBoardAction, type TaskBoardEventPayload, type TaskBoardParseDraft, type TaskBoardParseRequest, type TaskBoardSnapshot } from '../protocol.ts';
|
|
3
|
+
/**
|
|
4
|
+
* Failure classes of the Host API, in the language the panel renders
|
|
5
|
+
* (issue #1528). The Host is the only part that knows whether a task-board
|
|
6
|
+
* route exists at all, so a raw fetch/parse error must never reach the user:
|
|
7
|
+
* "Unexpected token 'o', \"not found\" is not valid JSON" is what a missing
|
|
8
|
+
* Host half looks like today.
|
|
9
|
+
*/
|
|
10
|
+
export type HostApiFailure = 'not-mounted' | 'unauthorized' | 'locked' | 'rejected' | 'timeout' | 'unreachable' | 'unexpected';
|
|
11
|
+
/** Transport failure carrying a stable class next to its user-facing message. */
|
|
12
|
+
export declare class HostApiError extends Error {
|
|
13
|
+
readonly failure: HostApiFailure;
|
|
14
|
+
readonly status?: number | undefined;
|
|
15
|
+
constructor(failure: HostApiFailure, message: string, status?: number | undefined);
|
|
16
|
+
}
|
|
3
17
|
export interface TaskBoardHostTransport {
|
|
4
18
|
bootstrap(legacy: readonly TaskRecord[]): Promise<TaskBoardSnapshot>;
|
|
5
19
|
state(): Promise<TaskBoardSnapshot>;
|
|
6
20
|
action(action: TaskBoardAction, initiator?: string): Promise<TaskBoardSnapshot>;
|
|
7
21
|
subscribe(listener: (event?: TaskBoardEventPayload) => void): () => void;
|
|
22
|
+
/** One-shot model parse of pasted text (issue #1540). */
|
|
23
|
+
parseDraft(request: TaskBoardParseRequest, signal?: AbortSignal): Promise<TaskBoardParseDraft>;
|
|
8
24
|
}
|
|
9
25
|
export declare class HttpTaskBoardHostTransport implements TaskBoardHostTransport {
|
|
10
26
|
private readonly storage;
|
|
@@ -14,5 +30,11 @@ export declare class HttpTaskBoardHostTransport implements TaskBoardHostTranspor
|
|
|
14
30
|
action(action: TaskBoardAction, initiator?: string): Promise<TaskBoardSnapshot>;
|
|
15
31
|
private post;
|
|
16
32
|
private request;
|
|
33
|
+
/**
|
|
34
|
+
* Ask the Host to turn pasted text into task fields. The route answers a
|
|
35
|
+
* typed failure (no model, timeout, unparseable reply) already phrased for
|
|
36
|
+
* the form, so the UI never renders a raw status code (issue #1540).
|
|
37
|
+
*/
|
|
38
|
+
parseDraft(request: TaskBoardParseRequest, signal?: AbortSignal): Promise<TaskBoardParseDraft>;
|
|
17
39
|
subscribe(listener: (event?: TaskBoardEventPayload) => void): () => void;
|
|
18
40
|
}
|
|
@@ -28,6 +28,30 @@ export declare const zh: {
|
|
|
28
28
|
'board.updated': string;
|
|
29
29
|
'board.created': string;
|
|
30
30
|
'board.hostError': string;
|
|
31
|
+
'board.hostError.notMounted': string;
|
|
32
|
+
'board.hostError.unauthorized': string;
|
|
33
|
+
'board.hostError.locked': string;
|
|
34
|
+
'board.hostError.timeout': string;
|
|
35
|
+
'board.hostError.unreachable': string;
|
|
36
|
+
'board.hostError.unexpected': string;
|
|
37
|
+
'board.project': string;
|
|
38
|
+
'board.projectAll': string;
|
|
39
|
+
'board.projectNew': string;
|
|
40
|
+
'board.projectNewPath': string;
|
|
41
|
+
'board.projectNewPathPlaceholder': string;
|
|
42
|
+
'board.projectCreate': string;
|
|
43
|
+
'board.projectCreateFailed': string;
|
|
44
|
+
'new.aiParse': string;
|
|
45
|
+
'new.aiParseHint': string;
|
|
46
|
+
'new.aiParsePlaceholder': string;
|
|
47
|
+
'new.aiParseModel': string;
|
|
48
|
+
'new.aiParseRun': string;
|
|
49
|
+
'new.aiParseCancel': string;
|
|
50
|
+
'new.aiParseEmpty': string;
|
|
51
|
+
'new.aiParseNoModel': string;
|
|
52
|
+
'new.aiParseUnavailable': string;
|
|
53
|
+
'new.aiParseTimeout': string;
|
|
54
|
+
'new.aiParseFailed': string;
|
|
31
55
|
'board.retryHost': string;
|
|
32
56
|
'board.hostMeta': string;
|
|
33
57
|
'new.title': string;
|
|
@@ -54,11 +78,23 @@ export declare const zh: {
|
|
|
54
78
|
'detail.handover': string;
|
|
55
79
|
'detail.handover.bundledAt': string;
|
|
56
80
|
'detail.handover.references': string;
|
|
81
|
+
'new.tags': string;
|
|
82
|
+
'new.tagsHint': string;
|
|
83
|
+
'new.tagName': string;
|
|
84
|
+
'new.tagNamePlaceholder': string;
|
|
85
|
+
'new.tagPrompt': string;
|
|
86
|
+
'new.tagPromptPlaceholder': string;
|
|
87
|
+
'new.tagAdd': string;
|
|
88
|
+
'new.tagRemove': string;
|
|
89
|
+
'board.tagFilter': string;
|
|
90
|
+
'board.tagFilterClear': string;
|
|
91
|
+
'board.tagEmpty': string;
|
|
57
92
|
'detail.permissionPending': string;
|
|
58
93
|
'detail.permissionConfirm': string;
|
|
59
94
|
'detail.permissionConfirmed': string;
|
|
60
95
|
'detail.title': string;
|
|
61
96
|
'detail.edit': string;
|
|
97
|
+
'detail.editTags': string;
|
|
62
98
|
'detail.duplicate': string;
|
|
63
99
|
'detail.duplicateAndEdit': string;
|
|
64
100
|
'detail.close': string;
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
import type { TaskStore } from './store.ts';
|
|
15
15
|
import { type NewTaskInput, type TaskRecord, type TaskStatus } from './tasks.ts';
|
|
16
16
|
import { type TaskUpdatePatch } from './use-cases/task-update.ts';
|
|
17
|
-
import type { TaskBoardAction, TaskBoardEventPayload, TaskBoardSnapshot } from '../protocol.ts';
|
|
17
|
+
import type { TaskBoardAction, TaskBoardEventPayload, TaskBoardParseDraft, TaskBoardParseRequest, TaskBoardSnapshot } from '../protocol.ts';
|
|
18
18
|
export interface TaskBoardTransport {
|
|
19
19
|
bootstrap(legacy: readonly TaskRecord[]): Promise<TaskBoardSnapshot>;
|
|
20
20
|
state(): Promise<TaskBoardSnapshot>;
|
|
@@ -24,6 +24,11 @@ export interface TaskBoardTransport {
|
|
|
24
24
|
*/
|
|
25
25
|
action(action: TaskBoardAction, initiator?: string): Promise<TaskBoardSnapshot>;
|
|
26
26
|
subscribe(listener: (event?: TaskBoardEventPayload) => void): () => void;
|
|
27
|
+
/**
|
|
28
|
+
* One-shot model parse of pasted text (issue #1540). Optional: a deployment
|
|
29
|
+
* that cannot parse simply omits it, and the form hides the section.
|
|
30
|
+
*/
|
|
31
|
+
parseDraft?(request: TaskBoardParseRequest, signal?: AbortSignal): Promise<TaskBoardParseDraft>;
|
|
27
32
|
}
|
|
28
33
|
/** The sessions face the controller needs for navigation awareness. */
|
|
29
34
|
export interface SessionsControllerFace {
|
|
@@ -47,6 +52,14 @@ export interface ControllerDeps {
|
|
|
47
52
|
/** Host-authoritative transport; absent keeps the legacy in-memory test path. */
|
|
48
53
|
transport?: TaskBoardTransport;
|
|
49
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* Register one host directory as a DSH project (workspace). Wired by the
|
|
57
|
+
* browser apply() to the runtime's workspace controller; without it the board
|
|
58
|
+
* hides its "new project" action instead of offering a dead control (#1536).
|
|
59
|
+
*/
|
|
60
|
+
export type WorkspaceCreator = (path: string) => Promise<{
|
|
61
|
+
workspaceId: string;
|
|
62
|
+
}>;
|
|
50
63
|
/** One workspace option the execution-target pickers offer. */
|
|
51
64
|
export interface ExecutionWorkspaceOption {
|
|
52
65
|
workspaceId: string;
|
|
@@ -85,6 +98,10 @@ export interface ControllerSnapshot {
|
|
|
85
98
|
/** Picker option sets (workspace list + agent-preset roster). */
|
|
86
99
|
executionOptions: ExecutionOptionsSnapshot;
|
|
87
100
|
pendingTaskIds: readonly string[];
|
|
101
|
+
/** Whether the board may offer "register a new project" (issue #1536). */
|
|
102
|
+
canCreateWorkspace?: boolean;
|
|
103
|
+
/** Whether this deployment can parse pasted text into task fields (issue #1540). */
|
|
104
|
+
canParseTask?: boolean;
|
|
88
105
|
transportError?: string;
|
|
89
106
|
host?: Pick<TaskBoardSnapshot, 'revision' | 'scheduler' | 'power' | 'sessionDefaultPermission'>;
|
|
90
107
|
}
|
|
@@ -101,6 +118,7 @@ export declare class BoardController {
|
|
|
101
118
|
private archiveView;
|
|
102
119
|
private selectedTaskId;
|
|
103
120
|
private executionOptions;
|
|
121
|
+
private workspaceCreator;
|
|
104
122
|
private listeners;
|
|
105
123
|
private disposers;
|
|
106
124
|
private readonly now;
|
|
@@ -151,6 +169,22 @@ export declare class BoardController {
|
|
|
151
169
|
* and agent-preset roster come from the runtime, not the ledger).
|
|
152
170
|
*/
|
|
153
171
|
setExecutionOptions(patch: Partial<ExecutionOptionsSnapshot>): void;
|
|
172
|
+
/** Wire (or clear) the runtime's project registration face (issue #1536). */
|
|
173
|
+
setWorkspaceCreator(creator: WorkspaceCreator | undefined): void;
|
|
174
|
+
/**
|
|
175
|
+
* Register an existing host directory as a DSH project, exactly as the GUI's
|
|
176
|
+
* own "add project" does; the runtime's failure message surfaces unchanged.
|
|
177
|
+
*/
|
|
178
|
+
createWorkspace(path: string): Promise<{
|
|
179
|
+
workspaceId: string;
|
|
180
|
+
}>;
|
|
181
|
+
/** Whether this deployment can parse pasted text into task fields (issue #1540). */
|
|
182
|
+
canParseTask(): boolean;
|
|
183
|
+
/**
|
|
184
|
+
* Parse pasted text into task fields through the Host. The transport already
|
|
185
|
+
* phrases every failure for the user, so its message surfaces unchanged.
|
|
186
|
+
*/
|
|
187
|
+
parseTaskDraft(request: TaskBoardParseRequest, signal?: AbortSignal): Promise<TaskBoardParseDraft>;
|
|
154
188
|
moveTask(id: string, status: TaskStatus): void;
|
|
155
189
|
deleteTask(id: string): void;
|
|
156
190
|
/**
|
|
@@ -81,6 +81,55 @@ export interface TaskFreeze extends FreezeSnapshot {
|
|
|
81
81
|
*/
|
|
82
82
|
frozenBy?: string;
|
|
83
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* One task label (issue #1521). The name is the badge and the tag-filter key;
|
|
86
|
+
* the optional prompt line rides the execution prompt, so a label may be
|
|
87
|
+
* display-only (no prefix) or carry an instruction (business-line routing,
|
|
88
|
+
* output directory, house style) that every run of the task inherits.
|
|
89
|
+
*/
|
|
90
|
+
export interface TaskTag {
|
|
91
|
+
/** Display name; trimmed, non-empty, unique within the task. */
|
|
92
|
+
name: string;
|
|
93
|
+
/**
|
|
94
|
+
* Prompt line injected ahead of the execution prompt. Absent (or blank
|
|
95
|
+
* after trimming) keeps the tag display-only: it never touches the prompt,
|
|
96
|
+
* so a label with no prefix has zero execution side effects.
|
|
97
|
+
*/
|
|
98
|
+
promptPrefix?: string;
|
|
99
|
+
}
|
|
100
|
+
/** Maximum number of tags carried by one task. */
|
|
101
|
+
export declare const TASK_TAG_LIMIT = 8;
|
|
102
|
+
/** Maximum length of a tag name. */
|
|
103
|
+
export declare const TAG_NAME_MAX_LENGTH = 32;
|
|
104
|
+
/** Maximum length of a tag's injected prompt line. */
|
|
105
|
+
export declare const TAG_PROMPT_MAX_LENGTH = 200;
|
|
106
|
+
/** Whether an unknown value is a well-formed tag (strict: the wire gate). */
|
|
107
|
+
export declare function isTaskTag(value: unknown): value is TaskTag;
|
|
108
|
+
/**
|
|
109
|
+
* Whether an unknown value is a well-formed tag list (strict: the wire gate).
|
|
110
|
+
* An empty list is rejected — clearing tags is expressed by omitting the field
|
|
111
|
+
* (create) or by an explicit null (update), never by an empty array.
|
|
112
|
+
*/
|
|
113
|
+
export declare function isTaskTagList(value: unknown): value is TaskTag[];
|
|
114
|
+
/**
|
|
115
|
+
* Repair a persisted tag list: keep the well-formed entries, trim, drop
|
|
116
|
+
* blanks and repeats, cap the count, and collapse a blank prompt line to
|
|
117
|
+
* "display-only". Returns undefined when nothing usable remains, so the caller
|
|
118
|
+
* clears the field instead of storing an empty array.
|
|
119
|
+
*/
|
|
120
|
+
export declare function normalizeTags(value: unknown): TaskTag[] | undefined;
|
|
121
|
+
/**
|
|
122
|
+
* Stable palette slot (0..5) for a tag name. The same label always lands on the
|
|
123
|
+
* same tone, so a badge needs no stored colour and two tasks sharing a label
|
|
124
|
+
* cannot disagree about it.
|
|
125
|
+
*/
|
|
126
|
+
export declare function tagTone(name: string): number;
|
|
127
|
+
/**
|
|
128
|
+
* Union of the labels already carried by `tasks`, first occurrence wins (the
|
|
129
|
+
* oldest task's hint is the one offered). Feeds the editor's name datalist and
|
|
130
|
+
* the board's tag filter, so a label defined once can be reused everywhere.
|
|
131
|
+
*/
|
|
132
|
+
export declare function collectKnownTags(tasks: readonly TaskRecord[]): TaskTag[];
|
|
84
133
|
/** One task on the board. */
|
|
85
134
|
export interface TaskRecord {
|
|
86
135
|
/** Stable task id (uuid). */
|
|
@@ -145,6 +194,12 @@ export interface TaskRecord {
|
|
|
145
194
|
* bundle's triplet overrides the legacy pin fields at execution time.
|
|
146
195
|
*/
|
|
147
196
|
handover?: TaskHandover;
|
|
197
|
+
/**
|
|
198
|
+
* Task labels (issue #1521): optional, additive, and absent on every task
|
|
199
|
+
* created before the field existed. A tag whose `promptPrefix` is set is
|
|
200
|
+
* prepended to the execution prompt; a bare name is display and filter only.
|
|
201
|
+
*/
|
|
202
|
+
tags?: TaskTag[];
|
|
148
203
|
/**
|
|
149
204
|
* Human confirmation stamp for an above-default effective permission
|
|
150
205
|
* (ms epoch). Absent while the binding awaits confirmation; any permission
|
|
@@ -208,6 +263,11 @@ export interface NewTaskInput {
|
|
|
208
263
|
* sanitized by the protocol gate) attached at creation.
|
|
209
264
|
*/
|
|
210
265
|
handover?: TaskHandoverInput;
|
|
266
|
+
/**
|
|
267
|
+
* Optional task labels. A tag with a non-blank `promptPrefix` is injected
|
|
268
|
+
* ahead of the execution prompt; a bare name changes nothing at run time.
|
|
269
|
+
*/
|
|
270
|
+
tags?: TaskTag[];
|
|
211
271
|
}
|
|
212
272
|
/** The five kanban columns, in display order. */
|
|
213
273
|
export declare const COLUMNS: readonly {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* back to the runtime default); an unknown permission string is ignored so
|
|
9
9
|
* stale UI can never persist a value the execution service rejects.
|
|
10
10
|
*/
|
|
11
|
-
import { type TaskRecord } from '../tasks.ts';
|
|
11
|
+
import { type TaskRecord, type TaskTag } from '../tasks.ts';
|
|
12
12
|
import type { FreezeSnapshot } from '../freeze-snapshot.ts';
|
|
13
13
|
import type { TaskHandoverInput } from '../handover.ts';
|
|
14
14
|
/**
|
|
@@ -21,6 +21,13 @@ export type TaskUpdatePatch = Partial<Pick<TaskRecord, 'title' | 'description' |
|
|
|
21
21
|
} | null;
|
|
22
22
|
/** Replaces the handover bundle (restamping bundledAt); an explicit null clears it. */
|
|
23
23
|
handover?: TaskHandoverInput | null;
|
|
24
|
+
/**
|
|
25
|
+
* Replaces the task's labels (issue #1521). Unlike the content fields, tags
|
|
26
|
+
* stay editable after the first run: they classify the task and shape the
|
|
27
|
+
* NEXT execution prompt, they are not the record of what already ran. An
|
|
28
|
+
* explicit null (or a list that normalizes to nothing) clears them.
|
|
29
|
+
*/
|
|
30
|
+
tags?: TaskTag[] | null;
|
|
24
31
|
};
|
|
25
32
|
/** The fields that edit the task's content (what the user reads and what the
|
|
26
33
|
* next execution sends). Unlike the execution targets they stay editable only
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One-shot model parse behind the board's "parse pasted text" action
|
|
3
|
+
* (issue #1540).
|
|
4
|
+
*
|
|
5
|
+
* The browser half never talks to a model: it posts the pasted text and the
|
|
6
|
+
* route the user picked to the Host, which calls the injected `llm` service
|
|
7
|
+
* once and answers with the three draft fields the new-task form accepts. The
|
|
8
|
+
* reply is untrusted text, so extraction is defensive: code fences are
|
|
9
|
+
* stripped, the first JSON object wins, and an unusable reply falls back to
|
|
10
|
+
* the user's own words instead of an empty form.
|
|
11
|
+
*/
|
|
12
|
+
import { type LlmRuntime } from '@deepseek-ai/dsh-llm';
|
|
13
|
+
import type { TaskBoardParseDraft, TaskBoardParseRequest } from './protocol.ts';
|
|
14
|
+
/** How long one parse may take before the Host gives up on the model. */
|
|
15
|
+
export declare const TASK_PARSE_TIMEOUT_MS = 45000;
|
|
16
|
+
/** Largest pasted text one parse accepts (bytes). */
|
|
17
|
+
export declare const TASK_PARSE_MAX_INPUT: number;
|
|
18
|
+
export type TaskParseFailureCode = 'no-model' | 'model-error' | 'parse-failed' | 'timeout';
|
|
19
|
+
/** Typed parse failure; the route maps the code onto a status and a body. */
|
|
20
|
+
export declare class TaskParseError extends Error {
|
|
21
|
+
readonly code: TaskParseFailureCode;
|
|
22
|
+
constructor(code: TaskParseFailureCode, message: string);
|
|
23
|
+
}
|
|
24
|
+
/** Split a qualified `provider/model` route the same way the runner does. */
|
|
25
|
+
export declare function splitModelRoute(qualified: string | undefined): {
|
|
26
|
+
provider: string;
|
|
27
|
+
model: string;
|
|
28
|
+
} | undefined;
|
|
29
|
+
/**
|
|
30
|
+
* Read the draft out of a model reply. Fences and surrounding prose are
|
|
31
|
+
* tolerated; anything unusable returns undefined so the caller can decide
|
|
32
|
+
* between a typed failure and a fallback.
|
|
33
|
+
* @param reply - raw model text.
|
|
34
|
+
*/
|
|
35
|
+
export declare function extractTaskParseReply(reply: string): Partial<TaskBoardParseDraft> | undefined;
|
|
36
|
+
/**
|
|
37
|
+
* Fill the three form fields, falling back to the pasted text so a weak model
|
|
38
|
+
* reply never loses what the user actually wrote.
|
|
39
|
+
* @param reply - raw model text.
|
|
40
|
+
* @param source - the text the user pasted.
|
|
41
|
+
*/
|
|
42
|
+
export declare function draftFromReply(reply: string, source: string): TaskBoardParseDraft;
|
|
43
|
+
/**
|
|
44
|
+
* Parse one pasted text through the injected `llm` service.
|
|
45
|
+
* @param llm - the Host's llm service.
|
|
46
|
+
* @param request - pasted text plus the qualified model route.
|
|
47
|
+
* @param signal - caller cancellation (client disconnect); combined with the timeout.
|
|
48
|
+
* @returns the draft fields.
|
|
49
|
+
* @throws TaskParseError with a code the route maps onto a status.
|
|
50
|
+
*/
|
|
51
|
+
export declare function parseTaskDraft(llm: LlmRuntime, request: TaskBoardParseRequest, signal?: AbortSignal): Promise<TaskBoardParseDraft>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { IncomingMessage } from 'node:http';
|
|
2
2
|
import type { WebRoute } from '@deepseek-ai/dsh-host-webserver';
|
|
3
3
|
import type { TaskBoardHostService } from './host-service.ts';
|
|
4
|
+
import { type TaskBoardParseDraft, type TaskBoardParseRequest } from './protocol.ts';
|
|
4
5
|
/** Header replaced by an authenticated same-host reverse proxy. */
|
|
5
6
|
export declare const TASK_BOARD_PROXY_TOKEN_HEADER = "x-dsh-task-board-proxy-token";
|
|
6
7
|
/** Optional authenticated reverse-proxy access layered over the loopback default. */
|
|
@@ -8,6 +9,15 @@ export interface TaskBoardRouteAccess {
|
|
|
8
9
|
trustedProxyHosts?: readonly string[];
|
|
9
10
|
proxyToken?: string;
|
|
10
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* Host faces the routes need beyond the ledger service. The parse face is
|
|
14
|
+
* resolved lazily by the caller, so a deployment without an llm service still
|
|
15
|
+
* registers the route and answers with a typed "no model" failure instead of
|
|
16
|
+
* leaving the panel with an unmounted path (issue #1540).
|
|
17
|
+
*/
|
|
18
|
+
export interface TaskBoardRouteOptions {
|
|
19
|
+
parseTask?: (request: TaskBoardParseRequest, signal: AbortSignal) => Promise<TaskBoardParseDraft>;
|
|
20
|
+
}
|
|
11
21
|
interface ResolvedRouteAccess {
|
|
12
22
|
trustedProxyHosts: ReadonlySet<string>;
|
|
13
23
|
proxyToken?: string;
|
|
@@ -22,5 +32,5 @@ interface ResolvedRouteAccess {
|
|
|
22
32
|
* internal token header after their own authentication step.
|
|
23
33
|
*/
|
|
24
34
|
export declare function isTrustedTaskBoardRequest(req: IncomingMessage, access: ResolvedRouteAccess): boolean;
|
|
25
|
-
export declare function makeTaskBoardRoutes(service: TaskBoardHostService, access?: TaskBoardRouteAccess): WebRoute[];
|
|
35
|
+
export declare function makeTaskBoardRoutes(service: TaskBoardHostService, access?: TaskBoardRouteAccess, options?: TaskBoardRouteOptions): WebRoute[];
|
|
26
36
|
export {};
|
package/lib/types/index.d.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import type { Context } from '@deepseek-ai/cordis';
|
|
9
9
|
import type { SettingsNamespace } from '@deepseek-ai/dsh-settings';
|
|
10
10
|
import z from 'schemastery';
|
|
11
|
+
import type { LlmRuntime } from '@deepseek-ai/dsh-llm';
|
|
11
12
|
import { type TaskPermission } from './core/tasks.ts';
|
|
12
13
|
/** Default environment variable holding the authenticated proxy token. */
|
|
13
14
|
export declare const DEFAULT_PROXY_TOKEN_ENV = "DSH_TASK_BOARD_PROXY_TOKEN";
|
|
@@ -49,6 +50,14 @@ export declare function resolveProxyAccess(config: Config | undefined, env?: Nod
|
|
|
49
50
|
trustedProxyHosts: string[];
|
|
50
51
|
proxyToken?: string;
|
|
51
52
|
};
|
|
53
|
+
/**
|
|
54
|
+
* Read the optional `llm` service. The board deliberately does not inject it:
|
|
55
|
+
* a deployment without a model must still mount the board, and the parse route
|
|
56
|
+
* answers a typed failure instead of the plugin failing to load (issue #1540).
|
|
57
|
+
* @param ctx - the plugin context.
|
|
58
|
+
* @returns the llm service, or undefined when this deployment serves none.
|
|
59
|
+
*/
|
|
60
|
+
export declare function resolveLlmRuntime(ctx: Context): LlmRuntime | undefined;
|
|
52
61
|
/**
|
|
53
62
|
* Register the board's announcement section, gated on the composition entry's
|
|
54
63
|
* `announceToAgent` (and the live settings value once the web settings
|
package/lib/types/protocol.d.ts
CHANGED
|
@@ -36,6 +36,25 @@ export interface TaskBoardEventPayload {
|
|
|
36
36
|
scheduler: TaskBoardSchedulerSnapshot;
|
|
37
37
|
power: TaskBoardPowerSnapshot;
|
|
38
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* Request body of `POST {TASK_BOARD_API_PREFIX}/parse` (issue #1540): the text
|
|
41
|
+
* the user pasted plus the `provider/model` route that should parse it.
|
|
42
|
+
*/
|
|
43
|
+
export interface TaskBoardParseRequest {
|
|
44
|
+
text: string;
|
|
45
|
+
/** Qualified route from the model picker; absent when no route was chosen. */
|
|
46
|
+
model?: string;
|
|
47
|
+
}
|
|
48
|
+
/** Draft fields a parse returns; the user reviews them before creating the task. */
|
|
49
|
+
export interface TaskBoardParseDraft {
|
|
50
|
+
title: string;
|
|
51
|
+
description: string;
|
|
52
|
+
prompt: string;
|
|
53
|
+
}
|
|
54
|
+
/** Strict parse of a `/parse` request body; undefined rejects the request. */
|
|
55
|
+
export declare function parseTaskParseRequest(value: unknown): TaskBoardParseRequest | undefined;
|
|
56
|
+
/** Whether a decoded reply carries the three draft strings the form accepts. */
|
|
57
|
+
export declare function isTaskParseDraft(value: unknown): value is TaskBoardParseDraft;
|
|
39
58
|
export type TaskBoardAction = {
|
|
40
59
|
kind: 'import';
|
|
41
60
|
sourceId: string;
|
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.23",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "lib/types/index.d.ts",
|
|
@@ -7,15 +7,16 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { useState } from 'react'
|
|
9
9
|
import type { BoardController } from '../../core/controller.ts'
|
|
10
|
-
import type
|
|
10
|
+
import { collectKnownTags, type TaskRecord, type TaskTag } from '../../core/tasks.ts'
|
|
11
11
|
import { t } from '../locales.ts'
|
|
12
|
-
import { ModalShell, TaskContentFields } from './TaskForm.tsx'
|
|
12
|
+
import { ModalShell, TaskContentFields, TaskTagFields, cleanTags } from './TaskForm.tsx'
|
|
13
13
|
|
|
14
14
|
/** Edit-task form overlay. */
|
|
15
15
|
export function EditTaskModal({ controller, task, onClose }: { controller: BoardController; task: TaskRecord; onClose: () => void }) {
|
|
16
16
|
const [title, setTitle] = useState(task.title)
|
|
17
17
|
const [description, setDescription] = useState(task.description)
|
|
18
18
|
const [prompt, setPrompt] = useState(task.prompt)
|
|
19
|
+
const [tags, setTags] = useState<TaskTag[]>(task.tags ?? [])
|
|
19
20
|
const [error, setError] = useState<string | undefined>(undefined)
|
|
20
21
|
const [pending, setPending] = useState(false)
|
|
21
22
|
|
|
@@ -27,7 +28,17 @@ export function EditTaskModal({ controller, task, onClose }: { controller: Board
|
|
|
27
28
|
setPending(true)
|
|
28
29
|
// The Host confirms the mutation (and its fail-closed checks); only a
|
|
29
30
|
// confirmed save closes the modal.
|
|
30
|
-
|
|
31
|
+
// Labels ride the same patch as the content fields. A task that never
|
|
32
|
+
// carried one is not sent a clearing null: the wire stays minimal, and the
|
|
33
|
+
// no-tags path keeps producing exactly the patch it produced before.
|
|
34
|
+
const tagList = cleanTags(tags)
|
|
35
|
+
const patch = {
|
|
36
|
+
title,
|
|
37
|
+
description,
|
|
38
|
+
prompt,
|
|
39
|
+
...(tagList.length > 0 ? { tags: tagList } : (task.tags === undefined ? {} : { tags: null })),
|
|
40
|
+
}
|
|
41
|
+
if (await controller.updateTask(task.id, patch)) {
|
|
31
42
|
onClose()
|
|
32
43
|
return
|
|
33
44
|
}
|
|
@@ -53,6 +64,44 @@ export function EditTaskModal({ controller, task, onClose }: { controller: Board
|
|
|
53
64
|
onDescriptionChange={setDescription}
|
|
54
65
|
onPromptChange={setPrompt}
|
|
55
66
|
/>
|
|
67
|
+
|
|
68
|
+
<TaskTagFields tags={tags} knownTags={collectKnownTags(controller.getSnapshot().tasks)} onChange={setTags} />
|
|
69
|
+
</ModalShell>
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Edit-tags modal: edit labels only, shown for tasks after first execution. */
|
|
74
|
+
export function EditTagsModal({ controller, task, onClose }: { controller: BoardController; task: TaskRecord; onClose: () => void }) {
|
|
75
|
+
const [tags, setTags] = useState<TaskTag[]>(task.tags ?? [])
|
|
76
|
+
const [error, setError] = useState<string | undefined>(undefined)
|
|
77
|
+
const [pending, setPending] = useState(false)
|
|
78
|
+
|
|
79
|
+
const submit = async (): Promise<void> => {
|
|
80
|
+
setPending(true)
|
|
81
|
+
const tagList = cleanTags(tags)
|
|
82
|
+
const patch = {
|
|
83
|
+
tags: tagList.length > 0 ? tagList : null,
|
|
84
|
+
}
|
|
85
|
+
if (await controller.updateTask(task.id, patch)) {
|
|
86
|
+
onClose()
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
setPending(false)
|
|
90
|
+
setError(controller.getSnapshot().transportError ?? t('new.required'))
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<ModalShell
|
|
95
|
+
ariaLabel={t('detail.editTags')}
|
|
96
|
+
title={t('detail.editTags')}
|
|
97
|
+
error={error}
|
|
98
|
+
pending={pending}
|
|
99
|
+
submitLabel={t('edit.save')}
|
|
100
|
+
onSubmit={() => { void submit() }}
|
|
101
|
+
onClose={onClose}
|
|
102
|
+
>
|
|
103
|
+
<TaskTagFields tags={tags} knownTags={collectKnownTags(controller.getSnapshot().tasks)} onChange={setTags} />
|
|
56
104
|
</ModalShell>
|
|
57
105
|
)
|
|
58
106
|
}
|
|
107
|
+
|