dsh-taskboard 0.5.3 → 0.5.4

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.
@@ -16,7 +16,51 @@ import { MAX_CHECKLIST_ITEMS, defaultIsolationOf, nextCronTime, parseCron } from
16
16
  import { fmtTime } from './format.ts'
17
17
 
18
18
  /** One row of the configured model catalog (from llm.models). */
19
- export interface CatalogModel { provider: string; model: string; name?: string }
19
+ export interface CatalogModel {
20
+ provider: string
21
+ model: string
22
+ name?: string
23
+ reasoning?: {
24
+ efforts: Array<{ id: string; name: string; description?: string }>
25
+ defaultEffort?: string
26
+ }
27
+ }
28
+
29
+ /** Local storage key for remembering the last selected model in create mode. */
30
+ export const LAST_MODEL_KEY = 'dsh-taskboard-last-model-v1'
31
+
32
+ /** Read the remembered model from localStorage. */
33
+ export function loadLastModel(): { provider: string; model: string; reasoningEffort?: string } | undefined {
34
+ try {
35
+ const raw = localStorage.getItem(LAST_MODEL_KEY)
36
+ if (raw === null) return undefined
37
+ const parsed = JSON.parse(raw) as unknown
38
+ if (typeof parsed === 'object' && parsed !== null) {
39
+ const { provider, model, reasoningEffort } = parsed as { provider?: unknown; model?: unknown; reasoningEffort?: unknown }
40
+ if (typeof provider === 'string' && typeof model === 'string' && provider.trim().length > 0 && model.trim().length > 0) {
41
+ return {
42
+ provider: provider.trim(),
43
+ model: model.trim(),
44
+ ...(typeof reasoningEffort === 'string' && reasoningEffort.trim().length > 0 ? { reasoningEffort: reasoningEffort.trim() } : {}),
45
+ }
46
+ }
47
+ }
48
+ return undefined
49
+ } catch {
50
+ return undefined
51
+ }
52
+ }
53
+
54
+ /** Save the remembered model to localStorage. */
55
+ export function saveLastModel(model?: { provider: string; model: string; reasoningEffort?: string }): void {
56
+ try {
57
+ if (model === undefined) {
58
+ localStorage.removeItem(LAST_MODEL_KEY)
59
+ } else {
60
+ localStorage.setItem(LAST_MODEL_KEY, JSON.stringify(model))
61
+ }
62
+ } catch { /* storage unavailable */ }
63
+ }
20
64
 
21
65
  /** Urgency segmented options with a one-line hint each. */
22
66
  const URGENCY_OPTIONS: ReadonlyArray<{ value: Urgency; label: string; hint: string }> = [
@@ -126,7 +170,12 @@ export function TaskFormModal({ controller, task }: { controller: BoardControlle
126
170
  const [mode, setMode] = useState<'claim' | 'scheduled'>(task?.execution.mode === 'scheduled' || prefill?.execution?.mode === 'scheduled' ? 'scheduled' : 'claim')
127
171
  const [cron, setCron] = useState(task?.execution.cron ?? prefill?.execution?.cron ?? '0 9 * * *')
128
172
  const [catalog, setCatalog] = useState<CatalogModel[]>([])
129
- const [model, setModel] = useState(task?.model !== undefined || prefill?.model !== undefined ? JSON.stringify(task?.model ?? prefill?.model) : '')
173
+
174
+ // Model & reasoning effort selection:
175
+ // In create mode (when not pinned by template), prefill from remembered last choice.
176
+ const initialModel = task?.model ?? prefill?.model ?? (!editing ? loadLastModel() : undefined)
177
+ const [model, setModel] = useState(initialModel !== undefined ? JSON.stringify({ provider: initialModel.provider, model: initialModel.model }) : '')
178
+ const [reasoningEffort, setReasoningEffort] = useState(initialModel?.reasoningEffort ?? '')
130
179
  // Preset roster (0.3.3): create mode PRE-SELECTS the deployment default
131
180
  // (标准模式 in this deployment); '' = 跟随部署默认 (submit omits the field).
132
181
  const initialPreset = task?.presetId ?? prefill?.presetId ?? ''
@@ -215,9 +264,24 @@ export function TaskFormModal({ controller, task }: { controller: BoardControlle
215
264
  /** Checklist rows with non-empty text (blank rows are dropped on submit). */
216
265
  const filledRows = (): CheckRow[] => checkRows.map(r => ({ ...r, text: r.text.trim() })).filter(r => r.text.length > 0)
217
266
 
267
+ const parsedModel = model !== '' ? (JSON.parse(model) as { provider: string; model: string }) : undefined
268
+ const currentCatalogModel = parsedModel !== undefined ? catalog.find(m => m.provider === parsedModel.provider && m.model === parsedModel.model) : undefined
269
+ const modelReasoning = currentCatalogModel?.reasoning
270
+
271
+ const buildPickedModel = (): { provider: string; model: string; reasoningEffort?: string } | undefined => {
272
+ if (parsedModel === undefined) return undefined
273
+ const eff = reasoningEffort.trim()
274
+ return {
275
+ provider: parsedModel.provider,
276
+ model: parsedModel.model,
277
+ ...(eff.length > 0 ? { reasoningEffort: eff } : {}),
278
+ }
279
+ }
280
+
218
281
  const submit = (): void => {
219
282
  if (!valid || busy) return
220
- const picked = model !== '' ? (JSON.parse(model) as { provider: string; model: string }) : undefined
283
+ const picked = buildPickedModel()
284
+ if (!editing) saveLastModel(picked)
221
285
  const isolationOut = isolationPayload()
222
286
  const presetOut = presetPayload()
223
287
  const rows = filledRows()
@@ -255,7 +319,8 @@ export function TaskFormModal({ controller, task }: { controller: BoardControlle
255
319
  /** Save the form, then immediately trigger a manual run of the task. */
256
320
  const submitAndRun = (): void => {
257
321
  if (!valid || runBlocked || busy) return
258
- const picked = model !== '' ? (JSON.parse(model) as { provider: string; model: string }) : undefined
322
+ const picked = buildPickedModel()
323
+ if (!editing) saveLastModel(picked)
259
324
  const isolationOut = isolationPayload()
260
325
  const presetOut = presetPayload()
261
326
  const rows = filledRows()
@@ -325,7 +390,24 @@ export function TaskFormModal({ controller, task }: { controller: BoardControlle
325
390
  </Field>
326
391
 
327
392
  <Field label="模型(默认 = 会话默认模型)">
328
- <select value={model} onChange={e => setModel(e.target.value)}>
393
+ <select
394
+ value={model}
395
+ onChange={e => {
396
+ const val = e.target.value
397
+ setModel(val)
398
+ if (val === '') {
399
+ setReasoningEffort('')
400
+ } else {
401
+ const pm = JSON.parse(val) as { provider: string; model: string }
402
+ const cm = catalog.find(m => m.provider === pm.provider && m.model === pm.model)
403
+ if (cm?.reasoning?.defaultEffort !== undefined) {
404
+ setReasoningEffort(cm.reasoning.defaultEffort)
405
+ } else {
406
+ setReasoningEffort('')
407
+ }
408
+ }
409
+ }}
410
+ >
329
411
  <option value="">默认模型</option>
330
412
  {catalog.map(m => (
331
413
  <option key={`${m.provider}/${m.model}`} value={JSON.stringify({ provider: m.provider, model: m.model })}>
@@ -335,6 +417,32 @@ export function TaskFormModal({ controller, task }: { controller: BoardControlle
335
417
  </select>
336
418
  </Field>
337
419
 
420
+ {parsedModel !== undefined && (
421
+ <Field label="思考强度(Reasoning Effort)">
422
+ <select
423
+ value={reasoningEffort}
424
+ onChange={e => setReasoningEffort(e.target.value)}
425
+ title="设置模型的思考强度(如 low/medium/high);默认 = 跟随模型/提供商默认"
426
+ >
427
+ <option value="">跟随模型默认{modelReasoning?.defaultEffort !== undefined ? `(当前:${modelReasoning.efforts.find(ef => ef.id === modelReasoning.defaultEffort)?.name ?? modelReasoning.defaultEffort})` : ''}</option>
428
+ {modelReasoning !== undefined && modelReasoning.efforts.length > 0 ? (
429
+ modelReasoning.efforts.map(eff => (
430
+ <option key={eff.id} value={eff.id}>
431
+ {eff.name}{eff.description ? ` (${eff.description})` : ''}
432
+ </option>
433
+ ))
434
+ ) : (
435
+ <>
436
+ <option value="low">低 (low)</option>
437
+ <option value="medium">中 (medium)</option>
438
+ <option value="high">高 (high)</option>
439
+ <option value="none">关闭思考 (none)</option>
440
+ </>
441
+ )}
442
+ </select>
443
+ </Field>
444
+ )}
445
+
338
446
  {presets.length > 0 && (
339
447
  <Field label="执行模式(preset)">
340
448
  <select value={presetId} onChange={e => setPresetId(e.target.value)} title="执行会话按该 preset 组合(决定工具集与人设);默认 = 部署默认 preset">
@@ -484,7 +592,7 @@ interface TaskRecordLike {
484
592
  workspaceId: string
485
593
  urgency: Urgency
486
594
  execution: { mode: 'claim' | 'scheduled'; cron?: string }
487
- model?: { provider: string; model: string }
595
+ model?: { provider: string; model: string; reasoningEffort?: string }
488
596
  isolation?: IsolationMode
489
597
  presetId?: string
490
598
  checklist?: ChecklistItem[]
@@ -64,7 +64,10 @@ export function TemplateManager({ controller }: { controller: BoardController })
64
64
  if (e.key === 'Enter') save(t.id, nameOf(t.id, t.name))
65
65
  }}
66
66
  />
67
- <span className="dsh-atb-tplm-meta">
67
+ <span
68
+ className="dsh-atb-tplm-meta"
69
+ title={`${t.builtin === true ? '内置' : '自建'}${t.task.checklist !== undefined && t.task.checklist.length > 0 ? ` · 清单 ${t.task.checklist.length} 项` : ''}${t.task.urgency !== undefined ? ` · ${t.task.urgency}` : ''}`}
70
+ >
68
71
  {t.builtin === true ? '内置' : '自建'}
69
72
  {t.task.checklist !== undefined && t.task.checklist.length > 0 ? ` · 清单 ${t.task.checklist.length} 项` : ''}
70
73
  {t.task.urgency !== undefined ? ` · ${t.task.urgency}` : ''}
@@ -22,7 +22,7 @@ export interface BoardFilters {
22
22
  }
23
23
 
24
24
  /** Column sort orders. */
25
- export type SortBy = 'default' | 'updated' | 'urgency' | 'created'
25
+ export type SortBy = 'default' | 'updated' | 'urgency' | 'created' | 'title'
26
26
 
27
27
  /** localStorage key for persisted view state (filters + sort). */
28
28
  const VIEW_KEY = 'dsh-taskboard-view-v1'
@@ -33,7 +33,7 @@ function loadView(): { workspaceId?: string; urgencies: Urgency[]; sortBy: SortB
33
33
  const raw = localStorage.getItem(VIEW_KEY)
34
34
  if (raw === null) return { urgencies: [], sortBy: 'default' }
35
35
  const parsed = JSON.parse(raw) as { workspaceId?: string; urgencies?: Urgency[]; sortBy?: SortBy }
36
- const sortBy = parsed.sortBy === 'updated' || parsed.sortBy === 'urgency' || parsed.sortBy === 'created' ? parsed.sortBy : 'default'
36
+ const sortBy = parsed.sortBy === 'updated' || parsed.sortBy === 'urgency' || parsed.sortBy === 'created' || parsed.sortBy === 'title' ? parsed.sortBy : 'default'
37
37
  return {
38
38
  workspaceId: typeof parsed.workspaceId === 'string' ? parsed.workspaceId : undefined,
39
39
  urgencies: Array.isArray(parsed.urgencies) ? parsed.urgencies.filter(u => u === 'urgent' || u === 'normal' || u === 'relaxed') : [],
@@ -114,7 +114,15 @@ export class BoardController {
114
114
  private sessionJumper: ((sessionId: string) => Promise<SessionJumpResult>) | undefined
115
115
  /** Composer catalog faces, installed formally by the client entry (T13). */
116
116
  private readonly catalogFaces: {
117
- models?: () => Promise<Array<{ provider: string; model: string; name?: string }>>
117
+ models?: () => Promise<Array<{
118
+ provider: string
119
+ model: string
120
+ name?: string
121
+ reasoning?: {
122
+ efforts: Array<{ id: string; name: string; description?: string }>
123
+ defaultEffort?: string
124
+ }
125
+ }>>
118
126
  presets?: () => Promise<{ presets: Array<{ id: string; name?: string }>; defaultId?: string }>
119
127
  } = {}
120
128
 
@@ -278,7 +286,15 @@ export class BoardController {
278
286
  }
279
287
 
280
288
  /** T13: formal installers for the composer catalog faces (was a monkeypatch from the client entry). */
281
- installModelCatalog(fn: () => Promise<Array<{ provider: string; model: string; name?: string }>>): void {
289
+ installModelCatalog(fn: () => Promise<Array<{
290
+ provider: string
291
+ model: string
292
+ name?: string
293
+ reasoning?: {
294
+ efforts: Array<{ id: string; name: string; description?: string }>
295
+ defaultEffort?: string
296
+ }
297
+ }>>): void {
282
298
  this.catalogFaces.models = fn
283
299
  }
284
300
 
@@ -288,7 +304,15 @@ export class BoardController {
288
304
  }
289
305
 
290
306
  /** The installed model catalog face, when the runtime provides one. */
291
- get modelCatalog(): (() => Promise<Array<{ provider: string; model: string; name?: string }>>) | undefined {
307
+ get modelCatalog(): (() => Promise<Array<{
308
+ provider: string
309
+ model: string
310
+ name?: string
311
+ reasoning?: {
312
+ efforts: Array<{ id: string; name: string; description?: string }>
313
+ defaultEffort?: string
314
+ }
315
+ }>>) | undefined {
292
316
  return this.catalogFaces.models
293
317
  }
294
318
 
@@ -28,7 +28,25 @@ export const inject = ['connection']
28
28
  interface ConnectionFace {
29
29
  api: {
30
30
  llm: {
31
- models(payload: Record<string, never>): Promise<{ result: { ok: true; value: { groups: Array<{ id: string; name: string; models: Array<{ id: string; name?: string }> }> } } | { ok: false } }>
31
+ models(payload: Record<string, never>): Promise<{
32
+ result: {
33
+ ok: true
34
+ value: {
35
+ groups: Array<{
36
+ id: string
37
+ name: string
38
+ models: Array<{
39
+ id: string
40
+ name?: string
41
+ reasoning?: {
42
+ efforts: Array<{ id: string; name: string; description?: string }>
43
+ defaultEffort?: string
44
+ }
45
+ }>
46
+ }>
47
+ }
48
+ } | { ok: false }
49
+ }>
32
50
  }
33
51
  agentPresets?: {
34
52
  list(payload: Record<string, never>): Promise<{ result: { ok: true; value: { presets: Array<{ id: string; name?: string; isDefault: boolean }> } } | { ok: false } }>
@@ -43,8 +61,8 @@ interface ClientContextFace {
43
61
  }
44
62
 
45
63
  /**
46
- * Mount the client half.
47
- * @param ctx - the client context (connection injected).
64
+ * Client entry: installs styles, starts the controller, mounts DOM seats.
65
+ * @param ctx - the cordis client context.
48
66
  */
49
67
  export function apply(ctx: ClientContextFace): void {
50
68
  try {
@@ -57,14 +75,27 @@ export function apply(ctx: ClientContextFace): void {
57
75
  // monkeypatched instance properties).
58
76
  const connection = ctx.get?.('connection') as ConnectionFace | undefined
59
77
  if (connection !== undefined) {
60
- type CatalogRow = { provider: string; model: string; name?: string }
78
+ type CatalogRow = {
79
+ provider: string
80
+ model: string
81
+ name?: string
82
+ reasoning?: {
83
+ efforts: Array<{ id: string; name: string; description?: string }>
84
+ defaultEffort?: string
85
+ }
86
+ }
61
87
  controller.installModelCatalog(async (): Promise<CatalogRow[]> => {
62
88
  const response = await connection.api.llm.models({})
63
89
  if (!response.result.ok) return []
64
90
  const out: CatalogRow[] = []
65
91
  for (const group of response.result.value.groups) {
66
92
  for (const model of group.models) {
67
- out.push({ provider: group.id, model: model.id, name: model.name })
93
+ out.push({
94
+ provider: group.id,
95
+ model: model.id,
96
+ name: model.name,
97
+ ...(model.reasoning !== undefined ? { reasoning: model.reasoning } : {}),
98
+ })
68
99
  }
69
100
  }
70
101
  return out
@@ -95,8 +95,43 @@ html[data-dsh-atb-active] .dsh-atb-view { display: flex; flex-direction: column;
95
95
  .dsh-atb-spacer { flex: 1; }
96
96
  .dsh-atb-select, .dsh-atb-input {
97
97
  font: inherit; font-size: 12.5px; padding: 5px 8px; border-radius: 7px;
98
- border: 1px solid var(--dsw-border, rgba(128,128,128,.35));
99
- background: var(--dsw-bg, transparent); color: inherit;
98
+ border: 1px solid var(--dsw-border, var(--dsw-alias-border-l2, rgba(128,128,128,.35)));
99
+ background: var(--dsw-alias-bg-module-platform, var(--dsw-bg-elevated, var(--dsw-bg, transparent)));
100
+ color: var(--dsw-alias-label-primary, var(--dsw-text-primary, inherit));
101
+ color-scheme: light dark;
102
+ }
103
+ .dsh-atb-select option,
104
+ .dsh-atb-modal-body select option {
105
+ background: var(--dsw-alias-bg-overlay, var(--dsw-alias-bg-module-platform, #252830));
106
+ color: var(--dsw-alias-label-primary, var(--dsw-text-primary, #e6e8eb));
107
+ }
108
+ body[data-ds-dark-theme] .dsh-atb-board,
109
+ body[data-ds-dark-theme] .dsh-atb-modal,
110
+ body[data-ds-dark-theme] .dsh-atb-select,
111
+ body[data-ds-dark-theme] .dsh-atb-input,
112
+ body[data-ds-dark-theme] .dsh-atb-modal-body select,
113
+ body[data-ds-dark-theme] .dsh-atb-modal-body input,
114
+ body[data-ds-dark-theme] .dsh-atb-modal-body textarea {
115
+ color-scheme: dark;
116
+ }
117
+ body[data-ds-dark-theme] .dsh-atb-select option,
118
+ body[data-ds-dark-theme] .dsh-atb-modal-body select option {
119
+ background: #252830;
120
+ color: #e6e8eb;
121
+ }
122
+ @media (prefers-color-scheme: dark) {
123
+ .dsh-atb-select,
124
+ .dsh-atb-input,
125
+ .dsh-atb-modal-body select,
126
+ .dsh-atb-modal-body input,
127
+ .dsh-atb-modal-body textarea {
128
+ color-scheme: dark;
129
+ }
130
+ .dsh-atb-select option,
131
+ .dsh-atb-modal-body select option {
132
+ background: #252830;
133
+ color: #e6e8eb;
134
+ }
100
135
  }
101
136
  .dsh-atb-chip {
102
137
  display: inline-flex; align-items: center; gap: 5px;
@@ -172,6 +207,19 @@ html[data-dsh-atb-active] .dsh-atb-view { display: flex; flex-direction: column;
172
207
  .dsh-atb-badge[data-kind="trashed"] { background: rgba(229,72,77,.14); color: #e5484d; text-decoration: line-through; }
173
208
  .dsh-atb-badge[data-kind="done"] { background: rgba(46,160,67,.16); color: #2ea043; }
174
209
  .dsh-atb-badge[data-kind="running"] { background: rgba(229,152,42,.16); color: #e69842; }
210
+ .dsh-atb-card-session {
211
+ font: inherit; font-size: 10.5px; line-height: 1; padding: 2px 6px; border-radius: 5px;
212
+ border: 1px solid var(--dsw-border, rgba(128,128,128,.3));
213
+ background: var(--dsw-bg-elevated, rgba(128,128,128,.14));
214
+ color: var(--dsw-text-secondary, inherit);
215
+ cursor: pointer; display: inline-flex; align-items: center; gap: 3px;
216
+ transition: all .15s ease;
217
+ }
218
+ .dsh-atb-card-session:hover {
219
+ border-color: var(--dsw-alias-brand-primary, #1f2328);
220
+ background: var(--dsw-hover, rgba(128,128,128,.24));
221
+ color: var(--dsw-alias-label-primary, inherit);
222
+ }
175
223
 
176
224
  /* ---------- card quick review (in_review column) ---------- */
177
225
  .dsh-atb-quick { display: flex; gap: 6px; margin-top: 7px; }
@@ -219,6 +267,17 @@ html[data-dsh-atb-active] .dsh-atb-view { display: flex; flex-direction: column;
219
267
  background: var(--dsw-bg-elevated, rgba(128,128,128,.07)); color: var(--dsw-text-secondary, inherit);
220
268
  }
221
269
  .dsh-atb-detail-edit:hover { border-color: var(--dsw-alias-brand-primary, #1f2328); color: var(--dsw-alias-label-primary, inherit); }
270
+ .dsh-atb-detail-session {
271
+ font: inherit; font-size: 12px; padding: 4px 10px; border-radius: 7px; cursor: pointer;
272
+ border: 1px solid var(--dsw-border, rgba(128,128,128,.32));
273
+ background: var(--dsw-bg-elevated, rgba(128,128,128,.1));
274
+ color: var(--dsw-alias-label-primary, inherit);
275
+ display: inline-flex; align-items: center; gap: 4px;
276
+ }
277
+ .dsh-atb-detail-session:hover {
278
+ border-color: var(--dsw-alias-brand-primary, #1f2328);
279
+ background: var(--dsw-hover, rgba(128,128,128,.22));
280
+ }
222
281
 
223
282
  .dsh-atb-statuspill {
224
283
  flex: none; font-size: 11px; font-weight: 600; padding: 2px 9px; border-radius: 999px; letter-spacing: .02em;
@@ -236,6 +295,14 @@ html[data-dsh-atb-active] .dsh-atb-view { display: flex; flex-direction: column;
236
295
  padding: 3px 8px; border-radius: 6px;
237
296
  background: var(--dsw-bg-inset, rgba(128,128,128,.09)); color: var(--dsw-text-secondary, #999);
238
297
  }
298
+ button.dsh-atb-chip2.dsh-atb-chip-btn {
299
+ font: inherit; border: 1px solid var(--dsw-border, rgba(128,128,128,.25)); cursor: pointer;
300
+ }
301
+ button.dsh-atb-chip2.dsh-atb-chip-btn:hover {
302
+ border-color: var(--dsw-alias-brand-primary, #1f2328);
303
+ color: var(--dsw-alias-label-primary, inherit);
304
+ background: var(--dsw-hover, rgba(128,128,128,.2));
305
+ }
239
306
  .dsh-atb-chip2-icon { font-size: 10.5px; opacity: .85; }
240
307
  .dsh-atb-chip2[data-tone="urgent"] { background: rgba(229,72,77,.15); color: #e5484d; }
241
308
  .dsh-atb-chip2[data-tone="normal"] { background: rgba(142,78,198,.14); color: #a06ce0; }
@@ -649,19 +716,30 @@ html[data-dsh-atb-active] .dsh-atb-view { display: flex; flex-direction: column;
649
716
  .dsh-atb-newmenu-opt:hover { background: var(--dsw-alias-bg-layer-2, rgba(128,128,128,.1)); }
650
717
  .dsh-atb-newmenu-sep { height: 1px; margin: 4px 6px; background: var(--dsw-alias-border-l2, rgba(128,128,128,.25)); }
651
718
 
652
- .dsh-atb-tplm { max-width: 560px; width: min(560px, 92vw); }
653
- .dsh-atb-tplm-list { display: flex; flex-direction: column; gap: 8px; }
719
+ .dsh-atb-tplm { max-width: 600px; width: min(600px, 92vw); }
720
+ .dsh-atb-tplm .dsh-atb-modal-body,
721
+ .dsh-atb-set .dsh-atb-modal-body,
722
+ .dsh-atb-diag .dsh-atb-modal-body,
723
+ .dsh-atb-imp .dsh-atb-modal-body {
724
+ display: flex; flex-direction: column; gap: 10px;
725
+ }
726
+ .dsh-atb-tplm-list { display: flex; flex-direction: column; gap: 8px; width: 100%; }
654
727
  .dsh-atb-tplm-row {
655
- display: flex; align-items: center; gap: 10px; padding: 8px 10px; border-radius: 8px;
728
+ display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 8px 12px; border-radius: 8px;
656
729
  border: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,.25));
730
+ background: var(--dsw-alias-bg-layer-1, rgba(128,128,128,.02));
657
731
  }
658
732
  .dsh-atb-tplm-name {
659
- flex: 0 0 160px; font-size: 12.5px; padding: 5px 8px; border-radius: 7px;
733
+ flex: 0 0 150px; width: 150px; max-width: 150px; font-size: 12.5px; padding: 5px 8px; border-radius: 7px;
660
734
  border: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,.3));
661
735
  background: var(--dsw-alias-bg-layer-1, transparent); color: inherit;
736
+ box-sizing: border-box;
737
+ }
738
+ .dsh-atb-tplm-meta {
739
+ flex: 1; min-width: 0; font-size: 11px; color: var(--dsw-alias-label-tertiary, gray);
740
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
662
741
  }
663
- .dsh-atb-tplm-meta { flex: 1; min-width: 0; font-size: 10.5px; color: var(--dsw-alias-label-tertiary, gray); }
664
- .dsh-atb-tplm-btns { display: flex; gap: 6px; flex-shrink: 0; }
742
+ .dsh-atb-tplm-btns { display: flex; align-items: center; gap: 6px; flex-shrink: 0; }
665
743
 
666
744
  .dsh-atb-imp { max-width: 600px; width: min(600px, 92vw); }
667
745
  .dsh-atb-imp-picker { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
@@ -18,6 +18,7 @@ import {
18
18
  normalizeBody,
19
19
  type ExecutionRecord,
20
20
  type IsolationMode,
21
+ type TaskModel,
21
22
  type TaskRecord,
22
23
  } from '../shared/protocol.ts'
23
24
  import { sanitizeBranchName, worktreePathOf, type GitFace, type SettlementFacts } from './git.ts'
@@ -32,7 +33,7 @@ export interface AgentsFace {
32
33
  create(options: {
33
34
  sessionId: string
34
35
  meta?: { cwd?: string; agentPreset?: string }
35
- agentOptions?: { provider?: string; model?: string }
36
+ agentOptions?: { provider?: string; model?: string; reasoningEffort?: string }
36
37
  /** Preset composition callback: mounts tools/persona into the agent's scoped context. */
37
38
  setup?: (agentCtx: unknown) => Promise<void> | void
38
39
  }): Promise<{
@@ -77,7 +78,7 @@ export interface ExecutionDeps {
77
78
  events: EventsFace
78
79
  now: () => number
79
80
  /** The deployment default model (fills sessions of unpinned tasks). */
80
- defaultModel?: () => { provider: string; model: string } | undefined
81
+ defaultModel?: () => TaskModel | undefined
81
82
  /** Mint session ids (injectable for tests). */
82
83
  mintSessionId?: () => string
83
84
  /** Mint message ids (injectable for tests). */
@@ -404,7 +405,13 @@ export class ExecutionService {
404
405
  cwd: workspace.path,
405
406
  ...(composition !== undefined ? { agentPreset: composition.agentPreset } : {}),
406
407
  },
407
- ...(model !== undefined ? { agentOptions: { provider: model.provider, model: model.model } } : {}),
408
+ ...(model !== undefined ? {
409
+ agentOptions: {
410
+ provider: model.provider,
411
+ model: model.model,
412
+ ...(model.reasoningEffort !== undefined ? { reasoningEffort: model.reasoningEffort } : {}),
413
+ },
414
+ } : {}),
408
415
  ...(composition !== undefined ? { setup: composition.setup } : {}),
409
416
  })
410
417
  } catch (error) {
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/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?: { provider: string; model: string }
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?: { provider: string; model: string } | null
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?: { provider: string; model: string }
133
+ model?: TaskModel
134
134
  isolation?: string
135
135
  presetId?: string
136
136
  /** Checklist item texts (host mints ids at create time). */
@@ -378,6 +378,8 @@ export type ExecutionRecord = {
378
378
  export type TaskModel = {
379
379
  provider: string
380
380
  model: string
381
+ /** Thinking intensity / reasoning effort (optional; e.g. 'low', 'medium', 'high', 'none'). */
382
+ reasoningEffort?: string
381
383
  }
382
384
 
383
385
  /** One task on the board. */
@@ -638,8 +640,8 @@ export function syncClaim(task: TaskRecord, to: TaskStatus, now: number, holder?
638
640
  }
639
641
 
640
642
  /**
641
- * Validate and normalize a pinned model: `{ provider, model }`, both
642
- * non-empty trimmed strings.
643
+ * Validate and normalize a pinned model: `{ provider, model, reasoningEffort? }`,
644
+ * provider and model must be non-empty trimmed strings.
643
645
  * @param raw - the raw input.
644
646
  * @returns the normalized model.
645
647
  * @throws when the shape or the fields are invalid.
@@ -648,7 +650,7 @@ export function normalizeModel(raw: unknown): TaskModel {
648
650
  if (typeof raw !== 'object' || raw === null) {
649
651
  throw new Error('model must be { provider: string, model: string }')
650
652
  }
651
- const { provider, model } = raw as { provider?: unknown; model?: unknown }
653
+ const { provider, model, reasoningEffort } = raw as { provider?: unknown; model?: unknown; reasoningEffort?: unknown }
652
654
  if (typeof provider !== 'string' || typeof model !== 'string') {
653
655
  throw new Error('model must be { provider: string, model: string }')
654
656
  }
@@ -657,7 +659,8 @@ export function normalizeModel(raw: unknown): TaskModel {
657
659
  if (p.length === 0 || m.length === 0) {
658
660
  throw new Error('model.provider and model.model must be non-empty strings')
659
661
  }
660
- return { provider: p, model: m }
662
+ const eff = typeof reasoningEffort === 'string' && reasoningEffort.trim().length > 0 ? reasoningEffort.trim() : undefined
663
+ return { provider: p, model: m, ...(eff !== undefined ? { reasoningEffort: eff } : {}) }
661
664
  }
662
665
 
663
666
  // ---------------------------------------------------------------------------
@@ -6,4 +6,4 @@
6
6
  */
7
7
 
8
8
  /** The package version (must equal package.json "version"). */
9
- export const PLUGIN_VERSION = '0.5.3'
9
+ export const PLUGIN_VERSION = '0.5.4'