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.
- package/README.md +25 -1
- package/lib/client.js +242 -174
- package/lib/host/execution.js +80 -33
- package/lib/host/execution.js.map +1 -1
- package/lib/host/git.js +49 -5
- package/lib/host/git.js.map +1 -1
- package/lib/host/routes.js +180 -109
- package/lib/host/routes.js.map +1 -1
- package/lib/host/scheduler.js +50 -28
- package/lib/host/scheduler.js.map +1 -1
- package/lib/host/sdk.js +7 -2
- package/lib/host/sdk.js.map +1 -1
- package/lib/host/store.js +41 -8
- package/lib/host/store.js.map +1 -1
- package/lib/host/templates.js +10 -3
- package/lib/host/templates.js.map +1 -1
- package/lib/host/tools.js +124 -93
- package/lib/host/tools.js.map +1 -1
- package/lib/index.js +3 -1
- package/lib/index.js.map +1 -1
- package/lib/shared/api.js.map +1 -1
- package/lib/shared/protocol.js +23 -2
- package/lib/shared/protocol.js.map +1 -1
- package/package.json +3 -2
- package/src/client/api.ts +19 -9
- package/src/client/board/ImportModal.tsx +1 -1
- package/src/client/board/TaskBoard.tsx +7 -38
- package/src/client/board/TaskCard.tsx +3 -5
- package/src/client/board/TaskDetail.tsx +30 -21
- package/src/client/board/TaskFormModal.tsx +30 -23
- package/src/client/board/format.ts +26 -0
- package/src/client/board/labels.ts +44 -0
- package/src/client/board-mount.tsx +9 -6
- package/src/client/controller.ts +60 -13
- package/src/client/index.ts +7 -5
- package/src/client/sidebar-entry.ts +16 -5
- package/src/client/styles.ts +5 -3
- package/src/host/execution.ts +90 -16
- package/src/host/git.ts +39 -10
- package/src/host/routes.ts +227 -126
- package/src/host/scheduler.ts +62 -36
- package/src/host/sdk.ts +12 -1
- package/src/host/store.ts +53 -7
- package/src/host/templates.ts +12 -3
- package/src/host/tools.ts +180 -123
- package/src/index.ts +10 -1
- package/src/shared/api.ts +1 -1
- package/src/shared/protocol.ts +35 -1
- package/src/shared/version.ts +1 -1
- 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 './
|
|
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
|
|
162
|
+
// Model catalog: the controller exposes the installed face when the runtime is up.
|
|
159
163
|
useEffect(() => {
|
|
160
|
-
const face =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
217
|
-
|
|
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
|
-
|
|
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
|
-
|
|
255
|
-
|
|
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
|
|
8
|
-
* `data-pane="conversation"`; the
|
|
9
|
-
* dropped data-pane
|
|
10
|
-
* (`pI_x6G_centerCol`) —
|
|
11
|
-
*
|
|
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']
|
package/src/client/controller.ts
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
/**
|
|
377
|
-
|
|
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
|
|
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
|
-
|
|
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']
|
package/src/client/index.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
* (
|
|
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>(
|
|
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
|
-
|
|
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
|
|
package/src/client/styles.ts
CHANGED
|
@@ -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
|
-
/*
|
|
73
|
-
* shell's CSS-Module hashed centerCol (
|
|
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])
|
|
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
|
|