@linxin666/dsh-client-ui-task-board 0.1.2 → 0.1.6
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/lib/client.js +141 -101
- package/lib/client.js.map +1 -1
- package/package.json +1 -1
- package/src/client/board-mount.tsx +31 -0
- package/src/client/board.module.css +5 -2
- package/src/client/sidebar-entry.ts +52 -14
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linxin666/dsh-client-ui-task-board",
|
|
3
3
|
"description": "Task board for the dsh web GUI: a sidebar entry plus a multi-column kanban view with local persistence, task details, and real execution through dsh sessions (session.prompt). Hot-pluggable — mounted via ~/.dsh/cordis.patch.yml + a profile node_modules symlink, no dsh source changes.",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.6",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "lib/types/index.d.ts",
|
|
@@ -19,6 +19,11 @@ export const BOARD_VIEW_SELECTOR = '[data-dsh-taskboard-view]'
|
|
|
19
19
|
|
|
20
20
|
const CONVERSATION_COLUMN_SELECTOR = '[data-pane="conversation"]'
|
|
21
21
|
const ACTIVE_ATTR = 'data-dsh-taskboard-active'
|
|
22
|
+
/** The sibling panel's activation attribute (ssh), removed when this panel opens. */
|
|
23
|
+
const OTHER_ACTIVE_ATTR = 'data-dsh-ssh-active'
|
|
24
|
+
/** Cross-plugin activation event; detail is the activating panel name. */
|
|
25
|
+
const ACTIVATE_EVENT = 'dsh-panel-activate'
|
|
26
|
+
const PANEL_NAME = 'taskboard'
|
|
22
27
|
|
|
23
28
|
/** Find the center column, or undefined while the frame is not mounted. */
|
|
24
29
|
function conversationColumn(): HTMLElement | undefined {
|
|
@@ -53,16 +58,42 @@ export function mountBoard(controller: BoardController): () => void {
|
|
|
53
58
|
|
|
54
59
|
const applyActive = (): void => {
|
|
55
60
|
if (controller.getSnapshot().boardOpen) {
|
|
61
|
+
// Single-occupant center column: opening this panel must evict the
|
|
62
|
+
// sibling panel (ssh), both its html attribute and its controller
|
|
63
|
+
// state, otherwise the two panels' visibility rules fight and the
|
|
64
|
+
// second click appears dead.
|
|
65
|
+
document.documentElement.removeAttribute(OTHER_ACTIVE_ATTR)
|
|
56
66
|
document.documentElement.setAttribute(ACTIVE_ATTR, '')
|
|
67
|
+
document.dispatchEvent(new CustomEvent(ACTIVATE_EVENT, { detail: PANEL_NAME }))
|
|
57
68
|
} else {
|
|
58
69
|
document.documentElement.removeAttribute(ACTIVE_ATTR)
|
|
59
70
|
}
|
|
60
71
|
}
|
|
72
|
+
const onOtherActivate = (event: Event): void => {
|
|
73
|
+
if ((event as CustomEvent).detail === 'ssh' && controller.getSnapshot().boardOpen) {
|
|
74
|
+
controller.closeBoard()
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// Jump out on sidebar context clicks: clicking a session/workspace row
|
|
78
|
+
// (including the already-current one, which produces no session-change
|
|
79
|
+
// event) hands the center column back to the conversation. Capture phase,
|
|
80
|
+
// so the panel closes before the shell processes the click.
|
|
81
|
+
const SIDEBAR_ROW_SELECTOR = '[class*="sessionRow"], [class*="projectRow"], [class*="searchResultRow"], [class*="searchResultWorkspace"], [class*="newSession"]'
|
|
82
|
+
const onClickSidebarRow = (event: MouseEvent): void => {
|
|
83
|
+
if (!controller.getSnapshot().boardOpen) return
|
|
84
|
+
const target = event.target as HTMLElement | null
|
|
85
|
+
if (target === null) return
|
|
86
|
+
if (target.closest(SIDEBAR_ROW_SELECTOR) !== null) controller.closeBoard()
|
|
87
|
+
}
|
|
88
|
+
document.addEventListener('click', onClickSidebarRow, true)
|
|
89
|
+
document.addEventListener(ACTIVATE_EVENT, onOtherActivate)
|
|
61
90
|
const unsubscribe = controller.subscribe(applyActive)
|
|
62
91
|
applyActive()
|
|
63
92
|
ensure()
|
|
64
93
|
|
|
65
94
|
return () => {
|
|
95
|
+
document.removeEventListener('click', onClickSidebarRow, true)
|
|
96
|
+
document.removeEventListener(ACTIVATE_EVENT, onOtherActivate)
|
|
66
97
|
waitObserver.disconnect()
|
|
67
98
|
unsubscribe()
|
|
68
99
|
document.documentElement.removeAttribute(ACTIVE_ATTR)
|
|
@@ -19,13 +19,16 @@
|
|
|
19
19
|
z-index: 5;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
/* The center column is single-occupant; the :not() guards keep the two
|
|
23
|
+
sibling panels (task board / ssh) from fighting over visibility if both
|
|
24
|
+
activation attributes ever coexist. */
|
|
25
|
+
html[data-dsh-taskboard-active]:not([data-dsh-ssh-active]) [data-dsh-taskboard-view] {
|
|
23
26
|
display: block;
|
|
24
27
|
}
|
|
25
28
|
|
|
26
29
|
/* While the board is active, the conversation content underneath is hidden
|
|
27
30
|
(it stays mounted and stateful). */
|
|
28
|
-
html[data-dsh-taskboard-active] [data-pane='conversation'] > :not([data-dsh-taskboard-view]) {
|
|
31
|
+
html[data-dsh-taskboard-active]:not([data-dsh-ssh-active]) [data-pane='conversation'] > :not([data-dsh-taskboard-view]) {
|
|
29
32
|
display: none;
|
|
30
33
|
}
|
|
31
34
|
|
|
@@ -20,21 +20,24 @@ import css from './board.module.css'
|
|
|
20
20
|
/** Stable data attribute identifying the injected entry row. */
|
|
21
21
|
export const ENTRY_SELECTOR = '[data-dsh-taskboard-entry]'
|
|
22
22
|
|
|
23
|
-
/** The sidebar column is the grid item AppFrame renders with this pane marker. */
|
|
24
|
-
const SIDEBAR_COLUMN_SELECTOR = '[data-pane="sidebar"]'
|
|
25
|
-
|
|
26
23
|
/** Inline icon (matches the shell's 16px nav-icon look). */
|
|
27
24
|
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>`
|
|
28
25
|
|
|
29
26
|
/** Find the sidebar shell root element, or undefined while not yet mounted. */
|
|
30
27
|
function sidebarRoot(): HTMLElement | undefined {
|
|
31
|
-
const column = document.querySelector<HTMLElement>(
|
|
32
|
-
|
|
33
|
-
|
|
28
|
+
const column = document.querySelector<HTMLElement>('[data-pane="sidebar"], [class*="sidebarCol"]')
|
|
29
|
+
if (column === null) return undefined
|
|
30
|
+
// Current shells wrap the sidebar UI: column > wrapper > root(logoRow owner).
|
|
31
|
+
// Prefer the element that owns the logo row — the real sidebar UI root —
|
|
32
|
+
// and fall back to the column's first child for legacy shells.
|
|
33
|
+
const logoOwner = column.querySelector<HTMLElement>('[class*="logoRow"]')?.parentElement
|
|
34
|
+
return logoOwner ?? (column.firstElementChild as HTMLElement | undefined)
|
|
34
35
|
}
|
|
35
36
|
|
|
36
|
-
/** The New Session button: the
|
|
37
|
+
/** The New Session button: nested in the logo row on current shells, a direct child on legacy shells. */
|
|
37
38
|
function newSessionButton(root: HTMLElement): HTMLButtonElement | undefined {
|
|
39
|
+
const nested = root.querySelector<HTMLButtonElement>('button[class*="newSession"]')
|
|
40
|
+
if (nested !== null) return nested
|
|
38
41
|
for (const child of root.children) {
|
|
39
42
|
if (child.tagName === 'BUTTON') return child as HTMLButtonElement
|
|
40
43
|
}
|
|
@@ -53,14 +56,26 @@ function createEntry(controller: BoardController): HTMLButtonElement {
|
|
|
53
56
|
return entry
|
|
54
57
|
}
|
|
55
58
|
|
|
56
|
-
/** Re-insert the entry after the New Session
|
|
59
|
+
/** Re-insert the entry after the New Session row (before the browser region). */
|
|
57
60
|
function placeEntry(root: HTMLElement, entry: HTMLButtonElement): boolean {
|
|
58
61
|
const button = newSessionButton(root)
|
|
59
62
|
if (button === undefined) return false
|
|
60
63
|
if (entry.parentElement !== root) {
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
+
// Position relative to the family block (entries injected by sibling
|
|
65
|
+
// plugins), never relative to transient logoRow geometry: every family
|
|
66
|
+
// plugin that self-heals during a re-render then lands in the same
|
|
67
|
+
// relative order, so the entries cannot swap positions regardless of
|
|
68
|
+
// observer callback order or of shell wrapper changes. There is no
|
|
69
|
+
// append-to-end fallback: appending at the end would randomly reorder
|
|
70
|
+
// the block after a shell re-render.
|
|
71
|
+
const row = button.closest('[class*="logoRow"]')
|
|
72
|
+
const base = (row !== null && row.parentElement === root) ? row : button
|
|
73
|
+
const family = Array.from(root.children).filter(
|
|
74
|
+
(el): el is HTMLElement => el instanceof HTMLElement && el.matches('[data-dsh-taskboard-entry], [data-dsh-ssh-entry]'),
|
|
75
|
+
)
|
|
76
|
+
// task board sits before the whole family block.
|
|
77
|
+
const anchor = family.length > 0 ? family[0] : base.nextElementSibling
|
|
78
|
+
root.insertBefore(entry, anchor)
|
|
64
79
|
}
|
|
65
80
|
return true
|
|
66
81
|
}
|
|
@@ -77,14 +92,37 @@ export function mountSidebarEntry(controller: BoardController): () => void {
|
|
|
77
92
|
let placed = false
|
|
78
93
|
|
|
79
94
|
const tryPlace = (): void => {
|
|
80
|
-
if (
|
|
95
|
+
if (root !== undefined && !root.isConnected) {
|
|
96
|
+
// The shell rebuilt the sidebar pane (whole-tree teardown); the root
|
|
97
|
+
// observer is gone with the old tree, so detach it and re-query from
|
|
98
|
+
// scratch. The new pane is later noticed by the body-level watcher.
|
|
99
|
+
rootObserver.disconnect()
|
|
100
|
+
root = undefined
|
|
101
|
+
placed = false
|
|
102
|
+
}
|
|
103
|
+
if (placed) {
|
|
104
|
+
// Cheap short-circuit: entry still lives in a mountable subtree.
|
|
105
|
+
if (document.body.contains(entry)) return
|
|
106
|
+
// Entry was torn down together with the old tree; reset and re-place.
|
|
107
|
+
rootObserver.disconnect()
|
|
108
|
+
root = undefined
|
|
109
|
+
placed = false
|
|
110
|
+
}
|
|
81
111
|
root ??= sidebarRoot()
|
|
82
112
|
if (root === undefined) return
|
|
83
113
|
placed = placeEntry(root, entry)
|
|
84
|
-
if (placed)
|
|
114
|
+
if (placed) {
|
|
115
|
+
rootObserver.observe(root, { childList: true, subtree: true })
|
|
116
|
+
}
|
|
85
117
|
}
|
|
86
118
|
|
|
87
|
-
//
|
|
119
|
+
// Body-level watcher retained as the "whole rebuild" fallback: when the shell
|
|
120
|
+
// tears down the whole sidebar pane, the root observer is gone with it and
|
|
121
|
+
// only this body observation can notice the new pane mounting. It is no
|
|
122
|
+
// longer disconnected after placement; the placed-and-still-mounted case
|
|
123
|
+
// short-circuits through the cheap document.body.contains(entry) check, so
|
|
124
|
+
// unrelated app mutations (e.g. chat streaming) cost one contains check
|
|
125
|
+
// instead of churning the full re-query.
|
|
88
126
|
const waitObserver = new MutationObserver(() => { tryPlace() })
|
|
89
127
|
waitObserver.observe(document.body, { childList: true, subtree: true })
|
|
90
128
|
|