@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
@@ -686,6 +686,10 @@ export const TYPERT = {
686
686
  "name": "RoomDispatchEvent",
687
687
  "declaration": "export interface RoomDispatchEvent {\n readonly plan?: { readonly goalId: string; readonly taskId?: string; readonly attemptId?: string; };\n readonly requestSignature?: string;\n readonly id?: string;\n readonly targetIds?: readonly string[];\n readonly origin?: 'human' | 'coordinator' | 'report' | 'relay';\n readonly replyTo?: string;\n readonly reportFor?: string;\n readonly targets: readonly string[];\n readonly text: string;\n}"
688
688
  },
689
+ {
690
+ "name": "RoomExecutionMetadataEvent",
691
+ "declaration": "export interface RoomExecutionMetadataEvent {\n readonly runId: string;\n readonly model?: string;\n readonly effort?: string;\n readonly tokens?: number;\n}"
692
+ },
689
693
  {
690
694
  "name": "RoomGoalEvent",
691
695
  "declaration": "export interface RoomGoalEvent {\n readonly text: string;\n}"
@@ -720,7 +724,7 @@ export const TYPERT = {
720
724
  },
721
725
  {
722
726
  "name": "RoomRunStateEvent",
723
- "declaration": "export interface RoomRunStateEvent {\n readonly runId?: string;\n readonly member: string;\n readonly state: 'running' | 'done' | 'cancelled' | 'failed';\n readonly startedAt: number;\n readonly elapsedMs?: number;\n readonly error?: string;\n}"
727
+ "declaration": "export interface RoomRunStateEvent {\n readonly childSessionId?: SessionId;\n readonly model?: string;\n readonly effort?: string;\n readonly tokens?: number;\n readonly runId?: string;\n readonly member: string;\n readonly state: 'running' | 'done' | 'cancelled' | 'failed';\n readonly startedAt: number;\n readonly elapsedMs?: number;\n readonly error?: string;\n}"
724
728
  },
725
729
  {
726
730
  "name": "RoomSpeechEvent",
@@ -752,7 +756,7 @@ export const TYPERT = {
752
756
  },
753
757
  {
754
758
  "name": "SessionEventMap",
755
- "declaration": "export interface SessionEventMap {\n 'turn/start': { turn: number; };\n 'turn/end': { turn: number; reason: TurnEndReason; };\n 'step/start': { turn: number; step: number; };\n 'step/end': { turn: number; step: number; };\n 'user/message': UserMessage;\n 'developer/message': { turn: number; step: number; message: DeveloperMessage; headerSeq?: SessionSeq; };\n 'system/message': { turn: number; step: number; message: SystemMessage; };\n 'assistant/message': { turn: number; step: number; message: AssistantMessage; stream: AssistantStreamRecord[]; usage?: TokenUsage; interrupted?: true; };\n 'assistant/attempt': { turn: number; step: number; stream: AssistantStreamRecord[]; };\n 'tool/call': { turn: number; step: number; callId: ToolCallId; name: string; arguments: string; };\n 'tool/result': { turn: number; step: number; message: ToolResultMessage; error?: { name: string; code: string; reason?: string; }; meta?: JsonValue; };\n 'request/header': { header: EpochHeader; reason: RequestHeaderReason; startsSeries?: true; };\n 'request/context': RequestContext;\n 'session/end-seed': { inherited?: true; };\n 'agent/inbox/spliced': { target: InboxTarget; start: number; removedCount?: number; inserted: UserMessage[]; outcome?: 'canceled'; };\n 'sandbox/mode': { mode: SandboxMode; source?: 'delegation'; };\n 'approval/asked': { id: ApprovalRequestId; toolName: string; callId?: ToolCallId; reason?: string; };\n 'approval/decided': { id: ApprovalRequestId; outcome: ApprovalOutcome; };\n 'approval/policy': { policy: ApprovalPolicy; source?: 'delegation'; };\n 'tool/ptc-dispatch-start': PtcDispatchStartEventData;\n 'tool/ptc-dispatch': PtcDispatchEventData;\n 'agent-preset/selected': { agentPreset: string; };\n 'command/run': { commandId: CommandId; name: string; args?: string; source: CommandSource; };\n 'command/done': { commandId: CommandId; kind: 'success' | 'error'; text?: string; sourceEventSeq?: import('@deepseek-ai/dsh-session/types').SessionSeq; };\n 'subagent/descriptor': SubagentDescriptorData;\n 'image/offload': { targets: ImageOffloadTarget[]; };\n 'session/title': SessionTitleEventData;\n 'todo/write': { todos: TodoItem[]; };\n 'permission/preset': { preset: string; };\n 'subagent/catalog': SubagentCatalogEvent;\n 'local-agent/stream': LocalAgentStreamCheckpoint;\n 'room/created': RoomCreatedEvent;\n 'room/member-added': RoomMemberAddedEvent;\n 'room/member-updated': RoomMemberUpdatedEvent;\n 'room/member-removed': RoomMemberRemovedEvent;\n 'room/dispatch': RoomDispatchEvent;\n 'room/speech': RoomSpeechEvent;\n 'room/run-state': RoomRunStateEvent;\n 'room/relay': RoomRelayEvent;\n 'room/relay-resolved': RoomRelayResolvedEvent;\n 'room/task-added': RoomTaskAddedEvent;\n 'room/task-updated': RoomTaskUpdatedEvent;\n 'room/task-edited': RoomTaskEditedEvent;\n 'room/goal': RoomGoalEvent;\n 'room/plan-state': RoomPlan;\n 'room/coordinator': RoomCoordinatorEvent;\n 'room/delivery-state': RoomDeliveryStateEvent;\n}"
759
+ "declaration": "export interface SessionEventMap {\n 'turn/start': { turn: number; };\n 'turn/end': { turn: number; reason: TurnEndReason; };\n 'step/start': { turn: number; step: number; };\n 'step/end': { turn: number; step: number; };\n 'user/message': UserMessage;\n 'developer/message': { turn: number; step: number; message: DeveloperMessage; headerSeq?: SessionSeq; };\n 'system/message': { turn: number; step: number; message: SystemMessage; };\n 'assistant/message': { turn: number; step: number; message: AssistantMessage; stream: AssistantStreamRecord[]; usage?: TokenUsage; interrupted?: true; };\n 'assistant/attempt': { turn: number; step: number; stream: AssistantStreamRecord[]; };\n 'tool/call': { turn: number; step: number; callId: ToolCallId; name: string; arguments: string; };\n 'tool/result': { turn: number; step: number; message: ToolResultMessage; error?: { name: string; code: string; reason?: string; }; meta?: JsonValue; };\n 'request/header': { header: EpochHeader; reason: RequestHeaderReason; startsSeries?: true; };\n 'request/context': RequestContext;\n 'session/end-seed': { inherited?: true; };\n 'agent/inbox/spliced': { target: InboxTarget; start: number; removedCount?: number; inserted: UserMessage[]; outcome?: 'canceled'; };\n 'sandbox/mode': { mode: SandboxMode; source?: 'delegation'; };\n 'approval/asked': { id: ApprovalRequestId; toolName: string; callId?: ToolCallId; reason?: string; };\n 'approval/decided': { id: ApprovalRequestId; outcome: ApprovalOutcome; };\n 'approval/policy': { policy: ApprovalPolicy; source?: 'delegation'; };\n 'tool/ptc-dispatch-start': PtcDispatchStartEventData;\n 'tool/ptc-dispatch': PtcDispatchEventData;\n 'agent-preset/selected': { agentPreset: string; };\n 'command/run': { commandId: CommandId; name: string; args?: string; source: CommandSource; };\n 'command/done': { commandId: CommandId; kind: 'success' | 'error'; text?: string; sourceEventSeq?: import('@deepseek-ai/dsh-session/types').SessionSeq; };\n 'subagent/descriptor': SubagentDescriptorData;\n 'image/offload': { targets: ImageOffloadTarget[]; };\n 'session/title': SessionTitleEventData;\n 'todo/write': { todos: TodoItem[]; };\n 'permission/preset': { preset: string; };\n 'subagent/catalog': SubagentCatalogEvent;\n 'local-agent/stream': LocalAgentStreamCheckpoint;\n 'room/created': RoomCreatedEvent;\n 'room/member-added': RoomMemberAddedEvent;\n 'room/member-updated': RoomMemberUpdatedEvent;\n 'room/member-removed': RoomMemberRemovedEvent;\n 'room/dispatch': RoomDispatchEvent;\n 'room/speech': RoomSpeechEvent;\n 'room/run-state': RoomRunStateEvent;\n 'room/execution-metadata': RoomExecutionMetadataEvent;\n 'room/relay': RoomRelayEvent;\n 'room/relay-resolved': RoomRelayResolvedEvent;\n 'room/task-added': RoomTaskAddedEvent;\n 'room/task-updated': RoomTaskUpdatedEvent;\n 'room/task-edited': RoomTaskEditedEvent;\n 'room/goal': RoomGoalEvent;\n 'room/plan-state': RoomPlan;\n 'room/coordinator': RoomCoordinatorEvent;\n 'room/delivery-state': RoomDeliveryStateEvent;\n}"
756
760
  },
757
761
  {
758
762
  "name": "SessionEventType",
@@ -0,0 +1,20 @@
1
+ /** Change-history view: steps through every recorded write/edit diff of one
2
+ * file — the surface the official document tab has no counterpart for, and the
3
+ * reason this package keeps a self-drawn body at all. */
4
+ import { type ReactNode } from 'react';
5
+ import type { TranslateNS } from '@deepseek-ai/dsh-client-ui-slots';
6
+ import type { FilePreviewEntry } from '../types.ts';
7
+ /**
8
+ * One file's change history: the recorded write/edit diffs with a stepper. The
9
+ * step index counts from the LATEST change (0 = most recent, the default
10
+ * position), matching "修改记录 2/2 · 最新" reading where ‹ walks older and ›
11
+ * walks newer. Keyed by the selection at the render site, so the position
12
+ * resets per file. The pane's diff face owns the inset and the scrollport
13
+ * (the kernel's .diffScroll), so this view draws no surface of its own.
14
+ * @param props - the list entry carrying the diffs, and the locale seat.
15
+ */
16
+ export declare function DiffHistory(props: {
17
+ entry: FilePreviewEntry;
18
+ t: TranslateNS<'filePreview'>;
19
+ }): ReactNode;
20
+ //# sourceMappingURL=DiffHistory.d.ts.map
@@ -0,0 +1,56 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /** Change-history view: steps through every recorded write/edit diff of one
3
+ * file — the surface the official document tab has no counterpart for, and the
4
+ * reason this package keeps a self-drawn body at all. */
5
+ import { useState } from 'react';
6
+ import { DiffBlock } from '@deepseek-ai/dsh-client-ui-primitives';
7
+ import css from './FilePreviewTab.module.css';
8
+ /** Localized DiffBlock chrome from the filePreview dictionary. */
9
+ function diffBlockChrome(t) {
10
+ const labels = {
11
+ copy: t('diff.copy'),
12
+ copied: t('diff.copied'),
13
+ collapse: t('diff.collapse'),
14
+ collapseAria: t('diff.collapseAria'),
15
+ expand: (count) => t('diff.expand', { count }),
16
+ expandAria: (count) => t('diff.expandAria', { count }),
17
+ codeLabel: t('diff.code'),
18
+ wrapLabel: t('diff.wrap'),
19
+ unwrapLabel: t('diff.unwrap'),
20
+ };
21
+ labels.files = (count) => t('diff.files', { count });
22
+ return { labels };
23
+ }
24
+ /**
25
+ * One file's change history: the recorded write/edit diffs with a stepper. The
26
+ * step index counts from the LATEST change (0 = most recent, the default
27
+ * position), matching "修改记录 2/2 · 最新" reading where ‹ walks older and ›
28
+ * walks newer. Keyed by the selection at the render site, so the position
29
+ * resets per file. The pane's diff face owns the inset and the scrollport
30
+ * (the kernel's .diffScroll), so this view draws no surface of its own.
31
+ * @param props - the list entry carrying the diffs, and the locale seat.
32
+ */
33
+ export function DiffHistory(props) {
34
+ const { entry, t } = props;
35
+ const diffs = entry.diffs;
36
+ // Index over `entry.diffs` (event order); 0 = the latest change, the default.
37
+ const [diffIndex, setDiffIndex] = useState(0);
38
+ const current = diffs[diffs.length - 1 - diffIndex];
39
+ if (current === undefined)
40
+ return null;
41
+ const stepDiff = (delta) => {
42
+ setDiffIndex((index) => {
43
+ const next = index + delta;
44
+ // The stepper buttons are disabled at both bounds, so a delta can never
45
+ // leave the range; the clamps keep the index honest for direct callers.
46
+ /* v8 ignore next -- bounds are disabled at the UI; direct callers stay in range */
47
+ if (next < 0)
48
+ return 0;
49
+ /* v8 ignore next -- bounds are disabled at the UI; direct callers stay in range */
50
+ if (next >= diffs.length)
51
+ return diffs.length - 1;
52
+ return next;
53
+ });
54
+ };
55
+ return (_jsxs("div", { children: [_jsxs("div", { className: css.stepper, children: [_jsx("button", { type: "button", className: css.stepButton, onClick: () => { stepDiff(1); }, disabled: diffIndex >= diffs.length - 1, "aria-label": t('history.step.older'), children: "\u2039" }), _jsxs("span", { className: css.stepLabel, children: [t('history.step.count', { current: diffs.length - diffIndex, total: diffs.length }), diffIndex === 0 ? ` · ${t('history.step.latest')}` : '', ' · ', t('history.step', { turn: current.turn, step: current.step })] }), _jsx("button", { type: "button", className: css.stepButton, onClick: () => { stepDiff(-1); }, disabled: diffIndex <= 0, "aria-label": t('history.step.newer'), children: "\u203A" })] }), _jsx(DiffBlock, { className: css.diffWrap, diffs: [{ path: entry.path, oldText: current.oldText, newText: current.newText }], ...diffBlockChrome(t) })] }));
56
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * The file-preview right-Sidebar tab body — **both host lines** (the plan-B
3
+ * embedding into the official document tab landed and was vetoed the same
4
+ * day, 2026-09-24): the session's touched files as a
5
+ * full-height list, navigating IN-TAB to a detail view on row click. The
6
+ * tab type claims `dsh-resource://file/**` at the extension band, which the
7
+ * rc.1 tab registry kept outranking the official document tab's fallback
8
+ * band — so file clicks (file tree, mentions, deliverables row, turn card)
9
+ * land here with this package's full chrome, and only the declined suffixes
10
+ * (pdf, archives, binaries) fall through to the official tab.
11
+ *
12
+ * The detail view is the SHARED content pane (@khorsheed/dsh-client-ui-content-preview)
13
+ * with this plugin's adapter (preview.ts): the pane owns the title/path rows,
14
+ * the copy-path / show-in-folder / open-in-IDE (split, app-listed) gestures,
15
+ * the content⇄change-history switch, the sandboxed HTML tiers, content search
16
+ * and the markdown/JSON/CSV renderings; this file supplies the read, the IDE
17
+ * choices and the 改动记录 body (the shared DiffHistory). Being our own view it
18
+ * works for outside-workspace files too (reveal/openExternal take absolute
19
+ * paths, and the host `read` resolves them) — they are no longer list-only
20
+ * rows. The mention wrap routes prose opens through the same claim, so a
21
+ * mentioned file lands in this page too.
22
+ *
23
+ * Navigation params (`openTab('file-preview', { params: { path } })`, the
24
+ * turn card's outside-workspace gesture) select the path on arrival — which
25
+ * lands directly on the detail view.
26
+ */
27
+ import { type ReactNode } from 'react';
28
+ import type { FilePreviewTabProps } from './contract.ts';
29
+ /**
30
+ * The file-preview tab body.
31
+ * @param props - composed props (runtime + store + injected + locale shares).
32
+ */
33
+ export declare function FilePreviewTab(props: FilePreviewTabProps): ReactNode;
34
+ //# sourceMappingURL=FilePreviewTab.d.ts.map
@@ -0,0 +1,193 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * The file-preview right-Sidebar tab body — **both host lines** (the plan-B
4
+ * embedding into the official document tab landed and was vetoed the same
5
+ * day, 2026-09-24): the session's touched files as a
6
+ * full-height list, navigating IN-TAB to a detail view on row click. The
7
+ * tab type claims `dsh-resource://file/**` at the extension band, which the
8
+ * rc.1 tab registry kept outranking the official document tab's fallback
9
+ * band — so file clicks (file tree, mentions, deliverables row, turn card)
10
+ * land here with this package's full chrome, and only the declined suffixes
11
+ * (pdf, archives, binaries) fall through to the official tab.
12
+ *
13
+ * The detail view is the SHARED content pane (@khorsheed/dsh-client-ui-content-preview)
14
+ * with this plugin's adapter (preview.ts): the pane owns the title/path rows,
15
+ * the copy-path / show-in-folder / open-in-IDE (split, app-listed) gestures,
16
+ * the content⇄change-history switch, the sandboxed HTML tiers, content search
17
+ * and the markdown/JSON/CSV renderings; this file supplies the read, the IDE
18
+ * choices and the 改动记录 body (the shared DiffHistory). Being our own view it
19
+ * works for outside-workspace files too (reveal/openExternal take absolute
20
+ * paths, and the host `read` resolves them) — they are no longer list-only
21
+ * rows. The mention wrap routes prose opens through the same claim, so a
22
+ * mentioned file lands in this page too.
23
+ *
24
+ * Navigation params (`openTab('file-preview', { params: { path } })`, the
25
+ * turn card's outside-workspace gesture) select the path on arrival — which
26
+ * lands directly on the detail view.
27
+ */
28
+ import { useEffect, useMemo, useRef, useState } from 'react';
29
+ import { parseFileAddress, resolveWorkspacePath } from '@deepseek-ai/dsh-util-workspace-path';
30
+ import { FileTypeIcon } from '@deepseek-ai/dsh-client-ui-primitives';
31
+ import { IconGlobeOutlineMedium, IconRefreshOutlineMedium } from './icons.js';
32
+ import { ContentPane, } from '@khorsheed/dsh-client-ui-content-preview/src/client/index.ts';
33
+ import { DiffHistory } from './DiffHistory.js';
34
+ import { listIdes, pickFileManager } from './open-in-app.js';
35
+ import { previewTranslator, structuredLabels, toPreviewRead } from './preview.js';
36
+ import { basename, highlightMatch, isWithinWorkspace, matchesQuery, parentPath, relativeToCwd, sortByLatest } from './path-utils.js';
37
+ import css from './FilePreviewTab.module.css';
38
+ /** One detail view: header (back + breadcrumb + actions) over the preview pane. */
39
+ function DetailView(props) {
40
+ const { sessionId, path, entry, cwd, apps, readFile, copyPath, revealFolder, openInIde, onBack, t } = props;
41
+ const [read, setRead] = useState(null);
42
+ const [error, setError] = useState(null);
43
+ // The content⇄change-history view is the caller's state (the kernel only
44
+ // renders the switch); a new selection starts on the content.
45
+ const [view, setView] = useState('content');
46
+ const fileManager = apps === null ? undefined : pickFileManager(apps);
47
+ const ides = apps === null ? [] : listIdes(apps);
48
+ const [ideChoice, setIdeChoice] = useState(ides[0]?.id);
49
+ const currentIde = ides.find(entry => entry.id === ideChoice) ?? ides[0];
50
+ useEffect(() => { setView('content'); }, [path]);
51
+ // The header shows the host-resolved absolute spelling (the same one the
52
+ // copy/open gestures act on) as a segmented breadcrumb: directory segments
53
+ // dimmed with ' / ' between them (the official files header's reading), the
54
+ // file name solid. Long segments ellipsize individually; the name never
55
+ // truncates.
56
+ const displayPath = resolveWorkspacePath(cwd, path);
57
+ const showTabs = entry !== undefined && entry.diffs.length > 0;
58
+ // Fetch the current content for the content tab; a stale answer (selection
59
+ // moved) is dropped by the effect cleanup.
60
+ useEffect(() => {
61
+ let cancelled = false;
62
+ setRead(null);
63
+ setError(null);
64
+ void readFile(sessionId, path).then((result) => {
65
+ if (cancelled)
66
+ return;
67
+ if (result.ok)
68
+ setRead(result.value);
69
+ else
70
+ setError(result.error.message);
71
+ });
72
+ return () => { cancelled = true; };
73
+ }, [sessionId, path, readFile]);
74
+ // The pane's reload gesture re-reads the same file. Unlike a fresh selection
75
+ // the old read is NOT cleared first — the panel keeps showing it in flight,
76
+ // and a failure keeps it in state, reported through the same error slot. An
77
+ // answer landing after the selection moved is dropped.
78
+ const pathRef = useRef(path);
79
+ pathRef.current = path;
80
+ const reload = () => {
81
+ const target = path;
82
+ return readFile(sessionId, target).then((result) => {
83
+ if (pathRef.current !== target)
84
+ return;
85
+ if (result.ok) {
86
+ setRead(result.value);
87
+ setError(null);
88
+ }
89
+ else
90
+ setError(result.error.message);
91
+ });
92
+ };
93
+ return (_jsx("div", { className: css.detail, children: _jsx(ContentPane, { path: path, sessionId: sessionId, read: read === null ? null : toPreviewRead(read), loading: read === null && error === null, error: error, displayPath: displayPath, onBack: onBack, onCopyPath: () => copyPath(path), onReload: reload, chrome: {
94
+ ...(fileManager === undefined ? {} : { openFolder: () => { revealFolder(path); } }),
95
+ ...(currentIde === undefined
96
+ ? {}
97
+ : {
98
+ openIDE: () => { openInIde(path, currentIde.id); },
99
+ ideChoices: ides.map(ide => ({ id: ide.id, label: ide.label })),
100
+ onIdeChoice: (id) => { setIdeChoice(id); openInIde(path, id); },
101
+ }),
102
+ }, ...(showTabs ? { diffView: _jsx(DiffHistory, { entry: entry, t: t }), view, onViewChange: setView } : {}), labels: structuredLabels(t), t: previewTranslator(t) }, path) }));
103
+ }
104
+ /**
105
+ * The file-preview tab body.
106
+ * @param props - composed props (runtime + store + injected + locale shares).
107
+ */
108
+ export function FilePreviewTab(props) {
109
+ const { useTabInfo, sessionId, useSessions, useStore, actions, listFiles, readFile, copyPath, revealFolder, openInIde, loadOpenInApps, t, } = props;
110
+ const { useOpenInApps } = props;
111
+ const { tab } = useTabInfo();
112
+ const { signal, navigation } = tab;
113
+ const cwd = useSessions(s => s.byId[sessionId]?.cwd);
114
+ const apps = useOpenInApps(value => value);
115
+ const list = useStore(s => s.list);
116
+ const listLoading = useStore(s => s.listLoading);
117
+ const listError = useStore(s => s.listError);
118
+ const listRequestRev = useStore(s => s.listRequestRev);
119
+ const selectedPath = useStore(s => s.selected[tab.id] ?? null);
120
+ // Component-private view state: the name-filter search term.
121
+ const [search, setSearch] = useState('');
122
+ // Fetch the list on mount and on refresh; the whole value replaces the
123
+ // previous list. A stale answer (tab closed mid-flight) is dropped on the
124
+ // occurrence's abort signal.
125
+ useEffect(() => {
126
+ let cancelled = false;
127
+ actions.setListLoading(true);
128
+ actions.setListError(null);
129
+ void listFiles(sessionId).then((result) => {
130
+ if (cancelled || signal.aborted)
131
+ return;
132
+ actions.setListLoading(false);
133
+ if (result.ok)
134
+ actions.setList(result.value);
135
+ else
136
+ actions.setListError(result.error.message);
137
+ });
138
+ return () => { cancelled = true; };
139
+ }, [sessionId, listRequestRev, actions, listFiles, signal]);
140
+ // The gestures' availability rides the once-per-page probe; start it with
141
+ // the first mounted body.
142
+ useEffect(() => { loadOpenInApps(); }, [loadOpenInApps]);
143
+ // A navigation selects its path: `params.path` (the turn card's
144
+ // outside-workspace gesture), or the claimed address's path (every
145
+ // openResource route — the official card, the file tree, mentions). The
146
+ // revision bumps on every navigation, so a repeat open re-applies it.
147
+ const revision = navigation.revision;
148
+ // The params union also carries the official file resource params ({ line }),
149
+ // so narrow on the key the way the official TextPreview does.
150
+ const navPath = navigation.params !== undefined && 'path' in navigation.params ? navigation.params.path : undefined;
151
+ const navAddress = navigation.address;
152
+ // Applied once per navigation revision: the list arriving later must NOT
153
+ // re-apply (a refresh would yank the user out of the list they backed into).
154
+ const appliedRevision = useRef(0);
155
+ useEffect(() => {
156
+ if (revision === 0 || appliedRevision.current === revision)
157
+ return;
158
+ const path = navPath ?? (() => {
159
+ const parsed = navAddress === undefined ? undefined : parseFileAddress(navAddress);
160
+ return parsed?.scope === 'session' ? parsed.path : undefined;
161
+ })();
162
+ if (path === undefined)
163
+ return;
164
+ appliedRevision.current = revision;
165
+ // Select the fold's own spelling when the list knows the file (the store
166
+ // keys entries by their recorded display path), else the address's.
167
+ const known = list?.find(entry => resolveWorkspacePath(cwd, entry.path) === resolveWorkspacePath(cwd, path));
168
+ actions.select(tab.id, known?.path ?? path);
169
+ }, [revision, navPath, navAddress, list, cwd, tab.id, actions]);
170
+ // Latest activity first: the fold keeps first-seen order. The list shows
171
+ // the session's products only — written/edited files, matching the chat
172
+ // area's 产物 vocabulary; reads never appear. A non-empty search narrows
173
+ // to paths containing the term.
174
+ const ordered = useMemo(() => {
175
+ if (list === null)
176
+ return null;
177
+ const products = sortByLatest(list).filter((entry) => entry.op !== 'read');
178
+ const query = search.trim();
179
+ return query.length === 0 ? products : products.filter(entry => matchesQuery(entry.path, query));
180
+ }, [list, search]);
181
+ const searching = search.trim().length > 0;
182
+ if (selectedPath !== null) {
183
+ return (_jsx("div", { className: css.root, children: _jsx(DetailView, { sessionId: sessionId, path: selectedPath, entry: list?.find(entry => entry.path === selectedPath), cwd: cwd, apps: apps, readFile: readFile, copyPath: copyPath, revealFolder: revealFolder, openInIde: openInIde, onBack: () => { actions.deselect(tab.id); }, t: t }) }));
184
+ }
185
+ return (_jsxs("div", { className: css.root, children: [_jsxs("div", { className: css.filterRow, children: [_jsx("input", { type: "search", className: css.search, placeholder: t('view.search.placeholder'), value: search, onChange: (event) => { setSearch(event.target.value); }, "aria-label": t('view.search.placeholder') }), _jsx("button", { type: "button", className: css.tool, "aria-label": t('list.refresh'), title: t('list.refresh'), onClick: () => { actions.refreshList(); }, children: _jsx(IconRefreshOutlineMedium, {}) })] }), _jsxs("nav", { className: css.list, "aria-label": t('open'), children: [listLoading && list === null && _jsx("div", { className: css.empty, children: t('list.loading') }), !listLoading && listError !== null && list === null && (_jsx("div", { className: css.empty, children: t('list.error') })), !listLoading && listError === null && list !== null && ordered !== null && ordered.length === 0 && (_jsx("div", { className: css.empty, children: searching ? t('view.search.noMatch') : t('list.empty') })), ordered?.map((entry) => {
186
+ const name = basename(entry.path);
187
+ const parts = searching ? highlightMatch(name, search.trim()) : null;
188
+ const within = isWithinWorkspace(cwd, entry.path);
189
+ return (_jsxs("button", { type: "button", className: css.row, title: within ? entry.path : `${entry.path} — ${t('list.outsideWorkspace')}`, onClick: () => { actions.select(tab.id, entry.path); }, children: [_jsxs("span", { className: css.rowName, children: [_jsx(FileTypeIcon, { path: entry.path, size: 14 }), parts === null
190
+ ? name
191
+ : (_jsxs(_Fragment, { children: [parts[0], _jsx("mark", { className: css.match, children: parts[1] }), parts[2]] })), !within && _jsx(IconGlobeOutlineMedium, { className: css.rowOutside, size: 12 })] }), _jsx("span", { className: css.rowDir, children: relativeToCwd(parentPath(entry.path), cwd) }), _jsx("span", { className: css.rowStep, children: t('history.step', { turn: entry.turn, step: entry.step }) })] }, entry.path));
192
+ })] })] }));
193
+ }
@@ -0,0 +1,30 @@
1
+ /** TurnFileRow: the turn's product table — every file the turn created or
2
+ * edited, as a compact table in the `conversation.chat.turnTail` slot.
3
+ *
4
+ * The paths come from the host `filePreview.turnFiles` RPC — the single
5
+ * source of truth (write/edit calls, Code Mode dispatches, render-intent
6
+ * paths, and bash captures all land here), fetched once per session through
7
+ * the turn-files cache. 0.1.6-alpha.2 re-kinded the slot chain → list; the
8
+ * 2026-09-24 convergence decision keeps ONLY this row in the slot: the
9
+ * official deliverables entry (present card + memory-resident changes card)
10
+ * is shadowed by an empty lower-priority registration under its cell id (see
11
+ * index.ts), superseding the 2026-09-18 coexistence. On a 0.1.5 host the
12
+ * registration keeps the old chain preemption instead. Either way the row
13
+ * itself decides visibility from its data.
14
+ *
15
+ * Density: up to three products render as plain rows; beyond that the card
16
+ * collapses to a "N 个产物" summary row that expands in place. Every file
17
+ * click goes through the owner's `openFile` — the official openResource
18
+ * route, where this package's tab type claims the renderable addresses on
19
+ * both host lines (extension band over the official document tab's
20
+ * fallback) — so one file is one tab for every origin.
21
+ * Until the fetch settles — or when the turn has no files, or the fetch fails
22
+ * — the card renders nothing. */
23
+ import type { FilePreviewTurnRowProps } from './contract.ts';
24
+ /**
25
+ * Render one turn's products as a compact table, fetched from the host.
26
+ * @param props - session standard kit (sessionId), the turn-tail owner (turn,
27
+ * openFile), the injected turn-files loader, and the locale seat.
28
+ */
29
+ export declare function TurnFileRow(props: FilePreviewTurnRowProps): import("react").JSX.Element | null;
30
+ //# sourceMappingURL=TurnFileRow.d.ts.map
@@ -0,0 +1,72 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /** TurnFileRow: the turn's product table — every file the turn created or
3
+ * edited, as a compact table in the `conversation.chat.turnTail` slot.
4
+ *
5
+ * The paths come from the host `filePreview.turnFiles` RPC — the single
6
+ * source of truth (write/edit calls, Code Mode dispatches, render-intent
7
+ * paths, and bash captures all land here), fetched once per session through
8
+ * the turn-files cache. 0.1.6-alpha.2 re-kinded the slot chain → list; the
9
+ * 2026-09-24 convergence decision keeps ONLY this row in the slot: the
10
+ * official deliverables entry (present card + memory-resident changes card)
11
+ * is shadowed by an empty lower-priority registration under its cell id (see
12
+ * index.ts), superseding the 2026-09-18 coexistence. On a 0.1.5 host the
13
+ * registration keeps the old chain preemption instead. Either way the row
14
+ * itself decides visibility from its data.
15
+ *
16
+ * Density: up to three products render as plain rows; beyond that the card
17
+ * collapses to a "N 个产物" summary row that expands in place. Every file
18
+ * click goes through the owner's `openFile` — the official openResource
19
+ * route, where this package's tab type claims the renderable addresses on
20
+ * both host lines (extension band over the official document tab's
21
+ * fallback) — so one file is one tab for every origin.
22
+ * Until the fetch settles — or when the turn has no files, or the fetch fails
23
+ * — the card renders nothing. */
24
+ import { useEffect, useState } from 'react';
25
+ import { FileTypeIcon } from '@deepseek-ai/dsh-client-ui-primitives';
26
+ import { IconChevronDownOutlineMedium, IconChevronUpOutlineMedium } from './icons.js';
27
+ import { basename } from './turn-files.js';
28
+ import { parentPath } from './path-utils.js';
29
+ import css from './TurnFileRow.module.css';
30
+ /** Above this many products the card collapses to the summary row. */
31
+ const COLLAPSE_OVER = 3;
32
+ /**
33
+ * Render one turn's products as a compact table, fetched from the host.
34
+ * @param props - session standard kit (sessionId), the turn-tail owner (turn,
35
+ * openFile), the injected turn-files loader, and the locale seat.
36
+ */
37
+ export function TurnFileRow(props) {
38
+ const { sessionId, openFile, turnFiles, t } = props;
39
+ const turn = props.turn.turn;
40
+ const [files, setFiles] = useState(null);
41
+ // Expanded means: the table is showing. Default: showing at ≤3 products,
42
+ // collapsed to the summary row above that.
43
+ const [expanded, setExpanded] = useState(false);
44
+ useEffect(() => {
45
+ let cancelled = false;
46
+ setFiles(null);
47
+ setExpanded(false);
48
+ void turnFiles(sessionId, turn).then((loaded) => {
49
+ if (cancelled)
50
+ return;
51
+ setFiles(loaded);
52
+ });
53
+ return () => { cancelled = true; };
54
+ }, [sessionId, turn, turnFiles]);
55
+ // Data not arrived, or nothing to show: the card stays invisible (on the
56
+ // list-kind slot every entry renders; visibility is decided here).
57
+ if (files === null || files.length === 0)
58
+ return null;
59
+ const collapsible = files.length > COLLAPSE_OVER;
60
+ const showing = !collapsible || expanded;
61
+ return (_jsxs("div", { className: css.card, "data-turn-file-row": true, children: [collapsible && (_jsxs("button", { type: "button", className: css.summary, "aria-expanded": expanded, onClick: () => { setExpanded(value => !value); }, children: [_jsx("span", { className: css.summaryText, children: t('turn.count', { count: files.length }) }), _jsx("span", { className: css.chevron, "aria-hidden": true, children: expanded ? _jsx(IconChevronUpOutlineMedium, {}) : _jsx(IconChevronDownOutlineMedium, {}) })] })), showing && (_jsx("div", { className: css.list, role: "table", "aria-label": t('turn.count', { count: files.length }), children: files.map((file) => {
62
+ const dir = parentPath(file.path);
63
+ return (_jsxs("button", { type: "button", role: "row", className: css.file, title: file.path,
64
+ // Every path goes through the owner's openFile — the official
65
+ // openResource route, where our tab type claims the renderable
66
+ // addresses on both host lines (fileAddressFor keeps
67
+ // outside-workspace absolutes inside the session address, and
68
+ // our detail view renders them via the Remote read), so one
69
+ // file is one tab for every origin.
70
+ onClick: () => { void openFile(file.path); }, children: [_jsx(FileTypeIcon, { path: file.path, size: 14, className: css.fileIcon }), _jsx("span", { className: css.name, children: basename(file.path) }), dir !== '' && _jsx("span", { className: css.dir, children: dir }), _jsxs("span", { className: css.stats, children: [file.added !== undefined && file.added > 0 && (_jsxs("span", { className: css.added, children: ["+", file.added] })), file.removed !== undefined && file.removed > 0 && (_jsxs("span", { className: css.removed, children: ["\u2212", file.removed] }))] })] }, file.path));
71
+ }) }))] }));
72
+ }
@@ -0,0 +1,39 @@
1
+ /** Composed props contracts for the file-preview right-Sidebar tab and the turn card. */
2
+ import type { FilePreviewList, FilePreviewRead, FilePreviewTurnFile } from '../types.ts';
3
+ import type { SessionId } from '@deepseek-ai/dsh-api-remotes/client';
4
+ import type { ObservableSnapshot } from '@deepseek-ai/dsh-client-store';
5
+ import type { InjectFace, PropsLocale, PropsRuntime, PropsStore } from '@deepseek-ai/dsh-client-ui-slots';
6
+ import type { RemoteResult } from '@deepseek-ai/dsh-typert-protocol';
7
+ import type { TypertRemoteNamespaceMap } from '@deepseek-ai/dsh-typert-protocol';
8
+ import type { createFilePreviewStore } from './file-preview-store.ts';
9
+ /** The filePreview Remote namespace (list + read + turnFiles), as mounted by this plugin. */
10
+ export type FilePreviewRemote = TypertRemoteNamespaceMap['filePreview'];
11
+ /** Business face injected into the right-Sidebar tab body. */
12
+ export interface FilePreviewTabInjected {
13
+ /** Fetch one session's touched-file list (one RPC; supplies the diff history). */
14
+ listFiles: (sessionId: SessionId) => Promise<RemoteResult<FilePreviewList>>;
15
+ /** Fetch one file's current content (one RPC, host-capped) — the detail view's content tab. */
16
+ readFile: (sessionId: SessionId, path: string) => Promise<RemoteResult<FilePreviewRead>>;
17
+ /** Copy one path's host-resolved absolute spelling to the clipboard; resolves true only when the host accepted the write. */
18
+ copyPath: (path: string) => Promise<boolean>;
19
+ /** Reveal one path in the host file manager (file selected; falls back to the official open-in-app route on its parent folder). */
20
+ revealFolder: (path: string) => void;
21
+ /** Open one path in an IDE application (file-exact, through the host Remote's openExternal); defaults to the probed preference. */
22
+ openInIde: (path: string, app?: string) => void;
23
+ /** Start the once-per-page open-in-app probe (the gestures hide until it answers). */
24
+ loadOpenInApps: () => void;
25
+ hooks: {
26
+ /** Probed open-in-app catalog ids (null until answered), bound by the slot renderer. */
27
+ openInApps: ObservableSnapshot<readonly string[] | null>;
28
+ };
29
+ }
30
+ /** Full props of the tab body entry (runtime + store + injected + locale shares). */
31
+ export type FilePreviewTabProps = PropsRuntime<'sidebar.right.pane.tab'> & PropsStore<ReturnType<typeof createFilePreviewStore>> & InjectFace<FilePreviewTabInjected> & PropsLocale<'filePreview'>;
32
+ /** Business face injected into the conversation.chat.turnTail mutation card. */
33
+ export interface FilePreviewTurnRowInjected {
34
+ /** Resolve one session's files for one turn through the session-level cache. */
35
+ turnFiles: (sessionId: SessionId, turn: number) => Promise<readonly FilePreviewTurnFile[]>;
36
+ }
37
+ /** Full props of the turn mutation card (owner + injected + locale shares). */
38
+ export type FilePreviewTurnRowProps = PropsRuntime<'conversation.chat.turnTail'> & InjectFace<FilePreviewTurnRowInjected> & PropsLocale<'filePreview'>;
39
+ //# sourceMappingURL=contract.d.ts.map
@@ -0,0 +1,2 @@
1
+ /** Composed props contracts for the file-preview right-Sidebar tab and the turn card. */
2
+ export {};
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Stage one of this package's right-Sidebar registration: what the
3
+ * `file-preview` tab type IS — on both host lines (the plan-B embedding into
4
+ * the official document tab landed and was vetoed the same day, 2026-09-24;
5
+ * this self-drawn page is the content face again).
6
+ *
7
+ * Both a page and a claimant: the guide page offers it as an entry box, and
8
+ * it claims `dsh-resource://file/**` addresses whose suffix the detail view's
9
+ * preview stack renders (text/code/markup/data/images — pdf, archives, and
10
+ * binaries stay with the official document tab). At the default `extension`
11
+ * band it outranks the official `text` type (fallback band) — the rc.1 tab
12
+ * registry kept the band mechanism — so every openResource route (the
13
+ * deliverables card, the file tree, wrapped mentions' fallback, the turn
14
+ * card) lands files in our detail view. The claim is deterministic by
15
+ * construction: an earlier cut also required fold membership, but the cache
16
+ * warms only after our surfaces fetch, and turns the official card claims
17
+ * never mount our card (its loader) — exactly those files kept falling back
18
+ * to the official tab (the "sometimes ours, sometimes official" race). The
19
+ * turn card opens the page by kind with `params: { path }` for
20
+ * outside-workspace paths (no addressable resource).
21
+ */
22
+ import type { SidebarRightTabDefinition } from '@deepseek-ai/dsh-client-ui-sidebar-right/client';
23
+ import type { TranslateNS } from '@deepseek-ai/dsh-client-locale/client';
24
+ /** The tab kind this package owns. */
25
+ export declare const FILE_PREVIEW_KIND = "file-preview";
26
+ /** This implementation's identity in the tab system, and the key its body registers under. */
27
+ export declare const FILE_PREVIEW_ID = "@khorsheed/dsh-file-preview";
28
+ /**
29
+ * Suffixes the detail view's preview stack renders: the change-history text
30
+ * set plus the image formats the pane's image arm serves. Everything else
31
+ * (pdf, archives, binaries) stays with the official document tab — the type's
32
+ * `canOpen` filters on this set before claiming an address.
33
+ */
34
+ export declare const RENDERABLE_EXTENSIONS: readonly string[];
35
+ /**
36
+ * Whether the detail view can render this path's content (suffix match,
37
+ * case-insensitive, compound suffixes intentionally not special-cased).
38
+ * @param path - any file path spelling.
39
+ */
40
+ export declare function renderablePath(path: string): boolean;
41
+ declare module '@deepseek-ai/dsh-client-ui-sidebar-right/client' {
42
+ interface SidebarRightTabParamsMap {
43
+ /** The file-preview page takes the path to select (the turn card's outside-workspace gesture). */
44
+ 'file-preview': {
45
+ readonly path?: string;
46
+ };
47
+ }
48
+ }
49
+ /**
50
+ * The file-preview type's registry definition.
51
+ * @param t - namespace-bound translate, read fresh on every label call.
52
+ * @returns the definition to register.
53
+ */
54
+ export declare function filePreviewDefinition(t: TranslateNS<'filePreview'>): SidebarRightTabDefinition;
55
+ //# sourceMappingURL=definition.d.ts.map