@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.
- package/CHANGELOG.md +6 -0
- package/README.en.md +38 -20
- package/README.i18n.yaml +2 -2
- package/README.md +39 -21
- package/cordis.patch.yml +10 -2
- package/lib/client.js +8427 -0
- package/lib/typert.host.js +6 -2
- package/lib/types/client/DiffHistory.d.ts +20 -0
- package/lib/types/client/DiffHistory.js +56 -0
- package/lib/types/client/FilePreviewTab.d.ts +34 -0
- package/lib/types/client/FilePreviewTab.js +193 -0
- package/lib/types/client/TurnFileRow.d.ts +30 -0
- package/lib/types/client/TurnFileRow.js +72 -0
- package/lib/types/client/contract.d.ts +39 -0
- package/lib/types/client/contract.js +2 -0
- package/lib/types/client/definition.d.ts +55 -0
- package/lib/types/client/definition.js +93 -0
- package/lib/types/client/file-preview-store.d.ts +42 -0
- package/lib/types/client/file-preview-store.js +58 -0
- package/lib/types/client/icons.d.ts +21 -0
- package/lib/types/client/icons.js +5 -0
- package/lib/types/client/index.d.ts +80 -0
- package/lib/types/client/index.js +238 -0
- package/lib/types/client/locales.d.ts +16 -0
- package/lib/types/client/locales.js +177 -0
- package/lib/types/client/mentions-wrap.d.ts +60 -0
- package/lib/types/client/mentions-wrap.js +42 -0
- package/lib/types/client/open-in-app.d.ts +85 -0
- package/lib/types/client/open-in-app.js +146 -0
- package/lib/types/client/path-utils.d.ts +53 -0
- package/lib/types/client/path-utils.js +110 -0
- package/lib/types/client/preview.d.ts +27 -0
- package/lib/types/client/preview.js +72 -0
- package/lib/types/client/turn-files-cache.d.ts +24 -0
- package/lib/types/client/turn-files-cache.js +39 -0
- package/lib/types/client/turn-files.d.ts +26 -0
- package/lib/types/client/turn-files.js +14 -0
- package/locale/en.json +2 -2
- package/locale/zh.json +2 -2
- 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
|