dsh-taskboard 0.6.0 → 0.6.1
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 +5 -1
- package/lib/client.js +64 -5
- package/package.json +1 -1
- package/src/client/board/SlashPromptInput.tsx +74 -6
- package/src/client/styles.ts +4 -2
- package/src/shared/version.ts +1 -1
package/README.md
CHANGED
|
@@ -215,13 +215,17 @@ pnpm 的构建授权——按报错把 key 加进 profile 的 `pnpm-workspace.ya
|
|
|
215
215
|
git clone https://github.com/cloader/dsh-taskboard.git
|
|
216
216
|
cd dsh-taskboard
|
|
217
217
|
npm install && npm run build # host ESM + client CJS 双构建
|
|
218
|
-
npm test # vitest 全量(
|
|
218
|
+
npm test # vitest 全量(233 项)
|
|
219
219
|
node tests/manual-git-e2e.mjs # 真 git 端到端手测(worktree 全链路 + 续跑 + diff 查看器)
|
|
220
220
|
node scripts/screenshot.mjs # 重新生成 img/ 截图(需本机 Edge)
|
|
221
221
|
```
|
|
222
222
|
|
|
223
223
|
## 升级日志
|
|
224
224
|
|
|
225
|
+
### 0.6.1
|
|
226
|
+
|
|
227
|
+
- **修复:`/` 快捷补全弹层被任务弹窗滚动容器裁剪**:弹层 portal 到 document.body 并 fixed 锚定输入框,不再被表单滚动容器裁剪;上方空间不足自动下翻、贴边收拢,并随滚动/缩放实时跟随
|
|
228
|
+
- **修复:键盘 ↑/↓ 选择补全项时列表不滚动**:高亮项自动滚入视野(含 wrap-around),直调列表 scrollTop 避免连带滚动模态表体
|
|
225
229
|
### 0.6.0
|
|
226
230
|
|
|
227
231
|
- **任务弹窗左右双栏宽屏布局、输入框 / 快捷补全与执行权限选择: [@jw5555555555](https://github.com/jw5555555555)([#14](https://github.com/cloader/dsh-taskboard/pull/14))**
|
package/lib/client.js
CHANGED
|
@@ -8,6 +8,7 @@ window.__ModuleLoader__.load({
|
|
|
8
8
|
let react = require("react");
|
|
9
9
|
let react_dom_client = require("react-dom/client");
|
|
10
10
|
let react_jsx_runtime = require("react/jsx-runtime");
|
|
11
|
+
let react_dom = require("react-dom");
|
|
11
12
|
|
|
12
13
|
//#region src/client/api.ts
|
|
13
14
|
/** Unwrap the envelope or throw a readable error. */
|
|
@@ -2857,9 +2858,11 @@ window.__ModuleLoader__.load({
|
|
|
2857
2858
|
box-shadow: 0 0 0 3px color-mix(in srgb, var(--dsw-alias-brand-primary, #1f2328) 18%, transparent);
|
|
2858
2859
|
}
|
|
2859
2860
|
|
|
2860
|
-
/* Slash Autocomplete Popup
|
|
2861
|
+
/* Slash Autocomplete Popup. Fixed positioning (left/top/width/maxHeight/z-index)
|
|
2862
|
+
* is set INLINE by SlashPromptInput: the popup is portaled to document.body and
|
|
2863
|
+
* anchored to the textarea's viewport rect, so the scrollable modal body can no
|
|
2864
|
+
* longer clip its top (0.6.0 field report). Only the visual shell lives here. */
|
|
2861
2865
|
.dsh-atb-slash-popup {
|
|
2862
|
-
position: absolute; left: 0; bottom: calc(100% + 6px); width: 100%; max-height: 240px; z-index: 100;
|
|
2863
2866
|
display: flex; flex-direction: column; overflow: hidden; border-radius: 10px;
|
|
2864
2867
|
background: var(--dsw-alias-bg-overlay, #fff); color: var(--dsw-alias-label-primary, inherit);
|
|
2865
2868
|
border: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,.28));
|
|
@@ -3190,7 +3193,7 @@ window.__ModuleLoader__.load({
|
|
|
3190
3193
|
* @module dsh-taskboard/shared/version
|
|
3191
3194
|
*/
|
|
3192
3195
|
/** The package version (must equal package.json "version"). */
|
|
3193
|
-
const PLUGIN_VERSION = "0.6.
|
|
3196
|
+
const PLUGIN_VERSION = "0.6.1";
|
|
3194
3197
|
|
|
3195
3198
|
//#endregion
|
|
3196
3199
|
//#region src/client/board/labels.ts
|
|
@@ -4750,6 +4753,9 @@ window.__ModuleLoader__.load({
|
|
|
4750
4753
|
function SlashPromptInput({ value, onChange, controller, placeholder, rows = 4, maxLength = 8e3, disabled = false, autoFocus = false, className, ariaLabel }) {
|
|
4751
4754
|
const t = useT();
|
|
4752
4755
|
const textareaRef = (0, react.useRef)(null);
|
|
4756
|
+
const popupRef = (0, react.useRef)(null);
|
|
4757
|
+
const listRef = (0, react.useRef)(null);
|
|
4758
|
+
const [popupStyle, setPopupStyle] = (0, react.useState)({});
|
|
4753
4759
|
const [hostCompletions, setHostCompletions] = (0, react.useState)(void 0);
|
|
4754
4760
|
const completions = (0, react.useMemo)(() => {
|
|
4755
4761
|
const merge = (defaults, host) => {
|
|
@@ -4796,6 +4802,20 @@ window.__ModuleLoader__.load({
|
|
|
4796
4802
|
(0, react.useEffect)(() => {
|
|
4797
4803
|
if (selectedIndex >= filteredItems.length) setSelectedIndex(Math.max(0, filteredItems.length - 1));
|
|
4798
4804
|
}, [filteredItems.length, selectedIndex]);
|
|
4805
|
+
(0, react.useLayoutEffect)(() => {
|
|
4806
|
+
if (!popupOpen) return;
|
|
4807
|
+
const list = listRef.current;
|
|
4808
|
+
const active = list?.children[selectedIndex];
|
|
4809
|
+
if (list === null || !(active instanceof HTMLElement)) return;
|
|
4810
|
+
const listRect = list.getBoundingClientRect();
|
|
4811
|
+
const itemRect = active.getBoundingClientRect();
|
|
4812
|
+
if (itemRect.top < listRect.top) list.scrollTop -= listRect.top - itemRect.top;
|
|
4813
|
+
else if (itemRect.bottom > listRect.bottom) list.scrollTop += itemRect.bottom - listRect.bottom;
|
|
4814
|
+
}, [
|
|
4815
|
+
popupOpen,
|
|
4816
|
+
selectedIndex,
|
|
4817
|
+
filteredItems
|
|
4818
|
+
]);
|
|
4799
4819
|
const checkSlashTrigger = () => {
|
|
4800
4820
|
const el = textareaRef.current;
|
|
4801
4821
|
if (el === null) return;
|
|
@@ -4860,6 +4880,42 @@ window.__ModuleLoader__.load({
|
|
|
4860
4880
|
}
|
|
4861
4881
|
}
|
|
4862
4882
|
};
|
|
4883
|
+
const positionPopup = (0, react.useCallback)(() => {
|
|
4884
|
+
const anchor = textareaRef.current;
|
|
4885
|
+
if (anchor === null) return;
|
|
4886
|
+
const rect = anchor.getBoundingClientRect();
|
|
4887
|
+
const gap = 6;
|
|
4888
|
+
const margin = 8;
|
|
4889
|
+
const vh = window.innerHeight;
|
|
4890
|
+
const measured = popupRef.current?.offsetHeight ?? 0;
|
|
4891
|
+
const natural = measured > 0 ? measured : 240;
|
|
4892
|
+
const roomAbove = rect.top - gap - margin;
|
|
4893
|
+
const roomBelow = vh - margin - (rect.bottom + gap);
|
|
4894
|
+
const openBelow = roomBelow > roomAbove;
|
|
4895
|
+
const height = Math.min(natural, Math.max(openBelow ? roomBelow : roomAbove, 120));
|
|
4896
|
+
const top = openBelow ? rect.bottom + gap : rect.top - gap - height;
|
|
4897
|
+
setPopupStyle((prev) => prev.left === rect.left && prev.top === top && prev.width === rect.width && prev.maxHeight === height ? prev : {
|
|
4898
|
+
position: "fixed",
|
|
4899
|
+
left: rect.left,
|
|
4900
|
+
top,
|
|
4901
|
+
width: rect.width,
|
|
4902
|
+
maxHeight: height,
|
|
4903
|
+
zIndex: 100
|
|
4904
|
+
});
|
|
4905
|
+
}, []);
|
|
4906
|
+
(0, react.useLayoutEffect)(() => {
|
|
4907
|
+
if (!popupOpen) return;
|
|
4908
|
+
positionPopup();
|
|
4909
|
+
});
|
|
4910
|
+
(0, react.useEffect)(() => {
|
|
4911
|
+
if (!popupOpen) return;
|
|
4912
|
+
window.addEventListener("scroll", positionPopup, true);
|
|
4913
|
+
window.addEventListener("resize", positionPopup);
|
|
4914
|
+
return () => {
|
|
4915
|
+
window.removeEventListener("scroll", positionPopup, true);
|
|
4916
|
+
window.removeEventListener("resize", positionPopup);
|
|
4917
|
+
};
|
|
4918
|
+
}, [popupOpen, positionPopup]);
|
|
4863
4919
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4864
4920
|
className: `dsh-atb-prompt-wrap ${className ?? ""}`,
|
|
4865
4921
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -4881,8 +4937,10 @@ window.__ModuleLoader__.load({
|
|
|
4881
4937
|
onKeyUp: checkSlashTrigger,
|
|
4882
4938
|
onClick: checkSlashTrigger,
|
|
4883
4939
|
onKeyDown: handleKeyDown
|
|
4884
|
-
}), popupOpen && filteredItems.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4940
|
+
}), popupOpen && filteredItems.length > 0 && (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4941
|
+
ref: popupRef,
|
|
4885
4942
|
className: "dsh-atb-slash-popup",
|
|
4943
|
+
style: popupStyle,
|
|
4886
4944
|
role: "listbox",
|
|
4887
4945
|
"aria-label": t("slash.aria"),
|
|
4888
4946
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -4895,6 +4953,7 @@ window.__ModuleLoader__.load({
|
|
|
4895
4953
|
children: t("slash.hint")
|
|
4896
4954
|
})]
|
|
4897
4955
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4956
|
+
ref: listRef,
|
|
4898
4957
|
className: "dsh-atb-slash-list",
|
|
4899
4958
|
children: filteredItems.map((item, idx) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4900
4959
|
role: "option",
|
|
@@ -4925,7 +4984,7 @@ window.__ModuleLoader__.load({
|
|
|
4925
4984
|
]
|
|
4926
4985
|
}, `${item.kind}-${item.name}`))
|
|
4927
4986
|
})]
|
|
4928
|
-
})]
|
|
4987
|
+
}), document.body)]
|
|
4929
4988
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
4930
4989
|
className: "dsh-atb-prompt-foot",
|
|
4931
4990
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
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.
|
|
4
|
+
"version": "0.6.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"exports": {
|
|
@@ -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
|
-
|
|
234
|
-
|
|
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
|
|
package/src/client/styles.ts
CHANGED
|
@@ -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));
|
package/src/shared/version.ts
CHANGED