@linxin666/dsh-client-ui-task-board 0.4.1 → 0.4.3
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.i18n.yaml +2 -2
- package/README.md +28 -3
- package/README.zh.md +28 -3
- package/lib/client.js +1142 -467
- package/lib/client.js.map +1 -1
- package/lib/index.js +2066 -1475
- package/lib/types/client/TaskBoardSettingsCard.d.ts +4 -0
- package/lib/types/client/board/LinkSubtaskModal.d.ts +11 -0
- package/lib/types/client/board/NewTaskModal.d.ts +7 -1
- package/lib/types/client/board/TaskCard.d.ts +11 -1
- package/lib/types/client/index.d.ts +15 -1
- package/lib/types/client/locales.d.ts +33 -0
- package/lib/types/client/native-panel.d.ts +62 -0
- package/lib/types/client/settings-entry-form.d.ts +49 -0
- package/lib/types/core/controller.d.ts +71 -3
- package/lib/types/core/subtask.d.ts +122 -0
- package/lib/types/core/tasks.d.ts +46 -3
- package/lib/types/core/use-cases/task-archive.d.ts +10 -13
- package/lib/types/core/use-cases/task-create.d.ts +13 -4
- package/lib/types/core/use-cases/task-delete.d.ts +7 -1
- package/lib/types/core/use-cases/task-parent.d.ts +21 -0
- package/lib/types/core/use-cases/task-update.d.ts +1 -1
- package/lib/types/host/agent-tools.d.ts +27 -0
- package/lib/types/host-ledger.d.ts +97 -4
- package/lib/types/host-runner.d.ts +39 -1
- package/lib/types/host-service.d.ts +98 -5
- package/lib/types/index.d.ts +93 -5
- package/lib/types/mount-once.d.ts +38 -7
- package/lib/types/protocol.d.ts +14 -0
- package/package.json +26 -21
- package/src/client/TaskBoardSettingsCard.tsx +48 -3
- package/src/client/board/LinkSubtaskModal.tsx +82 -0
- package/src/client/board/NewTaskModal.tsx +40 -6
- package/src/client/board/TaskBoard.tsx +88 -8
- package/src/client/board/TaskCard.tsx +52 -2
- package/src/client/board/TaskDetail.tsx +124 -1
- package/src/client/board.module.css +112 -131
- package/src/client/host-api.ts +14 -4
- package/src/client/index.ts +56 -18
- package/src/client/locales.ts +70 -2
- package/src/client/native-panel.tsx +124 -0
- package/src/client/settings-entry-form.ts +142 -0
- package/src/core/controller.ts +128 -10
- package/src/core/store.ts +15 -0
- package/src/core/subtask.ts +286 -0
- package/src/core/tasks.ts +50 -2
- package/src/core/use-cases/task-archive.ts +46 -12
- package/src/core/use-cases/task-create.ts +42 -7
- package/src/core/use-cases/task-delete.ts +11 -1
- package/src/core/use-cases/task-parent.ts +64 -0
- package/src/core/use-cases/task-update.ts +3 -1
- package/src/host/agent-tools.ts +643 -0
- package/src/host-ledger.ts +343 -45
- package/src/host-routes.ts +37 -1
- package/src/host-runner.ts +141 -12
- package/src/host-service.ts +265 -32
- package/src/index.ts +203 -9
- package/src/mount-once.ts +107 -13
- package/src/protocol.ts +26 -2
- package/lib/types/client/board-mount.d.ts +0 -23
- package/lib/types/client/body-mutations.d.ts +0 -45
- package/lib/types/client/panel-mount-core.d.ts +0 -56
- package/lib/types/client/sidebar-entry-core.d.ts +0 -55
- package/lib/types/client/sidebar-entry.d.ts +0 -29
- package/src/client/board-mount.tsx +0 -43
- package/src/client/body-mutations.ts +0 -133
- package/src/client/panel-mount-core.ts +0 -155
- package/src/client/sidebar-entry-core.ts +0 -237
- package/src/client/sidebar-entry.ts +0 -53
|
@@ -37,7 +37,32 @@ export function formatTime(ms: number, timeZone?: string): string {
|
|
|
37
37
|
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
function TaskCardInner({
|
|
40
|
+
function TaskCardInner({
|
|
41
|
+
task,
|
|
42
|
+
pending,
|
|
43
|
+
timeZone,
|
|
44
|
+
onClick,
|
|
45
|
+
subtaskCount = 0,
|
|
46
|
+
isSubtask = false,
|
|
47
|
+
subtasksDone = 0,
|
|
48
|
+
subtasksRunning = 0,
|
|
49
|
+
subtasksFailed = 0,
|
|
50
|
+
}: {
|
|
51
|
+
task: TaskRecord
|
|
52
|
+
pending: boolean
|
|
53
|
+
timeZone?: string
|
|
54
|
+
onClick: () => void
|
|
55
|
+
/** Direct subtasks of this card, shown as a badge (0 hides it). */
|
|
56
|
+
subtaskCount?: number
|
|
57
|
+
/** Whether this card is itself a subtask. */
|
|
58
|
+
isSubtask?: boolean
|
|
59
|
+
/** Direct subtasks already settled as done, for the roll-up badge. */
|
|
60
|
+
subtasksDone?: number
|
|
61
|
+
/** Direct subtasks currently running, for the roll-up badge. */
|
|
62
|
+
subtasksRunning?: number
|
|
63
|
+
/** Direct subtasks that failed, for the roll-up badge. */
|
|
64
|
+
subtasksFailed?: number
|
|
65
|
+
}) {
|
|
41
66
|
const latest = task.executions[task.executions.length - 1]
|
|
42
67
|
const runs = task.executions.length
|
|
43
68
|
const archived = task.archivedAt !== undefined
|
|
@@ -77,6 +102,29 @@ function TaskCardInner({ task, pending, timeZone, onClick }: { task: TaskRecord;
|
|
|
77
102
|
)}
|
|
78
103
|
{task.description !== '' && <span className={css.cardExcerpt}>{task.description}</span>}
|
|
79
104
|
<span className={css.cardMeta}>
|
|
105
|
+
{isSubtask && (
|
|
106
|
+
<span className={css.cardSubtask} data-dsh-part="subtask-badge">{t('card.subtask')}</span>
|
|
107
|
+
)}
|
|
108
|
+
{subtaskCount > 0 && (
|
|
109
|
+
// A board that hides subtask cards still has to report them: the badge
|
|
110
|
+
// rolls up the direct children and takes the tone of the worst state,
|
|
111
|
+
// so a hidden failing tree cannot look green.
|
|
112
|
+
<span
|
|
113
|
+
className={css.cardSubtask}
|
|
114
|
+
data-dsh-part="subtask-count"
|
|
115
|
+
data-tone={subtasksFailed > 0 ? 'failed' : subtasksRunning > 0 ? 'running' : subtasksDone === subtaskCount ? 'done' : undefined}
|
|
116
|
+
title={t('card.subtasksBreakdown', {
|
|
117
|
+
total: String(subtaskCount),
|
|
118
|
+
done: String(subtasksDone),
|
|
119
|
+
running: String(subtasksRunning),
|
|
120
|
+
failed: String(subtasksFailed),
|
|
121
|
+
})}
|
|
122
|
+
>
|
|
123
|
+
{t('card.subtasks', { count: String(subtaskCount) })}
|
|
124
|
+
{subtasksFailed > 0 ? ' · ' + t('card.subtasksFailed', { count: String(subtasksFailed) }) : ''}
|
|
125
|
+
{subtasksFailed === 0 && subtasksRunning > 0 ? ' · ' + t('card.subtasksRunning', { count: String(subtasksRunning) }) : ''}
|
|
126
|
+
</span>
|
|
127
|
+
)}
|
|
80
128
|
<span className={css.cardTime}>{t('board.updated')} {formatTime(task.updatedAt)}</span>
|
|
81
129
|
{task.freeze !== undefined && (
|
|
82
130
|
<span className={css.cardSchedule} title={task.freeze.goal}>{t('card.frozen')}</span>
|
|
@@ -103,7 +151,9 @@ function TaskCardInner({ task, pending, timeZone, onClick }: { task: TaskRecord;
|
|
|
103
151
|
</span>
|
|
104
152
|
{!archived && pending && <span className={css.cardRunningLabel}>{t('board.pending')}…</span>}
|
|
105
153
|
{!archived && latest !== undefined && executionLabel(latest) === 'running' && (
|
|
106
|
-
<span className={css.cardRunningLabel}>
|
|
154
|
+
<span className={css.cardRunningLabel}>
|
|
155
|
+
{latest.ownResult === undefined ? t('detail.result.running') : t('detail.subtasks.waiting')}…
|
|
156
|
+
</span>
|
|
107
157
|
)}
|
|
108
158
|
</button>
|
|
109
159
|
)
|
|
@@ -10,11 +10,13 @@ import { isValidCron } from '../../core/schedule.ts'
|
|
|
10
10
|
import { MANUAL_STATUSES, TASK_PERMISSIONS, tagTone, type ExecutionRecord, type TaskPermission, type TaskRecord } from '../../core/tasks.ts'
|
|
11
11
|
import { canEditTaskContent } from '../../core/use-cases/task-update.ts'
|
|
12
12
|
import { requiresPermissionConfirmation } from '../../core/handover.ts'
|
|
13
|
+
import { DEFAULT_SUBTASK_DEPTH, directSubtasks, taskDepth } from '../../core/subtask.ts'
|
|
13
14
|
import { t, type TaskBoardKey } from '../locales.ts'
|
|
14
15
|
import { SCHEDULE_PRESETS } from '../schedule-presets.ts'
|
|
15
16
|
import css from '../board.module.css'
|
|
16
17
|
import { ConfirmDialog } from './ConfirmDialog.tsx'
|
|
17
18
|
import { EditTaskModal, EditTagsModal } from './EditTaskModal.tsx'
|
|
19
|
+
import { LinkSubtaskModal } from './LinkSubtaskModal.tsx'
|
|
18
20
|
import { NewTaskModal } from './NewTaskModal.tsx'
|
|
19
21
|
import { formatHostTimestamp, formatTime } from './TaskCard.tsx'
|
|
20
22
|
import { STATUS_KEY } from './status-key.ts'
|
|
@@ -63,8 +65,16 @@ function ExecutionRow({ execution, timeZone, onOpen }: { execution: ExecutionRec
|
|
|
63
65
|
/** The execution-target editor: workspace / mode / permission pickers. */
|
|
64
66
|
function ExecutionSettingsSection({ controller, task, pending }: { controller: BoardController; task: TaskRecord; pending: boolean }) {
|
|
65
67
|
const [options, setOptions] = useState(controller.getSnapshot().executionOptions)
|
|
68
|
+
// Whether this deployment serves the Agent Teams service: without it a team
|
|
69
|
+
// run is refused, so the opt-in is offered read-only with the reason.
|
|
70
|
+
const teamRunOffered = (): boolean => controller.getSnapshot().host?.teamRunAvailable === true
|
|
71
|
+
const [teamRunAvailable, setTeamRunAvailable] = useState(teamRunOffered())
|
|
66
72
|
useEffect(
|
|
67
|
-
() => controller.subscribe(() =>
|
|
73
|
+
() => controller.subscribe(() => {
|
|
74
|
+
const snapshot = controller.getSnapshot()
|
|
75
|
+
setOptions(snapshot.executionOptions)
|
|
76
|
+
setTeamRunAvailable(snapshot.host?.teamRunAvailable === true)
|
|
77
|
+
}),
|
|
68
78
|
[controller],
|
|
69
79
|
)
|
|
70
80
|
const workspaceId = task.workspaceId ?? ''
|
|
@@ -154,6 +164,16 @@ function ExecutionSettingsSection({ controller, task, pending }: { controller: B
|
|
|
154
164
|
<span>{t('exec.reuseSession')}</span>
|
|
155
165
|
</label>
|
|
156
166
|
<p className={css.detailText}>{t('exec.reuseSessionHint')}</p>
|
|
167
|
+
<label className={css.scheduleToggle}>
|
|
168
|
+
<input
|
|
169
|
+
type="checkbox"
|
|
170
|
+
checked={task.teamRun === true}
|
|
171
|
+
disabled={pending || !teamRunAvailable}
|
|
172
|
+
onChange={event => { controller.updateTask(task.id, { teamRun: event.target.checked }) }}
|
|
173
|
+
/>
|
|
174
|
+
<span>{t('exec.teamRun')}</span>
|
|
175
|
+
</label>
|
|
176
|
+
<p className={css.detailText}>{teamRunAvailable ? t('exec.teamRunHint') : t('exec.teamRunUnavailable')}</p>
|
|
157
177
|
</section>
|
|
158
178
|
)
|
|
159
179
|
}
|
|
@@ -265,6 +285,103 @@ function ScheduleSection({ controller, task, pending }: { controller: BoardContr
|
|
|
265
285
|
)
|
|
266
286
|
}
|
|
267
287
|
|
|
288
|
+
/**
|
|
289
|
+
* The subtask block: the parent link, the direct subtasks with their status,
|
|
290
|
+
* and the actions that grow or prune the tree. Every gate here mirrors the
|
|
291
|
+
* Host lineage gate for affordance only; the Host re-checks the action.
|
|
292
|
+
*/
|
|
293
|
+
function SubtaskSection({ controller, task, pending, archived }: {
|
|
294
|
+
controller: BoardController
|
|
295
|
+
task: TaskRecord
|
|
296
|
+
pending: boolean
|
|
297
|
+
archived: boolean
|
|
298
|
+
}) {
|
|
299
|
+
const [snapshot, setSnapshot] = useState(controller.getSnapshot())
|
|
300
|
+
useEffect(
|
|
301
|
+
() => controller.subscribe(() => setSnapshot(controller.getSnapshot())),
|
|
302
|
+
[controller],
|
|
303
|
+
)
|
|
304
|
+
const [showAdd, setShowAdd] = useState(false)
|
|
305
|
+
const [showLink, setShowLink] = useState(false)
|
|
306
|
+
const tasks = snapshot.tasks
|
|
307
|
+
const parent = task.parentId === undefined ? undefined : tasks.find(candidate => candidate.id === task.parentId)
|
|
308
|
+
const children = directSubtasks(tasks, task.id)
|
|
309
|
+
const limit = snapshot.host?.maxSubtaskDepth ?? DEFAULT_SUBTASK_DEPTH
|
|
310
|
+
// A new subtask is a leaf, so it fits whenever one more level is allowed.
|
|
311
|
+
const canAddChild = taskDepth(tasks, task.id) + 1 <= limit
|
|
312
|
+
const editable = !archived && !pending
|
|
313
|
+
return (
|
|
314
|
+
<section className={css.detailSection} data-dsh-part="subtasks">
|
|
315
|
+
<h4>{t('detail.subtasks')}</h4>
|
|
316
|
+
{parent !== undefined && (
|
|
317
|
+
<p className={css.detailText}>
|
|
318
|
+
{t('detail.parent')}:{' '}
|
|
319
|
+
<button
|
|
320
|
+
type="button"
|
|
321
|
+
className={css.linkButton}
|
|
322
|
+
title={t('detail.parent.open')}
|
|
323
|
+
onClick={() => { controller.openTask(parent.id) }}
|
|
324
|
+
>
|
|
325
|
+
{parent.title}
|
|
326
|
+
</button>
|
|
327
|
+
</p>
|
|
328
|
+
)}
|
|
329
|
+
{children.length === 0
|
|
330
|
+
? <p className={css.detailText}>{t('detail.subtasks.empty')}</p>
|
|
331
|
+
: (
|
|
332
|
+
<ul className={css.subtaskList}>
|
|
333
|
+
{children.map(child => (
|
|
334
|
+
<li key={child.id} className={css.subtaskRow}>
|
|
335
|
+
<button
|
|
336
|
+
type="button"
|
|
337
|
+
className={css.linkButton}
|
|
338
|
+
onClick={() => { controller.openTask(child.id) }}
|
|
339
|
+
>
|
|
340
|
+
{child.title}
|
|
341
|
+
</button>
|
|
342
|
+
<span className={css.statusBadge} data-status={child.status}>{t(STATUS_KEY[child.status])}</span>
|
|
343
|
+
{!archived && (
|
|
344
|
+
<button
|
|
345
|
+
type="button"
|
|
346
|
+
className={css.linkButton}
|
|
347
|
+
disabled={pending || child.status === 'running'}
|
|
348
|
+
title={child.status === 'running' ? t('detail.subtasks.runningLock') : undefined}
|
|
349
|
+
onClick={() => { void controller.setParent(child.id, null) }}
|
|
350
|
+
>
|
|
351
|
+
{t('detail.subtasks.detach')}
|
|
352
|
+
</button>
|
|
353
|
+
)}
|
|
354
|
+
</li>
|
|
355
|
+
))}
|
|
356
|
+
</ul>
|
|
357
|
+
)}
|
|
358
|
+
{children.length > 0 && (
|
|
359
|
+
<p className={css.detailText}>{t('detail.subtasks.runHint', { count: String(children.length) })}</p>
|
|
360
|
+
)}
|
|
361
|
+
{!archived && (canAddChild
|
|
362
|
+
? (controller.isHostBacked()
|
|
363
|
+
? (
|
|
364
|
+
<div className={css.subtaskAddRow}>
|
|
365
|
+
<button type="button" className={css.ghostButton} disabled={!editable} onClick={() => { setShowAdd(true) }}>
|
|
366
|
+
+ {t('detail.subtasks.add')}
|
|
367
|
+
</button>
|
|
368
|
+
<button type="button" className={css.ghostButton} disabled={!editable} onClick={() => { setShowLink(true) }}>
|
|
369
|
+
{t('detail.subtasks.link')}
|
|
370
|
+
</button>
|
|
371
|
+
</div>
|
|
372
|
+
)
|
|
373
|
+
: <p className={css.detailMeta}>{t('detail.subtasks.hostOnly')}</p>)
|
|
374
|
+
: <p className={css.detailMeta}>{t('detail.subtasks.depthLimit', { depth: String(limit) })}</p>)}
|
|
375
|
+
{showAdd && (
|
|
376
|
+
<NewTaskModal controller={controller} parentTask={task} onClose={() => { setShowAdd(false) }} />
|
|
377
|
+
)}
|
|
378
|
+
{showLink && (
|
|
379
|
+
<LinkSubtaskModal controller={controller} parent={task} onClose={() => { setShowLink(false) }} />
|
|
380
|
+
)}
|
|
381
|
+
</section>
|
|
382
|
+
)
|
|
383
|
+
}
|
|
384
|
+
|
|
268
385
|
/** Task detail overlay. */
|
|
269
386
|
export function TaskDetail({ controller, task }: { controller: BoardController; task: TaskRecord }) {
|
|
270
387
|
const [confirmDelete, setConfirmDelete] = useState(false)
|
|
@@ -289,6 +406,7 @@ export function TaskDetail({ controller, task }: { controller: BoardController;
|
|
|
289
406
|
const transportError = snapshot.transportError
|
|
290
407
|
const timeZone = snapshot.host?.scheduler.timeZone
|
|
291
408
|
const permissionPending = requiresPermissionConfirmation(current, snapshot.host?.sessionDefaultPermission)
|
|
409
|
+
const subtaskChildren = directSubtasks(snapshot.tasks, current.id)
|
|
292
410
|
|
|
293
411
|
return (
|
|
294
412
|
<div className={css.modalBackdrop} onMouseDown={event => { if (event.target === event.currentTarget) controller.closeTask() }}>
|
|
@@ -322,6 +440,8 @@ export function TaskDetail({ controller, task }: { controller: BoardController;
|
|
|
322
440
|
<p className={css.detailText}>{current.description !== '' ? current.description : '—'}</p>
|
|
323
441
|
</section>
|
|
324
442
|
|
|
443
|
+
<SubtaskSection controller={controller} task={current} pending={pending} archived={archived} />
|
|
444
|
+
|
|
325
445
|
{current.tags !== undefined && current.tags.length > 0 && (
|
|
326
446
|
<section className={css.detailSection} data-dsh-part="tags">
|
|
327
447
|
<h4>{t('new.tags')}</h4>
|
|
@@ -479,6 +599,9 @@ export function TaskDetail({ controller, task }: { controller: BoardController;
|
|
|
479
599
|
type="button"
|
|
480
600
|
className={css.primaryButton}
|
|
481
601
|
disabled={running || pending}
|
|
602
|
+
title={subtaskChildren.length > 0
|
|
603
|
+
? t('detail.subtasks.runHint', { count: String(subtaskChildren.length) })
|
|
604
|
+
: undefined}
|
|
482
605
|
onClick={() => {
|
|
483
606
|
void controller.rerunTask(current.id).then(() => {
|
|
484
607
|
if (controller.getSnapshot().transportError === undefined) controller.closeTask()
|
|
@@ -4,135 +4,30 @@
|
|
|
4
4
|
* board follows the active theme (light/dark and skins).
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
/* ---
|
|
7
|
+
/* --- native panel page ------------------------------------------------------- */
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
/* The board is an official center-column panel: the layout mounts this element
|
|
10
|
+
as the selected keyed 'main' occupant, so the shell's own column already
|
|
11
|
+
owns position, sizing and the conversation underneath. Nothing here hides or
|
|
12
|
+
re-parents another plugin's surface — the previous DOM takeover (absolute
|
|
13
|
+
overlay over a hidden conversation subtree) is gone with it.
|
|
13
14
|
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
The container query context stays: the board's responsive rules measure the
|
|
16
|
+
PANEL rather than the viewport, so a narrow column or a resized window
|
|
17
|
+
narrows the board at the same threshold the shell uses for its columns. */
|
|
16
18
|
[data-dsh-taskboard-view] {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
display: flex;
|
|
20
|
+
flex-direction: column;
|
|
21
|
+
box-sizing: border-box;
|
|
22
|
+
width: 100%;
|
|
23
|
+
height: 100%;
|
|
24
|
+
min-width: 0;
|
|
25
|
+
min-height: 0;
|
|
21
26
|
container-name: task-board-view;
|
|
22
27
|
container-type: inline-size;
|
|
23
|
-
/* Opaque backdrop: the conversation subtree stays mounted under the view.
|
|
24
|
-
Even if the hide rule below were defeated by a stronger host inline
|
|
25
|
-
style, this background keeps sticky code-block banners (host z-index 6)
|
|
26
|
-
from showing through the active board (issue #100). */
|
|
27
28
|
background: var(--dsw-alias-bg-base);
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
/* The center column is single-occupant; the :not() guards keep the two
|
|
31
|
-
sibling panels (task board / ssh) from fighting over visibility if both
|
|
32
|
-
activation attributes ever coexist. */
|
|
33
|
-
html[data-dsh-taskboard-active]:not([data-dsh-ssh-active]) [data-dsh-taskboard-view] {
|
|
34
|
-
display: block;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/* While the board is active, the conversation content underneath is hidden
|
|
38
|
-
(it stays mounted and stateful). The !important is required: the dsh shell
|
|
39
|
-
(0.1.0-rc.6) wraps the conversation view in a node with an inline
|
|
40
|
-
`display: contents`, and inline styles beat a plain stylesheet rule. Without
|
|
41
|
-
it the composer (input card) stays visible at the bottom and paints over the
|
|
42
|
-
board modals' footer, hiding the run/delete buttons (issue #76).
|
|
43
|
-
The rule targets both the pane attribute and the center-column class so the
|
|
44
|
-
chat subtree (including the code-block copy banner, issue #100) stays hidden
|
|
45
|
-
whichever host container carries it. */
|
|
46
|
-
html[data-dsh-taskboard-active]:not([data-dsh-ssh-active]) [data-pane='conversation'] > :not([data-dsh-taskboard-view]),
|
|
47
|
-
html[data-dsh-taskboard-active]:not([data-dsh-ssh-active]) [class*='centerCol'] > :not([data-dsh-taskboard-view]) {
|
|
48
|
-
display: none !important;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/* --- sidebar entry row ------------------------------------------------------- */
|
|
52
|
-
|
|
53
|
-
/* Same geometry as the shell's own panel rows (SidebarRoot .panelRow): 2px
|
|
54
|
-
inset with a 12px radius, 8px content padding, 36px row, 14/22 type, and the
|
|
55
|
-
primary ink — so an injected row and an official row line up column for
|
|
56
|
-
column instead of each carrying its own box. */
|
|
57
|
-
.entry {
|
|
58
|
-
box-sizing: border-box;
|
|
59
|
-
display: flex;
|
|
60
|
-
align-items: center;
|
|
61
|
-
gap: 8px;
|
|
62
|
-
min-height: 36px;
|
|
63
|
-
padding: 7px 8px;
|
|
64
|
-
margin: 0 2px;
|
|
65
|
-
background: transparent;
|
|
66
|
-
border: none;
|
|
67
|
-
border-radius: 12px;
|
|
68
|
-
color: var(--dsw-alias-label-primary);
|
|
69
|
-
cursor: pointer;
|
|
70
|
-
font: inherit;
|
|
71
|
-
font-size: 14px;
|
|
72
|
-
line-height: 22px;
|
|
73
|
-
text-align: left;
|
|
74
|
-
white-space: nowrap;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
.entry:hover {
|
|
78
|
-
background: var(--dsw-alias-interactive-bg-hover);
|
|
79
|
-
color: var(--dsw-alias-label-primary);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
.entry[data-active] {
|
|
83
|
-
background: var(--dsw-alias-interactive-bg-active);
|
|
84
|
-
color: var(--dsw-alias-label-primary);
|
|
85
|
-
font-weight: 600;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/* Icon box matches the shell's panel glyph exactly (16px), so the glyph column
|
|
89
|
-
and the label column land on the official rows' x positions. */
|
|
90
|
-
.entryIcon {
|
|
91
|
-
display: inline-flex;
|
|
92
|
-
align-items: center;
|
|
93
|
-
justify-content: center;
|
|
94
|
-
width: 16px;
|
|
95
|
-
height: 16px;
|
|
96
|
-
flex: none;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
.entryIcon svg {
|
|
100
|
-
display: block;
|
|
101
|
-
width: 16px;
|
|
102
|
-
height: 16px;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
.entryLabel {
|
|
106
|
-
overflow: hidden;
|
|
107
|
-
text-overflow: ellipsis;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/* Collapsed rail: icon-only, centered, matching the shell's 56px rail. The
|
|
111
|
-
shell asks its own panel rows for an 18px glyph on the rail, so the icon box
|
|
112
|
-
and the svg step up together with the 12px rail radius. */
|
|
113
|
-
:global([data-dsh-frame][data-sidebar-collapsed]) .entry,
|
|
114
|
-
:global([data-sidebar-collapsed]) .entry {
|
|
115
|
-
justify-content: center;
|
|
116
|
-
padding: 0;
|
|
117
|
-
width: 36px;
|
|
118
|
-
height: 36px;
|
|
119
|
-
margin: 0 auto 12px;
|
|
120
|
-
border-radius: 12px;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
:global([data-dsh-frame][data-sidebar-collapsed]) .entryIcon,
|
|
124
|
-
:global([data-sidebar-collapsed]) .entryIcon,
|
|
125
|
-
:global([data-dsh-frame][data-sidebar-collapsed]) .entryIcon svg,
|
|
126
|
-
:global([data-sidebar-collapsed]) .entryIcon svg {
|
|
127
|
-
width: 18px;
|
|
128
|
-
height: 18px;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
:global([data-dsh-frame][data-sidebar-collapsed]) .entryLabel,
|
|
132
|
-
:global([data-sidebar-collapsed]) .entryLabel {
|
|
133
|
-
display: none;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
31
|
/* --- board frame -------------------------------------------------------------- */
|
|
137
32
|
|
|
138
33
|
.board {
|
|
@@ -924,7 +819,6 @@ html[data-dsh-taskboard-active]:not([data-dsh-ssh-active]) [class*='centerCol']
|
|
|
924
819
|
height: 100dvh;
|
|
925
820
|
}
|
|
926
821
|
|
|
927
|
-
.entry,
|
|
928
822
|
.card,
|
|
929
823
|
.primaryButton,
|
|
930
824
|
.ghostButton,
|
|
@@ -1038,7 +932,6 @@ html[data-dsh-taskboard-active]:not([data-dsh-ssh-active]) [class*='centerCol']
|
|
|
1038
932
|
* --------------------------------------------------------------------------- */
|
|
1039
933
|
|
|
1040
934
|
/* --- one themed focus-visible ring (approx 2px, 2px offset) --- */
|
|
1041
|
-
.entry:focus-visible,
|
|
1042
935
|
.card:focus-visible,
|
|
1043
936
|
.primaryButton:focus-visible,
|
|
1044
937
|
.ghostButton:focus-visible,
|
|
@@ -1055,7 +948,6 @@ html[data-dsh-taskboard-active]:not([data-dsh-ssh-active]) [class*='centerCol']
|
|
|
1055
948
|
}
|
|
1056
949
|
|
|
1057
950
|
/* --- unified 120ms motion across the board's interactive controls --- */
|
|
1058
|
-
.entry,
|
|
1059
951
|
.primaryButton,
|
|
1060
952
|
.ghostButton,
|
|
1061
953
|
.dangerButton,
|
|
@@ -1075,7 +967,6 @@ html[data-dsh-taskboard-active]:not([data-dsh-ssh-active]) [class*='centerCol']
|
|
|
1075
967
|
transform: translateY(0);
|
|
1076
968
|
}
|
|
1077
969
|
|
|
1078
|
-
.entry:active,
|
|
1079
970
|
.primaryButton:active:not(:disabled),
|
|
1080
971
|
.ghostButton:active:not(:disabled),
|
|
1081
972
|
.dangerButton:active:not(:disabled),
|
|
@@ -1084,11 +975,6 @@ html[data-dsh-taskboard-active]:not([data-dsh-ssh-active]) [class*='centerCol']
|
|
|
1084
975
|
transform: translateY(1px);
|
|
1085
976
|
}
|
|
1086
977
|
|
|
1087
|
-
/* --- entry row (sidebar) states --- */
|
|
1088
|
-
.entry[data-active]:hover {
|
|
1089
|
-
background: var(--dsw-specific-sidebar-nav-item-active);
|
|
1090
|
-
}
|
|
1091
|
-
|
|
1092
978
|
/* --- guarded hover for the remaining icon/link buttons (defensive: never
|
|
1093
979
|
disabled today, but a disabled element must not react to hover) --- */
|
|
1094
980
|
.iconButton:hover:not(:disabled) {
|
|
@@ -1121,7 +1007,6 @@ html[data-dsh-taskboard-active]:not([data-dsh-ssh-active]) [class*='centerCol']
|
|
|
1121
1007
|
|
|
1122
1008
|
/* --- reduced motion: strip transitions, stop the decorative spinner --- */
|
|
1123
1009
|
@media (prefers-reduced-motion: reduce) {
|
|
1124
|
-
.entry,
|
|
1125
1010
|
.card,
|
|
1126
1011
|
.primaryButton,
|
|
1127
1012
|
.ghostButton,
|
|
@@ -1320,3 +1205,99 @@ html[data-dsh-taskboard-active]:not([data-dsh-ssh-active]) [class*='centerCol']
|
|
|
1320
1205
|
.aiParseRow .primaryButton {
|
|
1321
1206
|
flex: none;
|
|
1322
1207
|
}
|
|
1208
|
+
|
|
1209
|
+
/* --- subtask tree (parent link, cascade runs) ---------------------------------
|
|
1210
|
+
* A subtask is an ordinary card with a parent link; the badges below only mark
|
|
1211
|
+
* the lineage on the board, and the detail block lists the direct children
|
|
1212
|
+
* with their statuses.
|
|
1213
|
+
* ----------------------------------------------------------------------------- */
|
|
1214
|
+
|
|
1215
|
+
.cardSubtask {
|
|
1216
|
+
max-width: 100%;
|
|
1217
|
+
padding: 0 7px;
|
|
1218
|
+
overflow: hidden;
|
|
1219
|
+
border: 1px solid var(--dsw-alias-border-l2);
|
|
1220
|
+
border-radius: 999px;
|
|
1221
|
+
background: transparent;
|
|
1222
|
+
color: var(--dsw-alias-label-secondary);
|
|
1223
|
+
font-size: 10px;
|
|
1224
|
+
line-height: 16px;
|
|
1225
|
+
text-overflow: ellipsis;
|
|
1226
|
+
white-space: nowrap;
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
.cardSubtask[data-tone='failed'] {
|
|
1230
|
+
color: var(--dsw-alias-state-error-primary);
|
|
1231
|
+
border-color: var(--dsw-alias-state-error-primary);
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
.cardSubtask[data-tone='running'] {
|
|
1235
|
+
color: var(--dsw-alias-state-warn-primary);
|
|
1236
|
+
border-color: var(--dsw-alias-state-warn-primary);
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
.cardSubtask[data-tone='done'] {
|
|
1240
|
+
color: var(--dsw-alias-state-success-primary);
|
|
1241
|
+
border-color: var(--dsw-alias-state-success-primary);
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
.subtaskList {
|
|
1245
|
+
list-style: none;
|
|
1246
|
+
margin: 0;
|
|
1247
|
+
padding: 0;
|
|
1248
|
+
display: flex;
|
|
1249
|
+
flex-direction: column;
|
|
1250
|
+
gap: 6px;
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
.subtaskRow {
|
|
1254
|
+
display: flex;
|
|
1255
|
+
align-items: center;
|
|
1256
|
+
gap: 10px;
|
|
1257
|
+
padding: 6px 10px;
|
|
1258
|
+
border: 1px solid var(--dsw-alias-border-l1);
|
|
1259
|
+
border-radius: 8px;
|
|
1260
|
+
flex-wrap: wrap;
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
.subtaskRow .linkButton:first-child {
|
|
1264
|
+
flex: 1 1 auto;
|
|
1265
|
+
min-width: 0;
|
|
1266
|
+
text-align: left;
|
|
1267
|
+
overflow-wrap: anywhere;
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
.subtaskAddRow {
|
|
1271
|
+
display: flex;
|
|
1272
|
+
gap: 8px;
|
|
1273
|
+
flex-wrap: wrap;
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
.pickList {
|
|
1277
|
+
list-style: none;
|
|
1278
|
+
margin: 0;
|
|
1279
|
+
padding: 0;
|
|
1280
|
+
display: flex;
|
|
1281
|
+
flex-direction: column;
|
|
1282
|
+
gap: 6px;
|
|
1283
|
+
max-height: 320px;
|
|
1284
|
+
overflow-y: auto;
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
.pickRow {
|
|
1288
|
+
display: flex;
|
|
1289
|
+
align-items: center;
|
|
1290
|
+
gap: 10px;
|
|
1291
|
+
padding: 6px 10px;
|
|
1292
|
+
border: 1px solid var(--dsw-alias-border-l1);
|
|
1293
|
+
border-radius: 8px;
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
.pickTitle {
|
|
1297
|
+
flex: 1 1 auto;
|
|
1298
|
+
min-width: 0;
|
|
1299
|
+
font-size: 13px;
|
|
1300
|
+
color: var(--dsw-alias-label-primary);
|
|
1301
|
+
overflow-wrap: anywhere;
|
|
1302
|
+
}
|
|
1303
|
+
|
package/src/client/host-api.ts
CHANGED
|
@@ -11,6 +11,16 @@ import {
|
|
|
11
11
|
type TaskBoardSnapshot,
|
|
12
12
|
} from '../protocol.ts'
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Client-side base for the board's routes: DOCUMENT-RELATIVE (issue #1707).
|
|
16
|
+
*
|
|
17
|
+
* The host registers `TASK_BOARD_API_PREFIX` root-absolute; the page is served
|
|
18
|
+
* with `<base href="./">`, so the browser half must drop the leading slash or
|
|
19
|
+
* a sub-path deployment escapes its entry directory. Derived from the shared
|
|
20
|
+
* constant so host and client cannot drift.
|
|
21
|
+
*/
|
|
22
|
+
const CLIENT_API_PREFIX = TASK_BOARD_API_PREFIX.slice(1)
|
|
23
|
+
|
|
14
24
|
const IMPORT_MARKER = 'dsh.taskBoard.v2.hostImported'
|
|
15
25
|
const SOURCE_KEY = 'dsh.taskBoard.v2.sourceId'
|
|
16
26
|
const IMPORT_REQUEST_KEY = 'dsh.taskBoard.v2.importRequestId'
|
|
@@ -114,7 +124,7 @@ export class HttpTaskBoardHostTransport implements TaskBoardHostTransport {
|
|
|
114
124
|
}
|
|
115
125
|
|
|
116
126
|
async state(): Promise<TaskBoardSnapshot> {
|
|
117
|
-
return await this.request(`${
|
|
127
|
+
return await this.request(`${CLIENT_API_PREFIX}/state`, { cache: 'no-store' })
|
|
118
128
|
}
|
|
119
129
|
|
|
120
130
|
async action(action: TaskBoardAction, initiator?: string): Promise<TaskBoardSnapshot> {
|
|
@@ -123,7 +133,7 @@ export class HttpTaskBoardHostTransport implements TaskBoardHostTransport {
|
|
|
123
133
|
|
|
124
134
|
private async post(requestId: string, action: TaskBoardAction, initiator?: string): Promise<TaskBoardSnapshot> {
|
|
125
135
|
const envelope: TaskBoardActionEnvelope = { requestId, action, ...(initiator === undefined || initiator === '' ? {} : { initiator }) }
|
|
126
|
-
return await this.request(`${
|
|
136
|
+
return await this.request(`${CLIENT_API_PREFIX}/action`, {
|
|
127
137
|
method: 'POST',
|
|
128
138
|
headers: { 'content-type': 'application/json' },
|
|
129
139
|
body: JSON.stringify(envelope),
|
|
@@ -154,7 +164,7 @@ export class HttpTaskBoardHostTransport implements TaskBoardHostTransport {
|
|
|
154
164
|
async parseDraft(request: TaskBoardParseRequest, signal?: AbortSignal): Promise<TaskBoardParseDraft> {
|
|
155
165
|
let response: Response
|
|
156
166
|
try {
|
|
157
|
-
response = await fetch(`${
|
|
167
|
+
response = await fetch(`${CLIENT_API_PREFIX}/parse`, {
|
|
158
168
|
method: 'POST',
|
|
159
169
|
headers: { 'content-type': 'application/json' },
|
|
160
170
|
body: JSON.stringify(request),
|
|
@@ -193,7 +203,7 @@ export class HttpTaskBoardHostTransport implements TaskBoardHostTransport {
|
|
|
193
203
|
}
|
|
194
204
|
|
|
195
205
|
subscribe(listener: (event?: TaskBoardEventPayload) => void): () => void {
|
|
196
|
-
const events = new EventSource(`${
|
|
206
|
+
const events = new EventSource(`${CLIENT_API_PREFIX}/events`)
|
|
197
207
|
let lastStreamErrorNotify = 0
|
|
198
208
|
events.onmessage = (message: MessageEvent<string>): void => {
|
|
199
209
|
try {
|