dsh-taskboard 0.5.3 → 0.5.5
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.md +13 -156
- package/lib/client.js +313 -37
- package/lib/host/execution.js +2 -1
- package/lib/host/execution.js.map +1 -1
- package/lib/host/session-sync.js +249 -0
- package/lib/host/session-sync.js.map +1 -0
- package/lib/host/tools.js +6 -2
- package/lib/host/tools.js.map +1 -1
- package/lib/index.js +9 -1
- package/lib/index.js.map +1 -1
- package/lib/shared/api.js.map +1 -1
- package/lib/shared/protocol.js +15 -5
- package/lib/shared/protocol.js.map +1 -1
- package/package.json +1 -1
- package/src/client/board/SettingsModal.tsx +45 -9
- package/src/client/board/TaskBoard.tsx +2 -0
- package/src/client/board/TaskCard.tsx +43 -1
- package/src/client/board/TaskDetail.tsx +32 -6
- package/src/client/board/TaskFormModal.tsx +114 -6
- package/src/client/board/TemplateManager.tsx +4 -1
- package/src/client/controller.ts +29 -5
- package/src/client/index.ts +36 -5
- package/src/client/styles.ts +86 -8
- package/src/host/execution.ts +11 -4
- package/src/host/session-sync.ts +330 -0
- package/src/host/tools.ts +3 -2
- package/src/index.ts +11 -1
- package/src/shared/api.ts +7 -5
- package/src/shared/protocol.ts +20 -4
- package/src/shared/version.ts +1 -1
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* External workspace session synchronization service.
|
|
3
|
+
*
|
|
4
|
+
* When `settings.syncExternalSessions` is enabled (0.5.4):
|
|
5
|
+
* - Listens to session lifecycle events from outside the taskboard.
|
|
6
|
+
* - On `turn/start`: automatically captures or resumes the session on the board
|
|
7
|
+
* (status: `in_progress`, claimedBy: sessionId).
|
|
8
|
+
* - On `user/message` / `session/title`: enriches/updates task title & description.
|
|
9
|
+
* - On `turn/end`: settles the execution (success -> `in_review` 待验收, failure -> `todo`).
|
|
10
|
+
*
|
|
11
|
+
* @module dsh-taskboard/host/session-sync
|
|
12
|
+
*/
|
|
13
|
+
import {
|
|
14
|
+
defaultSyncExternalSessionsOf,
|
|
15
|
+
newCommentId,
|
|
16
|
+
newExecutionId,
|
|
17
|
+
newTaskId,
|
|
18
|
+
normalizeBody,
|
|
19
|
+
normalizeTitle,
|
|
20
|
+
type TaskRecord,
|
|
21
|
+
} from '../shared/protocol.ts'
|
|
22
|
+
import type { EventsFace } from './execution.ts'
|
|
23
|
+
import type { TaskStore } from './store.ts'
|
|
24
|
+
import type { WorkspaceFace } from './tools.ts'
|
|
25
|
+
|
|
26
|
+
/** Extract text content from a user message event payload. */
|
|
27
|
+
export function extractUserMessageText(msg: unknown): string {
|
|
28
|
+
if (typeof msg !== 'object' || msg === null) return ''
|
|
29
|
+
const content = (msg as { content?: unknown }).content
|
|
30
|
+
if (typeof content === 'string') return content
|
|
31
|
+
if (Array.isArray(content)) {
|
|
32
|
+
return content
|
|
33
|
+
.map(part => {
|
|
34
|
+
if (typeof part === 'string') return part
|
|
35
|
+
if (typeof part === 'object' && part !== null && 'text' in part && typeof (part as { text: unknown }).text === 'string') {
|
|
36
|
+
return (part as { text: string }).text
|
|
37
|
+
}
|
|
38
|
+
return ''
|
|
39
|
+
})
|
|
40
|
+
.filter(Boolean)
|
|
41
|
+
.join('\n')
|
|
42
|
+
}
|
|
43
|
+
return ''
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Extract a short one-line title from prompt text. */
|
|
47
|
+
export function titleFromText(text: string): string {
|
|
48
|
+
const clean = text.trim().replace(/^#+\s*/, '')
|
|
49
|
+
const firstLine = clean.split('\n')[0]?.trim() ?? ''
|
|
50
|
+
return firstLine.slice(0, 50).trim()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Dependencies required by the external session sync service. */
|
|
54
|
+
export interface SessionSyncDeps {
|
|
55
|
+
store: TaskStore
|
|
56
|
+
workspaces: WorkspaceFace
|
|
57
|
+
events: EventsFace
|
|
58
|
+
now: () => number
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Service that synchronizes external workspace sessions into the taskboard.
|
|
63
|
+
*/
|
|
64
|
+
export class ExternalSessionSyncService {
|
|
65
|
+
private readonly unsubscribe: () => void
|
|
66
|
+
|
|
67
|
+
constructor(private readonly deps: SessionSyncDeps) {
|
|
68
|
+
this.unsubscribe = deps.events.onSessionEvent((sessionId, event, sessionMeta) => {
|
|
69
|
+
void this.handleSessionEvent(sessionId, event, sessionMeta)
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Detach listener on teardown. */
|
|
74
|
+
dispose(): void {
|
|
75
|
+
this.unsubscribe()
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private async handleSessionEvent(
|
|
79
|
+
sessionId: string,
|
|
80
|
+
event: { type: string; data?: unknown },
|
|
81
|
+
sessionMeta?: { header?: { cwd?: string } },
|
|
82
|
+
): Promise<void> {
|
|
83
|
+
// 1. Ignore taskboard's internal execution sessions
|
|
84
|
+
if (sessionId.startsWith('session-taskboard-')) return
|
|
85
|
+
|
|
86
|
+
// 2. Check if external session sync is enabled in board settings
|
|
87
|
+
const snapshot = this.deps.store.snapshot()
|
|
88
|
+
if (!defaultSyncExternalSessionsOf(snapshot.settings)) return
|
|
89
|
+
|
|
90
|
+
const now = this.deps.now()
|
|
91
|
+
|
|
92
|
+
if (event.type === 'turn/start') {
|
|
93
|
+
await this.handleTurnStart(sessionId, sessionMeta?.header?.cwd, now)
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (event.type === 'user/message') {
|
|
98
|
+
await this.handleUserMessage(sessionId, event.data, now)
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (event.type === 'session/title') {
|
|
103
|
+
await this.handleSessionTitle(sessionId, event.data, now)
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (event.type === 'turn/end') {
|
|
108
|
+
await this.handleTurnEnd(sessionId, event.data, now)
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private async handleTurnStart(sessionId: string, cwd: string | undefined, now: number): Promise<void> {
|
|
114
|
+
// Resolve workspace
|
|
115
|
+
let wsId: string | undefined
|
|
116
|
+
if (cwd !== undefined && cwd.length > 0) {
|
|
117
|
+
const resolved = await this.deps.workspaces.resolveByPath(cwd)
|
|
118
|
+
wsId = resolved?.id
|
|
119
|
+
}
|
|
120
|
+
if (wsId === undefined) {
|
|
121
|
+
wsId = this.deps.workspaces.list()[0]?.id ?? 'default'
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
await this.deps.store.mutate('task-created', (ledger) => {
|
|
125
|
+
// Find existing task linked to this session
|
|
126
|
+
const existing = ledger.tasks.find(
|
|
127
|
+
t => t.claimedBy === sessionId || t.executions.some(e => e.sessionId === sessionId),
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
if (existing !== undefined) {
|
|
131
|
+
if (existing.trashedAt !== undefined) return undefined
|
|
132
|
+
// If already in_progress and holding claim, ensure running execution
|
|
133
|
+
if (existing.status === 'in_progress' && existing.claimedBy === sessionId) {
|
|
134
|
+
const hasRunning = existing.executions.some(e => e.sessionId === sessionId && e.outcome === 'running')
|
|
135
|
+
if (!hasRunning) {
|
|
136
|
+
existing.executions.push({
|
|
137
|
+
id: newExecutionId(),
|
|
138
|
+
sessionId,
|
|
139
|
+
trigger: 'manual',
|
|
140
|
+
startedAt: now,
|
|
141
|
+
outcome: 'running',
|
|
142
|
+
isolation: 'none',
|
|
143
|
+
})
|
|
144
|
+
existing.updatedAt = now
|
|
145
|
+
existing.updatedBy = { kind: 'agent', sessionId }
|
|
146
|
+
return [existing]
|
|
147
|
+
}
|
|
148
|
+
return undefined
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Resumed or continued turn (e.g. from in_review or todo)
|
|
152
|
+
existing.status = 'in_progress'
|
|
153
|
+
existing.claimedBy = sessionId
|
|
154
|
+
existing.claimedAt = now
|
|
155
|
+
existing.updatedAt = now
|
|
156
|
+
existing.updatedBy = { kind: 'agent', sessionId }
|
|
157
|
+
existing.executions.push({
|
|
158
|
+
id: newExecutionId(),
|
|
159
|
+
sessionId,
|
|
160
|
+
trigger: 'manual',
|
|
161
|
+
startedAt: now,
|
|
162
|
+
outcome: 'running',
|
|
163
|
+
isolation: 'none',
|
|
164
|
+
})
|
|
165
|
+
return [existing]
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Create new task for this external session
|
|
169
|
+
const shortId = sessionId.replace(/^session-/, '').slice(0, 8)
|
|
170
|
+
const newTask: TaskRecord = {
|
|
171
|
+
id: newTaskId(),
|
|
172
|
+
title: `会话 ${shortId}`,
|
|
173
|
+
description: '',
|
|
174
|
+
prompt: '',
|
|
175
|
+
workspaceId: wsId,
|
|
176
|
+
urgency: 'normal',
|
|
177
|
+
status: 'in_progress',
|
|
178
|
+
blocked: false,
|
|
179
|
+
execution: { mode: 'claim' },
|
|
180
|
+
isolation: 'none',
|
|
181
|
+
claimedBy: sessionId,
|
|
182
|
+
claimedAt: now,
|
|
183
|
+
version: 1,
|
|
184
|
+
createdAt: now,
|
|
185
|
+
updatedAt: now,
|
|
186
|
+
createdBy: { kind: 'agent', sessionId },
|
|
187
|
+
updatedBy: { kind: 'agent', sessionId },
|
|
188
|
+
comments: [],
|
|
189
|
+
executions: [
|
|
190
|
+
{
|
|
191
|
+
id: newExecutionId(),
|
|
192
|
+
sessionId,
|
|
193
|
+
trigger: 'manual',
|
|
194
|
+
startedAt: now,
|
|
195
|
+
outcome: 'running',
|
|
196
|
+
isolation: 'none',
|
|
197
|
+
},
|
|
198
|
+
],
|
|
199
|
+
}
|
|
200
|
+
ledger.tasks.push(newTask)
|
|
201
|
+
return [newTask]
|
|
202
|
+
})
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
private async handleUserMessage(sessionId: string, msgData: unknown, now: number): Promise<void> {
|
|
206
|
+
const text = extractUserMessageText(msgData)
|
|
207
|
+
if (text.trim().length === 0) return
|
|
208
|
+
|
|
209
|
+
await this.deps.store.mutate('task-updated', (ledger) => {
|
|
210
|
+
const task = ledger.tasks.find(
|
|
211
|
+
t => t.claimedBy === sessionId || t.executions.some(e => e.sessionId === sessionId),
|
|
212
|
+
)
|
|
213
|
+
if (task === undefined || task.trashedAt !== undefined) return undefined
|
|
214
|
+
|
|
215
|
+
let changed = false
|
|
216
|
+
// If title is default placeholder "会话 ...", replace with prompt summary
|
|
217
|
+
if (task.title.startsWith('会话 ') && task.title.length <= 16) {
|
|
218
|
+
const derived = titleFromText(text)
|
|
219
|
+
if (derived.length > 0) {
|
|
220
|
+
task.title = normalizeTitle(derived)
|
|
221
|
+
changed = true
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
// If description is empty, record initial prompt
|
|
225
|
+
if (task.description.length === 0) {
|
|
226
|
+
task.description = text.slice(0, 2000)
|
|
227
|
+
changed = true
|
|
228
|
+
}
|
|
229
|
+
if (changed) {
|
|
230
|
+
task.updatedAt = now
|
|
231
|
+
task.updatedBy = { kind: 'user' }
|
|
232
|
+
return [task]
|
|
233
|
+
}
|
|
234
|
+
return undefined
|
|
235
|
+
})
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
private async handleSessionTitle(sessionId: string, titleData: unknown, now: number): Promise<void> {
|
|
239
|
+
const rawTitle = typeof titleData === 'object' && titleData !== null && 'title' in titleData && typeof (titleData as { title: unknown }).title === 'string'
|
|
240
|
+
? (titleData as { title: string }).title
|
|
241
|
+
: typeof titleData === 'string'
|
|
242
|
+
? titleData
|
|
243
|
+
: ''
|
|
244
|
+
if (rawTitle.trim().length === 0) return
|
|
245
|
+
|
|
246
|
+
await this.deps.store.mutate('task-updated', (ledger) => {
|
|
247
|
+
const task = ledger.tasks.find(
|
|
248
|
+
t => t.claimedBy === sessionId || t.executions.some(e => e.sessionId === sessionId),
|
|
249
|
+
)
|
|
250
|
+
if (task === undefined || task.trashedAt !== undefined) return undefined
|
|
251
|
+
task.title = normalizeTitle(rawTitle)
|
|
252
|
+
task.updatedAt = now
|
|
253
|
+
task.updatedBy = { kind: 'user' }
|
|
254
|
+
return [task]
|
|
255
|
+
})
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
private async handleTurnEnd(sessionId: string, endData: unknown, now: number): Promise<void> {
|
|
259
|
+
const reason = typeof endData === 'object' && endData !== null && 'reason' in endData
|
|
260
|
+
? (endData as { reason: unknown }).reason
|
|
261
|
+
: endData
|
|
262
|
+
|
|
263
|
+
// Check if error or failure
|
|
264
|
+
let isFailure = false
|
|
265
|
+
let errorMessage = ''
|
|
266
|
+
if (typeof reason === 'object' && reason !== null) {
|
|
267
|
+
const r = reason as Record<string, unknown>
|
|
268
|
+
if (r.kind === 'error' || r.kind === 'failure') {
|
|
269
|
+
isFailure = true
|
|
270
|
+
errorMessage = typeof r.error === 'string' ? r.error : typeof r.message === 'string' ? r.message : 'turn error'
|
|
271
|
+
} else if (r.kind === 'cancel') {
|
|
272
|
+
isFailure = true
|
|
273
|
+
errorMessage = 'cancelled'
|
|
274
|
+
}
|
|
275
|
+
} else if (typeof reason === 'string' && (reason.includes('error') || reason.includes('fail'))) {
|
|
276
|
+
isFailure = true
|
|
277
|
+
errorMessage = reason
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
await this.deps.store.mutate('execution-recorded', (ledger) => {
|
|
281
|
+
const task = ledger.tasks.find(
|
|
282
|
+
t => t.claimedBy === sessionId || t.executions.some(e => e.sessionId === sessionId),
|
|
283
|
+
)
|
|
284
|
+
if (task === undefined || task.trashedAt !== undefined) return undefined
|
|
285
|
+
|
|
286
|
+
// Settle running execution
|
|
287
|
+
for (const exec of task.executions) {
|
|
288
|
+
if (exec.sessionId === sessionId && exec.outcome === 'running') {
|
|
289
|
+
exec.endedAt = now
|
|
290
|
+
if (isFailure) {
|
|
291
|
+
exec.outcome = 'failed'
|
|
292
|
+
exec.error = errorMessage.slice(0, 500)
|
|
293
|
+
} else {
|
|
294
|
+
exec.outcome = 'succeeded'
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
delete task.claimedBy
|
|
300
|
+
delete task.claimedAt
|
|
301
|
+
task.updatedAt = now
|
|
302
|
+
task.updatedBy = { kind: 'agent', sessionId }
|
|
303
|
+
|
|
304
|
+
if (isFailure) {
|
|
305
|
+
// Failed session hands back to todo with comment
|
|
306
|
+
if (task.status === 'in_progress') {
|
|
307
|
+
task.status = 'todo'
|
|
308
|
+
task.comments.push({
|
|
309
|
+
id: newCommentId(),
|
|
310
|
+
body: normalizeBody(`[系统] 会话执行异常:${errorMessage.slice(0, 300)};任务已退回待办。`),
|
|
311
|
+
version: 1,
|
|
312
|
+
createdAt: now,
|
|
313
|
+
})
|
|
314
|
+
}
|
|
315
|
+
} else {
|
|
316
|
+
// Successful settlement automatically moves to in_review (待验收)
|
|
317
|
+
if (task.status === 'in_progress') {
|
|
318
|
+
task.status = 'in_review'
|
|
319
|
+
task.comments.push({
|
|
320
|
+
id: newCommentId(),
|
|
321
|
+
body: normalizeBody('[系统] 会话执行完毕,已自动进入待验收。'),
|
|
322
|
+
version: 1,
|
|
323
|
+
createdAt: now,
|
|
324
|
+
})
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return [task]
|
|
328
|
+
})
|
|
329
|
+
}
|
|
330
|
+
}
|
package/src/host/tools.ts
CHANGED
|
@@ -91,7 +91,7 @@ function taskDetail(t: TaskRecord & { effectivePrompt?: string }): string {
|
|
|
91
91
|
const holder = isClaimedBy(t)
|
|
92
92
|
if (holder !== undefined) lines.push(`认领: agent ${String(holder).slice(0, 24)}(持有期间其他会话不可移动)`)
|
|
93
93
|
if (t.execution.nextRunAt !== undefined) lines.push(`下次触发: ${new Date(t.execution.nextRunAt).toISOString()}`)
|
|
94
|
-
if (t.model !== undefined) lines.push(`固定模型: ${t.model.provider}/${t.model.model}`)
|
|
94
|
+
if (t.model !== undefined) lines.push(`固定模型: ${t.model.provider}/${t.model.model}${t.model.reasoningEffort !== undefined ? ` (思考强度: ${t.model.reasoningEffort})` : ''}`)
|
|
95
95
|
if (t.presetId !== undefined) lines.push(`执行模式: ${t.presetId}(未指定时为部署默认 preset)`)
|
|
96
96
|
lines.push(`描述: ${t.description.length > 0 ? t.description : '(无)'}`)
|
|
97
97
|
lines.push(`执行 Prompt: ${t.effectivePrompt ?? effectivePrompt(t)}`)
|
|
@@ -383,10 +383,11 @@ export function registerTaskboardTools(ctx: ToolContextFace, deps: ToolDeps): Ar
|
|
|
383
383
|
model: {
|
|
384
384
|
type: 'object',
|
|
385
385
|
additionalProperties: false,
|
|
386
|
-
description: 'Pin executions to one configured model: { provider, model }. Omit to use the default model.',
|
|
386
|
+
description: 'Pin executions to one configured model: { provider, model, reasoningEffort? }. Omit to use the default model.',
|
|
387
387
|
properties: {
|
|
388
388
|
provider: { type: 'string', description: 'Provider route id.' },
|
|
389
389
|
model: { type: 'string', description: 'Provider-owned model id.' },
|
|
390
|
+
reasoningEffort: { type: 'string', description: 'Optional thinking intensity / reasoning effort (e.g. low, medium, high).' },
|
|
390
391
|
},
|
|
391
392
|
},
|
|
392
393
|
isolation: {
|
package/src/index.ts
CHANGED
|
@@ -27,6 +27,7 @@ import { SchedulerService } from './host/scheduler.ts'
|
|
|
27
27
|
import { dshHomePath } from './host/sdk.ts'
|
|
28
28
|
import { TaskStore } from './host/store.ts'
|
|
29
29
|
import { TemplateStore } from './host/templates.ts'
|
|
30
|
+
import { ExternalSessionSyncService } from './host/session-sync.ts'
|
|
30
31
|
import { registerTaskboardTools, workspaceFace } from './host/tools.ts'
|
|
31
32
|
|
|
32
33
|
/** Ledger file name under the DSH home. */
|
|
@@ -93,10 +94,19 @@ export function apply(ctx: Context): void {
|
|
|
93
94
|
// Settlement listener over the session event bus.
|
|
94
95
|
const events: EventsFace = {
|
|
95
96
|
onSessionEvent: (listener) => wsCtx.on('session/event', (session, event) => {
|
|
96
|
-
listener(session.id, event as { type: string; data?: unknown })
|
|
97
|
+
listener(session.id, event as { type: string; data?: unknown }, session as { header?: { cwd?: string } })
|
|
97
98
|
}),
|
|
98
99
|
}
|
|
99
100
|
|
|
101
|
+
// External workspace sessions sync service (0.5.4).
|
|
102
|
+
const sessionSync = new ExternalSessionSyncService({
|
|
103
|
+
store,
|
|
104
|
+
workspaces: workspaceFace(wsCtx.workspaceRegistry),
|
|
105
|
+
events,
|
|
106
|
+
now,
|
|
107
|
+
})
|
|
108
|
+
disposers.push(() => sessionSync.dispose())
|
|
109
|
+
|
|
100
110
|
// The narrow git face shared by execution (worktree isolation) and the
|
|
101
111
|
// routes (merge / remove / workspace detection).
|
|
102
112
|
const git = createGitFace()
|
package/src/shared/api.ts
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
*
|
|
6
6
|
* @module dsh-taskboard/shared/api
|
|
7
7
|
*/
|
|
8
|
-
import type { BoardSettings, TaskLedger, TaskRecord, TaskSummary } from './protocol.ts'
|
|
8
|
+
import type { BoardSettings, TaskLedger, TaskModel, TaskRecord, TaskSummary } from './protocol.ts'
|
|
9
9
|
|
|
10
|
-
export type { TaskRecord }
|
|
10
|
+
export type { TaskModel, TaskRecord }
|
|
11
11
|
|
|
12
12
|
/** Route prefix on the shared DSH webserver (same origin as the GUI). */
|
|
13
13
|
export const ROUTE_PREFIX = '/dsh-taskboard'
|
|
@@ -51,7 +51,7 @@ export type CreateTaskBody = {
|
|
|
51
51
|
description?: string
|
|
52
52
|
prompt?: string
|
|
53
53
|
execution?: { mode?: string; cron?: string }
|
|
54
|
-
model?:
|
|
54
|
+
model?: TaskModel
|
|
55
55
|
/** Code isolation for executions ('worktree' | 'none'); omitted = default. */
|
|
56
56
|
isolation?: string
|
|
57
57
|
/** Agent preset for execution sessions; omitted = deployment default. */
|
|
@@ -71,7 +71,7 @@ export type UpdateTaskBody = {
|
|
|
71
71
|
/** Rebind the task to another project (GUI owner surface only). */
|
|
72
72
|
workspaceId?: string
|
|
73
73
|
execution?: { mode?: string; cron?: string }
|
|
74
|
-
model?:
|
|
74
|
+
model?: TaskModel | null
|
|
75
75
|
/** Change isolation; locked once the task has execution history. */
|
|
76
76
|
isolation?: string
|
|
77
77
|
/** Change the execution preset (takes effect on the next run). */
|
|
@@ -130,7 +130,7 @@ export type TaskTemplateSpec = {
|
|
|
130
130
|
prompt?: string
|
|
131
131
|
urgency?: string
|
|
132
132
|
execution?: { mode?: string; cron?: string }
|
|
133
|
-
model?:
|
|
133
|
+
model?: TaskModel
|
|
134
134
|
isolation?: string
|
|
135
135
|
presetId?: string
|
|
136
136
|
/** Checklist item texts (host mints ids at create time). */
|
|
@@ -158,6 +158,8 @@ export type SettingsResponse = BoardSettings
|
|
|
158
158
|
export type UpdateSettingsBody = {
|
|
159
159
|
/** Default code isolation for NEW tasks ('worktree' | 'none'). */
|
|
160
160
|
defaultIsolation?: string
|
|
161
|
+
/** Automatically capture external workspace sessions into the taskboard. */
|
|
162
|
+
syncExternalSessions?: boolean
|
|
161
163
|
}
|
|
162
164
|
|
|
163
165
|
/** Import dry-run response (0.4.0): every task classified, nothing written. */
|
package/src/shared/protocol.ts
CHANGED
|
@@ -141,6 +141,8 @@ export function effectiveIsolation(task: Pick<TaskRecord, 'isolation'>): Isolati
|
|
|
141
141
|
export type BoardSettings = {
|
|
142
142
|
/** Default code isolation applied when a NEW task is created without an explicit choice. */
|
|
143
143
|
defaultIsolation?: IsolationMode
|
|
144
|
+
/** Automatically capture external workspace sessions into the taskboard (default: false). */
|
|
145
|
+
syncExternalSessions?: boolean
|
|
144
146
|
}
|
|
145
147
|
|
|
146
148
|
/** Validate raw input into sanitized {@link BoardSettings} (unknown fields dropped). */
|
|
@@ -156,6 +158,12 @@ export function asBoardSettings(raw: unknown): BoardSettings {
|
|
|
156
158
|
}
|
|
157
159
|
out.defaultIsolation = asIsolation(e.defaultIsolation)
|
|
158
160
|
}
|
|
161
|
+
if (e.syncExternalSessions !== undefined) {
|
|
162
|
+
if (typeof e.syncExternalSessions !== 'boolean') {
|
|
163
|
+
throw new Error('syncExternalSessions must be a boolean')
|
|
164
|
+
}
|
|
165
|
+
out.syncExternalSessions = e.syncExternalSessions
|
|
166
|
+
}
|
|
159
167
|
return out
|
|
160
168
|
}
|
|
161
169
|
|
|
@@ -164,6 +172,11 @@ export function defaultIsolationOf(settings?: BoardSettings): IsolationMode {
|
|
|
164
172
|
return settings?.defaultIsolation ?? DEFAULT_ISOLATION
|
|
165
173
|
}
|
|
166
174
|
|
|
175
|
+
/** The effective external session sync switch (board setting → factory default false). */
|
|
176
|
+
export function defaultSyncExternalSessionsOf(settings?: BoardSettings): boolean {
|
|
177
|
+
return settings?.syncExternalSessions ?? false
|
|
178
|
+
}
|
|
179
|
+
|
|
167
180
|
/** How a task may run. */
|
|
168
181
|
export type ExecutionMode = 'claim' | 'scheduled'
|
|
169
182
|
|
|
@@ -378,6 +391,8 @@ export type ExecutionRecord = {
|
|
|
378
391
|
export type TaskModel = {
|
|
379
392
|
provider: string
|
|
380
393
|
model: string
|
|
394
|
+
/** Thinking intensity / reasoning effort (optional; e.g. 'low', 'medium', 'high', 'none'). */
|
|
395
|
+
reasoningEffort?: string
|
|
381
396
|
}
|
|
382
397
|
|
|
383
398
|
/** One task on the board. */
|
|
@@ -638,8 +653,8 @@ export function syncClaim(task: TaskRecord, to: TaskStatus, now: number, holder?
|
|
|
638
653
|
}
|
|
639
654
|
|
|
640
655
|
/**
|
|
641
|
-
* Validate and normalize a pinned model: `{ provider, model }`,
|
|
642
|
-
* non-empty trimmed strings.
|
|
656
|
+
* Validate and normalize a pinned model: `{ provider, model, reasoningEffort? }`,
|
|
657
|
+
* provider and model must be non-empty trimmed strings.
|
|
643
658
|
* @param raw - the raw input.
|
|
644
659
|
* @returns the normalized model.
|
|
645
660
|
* @throws when the shape or the fields are invalid.
|
|
@@ -648,7 +663,7 @@ export function normalizeModel(raw: unknown): TaskModel {
|
|
|
648
663
|
if (typeof raw !== 'object' || raw === null) {
|
|
649
664
|
throw new Error('model must be { provider: string, model: string }')
|
|
650
665
|
}
|
|
651
|
-
const { provider, model } = raw as { provider?: unknown; model?: unknown }
|
|
666
|
+
const { provider, model, reasoningEffort } = raw as { provider?: unknown; model?: unknown; reasoningEffort?: unknown }
|
|
652
667
|
if (typeof provider !== 'string' || typeof model !== 'string') {
|
|
653
668
|
throw new Error('model must be { provider: string, model: string }')
|
|
654
669
|
}
|
|
@@ -657,7 +672,8 @@ export function normalizeModel(raw: unknown): TaskModel {
|
|
|
657
672
|
if (p.length === 0 || m.length === 0) {
|
|
658
673
|
throw new Error('model.provider and model.model must be non-empty strings')
|
|
659
674
|
}
|
|
660
|
-
|
|
675
|
+
const eff = typeof reasoningEffort === 'string' && reasoningEffort.trim().length > 0 ? reasoningEffort.trim() : undefined
|
|
676
|
+
return { provider: p, model: m, ...(eff !== undefined ? { reasoningEffort: eff } : {}) }
|
|
661
677
|
}
|
|
662
678
|
|
|
663
679
|
// ---------------------------------------------------------------------------
|
package/src/shared/version.ts
CHANGED