@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,110 @@
1
+ /** Pure path display helpers for the file-preview tab. */
2
+ import { isAbsoluteWorkspacePath } from '@deepseek-ai/dsh-util-workspace-path';
3
+ /** The last path segment; the path itself when it has no separator. */
4
+ export function basename(path) {
5
+ const slash = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\'));
6
+ return slash < 0 ? path : path.slice(slash + 1);
7
+ }
8
+ /** Everything before the last path separator ('' when the path has none). */
9
+ export function parentPath(path) {
10
+ const slash = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\'));
11
+ return slash <= 0 ? '' : path.slice(0, slash);
12
+ }
13
+ /**
14
+ * Sort touched files by their last recorded activity, newest first. The fold
15
+ * keeps first-seen order; the view presents latest-first so the agent's most
16
+ * recent work is at the top.
17
+ * @param entries - the fold's first-seen entries.
18
+ * @returns a new array sorted by descending last-occurrence seq.
19
+ */
20
+ export function sortByLatest(entries) {
21
+ return [...entries].sort((a, b) => b.seq - a.seq);
22
+ }
23
+ /**
24
+ * Case-insensitive substring match of a search query against a file's display
25
+ * path (the whole path, so a directory name matches too). Empty and
26
+ * whitespace-only queries match nothing deliberately — the view treats them as
27
+ * "no search" and shows the unfiltered list.
28
+ * @param path - the entry's display path.
29
+ * @param query - the trimmed search term.
30
+ * @returns whether the path contains the query, ignoring case.
31
+ */
32
+ export function matchesQuery(path, query) {
33
+ if (query.length === 0)
34
+ return false;
35
+ return path.toLowerCase().includes(query.toLowerCase());
36
+ }
37
+ /**
38
+ * Split a file name into [before, match, after] for the first case-insensitive
39
+ * occurrence of `query`, so the view can highlight the matched substring.
40
+ * @param name - the display name to search.
41
+ * @param query - the trimmed search term.
42
+ * @returns the three parts, or null when the name does not contain the query.
43
+ */
44
+ export function highlightMatch(name, query) {
45
+ if (query.length === 0)
46
+ return null;
47
+ const at = name.toLowerCase().indexOf(query.toLowerCase());
48
+ if (at < 0)
49
+ return null;
50
+ return [name.slice(0, at), name.slice(at, at + query.length), name.slice(at + query.length)];
51
+ }
52
+ /** Strip a workspace root prefix for compact display; the path stands when unrelated. */
53
+ export function relativeToCwd(path, cwd) {
54
+ if (cwd === undefined || cwd.length === 0)
55
+ return path;
56
+ if (path === cwd)
57
+ return basename(path);
58
+ const rooted = cwd.endsWith('/') ? cwd : `${cwd}/`;
59
+ return path.startsWith(rooted) ? path.slice(rooted.length) : path;
60
+ }
61
+ /**
62
+ * Whether the official document tab can render this path: the `file` resource
63
+ * is workspace-scoped (the host answers outside-workspace stats with
64
+ * `workspace-file/outside-workspace`), so only workspace-rooted paths get a
65
+ * `dsh-resource://file/...` open. Relative paths qualify (the host resolves
66
+ * them against the session root); an absolute path qualifies when the session
67
+ * cwd is known and contains it, or when the cwd is unknown — the host's own
68
+ * failure line is the honest answer there.
69
+ * @param cwd - the session's workspace root, when known.
70
+ * @param path - the recorded display path (absolute or workspace-relative).
71
+ * @returns whether `openResource` stands a chance of rendering the file.
72
+ */
73
+ export function isWithinWorkspace(cwd, path) {
74
+ if (!isAbsoluteWorkspacePath(path))
75
+ return true;
76
+ if (cwd === undefined || cwd.length === 0)
77
+ return true;
78
+ const root = cwd.replace(/\\/g, '/').replace(/\/+$/, '');
79
+ const target = path.replace(/\\/g, '/');
80
+ return target === root || target.startsWith(`${root}/`);
81
+ }
82
+ /** Whether a path names an HTML document (`.html`/`.htm`), offered a
83
+ * sandboxed render view alongside the source view. */
84
+ export function isHtmlPath(path) {
85
+ const dot = path.lastIndexOf('.');
86
+ if (dot < 0)
87
+ return false;
88
+ const ext = path.slice(dot).toLowerCase();
89
+ return ext === '.html' || ext === '.htm';
90
+ }
91
+ /** Map a file extension to a prism language name for CodeBlock, or undefined to auto-detect. */
92
+ const LANGUAGE_BY_EXTENSION = {
93
+ '.ts': 'typescript', '.tsx': 'typescript', '.mts': 'typescript', '.cts': 'typescript',
94
+ '.js': 'javascript', '.jsx': 'javascript', '.mjs': 'javascript', '.cjs': 'javascript',
95
+ '.json': 'json', '.jsonc': 'json', '.md': 'markdown', '.mdx': 'markdown',
96
+ '.yml': 'yaml', '.yaml': 'yaml', '.html': 'html', '.htm': 'html',
97
+ '.css': 'css', '.scss': 'scss', '.less': 'less', '.sh': 'bash', '.bash': 'bash',
98
+ '.py': 'python', '.sql': 'sql', '.xml': 'xml', '.toml': 'toml', '.ini': 'ini',
99
+ '.go': 'go', '.rs': 'rust', '.java': 'java', '.c': 'c', '.h': 'c',
100
+ '.cpp': 'cpp', '.hpp': 'cpp', '.rb': 'ruby', '.php': 'php', '.swift': 'swift',
101
+ '.kotlin': 'kotlin', '.vue': 'vue', '.svelte': 'svelte', '.dockerfile': 'docker',
102
+ '.graphql': 'graphql', '.proto': 'protobuf', '.diff': 'diff', '.patch': 'diff',
103
+ };
104
+ /** The prism language name for a path's extension, or undefined for auto-detection. */
105
+ export function languageFor(path) {
106
+ const dot = path.lastIndexOf('.');
107
+ if (dot < 0)
108
+ return undefined;
109
+ return LANGUAGE_BY_EXTENSION[path.slice(dot).toLowerCase()];
110
+ }
@@ -0,0 +1,27 @@
1
+ import type { TranslateNS } from '@deepseek-ai/dsh-client-ui-slots';
2
+ import type { PreviewRead, PreviewTranslator, StructuredLabels } from '@khorsheed/dsh-client-ui-content-preview/src/client/index.ts';
3
+ import type { FilePreviewRead } from '../types.ts';
4
+ /**
5
+ * Map one file-preview read onto the kernel's contract.
6
+ *
7
+ * The wire type carries `path` plus optional per-kind fields (like the
8
+ * local-files one), so the arms are spelled out rather than cast: an image read
9
+ * with no URL becomes `missing`/`unreadable` instead of an `<img>` with no
10
+ * source.
11
+ * @param read - the host's read result.
12
+ * @returns the kernel's normalized read.
13
+ */
14
+ export declare function toPreviewRead(read: FilePreviewRead): PreviewRead;
15
+ /**
16
+ * The pane's translator over this plugin's `filePreview` dictionary.
17
+ * @param t - the locale-bound translator for this plugin's namespace.
18
+ * @returns the kernel's translator.
19
+ */
20
+ export declare function previewTranslator(t: TranslateNS<'filePreview'>): PreviewTranslator;
21
+ /**
22
+ * The structured-render chrome for the kernel's JSON tree / CSV table / markdown.
23
+ * @param t - the locale-bound translator for this plugin's namespace.
24
+ * @returns the kernel's label object.
25
+ */
26
+ export declare function structuredLabels(t: TranslateNS<'filePreview'>): StructuredLabels;
27
+ //# sourceMappingURL=preview.d.ts.map
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Map one file-preview read onto the kernel's contract.
3
+ *
4
+ * The wire type carries `path` plus optional per-kind fields (like the
5
+ * local-files one), so the arms are spelled out rather than cast: an image read
6
+ * with no URL becomes `missing`/`unreadable` instead of an `<img>` with no
7
+ * source.
8
+ * @param read - the host's read result.
9
+ * @returns the kernel's normalized read.
10
+ */
11
+ export function toPreviewRead(read) {
12
+ switch (read.kind) {
13
+ case 'text':
14
+ return {
15
+ kind: 'text',
16
+ path: read.path,
17
+ content: read.content ?? '',
18
+ ...(read.truncated === true ? { truncated: true } : {}),
19
+ ...(read.htmlScripted === true ? { htmlScripted: true } : {}),
20
+ };
21
+ case 'image':
22
+ return read.url === undefined
23
+ ? { kind: 'missing', path: read.path, reason: 'unreadable' }
24
+ : { kind: 'image', path: read.path, url: read.url, ...(read.size === undefined ? {} : { size: read.size }) };
25
+ case 'binary':
26
+ return { kind: 'binary', path: read.path, ...(read.size === undefined ? {} : { size: read.size }) };
27
+ case 'too-large':
28
+ return { kind: 'too-large', path: read.path, ...(read.size === undefined ? {} : { size: read.size }) };
29
+ case 'missing':
30
+ return { kind: 'missing', path: read.path };
31
+ case 'error':
32
+ return { kind: 'error', path: read.path, ...(read.message === undefined ? {} : { message: read.message }) };
33
+ }
34
+ }
35
+ /**
36
+ * The pane's translator over this plugin's `filePreview` dictionary.
37
+ * @param t - the locale-bound translator for this plugin's namespace.
38
+ * @returns the kernel's translator.
39
+ */
40
+ export function previewTranslator(t) {
41
+ return (key, params) => t(key, params);
42
+ }
43
+ /** JsonTree chrome over this plugin's dictionary. */
44
+ function jsonTreeLabels(t) {
45
+ return {
46
+ copyValue: t('json.copyValue'),
47
+ copyJson: t('json.copyJson'),
48
+ copyPath: t('json.copyPath'),
49
+ copyPrettyJson: t('json.copyPrettyJson'),
50
+ copyCompactJson: t('json.copyCompactJson'),
51
+ copied: t('json.copied'),
52
+ copyFailed: t('json.copyFailed'),
53
+ collapseNode: t('json.collapseNode'),
54
+ expandNode: t('json.expandNode'),
55
+ copyButtonTitle: action => t('json.copyButtonTitle', { action }),
56
+ };
57
+ }
58
+ /** MarkdownText chrome over this plugin's dictionary. */
59
+ function markdownLabels(t) {
60
+ return {
61
+ code: { copyLabel: t('action.copy'), copiedLabel: t('action.copied') },
62
+ footnotes: t('markdown.footnotes'),
63
+ };
64
+ }
65
+ /**
66
+ * The structured-render chrome for the kernel's JSON tree / CSV table / markdown.
67
+ * @param t - the locale-bound translator for this plugin's namespace.
68
+ * @returns the kernel's label object.
69
+ */
70
+ export function structuredLabels(t) {
71
+ return { json: jsonTreeLabels(t), markdown: markdownLabels(t) };
72
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Session-level cache in front of the `filePreview.turnFiles` RPC. One fetch
3
+ * warms every turn the response carries (the host returns the whole per-turn
4
+ * map up to its watermark, and the host itself caches the fold by that
5
+ * watermark), and every requested turn — including one the response reports
6
+ * empty — is pinned so a re-mount of the same card never refetches. A card for
7
+ * a newer, not-yet-seen turn triggers one refetch, which the host answers
8
+ * without refolding. Errors resolve to an empty list — the card just shows
9
+ * nothing.
10
+ * @module @khorsheed/dsh-file-preview
11
+ */
12
+ import type { FilePreviewTurnFile } from '../types.ts';
13
+ import type { SessionId } from '@deepseek-ai/dsh-api-remotes/client';
14
+ import type { FilePreviewRemote } from './contract.ts';
15
+ /**
16
+ * Build a turn-files loader bound to one mounted Remote (created per apply, so
17
+ * the cache never pins module-level identity across plugin reloads).
18
+ * @param remote - the mounted filePreview Remote handle.
19
+ * @returns a loader resolving one session's files for one turn.
20
+ */
21
+ export declare function createTurnFilesLoader(remote: FilePreviewRemote): (sessionId: SessionId, turn: number) => Promise<readonly FilePreviewTurnFile[]>;
22
+ /** The loader's function shape, shared with the inject face. */
23
+ export type TurnFilesLoader = ReturnType<typeof createTurnFilesLoader>;
24
+ //# sourceMappingURL=turn-files-cache.d.ts.map
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Session-level cache in front of the `filePreview.turnFiles` RPC. One fetch
3
+ * warms every turn the response carries (the host returns the whole per-turn
4
+ * map up to its watermark, and the host itself caches the fold by that
5
+ * watermark), and every requested turn — including one the response reports
6
+ * empty — is pinned so a re-mount of the same card never refetches. A card for
7
+ * a newer, not-yet-seen turn triggers one refetch, which the host answers
8
+ * without refolding. Errors resolve to an empty list — the card just shows
9
+ * nothing.
10
+ * @module @khorsheed/dsh-file-preview
11
+ */
12
+ /**
13
+ * Build a turn-files loader bound to one mounted Remote (created per apply, so
14
+ * the cache never pins module-level identity across plugin reloads).
15
+ * @param remote - the mounted filePreview Remote handle.
16
+ * @returns a loader resolving one session's files for one turn.
17
+ */
18
+ export function createTurnFilesLoader(remote) {
19
+ const cache = new Map();
20
+ return (sessionId, turn) => {
21
+ const sessionCache = cache.get(sessionId);
22
+ if (sessionCache !== undefined && sessionCache.has(turn)) {
23
+ return Promise.resolve(sessionCache.get(turn) ?? []);
24
+ }
25
+ return remote.turnFiles(sessionId).then((result) => {
26
+ const merged = cache.get(sessionId) ?? new Map();
27
+ if (result.ok) {
28
+ for (const group of result.value.turns)
29
+ merged.set(group.turn, group.files);
30
+ }
31
+ // Pin the requested turn's resolution (possibly empty) so the same card
32
+ // never refetches; newer turns still trigger one refetch each.
33
+ if (!merged.has(turn))
34
+ merged.set(turn, []);
35
+ cache.set(sessionId, merged);
36
+ return merged.get(turn) ?? [];
37
+ });
38
+ };
39
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Turn-tail chain claim for the file-preview turn card — the 0.1.5 fallback
3
+ * path only. 0.1.6-alpha.2 re-kinded `conversation.chat.turnTail` from chain
4
+ * (election) to list (every entry renders; no selector exists), where the
5
+ * card coexists with the official cards and self-hides without data; this
6
+ * claim survives for the chain branch of the dual-host registration in
7
+ * index.ts. The card is an async thin shell: the host `filePreview.turnFiles`
8
+ * RPC is the SINGLE source of truth for every turn's mutations — write/edit
9
+ * calls, Code Mode dispatches, render-intent paths from result diff meta, and
10
+ * bash captures — so the card and the file-preview tab read the same host
11
+ * data. The claim is unconditional so the card claims every turn (priority
12
+ * -1, ahead of the official deliverables entry's default 0 — the chain elects
13
+ * ascending) and renders nothing until its fetch settles or the turn has no
14
+ * files.
15
+ */
16
+ import type { TurnTailOwnerProps } from '@deepseek-ai/dsh-client-ui-chat/client';
17
+ /**
18
+ * Claim every turn of the chain (the row decides visibility from its fetch).
19
+ * @param _owner - chain owner currency; unused — the claim never declines.
20
+ * @returns a non-null marker so this entry wins the election whenever the
21
+ * earlier official entries declined the turn.
22
+ */
23
+ export declare function selectTurnFiles(_owner: TurnTailOwnerProps): readonly [];
24
+ /** Re-export for the row component's basename display. */
25
+ export declare function basename(path: string): string;
26
+ //# sourceMappingURL=turn-files.d.ts.map
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Claim every turn of the chain (the row decides visibility from its fetch).
3
+ * @param _owner - chain owner currency; unused — the claim never declines.
4
+ * @returns a non-null marker so this entry wins the election whenever the
5
+ * earlier official entries declined the turn.
6
+ */
7
+ export function selectTurnFiles(_owner) {
8
+ return [];
9
+ }
10
+ /** Re-export for the row component's basename display. */
11
+ export function basename(path) {
12
+ const at = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\'));
13
+ return at === -1 ? path : path.slice(at + 1);
14
+ }
package/locale/en.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "meta": {
3
- "title": "File Preview",
4
- "description": "Read-only listing of session-written files with their current content."
3
+ "title": "Session Artifacts",
4
+ "description": "Preview files the agent wrote or edited and step through every change."
5
5
  }
6
6
  }
package/locale/zh.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "meta": {
3
- "title": "文件预览",
4
- "description": "只读列出会话写过、改过的文件并提供当前文本。"
3
+ "title": "会话产物",
4
+ "description": "预览 agent 写过、改过的文件,步进每一次改动。"
5
5
  }
6
6
  }
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@khorsheed/dsh-file-preview",
3
- "description": "Read-only file-preview Remote service: lists the files a session wrote or edited and serves their current text content",
3
+ "description": "Session products preview, host+client in one: the read-only file-preview Remote service (the files a session wrote or edited, current content, per-change diffs) plus the right-sidebar 会话产物 tab, per-turn change cards, and the file detail view",
4
4
  "keywords": [
5
5
  "dsh",
6
6
  "dsh-plugin",
7
7
  "cordis",
8
8
  "file-preview"
9
9
  ],
10
- "version": "0.3.2",
10
+ "version": "0.4.0",
11
11
  "type": "module",
12
12
  "main": "lib/index.js",
13
13
  "types": "lib/types/index.d.ts",
@@ -24,6 +24,10 @@
24
24
  "types": "./lib/types/types.d.ts",
25
25
  "default": "./lib/types/types.js"
26
26
  },
27
+ "./client": {
28
+ "types": "./lib/types/client/index.d.ts",
29
+ "default": "./lib/client.js"
30
+ },
27
31
  "./typert": {
28
32
  "types": "./lib/typert.host.d.ts",
29
33
  "default": "./lib/typert.host.js"
@@ -49,14 +53,48 @@
49
53
  ],
50
54
  "license": "MIT",
51
55
  "peerDependencies": {
56
+ "@deepseek-ai/cordis": "^4.0.1",
52
57
  "@deepseek-ai/dsh-agent": "^0.1.0-rc.6",
58
+ "@deepseek-ai/dsh-api-remotes": "^0.1.0-rc.6",
59
+ "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.6",
60
+ "@deepseek-ai/dsh-client-ui-chat": "^0.1.0-rc.6",
61
+ "@deepseek-ai/dsh-client-ui-primitives": "^0.1.0-rc.6",
62
+ "@deepseek-ai/dsh-client-ui-sidebar-right": "^0.1.7-rc.1",
63
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.6",
53
64
  "@deepseek-ai/dsh-fs": "^0.1.0-rc.6",
54
65
  "@deepseek-ai/dsh-invariants": "^0.1.0-rc.6",
55
66
  "@deepseek-ai/dsh-native-command": "^0.1.0-rc.6",
56
67
  "@deepseek-ai/dsh-session": "^0.1.0-rc.6",
57
68
  "@deepseek-ai/dsh-tools": "^0.1.0-rc.6",
58
69
  "@deepseek-ai/dsh-typert-protocol": "^0.1.0-rc.6",
59
- "@deepseek-ai/cordis": "^4.0.1"
70
+ "react": "^18.2.0",
71
+ "react-dom": "^18.2.0"
72
+ },
73
+ "peerDependenciesMeta": {
74
+ "@deepseek-ai/dsh-api-remotes": {
75
+ "optional": true
76
+ },
77
+ "@deepseek-ai/dsh-client-locale": {
78
+ "optional": true
79
+ },
80
+ "@deepseek-ai/dsh-client-ui-chat": {
81
+ "optional": true
82
+ },
83
+ "@deepseek-ai/dsh-client-ui-primitives": {
84
+ "optional": true
85
+ },
86
+ "@deepseek-ai/dsh-client-ui-sidebar-right": {
87
+ "optional": true
88
+ },
89
+ "@deepseek-ai/dsh-client-ui-slots": {
90
+ "optional": true
91
+ },
92
+ "react": {
93
+ "optional": true
94
+ },
95
+ "react-dom": {
96
+ "optional": true
97
+ }
60
98
  },
61
99
  "dependencies": {
62
100
  "zod": "^4.4.3"
@@ -65,9 +103,22 @@
65
103
  "bundle": {
66
104
  "patch": "./cordis.patch.yml"
67
105
  },
106
+ "client": {
107
+ "inject": [
108
+ "@deepseek-ai/dsh-api-remotes",
109
+ "@deepseek-ai/dsh-api-session-controller",
110
+ "@deepseek-ai/dsh-client-locale",
111
+ "@deepseek-ai/dsh-client-ui-conversation",
112
+ "@deepseek-ai/dsh-client-ui-deliverables",
113
+ "@deepseek-ai/dsh-client-ui-session",
114
+ "@deepseek-ai/dsh-client-ui-sidebar-right"
115
+ ],
116
+ "platform": "web"
117
+ },
68
118
  "compat": {
69
119
  "minHost": "0.1.5-rc.1",
70
- "verifiedHost": "0.1.5-rc.1"
120
+ "verifiedHost": "0.1.5-rc.1",
121
+ "notes": "单一形态双线一致:自建「会话产物」page 型右栏 tab(向导页进入 + extension 档认领 dsh-resource://file/** 可渲染后缀,压过官方 document tab 的 fallback 档——rc.1 的 tab-registry 保留 band 机制),文件树/mention/回合卡/产物行点击全部落自绘详情页(共享内容面板全 chrome + 内容/改动记录切换 + 复制路径/在文件夹/IDE 打开);pdf/压缩包/二进制等 decline 后缀落回官方 document tab。方案 B(内容面注册进官方 documentPreviews 面做默认渲染器)与二期 headless 嵌入已于 2026-09-24 同日被仓主否决回退——接缝成本(renderer loading 协议/extension 档语义/headless 布局耦合)与 UX 妥协(改动记录塞进渲染器下拉、复制钮浮动压搜索行)不成立;改动记录维度(TurnFileRow + 详情页 DiffHistory)官方仍无对应物(workspace-changes 内存态、git-only、重启即失),维持自留并跟踪上游。turnTail 回合卡照常:0.1.6-alpha.2 起 list 槽臂同时注册官方 deliverables 条目的一等遮蔽(同 cell id、priority -1 空体胜出,官方条目在册故其子槽不塌),0.1.5 chain 槽臂保持 select + priority -1 抢占回退——均不涉及 rc.1 专有 API,minHost 不动。0.4.0 起宿主 Remote 与浏览器半同包(ui-file-preview 并入),headless 组合只装宿主半照常工作(client 面在 web 组合里经 dsh.client 发现)。"
71
122
  }
72
123
  }
73
124
  }