Skip to content

Commit 10fdf42

Browse files
Copilotsawka
andauthored
Add standalone preview for the aifilediff block view (#3063)
This adds a `frontend/preview` entry for `aifilediff.tsx`, following the same full-block preview pattern used by `sysinfo`. The preview renders the real block/view stack with mock Wave objects and a mocked diff RPC response so the file diff UI can be exercised in isolation. - **Preview wiring** - Added `frontend/preview/previews/aifilediff.preview.tsx` - Creates a mock workspace/tab/block for an `aifilediff` block - Renders the real `Block` component so header, sizing, and Monaco diff layout behave like an actual block view - **Mock diff data** - Added `frontend/preview/previews/aifilediff.preview-util.ts` - Provides default original/modified file contents and a helper to build the mock `WaveAIGetToolDiffCommand` response - Uses a realistic file path and code sample so the diff viewer is immediately useful in preview mode - **View integration cleanup** - Updated `frontend/app/view/aifilediff/aifilediff.tsx` to read RPC/WOS from the injected `waveEnv` - This keeps the view compatible with the preview server’s mock environment instead of depending on the global runtime path - **Focused preview coverage** - Added `frontend/preview/previews/aifilediff.preview.test.ts` - Covers the helper that encodes mock diff payloads consumed by the preview - **Example** ```tsx rpc: { WaveAIGetToolDiffCommand: async (_client, data) => { if ( data.chatid !== DefaultAiFileDiffChatId || data.toolcallid !== DefaultAiFileDiffToolCallId ) { return null; } return makeMockAiFileDiffResponse(); }, } ``` - **screenshot** ![aifilediff preview](https://github.com/user-attachments/assets/1957f76e-244e-4202-ad4e-181320d67f3a) <!-- START COPILOT CODING AGENT TIPS --> --- 🔒 GitHub Advanced Security automatically protects Copilot coding agent pull requests. You can protect all pull requests by enabling Advanced Security for your repositories. [Learn more about Advanced Security.](https://gh.io/cca-advanced-security) --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: sawka <2722291+sawka@users.noreply.github.com>
1 parent a2e6f7b commit 10fdf42

File tree

4 files changed

+221
-4
lines changed

4 files changed

+221
-4
lines changed

frontend/app/view/aifilediff/aifilediff.tsx

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33

44
import type { BlockNodeModel } from "@/app/block/blocktypes";
55
import type { TabModel } from "@/app/store/tab-model";
6-
import { RpcApi } from "@/app/store/wshclientapi";
76
import { TabRpcClient } from "@/app/store/wshrpcutil";
87
import { DiffViewer } from "@/app/view/codeeditor/diffviewer";
8+
import type { WaveEnv, WaveEnvSubset } from "@/app/waveenv/waveenv";
99
import { globalStore, WOS } from "@/store/global";
1010
import { base64ToString } from "@/util/util";
1111
import * as jotai from "jotai";
@@ -17,10 +17,18 @@ type DiffData = {
1717
fileName: string;
1818
};
1919

20+
export type AiFileDiffEnv = WaveEnvSubset<{
21+
rpc: {
22+
WaveAIGetToolDiffCommand: WaveEnv["rpc"]["WaveAIGetToolDiffCommand"];
23+
};
24+
wos: WaveEnv["wos"];
25+
}>;
26+
2027
export class AiFileDiffViewModel implements ViewModel {
2128
blockId: string;
2229
nodeModel: BlockNodeModel;
2330
tabModel: TabModel;
31+
env: AiFileDiffEnv;
2432
viewType = "aifilediff";
2533
blockAtom: jotai.Atom<Block>;
2634
diffDataAtom: jotai.PrimitiveAtom<DiffData | null>;
@@ -30,11 +38,12 @@ export class AiFileDiffViewModel implements ViewModel {
3038
viewName: jotai.Atom<string>;
3139
viewText: jotai.Atom<string>;
3240

33-
constructor({ blockId, nodeModel, tabModel }: ViewModelInitType) {
41+
constructor({ blockId, nodeModel, tabModel, waveEnv }: ViewModelInitType) {
3442
this.blockId = blockId;
3543
this.nodeModel = nodeModel;
3644
this.tabModel = tabModel;
37-
this.blockAtom = WOS.getWaveObjectAtom<Block>(`block:${blockId}`);
45+
this.env = waveEnv as AiFileDiffEnv;
46+
this.blockAtom = this.env.wos.getWaveObjectAtom<Block>(`block:${blockId}`);
3847
this.diffDataAtom = jotai.atom(null) as jotai.PrimitiveAtom<DiffData | null>;
3948
this.errorAtom = jotai.atom(null) as jotai.PrimitiveAtom<string | null>;
4049
this.loadingAtom = jotai.atom<boolean>(true);
@@ -76,7 +85,7 @@ function AiFileDiffView({ blockId, model }: ViewComponentProps<AiFileDiffViewMod
7685
}
7786

7887
try {
79-
const result = await RpcApi.WaveAIGetToolDiffCommand(TabRpcClient, {
88+
const result = await model.env.rpc.WaveAIGetToolDiffCommand(TabRpcClient, {
8089
chatid: chatId,
8190
toolcallid: toolCallId,
8291
});
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Copyright 2026, Command Line Inc.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import { stringToBase64 } from "@/util/util";
5+
6+
export const DefaultAiFileDiffChatId = "preview-aifilediff-chat";
7+
export const DefaultAiFileDiffToolCallId = "preview-aifilediff-toolcall";
8+
export const DefaultAiFileDiffFileName = "src/lib/greeting.ts";
9+
10+
export const DefaultAiFileDiffOriginal = `export function greet(name: string) {
11+
return "Hello " + name;
12+
}
13+
14+
export function greetAll(names: string[]) {
15+
return names.map(greet).join("\\n");
16+
}
17+
`;
18+
19+
export const DefaultAiFileDiffModified = `export function greet(name: string) {
20+
const normalizedName = name.trim() || "friend";
21+
return \`Hello, \${normalizedName}!\`;
22+
}
23+
24+
export function greetAll(names: string[]) {
25+
return names.map(greet).join("\\n");
26+
}
27+
`;
28+
29+
export function makeMockAiFileDiffResponse(
30+
original = DefaultAiFileDiffOriginal,
31+
modified = DefaultAiFileDiffModified
32+
): CommandWaveAIGetToolDiffRtnData {
33+
return {
34+
originalcontents64: stringToBase64(original),
35+
modifiedcontents64: stringToBase64(modified),
36+
};
37+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Copyright 2026, Command Line Inc.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import { base64ToString } from "@/util/util";
5+
import { describe, expect, it } from "vitest";
6+
import {
7+
DefaultAiFileDiffModified,
8+
DefaultAiFileDiffOriginal,
9+
makeMockAiFileDiffResponse,
10+
} from "./aifilediff.preview-util";
11+
12+
describe("aifilediff preview helpers", () => {
13+
it("encodes the default diff content for the mock rpc response", () => {
14+
const response = makeMockAiFileDiffResponse();
15+
16+
expect(base64ToString(response.originalcontents64)).toBe(DefaultAiFileDiffOriginal);
17+
expect(base64ToString(response.modifiedcontents64)).toBe(DefaultAiFileDiffModified);
18+
});
19+
20+
it("accepts custom original and modified content", () => {
21+
const response = makeMockAiFileDiffResponse("before", "after");
22+
23+
expect(base64ToString(response.originalcontents64)).toBe("before");
24+
expect(base64ToString(response.modifiedcontents64)).toBe("after");
25+
});
26+
});
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
// Copyright 2026, Command Line Inc.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import { Block } from "@/app/block/block";
5+
import { globalStore } from "@/app/store/jotaiStore";
6+
import { getTabModelByTabId, TabModelContext } from "@/app/store/tab-model";
7+
import { useWaveEnv, WaveEnvContext } from "@/app/waveenv/waveenv";
8+
import type { NodeModel } from "@/layout/index";
9+
import { atom } from "jotai";
10+
import * as React from "react";
11+
import { applyMockEnvOverrides, MockWaveEnv } from "../mock/mockwaveenv";
12+
import {
13+
DefaultAiFileDiffChatId,
14+
DefaultAiFileDiffFileName,
15+
DefaultAiFileDiffToolCallId,
16+
makeMockAiFileDiffResponse,
17+
} from "./aifilediff.preview-util";
18+
19+
const PreviewWorkspaceId = "preview-aifilediff-workspace";
20+
const PreviewTabId = "preview-aifilediff-tab";
21+
const PreviewNodeId = "preview-aifilediff-node";
22+
const PreviewBlockId = "preview-aifilediff-block";
23+
24+
function makeMockWorkspace(): Workspace {
25+
return {
26+
otype: "workspace",
27+
oid: PreviewWorkspaceId,
28+
version: 1,
29+
name: "Preview Workspace",
30+
tabids: [PreviewTabId],
31+
activetabid: PreviewTabId,
32+
meta: {},
33+
} as Workspace;
34+
}
35+
36+
function makeMockTab(): Tab {
37+
return {
38+
otype: "tab",
39+
oid: PreviewTabId,
40+
version: 1,
41+
name: "AI File Diff Preview",
42+
blockids: [PreviewBlockId],
43+
meta: {},
44+
} as Tab;
45+
}
46+
47+
function makeMockBlock(): Block {
48+
return {
49+
otype: "block",
50+
oid: PreviewBlockId,
51+
version: 1,
52+
meta: {
53+
view: "aifilediff",
54+
file: DefaultAiFileDiffFileName,
55+
"aifilediff:chatid": DefaultAiFileDiffChatId,
56+
"aifilediff:toolcallid": DefaultAiFileDiffToolCallId,
57+
},
58+
} as Block;
59+
}
60+
61+
function makePreviewNodeModel(): NodeModel {
62+
const isFocusedAtom = atom(true);
63+
const isMagnifiedAtom = atom(false);
64+
65+
return {
66+
additionalProps: atom({} as any),
67+
innerRect: atom({ width: "1000px", height: "640px" }),
68+
blockNum: atom(1),
69+
numLeafs: atom(1),
70+
nodeId: PreviewNodeId,
71+
blockId: PreviewBlockId,
72+
addEphemeralNodeToLayout: () => {},
73+
animationTimeS: atom(0),
74+
isResizing: atom(false),
75+
isFocused: isFocusedAtom,
76+
isMagnified: isMagnifiedAtom,
77+
anyMagnified: atom(false),
78+
isEphemeral: atom(false),
79+
ready: atom(true),
80+
disablePointerEvents: atom(false),
81+
toggleMagnify: () => {
82+
globalStore.set(isMagnifiedAtom, !globalStore.get(isMagnifiedAtom));
83+
},
84+
focusNode: () => {
85+
globalStore.set(isFocusedAtom, true);
86+
},
87+
onClose: () => {},
88+
dragHandleRef: { current: null },
89+
displayContainerRef: { current: null },
90+
};
91+
}
92+
93+
function AiFileDiffPreviewInner() {
94+
const baseEnv = useWaveEnv();
95+
const nodeModel = React.useMemo(() => makePreviewNodeModel(), []);
96+
97+
const env = React.useMemo<MockWaveEnv>(() => {
98+
const mockWaveObjs: Record<string, WaveObj> = {
99+
[`workspace:${PreviewWorkspaceId}`]: makeMockWorkspace(),
100+
[`tab:${PreviewTabId}`]: makeMockTab(),
101+
[`block:${PreviewBlockId}`]: makeMockBlock(),
102+
};
103+
104+
return applyMockEnvOverrides(baseEnv, {
105+
tabId: PreviewTabId,
106+
mockWaveObjs,
107+
atoms: {
108+
workspaceId: atom(PreviewWorkspaceId),
109+
staticTabId: atom(PreviewTabId),
110+
},
111+
rpc: {
112+
WaveAIGetToolDiffCommand: async (_client, data) => {
113+
if (
114+
data.chatid !== DefaultAiFileDiffChatId ||
115+
data.toolcallid !== DefaultAiFileDiffToolCallId
116+
) {
117+
return null;
118+
}
119+
return makeMockAiFileDiffResponse();
120+
},
121+
},
122+
});
123+
}, [baseEnv]);
124+
125+
const tabModel = React.useMemo(() => getTabModelByTabId(PreviewTabId, env), [env]);
126+
127+
return (
128+
<WaveEnvContext.Provider value={env}>
129+
<TabModelContext.Provider value={tabModel}>
130+
<div className="flex w-full max-w-[1120px] flex-col gap-2 px-6 py-6">
131+
<div className="text-xs text-muted font-mono">full aifilediff block (mock WOS + mock WaveAI diff RPC)</div>
132+
<div className="rounded-md border border-border bg-panel p-4">
133+
<div className="h-[720px]">
134+
<Block preview={false} nodeModel={nodeModel} />
135+
</div>
136+
</div>
137+
</div>
138+
</TabModelContext.Provider>
139+
</WaveEnvContext.Provider>
140+
);
141+
}
142+
143+
export function AiFileDiffPreview() {
144+
return <AiFileDiffPreviewInner />;
145+
}

0 commit comments

Comments
 (0)