@achasoft/dsh-advanced-sidebar 0.1.0 → 0.3.0

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.
Files changed (38) hide show
  1. package/README.md +279 -128
  2. package/cordis.patch.yml +31 -3
  3. package/lib/client.js +2803 -466
  4. package/lib/client.js.map +1 -1
  5. package/lib/host.js +2071 -418
  6. package/lib/index.js +6 -2
  7. package/lib/preview-content-BVUQ5oOR.js +465 -0
  8. package/lib/remote.js +330 -25
  9. package/lib/typert.host.js +330 -25
  10. package/lib/ui-preview.js +352 -0
  11. package/package.json +8 -2
  12. package/types/client/ActionMenu.d.ts +16 -1
  13. package/types/client/LogDownloadDialog.d.ts +24 -0
  14. package/types/client/contract.d.ts +57 -1
  15. package/types/client/index.d.ts +4 -2
  16. package/types/client/locales.d.ts +100 -0
  17. package/types/client/log-download.d.ts +179 -0
  18. package/types/client/panels/PreviewPanel.d.ts +20 -15
  19. package/types/client/panels/preview-file.d.ts +61 -0
  20. package/types/client/panels/preview-mode.d.ts +67 -0
  21. package/types/client/panels/preview-scratchpad.d.ts +53 -0
  22. package/types/client/panels/preview-url.d.ts +17 -0
  23. package/types/client/panels/shared.d.ts +15 -2
  24. package/types/client/preview-driver.d.ts +121 -0
  25. package/types/client/preview-storage.d.ts +43 -0
  26. package/types/client/preview-types.d.ts +21 -0
  27. package/types/client/preview-values.d.ts +43 -0
  28. package/types/host/deletion.d.ts +32 -23
  29. package/types/host/git.d.ts +94 -8
  30. package/types/host/index.d.ts +97 -5
  31. package/types/host/preview-content.d.ts +179 -0
  32. package/types/host/preview-serve.d.ts +242 -0
  33. package/types/host/settings-section.d.ts +49 -0
  34. package/types/host/types.d.ts +341 -0
  35. package/types/host/ui-bridge.d.ts +197 -0
  36. package/types/host/ui-preview-tool.d.ts +60 -0
  37. package/types/index.d.ts +6 -2
  38. package/types/ui-preview.d.ts +11 -0
@@ -24,9 +24,19 @@ export declare const zh: {
24
24
  'menu.archive': string;
25
25
  'menu.preview': string;
26
26
  'menu.delete': string;
27
+ 'menu.downloadLog': string;
28
+ 'menu.downloadLog.busy': string;
29
+ 'menu.downloadLog.unavailable': string;
27
30
  'menu.empty': string;
28
31
  'menu.noSession': string;
29
32
  'menu.noDirectory': string;
33
+ 'logs.dialog.preparingTitle': string;
34
+ 'logs.dialog.preparingDescription': string;
35
+ 'logs.dialog.successTitle': string;
36
+ 'logs.dialog.successDescription': string;
37
+ 'logs.dialog.errorTitle': string;
38
+ 'logs.dialog.close': string;
39
+ 'logs.dialog.commandFailed': string;
30
40
  'panel.close': string;
31
41
  'panel.refresh': string;
32
42
  'panel.retry': string;
@@ -152,6 +162,46 @@ export declare const zh: {
152
162
  'preview.noUrl': string;
153
163
  'preview.empty': string;
154
164
  'preview.emptyFile': string;
165
+ 'preview.modes': string;
166
+ 'preview.mode.server': string;
167
+ 'preview.mode.file': string;
168
+ 'preview.mode.url': string;
169
+ 'preview.mode.scratchpad': string;
170
+ 'preview.frame.empty': string;
171
+ 'preview.viewport.fit': string;
172
+ 'preview.viewport.custom': string;
173
+ 'preview.device.custom': string;
174
+ 'preview.inspect': string;
175
+ 'preview.inspectHint': string;
176
+ 'preview.url.field': string;
177
+ 'preview.url.placeholder': string;
178
+ 'preview.url.go': string;
179
+ 'preview.url.reload': string;
180
+ 'preview.url.proxied': string;
181
+ 'preview.url.crossOrigin': string;
182
+ 'preview.url.empty': string;
183
+ 'preview.file.field': string;
184
+ 'preview.file.placeholder': string;
185
+ 'preview.file.open': string;
186
+ 'preview.file.empty': string;
187
+ 'preview.file.noWorkspace': string;
188
+ 'preview.file.facts': string;
189
+ 'preview.file.overLimit': string;
190
+ 'preview.file.notPreviewable': string;
191
+ 'preview.file.readFailed': string;
192
+ 'preview.kind.iframe': string;
193
+ 'preview.kind.markdown': string;
194
+ 'preview.kind.image': string;
195
+ 'preview.kind.media': string;
196
+ 'preview.kind.pdf': string;
197
+ 'preview.kind.text': string;
198
+ 'preview.kind.other': string;
199
+ 'preview.scratch.editor': string;
200
+ 'preview.scratch.render': string;
201
+ 'preview.scratch.pending': string;
202
+ 'preview.scratch.saved': string;
203
+ 'preview.scratch.unsaved': string;
204
+ 'preview.scratch.refused': string;
155
205
  'delete.title': string;
156
206
  'delete.body.archive': string;
157
207
  'delete.body.purge': string;
@@ -256,9 +306,19 @@ export declare const en: {
256
306
  'menu.archive': string;
257
307
  'menu.preview': string;
258
308
  'menu.delete': string;
309
+ 'menu.downloadLog': string;
310
+ 'menu.downloadLog.busy': string;
311
+ 'menu.downloadLog.unavailable': string;
259
312
  'menu.empty': string;
260
313
  'menu.noSession': string;
261
314
  'menu.noDirectory': string;
315
+ 'logs.dialog.preparingTitle': string;
316
+ 'logs.dialog.preparingDescription': string;
317
+ 'logs.dialog.successTitle': string;
318
+ 'logs.dialog.successDescription': string;
319
+ 'logs.dialog.errorTitle': string;
320
+ 'logs.dialog.close': string;
321
+ 'logs.dialog.commandFailed': string;
262
322
  'panel.close': string;
263
323
  'panel.refresh': string;
264
324
  'panel.retry': string;
@@ -384,6 +444,46 @@ export declare const en: {
384
444
  'preview.noUrl': string;
385
445
  'preview.empty': string;
386
446
  'preview.emptyFile': string;
447
+ 'preview.modes': string;
448
+ 'preview.mode.server': string;
449
+ 'preview.mode.file': string;
450
+ 'preview.mode.url': string;
451
+ 'preview.mode.scratchpad': string;
452
+ 'preview.frame.empty': string;
453
+ 'preview.viewport.fit': string;
454
+ 'preview.viewport.custom': string;
455
+ 'preview.device.custom': string;
456
+ 'preview.inspect': string;
457
+ 'preview.inspectHint': string;
458
+ 'preview.url.field': string;
459
+ 'preview.url.placeholder': string;
460
+ 'preview.url.go': string;
461
+ 'preview.url.reload': string;
462
+ 'preview.url.proxied': string;
463
+ 'preview.url.crossOrigin': string;
464
+ 'preview.url.empty': string;
465
+ 'preview.file.field': string;
466
+ 'preview.file.placeholder': string;
467
+ 'preview.file.open': string;
468
+ 'preview.file.empty': string;
469
+ 'preview.file.noWorkspace': string;
470
+ 'preview.file.facts': string;
471
+ 'preview.file.overLimit': string;
472
+ 'preview.file.notPreviewable': string;
473
+ 'preview.file.readFailed': string;
474
+ 'preview.kind.iframe': string;
475
+ 'preview.kind.markdown': string;
476
+ 'preview.kind.image': string;
477
+ 'preview.kind.media': string;
478
+ 'preview.kind.pdf': string;
479
+ 'preview.kind.text': string;
480
+ 'preview.kind.other': string;
481
+ 'preview.scratch.editor': string;
482
+ 'preview.scratch.render': string;
483
+ 'preview.scratch.pending': string;
484
+ 'preview.scratch.saved': string;
485
+ 'preview.scratch.unsaved': string;
486
+ 'preview.scratch.refused': string;
387
487
  'delete.title': string;
388
488
  'delete.body.archive': string;
389
489
  'delete.body.purge': string;
@@ -0,0 +1,179 @@
1
+ /**
2
+ * Download session log, absorbed into this plugin's menu.
3
+ *
4
+ * The harness ships `@deepseek-ai/dsh-session-log-export`, whose browser half does two things: it
5
+ * provides a `sessionLogDownload` controller on the client context (`lib/client.js:263`: one
6
+ * in-flight export per session, and a snapshot store describing it), and it registers a SECOND "⋯"
7
+ * button into the same
8
+ * `conversation.session.header.utilities` row this plugin's menu sits in, whose only entry is
9
+ * "Download session log" and which also renders the progress dialog. Two identical-looking triggers
10
+ * side by side, one of them holding a single verb, is what this module removes.
11
+ *
12
+ * How, and why this way (installed harness 0.1.5-rc.2):
13
+ *
14
+ * - The verb is REUSED, not rebuilt. The menu calls the package's own controller, read through the
15
+ * context service it provides, so the export request, the per-session de-duplication and the
16
+ * browser save stay the harness's; nothing here knows the export endpoint.
17
+ * - The button is SHADOWED, not patched out. `…/dsh-session-log-export/lib/client.js:274-276` registers
18
+ * into a `list` slot with `id: 'session-log-download'` at the default priority 0, and a list slot's
19
+ * cell is its `id`: entries sharing one coexist at distinct priorities and the lowest live one
20
+ * renders (`SlotCore.register` / `entriesOfSlot` in `@deepseek-ai/dsh-client-ui-slots`). This
21
+ * plugin registers that same id at priority -1, so its entry takes the cell without the other
22
+ * package noticing, and uninstalling this plugin hands the button straight back. The rejected
23
+ * alternative was a profile `cordis.patch.yml` row disabling the package: that would also remove
24
+ * the controller the menu calls and the `/export` command's dialog, and it asks every user to edit
25
+ * their profile.
26
+ * - The dialog is RE-RENDERED here. The harness's entry renders its button and its dialog as one
27
+ * component, and the module exports neither, so shadowing the cell takes the dialog with it. The
28
+ * shadowing entry therefore renders the same `Modal` from the same primitives, bound to the same
29
+ * store — which keeps `/export`, whose success also opens that dialog, working exactly as before.
30
+ *
31
+ * Everything degrades toward the harness's own behaviour rather than toward a missing verb: without
32
+ * the package, or with a controller whose shape no longer matches, nothing is shadowed and the menu
33
+ * has no Download entry; and the menu entry and the dialog appear only while the harness's button is
34
+ * actually being shadowed, so a future harness that renames or removes its seat can never produce
35
+ * two dialogs, or a verb in two places.
36
+ * @module @achasoft/dsh-advanced-sidebar/client/log-download
37
+ */
38
+ import type { HostObservable } from '@deepseek-ai/dsh-client-ui-slots';
39
+ /** The context service the harness package provides (`ctx.provide('sessionLogDownload', …)`). */
40
+ export declare const LOG_DOWNLOAD_SERVICE = "sessionLogDownload";
41
+ /** The header slot both the harness's button and this plugin's menu occupy. */
42
+ export declare const LOG_DOWNLOAD_SLOT = "conversation.session.header.utilities";
43
+ /** The list-slot cell the harness's download button occupies, and this plugin's entry shadows. */
44
+ export declare const LOG_DOWNLOAD_SEAT_ID = "session-log-download";
45
+ /**
46
+ * The shadowing rank. The harness registers at the default 0; one below is enough to render first,
47
+ * and staying next to the default leaves room for a profile to out-rank this plugin in turn.
48
+ */
49
+ export declare const LOG_DOWNLOAD_SHADOW_PRIORITY = -1;
50
+ /** One session's export, as the harness controller publishes it. */
51
+ export interface LogDownloadEntry {
52
+ /** Whether the dialog is showing; dismissing it does not cancel the export. */
53
+ readonly open: boolean;
54
+ /** Where the export is. */
55
+ readonly status: 'downloading' | 'success' | 'error';
56
+ /** The failure text when `status` is `error`; null otherwise. */
57
+ readonly error: string | null;
58
+ }
59
+ /** The harness controller's store snapshot. */
60
+ export interface LogDownloadState {
61
+ /** Export state by session id. */
62
+ readonly bySession: Readonly<Record<string, LogDownloadEntry | undefined>>;
63
+ }
64
+ /**
65
+ * The part of `SessionLogDownloadController` this plugin calls, typed locally.
66
+ *
67
+ * Deliberately not imported: the package is the harness's, not a dependency of this one, and a
68
+ * deployment composed without it must still load every other entry of the menu.
69
+ */
70
+ export interface LogDownloadService {
71
+ /** Snapshot store the harness's own dialog reads. */
72
+ readonly store: HostObservable<LogDownloadState>;
73
+ /**
74
+ * Start one session's export; a second call while one is in flight joins it.
75
+ * @param sessionId - root session whose tree is exported.
76
+ * @returns after the browser save starts or the failure is published.
77
+ */
78
+ download(sessionId: string): Promise<void>;
79
+ /**
80
+ * Close one session's dialog without cancelling its export.
81
+ * @param sessionId - session whose dialog closes.
82
+ */
83
+ dismiss(sessionId: string): void;
84
+ }
85
+ /**
86
+ * Accept a context service only when it still has the shape this plugin calls.
87
+ *
88
+ * The service is another package's, versioned with the harness, so its shape is checked rather than
89
+ * trusted: an incompatible controller makes this plugin stand aside — no shadow, no menu entry —
90
+ * which leaves the harness's own button in place instead of a menu entry that throws.
91
+ * @param value - whatever `ctx.get('sessionLogDownload')` returned.
92
+ * @returns the service, or undefined when absent or incompatible.
93
+ */
94
+ export declare function asLogDownloadService(value: unknown): LogDownloadService | undefined;
95
+ /** The registry facts {@link shadowsHarnessSeat} reads; a structural subset of a stored entry. */
96
+ export interface SeatEntryLike {
97
+ /** The entry's registration options. */
98
+ readonly options: {
99
+ readonly id?: string | undefined;
100
+ readonly priority?: number | undefined;
101
+ };
102
+ }
103
+ /**
104
+ * Whether the header row holds an entry this plugin's shadow is actually hiding.
105
+ *
106
+ * Checked against the live registry rather than assumed, because the id is the harness's and can
107
+ * change in an upgrade. Without an occupant to hide, the menu entry would duplicate a download
108
+ * affordance the harness moved elsewhere, and the dialog would open twice beside the harness's own.
109
+ * @param entries - the slot's raw entries, every priority included.
110
+ * @returns true when some entry of the download cell ranks after (numerically above) this plugin's
111
+ * shadow, and is therefore the one being hidden.
112
+ */
113
+ export declare function shadowsHarnessSeat(entries: readonly SeatEntryLike[]): boolean;
114
+ /** What the menu and the dialog render from. */
115
+ export interface LogDownloadView {
116
+ /**
117
+ * True while a compatible controller is attached AND its header button is being shadowed: the
118
+ * one condition under which this plugin, rather than the harness, is the download surface.
119
+ */
120
+ readonly active: boolean;
121
+ /** The controller's per-session export state; empty while detached. */
122
+ readonly bySession: LogDownloadState['bySession'];
123
+ }
124
+ /**
125
+ * Whether one session's export is in flight.
126
+ * @param view - the bridge snapshot.
127
+ * @param sessionId - the session to ask about.
128
+ * @returns true while that session's export has not settled.
129
+ */
130
+ export declare function isDownloading(view: LogDownloadView, sessionId: string): boolean;
131
+ /**
132
+ * Bridges the harness controller, which may arrive late, leave, or never exist, into one observable
133
+ * the menu can bind unconditionally.
134
+ *
135
+ * A `use<Name>` hook cannot be bound conditionally, and the menu is registered before the harness
136
+ * package has necessarily loaded, so the menu binds to this instead of to the controller's store:
137
+ * detached it reports inactive, attached it mirrors the controller's state.
138
+ */
139
+ export declare class LogDownloadBridge implements HostObservable<LogDownloadView> {
140
+ private state;
141
+ private readonly listeners;
142
+ private service;
143
+ private shadowing;
144
+ /**
145
+ * The current snapshot; stable between changes, as `useSyncExternalStore` requires.
146
+ * @returns the view.
147
+ */
148
+ getSnapshot(): LogDownloadView;
149
+ /**
150
+ * Listen for changes.
151
+ * @param listener - called after each change.
152
+ * @returns the unsubscribe.
153
+ */
154
+ subscribe(listener: () => void): () => void;
155
+ /**
156
+ * Attach a controller and mirror its store until the returned disposer runs.
157
+ * @param service - the harness controller.
158
+ * @returns the detach, which is a no-op once another controller has replaced this one.
159
+ */
160
+ attach(service: LogDownloadService): () => void;
161
+ /**
162
+ * Record whether the harness's button is currently being shadowed.
163
+ * @param shadowing - the result of {@link shadowsHarnessSeat} over the live registry.
164
+ */
165
+ setShadowing(shadowing: boolean): void;
166
+ /**
167
+ * Start one session's export through the harness controller.
168
+ * @param sessionId - the session to export.
169
+ * @returns false when no controller is attached, so the caller never reports a start that did not happen.
170
+ */
171
+ download(sessionId: string): boolean;
172
+ /**
173
+ * Close one session's dialog without cancelling the export.
174
+ * @param sessionId - the session whose dialog closes.
175
+ */
176
+ dismiss(sessionId: string): void;
177
+ /** Recompute the view and notify, keeping the identity when nothing a reader sees changed. */
178
+ private publish;
179
+ }
@@ -1,25 +1,30 @@
1
1
  /**
2
- * The Preview panel: run the thing you are building and look at it without leaving the session.
2
+ * The Preview panel: one surface with four ways to point it at something.
3
3
  *
4
- * A launch configuration is started on the Host, its port is watched until it accepts, and the
5
- * result is rendered in a frame beside the conversation. The frame is a plain `<iframe>` pointed at
6
- * a loopback URL, which is what makes this work for any dev server rather than for a list of
7
- * blessed ones.
4
+ * - **Server** — a launch configuration is started on the Host, its port is watched until it
5
+ * accepts, and the result is framed with its logs beside it. This is the original behaviour and it
6
+ * is unchanged, including the two controls that are permanent rather than error states: **Logs**,
7
+ * because a server that failed to start has only its stderr to explain itself, and **Open in a new
8
+ * window**, because a page can refuse to be framed and a cross-origin frame gives the panel no way
9
+ * to detect that the load event fired on an error page.
10
+ * - **Files** — any file in the session workspace, rendered by type: an HTML document is framed
11
+ * live *through this Host's own route*, so it is same-origin and therefore inspectable; Markdown
12
+ * is rendered as React elements with no raw HTML; images, PDF, audio and video go to the browser's
13
+ * own elements; anything else is reported with its size and an "open it in the OS" action.
14
+ * - **URL** — any address, with a loopback one routed through this Host's proxy so that it too
15
+ * becomes same-origin, and a public one framed as-is with the panel saying it is opaque.
16
+ * - **Scratchpad** — an HTML editor whose document is rendered beside it from this Host's route, so
17
+ * the frame has a real URL and a real origin rather than `srcdoc`'s opaque one.
8
18
  *
9
- * Two things a person needs are therefore always present, not hidden behind a state:
10
- *
11
- * - **Logs.** A server that failed to start has nothing to show in the frame, and its stderr is the
12
- * only place the reason exists. The log view is one toggle away at every state, and opens itself
13
- * when a start fails.
14
- * - **Open in a new window.** A page can refuse to be framed (`X-Frame-Options`,
15
- * `frame-ancestors`), and cross-origin framing gives the panel no way to detect that — the load
16
- * event fires either way. So the escape hatch is a permanent control rather than an error
17
- * recovery, and the panel says so instead of pretending a blank frame is a loading one.
19
+ * The panel also runs the browser end of the agent channel: a {@link PreviewDriver} polls for
20
+ * commands the model's `ui_preview` tool queued and executes them against whichever frame is
21
+ * mounted. That is the reason the same-origin work exists at all a cross-origin frame's document
22
+ * is unreachable, so neither the panel nor the model could inspect it.
18
23
  * @module @achasoft/dsh-advanced-sidebar/client/panels/PreviewPanel
19
24
  */
20
25
  import { type PanelProps } from './shared.tsx';
21
26
  /**
22
- * The server picker, the frame, and the log view.
27
+ * The four-mode Preview surface, its frame, its logs, and the agent driver.
23
28
  * @param props - the target, the translator, and the dock's face.
24
29
  * @returns the panel body.
25
30
  * @see {@link PanelProps}
@@ -0,0 +1,61 @@
1
+ /**
2
+ * The Preview panel's **Files** mode: preview any file in the session workspace by type.
3
+ *
4
+ * The mode is a thin control surface over one Host endpoint (`previewFileInfo`), which is what
5
+ * decides a file's kind, its size, its same-origin URL, and its change token. Deciding the kind on
6
+ * the Host rather than in the browser is deliberate: the same table then also types the bytes the
7
+ * file route serves, so what the panel renders and what the frame receives cannot disagree.
8
+ *
9
+ * The change token is what makes an edit on disk visible. It is re-read only while a file is
10
+ * previewed, at a cadence slow enough to be free and fast enough to feel live, and a token that
11
+ * moves REMOUNTS the frame rather than reloading it — a remount is the only way to be sure nothing
12
+ * the previous document cached survives into the new one.
13
+ * @module @achasoft/dsh-advanced-sidebar/client/panels/preview-file
14
+ */
15
+ import type { PreviewFileInfo } from '../../host/types.ts';
16
+ import type { PreviewModeProps } from './preview-mode.ts';
17
+ import type { PanelFace } from '../preview-types.ts';
18
+ /** What the file mode needs beyond the shared mode contract. */
19
+ export interface FileModeProps extends PreviewModeProps {
20
+ /** The dock's face, for the one endpoint this mode calls. */
21
+ readonly face: PanelFace;
22
+ /** The workspace a path is resolved against, absent when the session has none. */
23
+ readonly workspace: string | undefined;
24
+ /** The file's description, owned by the panel because it decides the frame. */
25
+ readonly info: PreviewFileInfo | undefined;
26
+ /** True while a description is in flight. */
27
+ readonly busy: boolean;
28
+ /** Replace the description. */
29
+ readonly onInfo: (info: PreviewFileInfo | undefined) => void;
30
+ /** A failure phrased for the panel's error line. */
31
+ readonly onError: (message: string | undefined) => void;
32
+ }
33
+ /**
34
+ * Read one file's description, contained to the workspace by the Host.
35
+ *
36
+ * The workspace is sent with the path and the Host proves containment, so a mistyped absolute path
37
+ * is refused by the same check the Files panel uses rather than by a string comparison here.
38
+ * @param face - the dock's face.
39
+ * @param workspace - the workspace the path must stay inside.
40
+ * @param path - the file, absolute or relative.
41
+ * @param signal - cancellation for the read.
42
+ * @returns the description, or a failure.
43
+ */
44
+ export declare function loadFileInfo(face: PanelFace, workspace: string, path: string, signal?: AbortSignal): Promise<PreviewFileInfo | {
45
+ error: string;
46
+ }>;
47
+ /**
48
+ * Re-read one open file's token and report when it moved.
49
+ * @param face - the dock's face.
50
+ * @param workspace - the workspace.
51
+ * @param path - the file on screen, or undefined to watch nothing.
52
+ * @param onInfo - called with every successful reading.
53
+ * @param onChanged - called when the file's token moved.
54
+ */
55
+ export declare function useFileWatch(face: PanelFace, workspace: string | undefined, path: string | undefined, onInfo: (info: PreviewFileInfo) => void, onChanged: () => void): void;
56
+ /**
57
+ * The path field, the file's facts, and the manual Refresh.
58
+ * @param props - the mode contract plus the file's state.
59
+ * @returns the mode's controls.
60
+ */
61
+ export declare function PreviewFileMode({ t, face, state, setState, workspace, info, busy, onInfo, onError, }: FileModeProps): import("react").JSX.Element;
@@ -0,0 +1,67 @@
1
+ /**
2
+ * The contract every Preview mode's controls share with the panel that owns the frame.
3
+ *
4
+ * The panel owns one piece of state — what the frame is showing, in which mode, under which
5
+ * viewport — and each mode's controls are handed a patch function rather than a setter per field.
6
+ * One patch shape keeps the mode components from needing to know the whole state, and it is what
7
+ * lets the agent's `open` control land on the same state a person's click writes.
8
+ * @module @achasoft/dsh-advanced-sidebar/client/panels/preview-mode
9
+ */
10
+ import type { Translate } from '../contract.ts';
11
+ import type { PreviewMode } from '../preview-types.ts';
12
+ /** What the panel is showing, as every mode reads it. */
13
+ export interface PreviewModeState {
14
+ /**
15
+ * The URL of the framed document, as a frame can use it: an absolute URL for URL and Server
16
+ * modes, the same-origin file route for a framed file, or the scratchpad route.
17
+ *
18
+ * Separate from the address a person typed because the two differ exactly where it matters: the
19
+ * typed address is what an operator recognizes, the frame source is what makes the document
20
+ * same-origin.
21
+ */
22
+ readonly src: string;
23
+ /** The address as a person or the agent spelled it, for the address bar. */
24
+ readonly url: string;
25
+ /** The workspace this panel acts on, absent when the session has no directory. */
26
+ readonly workspace: string | undefined;
27
+ /** The same-origin proxy route, absent when the Host serves none. */
28
+ readonly proxyRoute: string | undefined;
29
+ /** Whether the framed document is same-origin with the GUI, so the agent can inspect it. */
30
+ readonly inspectable: boolean;
31
+ /** The file being previewed, in file mode. */
32
+ readonly filePath: string;
33
+ /** The frame viewport, pinned by the resize control or the agent; zero means "fit the dock". */
34
+ readonly viewport: {
35
+ readonly width: number;
36
+ readonly height: number;
37
+ };
38
+ }
39
+ /** One patch to {@link PreviewModeState}; only the named fields change. */
40
+ export type PreviewModePatch = Partial<Omit<PreviewModeState, 'viewport'>> & {
41
+ /** Replace the pinned viewport; zero for both means "fit the dock". */
42
+ readonly viewport?: {
43
+ readonly width: number;
44
+ readonly height: number;
45
+ };
46
+ /**
47
+ * True when the patch is a person or agent COMMITTING a new document, which forces the frame to
48
+ * remount even when the source string is unchanged (the reload Go provides).
49
+ */
50
+ readonly committed?: boolean;
51
+ };
52
+ /** What every mode's controls are handed. */
53
+ export interface PreviewModeProps {
54
+ /** The namespace translator. */
55
+ t: Translate;
56
+ /** The panel's current state. */
57
+ readonly state: PreviewModeState;
58
+ /** Apply a patch. */
59
+ readonly setState: (patch: PreviewModePatch) => void;
60
+ /** Remount the frame, reloading whatever it shows. */
61
+ readonly onReload: () => void;
62
+ }
63
+ /** The panel's own state, mode included. */
64
+ export interface PreviewState extends PreviewModeState {
65
+ /** Which mode is showing. */
66
+ readonly mode: PreviewMode;
67
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * The Preview panel's **Scratchpad** mode: an editable HTML pane whose content renders live beside it.
3
+ *
4
+ * The document is saved per workspace in this browser's `localStorage`, and it is rendered from the
5
+ * Host's own scratchpad route rather than from `srcdoc`. Both are deliberate:
6
+ *
7
+ * - **Storage.** A scratchpad is a thought being worked out, not a deliverable; it must survive a
8
+ * reload without anybody having to create a file. Per workspace, because two projects' experiments
9
+ * have nothing to do with each other.
10
+ * - **A real URL.** `srcdoc` gives the frame an opaque origin, so `document.baseURI`, a relative
11
+ * `fetch`, and `window.location` are all nonsense inside it, and the agent cannot inspect a
12
+ * document that has no origin. Posting the text to the Host's route gives the frame this GUI's own
13
+ * origin — the same property that makes Files mode inspectable.
14
+ *
15
+ * The editor and the render are debounced together at {@link RENDER_DEBOUNCE_MS}, because a frame
16
+ * remounted per keystroke is unusable and a save per keystroke is what fills a storage quota.
17
+ * @module @achasoft/dsh-advanced-sidebar/client/panels/preview-scratchpad
18
+ */
19
+ import type { PreviewModeProps } from './preview-mode.ts';
20
+ /** What the panel needs from the scratchpad, whose `src` it also frames. */
21
+ export interface Scratchpad {
22
+ /** The document text. */
23
+ readonly text: string;
24
+ /** Replace the text, which schedules a save and a render. */
25
+ readonly setText: (value: string) => void;
26
+ /** True while a save and render are pending. */
27
+ readonly pending: boolean;
28
+ /** True when the last save was refused by the browser. */
29
+ readonly unsaved: boolean;
30
+ }
31
+ /**
32
+ * Own the scratchpad's text, its storage, and its debounce.
33
+ *
34
+ * The rendered document is not this hook's business: the panel holds the frame, so the hook reports
35
+ * the text and calls {@link onPublish} after the debounce, and the panel decides how a document
36
+ * becomes a frame source. Keeping one owner for the frame is what makes the agent's `open` and a
37
+ * person's keystroke land on the same state.
38
+ * @param workspace - the workspace whose scratchpad this is.
39
+ * @param onPublish - called with the document to render after the debounce.
40
+ * @returns the editor's state.
41
+ */
42
+ export declare function useScratchpad(workspace: string | undefined, onPublish: (document_: string) => void): Scratchpad;
43
+ /** What the scratchpad's controls need, beyond the shared mode contract. */
44
+ export interface ScratchpadModeProps extends PreviewModeProps {
45
+ /** The editor's state, owned by the panel. */
46
+ readonly scratch: Scratchpad;
47
+ }
48
+ /**
49
+ * The editor pane, its Render button, and the storage note.
50
+ * @param props - the mode contract plus the editor state.
51
+ * @returns the mode's controls.
52
+ */
53
+ export declare function PreviewScratchpadMode({ t, scratch }: ScratchpadModeProps): import("react").JSX.Element;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The Preview panel's **URL** mode controls: type an address, frame it, reload it.
3
+ *
4
+ * The only decision here is what "same-origin" means for an address a person typed. A loopback URL
5
+ * is routed through the Host's own proxy so the frame becomes inspectable — which is the reason the
6
+ * mode exists beside the dev-server one — while anything else is framed exactly as typed. A public
7
+ * URL is not refused: looking at a page is what the person asked for. The panel says the frame will
8
+ * be opaque, and the agent tool reports that honestly rather than returning an empty DOM.
9
+ * @module @achasoft/dsh-advanced-sidebar/client/panels/preview-url
10
+ */
11
+ import type { PreviewModeProps } from './preview-mode.ts';
12
+ /**
13
+ * The address bar, its Go, and the cross-origin explanation.
14
+ * @param props - the panel's mode contract.
15
+ * @returns the mode's controls.
16
+ */
17
+ export declare function PreviewUrlMode({ t, state, setState, onReload }: PreviewModeProps): import("react").JSX.Element;
@@ -3,8 +3,9 @@
3
3
  * otherwise be written five times, and the path renderer.
4
4
  * @module @achasoft/dsh-advanced-sidebar/client/panels/shared
5
5
  */
6
- import type { PanelHostInjected, Translate } from '../contract.ts';
6
+ import type { Translate } from '../contract.ts';
7
7
  import type { OperationTarget } from '../controller.ts';
8
+ import type { PanelFace } from '../preview-types.ts';
8
9
  /** What every panel is handed by the dock. */
9
10
  export interface PanelProps {
10
11
  /** The session and directory this panel acts on. */
@@ -16,7 +17,7 @@ export interface PanelProps {
16
17
  * that with bound `use<Name>` selector props before the dock is rendered, so no panel ever sees
17
18
  * the sources themselves.
18
19
  */
19
- face: Omit<PanelHostInjected, 'hooks'>;
20
+ face: PanelFace;
20
21
  }
21
22
  /**
22
23
  * Format a byte count for a file row.
@@ -63,3 +64,15 @@ export declare function useLatest<T>(value: T): {
63
64
  * @returns a single-line message.
64
65
  */
65
66
  export declare function transportMessage(reason: unknown, t: Translate): string;
67
+ /**
68
+ * Join a typed path against the workspace unless it is already absolute.
69
+ *
70
+ * The Host proves containment either way — this is not a containment check — so the join only saves
71
+ * a person from typing a long prefix. It is deliberately string arithmetic rather than a resolution:
72
+ * the Host is the authority on what a path means, and a browser-side `..` walk would be a second,
73
+ * weaker copy of that rule.
74
+ * @param workspace - the absolute workspace directory.
75
+ * @param path - the typed path.
76
+ * @returns the absolute candidate the Host will resolve and contain.
77
+ */
78
+ export declare function absoluteIn(workspace: string, path: string): string;