@linxin666/dsh-client-ui-task-board 0.3.14 → 0.3.16
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/lib/client.js +92 -54
- package/lib/client.js.map +1 -1
- package/lib/index.js +40 -10
- package/lib/types/client/locales.d.ts +3 -0
- package/lib/types/core/controller.d.ts +8 -0
- package/lib/types/core/tasks.d.ts +7 -0
- package/lib/types/core/use-cases/task-update.d.ts +1 -1
- package/package.json +1 -1
- package/src/client/board/NewTaskModal.tsx +16 -0
- package/src/client/board/TaskDetail.tsx +17 -0
- package/src/client/index.ts +34 -2
- package/src/client/locales.ts +6 -0
- package/src/core/controller.ts +10 -1
- package/src/core/tasks.ts +8 -0
- package/src/core/use-cases/task-update.ts +3 -1
- package/src/host-runner.ts +15 -0
- package/src/index.ts +14 -4
- package/src/protocol.ts +4 -4
package/lib/index.js
CHANGED
|
@@ -284,6 +284,7 @@ function createTask(input, now, id) {
|
|
|
284
284
|
workspaceId: normalizeTargetId(input.workspaceId),
|
|
285
285
|
mode: normalizeTargetId(input.mode),
|
|
286
286
|
permission: isTaskPermission(input.permission) ? input.permission : void 0,
|
|
287
|
+
model: normalizeTargetId(input.model),
|
|
287
288
|
...input.freeze === void 0 ? {} : { freeze: freezeOf(input.freeze, now) },
|
|
288
289
|
...input.handover === void 0 ? {} : { handover: {
|
|
289
290
|
...input.handover,
|
|
@@ -911,6 +912,7 @@ function applyUpdateTask(tasks, id, patch, now) {
|
|
|
911
912
|
const workspaceId = "workspaceId" in patch ? normalizeTargetId(patch.workspaceId) : void 0;
|
|
912
913
|
const mode = "mode" in patch ? normalizeTargetId(patch.mode) : void 0;
|
|
913
914
|
const permission = "permission" in patch ? normalizePermission(task.permission, patch.permission) : void 0;
|
|
915
|
+
const model = "model" in patch ? normalizeTargetId(patch.model) : void 0;
|
|
914
916
|
const next = {
|
|
915
917
|
...task,
|
|
916
918
|
...rest,
|
|
@@ -930,6 +932,7 @@ function applyUpdateTask(tasks, id, patch, now) {
|
|
|
930
932
|
if (workspaceId !== void 0 || "workspaceId" in patch) next.workspaceId = workspaceId;
|
|
931
933
|
if (mode !== void 0 || "mode" in patch) next.mode = mode;
|
|
932
934
|
if (permission !== void 0 || "permission" in patch) next.permission = permission;
|
|
935
|
+
if (model !== void 0 || "model" in patch) next.model = model;
|
|
933
936
|
return next;
|
|
934
937
|
});
|
|
935
938
|
}
|
|
@@ -1059,10 +1062,11 @@ function createInput(value) {
|
|
|
1059
1062
|
"permission",
|
|
1060
1063
|
"schedule",
|
|
1061
1064
|
"freeze",
|
|
1062
|
-
"handover"
|
|
1065
|
+
"handover",
|
|
1066
|
+
"model"
|
|
1063
1067
|
])) return false;
|
|
1064
1068
|
if (typeof input.title !== "string" || typeof input.description !== "string" || typeof input.prompt !== "string") return false;
|
|
1065
|
-
if (!optionalString(input.workspaceId) || !optionalString(input.mode)) return false;
|
|
1069
|
+
if (!optionalString(input.workspaceId) || !optionalString(input.mode) || !optionalString(input.model)) return false;
|
|
1066
1070
|
if (input.permission !== void 0 && !isTaskPermission(input.permission)) return false;
|
|
1067
1071
|
if (input.freeze !== void 0 && freezePayload(input.freeze) === void 0) return false;
|
|
1068
1072
|
if (input.handover !== void 0 && handoverPayload(input.handover) === void 0) return false;
|
|
@@ -1083,14 +1087,16 @@ function updatePatch(value) {
|
|
|
1083
1087
|
"mode",
|
|
1084
1088
|
"permission",
|
|
1085
1089
|
"freeze",
|
|
1086
|
-
"handover"
|
|
1090
|
+
"handover",
|
|
1091
|
+
"model"
|
|
1087
1092
|
])) return false;
|
|
1088
1093
|
for (const key of [
|
|
1089
1094
|
"title",
|
|
1090
1095
|
"description",
|
|
1091
1096
|
"prompt",
|
|
1092
1097
|
"workspaceId",
|
|
1093
|
-
"mode"
|
|
1098
|
+
"mode",
|
|
1099
|
+
"model"
|
|
1094
1100
|
]) if (!optionalString(patch[key])) return false;
|
|
1095
1101
|
if (patch.permission !== void 0 && !isTaskPermission(patch.permission)) return false;
|
|
1096
1102
|
if (patch.freeze !== void 0 && patch.freeze !== null && freezePayload(patch.freeze) === void 0) return false;
|
|
@@ -2149,6 +2155,21 @@ var HostExecutionRunner = class {
|
|
|
2149
2155
|
if (command === void 0) throw new Error("permission command was not acknowledged");
|
|
2150
2156
|
if (command.kind !== "success") throw new Error(command.text ?? "permission command failed");
|
|
2151
2157
|
}
|
|
2158
|
+
if (task.model !== void 0 && task.model.trim() !== "") {
|
|
2159
|
+
const rawModel = task.model.trim();
|
|
2160
|
+
const slashIdx = rawModel.indexOf("/");
|
|
2161
|
+
const provider = slashIdx >= 0 ? rawModel.slice(0, slashIdx).trim() : void 0;
|
|
2162
|
+
const modelId = slashIdx >= 0 ? rawModel.slice(slashIdx + 1).trim() : rawModel;
|
|
2163
|
+
try {
|
|
2164
|
+
await this.invoke("session", "selectModel", {
|
|
2165
|
+
sessionId,
|
|
2166
|
+
...provider ? { provider } : {},
|
|
2167
|
+
model: modelId
|
|
2168
|
+
});
|
|
2169
|
+
} catch (modelError) {
|
|
2170
|
+
console.warn(`[dsh-task-board] failed to select model "${task.model}" for session ${sessionId}, falling back to default:`, modelError);
|
|
2171
|
+
}
|
|
2172
|
+
}
|
|
2152
2173
|
await this.invoke("session", "prompt", {
|
|
2153
2174
|
sessionId,
|
|
2154
2175
|
requestId: "task-board-" + crypto.randomUUID(),
|
|
@@ -3179,12 +3200,21 @@ function applyImpl(ctx, config) {
|
|
|
3179
3200
|
});
|
|
3180
3201
|
};
|
|
3181
3202
|
ctx.inject(["settings"], (settingsCtx) => {
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3203
|
+
try {
|
|
3204
|
+
if (typeof settingsCtx.settings?.installSection === "function") settingsCtx.settings.installSection(ctx, TASK_BOARD_SETTINGS_NAMESPACE, Config, config ?? {}, {
|
|
3205
|
+
setSource: (source) => {
|
|
3206
|
+
current = source;
|
|
3207
|
+
},
|
|
3208
|
+
onChange: sync
|
|
3209
|
+
});
|
|
3210
|
+
else if (typeof settingsCtx.settings?.register === "function") {
|
|
3211
|
+
const scope = settingsCtx.settings.register(TASK_BOARD_SETTINGS_NAMESPACE, Config, { base: config ?? {} });
|
|
3212
|
+
current = () => scope?.get?.() ?? config ?? {};
|
|
3213
|
+
scope?.watch?.(() => {
|
|
3214
|
+
sync();
|
|
3215
|
+
});
|
|
3216
|
+
}
|
|
3217
|
+
} catch {}
|
|
3188
3218
|
});
|
|
3189
3219
|
sync();
|
|
3190
3220
|
}
|
|
@@ -108,6 +108,7 @@ export declare const zh: {
|
|
|
108
108
|
'new.workspace': string;
|
|
109
109
|
'new.mode': string;
|
|
110
110
|
'new.permission': string;
|
|
111
|
+
'new.model': string;
|
|
111
112
|
'exec.workspace.recent': string;
|
|
112
113
|
'exec.mode.default': string;
|
|
113
114
|
'exec.mode.defaultSuffix': string;
|
|
@@ -117,6 +118,8 @@ export declare const zh: {
|
|
|
117
118
|
'exec.permission.read-only': string;
|
|
118
119
|
'exec.permission.workspace-write': string;
|
|
119
120
|
'exec.permission.danger-full-access': string;
|
|
121
|
+
'exec.model.default': string;
|
|
122
|
+
'exec.model.unknown': string;
|
|
120
123
|
'detail.executionSettings': string;
|
|
121
124
|
'exec.hint': string;
|
|
122
125
|
'settings.title': string;
|
|
@@ -62,10 +62,18 @@ export interface ExecutionPresetOption {
|
|
|
62
62
|
broken?: string;
|
|
63
63
|
isDefault: boolean;
|
|
64
64
|
}
|
|
65
|
+
/** One model option the execution-target pickers offer. */
|
|
66
|
+
export interface ExecutionModelOption {
|
|
67
|
+
id: string;
|
|
68
|
+
name?: string;
|
|
69
|
+
label?: string;
|
|
70
|
+
provider?: string;
|
|
71
|
+
}
|
|
65
72
|
/** The execution-target option sets the UI feeds into the controller. */
|
|
66
73
|
export interface ExecutionOptionsSnapshot {
|
|
67
74
|
workspaces: readonly ExecutionWorkspaceOption[];
|
|
68
75
|
presets: readonly ExecutionPresetOption[];
|
|
76
|
+
models?: readonly ExecutionModelOption[];
|
|
69
77
|
}
|
|
70
78
|
/** Immutable controller snapshot for UI subscriptions. */
|
|
71
79
|
export interface ControllerSnapshot {
|
|
@@ -119,6 +119,11 @@ export interface TaskRecord {
|
|
|
119
119
|
* `/permission <id>` slash command; absent leaves the session default.
|
|
120
120
|
*/
|
|
121
121
|
permission?: TaskPermission;
|
|
122
|
+
/**
|
|
123
|
+
* Pinned model selection for the execution session (format: "provider/model" or model id);
|
|
124
|
+
* absent falls back to the host default (agent-default-model).
|
|
125
|
+
*/
|
|
126
|
+
model?: string;
|
|
122
127
|
/**
|
|
123
128
|
* Frozen context snapshot for a continuation card; absent on plain tasks.
|
|
124
129
|
* Sanitized before it enters the ledger (redaction, slash-command taint,
|
|
@@ -164,6 +169,8 @@ export interface NewTaskInput {
|
|
|
164
169
|
mode?: string;
|
|
165
170
|
/** Permission preset applied to the execution session; absent = session default. */
|
|
166
171
|
permission?: TaskPermission;
|
|
172
|
+
/** Optional pinned model for the execution session; absent = host default. */
|
|
173
|
+
model?: string;
|
|
167
174
|
/**
|
|
168
175
|
* Optional scheduled-run rule requested at creation time (the new-task
|
|
169
176
|
* dialog): an enable flag plus a 5-field cron expression. The create use
|
|
@@ -15,7 +15,7 @@ import type { TaskHandoverInput } from '../handover.ts';
|
|
|
15
15
|
* Editable fields on a task (the update patch surface). `freeze` replaces the
|
|
16
16
|
* continuation-card snapshot (restamping frozenAt); an explicit null clears it.
|
|
17
17
|
*/
|
|
18
|
-
export type TaskUpdatePatch = Partial<Pick<TaskRecord, 'title' | 'description' | 'prompt' | 'workspaceId' | 'mode' | 'permission'>> & {
|
|
18
|
+
export type TaskUpdatePatch = Partial<Pick<TaskRecord, 'title' | 'description' | 'prompt' | 'workspaceId' | 'mode' | 'permission' | 'model'>> & {
|
|
19
19
|
freeze?: FreezeSnapshot & {
|
|
20
20
|
redacted?: boolean;
|
|
21
21
|
} | null;
|
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.16",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "lib/types/index.d.ts",
|
|
@@ -20,6 +20,7 @@ export function NewTaskModal({ controller, onClose }: { controller: BoardControl
|
|
|
20
20
|
const [workspaceId, setWorkspaceId] = useState('')
|
|
21
21
|
const [mode, setMode] = useState('')
|
|
22
22
|
const [permission, setPermission] = useState('')
|
|
23
|
+
const [model, setModel] = useState('')
|
|
23
24
|
const [scheduleEnabled, setScheduleEnabled] = useState(false)
|
|
24
25
|
const [scheduleCron, setScheduleCron] = useState('')
|
|
25
26
|
const [scheduleError, setScheduleError] = useState<string | undefined>(undefined)
|
|
@@ -76,6 +77,7 @@ export function NewTaskModal({ controller, onClose }: { controller: BoardControl
|
|
|
76
77
|
workspaceId: workspaceId === '' ? undefined : workspaceId,
|
|
77
78
|
mode: mode === '' ? undefined : mode,
|
|
78
79
|
permission: permission === '' ? undefined : permission as TaskPermission,
|
|
80
|
+
model: model === '' ? undefined : model,
|
|
79
81
|
schedule: scheduleEnabled ? { enabled: true, cron: scheduleCron.trim() } : undefined,
|
|
80
82
|
})
|
|
81
83
|
if (task === undefined) {
|
|
@@ -181,6 +183,20 @@ export function NewTaskModal({ controller, onClose }: { controller: BoardControl
|
|
|
181
183
|
</select>
|
|
182
184
|
</label>
|
|
183
185
|
|
|
186
|
+
<label className={css.field}>
|
|
187
|
+
<span className={css.fieldLabel}>{t('new.model')}</span>
|
|
188
|
+
<select
|
|
189
|
+
className={css.select}
|
|
190
|
+
value={model}
|
|
191
|
+
onChange={event => { setModel(event.target.value) }}
|
|
192
|
+
>
|
|
193
|
+
<option value="">{t('exec.model.default')}</option>
|
|
194
|
+
{options.models?.map(item => (
|
|
195
|
+
<option key={item.id} value={item.id}>{item.name ?? item.id}</option>
|
|
196
|
+
))}
|
|
197
|
+
</select>
|
|
198
|
+
</label>
|
|
199
|
+
|
|
184
200
|
<section className={css.detailSection}>
|
|
185
201
|
<h4>{t('detail.schedule')}</h4>
|
|
186
202
|
<label className={css.scheduleToggle}>
|
|
@@ -69,11 +69,13 @@ function ExecutionSettingsSection({ controller, task, pending }: { controller: B
|
|
|
69
69
|
const workspaceId = task.workspaceId ?? ''
|
|
70
70
|
const mode = task.mode ?? ''
|
|
71
71
|
const permission = task.permission ?? ''
|
|
72
|
+
const model = task.model ?? ''
|
|
72
73
|
// A pinned target may disappear from the runtime (workspace deleted,
|
|
73
74
|
// preset removed); keep it selectable as a stale row instead of silently
|
|
74
75
|
// dropping it, so the user sees exactly what the task will ask for.
|
|
75
76
|
const workspaceKnown = workspaceId === '' || options.workspaces.some(item => item.workspaceId === workspaceId)
|
|
76
77
|
const modeKnown = mode === '' || options.presets.some(item => item.id === mode)
|
|
78
|
+
const modelKnown = model === '' || (options.models ?? []).some(item => item.id === model)
|
|
77
79
|
return (
|
|
78
80
|
<section className={css.detailSection}>
|
|
79
81
|
<h4>{t('detail.executionSettings')}</h4>
|
|
@@ -126,6 +128,21 @@ function ExecutionSettingsSection({ controller, task, pending }: { controller: B
|
|
|
126
128
|
))}
|
|
127
129
|
</select>
|
|
128
130
|
</label>
|
|
131
|
+
<label className={css.field}>
|
|
132
|
+
<span className={css.fieldLabel}>{t('new.model')}</span>
|
|
133
|
+
<select
|
|
134
|
+
className={css.select}
|
|
135
|
+
value={model}
|
|
136
|
+
disabled={pending}
|
|
137
|
+
onChange={event => { controller.updateTask(task.id, { model: event.target.value === '' ? undefined : event.target.value }) }}
|
|
138
|
+
>
|
|
139
|
+
<option value="">{t('exec.model.default')}</option>
|
|
140
|
+
{!modelKnown && <option value={model}>{model}{t('exec.model.unknown')}</option>}
|
|
141
|
+
{options.models?.map(item => (
|
|
142
|
+
<option key={item.id} value={item.id}>{item.name ?? item.id}</option>
|
|
143
|
+
))}
|
|
144
|
+
</select>
|
|
145
|
+
</label>
|
|
129
146
|
</section>
|
|
130
147
|
)
|
|
131
148
|
}
|
package/src/client/index.ts
CHANGED
|
@@ -249,8 +249,40 @@ export function apply(ctx: ClientContext): void {
|
|
|
249
249
|
console.error('[dsh-task-board] agent preset roster read failed', error)
|
|
250
250
|
}
|
|
251
251
|
}
|
|
252
|
-
|
|
253
|
-
|
|
252
|
+
const pushModelOptions = async (): Promise<void> => {
|
|
253
|
+
try {
|
|
254
|
+
const conn = ctx.get('connection') as { api?: { llm?: { discoverModels?: () => Promise<unknown> }; sessions?: { modelCatalog?: () => Promise<unknown> } } } | undefined
|
|
255
|
+
if (conn?.api) {
|
|
256
|
+
let models: Array<{ id: string; name?: string; provider?: string }> = []
|
|
257
|
+
if (typeof conn.api.llm?.discoverModels === 'function') {
|
|
258
|
+
const res = await conn.api.llm.discoverModels() as { result?: { value?: { models?: Array<{ id: string; name?: string }> } } }
|
|
259
|
+
const list = res?.result?.value?.models
|
|
260
|
+
if (Array.isArray(list)) {
|
|
261
|
+
models = list.map(m => ({ id: m.id, name: m.name }))
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
if (models.length === 0 && typeof conn.api.sessions?.modelCatalog === 'function') {
|
|
265
|
+
const res = await conn.api.sessions.modelCatalog() as { result?: { value?: { groups?: Array<{ provider?: string; models?: Array<{ id: string; name?: string }> }> } } }
|
|
266
|
+
const groups = res?.result?.value?.groups
|
|
267
|
+
if (Array.isArray(groups)) {
|
|
268
|
+
for (const g of groups) {
|
|
269
|
+
for (const m of g.models ?? []) {
|
|
270
|
+
const qualifiedId = g.provider ? `${g.provider}/${m.id}` : m.id
|
|
271
|
+
models.push({ id: qualifiedId, name: m.name ?? m.id, provider: g.provider })
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
if (models.length > 0) {
|
|
277
|
+
controller.setExecutionOptions({ models })
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
} catch (error) {
|
|
281
|
+
console.error('[dsh-task-board] model options read failed', error)
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
void pushModelOptions()
|
|
285
|
+
disposers.push(ctx.on('connection/reset', () => { void pushModelOptions() }))
|
|
254
286
|
try {
|
|
255
287
|
disposers.push(mountSidebarEntry(controller, ctx.locale))
|
|
256
288
|
disposers.push(mountBoard(controller, ctx.locale))
|
package/src/client/locales.ts
CHANGED
|
@@ -109,6 +109,7 @@ export const zh = {
|
|
|
109
109
|
'new.workspace': '工作区',
|
|
110
110
|
'new.mode': '模式',
|
|
111
111
|
'new.permission': '权限',
|
|
112
|
+
'new.model': '模型',
|
|
112
113
|
'exec.workspace.recent': '最近使用(默认)',
|
|
113
114
|
'exec.mode.default': '部署默认',
|
|
114
115
|
'exec.mode.defaultSuffix': '(默认)',
|
|
@@ -118,6 +119,8 @@ export const zh = {
|
|
|
118
119
|
'exec.permission.read-only': '只读',
|
|
119
120
|
'exec.permission.workspace-write': '工作区可写',
|
|
120
121
|
'exec.permission.danger-full-access': '完全访问',
|
|
122
|
+
'exec.model.default': '宿主默认(agent-default-model)',
|
|
123
|
+
'exec.model.unknown': '(未知模型/回退默认)',
|
|
121
124
|
'detail.executionSettings': '执行设置',
|
|
122
125
|
'exec.hint': '执行时生效:工作区决定执行会话落在哪个工作区;模式决定会话的 agent 预设;权限经 /permission 命令应用到会话。留空则使用运行时默认。',
|
|
123
126
|
// 插件设置卡片(settings.plugin.item 席位)。
|
|
@@ -254,6 +257,7 @@ export const en: Record<keyof typeof zh, string> = {
|
|
|
254
257
|
'new.workspace': 'Workspace',
|
|
255
258
|
'new.mode': 'Mode',
|
|
256
259
|
'new.permission': 'Permission',
|
|
260
|
+
'new.model': 'Model',
|
|
257
261
|
'exec.workspace.recent': 'Most recent (default)',
|
|
258
262
|
'exec.mode.default': 'Deployment default',
|
|
259
263
|
'exec.mode.defaultSuffix': ' (default)',
|
|
@@ -263,6 +267,8 @@ export const en: Record<keyof typeof zh, string> = {
|
|
|
263
267
|
'exec.permission.read-only': 'Read-only',
|
|
264
268
|
'exec.permission.workspace-write': 'Workspace Write',
|
|
265
269
|
'exec.permission.danger-full-access': 'Full Access',
|
|
270
|
+
'exec.model.default': 'Host default (agent-default-model)',
|
|
271
|
+
'exec.model.unknown': ' (unknown / fallback to default)',
|
|
266
272
|
'detail.executionSettings': 'Execution Settings',
|
|
267
273
|
'exec.hint': 'Applied when the task runs: the workspace decides where the execution session lands; the mode composes the session\'s agent preset; the permission is applied through the /permission command. Blank = runtime default.',
|
|
268
274
|
// Plugin settings card (the `settings.plugin.item` seat).
|
package/src/core/controller.ts
CHANGED
|
@@ -74,10 +74,19 @@ export interface ExecutionPresetOption {
|
|
|
74
74
|
isDefault: boolean
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
/** One model option the execution-target pickers offer. */
|
|
78
|
+
export interface ExecutionModelOption {
|
|
79
|
+
id: string
|
|
80
|
+
name?: string
|
|
81
|
+
label?: string
|
|
82
|
+
provider?: string
|
|
83
|
+
}
|
|
84
|
+
|
|
77
85
|
/** The execution-target option sets the UI feeds into the controller. */
|
|
78
86
|
export interface ExecutionOptionsSnapshot {
|
|
79
87
|
workspaces: readonly ExecutionWorkspaceOption[]
|
|
80
88
|
presets: readonly ExecutionPresetOption[]
|
|
89
|
+
models?: readonly ExecutionModelOption[]
|
|
81
90
|
}
|
|
82
91
|
|
|
83
92
|
/** Immutable controller snapshot for UI subscriptions. */
|
|
@@ -129,7 +138,7 @@ export class BoardController {
|
|
|
129
138
|
private boardOpen = false
|
|
130
139
|
private archiveView = false
|
|
131
140
|
private selectedTaskId: string | undefined
|
|
132
|
-
private executionOptions: ExecutionOptionsSnapshot = { workspaces: [], presets: [] }
|
|
141
|
+
private executionOptions: ExecutionOptionsSnapshot = { workspaces: [], presets: [], models: [] }
|
|
133
142
|
private listeners = new Set<() => void>()
|
|
134
143
|
private disposers: Array<() => void> = []
|
|
135
144
|
private readonly now: () => number
|
package/src/core/tasks.ts
CHANGED
|
@@ -132,6 +132,11 @@ export interface TaskRecord {
|
|
|
132
132
|
* `/permission <id>` slash command; absent leaves the session default.
|
|
133
133
|
*/
|
|
134
134
|
permission?: TaskPermission
|
|
135
|
+
/**
|
|
136
|
+
* Pinned model selection for the execution session (format: "provider/model" or model id);
|
|
137
|
+
* absent falls back to the host default (agent-default-model).
|
|
138
|
+
*/
|
|
139
|
+
model?: string
|
|
135
140
|
/**
|
|
136
141
|
* Frozen context snapshot for a continuation card; absent on plain tasks.
|
|
137
142
|
* Sanitized before it enters the ledger (redaction, slash-command taint,
|
|
@@ -185,6 +190,8 @@ export interface NewTaskInput {
|
|
|
185
190
|
mode?: string
|
|
186
191
|
/** Permission preset applied to the execution session; absent = session default. */
|
|
187
192
|
permission?: TaskPermission
|
|
193
|
+
/** Optional pinned model for the execution session; absent = host default. */
|
|
194
|
+
model?: string
|
|
188
195
|
/**
|
|
189
196
|
* Optional scheduled-run rule requested at creation time (the new-task
|
|
190
197
|
* dialog): an enable flag plus a 5-field cron expression. The create use
|
|
@@ -271,6 +278,7 @@ export function createTask(input: NewTaskInput, now: number, id: string): TaskRe
|
|
|
271
278
|
workspaceId: normalizeTargetId(input.workspaceId),
|
|
272
279
|
mode: normalizeTargetId(input.mode),
|
|
273
280
|
permission: isTaskPermission(input.permission) ? input.permission : undefined,
|
|
281
|
+
model: normalizeTargetId(input.model),
|
|
274
282
|
...(input.freeze === undefined ? {} : { freeze: freezeOf(input.freeze, now) }),
|
|
275
283
|
...(input.handover === undefined ? {} : { handover: { ...input.handover, bundledAt: now } }),
|
|
276
284
|
}
|
|
@@ -16,7 +16,7 @@ import type { TaskHandoverInput } from '../handover.ts'
|
|
|
16
16
|
* Editable fields on a task (the update patch surface). `freeze` replaces the
|
|
17
17
|
* continuation-card snapshot (restamping frozenAt); an explicit null clears it.
|
|
18
18
|
*/
|
|
19
|
-
export type TaskUpdatePatch = Partial<Pick<TaskRecord, 'title' | 'description' | 'prompt' | 'workspaceId' | 'mode' | 'permission'>> & {
|
|
19
|
+
export type TaskUpdatePatch = Partial<Pick<TaskRecord, 'title' | 'description' | 'prompt' | 'workspaceId' | 'mode' | 'permission' | 'model'>> & {
|
|
20
20
|
freeze?: FreezeSnapshot & { redacted?: boolean } | null
|
|
21
21
|
/** Replaces the handover bundle (restamping bundledAt); an explicit null clears it. */
|
|
22
22
|
handover?: TaskHandoverInput | null
|
|
@@ -72,6 +72,7 @@ export function applyUpdateTask(
|
|
|
72
72
|
const workspaceId = 'workspaceId' in patch ? normalizeTargetId(patch.workspaceId) : undefined
|
|
73
73
|
const mode = 'mode' in patch ? normalizeTargetId(patch.mode) : undefined
|
|
74
74
|
const permission = 'permission' in patch ? normalizePermission(task.permission, patch.permission) : undefined
|
|
75
|
+
const model = 'model' in patch ? normalizeTargetId(patch.model) : undefined
|
|
75
76
|
const next: TaskRecord = { ...task, ...rest, updatedAt: now }
|
|
76
77
|
// Content fields normalize like creation does (trimmed); an explicit
|
|
77
78
|
// undefined keeps the current value — content cannot be cleared.
|
|
@@ -95,6 +96,7 @@ export function applyUpdateTask(
|
|
|
95
96
|
if (workspaceId !== undefined || 'workspaceId' in patch) next.workspaceId = workspaceId
|
|
96
97
|
if (mode !== undefined || 'mode' in patch) next.mode = mode
|
|
97
98
|
if (permission !== undefined || 'permission' in patch) next.permission = permission
|
|
99
|
+
if (model !== undefined || 'model' in patch) next.model = model
|
|
98
100
|
return next
|
|
99
101
|
})
|
|
100
102
|
}
|
package/src/host-runner.ts
CHANGED
|
@@ -202,6 +202,21 @@ export class HostExecutionRunner {
|
|
|
202
202
|
if (command === undefined) throw new Error('permission command was not acknowledged')
|
|
203
203
|
if (command.kind !== 'success') throw new Error(command.text ?? 'permission command failed')
|
|
204
204
|
}
|
|
205
|
+
if (task.model !== undefined && task.model.trim() !== '') {
|
|
206
|
+
const rawModel = task.model.trim()
|
|
207
|
+
const slashIdx = rawModel.indexOf('/')
|
|
208
|
+
const provider = slashIdx >= 0 ? rawModel.slice(0, slashIdx).trim() : undefined
|
|
209
|
+
const modelId = slashIdx >= 0 ? rawModel.slice(slashIdx + 1).trim() : rawModel
|
|
210
|
+
try {
|
|
211
|
+
await this.invoke('session', 'selectModel', {
|
|
212
|
+
sessionId,
|
|
213
|
+
...(provider ? { provider } : {}),
|
|
214
|
+
model: modelId,
|
|
215
|
+
})
|
|
216
|
+
} catch (modelError) {
|
|
217
|
+
console.warn(`[dsh-task-board] failed to select model "${task.model}" for session ${sessionId}, falling back to default:`, modelError)
|
|
218
|
+
}
|
|
219
|
+
}
|
|
205
220
|
await this.invoke('session', 'prompt', {
|
|
206
221
|
sessionId,
|
|
207
222
|
requestId: 'task-board-' + crypto.randomUUID(),
|
package/src/index.ts
CHANGED
|
@@ -152,10 +152,20 @@ function applyImpl(ctx: Context, config?: Config): void {
|
|
|
152
152
|
}
|
|
153
153
|
|
|
154
154
|
ctx.inject(['settings'], (settingsCtx) => {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
155
|
+
try {
|
|
156
|
+
if (typeof settingsCtx.settings?.installSection === 'function') {
|
|
157
|
+
settingsCtx.settings.installSection(ctx, TASK_BOARD_SETTINGS_NAMESPACE, Config, config ?? {}, {
|
|
158
|
+
setSource: (source) => { current = source },
|
|
159
|
+
onChange: sync,
|
|
160
|
+
})
|
|
161
|
+
} else if (typeof settingsCtx.settings?.register === 'function') {
|
|
162
|
+
const scope = settingsCtx.settings.register(TASK_BOARD_SETTINGS_NAMESPACE, Config, { base: config ?? {} })
|
|
163
|
+
current = () => scope?.get?.() ?? (config ?? {})
|
|
164
|
+
scope?.watch?.(() => { sync() })
|
|
165
|
+
}
|
|
166
|
+
} catch {
|
|
167
|
+
// Defensive fallback against settings registration differences
|
|
168
|
+
}
|
|
159
169
|
})
|
|
160
170
|
|
|
161
171
|
// Initial registration from the composition entry (covers deployments with
|
package/src/protocol.ts
CHANGED
|
@@ -193,9 +193,9 @@ function handoverPayload(value: unknown): TaskHandoverInput | undefined {
|
|
|
193
193
|
|
|
194
194
|
function createInput(value: unknown): value is NewTaskInput {
|
|
195
195
|
const input = record(value)
|
|
196
|
-
if (input === undefined || !exactKeys(input, ['title', 'description', 'prompt', 'workspaceId', 'mode', 'permission', 'schedule', 'freeze', 'handover'])) return false
|
|
196
|
+
if (input === undefined || !exactKeys(input, ['title', 'description', 'prompt', 'workspaceId', 'mode', 'permission', 'schedule', 'freeze', 'handover', 'model'])) return false
|
|
197
197
|
if (typeof input.title !== 'string' || typeof input.description !== 'string' || typeof input.prompt !== 'string') return false
|
|
198
|
-
if (!optionalString(input.workspaceId) || !optionalString(input.mode)) return false
|
|
198
|
+
if (!optionalString(input.workspaceId) || !optionalString(input.mode) || !optionalString(input.model)) return false
|
|
199
199
|
if (input.permission !== undefined && !isTaskPermission(input.permission)) return false
|
|
200
200
|
if (input.freeze !== undefined && freezePayload(input.freeze) === undefined) return false
|
|
201
201
|
if (input.handover !== undefined && handoverPayload(input.handover) === undefined) return false
|
|
@@ -209,8 +209,8 @@ function createInput(value: unknown): value is NewTaskInput {
|
|
|
209
209
|
|
|
210
210
|
function updatePatch(value: unknown): boolean {
|
|
211
211
|
const patch = record(value)
|
|
212
|
-
if (patch === undefined || !exactKeys(patch, ['title', 'description', 'prompt', 'workspaceId', 'mode', 'permission', 'freeze', 'handover'])) return false
|
|
213
|
-
for (const key of ['title', 'description', 'prompt', 'workspaceId', 'mode'] as const) {
|
|
212
|
+
if (patch === undefined || !exactKeys(patch, ['title', 'description', 'prompt', 'workspaceId', 'mode', 'permission', 'freeze', 'handover', 'model'])) return false
|
|
213
|
+
for (const key of ['title', 'description', 'prompt', 'workspaceId', 'mode', 'model'] as const) {
|
|
214
214
|
if (!optionalString(patch[key])) return false
|
|
215
215
|
}
|
|
216
216
|
if (patch.permission !== undefined && !isTaskPermission(patch.permission)) return false
|