dsh-taskboard 0.6.0 → 0.6.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-taskboard",
3
3
  "description": "Agent-first task board for the DSH web GUI: host-authoritative task ledger with taskboard_* agent tools, project (= workspace) claim boundaries, per-task model execution in fresh sessions, optional per-task git-worktree isolation (dedicated task branches, commit evidence, one-click merge), host-side cron scheduling, and a live SSE kanban view. Mounts via the official dsh plugin system — no DSH source changes.",
4
- "version": "0.6.0",
4
+ "version": "0.6.2",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "exports": {
@@ -17,6 +17,13 @@
17
17
  "client": {
18
18
  "inject": [],
19
19
  "platform": "web"
20
+ },
21
+ "compatibility": {
22
+ "dshReleases": {
23
+ "0.1.0-rc.8": "unknown",
24
+ "0.1.1-rc.1": "unknown",
25
+ "0.1.1-rc.2": "compatible"
26
+ }
20
27
  }
21
28
  },
22
29
  "files": [
@@ -27,6 +34,9 @@
27
34
  "LICENSE"
28
35
  ],
29
36
  "license": "Apache-2.0",
37
+ "engines": {
38
+ "node": ">=22"
39
+ },
30
40
  "author": "cloader",
31
41
  "repository": {
32
42
  "type": "git",
@@ -6,7 +6,8 @@
6
6
  *
7
7
  * @module dsh-taskboard/client/board/SlashPromptInput
8
8
  */
9
- import { useEffect, useMemo, useRef, useState, type ChangeEvent, type KeyboardEvent } from 'react'
9
+ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, type ChangeEvent, type CSSProperties, type KeyboardEvent } from 'react'
10
+ import { createPortal } from 'react-dom'
10
11
  import type { BoardController } from '../controller.ts'
11
12
  import type { PromptCompletionItem } from '../../shared/api.ts'
12
13
  import { useT, type Translate } from '../i18n/runtime.ts'
@@ -83,6 +84,10 @@ export function SlashPromptInput({
83
84
  }: SlashPromptInputProps) {
84
85
  const t = useT()
85
86
  const textareaRef = useRef<HTMLTextAreaElement>(null)
87
+ const popupRef = useRef<HTMLDivElement>(null)
88
+ const listRef = useRef<HTMLDivElement>(null)
89
+ // Inline fixed-position style for the portaled popup (set by positionPopup).
90
+ const [popupStyle, setPopupStyle] = useState<CSSProperties>({})
86
91
 
87
92
  // Autocomplete state: only HOST-provided items are stateful; the built-in
88
93
  // defaults are re-derived per render so their descriptions follow the
@@ -131,6 +136,22 @@ export function SlashPromptInput({
131
136
  }
132
137
  }, [filteredItems.length, selectedIndex])
133
138
 
139
+ // Keep the keyboard-highlighted option visible inside the scrolling list:
140
+ // mouse hovering only ever targets rendered rows, but ArrowUp/ArrowDown can
141
+ // move the highlight past the clipped edge. Adjust the list's scrollTop
142
+ // directly from rect deltas — NOT scrollIntoView, which would also scroll
143
+ // ancestor containers (the modal body behind the portaled popup).
144
+ useLayoutEffect(() => {
145
+ if (!popupOpen) return
146
+ const list = listRef.current
147
+ const active = list?.children[selectedIndex]
148
+ if (list === null || !(active instanceof HTMLElement)) return
149
+ const listRect = list.getBoundingClientRect()
150
+ const itemRect = active.getBoundingClientRect()
151
+ if (itemRect.top < listRect.top) list.scrollTop -= listRect.top - itemRect.top
152
+ else if (itemRect.bottom > listRect.bottom) list.scrollTop += itemRect.bottom - listRect.bottom
153
+ }, [popupOpen, selectedIndex, filteredItems])
154
+
134
155
  // Detect slash typing on cursor movement or text change
135
156
  const checkSlashTrigger = (): void => {
136
157
  const el = textareaRef.current
@@ -207,6 +228,51 @@ export function SlashPromptInput({
207
228
  }
208
229
  }
209
230
 
231
+ // The popup is portaled to document.body and fixed-positioned from the
232
+ // textarea's viewport rect: an absolute popup inside the scrollable modal
233
+ // body was clipped at the container's top edge (0.6.0 field report).
234
+ // Opens above by preference, flips below when the top is tight, and clamps
235
+ // to the viewport (maxHeight shrinks; the list scrolls internally).
236
+ const positionPopup = useCallback((): void => {
237
+ const anchor = textareaRef.current
238
+ if (anchor === null) return
239
+ const rect = anchor.getBoundingClientRect()
240
+ const gap = 6
241
+ const margin = 8
242
+ const vh = window.innerHeight
243
+ const measured = popupRef.current?.offsetHeight ?? 0
244
+ const natural = measured > 0 ? measured : 240
245
+ const roomAbove = rect.top - gap - margin
246
+ const roomBelow = vh - margin - (rect.bottom + gap)
247
+ const openBelow = roomBelow > roomAbove
248
+ const height = Math.min(natural, Math.max(openBelow ? roomBelow : roomAbove, 120))
249
+ const top = openBelow ? rect.bottom + gap : rect.top - gap - height
250
+ // Bail out (return prev) when unchanged: the layout effect below runs on
251
+ // every open render, and a fresh object here would re-render forever.
252
+ setPopupStyle(prev => (prev.left === rect.left && prev.top === top && prev.width === rect.width && prev.maxHeight === height
253
+ ? prev
254
+ : { position: 'fixed', left: rect.left, top, width: rect.width, maxHeight: height, zIndex: 100 }))
255
+ }, [])
256
+
257
+ // Reposition on every open render: the popup height follows the filtered
258
+ // item count, so typing changes the geometry too.
259
+ useLayoutEffect(() => {
260
+ if (!popupOpen) return
261
+ positionPopup()
262
+ })
263
+
264
+ // Follow scrolling and viewport resizes while open (capture phase: the
265
+ // modal body scrolls, the window itself does not).
266
+ useEffect(() => {
267
+ if (!popupOpen) return
268
+ window.addEventListener('scroll', positionPopup, true)
269
+ window.addEventListener('resize', positionPopup)
270
+ return () => {
271
+ window.removeEventListener('scroll', positionPopup, true)
272
+ window.removeEventListener('resize', positionPopup)
273
+ }
274
+ }, [popupOpen, positionPopup])
275
+
210
276
  return (
211
277
  <div className={`dsh-atb-prompt-wrap ${className ?? ''}`}>
212
278
  <div className="dsh-atb-prompt-inner">
@@ -229,14 +295,15 @@ export function SlashPromptInput({
229
295
  onKeyDown={handleKeyDown}
230
296
  />
231
297
 
232
- {/* Slash Autocomplete Popup */}
233
- {popupOpen && filteredItems.length > 0 && (
234
- <div className="dsh-atb-slash-popup" role="listbox" aria-label={t('slash.aria')}>
298
+ {/* Slash Autocomplete Popup — portaled to document.body so the
299
+ scrollable modal body can never clip it (see positionPopup). */}
300
+ {popupOpen && filteredItems.length > 0 && createPortal(
301
+ <div ref={popupRef} className="dsh-atb-slash-popup" style={popupStyle} role="listbox" aria-label={t('slash.aria')}>
235
302
  <div className="dsh-atb-slash-head">
236
303
  <span className="dsh-atb-slash-title">{t('slash.title')}</span>
237
304
  <span className="dsh-atb-slash-hint">{t('slash.hint')}</span>
238
305
  </div>
239
- <div className="dsh-atb-slash-list">
306
+ <div ref={listRef} className="dsh-atb-slash-list">
240
307
  {filteredItems.map((item, idx) => (
241
308
  <div
242
309
  key={`${item.kind}-${item.name}`}
@@ -257,7 +324,8 @@ export function SlashPromptInput({
257
324
  </div>
258
325
  ))}
259
326
  </div>
260
- </div>
327
+ </div>,
328
+ document.body,
261
329
  )}
262
330
  </div>
263
331
 
@@ -868,9 +868,11 @@ button.dsh-atb-chip2.dsh-atb-chip-btn:hover {
868
868
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--dsw-alias-brand-primary, #1f2328) 18%, transparent);
869
869
  }
870
870
 
871
- /* Slash Autocomplete Popup */
871
+ /* Slash Autocomplete Popup. Fixed positioning (left/top/width/maxHeight/z-index)
872
+ * is set INLINE by SlashPromptInput: the popup is portaled to document.body and
873
+ * anchored to the textarea's viewport rect, so the scrollable modal body can no
874
+ * longer clip its top (0.6.0 field report). Only the visual shell lives here. */
872
875
  .dsh-atb-slash-popup {
873
- position: absolute; left: 0; bottom: calc(100% + 6px); width: 100%; max-height: 240px; z-index: 100;
874
876
  display: flex; flex-direction: column; overflow: hidden; border-radius: 10px;
875
877
  background: var(--dsw-alias-bg-overlay, #fff); color: var(--dsw-alias-label-primary, inherit);
876
878
  border: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,.28));
@@ -6,4 +6,4 @@
6
6
  */
7
7
 
8
8
  /** The package version (must equal package.json "version"). */
9
- export const PLUGIN_VERSION = '0.6.0'
9
+ export const PLUGIN_VERSION = '0.6.2'