@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,177 @@
1
+ /** `filePreview` namespace dictionaries (right-Sidebar tab + document renderer + turn card copy). */
2
+ /** Dictionary namespace owned by this plugin. */
3
+ export const NS = 'filePreview';
4
+ /** Simplified Chinese dictionary (the key-set source of truth). */
5
+ export const zh = {
6
+ 'open': '产物',
7
+ 'view.search.placeholder': '搜索文件…',
8
+ 'view.search.noMatch': '没有匹配的文件',
9
+ 'preview.search.placeholder': '搜索文件内容…',
10
+ 'preview.search.noMatch': '没有匹配内容',
11
+ 'preview.search.prev': '上一个匹配',
12
+ 'preview.search.next': '下一个匹配',
13
+ 'preview.htmlToggle': 'HTML 视图',
14
+ 'preview.htmlSource': '源码',
15
+ 'preview.htmlRender': '渲染',
16
+ 'preview.htmlScript': '运行脚本',
17
+ 'preview.htmlScriptStop': '停止运行',
18
+ 'preview.scriptConfirm': '此文件含脚本,将在隔离沙箱中运行(无网络、无法访问宿主)',
19
+ 'preview.scriptRun': '运行',
20
+ 'preview.scriptCancel': '取消',
21
+ 'preview.slowHint': '渲染超时——文档可能过大,可切源码视图或在浏览器中打开',
22
+ 'preview.fullscreen': '全屏',
23
+ 'preview.exitFullscreen': '退出全屏',
24
+ 'preview.staticHint': '静态预览:此页面含脚本,脚本不会运行——点右上角「运行脚本」可交互',
25
+ 'turn.count': '{count} 个产物',
26
+ 'json.copyValue': '复制值',
27
+ 'json.copyJson': '复制 JSON',
28
+ 'json.copyPath': '复制属性路径',
29
+ 'json.copyPrettyJson': '复制格式化 JSON',
30
+ 'json.copyCompactJson': '复制紧凑 JSON',
31
+ 'json.copied': '已复制',
32
+ 'json.copyFailed': '复制失败',
33
+ 'json.collapseNode': '折叠 JSON 节点',
34
+ 'json.expandNode': '展开 JSON 节点',
35
+ 'json.copyButtonTitle': '{action};右键查看更多复制选项',
36
+ 'json.treeLabel': 'JSON 结构',
37
+ 'markdown.copy': '复制',
38
+ 'markdown.copied': '已复制',
39
+ 'markdown.footnotes': '脚注',
40
+ 'diff.copy': '复制差异',
41
+ 'diff.copied': '已复制',
42
+ 'diff.collapse': '收起',
43
+ 'diff.collapseAria': '收起差异',
44
+ 'diff.expand': '展开其余 {count} 行',
45
+ 'diff.expandAria': '展开其余 {count} 行',
46
+ 'diff.files': '{count} 个文件',
47
+ 'diff.code': '代码块',
48
+ 'diff.wrap': '自动换行',
49
+ 'diff.unwrap': '取消自动换行',
50
+ 'guide.title': '会话产物',
51
+ 'guide.description': '会话写过的每个文件:看内容,也看每一次改动',
52
+ 'list.empty': '这个会话还没有写过文件',
53
+ 'list.error': '文件列表加载失败',
54
+ 'list.loading': '加载中…',
55
+ 'list.outsideWorkspace': '位于工作区外,无内容预览',
56
+ 'list.refresh': '刷新',
57
+ 'history.empty': '该文件没有记录到改动内容',
58
+ 'history.step': '第 {turn} 轮 · 第 {step} 步',
59
+ 'history.step.count': '修改 {current}/{total}',
60
+ 'history.step.latest': '最新',
61
+ 'history.step.older': '查看更早的修改',
62
+ 'history.step.newer': '查看更新的修改',
63
+ 'detail.back': '返回产物列表',
64
+ 'mention.open': '在侧边栏打开 {name}',
65
+ // The shared content pane's keys (see the header note above).
66
+ 'action.chooseIDE': '选择 IDE',
67
+ 'action.copy': '复制',
68
+ 'action.copied': '已复制',
69
+ 'action.copyPath': '复制路径',
70
+ 'action.openFolder': '打开目录',
71
+ 'action.openIDE': '在 IDE 中打开',
72
+ 'action.reload': '重新加载',
73
+ 'detail.content': '内容',
74
+ 'detail.deleted': '文件已删除',
75
+ 'detail.diff': '改动',
76
+ 'detail.noSelection': '选择一个文件查看详情',
77
+ 'detail.preview': '预览',
78
+ 'detail.source': '源码',
79
+ 'local.binary': '二进制文件,无法预览文本',
80
+ 'local.noSelection': '选择一个文件预览内容',
81
+ 'local.tooLarge': '文件过大,未载入预览',
82
+ 'local.unreadable': '无法读取该文件',
83
+ 'search.hit': '匹配 {current}/{total}',
84
+ 'search.next': '下一个匹配',
85
+ 'search.noMatch': '无匹配',
86
+ 'search.placeholder': '在内容中搜索…',
87
+ 'search.prev': '上一个匹配',
88
+ 'state.error': '加载失败:{message}',
89
+ 'state.loading': '加载中…',
90
+ };
91
+ /** English dictionary. */
92
+ export const en = {
93
+ 'open': 'Produced',
94
+ 'view.search.placeholder': 'Search files…',
95
+ 'view.search.noMatch': 'No matching files',
96
+ 'preview.search.placeholder': 'Search file content…',
97
+ 'preview.search.noMatch': 'No matching content',
98
+ 'preview.search.prev': 'Previous match',
99
+ 'preview.search.next': 'Next match',
100
+ 'preview.htmlToggle': 'HTML view',
101
+ 'preview.htmlSource': 'Source',
102
+ 'preview.htmlRender': 'Render',
103
+ 'preview.htmlScript': 'Run scripts',
104
+ 'preview.htmlScriptStop': 'Stop',
105
+ 'preview.scriptConfirm': 'This file contains scripts; they will run in an isolated sandbox (no network, no host access)',
106
+ 'preview.scriptRun': 'Run',
107
+ 'preview.scriptCancel': 'Cancel',
108
+ 'preview.slowHint': 'Render timed out — the document may be too large; try the source view or open it in a browser',
109
+ 'preview.fullscreen': 'Fullscreen',
110
+ 'preview.exitFullscreen': 'Exit fullscreen',
111
+ 'preview.staticHint': 'Static preview: this page contains scripts, which do not run here — use "Run scripts" above for interactivity',
112
+ 'turn.count': '{count} products',
113
+ 'json.copyValue': 'Copy value',
114
+ 'json.copyJson': 'Copy JSON',
115
+ 'json.copyPath': 'Copy property path',
116
+ 'json.copyPrettyJson': 'Copy pretty JSON',
117
+ 'json.copyCompactJson': 'Copy compact JSON',
118
+ 'json.copied': 'Copied',
119
+ 'json.copyFailed': 'Copy failed',
120
+ 'json.collapseNode': 'Collapse JSON node',
121
+ 'json.expandNode': 'Expand JSON node',
122
+ 'json.copyButtonTitle': '{action}; right-click for copy options',
123
+ 'json.treeLabel': 'JSON structure',
124
+ 'markdown.copy': 'Copy',
125
+ 'markdown.copied': 'Copied',
126
+ 'markdown.footnotes': 'Footnotes',
127
+ 'diff.copy': 'Copy diff',
128
+ 'diff.copied': 'Copied',
129
+ 'diff.collapse': 'Collapse',
130
+ 'diff.collapseAria': 'Collapse the diff',
131
+ 'diff.expand': 'Show {count} more lines',
132
+ 'diff.expandAria': 'Show {count} more lines',
133
+ 'diff.files': '{count} files',
134
+ 'diff.code': 'Code block',
135
+ 'diff.wrap': 'Wrap lines',
136
+ 'diff.unwrap': 'Do not wrap lines',
137
+ 'guide.title': 'Session products',
138
+ 'guide.description': 'Every file the session wrote — its content and each change',
139
+ 'list.empty': 'This session has not written any files yet',
140
+ 'list.error': 'Failed to load the file list',
141
+ 'list.loading': 'Loading…',
142
+ 'list.outsideWorkspace': 'Outside the workspace — no content preview',
143
+ 'list.refresh': 'Refresh',
144
+ 'history.empty': 'No change content recorded for this file',
145
+ 'history.step': 'Turn {turn} · Step {step}',
146
+ 'history.step.count': 'Change {current}/{total}',
147
+ 'history.step.latest': 'latest',
148
+ 'history.step.older': 'View an earlier change',
149
+ 'history.step.newer': 'View a newer change',
150
+ 'detail.back': 'Back to products',
151
+ 'mention.open': 'Open {name} in the sidebar',
152
+ // The shared content pane's keys (see the header note above).
153
+ 'action.chooseIDE': 'Choose IDE',
154
+ 'action.copy': 'Copy',
155
+ 'action.copied': 'Copied',
156
+ 'action.copyPath': 'Copy path',
157
+ 'action.openFolder': 'Open folder',
158
+ 'action.openIDE': 'Open in IDE',
159
+ 'action.reload': 'Reload',
160
+ 'detail.content': 'Content',
161
+ 'detail.deleted': 'File deleted',
162
+ 'detail.diff': 'Changes',
163
+ 'detail.noSelection': 'Select a file to view',
164
+ 'detail.preview': 'Preview',
165
+ 'detail.source': 'Source',
166
+ 'local.binary': 'Binary file — text preview unavailable',
167
+ 'local.noSelection': 'Select a file to preview',
168
+ 'local.tooLarge': 'File too large — not loaded into preview',
169
+ 'local.unreadable': 'This file could not be read',
170
+ 'search.hit': 'Match {current}/{total}',
171
+ 'search.next': 'Next match',
172
+ 'search.noMatch': 'No matches',
173
+ 'search.placeholder': 'Search in content…',
174
+ 'search.prev': 'Previous match',
175
+ 'state.error': 'Failed to load: {message}',
176
+ 'state.loading': 'Loading…',
177
+ };
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Mention open-route unification (S1 tail): the official ui-deliverables
3
+ * `chatFileMentions` routes clicks on files delivered through the `present`
4
+ * tool to the native default application (`opener.open`), while undelivered
5
+ * files open in the right sidebar (`owner.openFile`). This plugin wants every
6
+ * prose mention to open in this package's detail view, so the provided
7
+ * service's `forClosing` is WRAPPED IN PLACE: the original implementation
8
+ * keeps its claim logic (which paths link), while every resolved mention gets
9
+ * a new `open` (the detail view through the injected reroute, falling back to
10
+ * `owner.openFile` — the official openResource route, which the file-preview
11
+ * tab type claims for known products — when the reroute throws) and a new
12
+ * label (the official text for present-delivered files says "open in the
13
+ * default app", which is no longer true).
14
+ *
15
+ * Why in-place mutation: `ctx.provide` rejects a second provider and
16
+ * `ctx.set` rejects any fiber but the provider's own ("cannot set property in
17
+ * multiple fibers", vendor/cordis reflect.ts) — no official cross-plugin
18
+ * replacement seam exists (the open-direction override is the S1 tail
19
+ * registered in docs/upstream-seam-registry.md). The mutated object is the
20
+ * exact reference the chat view reads per call
21
+ * (`ctx.get('chatFileMentions')?.forClosing(...)`), so the wrap takes effect
22
+ * without a remount. Ordering rides the dsh.client.inject package edge on
23
+ * ui-deliverables (compose order = apply order). If the deliverables fiber
24
+ * ever reloads (HMR), its fresh provide discards this wrap — accepted; the
25
+ * original behavior (native open for presented files) simply returns.
26
+ *
27
+ * The resolved path is recovered from the mention's `title`, which the
28
+ * official builder sets to the path (producedFileMentions, 0.1.5-rc.1) — a
29
+ * structural assumption re-checked on every host upgrade.
30
+ *
31
+ * @module @khorsheed/dsh-file-preview
32
+ */
33
+ import type { ChatFileMentions } from '@deepseek-ai/dsh-client-ui-chat/client';
34
+ /** The open behavior and label the wrap substitutes for every resolved mention. */
35
+ export interface MentionReroute {
36
+ /**
37
+ * Open one resolved path in this package's own surface (the detail view).
38
+ * Takes the viewed session so the open can name the canonical
39
+ * `dsh-resource://file/session/<id>/<path>` address — the same tab the
40
+ * deliverables card's open reveals (one file, one tab). May throw (no
41
+ * mounted sidebar surface); the wrap then falls back to the owner's
42
+ * official `openFile`.
43
+ */
44
+ open(sessionId: string, path: string): void;
45
+ /**
46
+ * The mention's action label.
47
+ * @param path - the resolved path (the hit's title).
48
+ * @returns the visible label.
49
+ */
50
+ label(path: string): string;
51
+ }
52
+ /**
53
+ * Wrap the provided chatFileMentions service so every resolved mention opens
54
+ * in this package's detail view instead of the native default application.
55
+ * Idempotent: a second wrap of the same object is a no-op.
56
+ * @param mentions - the live service object ui-deliverables provided.
57
+ * @param reroute - the substituted open behavior and label.
58
+ */
59
+ export declare function wrapChatFileMentions(mentions: ChatFileMentions, reroute: MentionReroute): void;
60
+ //# sourceMappingURL=mentions-wrap.d.ts.map
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Wrap the provided chatFileMentions service so every resolved mention opens
3
+ * in this package's detail view instead of the native default application.
4
+ * Idempotent: a second wrap of the same object is a no-op.
5
+ * @param mentions - the live service object ui-deliverables provided.
6
+ * @param reroute - the substituted open behavior and label.
7
+ */
8
+ export function wrapChatFileMentions(mentions, reroute) {
9
+ if (WRAP_MARK in mentions)
10
+ return;
11
+ const original = mentions.forClosing.bind(mentions);
12
+ mentions.forClosing = (owner, sessionId) => {
13
+ const resolved = original(owner, sessionId);
14
+ if (resolved === undefined)
15
+ return undefined;
16
+ return {
17
+ resolve(value) {
18
+ const hit = resolved.resolve(value);
19
+ if (hit === undefined)
20
+ return undefined;
21
+ // hit.title is the resolved path (official builder contract).
22
+ return {
23
+ ...hit,
24
+ label: reroute.label(hit.title),
25
+ open: () => {
26
+ try {
27
+ reroute.open(sessionId, hit.title);
28
+ }
29
+ catch {
30
+ // No mounted sidebar surface (or a lost race): the official
31
+ // openFile still lands the file in the right sidebar.
32
+ owner.openFile(hit.title);
33
+ }
34
+ },
35
+ };
36
+ },
37
+ };
38
+ };
39
+ Object.defineProperty(mentions, WRAP_MARK, { value: true });
40
+ }
41
+ /** Non-enumerable marker making the in-place wrap idempotent. */
42
+ const WRAP_MARK = Symbol.for('dsh.file-preview.mentionsWrapped');
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Browser face of the official open-in-app capability (host 0.1.5,
3
+ * `@deepseek-ai/dsh-host-open-in-app`): a once-per-page probe of the apps
4
+ * route decides whether the products tab's folder/IDE gestures render, and
5
+ * the folder fallback POSTs the open route. Mirrors local-files'
6
+ * `open-in-app.ts` (the client bundle purity gate forbids a cross-plugin
7
+ * value import); the route constants and wire payloads mirror the official
8
+ * `./shared` module verbatim — a mirrored constant degrades to "gestures
9
+ * hidden" rather than a boot failure if the host routes ever move. A host
10
+ * without open-in-app answers the probe with 404 (or the request fails
11
+ * outright), which reads exactly like "no apps": the gestures stay hidden,
12
+ * silently.
13
+ *
14
+ * @module @khorsheed/dsh-file-preview
15
+ */
16
+ import { type SnapshotStore } from '@deepseek-ai/dsh-client-store';
17
+ /** GET route serving the probed application ids (official shared.ts). */
18
+ export declare const OPEN_IN_APP_APPS_ROUTE = "/open-in-app/apps";
19
+ /** POST route launching one application on one directory (official shared.ts). */
20
+ export declare const OPEN_IN_APP_OPEN_ROUTE = "/open-in-app/open";
21
+ /** Apps-route response: catalog ids probed as installed, in menu order. */
22
+ export interface OpenInAppAppsPayload {
23
+ readonly apps: readonly string[];
24
+ }
25
+ /** Open-route request body (directories only — the route 404s a file path). */
26
+ export interface OpenInAppOpenPayload {
27
+ readonly app: string;
28
+ readonly path: string;
29
+ }
30
+ /**
31
+ * The file-manager catalog id for the "open folder" gesture.
32
+ * @param apps - probed app ids in the host's menu order.
33
+ * @returns the chosen catalog id, or undefined when no file manager resolved.
34
+ */
35
+ export declare function pickFileManager(apps: readonly string[]): string | undefined;
36
+ /**
37
+ * The editor/IDE catalog id for the "open in IDE" gesture.
38
+ * @param apps - probed app ids in the host's menu order.
39
+ * @returns the chosen catalog id, or undefined when no IDE resolved.
40
+ */
41
+ export declare function pickIde(apps: readonly string[]): string | undefined;
42
+ /** One probed IDE entry for the split button's menu. */
43
+ export interface IdeChoice {
44
+ readonly id: string;
45
+ readonly label: string;
46
+ }
47
+ /**
48
+ * Every probed IDE in catalog order, nameable ones only (a catalog id without
49
+ * a known label stays invisible rather than showing a raw id — the official
50
+ * OpenInAppAction's rule).
51
+ * @param apps - probed app ids in the host's menu order.
52
+ */
53
+ export declare function listIdes(apps: readonly string[]): readonly IdeChoice[];
54
+ type Fetch = (input: string | URL, init?: RequestInit) => Promise<Response>;
55
+ /**
56
+ * Owns the once-per-page apps probe and the folder-fallback POST. The probe
57
+ * publishes through a snapshot store: null until the host answered, the
58
+ * probed id list after. A failed read publishes an empty list, which hides
59
+ * the gestures — the same degrade the official header split button renders.
60
+ */
61
+ export declare class OpenInAppProbe {
62
+ private readonly fetcher;
63
+ /** Installed app ids in host menu order; null until the host answered. */
64
+ readonly apps: SnapshotStore<readonly string[] | null>;
65
+ private loading;
66
+ /**
67
+ * @param fetcher - HTTP carrier for the apps read and the folder POST.
68
+ */
69
+ constructor(fetcher?: Fetch);
70
+ /**
71
+ * Read availability once per probe life; concurrent calls share the read.
72
+ * @returns after availability is published.
73
+ */
74
+ load(): Promise<void>;
75
+ /**
76
+ * Launch one installed app on a directory.
77
+ * @param appId - catalog id from the availability list.
78
+ * @param path - an absolute directory path (the open route refuses files).
79
+ * @returns true when the host acknowledged the launch, false on any failure.
80
+ */
81
+ open(appId: string, path: string): Promise<boolean>;
82
+ private run;
83
+ }
84
+ export {};
85
+ //# sourceMappingURL=open-in-app.d.ts.map
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Browser face of the official open-in-app capability (host 0.1.5,
3
+ * `@deepseek-ai/dsh-host-open-in-app`): a once-per-page probe of the apps
4
+ * route decides whether the products tab's folder/IDE gestures render, and
5
+ * the folder fallback POSTs the open route. Mirrors local-files'
6
+ * `open-in-app.ts` (the client bundle purity gate forbids a cross-plugin
7
+ * value import); the route constants and wire payloads mirror the official
8
+ * `./shared` module verbatim — a mirrored constant degrades to "gestures
9
+ * hidden" rather than a boot failure if the host routes ever move. A host
10
+ * without open-in-app answers the probe with 404 (or the request fails
11
+ * outright), which reads exactly like "no apps": the gestures stay hidden,
12
+ * silently.
13
+ *
14
+ * @module @khorsheed/dsh-file-preview
15
+ */
16
+ import { createSnapshotStore } from '@deepseek-ai/dsh-client-store';
17
+ /** GET route serving the probed application ids (official shared.ts). */
18
+ export const OPEN_IN_APP_APPS_ROUTE = '/open-in-app/apps';
19
+ /** POST route launching one application on one directory (official shared.ts). */
20
+ export const OPEN_IN_APP_OPEN_ROUTE = '/open-in-app/open';
21
+ /**
22
+ * Catalog ids (official catalog.ts) that open a directory in the host's file
23
+ * manager. In catalog order, so the first present id is the platform's own
24
+ * manager (Finder / Explorer / the Linux default handler).
25
+ */
26
+ const FILE_MANAGER_IDS = ['finder', 'explorer', 'filemanager'];
27
+ /** Catalog ids (official catalog.ts) that open a directory in an editor or IDE, in catalog order. */
28
+ const IDE_IDS = [
29
+ 'cursor', 'vscode', 'vscodeinsiders', 'windsurf', 'zed', 'sublimetext', 'xcode',
30
+ 'androidstudio', 'intellij', 'pycharm', 'webstorm', 'phpstorm', 'goland', 'rider', 'rustrover',
31
+ ];
32
+ /**
33
+ * The file-manager catalog id for the "open folder" gesture.
34
+ * @param apps - probed app ids in the host's menu order.
35
+ * @returns the chosen catalog id, or undefined when no file manager resolved.
36
+ */
37
+ export function pickFileManager(apps) {
38
+ return FILE_MANAGER_IDS.find(id => apps.includes(id));
39
+ }
40
+ /**
41
+ * The editor/IDE catalog id for the "open in IDE" gesture.
42
+ * @param apps - probed app ids in the host's menu order.
43
+ * @returns the chosen catalog id, or undefined when no IDE resolved.
44
+ */
45
+ export function pickIde(apps) {
46
+ return IDE_IDS.find(id => apps.includes(id));
47
+ }
48
+ /** Display names per IDE catalog id (official catalog naming; proper nouns, not localized). */
49
+ const IDE_LABELS = {
50
+ cursor: 'Cursor',
51
+ vscode: 'Visual Studio Code',
52
+ vscodeinsiders: 'Visual Studio Code - Insiders',
53
+ windsurf: 'Windsurf',
54
+ zed: 'Zed',
55
+ sublimetext: 'Sublime Text',
56
+ xcode: 'Xcode',
57
+ androidstudio: 'Android Studio',
58
+ intellij: 'IntelliJ IDEA',
59
+ pycharm: 'PyCharm',
60
+ webstorm: 'WebStorm',
61
+ phpstorm: 'PhpStorm',
62
+ goland: 'GoLand',
63
+ rider: 'Rider',
64
+ rustrover: 'RustRover',
65
+ };
66
+ /**
67
+ * Every probed IDE in catalog order, nameable ones only (a catalog id without
68
+ * a known label stays invisible rather than showing a raw id — the official
69
+ * OpenInAppAction's rule).
70
+ * @param apps - probed app ids in the host's menu order.
71
+ */
72
+ export function listIdes(apps) {
73
+ return IDE_IDS.filter(id => apps.includes(id) && IDE_LABELS[id] !== undefined)
74
+ .map(id => ({ id, label: IDE_LABELS[id] }));
75
+ }
76
+ /** Resolve the browser's Host base with the connection carrier's null-origin fallback. */
77
+ function hostBase() {
78
+ const origin = globalThis.location?.origin;
79
+ return origin !== undefined && origin !== 'null' ? origin : 'http://dsh.internal';
80
+ }
81
+ /**
82
+ * Owns the once-per-page apps probe and the folder-fallback POST. The probe
83
+ * publishes through a snapshot store: null until the host answered, the
84
+ * probed id list after. A failed read publishes an empty list, which hides
85
+ * the gestures — the same degrade the official header split button renders.
86
+ */
87
+ export class OpenInAppProbe {
88
+ fetcher;
89
+ /** Installed app ids in host menu order; null until the host answered. */
90
+ apps = createSnapshotStore(null);
91
+ loading;
92
+ /**
93
+ * @param fetcher - HTTP carrier for the apps read and the folder POST.
94
+ */
95
+ constructor(fetcher = (input, init) => fetch(input, init)) {
96
+ this.fetcher = fetcher;
97
+ }
98
+ /**
99
+ * Read availability once per probe life; concurrent calls share the read.
100
+ * @returns after availability is published.
101
+ */
102
+ load() {
103
+ this.loading ??= this.run();
104
+ return this.loading;
105
+ }
106
+ /**
107
+ * Launch one installed app on a directory.
108
+ * @param appId - catalog id from the availability list.
109
+ * @param path - an absolute directory path (the open route refuses files).
110
+ * @returns true when the host acknowledged the launch, false on any failure.
111
+ */
112
+ async open(appId, path) {
113
+ try {
114
+ const body = { app: appId, path };
115
+ const response = await this.fetcher(new URL(OPEN_IN_APP_OPEN_ROUTE, hostBase()), {
116
+ method: 'POST',
117
+ headers: { 'content-type': 'application/json' },
118
+ body: JSON.stringify(body),
119
+ });
120
+ return response.ok;
121
+ }
122
+ catch {
123
+ // Swallows network failures: a host that accepted the probe can still
124
+ // vanish mid-session; the native app surfaces its own launch errors.
125
+ return false;
126
+ }
127
+ }
128
+ async run() {
129
+ let apps = [];
130
+ try {
131
+ const response = await this.fetcher(new URL(OPEN_IN_APP_APPS_ROUTE, hostBase()), {
132
+ headers: { accept: 'application/json' },
133
+ });
134
+ if (response.ok) {
135
+ const payload = await response.json();
136
+ if (Array.isArray(payload.apps))
137
+ apps = payload.apps.filter(id => typeof id === 'string');
138
+ }
139
+ }
140
+ catch {
141
+ // Swallows network failures: a host without open-in-app (404) or an
142
+ // unreachable host reads as no apps, and the gestures simply stay hidden.
143
+ }
144
+ this.apps.set(apps);
145
+ }
146
+ }
@@ -0,0 +1,53 @@
1
+ /** Pure path display helpers for the file-preview tab. */
2
+ import type { FilePreviewEntry } from '../types.ts';
3
+ /** The last path segment; the path itself when it has no separator. */
4
+ export declare function basename(path: string): string;
5
+ /** Everything before the last path separator ('' when the path has none). */
6
+ export declare function parentPath(path: string): string;
7
+ /**
8
+ * Sort touched files by their last recorded activity, newest first. The fold
9
+ * keeps first-seen order; the view presents latest-first so the agent's most
10
+ * recent work is at the top.
11
+ * @param entries - the fold's first-seen entries.
12
+ * @returns a new array sorted by descending last-occurrence seq.
13
+ */
14
+ export declare function sortByLatest(entries: readonly FilePreviewEntry[]): FilePreviewEntry[];
15
+ /**
16
+ * Case-insensitive substring match of a search query against a file's display
17
+ * path (the whole path, so a directory name matches too). Empty and
18
+ * whitespace-only queries match nothing deliberately — the view treats them as
19
+ * "no search" and shows the unfiltered list.
20
+ * @param path - the entry's display path.
21
+ * @param query - the trimmed search term.
22
+ * @returns whether the path contains the query, ignoring case.
23
+ */
24
+ export declare function matchesQuery(path: string, query: string): boolean;
25
+ /**
26
+ * Split a file name into [before, match, after] for the first case-insensitive
27
+ * occurrence of `query`, so the view can highlight the matched substring.
28
+ * @param name - the display name to search.
29
+ * @param query - the trimmed search term.
30
+ * @returns the three parts, or null when the name does not contain the query.
31
+ */
32
+ export declare function highlightMatch(name: string, query: string): readonly [string, string, string] | null;
33
+ /** Strip a workspace root prefix for compact display; the path stands when unrelated. */
34
+ export declare function relativeToCwd(path: string, cwd: string | undefined): string;
35
+ /**
36
+ * Whether the official document tab can render this path: the `file` resource
37
+ * is workspace-scoped (the host answers outside-workspace stats with
38
+ * `workspace-file/outside-workspace`), so only workspace-rooted paths get a
39
+ * `dsh-resource://file/...` open. Relative paths qualify (the host resolves
40
+ * them against the session root); an absolute path qualifies when the session
41
+ * cwd is known and contains it, or when the cwd is unknown — the host's own
42
+ * failure line is the honest answer there.
43
+ * @param cwd - the session's workspace root, when known.
44
+ * @param path - the recorded display path (absolute or workspace-relative).
45
+ * @returns whether `openResource` stands a chance of rendering the file.
46
+ */
47
+ export declare function isWithinWorkspace(cwd: string | undefined, path: string): boolean;
48
+ /** Whether a path names an HTML document (`.html`/`.htm`), offered a
49
+ * sandboxed render view alongside the source view. */
50
+ export declare function isHtmlPath(path: string): boolean;
51
+ /** The prism language name for a path's extension, or undefined for auto-detection. */
52
+ export declare function languageFor(path: string): string | undefined;
53
+ //# sourceMappingURL=path-utils.d.ts.map