@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.
Files changed (40) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +5 -0
  3. package/README.zh.md +5 -0
  4. package/lib/client.js +723 -29
  5. package/lib/client.js.map +1 -1
  6. package/lib/index.js +2629 -158
  7. package/lib/types/client/board/EditTaskModal.d.ts +7 -1
  8. package/lib/types/client/board/NewTaskModal.d.ts +6 -1
  9. package/lib/types/client/board/TaskBoard.d.ts +10 -1
  10. package/lib/types/client/board/TaskForm.d.ts +18 -0
  11. package/lib/types/client/host-api.d.ts +23 -1
  12. package/lib/types/client/locales.d.ts +36 -0
  13. package/lib/types/core/controller.d.ts +35 -1
  14. package/lib/types/core/tasks.d.ts +60 -0
  15. package/lib/types/core/use-cases/task-update.d.ts +8 -1
  16. package/lib/types/host-ai.d.ts +51 -0
  17. package/lib/types/host-routes.d.ts +11 -1
  18. package/lib/types/index.d.ts +9 -0
  19. package/lib/types/protocol.d.ts +19 -0
  20. package/package.json +1 -1
  21. package/src/client/board/EditTaskModal.tsx +52 -3
  22. package/src/client/board/NewTaskModal.tsx +100 -5
  23. package/src/client/board/TaskBoard.tsx +150 -6
  24. package/src/client/board/TaskCard.tsx +17 -1
  25. package/src/client/board/TaskDetail.tsx +38 -2
  26. package/src/client/board/TaskForm.tsx +85 -0
  27. package/src/client/board.module.css +180 -0
  28. package/src/client/host-api.ts +124 -5
  29. package/src/client/index.ts +7 -0
  30. package/src/client/locales.ts +72 -0
  31. package/src/core/controller.ts +57 -1
  32. package/src/core/store.ts +5 -1
  33. package/src/core/tasks.ts +112 -0
  34. package/src/core/use-cases/task-update.ts +12 -2
  35. package/src/host-ai.ts +152 -0
  36. package/src/host-ledger.ts +25 -3
  37. package/src/host-routes.ts +68 -3
  38. package/src/host-runner.ts +24 -1
  39. package/src/index.ts +26 -1
  40. package/src/protocol.ts +45 -3
@@ -1,8 +1,14 @@
1
1
  import type { BoardController } from '../../core/controller.ts';
2
- import type { TaskRecord } from '../../core/tasks.ts';
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
- /** Case-insensitive title/description/freeze-snapshot match. */
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 {};
@@ -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
@@ -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.21",
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 { TaskRecord } from '../../core/tasks.ts'
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
- if (await controller.updateTask(task.id, { title, description, prompt })) {
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
+