dsh-taskboard 0.2.2 → 0.4.0

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 (43) hide show
  1. package/README.md +62 -6
  2. package/lib/client.js +1839 -74
  3. package/lib/host/execution.js +199 -54
  4. package/lib/host/execution.js.map +1 -1
  5. package/lib/host/git.js +327 -0
  6. package/lib/host/git.js.map +1 -0
  7. package/lib/host/protocol-text.js +5 -3
  8. package/lib/host/protocol-text.js.map +1 -1
  9. package/lib/host/routes.js +435 -5
  10. package/lib/host/routes.js.map +1 -1
  11. package/lib/host/store.js +12 -0
  12. package/lib/host/store.js.map +1 -1
  13. package/lib/host/templates.js +166 -0
  14. package/lib/host/templates.js.map +1 -0
  15. package/lib/host/tools.js +217 -3
  16. package/lib/host/tools.js.map +1 -1
  17. package/lib/index.js +23 -3
  18. package/lib/index.js.map +1 -1
  19. package/lib/shared/api.js.map +1 -1
  20. package/lib/shared/protocol.js +286 -1
  21. package/lib/shared/protocol.js.map +1 -1
  22. package/package.json +74 -74
  23. package/src/client/api.ts +47 -3
  24. package/src/client/board/ImportModal.tsx +182 -0
  25. package/src/client/board/TaskBoard.tsx +118 -3
  26. package/src/client/board/TaskCard.tsx +9 -0
  27. package/src/client/board/TaskDetail.tsx +360 -4
  28. package/src/client/board/TaskFormModal.tsx +193 -12
  29. package/src/client/board/TemplateManager.tsx +121 -0
  30. package/src/client/controller.ts +238 -11
  31. package/src/client/index.ts +18 -1
  32. package/src/client/styles.ts +198 -0
  33. package/src/host/execution.ts +301 -67
  34. package/src/host/git.ts +370 -0
  35. package/src/host/protocol-text.ts +5 -3
  36. package/src/host/routes.ts +483 -5
  37. package/src/host/store.ts +13 -0
  38. package/src/host/templates.ts +143 -0
  39. package/src/host/tools.ts +215 -2
  40. package/src/index.ts +30 -1
  41. package/src/shared/api.ts +89 -3
  42. package/src/shared/protocol.ts +408 -0
  43. package/src/shared/version.ts +1 -1
@@ -7,8 +7,8 @@
7
7
  *
8
8
  * @module dsh-taskboard/client/controller
9
9
  */
10
- import type { ChangeEvent, UpdateTaskBody, WorkspaceView } from '../shared/api.ts'
11
- import type { TaskLedger, TaskRecord, Urgency } from '../shared/protocol.ts'
10
+ import type { ChangeEvent, DiagnosticsResponse, DiffResponse, ImportCommitResponse, ImportPreviewResponse, TaskTemplate, TaskTemplateSpec, UpdateTaskBody, WorkspaceView } from '../shared/api.ts'
11
+ import type { ChecklistItem, IsolationMode, TaskLedger, TaskRecord, Urgency } from '../shared/protocol.ts'
12
12
  import { emptyLedger } from '../shared/protocol.ts'
13
13
  import type { TaskboardClient } from './api.ts'
14
14
  import type { SessionJumpResult } from './session-jump.ts'
@@ -27,6 +27,26 @@ export type SortBy = 'default' | 'updated' | 'urgency' | 'created'
27
27
  /** localStorage key for persisted view state (filters + sort). */
28
28
  const VIEW_KEY = 'dsh-taskboard-view-v1'
29
29
 
30
+ /** localStorage key for the remembered isolation toggle choice (0.3.0). */
31
+ const ISOLATION_KEY = 'dsh-taskboard-isolation-v1'
32
+
33
+ /** Load the remembered default isolation (worktree unless explicitly turned off). */
34
+ export function loadDefaultIsolation(): IsolationMode {
35
+ try {
36
+ const raw = localStorage.getItem(ISOLATION_KEY)
37
+ return raw === 'none' ? 'none' : 'worktree'
38
+ } catch {
39
+ return 'worktree'
40
+ }
41
+ }
42
+
43
+ /** Remember the isolation toggle choice across forms (best effort). */
44
+ export function saveDefaultIsolation(mode: IsolationMode): void {
45
+ try {
46
+ localStorage.setItem(ISOLATION_KEY, mode)
47
+ } catch { /* storage unavailable — choice just won't persist */ }
48
+ }
49
+
30
50
  /** Load the persisted view state (never throws; fresh on any parse error). */
31
51
  function loadView(): { workspaceId?: string; urgencies: Urgency[]; sortBy: SortBy } {
32
52
  try {
@@ -62,6 +82,18 @@ export interface ControllerState {
62
82
  editingId?: string
63
83
  /** Secondary (canceled/archived/trashed) tab visible. */
64
84
  secondaryOpen: boolean
85
+ /** Health-diagnostics panel (⚙) visible. */
86
+ diagOpen: boolean
87
+ /** Last fetched diagnostics payload (⚙ panel). */
88
+ diagnostics?: DiagnosticsResponse
89
+ /** Task templates (0.4.0), lazy-loaded when the new-task menu opens. */
90
+ templates: TaskTemplate[]
91
+ /** Template manager modal visible. */
92
+ tplManagerOpen: boolean
93
+ /** Import modal visible (0.4.0). */
94
+ importOpen: boolean
95
+ /** Fields a chosen template prefills into the create form (consumed on open). */
96
+ templatePrefill?: TaskTemplateSpec
65
97
  /** Transient error surface (action failures); cleared on next success. */
66
98
  error?: string
67
99
  }
@@ -78,6 +110,10 @@ function initialState(): ControllerState {
78
110
  sortBy: view.sortBy,
79
111
  composerOpen: false,
80
112
  secondaryOpen: false,
113
+ diagOpen: false,
114
+ templates: [],
115
+ tplManagerOpen: false,
116
+ importOpen: false,
81
117
  }
82
118
  }
83
119
 
@@ -209,18 +245,29 @@ export class BoardController {
209
245
  /** Select a task (open detail). */
210
246
  select(id?: string): void { this.setState({ selectedId: id }) }
211
247
 
212
- /** Show/hide the task form (create mode when opening). */
213
- setComposer(open: boolean): void { this.setState({ composerOpen: open, editingId: undefined }) }
248
+ /** Show/hide the task form (create mode when opening); always blank (no template prefill). */
249
+ setComposer(open: boolean): void { this.setState({ composerOpen: open, editingId: undefined, templatePrefill: undefined }) }
250
+
251
+ /** Open the create form prefilled from a chosen template (0.4.0). */
252
+ newFromTemplate(spec: TaskTemplateSpec): void {
253
+ this.setState({ composerOpen: true, editingId: undefined, templatePrefill: spec })
254
+ }
214
255
 
215
- /** Open the form modal editing an existing task. */
216
- openEditor(id: string): void { this.setState({ composerOpen: true, editingId: id }) }
256
+ /** Open the form modal editing an existing task (clears any template prefill). */
257
+ openEditor(id: string): void { this.setState({ composerOpen: true, editingId: id, templatePrefill: undefined }) }
217
258
 
218
259
  /** Close the form modal whatever its mode. */
219
- closeForm(): void { this.setState({ composerOpen: false, editingId: undefined }) }
260
+ closeForm(): void { this.setState({ composerOpen: false, editingId: undefined, templatePrefill: undefined }) }
220
261
 
221
262
  /** Toggle the secondary tab. */
222
263
  toggleSecondary(): void { this.setState({ secondaryOpen: !this.state.secondaryOpen }) }
223
264
 
265
+ /** Whether a workspace passed git detection (form toggle enablement). */
266
+ gitAvailable(workspaceId: string | undefined): boolean {
267
+ if (workspaceId === undefined) return true
268
+ return this.state.workspaces.find(w => w.id === workspaceId)?.gitAvailable === true
269
+ }
270
+
224
271
  /**
225
272
  * Install the session-jump bridge (built from the runtime sessions service
226
273
  * by the client entry). Without it openSession reports 'unavailable'.
@@ -315,6 +362,34 @@ export class BoardController {
315
362
  }
316
363
  }
317
364
 
365
+ /**
366
+ * Toggle one checklist item as the USER (0.4.0): flips the item, records
367
+ * `checkedBy: 'user'`, keeps other items as they are (one update call).
368
+ */
369
+ async toggleChecklistItem(task: TaskRecord, itemId: string): Promise<void> {
370
+ const items: ChecklistItem[] = (task.checklist ?? []).map(item => item.id === itemId
371
+ ? (item.checked
372
+ ? { id: item.id, text: item.text, checked: false }
373
+ : { id: item.id, text: item.text, checked: true, checkedBy: 'user', checkedAt: Date.now(), ...(item.note !== undefined ? { note: item.note } : {}) })
374
+ : item)
375
+ try {
376
+ await this.client.update(task.id, { ifVersion: task.version, checklist: items })
377
+ await this.refresh()
378
+ } catch (error) {
379
+ this.setState({ error: error instanceof Error ? error.message : String(error) })
380
+ }
381
+ }
382
+
383
+ /** Diff view (0.4.0): one execution's commit or changed path; errors surface via throw. */
384
+ async fetchDiff(taskId: string, query: { execution: string; commit?: string; path?: string }): Promise<DiffResponse | undefined> {
385
+ try {
386
+ return await this.client.diff(taskId, query)
387
+ } catch (error) {
388
+ this.setState({ error: error instanceof Error ? error.message : String(error) })
389
+ return undefined
390
+ }
391
+ }
392
+
318
393
  /** Append a user comment. */
319
394
  async comment(id: string, body: string): Promise<void> {
320
395
  try {
@@ -325,10 +400,10 @@ export class BoardController {
325
400
  }
326
401
  }
327
402
 
328
- /** Trigger a manual run (fresh in-project session, pinned model). */
329
- async run(id: string): Promise<void> {
403
+ /** Trigger a manual run (fresh in-project session, pinned model); `reuse` = 续跑. */
404
+ async run(id: string, reuse = false): Promise<void> {
330
405
  try {
331
- await this.client.run(id)
406
+ await this.client.run(id, reuse ? { reuse: true } : {})
332
407
  await this.refresh()
333
408
  } catch (error) {
334
409
  this.setState({ error: error instanceof Error ? error.message : String(error) })
@@ -345,6 +420,56 @@ export class BoardController {
345
420
  }
346
421
  }
347
422
 
423
+ /**
424
+ * ⇥ 合并 (detail page): merge the task branch into the main worktree.
425
+ * @returns the outcome; `noop` means the branch had no new commits (nothing merged).
426
+ */
427
+ async mergeBranch(id: string): Promise<{ ok: true; noop?: boolean } | { ok: false; error: string }> {
428
+ try {
429
+ const value = await this.client.mergeBranch(id)
430
+ await this.refresh()
431
+ return value.noop === true ? { ok: true, noop: true } : { ok: true }
432
+ } catch (error) {
433
+ return { ok: false, error: error instanceof Error ? error.message : String(error) }
434
+ }
435
+ }
436
+
437
+ /**
438
+ * 🗑 删除 worktree (detail page), optionally deleting the task branch too.
439
+ * @returns the outcome; failures carry the git message for an alert.
440
+ */
441
+ async removeWorktree(id: string, deleteBranch: boolean): Promise<{ ok: true; branchError?: string } | { ok: false; error: string }> {
442
+ try {
443
+ const value = await this.client.worktreeRemove(id, { deleteBranch })
444
+ await this.refresh()
445
+ return value.branchError !== undefined ? { ok: true, branchError: value.branchError } : { ok: true }
446
+ } catch (error) {
447
+ return { ok: false, error: error instanceof Error ? error.message : String(error) }
448
+ }
449
+ }
450
+
451
+ /** Open the ⚙ diagnostics panel and fetch a fresh snapshot. */
452
+ openDiagnostics(): void {
453
+ this.setState({ diagOpen: true })
454
+ void this.client.diagnostics()
455
+ .then(diagnostics => this.setState({ diagnostics }))
456
+ .catch(error => this.setState({ error: error instanceof Error ? error.message : String(error) }))
457
+ }
458
+
459
+ /** Close the ⚙ diagnostics panel. */
460
+ closeDiagnostics(): void { this.setState({ diagOpen: false }) }
461
+
462
+ /** Clean one orphan worktree (⚙ panel); refreshes the diagnostics payload. */
463
+ async cleanupOrphan(workspaceId: string, taskId: string): Promise<void> {
464
+ try {
465
+ await this.client.worktreeCleanup(workspaceId, taskId)
466
+ const diagnostics = await this.client.diagnostics()
467
+ this.setState({ diagnostics, error: undefined })
468
+ } catch (error) {
469
+ this.setState({ error: error instanceof Error ? error.message : String(error) })
470
+ }
471
+ }
472
+
348
473
  /** Soft-delete (agent parity) then optional purge. */
349
474
  async remove(id: string, ifVersion: number, purge: boolean): Promise<void> {
350
475
  try {
@@ -356,7 +481,7 @@ export class BoardController {
356
481
  }
357
482
  }
358
483
 
359
- /** Duplicate a task into a fresh todo card (same project/urgency/prompt/execution/model). */
484
+ /** Duplicate a task into a fresh todo card (same project/urgency/prompt/execution/model/isolation/checklist). */
360
485
  async duplicate(task: TaskRecord): Promise<void> {
361
486
  try {
362
487
  await this.client.create({
@@ -369,6 +494,9 @@ export class BoardController {
369
494
  ? { mode: 'scheduled', cron: task.execution.cron }
370
495
  : { mode: 'claim' },
371
496
  model: task.model,
497
+ isolation: task.isolation,
498
+ ...(task.presetId !== undefined ? { presetId: task.presetId } : {}),
499
+ ...(task.checklist !== undefined && task.checklist.length > 0 ? { checklist: task.checklist.map(i => i.text) } : {}),
372
500
  })
373
501
  await this.refresh()
374
502
  } catch (error) {
@@ -376,6 +504,105 @@ export class BoardController {
376
504
  }
377
505
  }
378
506
 
507
+ // ------------------------------------------------ templates (0.4.0)
508
+ /** Load the template list (best effort; errors surface). */
509
+ async loadTemplates(): Promise<TaskTemplate[]> {
510
+ try {
511
+ const value = await this.client.templates()
512
+ this.setState({ templates: value.templates, error: undefined })
513
+ return value.templates
514
+ } catch (error) {
515
+ this.setState({ error: error instanceof Error ? error.message : String(error) })
516
+ return []
517
+ }
518
+ }
519
+
520
+ /** Open the + 新建任务 dropdown's template list fresh (called on menu open). */
521
+ prepareTemplateMenu(): void {
522
+ if (this.state.templates.length === 0) void this.loadTemplates()
523
+ }
524
+
525
+ /** Open the template manager modal. */
526
+ openTemplateManager(): void {
527
+ this.setState({ tplManagerOpen: true })
528
+ void this.loadTemplates()
529
+ }
530
+
531
+ /** Close the template manager modal. */
532
+ closeTemplateManager(): void { this.setState({ tplManagerOpen: false }) }
533
+
534
+ /** Create or replace a template; refreshes the list. */
535
+ async upsertTemplate(body: { id?: string; name: string; task: TaskTemplateSpec }): Promise<boolean> {
536
+ try {
537
+ await this.client.templateUpsert(body)
538
+ await this.loadTemplates()
539
+ return true
540
+ } catch (error) {
541
+ this.setState({ error: error instanceof Error ? error.message : String(error) })
542
+ return false
543
+ }
544
+ }
545
+
546
+ /** Delete a template by id; refreshes the list. */
547
+ async deleteTemplate(id: string): Promise<void> {
548
+ try {
549
+ await this.client.templateDelete(id)
550
+ await this.loadTemplates()
551
+ } catch (error) {
552
+ this.setState({ error: error instanceof Error ? error.message : String(error) })
553
+ }
554
+ }
555
+
556
+ /** 存为模板 from a task card: carries every configurable field incl. checklist texts. */
557
+ async saveAsTemplate(task: TaskRecord): Promise<boolean> {
558
+ return this.upsertTemplate({
559
+ name: task.title.slice(0, 60),
560
+ task: {
561
+ title: task.title,
562
+ description: task.description.length > 0 ? task.description : undefined,
563
+ prompt: task.prompt.length > 0 ? task.prompt : undefined,
564
+ urgency: task.urgency,
565
+ execution: task.execution.mode === 'scheduled' && task.execution.cron !== undefined
566
+ ? { mode: 'scheduled', cron: task.execution.cron }
567
+ : { mode: 'claim' },
568
+ model: task.model,
569
+ isolation: task.isolation,
570
+ ...(task.presetId !== undefined ? { presetId: task.presetId } : {}),
571
+ ...(task.checklist !== undefined && task.checklist.length > 0 ? { checklist: task.checklist.map(i => i.text) } : {}),
572
+ },
573
+ })
574
+ }
575
+
576
+ // ------------------------------------------------ import (0.4.0)
577
+ /** Open the import modal. */
578
+ openImport(): void { this.setState({ importOpen: true }) }
579
+
580
+ /** Close the import modal. */
581
+ closeImport(): void { this.setState({ importOpen: false }) }
582
+
583
+ /** Dry-run an import file: classify its tasks against the live ledger. */
584
+ async importPreview(file: unknown): Promise<ImportPreviewResponse['plan'] | undefined> {
585
+ try {
586
+ const value = await this.client.importPreview(file)
587
+ return value.plan
588
+ } catch (error) {
589
+ this.setState({ error: error instanceof Error ? error.message : String(error) })
590
+ return undefined
591
+ }
592
+ }
593
+
594
+ /** Commit an import; refreshes the ledger afterwards. */
595
+ async importCommit(mode: 'merge' | 'replace', ledger: unknown): Promise<ImportCommitResponse | undefined> {
596
+ try {
597
+ const value = await this.client.importCommit(mode, ledger)
598
+ await this.refresh()
599
+ return value
600
+ } catch (error) {
601
+ this.setState({ error: error instanceof Error ? error.message : String(error) })
602
+ return undefined
603
+ }
604
+ }
605
+
379
606
  /** Download the whole ledger as a JSON backup file. */
380
607
  exportJson(): void {
381
608
  const stamp = new Date()
@@ -24,12 +24,15 @@ export const name = 'dsh-taskboard/client'
24
24
  /** Required client services (fiber inject waiting). */
25
25
  export const inject = ['connection']
26
26
 
27
- /** Narrow connection face for the model catalog. */
27
+ /** Narrow connection face for the model catalog + preset roster. */
28
28
  interface ConnectionFace {
29
29
  api: {
30
30
  llm: {
31
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 } }>
32
32
  }
33
+ agentPresets?: {
34
+ list(payload: Record<string, never>): Promise<{ result: { ok: true; value: { presets: Array<{ id: string; name?: string; isDefault: boolean }> } } | { ok: false } }>
35
+ }
33
36
  }
34
37
  }
35
38
 
@@ -64,6 +67,20 @@ export function apply(ctx: ClientContextFace): void {
64
67
  }
65
68
  return out
66
69
  }
70
+
71
+ // Preset roster for the composer (0.3.3): agentPreset.list over the
72
+ // connection RPC — [{id, name}] plus which one is the deployment
73
+ // default (the form pre-selects it on create).
74
+ type PresetRow = { id: string; name?: string }
75
+ ;(controller as unknown as { presetCatalog?: () => Promise<{ presets: PresetRow[]; defaultId?: string }> }).presetCatalog = async (): Promise<{ presets: PresetRow[]; defaultId?: string }> => {
76
+ const list = connection.api.agentPresets
77
+ if (list === undefined) return { presets: [] }
78
+ const response = await list.list({})
79
+ if (!response.result.ok) return { presets: [] }
80
+ const presets = response.result.value.presets.map((p: { id: string; name?: string }) => ({ id: p.id, name: p.name }))
81
+ const def = response.result.value.presets.find((p: { id: string; isDefault: boolean }) => p.isDefault)
82
+ return { presets, ...(def !== undefined ? { defaultId: def.id } : {}) }
83
+ }
67
84
  }
68
85
 
69
86
  // Session navigation for execution rows: resolved LAZILY on every jump —
@@ -468,6 +468,204 @@ html[data-dsh-atb-active] .dsh-atb-view { display: flex; flex-direction: column;
468
468
  color: var(--dsw-alias-label-primary, inherit);
469
469
  }
470
470
  .dsh-atb-alert .dsh-atb-btn { padding: 6px 28px; font-size: 13px; }
471
+
472
+ /* ---------- 0.3.0 isolation ---------- */
473
+ .dsh-atb-isolation-note { display: block; margin-top: 6px; font-size: 11px; color: var(--dsw-alias-label-tertiary, gray); }
474
+ .dsh-atb-mode-picker[data-disabled="true"] .dsh-atb-mode-opt { cursor: not-allowed; opacity: .55; }
475
+ .dsh-atb-iso-none { font-size: 12.5px; color: var(--dsw-alias-label-secondary, inherit); }
476
+ .dsh-atb-iso-facts { display: flex; flex-wrap: wrap; gap: 6px 12px; margin-bottom: 8px; }
477
+ .dsh-atb-iso-fact { font-size: 11.5px; color: var(--dsw-alias-label-secondary, inherit); }
478
+ .dsh-atb-iso-fact b { font-weight: 600; color: var(--dsw-alias-state-business-primary, #3e63dd); }
479
+ .dsh-atb-iso-commits { display: flex; flex-direction: column; gap: 3px; margin-bottom: 8px; }
480
+ .dsh-atb-iso-commit { display: flex; gap: 8px; font-size: 11.5px; align-items: baseline; }
481
+ .dsh-atb-iso-commit code {
482
+ font-family: ui-monospace, Consolas, monospace; font-size: 10.5px;
483
+ color: var(--dsh-alias-state-business-primary, #3e63dd); flex-shrink: 0;
484
+ }
485
+ .dsh-atb-iso-commit span { word-break: break-all; color: var(--dsw-alias-label-secondary, inherit); }
486
+ .dsh-atb-iso-more { font-size: 11px; color: var(--dsw-alias-label-tertiary, gray); }
487
+ .dsh-atb-iso-nocommit { font-size: 11.5px; color: var(--dsw-alias-label-tertiary, gray); margin-bottom: 8px; }
488
+ .dsh-atb-iso-dirty {
489
+ font-size: 11.5px; color: var(--dsw-alias-state-error-primary, #e5484d);
490
+ background: rgba(229,72,77,.09); border: 1px solid rgba(229,72,77,.35);
491
+ border-radius: 8px; padding: 6px 10px; margin-bottom: 8px;
492
+ }
493
+ .dsh-atb-iso-actions { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
494
+ .dsh-atb-iso-hint { font-size: 11px; color: var(--dsw-alias-label-tertiary, gray); }
495
+
496
+ /* ---------- 0.3.0 diagnostics ---------- */
497
+ .dsh-atb-diag { max-width: 520px; width: min(520px, 92vw); }
498
+ .dsh-atb-diag-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; margin-bottom: 14px; }
499
+ .dsh-atb-diag-item {
500
+ display: flex; flex-direction: column; align-items: center; gap: 2px;
501
+ padding: 10px 6px; border-radius: 10px;
502
+ border: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,.25));
503
+ background: var(--dsw-alias-bg-layer-1, rgba(128,128,128,.04));
504
+ }
505
+ .dsh-atb-diag-item b { font-size: 18px; font-weight: 700; }
506
+ .dsh-atb-diag-item span { font-size: 10.5px; color: var(--dsw-alias-label-tertiary, gray); }
507
+ .dsh-atb-diag-item[data-bad="true"] b { color: var(--dsw-alias-state-error-primary, #e5484d); }
508
+ .dsh-atb-diag-sec h4 { margin: 0 0 8px; font-size: 12.5px; }
509
+ .dsh-atb-diag-orphans { display: flex; flex-direction: column; gap: 6px; }
510
+ .dsh-atb-diag-orphan {
511
+ display: flex; align-items: center; gap: 10px; justify-content: space-between;
512
+ padding: 7px 10px; border-radius: 8px;
513
+ border: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,.25));
514
+ }
515
+ .dsh-atb-diag-orphan-path { font-size: 11.5px; font-family: ui-monospace, Consolas, monospace; word-break: break-all; }
516
+
517
+ /* ---------- 0.4.0 checklist ---------- */
518
+ .dsh-atb-cke { display: flex; flex-direction: column; gap: 6px; }
519
+ .dsh-atb-cke-row { display: flex; align-items: center; gap: 8px; }
520
+ .dsh-atb-cke-box { flex-shrink: 0; width: 15px; height: 15px; cursor: pointer; }
521
+ .dsh-atb-cke-text {
522
+ flex: 1; min-width: 0; font-size: 12.5px; padding: 6px 9px;
523
+ border-radius: 8px; border: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,.3));
524
+ background: var(--dsw-alias-bg-layer-1, transparent); color: inherit;
525
+ }
526
+ .dsh-atb-cke-del {
527
+ flex-shrink: 0; border: none; background: none; cursor: pointer; padding: 4px;
528
+ color: var(--dsw-alias-label-tertiary, gray); font-size: 12px; border-radius: 6px;
529
+ }
530
+ .dsh-atb-cke-del:hover { color: var(--dsw-alias-state-error-primary, #e5484d); background: rgba(229,72,77,.08); }
531
+ .dsh-atb-cke-add {
532
+ align-self: flex-start; border: 1px dashed var(--dsw-alias-border-l2, rgba(128,128,128,.4));
533
+ background: none; color: var(--dsw-alias-label-secondary, inherit); cursor: pointer;
534
+ font-size: 11.5px; padding: 5px 12px; border-radius: 8px;
535
+ }
536
+ .dsh-atb-cke-add:hover { color: var(--dsw-alias-state-business-primary, #3e63dd); border-color: var(--dsw-alias-state-business-primary, #3e63dd); }
537
+ .dsh-atb-cke-hint { font-size: 10.5px; color: var(--dsw-alias-label-tertiary, gray); }
538
+
539
+ .dsh-atb-cl-progress { margin-left: 8px; font-size: 11px; font-weight: 400; color: var(--dsw-alias-label-tertiary, gray); }
540
+ .dsh-atb-cl-progress[data-tone="bad"] { color: var(--dsw-alias-state-error-primary, #e5484d); font-weight: 600; }
541
+ .dsh-atb-cl-items { display: flex; flex-direction: column; gap: 5px; }
542
+ .dsh-atb-cl-item {
543
+ display: flex; align-items: baseline; gap: 9px; padding: 6px 9px; border-radius: 8px;
544
+ border: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,.22));
545
+ background: var(--dsw-alias-bg-layer-1, rgba(128,128,128,.03)); cursor: pointer;
546
+ }
547
+ .dsh-atb-cl-item:hover { border-color: var(--dsw-alias-border-l2, rgba(128,128,128,.4)); }
548
+ .dsh-atb-cl-item input { flex-shrink: 0; transform: translateY(1px); cursor: pointer; }
549
+ .dsh-atb-cl-item[data-checked="true"] .dsh-atb-cl-text { text-decoration: line-through; color: var(--dsw-alias-label-tertiary, gray); }
550
+ .dsh-atb-cl-item[data-alert="true"] {
551
+ border-color: rgba(229,72,77,.45); background: rgba(229,72,77,.06);
552
+ }
553
+ .dsh-atb-cl-text { flex: 1; min-width: 0; font-size: 12.5px; word-break: break-word; }
554
+ .dsh-atb-cl-meta { flex-shrink: 0; font-size: 10.5px; color: var(--dsw-alias-label-tertiary, gray); display: flex; flex-direction: column; gap: 2px; align-items: flex-end; }
555
+ .dsh-atb-cl-note { max-width: 280px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: var(--dsw-alias-label-secondary, inherit); }
556
+
557
+ /* ---------- 0.4.0 report ---------- */
558
+ .dsh-atb-rpt-summary { font-size: 12.5px; line-height: 1.6; white-space: pre-wrap; word-break: break-word; margin-bottom: 8px; }
559
+ .dsh-atb-rpt-sec { margin-bottom: 8px; }
560
+ .dsh-atb-rpt-label { font-size: 11px; color: var(--dsw-alias-label-tertiary, gray); margin-bottom: 4px; }
561
+ .dsh-atb-rpt-list { margin: 0; padding-left: 18px; font-size: 12px; line-height: 1.55; word-break: break-all; }
562
+ .dsh-atb-rpt-risk {
563
+ font-size: 12px; line-height: 1.55; white-space: pre-wrap; word-break: break-word;
564
+ color: var(--dsw-alias-state-warn-primary, #f5a524);
565
+ background: rgba(245,165,36,.08); border: 1px solid rgba(245,165,36,.3);
566
+ border-radius: 8px; padding: 6px 10px;
567
+ }
568
+
569
+ /* ---------- 0.4.0 diff viewer ---------- */
570
+ .dsh-atb-iso-commit { display: flex; flex-direction: column; gap: 3px; }
571
+ .dsh-atb-iso-commit-btn {
572
+ display: flex; gap: 8px; font-size: 11.5px; align-items: baseline; text-align: left;
573
+ border: none; background: none; padding: 2px 4px; margin: 0 -4px; border-radius: 6px; cursor: pointer;
574
+ color: inherit; width: fit-content; max-width: 100%;
575
+ }
576
+ .dsh-atb-iso-commit-btn:hover { background: var(--dsw-alias-bg-layer-2, rgba(128,128,128,.08)); }
577
+ .dsh-atb-iso-commit-btn code {
578
+ font-family: ui-monospace, Consolas, monospace; font-size: 10.5px;
579
+ color: var(--dsw-alias-state-business-primary, #3e63dd); flex-shrink: 0;
580
+ }
581
+ .dsh-atb-iso-commit-btn span { word-break: break-all; color: var(--dsw-alias-label-secondary, inherit); }
582
+ .dsh-atb-iso-commit[data-open="true"] > .dsh-atb-iso-commit-btn code { font-weight: 700; }
583
+ .dsh-atb-iso-dirty { display: flex; flex-direction: column; gap: 6px;
584
+ font-size: 11.5px; color: var(--dsw-alias-state-error-primary, #e5484d);
585
+ background: rgba(229,72,77,.09); border: 1px solid rgba(229,72,77,.35);
586
+ border-radius: 8px; padding: 6px 10px; margin-bottom: 8px;
587
+ }
588
+ .dsh-atb-iso-dirty-toggle { border: none; background: none; cursor: pointer; padding: 0; text-align: left; color: inherit; font-size: inherit; }
589
+ .dsh-atb-iso-dirty-files { display: flex; flex-direction: column; gap: 2px; }
590
+ .dsh-atb-iso-dirty-file {
591
+ border: none; background: none; cursor: pointer; text-align: left; padding: 1px 2px;
592
+ font-size: 11px; color: var(--dsw-alias-label-secondary, inherit); border-radius: 4px; word-break: break-all;
593
+ }
594
+ .dsh-atb-iso-dirty-file:hover { background: rgba(128,128,128,.1); color: var(--dsw-alias-state-business-primary, #3e63dd); }
595
+ .dsh-atb-iso-dirty-file code { font-family: ui-monospace, Consolas, monospace; font-size: 10px; margin-right: 6px; }
596
+ .dsh-atb-diffview { margin-top: 6px; border-radius: 8px; overflow: hidden;
597
+ border: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,.25)); }
598
+ .dsh-atb-diffview-head { display: flex; align-items: center; gap: 10px; padding: 5px 10px;
599
+ background: var(--dsw-alias-bg-layer-2, rgba(128,128,128,.08)); }
600
+ .dsh-atb-diffview-title { font-family: ui-monospace, Consolas, monospace; font-size: 10.5px;
601
+ color: var(--dsw-alias-state-business-primary, #3e63dd); word-break: break-all; }
602
+ .dsh-atb-diffview-hint { font-size: 10.5px; color: var(--dsw-alias-label-tertiary, gray); }
603
+ .dsh-atb-diffview-error { padding: 8px 10px; font-size: 11.5px; color: var(--dsw-alias-state-error-primary, #e5484d); }
604
+ .dsh-atb-diffview-pre {
605
+ margin: 0; padding: 8px 10px; max-height: 340px; overflow: auto;
606
+ font-family: ui-monospace, Consolas, monospace; font-size: 10.5px; line-height: 1.5;
607
+ white-space: pre; color: var(--dsw-alias-label-secondary, inherit);
608
+ }
609
+
610
+ /* ---------- 0.4.0 new-task menu + template manager + import ---------- */
611
+ .dsh-atb-newmenu { position: relative; display: inline-flex; }
612
+ .dsh-atb-newmenu-backdrop { position: fixed; inset: 0; z-index: 40; }
613
+ .dsh-atb-newmenu-list {
614
+ position: absolute; top: calc(100% + 4px); left: 0; z-index: 41; min-width: 180px;
615
+ display: flex; flex-direction: column; padding: 5px; border-radius: 10px;
616
+ background: var(--dsw-alias-bg-overlay, #fff); color: var(--dsw-alias-label-primary, inherit);
617
+ border: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,.25));
618
+ box-shadow: var(--dsw-shadow-lv3, 0 12px 32px rgba(0,0,0,.18));
619
+ }
620
+ .dsh-atb-newmenu-opt {
621
+ border: none; background: none; text-align: left; cursor: pointer; padding: 7px 10px;
622
+ font-size: 12.5px; color: inherit; border-radius: 7px; white-space: nowrap;
623
+ }
624
+ .dsh-atb-newmenu-opt:hover { background: var(--dsw-alias-bg-layer-2, rgba(128,128,128,.1)); }
625
+ .dsh-atb-newmenu-sep { height: 1px; margin: 4px 6px; background: var(--dsw-alias-border-l2, rgba(128,128,128,.25)); }
626
+
627
+ .dsh-atb-tplm { max-width: 560px; width: min(560px, 92vw); }
628
+ .dsh-atb-tplm-list { display: flex; flex-direction: column; gap: 8px; }
629
+ .dsh-atb-tplm-row {
630
+ display: flex; align-items: center; gap: 10px; padding: 8px 10px; border-radius: 8px;
631
+ border: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,.25));
632
+ }
633
+ .dsh-atb-tplm-name {
634
+ flex: 0 0 160px; font-size: 12.5px; padding: 5px 8px; border-radius: 7px;
635
+ border: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,.3));
636
+ background: var(--dsw-alias-bg-layer-1, transparent); color: inherit;
637
+ }
638
+ .dsh-atb-tplm-meta { flex: 1; min-width: 0; font-size: 10.5px; color: var(--dsw-alias-label-tertiary, gray); }
639
+ .dsh-atb-tplm-btns { display: flex; gap: 6px; flex-shrink: 0; }
640
+
641
+ .dsh-atb-imp { max-width: 600px; width: min(600px, 92vw); }
642
+ .dsh-atb-imp-picker { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
643
+ .dsh-atb-imp-picker input[type="file"] { font-size: 12px; }
644
+ .dsh-atb-imp-filename { font-size: 11.5px; color: var(--dsw-alias-state-business-primary, #3e63dd); word-break: break-all; }
645
+ .dsh-atb-imp-note { font-size: 11px; color: var(--dsw-alias-label-tertiary, gray); margin-bottom: 10px; }
646
+ .dsh-atb-imp-error { font-size: 12px; color: var(--dsw-alias-state-error-primary, #e5484d); margin-bottom: 8px; }
647
+ .dsh-atb-imp-stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-bottom: 12px; }
648
+ .dsh-atb-imp-stat {
649
+ display: flex; flex-direction: column; align-items: center; gap: 2px; padding: 9px 6px; border-radius: 9px;
650
+ border: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,.25));
651
+ }
652
+ .dsh-atb-imp-stat b { font-size: 17px; font-weight: 700; }
653
+ .dsh-atb-imp-stat span { font-size: 10.5px; color: var(--dsw-alias-label-tertiary, gray); }
654
+ .dsh-atb-imp-stat[data-tone="ok"] b { color: var(--dsw-alias-state-success-primary, #30a46c); }
655
+ .dsh-atb-imp-stat[data-tone="warn"] b { color: var(--dsw-alias-state-warn-primary, #f5a524); }
656
+ .dsh-atb-imp-stat[data-tone="bad"] b { color: var(--dsw-alias-state-error-primary, #e5484d); }
657
+ .dsh-atb-imp-sec h4 { margin: 0 0 6px; font-size: 12px; }
658
+ .dsh-atb-imp-sec { margin-bottom: 10px; }
659
+ .dsh-atb-imp-list { display: flex; flex-direction: column; gap: 4px; max-height: 160px; overflow-y: auto; }
660
+ .dsh-atb-imp-row {
661
+ display: flex; align-items: center; gap: 10px; justify-content: space-between;
662
+ padding: 5px 9px; border-radius: 7px; border: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,.2));
663
+ }
664
+ .dsh-atb-imp-row[data-tone="bad"] { border-color: rgba(229,72,77,.35); }
665
+ .dsh-atb-imp-row-title { font-size: 12px; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
666
+ .dsh-atb-imp-row-status { font-size: 10.5px; color: var(--dsw-alias-label-tertiary, gray); flex-shrink: 0; }
667
+ .dsh-atb-imp-result { font-size: 12px; color: var(--dsw-alias-state-success-primary, #30a46c); margin-top: 10px; }
668
+ .dsh-atb-badge[data-kind="checklist"] { color: var(--dsw-alias-label-secondary, inherit); }
471
669
  `
472
670
 
473
671
  let injected = false