@khorsheed/dsh-file-preview 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/README.en.md +38 -20
- package/README.i18n.yaml +2 -2
- package/README.md +39 -21
- package/cordis.patch.yml +10 -2
- package/lib/client.js +8427 -0
- package/lib/typert.host.js +11 -3
- package/lib/types/client/DiffHistory.d.ts +20 -0
- package/lib/types/client/DiffHistory.js +56 -0
- package/lib/types/client/FilePreviewTab.d.ts +34 -0
- package/lib/types/client/FilePreviewTab.js +193 -0
- package/lib/types/client/TurnFileRow.d.ts +30 -0
- package/lib/types/client/TurnFileRow.js +72 -0
- package/lib/types/client/contract.d.ts +39 -0
- package/lib/types/client/contract.js +2 -0
- package/lib/types/client/definition.d.ts +55 -0
- package/lib/types/client/definition.js +93 -0
- package/lib/types/client/file-preview-store.d.ts +42 -0
- package/lib/types/client/file-preview-store.js +58 -0
- package/lib/types/client/icons.d.ts +21 -0
- package/lib/types/client/icons.js +5 -0
- package/lib/types/client/index.d.ts +80 -0
- package/lib/types/client/index.js +238 -0
- package/lib/types/client/locales.d.ts +16 -0
- package/lib/types/client/locales.js +177 -0
- package/lib/types/client/mentions-wrap.d.ts +60 -0
- package/lib/types/client/mentions-wrap.js +42 -0
- package/lib/types/client/open-in-app.d.ts +85 -0
- package/lib/types/client/open-in-app.js +146 -0
- package/lib/types/client/path-utils.d.ts +53 -0
- package/lib/types/client/path-utils.js +110 -0
- package/lib/types/client/preview.d.ts +27 -0
- package/lib/types/client/preview.js +72 -0
- package/lib/types/client/turn-files-cache.d.ts +24 -0
- package/lib/types/client/turn-files-cache.js +39 -0
- package/lib/types/client/turn-files.d.ts +26 -0
- package/lib/types/client/turn-files.js +14 -0
- package/locale/en.json +2 -2
- package/locale/zh.json +2 -2
- package/package.json +55 -4
package/lib/typert.host.js
CHANGED
|
@@ -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",
|
|
@@ -744,7 +748,7 @@ export const TYPERT = {
|
|
|
744
748
|
},
|
|
745
749
|
{
|
|
746
750
|
"name": "Session",
|
|
747
|
-
"declaration": "export class Session {\n get surface(): SessionSurface;\n readonly header: SessionHeader;\n readonly inheritedEventCount: SessionLogOffset;\n get id(): SessionId;\n readonly firstLiveSeq: SessionLogOffset;\n readonly firstLifecycleSeq: SessionLogOffset;\n eventAt(seq: SessionSeq): SessionEvent | undefined;\n snapshotEvents(fromSeq: SessionLogOffset = SessionLogOffset(0), toSeqExclusive: SessionLogOffset = this.seq): readonly SessionEvent[];\n ownEvents(): readonly SessionEvent[];\n isOwnSeq(seq: SessionSeq): boolean;\n get seq(): SessionLogOffset;\n append<T extends SessionEventType>(type: T, data: SessionEventMap[T], ...opts: T extends SurfaceEventType ? [opts: SurfaceIntent<T>] : []): SessionEvent<T>;\n requestHeader(): EpochHeader | undefined;\n requestContext(): RequestContext | undefined;\n deriveMessages(): Message[];\n deriveEventMessage(event: SessionEvent): Message | null;\n}"
|
|
751
|
+
"declaration": "export class Session {\n get surface(): SessionSurface;\n readonly header: SessionHeader;\n readonly inheritedEventCount: SessionLogOffset;\n get id(): SessionId;\n readonly firstLiveSeq: SessionLogOffset;\n readonly firstLifecycleSeq: SessionLogOffset;\n eventAt(seq: SessionSeq): SessionEvent | undefined;\n snapshotEvents(fromSeq: SessionLogOffset = SessionLogOffset(0), toSeqExclusive: SessionLogOffset = this.seq): readonly SessionEvent[];\n ownEvents(): readonly SessionEvent[];\n isOwnSeq(seq: SessionSeq): boolean;\n get seq(): SessionLogOffset;\n append<T extends SessionEventType>(type: T, data: SessionEventMap[T], ...opts: T extends SurfaceEventType ? [opts: SurfaceIntent<T>] : []): SessionEvent<T>;\n requestHeader(): EpochHeader | undefined;\n requestContext(): RequestContext | undefined;\n toolHistory(): ToolHistory;\n deriveMessages(): Message[];\n deriveEventMessage(event: SessionEvent): Message | null;\n}"
|
|
748
752
|
},
|
|
749
753
|
{
|
|
750
754
|
"name": "SessionEvent",
|
|
@@ -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",
|
|
@@ -866,6 +870,10 @@ export const TYPERT = {
|
|
|
866
870
|
"name": "ToolCallId",
|
|
867
871
|
"declaration": "export type ToolCallId = Branded<'ToolCallId'>;"
|
|
868
872
|
},
|
|
873
|
+
{
|
|
874
|
+
"name": "ToolHistory",
|
|
875
|
+
"declaration": "export interface ToolHistory {\n readonly tools: readonly ToolSchema[];\n readonly updates: readonly { readonly messageId: MessageId; readonly additions: readonly ToolSchema[]; }[];\n}"
|
|
876
|
+
},
|
|
869
877
|
{
|
|
870
878
|
"name": "ToolMessageSource",
|
|
871
879
|
"declaration": "export interface ToolMessageSource {\n kind: 'tool';\n callId: ToolCallId;\n}"
|
|
@@ -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,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
|