@khorsheed/dsh-file-preview 0.3.2 → 0.4.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 (40) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.en.md +38 -20
  3. package/README.i18n.yaml +2 -2
  4. package/README.md +39 -21
  5. package/cordis.patch.yml +10 -2
  6. package/lib/client.js +8427 -0
  7. package/lib/typert.host.js +6 -2
  8. package/lib/types/client/DiffHistory.d.ts +20 -0
  9. package/lib/types/client/DiffHistory.js +56 -0
  10. package/lib/types/client/FilePreviewTab.d.ts +34 -0
  11. package/lib/types/client/FilePreviewTab.js +193 -0
  12. package/lib/types/client/TurnFileRow.d.ts +30 -0
  13. package/lib/types/client/TurnFileRow.js +72 -0
  14. package/lib/types/client/contract.d.ts +39 -0
  15. package/lib/types/client/contract.js +2 -0
  16. package/lib/types/client/definition.d.ts +55 -0
  17. package/lib/types/client/definition.js +93 -0
  18. package/lib/types/client/file-preview-store.d.ts +42 -0
  19. package/lib/types/client/file-preview-store.js +58 -0
  20. package/lib/types/client/icons.d.ts +21 -0
  21. package/lib/types/client/icons.js +5 -0
  22. package/lib/types/client/index.d.ts +80 -0
  23. package/lib/types/client/index.js +238 -0
  24. package/lib/types/client/locales.d.ts +16 -0
  25. package/lib/types/client/locales.js +177 -0
  26. package/lib/types/client/mentions-wrap.d.ts +60 -0
  27. package/lib/types/client/mentions-wrap.js +42 -0
  28. package/lib/types/client/open-in-app.d.ts +85 -0
  29. package/lib/types/client/open-in-app.js +146 -0
  30. package/lib/types/client/path-utils.d.ts +53 -0
  31. package/lib/types/client/path-utils.js +110 -0
  32. package/lib/types/client/preview.d.ts +27 -0
  33. package/lib/types/client/preview.js +72 -0
  34. package/lib/types/client/turn-files-cache.d.ts +24 -0
  35. package/lib/types/client/turn-files-cache.js +39 -0
  36. package/lib/types/client/turn-files.d.ts +26 -0
  37. package/lib/types/client/turn-files.js +14 -0
  38. package/locale/en.json +2 -2
  39. package/locale/zh.json +2 -2
  40. package/package.json +55 -4
@@ -0,0 +1,93 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { parseFileAddress } from '@deepseek-ai/dsh-util-workspace-path';
3
+ /** The tab kind this package owns. */
4
+ export const FILE_PREVIEW_KIND = 'file-preview';
5
+ /** This implementation's identity in the tab system, and the key its body registers under. */
6
+ export const FILE_PREVIEW_ID = '@khorsheed/dsh-file-preview';
7
+ /**
8
+ * Suffixes the detail view's preview stack renders: the change-history text
9
+ * set plus the image formats the pane's image arm serves. Everything else
10
+ * (pdf, archives, binaries) stays with the official document tab — the type's
11
+ * `canOpen` filters on this set before claiming an address.
12
+ */
13
+ export const RENDERABLE_EXTENSIONS = [
14
+ 'md', 'mdx', 'txt', 'log',
15
+ 'json', 'jsonc', 'jsonl', 'csv', 'tsv', 'xml', 'svg',
16
+ 'yaml', 'yml', 'toml', 'ini',
17
+ 'ts', 'tsx', 'mts', 'cts', 'js', 'jsx', 'mjs', 'cjs',
18
+ 'py', 'sh', 'bash', 'sql',
19
+ 'html', 'htm', 'css', 'scss', 'less',
20
+ 'go', 'rs', 'java', 'c', 'h', 'cpp', 'hpp', 'rb', 'php', 'swift', 'kt',
21
+ 'vue', 'svelte', 'diff', 'patch',
22
+ 'png', 'jpg', 'jpeg', 'gif', 'webp', 'avif', 'bmp', 'ico',
23
+ ];
24
+ const RENDERABLE = new Set(RENDERABLE_EXTENSIONS);
25
+ /**
26
+ * Whether the detail view can render this path's content (suffix match,
27
+ * case-insensitive, compound suffixes intentionally not special-cased).
28
+ * @param path - any file path spelling.
29
+ */
30
+ export function renderablePath(path) {
31
+ const base = path.replaceAll('\\', '/').toLowerCase();
32
+ const name = base.slice(base.lastIndexOf('/') + 1);
33
+ const dot = name.lastIndexOf('.');
34
+ return dot > 0 && RENDERABLE.has(name.slice(dot + 1));
35
+ }
36
+ /**
37
+ * The type's glyph at the guide capsule's size: a document sheet with the
38
+ * change-history accent (the official files capsule's two-tone flat style,
39
+ * re-cut for "products with history").
40
+ */
41
+ function ProductsGlyph({ size, className }) {
42
+ return (_jsxs("svg", { width: size, height: size, viewBox: "0 0 26 26", className: className, "aria-hidden": "true", children: [_jsx("path", { d: "M7 3.5h7.2c.5 0 .9.2 1.2.5l4.4 4.4c.3.3.5.7.5 1.2v12.1c0 1-.8 1.8-1.8 1.8H7c-1 0-1.8-.8-1.8-1.8V5.3c0-1 .8-1.8 1.8-1.8Z", fill: "#4f8ff7", opacity: "0.28" }), _jsx("path", { d: "M7 3.5h7.2c.5 0 .9.2 1.2.5l4.4 4.4c.3.3.5.7.5 1.2v12.1c0 1-.8 1.8-1.8 1.8H7c-1 0-1.8-.8-1.8-1.8V5.3c0-1 .8-1.8 1.8-1.8Z", fill: "none", stroke: "#4f8ff7", strokeWidth: "1.6" }), _jsx("path", { d: "M14.6 3.8v4.4c0 .6.5 1.1 1.1 1.1h4.4", fill: "none", stroke: "#4f8ff7", strokeWidth: "1.6" }), _jsx("line", { x1: "8.6", y1: "13.6", x2: "13.4", y2: "13.6", stroke: "#e5645f", strokeWidth: "1.7", strokeLinecap: "round" }), _jsx("line", { x1: "8.6", y1: "17.4", x2: "15.6", y2: "17.4", stroke: "#3fb27f", strokeWidth: "1.7", strokeLinecap: "round" })] }));
43
+ }
44
+ /**
45
+ * The tab chip text: the decoded basename for a claimed file address (the
46
+ * document tab's convention), the type label for a page open. Decoding is per
47
+ * segment, matching how the address was built, so a name carrying `#`, `?`,
48
+ * or a space reads as itself.
49
+ * @param address - the address being opened.
50
+ * @param fallback - the type label for non-file addresses.
51
+ */
52
+ function chipTitle(address, fallback) {
53
+ if (!address.startsWith('dsh-resource://'))
54
+ return fallback();
55
+ const name = address.slice(address.lastIndexOf('/') + 1);
56
+ if (name === '')
57
+ return fallback();
58
+ try {
59
+ return decodeURIComponent(name);
60
+ }
61
+ catch {
62
+ // A malformed percent sequence is still a name; showing it raw beats refusing the address.
63
+ return name;
64
+ }
65
+ }
66
+ /**
67
+ * The file-preview type's registry definition.
68
+ * @param t - namespace-bound translate, read fresh on every label call.
69
+ * @returns the definition to register.
70
+ */
71
+ export function filePreviewDefinition(t) {
72
+ return {
73
+ id: FILE_PREVIEW_ID,
74
+ kind: FILE_PREVIEW_KIND,
75
+ patterns: ['dsh-resource://file/**'],
76
+ // Claim every session-scoped file address the preview stack renders.
77
+ // Synchronous and deterministic by construction (no data dependency);
78
+ // unrenderable suffixes and session-less addresses decline, falling
79
+ // through to the official document tab (fallback band).
80
+ canOpen: (address) => {
81
+ const parsed = parseFileAddress(address);
82
+ return parsed?.scope === 'session' && renderablePath(parsed.path);
83
+ },
84
+ title: (address) => chipTitle(address, () => t('open')),
85
+ guide: [{
86
+ id: FILE_PREVIEW_KIND,
87
+ order: 20,
88
+ title: () => t('guide.title'),
89
+ description: () => t('guide.description'),
90
+ icon: ProductsGlyph,
91
+ }],
92
+ };
93
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * The file-preview tab's store: the session's touched-file list (one fetch,
3
+ * shared by every tab of this kind in the session) and each tab's selected
4
+ * path. Module level exports the factory only — a module-level handle would
5
+ * pin the store's identity in the module cache (a de-facto singleton
6
+ * surviving plugin reloads). register() receives the factory (exclusive use:
7
+ * the framework mints one instance per session), and the body derives its
8
+ * PropsStore share from the return type.
9
+ */
10
+ import { type EngineStoreHandle } from '@deepseek-ai/dsh-client-store';
11
+ import type { TabId } from '@deepseek-ai/dsh-client-ui-sidebar-right/client';
12
+ import type { FilePreviewEntry, FilePreviewList } from '../types.ts';
13
+ /** The tab body's state; fetched results are whole values, null until loaded. */
14
+ export interface FilePreviewState {
15
+ /** The latest fetched file list, or null before the first successful load. */
16
+ list: readonly FilePreviewEntry[] | null;
17
+ /** Seq watermark of the loaded list. */
18
+ listAsOfSeq: number;
19
+ /** Bumped by the refresh action to re-trigger the list fetch. */
20
+ listRequestRev: number;
21
+ /** Whether a list fetch is in flight. */
22
+ listLoading: boolean;
23
+ /** Human-readable list-fetch failure, or null when idle. */
24
+ listError: string | null;
25
+ /** Each tab's selected path (tabs of this kind in one session select independently). */
26
+ selected: Record<string, string>;
27
+ }
28
+ /** Annotation twin of the actions literal below (drift fails assignability at defineStore). */
29
+ export type FilePreviewActions = {
30
+ select: (draft: FilePreviewState, tabId: TabId, path: string) => void;
31
+ deselect: (draft: FilePreviewState, tabId: TabId) => void;
32
+ refreshList: (draft: FilePreviewState) => void;
33
+ setList: (draft: FilePreviewState, list: FilePreviewList) => void;
34
+ setListLoading: (draft: FilePreviewState, loading: boolean) => void;
35
+ setListError: (draft: FilePreviewState, error: string | null) => void;
36
+ };
37
+ /**
38
+ * Create the file-preview tab store handle.
39
+ * @returns the store handle (spec + type + identity + factory in one).
40
+ */
41
+ export declare function createFilePreviewStore(): EngineStoreHandle<FilePreviewState, FilePreviewActions>;
42
+ //# sourceMappingURL=file-preview-store.d.ts.map
@@ -0,0 +1,58 @@
1
+ /**
2
+ * The file-preview tab's store: the session's touched-file list (one fetch,
3
+ * shared by every tab of this kind in the session) and each tab's selected
4
+ * path. Module level exports the factory only — a module-level handle would
5
+ * pin the store's identity in the module cache (a de-facto singleton
6
+ * surviving plugin reloads). register() receives the factory (exclusive use:
7
+ * the framework mints one instance per session), and the body derives its
8
+ * PropsStore share from the return type.
9
+ */
10
+ import { defineStore } from '@deepseek-ai/dsh-client-store';
11
+ const INITIAL = {
12
+ list: null,
13
+ listAsOfSeq: -1,
14
+ listRequestRev: 0,
15
+ listLoading: false,
16
+ listError: null,
17
+ selected: {},
18
+ };
19
+ /**
20
+ * Create the file-preview tab store handle.
21
+ * @returns the store handle (spec + type + identity + factory in one).
22
+ */
23
+ export function createFilePreviewStore() {
24
+ return defineStore({
25
+ init: () => ({ ...INITIAL, selected: {} }),
26
+ actions: {
27
+ /**
28
+ * Select one file in one tab (its change history renders below the list).
29
+ * @param d - draft state.
30
+ * @param tabId - the selecting tab.
31
+ * @param path - the recorded display path.
32
+ */
33
+ select: (d, tabId, path) => { d.selected[tabId] = path; },
34
+ /**
35
+ * Clear one tab's selection (the detail view's back gesture).
36
+ * @param d - draft state.
37
+ * @param tabId - the tab going back to the list.
38
+ */
39
+ deselect: (d, tabId) => { delete d.selected[tabId]; },
40
+ /** Re-trigger the list fetch. @param d - draft state. */
41
+ refreshList: (d) => { d.listRequestRev += 1; },
42
+ /**
43
+ * Record a fetched list.
44
+ * @param d - draft state.
45
+ * @param list - the Remote's whole response.
46
+ */
47
+ setList: (d, list) => {
48
+ d.list = list.entries;
49
+ d.listAsOfSeq = list.asOfSeq;
50
+ d.listError = null;
51
+ },
52
+ /** @param d - draft state. @param loading - whether a fetch is in flight. */
53
+ setListLoading: (d, loading) => { d.listLoading = loading; },
54
+ /** @param d - draft state. @param error - the failure to show, or null. */
55
+ setListError: (d, error) => { d.listError = error; },
56
+ },
57
+ });
58
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Generated by scripts/sync-icon-artwork.mts from @deepseek-ai/dsh-client-ui-primitives
3
+ * (v0.1.7-rc.1, harness 46a7f68b09) — do not edit by hand. The 0.1.5 and rc.1 host lines
4
+ * share no icon export name, so the rc.1 artwork is inlined here: pure SVG,
5
+ * no host runtime identity, identical on every line. Re-run the generator
6
+ * after adding an icon import or bumping the harness checkout.
7
+ *
8
+ * @module @khorsheed/dsh-file-preview/client
9
+ */
10
+ /** Shared props for every product icon component (mirrors the upstream IconProps). */
11
+ export interface IconProps {
12
+ /** Square edge in px; defaults to the glyph's own drawn size. */
13
+ size?: number | undefined;
14
+ /** Extra class for layout placement; color rides currentColor. */
15
+ className?: string | undefined;
16
+ }
17
+ export declare const IconChevronDownOutlineMedium: ({ size, className }: IconProps) => import("react").JSX.Element;
18
+ export declare const IconChevronUpOutlineMedium: ({ size, className }: IconProps) => import("react").JSX.Element;
19
+ export declare const IconGlobeOutlineMedium: ({ size, className }: IconProps) => import("react").JSX.Element;
20
+ export declare const IconRefreshOutlineMedium: ({ size, className }: IconProps) => import("react").JSX.Element;
21
+ //# sourceMappingURL=icons.d.ts.map
@@ -0,0 +1,5 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ export const IconChevronDownOutlineMedium = ({ size = 14, className }) => (_jsx("svg", { width: size, height: size, className: className, viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", "aria-hidden": "true", strokeWidth: 1.3, children: _jsx("path", { d: "M4 6L7.29289 9.29289C7.68342 9.68342 8.31658 9.68342 8.70711 9.29289L12 6", stroke: "currentColor" }) }));
3
+ export const IconChevronUpOutlineMedium = ({ size = 14, className }) => (_jsx("svg", { width: size, height: size, className: className, viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", "aria-hidden": "true", strokeWidth: 1.3, children: _jsx("path", { d: "M12 10L8.70711 6.70711C8.31658 6.31658 7.68342 6.31658 7.29289 6.70711L4 10", stroke: "currentColor" }) }));
4
+ export const IconGlobeOutlineMedium = ({ size = 14, className }) => (_jsxs("svg", { width: size, height: size, className: className, viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", "aria-hidden": "true", strokeWidth: 1.3, children: [_jsx("path", { d: "M7.99986 14.0887C11.3626 14.0887 14.0886 11.3627 14.0886 7.99998C14.0886 4.63727 11.3626 1.91125 7.99986 1.91125C4.63715 1.91125 1.91113 4.63727 1.91113 7.99998C1.91113 11.3627 4.63715 14.0887 7.99986 14.0887Z", stroke: "currentColor" }), _jsx("path", { d: "M2.34619 8H13.6538", stroke: "currentColor", strokeLinecap: "square" }), _jsx("path", { d: "M7.99976 14.0889C9.23509 14.0889 10.1743 11.3629 10.1743 8.00006C10.1743 4.63739 9.23509 1.91138 7.99976 1.91138", stroke: "currentColor" }), _jsx("path", { d: "M7.99973 14.0889C6.76445 14.0889 5.8252 11.3629 5.8252 8.00006C5.8252 4.63739 6.76445 1.91138 7.99973 1.91138", stroke: "currentColor" })] }));
5
+ export const IconRefreshOutlineMedium = ({ size = 16, className }) => (_jsxs("svg", { width: size, height: size, className: className, viewBox: "0 0 16 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", "aria-hidden": "true", strokeWidth: 1.3, children: [_jsx("path", { d: "M14.5001 8C14.5 9.28552 14.1188 10.5422 13.4045 11.611C12.6903 12.6799 11.6752 13.5129 10.4875 14.0049C9.29982 14.4968 7.99295 14.6255 6.73212 14.3747C5.4713 14.124 4.31314 13.505 3.4041 12.596C2.49514 11.687 1.87614 10.5288 1.62537 9.26798C1.37459 8.00716 1.50331 6.70028 1.99525 5.51261C2.48719 4.32494 3.32025 3.30981 4.3891 2.59557C5.45795 1.88134 6.71458 1.50008 8.0001 1.5C9.9001 1.5 11.7001 2.3 13.0001 3.6L14.5001 5.1", stroke: "currentColor" }), _jsx("path", { d: "M14.4999 1.5V5.1H10.8999", stroke: "currentColor" })] }));
@@ -0,0 +1,80 @@
1
+ /**
2
+ * File-preview plugin, browser half: a pure additive surface over official
3
+ * extension points only. It registers a page-type right-Sidebar tab (the
4
+ * session's touched files as one full-height list, entered from the guide
5
+ * page), claiming `dsh-resource://file/**` addresses the preview stack renders
6
+ * at the extension band — which outranks the official document tab's fallback
7
+ * band on BOTH host lines (the rc.1 tab registry kept the band mechanism), so
8
+ * file clicks (the file tree, wrapped mentions, the turn card, the
9
+ * deliverables row) land in this package's own page with its full chrome;
10
+ * suffixes the stack declines (pdf, archives, binaries) fall through to the
11
+ * official document tab. A per-turn mutation card sits in the
12
+ * `conversation.chat.turnTail` slot (the bash captures the host half
13
+ * collects, S2). The filePreview Remote is mounted here through the official
14
+ * `ctx.remote.$mount` channel, so the plugin distributes as an independent
15
+ * package with no edits to core packages. Composing this plugin out of
16
+ * cordis.yml removes every surface it adds.
17
+ *
18
+ * Plan B — registering the content pane INTO the official document tab as its
19
+ * default renderer (rc.1's `documentPreviews` registry) plus the headless
20
+ * embedding of phase two — landed and was VETOED by the repo owner the same
21
+ * day (2026-09-24): the seam cost (the renderer loading protocol, the
22
+ * extension-band takeover semantics, the headless layout coupling) and the UX
23
+ * compromises (change history demoted to the renderer dropdown, the copy
24
+ * button floating over the search row) lost to the self-drawn page, which the
25
+ * address claim restores wholesale. The turn-tail convergence survives the
26
+ * veto and stays: on list-kind hosts (0.1.6-alpha.2+) the arm also shadows
27
+ * the official deliverables entry (a second registration under its cell id at
28
+ * `priority: -1` whose empty body wins the cell — the slot system's
29
+ * first-class shadowing; the official entry stays on the ledger so its
30
+ * declared `deliverables.file.actions` child slot never collapses), because
31
+ * the official present card duplicates our durable row with a model-curated
32
+ * subset and the official changes card is memory-resident. On a 0.1.5 host
33
+ * the slot is still the election chain; the registration probes the
34
+ * declaration kind and keeps the old preemptive shape (select + priority -1)
35
+ * there.
36
+ *
37
+ * Retired at the 0.1.5-rc.1 move (seam registry S1): the conversation.view
38
+ * tab, the shell.overlay drawer, and the mention capture-phase DOM
39
+ * interception — the right-Sidebar resource routing (openResource + the
40
+ * tab-type registry) covers all three openings. Retired with the plan-B
41
+ * revert (2026-09-24): the change-history renderer in the official document
42
+ * tab's dropdown (FileHistoryBody) — the change history lives only in this
43
+ * page's detail view again (the Content / 改动记录 toggle).
44
+ */
45
+ import type { Context } from '@deepseek-ai/cordis';
46
+ import { FilePreviewTab } from './FilePreviewTab.tsx';
47
+ import { TurnFileRow } from './TurnFileRow.tsx';
48
+ import { FILE_PREVIEW_ID, FILE_PREVIEW_KIND } from './definition.tsx';
49
+ import type { FilePreviewRemote } from './contract.ts';
50
+ export { DiffHistory } from './DiffHistory.tsx';
51
+ export { FilePreviewTab, TurnFileRow };
52
+ export { FILE_PREVIEW_ID, FILE_PREVIEW_KIND };
53
+ /** Required services: slots, sessions (cwd for the row gestures), the remote
54
+ * channel, the locale, and the right-Sidebar faces (tab-type registry + the
55
+ * navigation service the turn card's outside-workspace gesture opens pages
56
+ * through). The
57
+ * `remote.filePreview` namespace is deliberately NOT an inject: this plugin
58
+ * both mounts the namespace (through `$mount` below) and consumes it, and the
59
+ * Cordis property proxy only resolves services declared in `inject` or
60
+ * provided by an ancestor fiber. Declaring it would deadlock the loader (the
61
+ * fiber waits for the service, which only this apply's `$mount` can provide);
62
+ * the property proxy cannot see the sibling namespace fiber `$mount` spawns.
63
+ * The mount is awaited and the namespace is then read back from the global
64
+ * store with `ctx.get`, which resolves any active provider in the same
65
+ * isolation scope. */
66
+ export declare const inject: string[];
67
+ /**
68
+ * Client plugin body: mount the Remote, register the dictionaries, the tab
69
+ * type, its body, and the per-turn file row.
70
+ * @param ctx - client root context.
71
+ */
72
+ export declare function apply(ctx: Context): Promise<() => Promise<void>>;
73
+ /**
74
+ * Install every visible file-preview client surface after a successful host
75
+ * handshake. Tests can drive this boundary directly; disposal removes the
76
+ * dictionaries, the tab type/body, the turn row and its deliverables shadow,
77
+ * and the mention wrapper.
78
+ */
79
+ export declare function installFilePreviewSurfaces(ctx: Context, remote: FilePreviewRemote): () => Promise<void>;
80
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,238 @@
1
+ import { fileAddressFor, resolveWorkspacePath } from '@deepseek-ai/dsh-util-workspace-path';
2
+ import { writeClipboard } from '@deepseek-ai/dsh-client-ui-primitives';
3
+ import filePreviewRemote from '@khorsheed/dsh-file-preview/remote';
4
+ import { FilePreviewTab } from './FilePreviewTab.js';
5
+ import { TurnFileRow } from './TurnFileRow.js';
6
+ import { createFilePreviewStore } from './file-preview-store.js';
7
+ import { FILE_PREVIEW_ID, FILE_PREVIEW_KIND, filePreviewDefinition } from './definition.js';
8
+ import { basename } from './path-utils.js';
9
+ import { en, NS, zh } from './locales.js';
10
+ import { wrapChatFileMentions } from './mentions-wrap.js';
11
+ import { OpenInAppProbe, pickFileManager, pickIde } from './open-in-app.js';
12
+ import { parentPath } from './path-utils.js';
13
+ import { createTurnFilesLoader } from './turn-files-cache.js';
14
+ import { selectTurnFiles } from './turn-files.js';
15
+ export { DiffHistory } from './DiffHistory.js';
16
+ export { FilePreviewTab, TurnFileRow };
17
+ export { FILE_PREVIEW_ID, FILE_PREVIEW_KIND };
18
+ /** The official deliverables card's turnTail cell id — the shadow target. */
19
+ const DELIVERABLES_TURN_ENTRY = '@deepseek-ai/dsh-client-ui-deliverables';
20
+ /**
21
+ * The deliverables shadow: the empty body that wins the official card's list
22
+ * cell at a lower priority, so the present/changes cards stop rendering while
23
+ * the official registration (and the child slot it declares) stays live.
24
+ */
25
+ function DeliverablesShadow() {
26
+ return null;
27
+ }
28
+ /** Required services: slots, sessions (cwd for the row gestures), the remote
29
+ * channel, the locale, and the right-Sidebar faces (tab-type registry + the
30
+ * navigation service the turn card's outside-workspace gesture opens pages
31
+ * through). The
32
+ * `remote.filePreview` namespace is deliberately NOT an inject: this plugin
33
+ * both mounts the namespace (through `$mount` below) and consumes it, and the
34
+ * Cordis property proxy only resolves services declared in `inject` or
35
+ * provided by an ancestor fiber. Declaring it would deadlock the loader (the
36
+ * fiber waits for the service, which only this apply's `$mount` can provide);
37
+ * the property proxy cannot see the sibling namespace fiber `$mount` spawns.
38
+ * The mount is awaited and the namespace is then read back from the global
39
+ * store with `ctx.get`, which resolves any active provider in the same
40
+ * isolation scope. */
41
+ export const inject = ['slots', 'sessions', 'remote', 'locale', 'sidebarRight', 'sidebarRightTabs'];
42
+ /**
43
+ * Client plugin body: mount the Remote, register the dictionaries, the tab
44
+ * type, its body, and the per-turn file row.
45
+ * @param ctx - client root context.
46
+ */
47
+ export async function apply(ctx) {
48
+ const disposers = [];
49
+ try {
50
+ disposers.push(await ctx.remote.$mount(filePreviewRemote));
51
+ }
52
+ catch (error) {
53
+ // A duplicate descriptor is a composition error. The capability probe
54
+ // below still decides whether any visible surface may exist.
55
+ /* v8 ignore next -- double-mount is a composition error, not a runtime path */
56
+ ctx.logger.error(error);
57
+ }
58
+ // The namespace is registered by $mount above; `ctx.remote.filePreview`
59
+ // cannot see it (the property proxy walks the fiber chain, and the namespace
60
+ // lives in the sibling fiber $mount spawned), so read it back from the global
61
+ // store once the mount has settled and hand the concrete handle to the
62
+ // inject closures, so a lazy `ctx.remote.filePreview` read at call time
63
+ // never trips the property proxy.
64
+ const remote = ctx.get('remote.filePreview');
65
+ if (remote === undefined) {
66
+ return async () => { await Promise.all(disposers.map(dispose => dispose())); };
67
+ }
68
+ try {
69
+ const capabilities = await remote.capabilities();
70
+ if (!capabilities.ok || capabilities.value.protocolVersion !== 1) {
71
+ return async () => { await Promise.all(disposers.map(dispose => dispose())); };
72
+ }
73
+ }
74
+ catch {
75
+ return async () => { await Promise.all(disposers.map(dispose => dispose())); };
76
+ }
77
+ const uninstall = installFilePreviewSurfaces(ctx, remote);
78
+ return async () => {
79
+ await uninstall();
80
+ await Promise.all(disposers.map(dispose => dispose()));
81
+ };
82
+ }
83
+ /**
84
+ * Install every visible file-preview client surface after a successful host
85
+ * handshake. Tests can drive this boundary directly; disposal removes the
86
+ * dictionaries, the tab type/body, the turn row and its deliverables shadow,
87
+ * and the mention wrapper.
88
+ */
89
+ export function installFilePreviewSurfaces(ctx, remote) {
90
+ const disposers = [];
91
+ disposers.push(ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'file-preview: dictionaries'));
92
+ const t = ctx.locale.bind(NS);
93
+ // The turn card's host-fed loader: one RPC warms every turn rendered so far
94
+ // (the host returns the whole per-turn map, cached by the log watermark).
95
+ const turnFilesLoader = createTurnFilesLoader(remote);
96
+ // S1 tail: route prose-mention opens into the sidebar too. The wrap is
97
+ // in-place on ui-deliverables' provided object (see mentions-wrap.ts for
98
+ // why not provide/set). Ordering cannot ride the package edge alone — cordis
99
+ // only re-wakes fibers that DECLARE the service in `inject`
100
+ // (vendor/cordis reflect.ts notify), so a one-shot `ctx.get` here races the
101
+ // deliverables fiber's provide. A nested plugin whose inject names
102
+ // 'chatFileMentions' stays pending until the service appears (and re-runs on
103
+ // HMR re-provide, re-wrapping); in compositions without ui-deliverables it
104
+ // simply never activates, without blocking this plugin.
105
+ const mentionsFiber = ctx.plugin({
106
+ name: '@khorsheed/dsh-file-preview/mentions-wrap',
107
+ inject: ['chatFileMentions'],
108
+ apply: (sub) => {
109
+ wrapChatFileMentions(sub.chatFileMentions, {
110
+ // Mentions open at the canonical file address — the same
111
+ // `dsh-resource://file/session/<id>/<path>` content id the
112
+ // deliverables card's open resolves to, so one file is one tab (the
113
+ // page address `sidebar://file-preview` would mint a second tab for
114
+ // the same file). Our type claims the renderable ones on both host
115
+ // lines; the rest fall through to the official document tab. A throw
116
+ // (no mounted sidebar surface) falls back to the owner's openFile
117
+ // inside the wrap.
118
+ open: (sessionId, path) => {
119
+ ctx.sidebarRight.openResource(fileAddressFor(sessionId, sessionCwd(sessionId), path));
120
+ },
121
+ label: (path) => t('mention.open', { name: basename(path) }),
122
+ });
123
+ },
124
+ });
125
+ disposers.push(() => mentionsFiber.dispose());
126
+ // Row gestures: copy always works (clipboard needs no host capability);
127
+ // folder/IDE gestures key off the once-per-page open-in-app probe — a host
128
+ // without the route (or a failed probe) reads as "no apps" and the buttons
129
+ // stay hidden. Folder open keeps the old drawer's semantics: reveal the
130
+ // file selected in the host file manager (host Remote), falling back to the
131
+ // official open route on the parent folder. IDE open is file-exact, which
132
+ // the official route refuses (directories only) — it rides the host half's
133
+ // `openExternal` instead.
134
+ const sessions = ctx.sessions;
135
+ const openInApps = new OpenInAppProbe();
136
+ const sessionCwd = (sessionId) => sessions.list.getSnapshot().byId[sessionId]?.cwd;
137
+ const revealFolder = (sessionId, path) => {
138
+ const resolved = resolveWorkspacePath(sessionCwd(sessionId), path);
139
+ const fallback = () => {
140
+ const fileManager = pickFileManager(openInApps.apps.getSnapshot() ?? []);
141
+ if (fileManager !== undefined)
142
+ void openInApps.open(fileManager, parentPath(resolved) || '.');
143
+ };
144
+ void remote.reveal(sessionId, path).then((result) => {
145
+ if (result.ok && result.value.revealed)
146
+ return;
147
+ fallback();
148
+ }).catch(fallback);
149
+ };
150
+ const openInIde = (sessionId, path, app) => {
151
+ const ide = app ?? pickIde(openInApps.apps.getSnapshot() ?? []);
152
+ if (ide === undefined)
153
+ return;
154
+ void remote.openExternal(sessionId, path, ide).catch(() => {
155
+ // A launch failure stays silent in the row; the native app surfaces its
156
+ // own error dialog when the path is unusable.
157
+ });
158
+ };
159
+ // Stage one of the right-Sidebar registration: the tab type (guide entry
160
+ // PLUS address claims — `dsh-resource://file/**` for every session-scoped
161
+ // path the preview stack renders; see definition.tsx). Unconditional on
162
+ // both host lines: the default 'extension' band outranks the official
163
+ // document tab's 'fallback'.
164
+ disposers.push(ctx.effect(() => ctx.sidebarRightTabs.register(filePreviewDefinition(t)), 'file-preview: tab type'));
165
+ // Stage two: the body under the type's id in the keyed pane seat.
166
+ disposers.push(ctx.effect(() => ctx.slots.inject('sidebar.right.pane.tab', () => ctx.slots.register({
167
+ name: 'sidebar.right.pane.tab',
168
+ key: FILE_PREVIEW_ID,
169
+ locale: NS,
170
+ store: createFilePreviewStore,
171
+ inject: (sessionId) => ({
172
+ listFiles: (sid) => remote.list(sid),
173
+ readFile: (sid, path) => remote.read(sid, path),
174
+ copyPath: (path) => writeClipboard(resolveWorkspacePath(sessionCwd(sessionId), path)),
175
+ revealFolder: (path) => { revealFolder(sessionId, path); },
176
+ openInIde: (path, app) => { openInIde(sessionId, path, app); },
177
+ loadOpenInApps: () => { void openInApps.load(); },
178
+ hooks: { openInApps: openInApps.apps },
179
+ }),
180
+ }, FilePreviewTab)), 'file-preview: tab body'));
181
+ // The turn card. Probe the declared slot kind inside inject (the callback
182
+ // only runs once the slot exists): a 0.1.6-alpha.2+ host declares a LIST —
183
+ // register a plain entry under the package id, plus the deliverables shadow
184
+ // (the convergence, user decision 2026-09-24: the official present card
185
+ // duplicates this row with a model-curated subset and the official changes
186
+ // card is memory-resident, so only this row renders). The shadow is the
187
+ // slot system's first-class mechanism, not a DOM hack: one cell (the
188
+ // official entry's id), two entries at distinct priorities, the lowest
189
+ // renders (ui-slots SlotCore.register pins the rule — a same-id
190
+ // same-priority registration throws with "register at a different priority
191
+ // to shadow it", and entriesOfSlot projects each cell to its lowest live
192
+ // entry). The official entry stays registered — its declared
193
+ // `deliverables.file.actions` child slot survives for ui-open-in-app's
194
+ // contributions — it just never renders. The row itself returns null until
195
+ // its fetch settles. A 0.1.5 host declares the election CHAIN — keep the
196
+ // old preemptive shape (select claims every turn; priority -1 elects
197
+ // ascending, ahead of the official entry's default 0). The alpha.2
198
+ // KindOptions for this key carry no chain fields, so the chain-branch call
199
+ // is duck-typed through `never` (the escape the spec bench already uses);
200
+ // the list branch stays fully typed and is what checks TurnFileRow's props
201
+ // contract.
202
+ disposers.push(ctx.slots.inject('conversation.chat.turnTail', () => {
203
+ const injectRow = () => ({
204
+ turnFiles: (sessionId, turn) => turnFilesLoader(sessionId, turn),
205
+ });
206
+ const spec = ctx.slots.spec('conversation.chat.turnTail');
207
+ if (spec?.kind === 'chain') {
208
+ return ctx.slots.register({
209
+ name: 'conversation.chat.turnTail',
210
+ priority: -1,
211
+ select: selectTurnFiles,
212
+ locale: NS,
213
+ inject: injectRow,
214
+ }, TurnFileRow);
215
+ }
216
+ const row = ctx.slots.register({
217
+ name: 'conversation.chat.turnTail',
218
+ id: FILE_PREVIEW_ID,
219
+ locale: NS,
220
+ inject: injectRow,
221
+ }, TurnFileRow);
222
+ // Lower than the official entry's default 0, so the empty body wins the
223
+ // cell; a same-priority second registration would throw instead.
224
+ const shadow = ctx.slots.register({
225
+ name: 'conversation.chat.turnTail',
226
+ id: DELIVERABLES_TURN_ENTRY,
227
+ priority: -1,
228
+ }, DeliverablesShadow);
229
+ return () => {
230
+ row();
231
+ shadow();
232
+ };
233
+ }));
234
+ return async () => {
235
+ for (const dispose of disposers.reverse())
236
+ await dispose();
237
+ };
238
+ }
@@ -0,0 +1,16 @@
1
+ /** `filePreview` namespace dictionaries (right-Sidebar tab + document renderer + turn card copy). */
2
+ /** Dictionary namespace owned by this plugin. */
3
+ export declare const NS = "filePreview";
4
+ /** The file-preview dictionary key set (the source of truth for both locales). */
5
+ export type FilePreviewKey = 'open' | 'view.search.placeholder' | 'view.search.noMatch' | 'preview.search.placeholder' | 'preview.search.noMatch' | 'preview.search.prev' | 'preview.search.next' | 'preview.htmlToggle' | 'preview.htmlSource' | 'preview.htmlRender' | 'preview.htmlScript' | 'preview.htmlScriptStop' | 'preview.scriptConfirm' | 'preview.scriptRun' | 'preview.scriptCancel' | 'preview.slowHint' | 'preview.fullscreen' | 'preview.exitFullscreen' | 'preview.staticHint' | 'turn.count' | 'json.copyValue' | 'json.copyJson' | 'json.copyPath' | 'json.copyPrettyJson' | 'json.copyCompactJson' | 'json.copied' | 'json.copyFailed' | 'json.collapseNode' | 'json.expandNode' | 'json.copyButtonTitle' | 'json.treeLabel' | 'markdown.copy' | 'markdown.copied' | 'markdown.footnotes' | 'diff.copy' | 'diff.copied' | 'diff.collapse' | 'diff.collapseAria' | 'diff.expand' | 'diff.expandAria' | 'diff.files' | 'diff.code' | 'diff.wrap' | 'diff.unwrap' | 'guide.title' | 'guide.description' | 'list.empty' | 'list.error' | 'list.loading' | 'list.outsideWorkspace' | 'list.refresh' | 'history.empty' | 'history.step' | 'history.step.count' | 'history.step.latest' | 'history.step.older' | 'history.step.newer' | 'detail.back' | 'mention.open' | 'action.chooseIDE' | 'action.copy' | 'action.copied' | 'action.copyPath' | 'action.openFolder' | 'action.openIDE' | 'action.reload' | 'detail.content' | 'detail.deleted' | 'detail.diff' | 'detail.noSelection' | 'detail.preview' | 'detail.source' | 'local.binary' | 'local.noSelection' | 'local.tooLarge' | 'local.unreadable' | 'search.hit' | 'search.next' | 'search.noMatch' | 'search.placeholder' | 'search.prev' | 'state.error' | 'state.loading';
6
+ declare module '@deepseek-ai/dsh-client-ui-slots' {
7
+ interface LocaleNamespaceMap {
8
+ /** The file-preview tab, document renderer, and turn card copy. */
9
+ 'filePreview': FilePreviewKey;
10
+ }
11
+ }
12
+ /** Simplified Chinese dictionary (the key-set source of truth). */
13
+ export declare const zh: Record<FilePreviewKey, string>;
14
+ /** English dictionary. */
15
+ export declare const en: Record<FilePreviewKey, string>;
16
+ //# sourceMappingURL=locales.d.ts.map