@benz-ai-x/dsh-md-preview 0.2.6 → 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.
package/lib/index.js CHANGED
@@ -146,24 +146,7 @@ let MdPreviewService = (() => {
146
146
  * @throws RemoteError with a stable MdPreview failure code.
147
147
  */
148
148
  async read(sessionId, path, signal) {
149
- if (path.trim().length === 0) throw failure("md-preview/bad-request", "mdPreview/read requires a non-empty path");
150
- if (!isAllowedExtension(path, this.config.allowedExtensions)) throw failure("md-preview/unsupported-extension", `mdPreview/read refuses non-previewable path "${path}"`);
151
- const session = this.ctx.sessions.get(sessionId);
152
- if (session === void 0) throw failure("md-preview/unknown-session", `mdPreview/read cannot resolve session "${sessionId}"`);
153
- const cwd = session.header.cwd;
154
- if (cwd === void 0) throw failure("md-preview/no-workspace", `mdPreview/read session "${sessionId}" has no working directory`);
155
- const root = await this.resolveWorkspacePath(cwd, signal);
156
- let target;
157
- try {
158
- target = await this.resolveWorkspacePath(path, signal, cwd);
159
- } catch (error) {
160
- if (signal.aborted) throw error;
161
- throw failure("md-preview/not-found", `mdPreview/read cannot resolve path "${path}"`);
162
- }
163
- if (!this.ctx.fs.contains(root, target)) throw failure("md-preview/forbidden", "mdPreview/read refuses paths outside the session workspace");
164
- const info = await this.ctx.fs.stat(target, signal);
165
- if (info === void 0) throw failure("md-preview/not-found", `mdPreview/read cannot find "${path}"`);
166
- if (info.type !== "file") throw failure("md-preview/unsupported-extension", `mdPreview/read target "${path}" is not a regular file`);
149
+ const { target, info } = await this.resolveWorkspaceTarget(sessionId, path, signal, "read");
167
150
  if (info.size !== void 0 && info.size > this.config.maxBytes) throw failure("md-preview/too-large", `mdPreview/read refuses "${path}" above the configured byte cap`);
168
151
  let content;
169
152
  try {
@@ -192,24 +175,7 @@ let MdPreviewService = (() => {
192
175
  * @throws RemoteError with a stable MdPreview failure code.
193
176
  */
194
177
  async write(sessionId, path, content, fingerprint, force, signal) {
195
- if (path.trim().length === 0) throw failure("md-preview/bad-request", "mdPreview/write requires a non-empty path");
196
- if (!isAllowedExtension(path, this.config.allowedExtensions)) throw failure("md-preview/unsupported-extension", `mdPreview/write refuses non-previewable path "${path}"`);
197
- const session = this.ctx.sessions.get(sessionId);
198
- if (session === void 0) throw failure("md-preview/unknown-session", `mdPreview/write cannot resolve session "${sessionId}"`);
199
- const cwd = session.header.cwd;
200
- if (cwd === void 0) throw failure("md-preview/no-workspace", `mdPreview/write session "${sessionId}" has no working directory`);
201
- const root = await this.resolveWorkspacePath(cwd, signal);
202
- let target;
203
- try {
204
- target = await this.resolveWorkspacePath(path, signal, cwd);
205
- } catch (error) {
206
- if (signal.aborted) throw error;
207
- throw failure("md-preview/not-found", `mdPreview/write cannot resolve path "${path}"`);
208
- }
209
- if (!this.ctx.fs.contains(root, target)) throw failure("md-preview/forbidden", "mdPreview/write refuses paths outside the session workspace");
210
- const info = await this.ctx.fs.stat(target, signal);
211
- if (info === void 0) throw failure("md-preview/not-found", `mdPreview/write cannot find "${path}"`);
212
- if (info.type !== "file") throw failure("md-preview/unsupported-extension", `mdPreview/write target "${path}" is not a regular file`);
178
+ const { target, cwd } = await this.resolveWorkspaceTarget(sessionId, path, signal, "write");
213
179
  if (content.length > this.config.maxBytes) throw failure("md-preview/too-large", `mdPreview/write refuses "${path}" above the configured byte cap`);
214
180
  if (fingerprint === void 0 && !force) throw failure("md-preview/bad-request", "mdPreview/write requires a fingerprint or force");
215
181
  const expected = fingerprint === void 0 ? void 0 : {
@@ -237,12 +203,45 @@ let MdPreviewService = (() => {
237
203
  fingerprint: outcome.version
238
204
  };
239
205
  }
240
- /** Resolve one path against the workspace, with a plain not-found mapping. */
241
- async resolveWorkspacePath(path, signal, cwd) {
242
- return await this.ctx.fs.resolve(path, cwd === void 0 ? { signal } : {
243
- cwd,
244
- signal
245
- });
206
+ /**
207
+ * The shared authority preamble of the remote methods: resolve a path to a
208
+ * live, contained, regular workspace file, or reject it with the stable
209
+ * failure code. One home for the check order and the aborted-rethrow
210
+ * convention; `op` only names the calling method in diagnostics.
211
+ * @param sessionId - owning session; its header cwd roots the resolution.
212
+ * @param path - path as it appeared in the conversation.
213
+ * @param signal - caller cancellation carried through every fs call.
214
+ * @param op - calling method name for diagnostic messages.
215
+ * @returns the resolved target, its stat info, and the workspace cwd.
216
+ * @throws RemoteError with a stable MdPreview failure code.
217
+ */
218
+ async resolveWorkspaceTarget(sessionId, path, signal, op) {
219
+ if (path.trim().length === 0) throw failure("md-preview/bad-request", `mdPreview/${op} requires a non-empty path`);
220
+ if (!isAllowedExtension(path, this.config.allowedExtensions)) throw failure("md-preview/unsupported-extension", `mdPreview/${op} refuses non-previewable path "${path}"`);
221
+ const session = this.ctx.sessions.get(sessionId);
222
+ if (session === void 0) throw failure("md-preview/unknown-session", `mdPreview/${op} cannot resolve session "${sessionId}"`);
223
+ const cwd = session.header.cwd;
224
+ if (cwd === void 0) throw failure("md-preview/no-workspace", `mdPreview/${op} session "${sessionId}" has no working directory`);
225
+ const root = await this.ctx.fs.resolve(cwd, { signal });
226
+ let target;
227
+ try {
228
+ target = await this.ctx.fs.resolve(path, {
229
+ cwd,
230
+ signal
231
+ });
232
+ } catch (error) {
233
+ if (signal.aborted) throw error;
234
+ throw failure("md-preview/not-found", `mdPreview/${op} cannot resolve path "${path}"`);
235
+ }
236
+ if (!this.ctx.fs.contains(root, target)) throw failure("md-preview/forbidden", `mdPreview/${op} refuses paths outside the session workspace`);
237
+ const info = await this.ctx.fs.stat(target, signal);
238
+ if (info === void 0) throw failure("md-preview/not-found", `mdPreview/${op} cannot find "${path}"`);
239
+ if (info.type !== "file") throw failure("md-preview/unsupported-extension", `mdPreview/${op} target "${path}" is not a regular file`);
240
+ return {
241
+ target,
242
+ info,
243
+ cwd
244
+ };
246
245
  }
247
246
  };
248
247
  })();
@@ -1,6 +1,5 @@
1
1
  import type { SnapshotStore } from '@deepseek-ai/dsh-client-store';
2
2
  import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
3
- import type { RemoteResult } from '@deepseek-ai/dsh-typert-protocol';
4
3
  import type { SessionId } from '@deepseek-ai/dsh-session/types';
5
4
  import type { MdPreviewFile, MdPreviewWriteResult } from '../protocol.ts';
6
5
  import type { MdPreviewState } from './preview-state.ts';
@@ -13,15 +12,15 @@ export interface PreviewOverlayInjected {
13
12
  /** Dismiss the panel and drop the target. */
14
13
  close(): void;
15
14
  /** One bounded read; the transport carries the AbortSignal. */
16
- read(sessionId: SessionId, path: string, signal: AbortSignal): Promise<RemoteResult<MdPreviewFile>>;
15
+ read(sessionId: SessionId, path: string, signal: AbortSignal): Promise<import('@deepseek-ai/dsh-typert-protocol').RemoteResult<MdPreviewFile>>;
17
16
  /** One guarded write; `fingerprint` comes from the backing read. */
18
- write(sessionId: SessionId, path: string, content: string, fingerprint: string | undefined, force: boolean, signal: AbortSignal): Promise<RemoteResult<MdPreviewWriteResult>>;
17
+ write(sessionId: SessionId, path: string, content: string, fingerprint: string | undefined, force: boolean, signal: AbortSignal): Promise<import('@deepseek-ai/dsh-typert-protocol').RemoteResult<MdPreviewWriteResult>>;
19
18
  }
20
19
  /** Full composed panel props. */
21
20
  export type PreviewOverlayProps = PropsRuntime<'shell.overlay'> & InjectFace<PreviewOverlayInjected> & PropsLocale<'md-preview'>;
22
21
  /**
23
22
  * Render the preview panel for the current target.
24
- * @param props - target hook, read RPC, dismissal, and the locale seat.
23
+ * @param props - target hook, read/write RPCs, dismissal, and the locale seat.
25
24
  * @returns the docked panel, or null while closed.
26
25
  */
27
26
  export declare function PreviewOverlay({ usePreviewTarget, close, read, write, t }: PreviewOverlayProps): import("react").JSX.Element | null;
@@ -0,0 +1,96 @@
1
+ /**
2
+ * The PreviewSession machine: the pure state algebra of one preview target's
3
+ * lifecycle — content read, edit session, guarded save, prompts. The React
4
+ * adapter lives beside the panel (use-preview-session.ts); effects and RPC
5
+ * never enter here. `READ_STARTED` is the single reset point: a new read
6
+ * begins only when the previous document's whole session is over.
7
+ */
8
+ import type { MdPreviewFile, MdPreviewWriteResult } from '../protocol.ts';
9
+ /** Content lifecycle of one preview target. */
10
+ export type PreviewSessionContent = {
11
+ readonly state: 'loading';
12
+ } | {
13
+ readonly state: 'ready';
14
+ readonly file: MdPreviewFile;
15
+ } | {
16
+ readonly state: 'failed';
17
+ readonly code: string;
18
+ readonly message: string;
19
+ };
20
+ /** Whole machine state for the panel's current preview target. */
21
+ export interface PreviewSessionState {
22
+ /** Read lifecycle of the current target. */
23
+ readonly content: PreviewSessionContent;
24
+ /** Panel face: rendering the document or editing a draft of it. */
25
+ readonly face: 'view' | 'edit';
26
+ /** The edit session's draft; meaningful only while `face` is edit. */
27
+ readonly draft: string;
28
+ /** A save is in flight. */
29
+ readonly saving: boolean;
30
+ /** The last save met a changed file; the conflict bar is up. */
31
+ readonly conflicted: boolean;
32
+ /** The last save failed for a non-conflict reason; the error bar is up. */
33
+ readonly saveError: {
34
+ readonly code: string;
35
+ readonly message: string;
36
+ } | null;
37
+ /** The unsaved guard is asking before a close. */
38
+ readonly unsavedPrompt: boolean;
39
+ /** The saved toast is showing. */
40
+ readonly toast: boolean;
41
+ /** The session asked the shell to close the panel; the adapter acts once. */
42
+ readonly closeRequested: boolean;
43
+ }
44
+ /** One machine transition. */
45
+ export type PreviewSessionAction = {
46
+ readonly type: 'READ_STARTED';
47
+ } | {
48
+ readonly type: 'RETRY_READ';
49
+ } | {
50
+ readonly type: 'READ_RESOLVED';
51
+ readonly file: MdPreviewFile;
52
+ } | {
53
+ readonly type: 'READ_FAILED';
54
+ readonly code: string;
55
+ readonly message: string;
56
+ } | {
57
+ readonly type: 'ENTER_EDIT';
58
+ } | {
59
+ readonly type: 'EDIT';
60
+ readonly draft: string;
61
+ } | {
62
+ readonly type: 'SAVE_STARTED';
63
+ } | {
64
+ readonly type: 'SAVE_RESOLVED';
65
+ readonly result: MdPreviewWriteResult;
66
+ } | {
67
+ readonly type: 'SAVE_CONFLICT';
68
+ } | {
69
+ readonly type: 'SAVE_FAILED';
70
+ readonly code: string;
71
+ readonly message: string;
72
+ } | {
73
+ readonly type: 'CANCEL_EDIT';
74
+ } | {
75
+ readonly type: 'REQUEST_CLOSE';
76
+ } | {
77
+ readonly type: 'DISCARD';
78
+ } | {
79
+ readonly type: 'KEEP_EDITING';
80
+ } | {
81
+ readonly type: 'TOAST_EXPIRED';
82
+ };
83
+ /** The pristine state every session starts (and resets) from. */
84
+ export declare function initialPreviewSession(): PreviewSessionState;
85
+ /** Whether the draft differs from the loaded document. */
86
+ export declare function isDirty(state: PreviewSessionState): boolean;
87
+ /** The save-enable policy: something to save (dirty or conflicted), no save in flight. */
88
+ export declare function canSave(state: PreviewSessionState): boolean;
89
+ /**
90
+ * Apply one transition.
91
+ * @param state - current machine state.
92
+ * @param action - the transition.
93
+ * @returns the next state (transitions are total; guards decline no-ops).
94
+ */
95
+ export declare function transition(state: PreviewSessionState, action: PreviewSessionAction): PreviewSessionState;
96
+ //# sourceMappingURL=preview-session.d.ts.map
@@ -0,0 +1,36 @@
1
+ import type { RemoteResult } from '@deepseek-ai/dsh-typert-protocol';
2
+ import type { SessionId } from '@deepseek-ai/dsh-session/types';
3
+ import type { MdPreviewFile, MdPreviewWriteResult } from '../protocol.ts';
4
+ import type { MdPreviewTarget } from './preview-state.ts';
5
+ import { initialPreviewSession } from './preview-session.ts';
6
+ /** The adapter's inputs: the RPCs and dismissal from the plugin's apply world. */
7
+ export interface PanelDocumentSessionDeps {
8
+ readonly read: (sessionId: SessionId, path: string, signal: AbortSignal) => Promise<RemoteResult<MdPreviewFile>>;
9
+ readonly write: (sessionId: SessionId, path: string, content: string, fingerprint: string | undefined, force: boolean, signal: AbortSignal) => Promise<RemoteResult<MdPreviewWriteResult>>;
10
+ readonly close: () => void;
11
+ }
12
+ /** The panel-facing surface: machine state plus intent actions. */
13
+ export interface PanelDocumentSession {
14
+ readonly state: ReturnType<typeof initialPreviewSession>;
15
+ readonly canSave: boolean;
16
+ readonly dirty: boolean;
17
+ readonly actions: {
18
+ enterEdit(): void;
19
+ edit(draft: string): void;
20
+ save(force: boolean): void;
21
+ cancelEdit(): void;
22
+ reload(): void;
23
+ retryRead(): void;
24
+ requestClose(): void;
25
+ discard(): void;
26
+ keepEditing(): void;
27
+ };
28
+ }
29
+ /**
30
+ * Run one PreviewSession for the current preview target.
31
+ * @param deps - read/write RPCs and the panel dismissal.
32
+ * @param target - the current preview target; null while the panel is closed.
33
+ * @returns machine state with derived flags and intent actions.
34
+ */
35
+ export declare function usePanelDocumentSession(deps: PanelDocumentSessionDeps, target: MdPreviewTarget | null): PanelDocumentSession;
36
+ //# sourceMappingURL=use-preview-session.d.ts.map
@@ -50,7 +50,18 @@ export declare class MdPreviewService extends TypertRemoteService {
50
50
  * @throws RemoteError with a stable MdPreview failure code.
51
51
  */
52
52
  write(sessionId: SessionId, path: string, content: string, fingerprint: string | undefined, force: boolean, signal: AbortSignal): Promise<MdPreviewWriteResult>;
53
- /** Resolve one path against the workspace, with a plain not-found mapping. */
54
- private resolveWorkspacePath;
53
+ /**
54
+ * The shared authority preamble of the remote methods: resolve a path to a
55
+ * live, contained, regular workspace file, or reject it with the stable
56
+ * failure code. One home for the check order and the aborted-rethrow
57
+ * convention; `op` only names the calling method in diagnostics.
58
+ * @param sessionId - owning session; its header cwd roots the resolution.
59
+ * @param path - path as it appeared in the conversation.
60
+ * @param signal - caller cancellation carried through every fs call.
61
+ * @param op - calling method name for diagnostic messages.
62
+ * @returns the resolved target, its stat info, and the workspace cwd.
63
+ * @throws RemoteError with a stable MdPreview failure code.
64
+ */
65
+ private resolveWorkspaceTarget;
55
66
  }
56
67
  //# sourceMappingURL=remote.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@benz-ai-x/dsh-md-preview",
3
- "version": "0.2.6",
3
+ "version": "0.3.0",
4
4
  "description": "DSH markdown preview: clicking a markdown document in the conversation opens a rendered preview panel docked to the right of the chat.",
5
5
  "license": "MIT",
6
6
  "repository": {