dsh-taskboard 0.5.0 → 0.5.2

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 (50) hide show
  1. package/README.md +25 -1
  2. package/lib/client.js +242 -174
  3. package/lib/host/execution.js +80 -33
  4. package/lib/host/execution.js.map +1 -1
  5. package/lib/host/git.js +49 -5
  6. package/lib/host/git.js.map +1 -1
  7. package/lib/host/routes.js +180 -109
  8. package/lib/host/routes.js.map +1 -1
  9. package/lib/host/scheduler.js +50 -28
  10. package/lib/host/scheduler.js.map +1 -1
  11. package/lib/host/sdk.js +7 -2
  12. package/lib/host/sdk.js.map +1 -1
  13. package/lib/host/store.js +41 -8
  14. package/lib/host/store.js.map +1 -1
  15. package/lib/host/templates.js +10 -3
  16. package/lib/host/templates.js.map +1 -1
  17. package/lib/host/tools.js +124 -93
  18. package/lib/host/tools.js.map +1 -1
  19. package/lib/index.js +3 -1
  20. package/lib/index.js.map +1 -1
  21. package/lib/shared/api.js.map +1 -1
  22. package/lib/shared/protocol.js +23 -2
  23. package/lib/shared/protocol.js.map +1 -1
  24. package/package.json +3 -2
  25. package/src/client/api.ts +19 -9
  26. package/src/client/board/ImportModal.tsx +1 -1
  27. package/src/client/board/TaskBoard.tsx +7 -38
  28. package/src/client/board/TaskCard.tsx +3 -5
  29. package/src/client/board/TaskDetail.tsx +30 -21
  30. package/src/client/board/TaskFormModal.tsx +30 -23
  31. package/src/client/board/format.ts +26 -0
  32. package/src/client/board/labels.ts +44 -0
  33. package/src/client/board-mount.tsx +9 -6
  34. package/src/client/controller.ts +60 -13
  35. package/src/client/index.ts +7 -5
  36. package/src/client/sidebar-entry.ts +16 -5
  37. package/src/client/styles.ts +5 -3
  38. package/src/host/execution.ts +90 -16
  39. package/src/host/git.ts +39 -10
  40. package/src/host/routes.ts +227 -126
  41. package/src/host/scheduler.ts +62 -36
  42. package/src/host/sdk.ts +12 -1
  43. package/src/host/store.ts +53 -7
  44. package/src/host/templates.ts +12 -3
  45. package/src/host/tools.ts +180 -123
  46. package/src/index.ts +10 -1
  47. package/src/shared/api.ts +1 -1
  48. package/src/shared/protocol.ts +35 -1
  49. package/src/shared/version.ts +1 -1
  50. package/src/client/board/NewTaskModal.tsx +0 -8
@@ -13,7 +13,7 @@ import type { BoardController } from '../controller.ts'
13
13
  import type { TaskTemplateSpec } from '../../shared/api.ts'
14
14
  import type { ChecklistItem, IsolationMode, Urgency } from '../../shared/protocol.ts'
15
15
  import { MAX_CHECKLIST_ITEMS, defaultIsolationOf, nextCronTime, parseCron } from '../../shared/protocol.ts'
16
- import { fmtTime } from './TaskBoard.tsx'
16
+ import { fmtTime } from './format.ts'
17
17
 
18
18
  /** One row of the configured model catalog (from llm.models). */
19
19
  export interface CatalogModel { provider: string; model: string; name?: string }
@@ -144,6 +144,10 @@ export function TaskFormModal({ controller, task }: { controller: BoardControlle
144
144
  : (prefill?.checklist ?? []).map(text => ({ text, checked: false })),
145
145
  )
146
146
  const titleRef = useRef<HTMLInputElement>(null)
147
+ // One in-flight write at a time: the foot buttons disable while a
148
+ // create/update/run round-trip is pending — a double click used to fire
149
+ // duplicate creates (and runs) before the first one returned (review P0).
150
+ const [busy, setBusy] = useState(false)
147
151
 
148
152
  // Focus the title and close on Esc while the dialog is open.
149
153
  useEffect(() => {
@@ -155,9 +159,9 @@ export function TaskFormModal({ controller, task }: { controller: BoardControlle
155
159
  return () => document.removeEventListener('keydown', onKey)
156
160
  }, [controller])
157
161
 
158
- // Model catalog: the plugin face provides it when the runtime is up.
162
+ // Model catalog: the controller exposes the installed face when the runtime is up.
159
163
  useEffect(() => {
160
- const face = (controller as unknown as { modelCatalog?: () => Promise<CatalogModel[]> }).modelCatalog
164
+ const face = controller.modelCatalog
161
165
  if (face === undefined) return
162
166
  void face().then(setCatalog).catch(() => setCatalog([]))
163
167
  }, [controller])
@@ -166,14 +170,18 @@ export function TaskFormModal({ controller, task }: { controller: BoardControlle
166
170
  // create mode (unless a template pinned one) so executions run with a
167
171
  // real tool set out of the box.
168
172
  useEffect(() => {
169
- const face = (controller as unknown as { presetCatalog?: () => Promise<{ presets: Array<{ id: string; name?: string }>; defaultId?: string }> }).presetCatalog
173
+ const face = controller.presetCatalog
170
174
  if (face === undefined) return
171
175
  void face().then(roster => {
172
176
  setPresets(roster.presets)
173
177
  setPresetDefault(roster.defaultId)
174
- if (task?.presetId === undefined && initialPreset === '' && roster.defaultId !== undefined) setPresetId(roster.defaultId)
178
+ // CREATE mode only (review P1): pre-selecting in edit mode would
179
+ // silently pin the deployment default onto tasks that deliberately
180
+ // follow it. In create mode `task` is undefined, so checking
181
+ // `initialPreset` (template pin) alone is sufficient.
182
+ if (!editing && initialPreset === '' && roster.defaultId !== undefined) setPresetId(roster.defaultId)
175
183
  }).catch(() => setPresets([]))
176
- }, [controller, task?.presetId, initialPreset])
184
+ }, [controller, editing, task?.presetId, initialPreset])
177
185
 
178
186
  // Live cron validation + next-run preview (same math as the host).
179
187
  const cronMatch = mode === 'scheduled' ? parseCron(cron.trim()) : null
@@ -208,13 +216,14 @@ export function TaskFormModal({ controller, task }: { controller: BoardControlle
208
216
  const filledRows = (): CheckRow[] => checkRows.map(r => ({ ...r, text: r.text.trim() })).filter(r => r.text.length > 0)
209
217
 
210
218
  const submit = (): void => {
211
- if (!valid) return
219
+ if (!valid || busy) return
212
220
  const picked = model !== '' ? (JSON.parse(model) as { provider: string; model: string }) : undefined
213
221
  const isolationOut = isolationPayload()
214
222
  const presetOut = presetPayload()
215
223
  const rows = filledRows()
216
- if (editing) {
217
- void controller.update(task.id, task.version, {
224
+ setBusy(true)
225
+ const action = editing
226
+ ? controller.update(task.id, task.version, {
218
227
  title,
219
228
  description,
220
229
  prompt,
@@ -228,8 +237,7 @@ export function TaskFormModal({ controller, task }: { controller: BoardControlle
228
237
  // [] clears the checklist (host deletes the field on empty).
229
238
  checklist: rows.length > 0 ? rows : null,
230
239
  })
231
- } else {
232
- void controller.create({
240
+ : controller.create({
233
241
  title,
234
242
  workspaceId,
235
243
  urgency,
@@ -241,18 +249,19 @@ export function TaskFormModal({ controller, task }: { controller: BoardControlle
241
249
  ...(presetOut !== undefined ? { presetId: presetOut } : {}),
242
250
  ...(rows.length > 0 ? { checklist: rows.map(r => r.text) } : {}),
243
251
  })
244
- }
252
+ void action.catch(() => undefined).finally(() => setBusy(false))
245
253
  }
246
254
 
247
255
  /** Save the form, then immediately trigger a manual run of the task. */
248
256
  const submitAndRun = (): void => {
249
- if (!valid || runBlocked) return
257
+ if (!valid || runBlocked || busy) return
250
258
  const picked = model !== '' ? (JSON.parse(model) as { provider: string; model: string }) : undefined
251
259
  const isolationOut = isolationPayload()
252
260
  const presetOut = presetPayload()
253
261
  const rows = filledRows()
254
- if (editing) {
255
- void (async () => {
262
+ setBusy(true)
263
+ void (async () => {
264
+ if (editing) {
256
265
  const saved = await controller.update(task.id, task.version, {
257
266
  title,
258
267
  description,
@@ -266,9 +275,7 @@ export function TaskFormModal({ controller, task }: { controller: BoardControlle
266
275
  checklist: rows.length > 0 ? rows : null,
267
276
  })
268
277
  if (saved) await controller.run(task.id)
269
- })()
270
- } else {
271
- void (async () => {
278
+ } else {
272
279
  const id = await controller.create({
273
280
  title,
274
281
  workspaceId,
@@ -282,8 +289,8 @@ export function TaskFormModal({ controller, task }: { controller: BoardControlle
282
289
  ...(rows.length > 0 ? { checklist: rows.map(r => r.text) } : {}),
283
290
  })
284
291
  if (id !== undefined) await controller.run(id)
285
- })()
286
- }
292
+ }
293
+ })().catch(() => undefined).finally(() => setBusy(false))
287
294
  }
288
295
 
289
296
  const hint = !valid
@@ -450,13 +457,13 @@ export function TaskFormModal({ controller, task }: { controller: BoardControlle
450
457
  <button
451
458
  type="button"
452
459
  className="dsh-atb-btn"
453
- disabled={!valid || runBlocked}
454
- title={runBlocked ? '任务正在执行中,不能重复发起' : '保存后立即发起执行(新会话)'}
460
+ disabled={!valid || runBlocked || busy}
461
+ title={runBlocked ? '任务正在执行中,不能重复发起' : busy ? '正在提交…' : '保存后立即发起执行(新会话)'}
455
462
  onClick={submitAndRun}
456
463
  >
457
464
  ⚡ 立即执行
458
465
  </button>
459
- <button type="button" className="dsh-atb-btn" data-primary="true" disabled={!valid} onClick={submit}>
466
+ <button type="button" className="dsh-atb-btn" data-primary="true" disabled={!valid || busy} onClick={submit}>
460
467
  {editing ? '保存修改' : '创建任务'}
461
468
  </button>
462
469
  </span>
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Pure display helpers shared by the board components.
3
+ *
4
+ * Review P2: these lived in TaskBoard.tsx and were imported back out by
5
+ * TaskCard/TaskDetail/TaskFormModal, forming import cycles with the view
6
+ * root. They have no component dependencies — they belong here.
7
+ *
8
+ * @module dsh-taskboard/client/board/format
9
+ */
10
+ import type { TaskRecord } from '../../shared/protocol.ts'
11
+
12
+ /** Format an epoch ms as a short local stamp. */
13
+ export function fmtTime(ms: number | undefined): string {
14
+ if (ms === undefined) return ''
15
+ const d = new Date(ms)
16
+ const pad = (n: number) => String(n).padStart(2, '0')
17
+ return `${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`
18
+ }
19
+
20
+ /** A claim idle for longer than this is highlighted as stale (ms). */
21
+ export const STALE_CLAIM_MS = 30 * 60_000
22
+
23
+ /** Whether the task's claim is stale (in_progress, held, idle too long). */
24
+ export function isStaleClaim(task: TaskRecord, now: number): boolean {
25
+ return task.status === 'in_progress' && task.claimedAt !== undefined && now - task.claimedAt > STALE_CLAIM_MS
26
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Centralized zh-CN display labels for the board UI.
3
+ *
4
+ * Review P2: the same status/urgency/outcome text used to live in three
5
+ * components (TaskBoard / TaskCard / TaskDetail) and drifted — adding a
6
+ * status meant three edits, missing one leaked the raw English key.
7
+ *
8
+ * @module dsh-taskboard/client/board/labels
9
+ */
10
+ import type { TaskStatus, Urgency } from '../../shared/protocol.ts'
11
+
12
+ /** Column headers on the five-column main board (+ secondary tab). */
13
+ export const COLUMN_LABELS: Readonly<Record<TaskStatus, string>> = {
14
+ backlog: '待规划',
15
+ todo: '待办',
16
+ in_progress: '进行中',
17
+ in_review: '待验收',
18
+ done: '已完成',
19
+ canceled: '已取消',
20
+ archived: '已归档',
21
+ }
22
+
23
+ /** Status pill text (detail pane) — historical wording kept verbatim:
24
+ * terminal states read short here, the column headers carry the full forms. */
25
+ export const STATUS_LABEL: Readonly<Record<TaskStatus, string>> = {
26
+ backlog: '待规划', todo: '待办', in_progress: '进行中', in_review: '待验收',
27
+ done: '完成', canceled: '取消', archived: '归档',
28
+ }
29
+
30
+ /** Move-button verbs (shorter than the pill text). */
31
+ export const MOVE_LABEL: Readonly<Record<TaskStatus, string>> = {
32
+ backlog: '待规划', todo: '待办', in_progress: '进行中', in_review: '待验收',
33
+ done: '完成', canceled: '取消', archived: '归档',
34
+ }
35
+
36
+ /** Urgency chip labels. */
37
+ export const URGENCY_LABEL: Readonly<Record<Urgency, string>> = {
38
+ urgent: '紧急', normal: '一般', relaxed: '不急',
39
+ }
40
+
41
+ /** Execution outcome labels. */
42
+ export const OUTCOME_LABEL: Readonly<Record<string, string>> = {
43
+ running: '执行中', succeeded: '成功', failed: '失败', cancelled: '已取消',
44
+ }
@@ -4,11 +4,14 @@
4
4
  * conversation content while the board is active. Toggling rides a data
5
5
  * attribute on <html> — no React involvement in the shell.
6
6
  *
7
- * Column matching is DUAL (0.4.2): the dev shell marks the column with
8
- * `data-pane="conversation"`; the DSH Desktop shell (dsh-client-ui-layout)
9
- * dropped data-pane entirely and uses CSS-Module hashed class names
10
- * (`pI_x6G_centerCol`) — the class-substring fallback keeps both mounting,
11
- * exactly like sidebar-entry's `[class*="sidebarCol"]` fallback.
7
+ * Column matching is TRIPLE-generation: the dev shell marks the column
8
+ * with `data-pane="conversation"`; the official layout shell
9
+ * (dsh-client-ui-layout) dropped data-pane and uses CSS-Module hashed
10
+ * class names (`pI_x6G_centerCol`) — and DSH Desktop's non-compat
11
+ * (extended) mode disables the official layout row entirely, owning the
12
+ * columns itself (`main.dshDesktopConversationSurface`, 0.5.2) — the
13
+ * fallbacks keep all three mounting, exactly like sidebar-entry's column
14
+ * selector.
12
15
  *
13
16
  * @module dsh-taskboard/client/board-mount
14
17
  */
@@ -20,7 +23,7 @@ import { ENTRY_SELECTOR } from './sidebar-entry.ts'
20
23
  /** The injected board container. */
21
24
  export const BOARD_VIEW_SELECTOR = '[data-dsh-atb-view]'
22
25
 
23
- const CONVERSATION_COLUMN_SELECTOR = '[data-pane="conversation"], [class*="centerCol"]'
26
+ const CONVERSATION_COLUMN_SELECTOR = '[data-pane="conversation"], [class*="centerCol"], .dshDesktopConversationSurface'
24
27
  const ACTIVE_ATTR = 'data-dsh-atb-active'
25
28
  /** Sibling panels' activation attributes, evicted when this board opens. */
26
29
  const OTHER_ACTIVE_ATTRS = ['data-dsh-taskboard-active', 'data-dsh-ssh-active']
@@ -109,7 +109,14 @@ export class BoardController {
109
109
  private disposed = false
110
110
  private disposeStream: (() => void) | undefined
111
111
  private refreshInFlight: Promise<void> | undefined
112
+ /** Newest change-frame revision seen on the SSE stream (S16 refresh chase). */
113
+ private seenRevision: number | undefined
112
114
  private sessionJumper: ((sessionId: string) => Promise<SessionJumpResult>) | undefined
115
+ /** Composer catalog faces, installed formally by the client entry (T13). */
116
+ private readonly catalogFaces: {
117
+ models?: () => Promise<Array<{ provider: string; model: string; name?: string }>>
118
+ presets?: () => Promise<{ presets: Array<{ id: string; name?: string }>; defaultId?: string }>
119
+ } = {}
113
120
 
114
121
  /** @param client - the route client. */
115
122
  constructor(private readonly client: TaskboardClient) {}
@@ -140,8 +147,10 @@ export class BoardController {
140
147
  void this.refresh()
141
148
  this.disposeStream = this.client.stream(
142
149
  (change: ChangeEvent) => {
143
- this.setState({ ledger: { ...this.state.ledger, revision: change.revision } })
150
+ this.seenRevision = change.revision
144
151
  // Any change invalidates the full snapshot; refetch (cheap, local).
152
+ // No intermediate revision-only setState — the refresh result is the
153
+ // single render a frame produces (review P2: frames rendered twice).
145
154
  void this.refresh()
146
155
  },
147
156
  () => { void this.refresh() },
@@ -153,15 +162,23 @@ export class BoardController {
153
162
  if (this.refreshInFlight !== undefined) return this.refreshInFlight
154
163
  this.refreshInFlight = (async () => {
155
164
  try {
156
- const [ledger, workspaces] = await Promise.all([
157
- this.client.state(),
158
- this.client.workspaces(),
159
- ])
160
- let selected: TaskRecord | undefined
161
- if (this.state.selectedId !== undefined) {
162
- selected = ledger.tasks.find(t => t.id === this.state.selectedId)
165
+ // S16: a change frame landing while a fetch is in flight used to
166
+ // strand the board on a stale snapshot forever (the deduped request
167
+ // predates the newest frame and no further event arrives). Chase the
168
+ // newest seen revision — bounded rounds, then give up until the next
169
+ // frame.
170
+ for (let round = 0; round < 3; round++) {
171
+ const [ledger, workspaces] = await Promise.all([
172
+ this.client.state(),
173
+ this.client.workspaces(),
174
+ ])
175
+ let selected: TaskRecord | undefined
176
+ if (this.state.selectedId !== undefined) {
177
+ selected = ledger.tasks.find(t => t.id === this.state.selectedId)
178
+ }
179
+ this.setState({ ledger, workspaces, error: undefined, selectedId: selected === undefined ? undefined : this.state.selectedId })
180
+ if (this.seenRevision === undefined || ledger.revision >= this.seenRevision) break
163
181
  }
164
- this.setState({ ledger, workspaces, error: undefined, selectedId: selected === undefined ? undefined : this.state.selectedId })
165
182
  } catch (error) {
166
183
  this.setState({ error: error instanceof Error ? error.message : String(error) })
167
184
  } finally {
@@ -260,6 +277,26 @@ export class BoardController {
260
277
  this.sessionJumper = jumper
261
278
  }
262
279
 
280
+ /** 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 {
282
+ this.catalogFaces.models = fn
283
+ }
284
+
285
+ /** T13: formal installer for the preset roster face. */
286
+ installPresetRoster(fn: () => Promise<{ presets: Array<{ id: string; name?: string }>; defaultId?: string }>): void {
287
+ this.catalogFaces.presets = fn
288
+ }
289
+
290
+ /** The installed model catalog face, when the runtime provides one. */
291
+ get modelCatalog(): (() => Promise<Array<{ provider: string; model: string; name?: string }>>) | undefined {
292
+ return this.catalogFaces.models
293
+ }
294
+
295
+ /** The installed preset roster face, when the runtime provides one. */
296
+ get presetCatalog(): (() => Promise<{ presets: Array<{ id: string; name?: string }>; defaultId?: string }>) | undefined {
297
+ return this.catalogFaces.presets
298
+ }
299
+
263
300
  /**
264
301
  * Jump to an execution's session (open it in the GUI). On success the board
265
302
  * closes so the conversation shows; a deleted-or-archived session reports
@@ -373,13 +410,18 @@ export class BoardController {
373
410
  }
374
411
  }
375
412
 
376
- /** Append a user comment. */
377
- async comment(id: string, body: string): Promise<void> {
413
+ /**
414
+ * Append a user comment. Returns whether it landed — the composer keeps its
415
+ * text on failure (T13: it used to clear unconditionally and lose the draft).
416
+ */
417
+ async comment(id: string, body: string): Promise<boolean> {
378
418
  try {
379
419
  await this.client.comment(id, body)
380
420
  await this.refresh()
421
+ return true
381
422
  } catch (error) {
382
423
  this.setState({ error: error instanceof Error ? error.message : String(error) })
424
+ return false
383
425
  }
384
426
  }
385
427
 
@@ -490,7 +532,8 @@ export class BoardController {
490
532
  async duplicate(task: TaskRecord): Promise<void> {
491
533
  try {
492
534
  await this.client.create({
493
- title: `${task.title}(副本)`,
535
+ // Keep the suffix under the host's 200-char title cap (review P1).
536
+ title: `${task.title.slice(0, 196)}(副本)`,
494
537
  workspaceId: task.workspaceId,
495
538
  urgency: task.urgency,
496
539
  description: task.description.length > 0 ? task.description : undefined,
@@ -620,7 +663,11 @@ export class BoardController {
620
663
  /** Download the task list as a CSV (BOM-prefixed for Excel + Chinese text). */
621
664
  exportCsv(): void {
622
665
  const esc = (v: unknown): string => {
623
- const s = String(v ?? '')
666
+ let s = String(v ?? '')
667
+ // S17: formula-injection guard — title/description are agent-controllable
668
+ // and a cell starting with = + - @ would be EXECUTED as a formula by
669
+ // Excel; neutralize with a leading apostrophe.
670
+ if (/^[=+\-@\t\r]/.test(s)) s = `'${s}`
624
671
  return /[",\n\r]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s
625
672
  }
626
673
  const header = ['id', 'title', 'status', 'urgency', 'blocked', 'project', 'claimedBy', 'mode', 'cron', 'nextRunAt', 'model', 'createdAt', 'updatedAt', 'comments', 'executions']
@@ -52,11 +52,13 @@ export function apply(ctx: ClientContextFace): void {
52
52
  const client = createClient()
53
53
  const controller = new BoardController(client)
54
54
 
55
- // Model catalog for the composer: llm.models over the connection RPC.
55
+ // Model catalog for the composer: llm.models over the connection RPC
56
+ // installed through the controller's formal installer (T13: no more
57
+ // monkeypatched instance properties).
56
58
  const connection = ctx.get?.('connection') as ConnectionFace | undefined
57
59
  if (connection !== undefined) {
58
60
  type CatalogRow = { provider: string; model: string; name?: string }
59
- ;(controller as unknown as { modelCatalog?: () => Promise<CatalogRow[]> }).modelCatalog = async (): Promise<CatalogRow[]> => {
61
+ controller.installModelCatalog(async (): Promise<CatalogRow[]> => {
60
62
  const response = await connection.api.llm.models({})
61
63
  if (!response.result.ok) return []
62
64
  const out: CatalogRow[] = []
@@ -66,13 +68,13 @@ export function apply(ctx: ClientContextFace): void {
66
68
  }
67
69
  }
68
70
  return out
69
- }
71
+ })
70
72
 
71
73
  // Preset roster for the composer (0.3.3): agentPreset.list over the
72
74
  // connection RPC — [{id, name}] plus which one is the deployment
73
75
  // default (the form pre-selects it on create).
74
76
  type PresetRow = { id: string; name?: string }
75
- ;(controller as unknown as { presetCatalog?: () => Promise<{ presets: PresetRow[]; defaultId?: string }> }).presetCatalog = async (): Promise<{ presets: PresetRow[]; defaultId?: string }> => {
77
+ controller.installPresetRoster(async (): Promise<{ presets: PresetRow[]; defaultId?: string }> => {
76
78
  const list = connection.api.agentPresets
77
79
  if (list === undefined) return { presets: [] }
78
80
  const response = await list.list({})
@@ -80,7 +82,7 @@ export function apply(ctx: ClientContextFace): void {
80
82
  const presets = response.result.value.presets.map((p: { id: string; name?: string }) => ({ id: p.id, name: p.name }))
81
83
  const def = response.result.value.presets.find((p: { id: string; isDefault: boolean }) => p.isDefault)
82
84
  return { presets, ...(def !== undefined ? { defaultId: def.id } : {}) }
83
- }
85
+ })
84
86
  }
85
87
 
86
88
  // Session navigation for execution rows: resolved LAZILY on every jump —
@@ -19,15 +19,22 @@ import type { BoardController } from './controller.ts'
19
19
  /** Stable data attribute identifying this entry row. */
20
20
  export const ENTRY_SELECTOR = '[data-dsh-atb-entry]'
21
21
 
22
- /** Inline icon (16px nav-icon look). */
23
- const ICON = '<svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2" y="2.5" width="12" height="11" rx="1.5"/><path d="M2 6.5h12M6.5 6.5v7"/></svg>'
22
+ /** Inline icon: a three-lane kanban board (16px nav-icon look). */
23
+ const ICON = '<svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2" y="2" width="12" height="12" rx="2"/><path d="M6 2v12M10 2v12"/></svg>'
24
24
 
25
25
  /**
26
26
  * Find the sidebar shell root element, or undefined while not yet mounted.
27
- * (Same as the working family plugins: sidebarCol pane → logoRow owner.)
27
+ * Triple-generation matching (0.5.2): the dev shell's data-pane pane, the
28
+ * official layout's CSS-Module sidebarCol, and DSH Desktop's non-compat
29
+ * (extended) frame — which disables the official ui-layout row and owns
30
+ * the columns itself (aside.dshDesktopSidebarSurface >
31
+ * div.dshDesktopUpstreamSidebar wrapping the unchanged official sidebar).
32
+ * logoRow-owner resolution is identical on all three generations.
28
33
  */
29
34
  function sidebarRoot(): HTMLElement | undefined {
30
- const column = document.querySelector<HTMLElement>('[data-pane="sidebar"], [class*="sidebarCol"]')
35
+ const column = document.querySelector<HTMLElement>(
36
+ '[data-pane="sidebar"], [class*="sidebarCol"], .dshDesktopUpstreamSidebar, .dshDesktopSidebarSurface',
37
+ )
31
38
  if (column === null) return undefined
32
39
  const logoOwner = column.querySelector<HTMLElement>('[class*="logoRow"]')?.parentElement
33
40
  return logoOwner ?? (column.firstElementChild as HTMLElement | undefined)
@@ -196,7 +203,11 @@ interface AtbDebug { attempts: number; found: boolean; placed: boolean }
196
203
  export function mountSidebarEntry(controller: BoardController): () => void {
197
204
  const entry = createEntry(controller)
198
205
  const debug: AtbDebug = { attempts: 0, found: false, placed: false }
199
- ;(window as unknown as { __atbDebug?: AtbDebug }).__atbDebug = debug
206
+ // Debug handle only where the GUI runs locally; never on remote origins.
207
+ const host = globalThis.location?.hostname
208
+ if (host === 'localhost' || host === '127.0.0.1') {
209
+ ;(window as unknown as { __atbDebug?: AtbDebug }).__atbDebug = debug
210
+ }
200
211
  let root: HTMLElement | undefined
201
212
  let placed = false
202
213
 
@@ -69,10 +69,12 @@ export const STYLES = `
69
69
  .dsh-atb-search { width: 130px; }
70
70
  .dsh-atb-badge[data-kind="stale"] { background: rgba(217,130,43,.15); color: #d9822b; }
71
71
 
72
- /* 0.4.2: dual column matching — dev shell's data-pane pane OR the Desktop
73
- * shell's CSS-Module hashed centerCol (see board-mount.tsx). */
72
+ /* Triple-generation column matching — dev shell's data-pane pane, the
73
+ * official layout shell's CSS-Module hashed centerCol (0.4.2), or DSH
74
+ * Desktop's non-compat extended frame surface (0.5.2, see board-mount.tsx). */
74
75
  html[data-dsh-atb-active] [data-pane="conversation"] > *:not([data-dsh-atb-view]),
75
- html[data-dsh-atb-active] [class*="centerCol"] > *:not([data-dsh-atb-view]) { display: none !important; }
76
+ html[data-dsh-atb-active] [class*="centerCol"] > *:not([data-dsh-atb-view]),
77
+ html[data-dsh-atb-active] .dshDesktopConversationSurface > *:not([data-dsh-atb-view]) { display: none !important; }
76
78
  .dsh-atb-view { display: none; }
77
79
  html[data-dsh-atb-active] .dsh-atb-view { display: flex; flex-direction: column; height: 100%; overflow: hidden; }
78
80