dsh-plugin-workbench 0.0.12 → 0.0.13
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/CHANGELOG.md +22 -0
- package/lib/client.js +84 -6
- package/lib/client.js.map +1 -1
- package/lib/index.js +31 -1
- package/package.json +1 -1
- package/src/client/FilePreview.tsx +106 -6
- package/src/index.ts +21 -1
package/lib/index.js
CHANGED
|
@@ -230,7 +230,37 @@ function ensureLayoutPatch() {
|
|
|
230
230
|
layoutPatchScheduled = false;
|
|
231
231
|
}
|
|
232
232
|
}
|
|
233
|
-
/**
|
|
233
|
+
/**
|
|
234
|
+
* Stable system-prompt section teaching the model the workbench `@.\` mention
|
|
235
|
+
* grammar. The core already contributes a generic "paths prefixed with @ are
|
|
236
|
+
* files referenced by the user" section (dsh-file-reference-local, order 99);
|
|
237
|
+
* this one documents the actual syntax the GUI inserts (the `.\` workspace
|
|
238
|
+
* marker, quoted form for paths with spaces) so the model resolves mentions
|
|
239
|
+
* against the session workspace root instead of treating `@.\…` as noise.
|
|
240
|
+
* Static text only — the string is identical on every assembly (KV-cache safe).
|
|
241
|
+
*/
|
|
242
|
+
const FILE_MENTION_PROMPT = "Workspace file mentions written by the file panel use the form `@.<relative-path>` (a `.` or `./` prefix marks a path relative to the current session workspace root; both `\\` and `/` separators are accepted, and paths with spaces use the quoted form `@\"<relative-path>\"`, for example `@\"\\.\\my plan.md\"`). Treat any such mention as a file the user wants you to read or edit: resolve the path against the workspace root and act on it with the file tools (read, glob, grep, edit, write); never claim to have inspected a file you did not actually open.";
|
|
243
|
+
/**
|
|
244
|
+
* File-mention grammar taught to the model (see the FILE_MENTION_PROMPT doc).
|
|
245
|
+
* One section per agent fiber: existing agents at apply time plus every agent
|
|
246
|
+
* created later (agent/created). The core already contributes a generic "paths
|
|
247
|
+
* prefixed with @ are files" section (dsh-file-reference-local, order 99);
|
|
248
|
+
* order 100 puts this more specific grammar right after it.
|
|
249
|
+
*/
|
|
250
|
+
function installMentionPrompt(ctx) {
|
|
251
|
+
const teach = (agent) => {
|
|
252
|
+
agent.ctx.systemPrompt.section({
|
|
253
|
+
name: "dsh-plugin-workbench:file-mention",
|
|
254
|
+
order: 100,
|
|
255
|
+
text: FILE_MENTION_PROMPT
|
|
256
|
+
});
|
|
257
|
+
};
|
|
258
|
+
for (const agent of ctx.agents.list()) teach(agent);
|
|
259
|
+
ctx.on("agent/created", (payload) => {
|
|
260
|
+
const { agent } = payload;
|
|
261
|
+
teach(agent);
|
|
262
|
+
});
|
|
263
|
+
}
|
|
234
264
|
/**
|
|
235
265
|
* One filesystem-backed RPC endpoint pair. Reads never mutate; `signal`
|
|
236
266
|
* cancels the underlying fs call (or aborts between steps).
|
package/package.json
CHANGED
|
@@ -47,6 +47,22 @@ const PREVIEW_MIN = 240
|
|
|
47
47
|
const CHAT_MIN = 240
|
|
48
48
|
const PREVIEW_TOO_LARGE_LABEL = '512KB'
|
|
49
49
|
|
|
50
|
+
/** Persisted split width (px) — survives reloads so a drag is never lost. */
|
|
51
|
+
const PREVIEW_WIDTH_KEY = 'dsh-plugin-workbench:preview-width'
|
|
52
|
+
|
|
53
|
+
/** Read the persisted preview width; `null` means "use the default 55%". */
|
|
54
|
+
function storedPreviewWidth(): number | null {
|
|
55
|
+
try {
|
|
56
|
+
if (typeof window === 'undefined') return null
|
|
57
|
+
const raw = window.localStorage.getItem(PREVIEW_WIDTH_KEY)
|
|
58
|
+
if (raw === null) return null
|
|
59
|
+
const px = Number(raw)
|
|
60
|
+
return Number.isFinite(px) && px > 0 ? px : null
|
|
61
|
+
} catch {
|
|
62
|
+
return null
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
50
66
|
/** Same-origin raw-bytes route registered by the host half (see src/index.ts). */
|
|
51
67
|
const RAW_PREFIX = '/dsh-plugin-files/raw'
|
|
52
68
|
|
|
@@ -108,6 +124,23 @@ function clamp(value: number, min: number, max: number): number {
|
|
|
108
124
|
return Math.min(max, Math.max(min, value))
|
|
109
125
|
}
|
|
110
126
|
|
|
127
|
+
/**
|
|
128
|
+
* Nearest ancestor that actually takes part in layout. The slot system wraps
|
|
129
|
+
* each slot's content in a `display: contents` element: children still join
|
|
130
|
+
* the OUTER flex row, but the wrapper itself reports a 0×0 bounding rect.
|
|
131
|
+
* Measuring that (the old `handle.parentElement`) made `max` collapse to
|
|
132
|
+
* `PREVIEW_MIN` on the first move — the pane jumped to its minimum width and
|
|
133
|
+
* could never be dragged back out. Skip every `display: contents` layer.
|
|
134
|
+
*/
|
|
135
|
+
function laidOutParent(el: HTMLElement | null): HTMLElement | null {
|
|
136
|
+
let node = el?.parentElement ?? null
|
|
137
|
+
while (node !== null) {
|
|
138
|
+
if (getComputedStyle(node).display !== 'contents') return node
|
|
139
|
+
node = node.parentElement
|
|
140
|
+
}
|
|
141
|
+
return null
|
|
142
|
+
}
|
|
143
|
+
|
|
111
144
|
/** Parent directory of a path ('C:/a/b.md' → 'C:/a'; '' when there is none). */
|
|
112
145
|
function dirnameOf(path: string): string {
|
|
113
146
|
const idx = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\'))
|
|
@@ -163,6 +196,10 @@ export function FilePreview({ t, readFile, writeFile, watchFiles }: FilePreviewP
|
|
|
163
196
|
const highlightRef = useRef<HTMLPreElement>(null)
|
|
164
197
|
const textareaRef = useRef<HTMLTextAreaElement>(null)
|
|
165
198
|
const gutterRef = useRef<HTMLDivElement>(null)
|
|
199
|
+
// Live drag listeners, kept in refs so a new drag can ALWAYS clear any
|
|
200
|
+
// leftovers (a lost pointerup must not leak a stale onMove into the page).
|
|
201
|
+
const dragMoveRef = useRef<(ev: PointerEvent) => void>(() => undefined)
|
|
202
|
+
const dragUpRef = useRef<() => void>(() => undefined)
|
|
166
203
|
|
|
167
204
|
const refresh = useCallback(() => bump((v) => v + 1), [])
|
|
168
205
|
|
|
@@ -391,26 +428,89 @@ export function FilePreview({ t, readFile, writeFile, watchFiles }: FilePreviewP
|
|
|
391
428
|
}
|
|
392
429
|
}, [isOpen])
|
|
393
430
|
|
|
431
|
+
// Apply the persisted width once the pane gets its first real layout: the
|
|
432
|
+
// saved px may exceed the CURRENT center column (window resized since the
|
|
433
|
+
// last drag), so clamp it against a live measurement before applying —
|
|
434
|
+
// otherwise a stale wide value would squeeze the chat seat to nothing.
|
|
435
|
+
useEffect(() => {
|
|
436
|
+
const saved = storedPreviewWidth()
|
|
437
|
+
if (saved === null) return
|
|
438
|
+
const preview = previewRef.current
|
|
439
|
+
const center = laidOutParent(preview)
|
|
440
|
+
if (preview === null || preview === undefined || center === null || center === undefined) return
|
|
441
|
+
const centerWidth = center.getBoundingClientRect().width
|
|
442
|
+
const max = Math.max(PREVIEW_MIN, centerWidth - CHAT_MIN)
|
|
443
|
+
setPreviewWidth(clamp(saved, PREVIEW_MIN, max))
|
|
444
|
+
}, [isOpen])
|
|
445
|
+
|
|
446
|
+
// Unmount during a drag (slot removed mid-drag): drop the window listeners
|
|
447
|
+
// so no stale move handler survives to rewrite widths in the next mount.
|
|
448
|
+
useEffect(() => () => {
|
|
449
|
+
window.removeEventListener('pointermove', dragMoveRef.current)
|
|
450
|
+
window.removeEventListener('pointerup', dragUpRef.current)
|
|
451
|
+
window.removeEventListener('pointercancel', dragUpRef.current)
|
|
452
|
+
}, [])
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Start a split-width drag. Robustness notes (the "pane suddenly shrinks
|
|
456
|
+
* and freezes" bug class):
|
|
457
|
+
*
|
|
458
|
+
* - Pointer capture reroutes every later pointer event to the handle, so
|
|
459
|
+
* `pointerup` fires EVEN when the mouse is released outside the window.
|
|
460
|
+
* Without it the up event is lost, `onUp` never runs, and the leftover
|
|
461
|
+
* `onMove` keeps rewriting the width from its stale baseline on every
|
|
462
|
+
* mouse move anywhere on the page — that is the "cannot drag / cannot
|
|
463
|
+
* restore" state.
|
|
464
|
+
* - `pointercancel` is cleaned up too (browser steals the pointer, e.g. a
|
|
465
|
+
* tablet palm or an OS gesture).
|
|
466
|
+
* - Pointerdown defensively removes any previous listeners first, so even a
|
|
467
|
+
* capture-less leftover cannot survive into a second drag.
|
|
468
|
+
* - The max is re-measured every move: the chat minimum is relative to the
|
|
469
|
+
* CURRENT center column, which can change while the drag is in flight.
|
|
470
|
+
*/
|
|
394
471
|
const onHandleDown = useCallback((e: ReactPointerEvent<HTMLDivElement>) => {
|
|
395
472
|
e.preventDefault()
|
|
396
473
|
const handle = handleRef.current
|
|
397
474
|
const preview = previewRef.current
|
|
398
475
|
if (handle === null || preview === null) return
|
|
399
|
-
|
|
476
|
+
// NOT handle.parentElement: the slot wrapper is `display: contents` and
|
|
477
|
+
// measures 0×0 — the flex container to clamp against is one level up (see
|
|
478
|
+
// laidOutParent). Measuring the wrapper made every drag snap to
|
|
479
|
+
// PREVIEW_MIN and then stick (the reported "sudden shrink / can't drag").
|
|
480
|
+
const center = laidOutParent(handle)
|
|
400
481
|
if (center === null) return
|
|
401
482
|
const startX = e.clientX
|
|
402
483
|
const startWidth = preview.getBoundingClientRect().width
|
|
403
|
-
const centerWidth = center.getBoundingClientRect().width
|
|
404
|
-
const max = Math.max(PREVIEW_MIN, centerWidth - CHAT_MIN)
|
|
405
484
|
const onMove = (ev: PointerEvent) => {
|
|
406
|
-
|
|
485
|
+
const centerWidth = center.getBoundingClientRect().width
|
|
486
|
+
const max = Math.max(PREVIEW_MIN, centerWidth - CHAT_MIN)
|
|
487
|
+
const width = clamp(startWidth + ev.clientX - startX, PREVIEW_MIN, max)
|
|
488
|
+
setPreviewWidth(width)
|
|
489
|
+
try {
|
|
490
|
+
window.localStorage.setItem(PREVIEW_WIDTH_KEY, String(width))
|
|
491
|
+
} catch {
|
|
492
|
+
// storage unavailable — the width just won't persist across reloads
|
|
493
|
+
}
|
|
407
494
|
}
|
|
408
495
|
const onUp = () => {
|
|
409
|
-
window.removeEventListener('pointermove',
|
|
410
|
-
window.removeEventListener('pointerup',
|
|
496
|
+
window.removeEventListener('pointermove', dragMoveRef.current)
|
|
497
|
+
window.removeEventListener('pointerup', dragUpRef.current)
|
|
498
|
+
window.removeEventListener('pointercancel', dragUpRef.current)
|
|
411
499
|
}
|
|
500
|
+
// Defensive reset of any previous drag (see the doc comment above).
|
|
501
|
+
window.removeEventListener('pointermove', dragMoveRef.current)
|
|
502
|
+
window.removeEventListener('pointerup', dragUpRef.current)
|
|
503
|
+
window.removeEventListener('pointercancel', dragUpRef.current)
|
|
504
|
+
dragMoveRef.current = onMove
|
|
505
|
+
dragUpRef.current = onUp
|
|
412
506
|
window.addEventListener('pointermove', onMove)
|
|
413
507
|
window.addEventListener('pointerup', onUp)
|
|
508
|
+
window.addEventListener('pointercancel', onUp)
|
|
509
|
+
try {
|
|
510
|
+
handle.setPointerCapture(e.pointerId)
|
|
511
|
+
} catch {
|
|
512
|
+
// Pointer capture unsupported — the window listeners still cover the drag.
|
|
513
|
+
}
|
|
414
514
|
}, [])
|
|
415
515
|
|
|
416
516
|
const onSave = useCallback(async () => {
|
package/src/index.ts
CHANGED
|
@@ -342,7 +342,27 @@ function ensureLayoutPatch(): void {
|
|
|
342
342
|
const FILE_MENTION_PROMPT =
|
|
343
343
|
'Workspace file mentions written by the file panel use the form `@.<relative-path>` (a `.\` or `./` prefix marks a path relative to the current session workspace root; both `\\` and `/` separators are accepted, and paths with spaces use the quoted form `@"<relative-path>"`, for example `@"\\.\\my plan.md"`). Treat any such mention as a file the user wants you to read or edit: resolve the path against the workspace root and act on it with the file tools (read, glob, grep, edit, write); never claim to have inspected a file you did not actually open.'
|
|
344
344
|
|
|
345
|
-
/**
|
|
345
|
+
/**
|
|
346
|
+
* File-mention grammar taught to the model (see the FILE_MENTION_PROMPT doc).
|
|
347
|
+
* One section per agent fiber: existing agents at apply time plus every agent
|
|
348
|
+
* created later (agent/created). The core already contributes a generic "paths
|
|
349
|
+
* prefixed with @ are files" section (dsh-file-reference-local, order 99);
|
|
350
|
+
* order 100 puts this more specific grammar right after it.
|
|
351
|
+
*/
|
|
352
|
+
function installMentionPrompt(ctx: Context): void {
|
|
353
|
+
const teach = (agent: { ctx: Context }): void => {
|
|
354
|
+
agent.ctx.systemPrompt.section({
|
|
355
|
+
name: 'dsh-plugin-workbench:file-mention',
|
|
356
|
+
order: 100,
|
|
357
|
+
text: FILE_MENTION_PROMPT,
|
|
358
|
+
})
|
|
359
|
+
}
|
|
360
|
+
for (const agent of ctx.agents.list()) teach(agent)
|
|
361
|
+
ctx.on('agent/created', (payload: unknown) => {
|
|
362
|
+
const { agent } = payload as { agent: { ctx: Context } }
|
|
363
|
+
teach(agent)
|
|
364
|
+
})
|
|
365
|
+
}
|
|
346
366
|
|
|
347
367
|
/**
|
|
348
368
|
* One filesystem-backed RPC endpoint pair. Reads never mutate; `signal`
|