Skip to content

Commit b1d7f42

Browse files
Copilotsawka
andauthored
Begin PreviewEnv narrowing for the preview widget (#3065)
This starts the WaveEnv narrowing work for the preview widget, focused on wiring the preview view tree to the environment surface it already uses today. The change intentionally avoids adding new mock behavior and instead codifies the existing preview dependencies so the widget can move toward the same env-contract pattern used elsewhere. - **Preview env contract** - Adds `PreviewEnv` in `frontend/app/view/preview/previewenv.ts` - Narrows the preview widget to the specific env surface currently exercised by the preview stack: - file RPCs - suggestion RPCs - config/meta RPCs - `ObjectService` - `fullConfigAtom` - settings and connection atom helpers - preview-only electron quicklook hook - **Preview model wiring** - Updates `PreviewModel` to retain and use the injected `waveEnv` instead of reaching directly for global RPC/service helpers where the env already provides equivalents - Moves existing preview operations onto `env.rpc`, `env.services`, `env.wos`, and env-provided atom helpers without changing behavior - **Preview component adoption** - Switches the top-level preview view to `useWaveEnv<PreviewEnv>()` for suggestion fetch/dispose calls - Switches directory preview code to the narrowed env for: - config reads - directory file reads - copy/create/mkdir actions - quicklook dispatch - **Preview-server compatibility** - Adds a small type-level/runtime check that the existing mock wave env satisfies `PreviewEnv` - Keeps the change scoped to the functionality already present in the preview server Example of the new pattern: ```tsx const env = useWaveEnv<PreviewEnv>(); const defaultSort = useAtomValue(env.getSettingsKeyAtom("preview:defaultsort")) ?? "name"; await env.rpc.FileReadCommand(TabRpcClient, { info: { path: await model.formatRemoteUri(dirPath, globalStore.get) }, }); ``` - **Screenshot** - Existing preview-server UI used for manual verification: <screenshot> ![Preview widgets screenshot](https://github.com/user-attachments/assets/4c996be8-4e3b-4599-85bf-0f4689d680e0) </screenshot> <!-- START COPILOT CODING AGENT TIPS --> --- ✨ Let Copilot coding agent [set things up for you](https://github.com/wavetermdev/waveterm/issues/new?title=✨+Set+up+Copilot+instructions&body=Configure%20instructions%20for%20this%20repository%20as%20documented%20in%20%5BBest%20practices%20for%20Copilot%20coding%20agent%20in%20your%20repository%5D%28https://gh.io/copilot-coding-agent-tips%29%2E%0A%0A%3COnboard%20this%20repo%3E&assignees=copilot) — coding agent works faster and does higher quality work when set up for your repo. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: sawka <2722291+sawka@users.noreply.github.com> Co-authored-by: sawka <mike@commandline.dev>
1 parent 10fdf42 commit b1d7f42

File tree

5 files changed

+87
-43
lines changed

5 files changed

+87
-43
lines changed

frontend/app/view/preview/preview-directory-utils.tsx

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
// Copyright 2026, Command Line Inc.
22
// SPDX-License-Identifier: Apache-2.0
33

4-
import { getSettingsKeyAtom, globalStore } from "@/app/store/global";
5-
import { RpcApi } from "@/app/store/wshclientapi";
4+
import { globalStore } from "@/app/store/jotaiStore";
65
import { TabRpcClient } from "@/app/store/wshrpcutil";
76
import { fireAndForget, isBlank } from "@/util/util";
87
import dayjs from "dayjs";
@@ -95,7 +94,7 @@ export function handleRename(
9594
if (isDir) {
9695
srcuri += "/";
9796
}
98-
await RpcApi.FileMoveCommand(TabRpcClient, {
97+
await model.env.rpc.FileMoveCommand(TabRpcClient, {
9998
srcuri,
10099
desturi: await model.formatRemoteUri(newPath, globalStore.get),
101100
});
@@ -121,7 +120,7 @@ export function handleFileDelete(
121120
fireAndForget(async () => {
122121
const formattedPath = await model.formatRemoteUri(path, globalStore.get);
123122
try {
124-
await RpcApi.FileDeleteCommand(TabRpcClient, {
123+
await model.env.rpc.FileDeleteCommand(TabRpcClient, {
125124
path: formattedPath,
126125
recursive,
127126
});
@@ -154,7 +153,7 @@ export function handleFileDelete(
154153
}
155154

156155
export function makeDirectoryDefaultMenuItems(model: PreviewModel): ContextMenuItem[] {
157-
const defaultSort = globalStore.get(getSettingsKeyAtom("preview:defaultsort")) ?? "name";
156+
const defaultSort = globalStore.get(model.env.getSettingsKeyAtom("preview:defaultsort")) ?? "name";
158157
const showHiddenFiles = globalStore.get(model.showHiddenFiles) ?? true;
159158
return [
160159
{
@@ -165,15 +164,17 @@ export function makeDirectoryDefaultMenuItems(model: PreviewModel): ContextMenuI
165164
type: "checkbox",
166165
checked: defaultSort === "name",
167166
click: () =>
168-
fireAndForget(() => RpcApi.SetConfigCommand(TabRpcClient, { "preview:defaultsort": "name" })),
167+
fireAndForget(() =>
168+
model.env.rpc.SetConfigCommand(TabRpcClient, { "preview:defaultsort": "name" })
169+
),
169170
},
170171
{
171172
label: "Last Modified",
172173
type: "checkbox",
173174
checked: defaultSort === "modtime",
174175
click: () =>
175176
fireAndForget(() =>
176-
RpcApi.SetConfigCommand(TabRpcClient, { "preview:defaultsort": "modtime" })
177+
model.env.rpc.SetConfigCommand(TabRpcClient, { "preview:defaultsort": "modtime" })
177178
),
178179
},
179180
],
@@ -187,7 +188,9 @@ export function makeDirectoryDefaultMenuItems(model: PreviewModel): ContextMenuI
187188
checked: showHiddenFiles,
188189
click: () => {
189190
globalStore.set(model.showHiddenFiles, true);
190-
fireAndForget(() => RpcApi.SetConfigCommand(TabRpcClient, { "preview:showhiddenfiles": true }));
191+
fireAndForget(() =>
192+
model.env.rpc.SetConfigCommand(TabRpcClient, { "preview:showhiddenfiles": true })
193+
);
191194
},
192195
},
193196
{
@@ -197,7 +200,7 @@ export function makeDirectoryDefaultMenuItems(model: PreviewModel): ContextMenuI
197200
click: () => {
198201
globalStore.set(model.showHiddenFiles, false);
199202
fireAndForget(() =>
200-
RpcApi.SetConfigCommand(TabRpcClient, { "preview:showhiddenfiles": false })
203+
model.env.rpc.SetConfigCommand(TabRpcClient, { "preview:showhiddenfiles": false })
201204
);
202205
},
203206
},

frontend/app/view/preview/preview-directory.tsx

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
// Copyright 2025, Command Line Inc.
1+
// Copyright 2026, Command Line Inc.
22
// SPDX-License-Identifier: Apache-2.0
33

44
import { ContextMenuModel } from "@/app/store/contextmenu";
5-
import { atoms, getApi, getSettingsKeyAtom, globalStore } from "@/app/store/global";
6-
import { RpcApi } from "@/app/store/wshclientapi";
5+
import { useWaveEnv } from "@/app/waveenv/waveenv";
6+
import { globalStore } from "@/app/store/jotaiStore";
77
import { TabRpcClient } from "@/app/store/wshrpcutil";
88
import { checkKeyPressed, isCharacterKeyEvent } from "@/util/keyutil";
99
import { PLATFORM, PlatformMacOS } from "@/util/platformutil";
@@ -44,6 +44,7 @@ import {
4444
overwriteError,
4545
} from "./preview-directory-utils";
4646
import { type PreviewModel } from "./preview-model";
47+
import type { PreviewEnv } from "./previewenv";
4748

4849
const PageJumpSize = 20;
4950

@@ -110,9 +111,10 @@ function DirectoryTable({
110111
newFile,
111112
newDirectory,
112113
}: DirectoryTableProps) {
114+
const env = useWaveEnv<PreviewEnv>();
113115
const searchActive = useAtomValue(model.directorySearchActive);
114-
const fullConfig = useAtomValue(atoms.fullConfigAtom);
115-
const defaultSort = useAtomValue(getSettingsKeyAtom("preview:defaultsort")) ?? "name";
116+
const fullConfig = useAtomValue(env.atoms.fullConfigAtom);
117+
const defaultSort = useAtomValue(env.getSettingsKeyAtom("preview:defaultsort")) ?? "name";
116118
const setErrorMsg = useSetAtom(model.errorMsgAtom);
117119
const getIconFromMimeType = useCallback(
118120
(mimeType: string): string => {
@@ -560,6 +562,7 @@ interface DirectoryPreviewProps {
560562
}
561563

562564
function DirectoryPreview({ model }: DirectoryPreviewProps) {
565+
const env = useWaveEnv<PreviewEnv>();
563566
const [searchText, setSearchText] = useState("");
564567
const [focusIndex, setFocusIndex] = useState(0);
565568
const [unfilteredData, setUnfilteredData] = useState<FileInfo[]>([]);
@@ -586,7 +589,7 @@ function DirectoryPreview({ model }: DirectoryPreviewProps) {
586589
fireAndForget(async () => {
587590
let entries: FileInfo[];
588591
try {
589-
const file = await RpcApi.FileReadCommand(
592+
const file = await env.rpc.FileReadCommand(
590593
TabRpcClient,
591594
{
592595
info: {
@@ -680,7 +683,7 @@ function DirectoryPreview({ model }: DirectoryPreviewProps) {
680683
PLATFORM == PlatformMacOS &&
681684
!blockData?.meta?.connection
682685
) {
683-
getApi().onQuicklook(selectedPath);
686+
env.electron.onQuicklook(selectedPath);
684687
return true;
685688
}
686689
if (isCharacterKeyEvent(waveEvent)) {
@@ -714,7 +717,7 @@ function DirectoryPreview({ model }: DirectoryPreviewProps) {
714717
const handleDropCopy = useCallback(
715718
async (data: CommandFileCopyData, isDir: boolean) => {
716719
try {
717-
await RpcApi.FileCopyCommand(TabRpcClient, data, { timeout: data.opts.timeout });
720+
await env.rpc.FileCopyCommand(TabRpcClient, data, { timeout: data.opts.timeout });
718721
} catch (e) {
719722
console.warn("Copy failed:", e);
720723
const copyError = `${e}`;
@@ -801,7 +804,7 @@ function DirectoryPreview({ model }: DirectoryPreviewProps) {
801804
onSave: (newName: string) => {
802805
console.log(`newFile: ${newName}`);
803806
fireAndForget(async () => {
804-
await RpcApi.FileCreateCommand(
807+
await env.rpc.FileCreateCommand(
805808
TabRpcClient,
806809
{
807810
info: {
@@ -822,7 +825,7 @@ function DirectoryPreview({ model }: DirectoryPreviewProps) {
822825
onSave: (newName: string) => {
823826
console.log(`newDirectory: ${newName}`);
824827
fireAndForget(async () => {
825-
await RpcApi.FileMkdirCommand(TabRpcClient, {
828+
await env.rpc.FileMkdirCommand(TabRpcClient, {
826829
info: {
827830
path: await model.formatRemoteUri(`${dirPath}/${newName}`, globalStore.get),
828831
},

frontend/app/view/preview/preview-model.tsx

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,8 @@
44
import { BlockNodeModel } from "@/app/block/blocktypes";
55
import { ContextMenuModel } from "@/app/store/contextmenu";
66
import type { TabModel } from "@/app/store/tab-model";
7-
import { RpcApi } from "@/app/store/wshclientapi";
87
import { TabRpcClient } from "@/app/store/wshrpcutil";
9-
import { getConnStatusAtom, getOverrideConfigAtom, getSettingsKeyAtom, globalStore, refocusNode } from "@/store/global";
10-
import * as services from "@/store/services";
8+
import { getOverrideConfigAtom, globalStore, refocusNode } from "@/store/global";
119
import * as WOS from "@/store/wos";
1210
import { goHistory, goHistoryBack, goHistoryForward } from "@/util/historyutil";
1311
import { checkKeyPressed } from "@/util/keyutil";
@@ -21,6 +19,7 @@ import type * as MonacoTypes from "monaco-editor";
2119
import { createRef } from "react";
2220
import { PreviewView } from "./preview";
2321
import { makeDirectoryDefaultMenuItems } from "./preview-directory-utils";
22+
import type { PreviewEnv } from "./previewenv";
2423

2524
// TODO drive this using config
2625
const BOOKMARKS: { label: string; path: string }[] = [
@@ -168,13 +167,15 @@ export class PreviewModel implements ViewModel {
168167
refreshCallback: () => void;
169168
directoryKeyDownHandler: (waveEvent: WaveKeyboardEvent) => boolean;
170169
codeEditKeyDownHandler: (waveEvent: WaveKeyboardEvent) => boolean;
170+
env: PreviewEnv;
171171

172-
constructor({ blockId, nodeModel, tabModel }: ViewModelInitType) {
172+
constructor({ blockId, nodeModel, tabModel, waveEnv }: ViewModelInitType) {
173173
this.viewType = "preview";
174174
this.blockId = blockId;
175175
this.nodeModel = nodeModel;
176176
this.tabModel = tabModel;
177-
let showHiddenFiles = globalStore.get(getSettingsKeyAtom("preview:showhiddenfiles")) ?? true;
177+
this.env = waveEnv;
178+
let showHiddenFiles = globalStore.get(this.env.getSettingsKeyAtom("preview:showhiddenfiles")) ?? true;
178179
this.showHiddenFiles = atom<boolean>(showHiddenFiles);
179180
this.refreshVersion = atom(0);
180181
this.directorySearchActive = atom(false);
@@ -184,7 +185,7 @@ export class PreviewModel implements ViewModel {
184185
this.openFileError = atom(null) as PrimitiveAtom<string>;
185186
this.openFileModalGiveFocusRef = createRef();
186187
this.manageConnection = atom(true);
187-
this.blockAtom = WOS.getWaveObjectAtom<Block>(`block:${blockId}`);
188+
this.blockAtom = this.env.wos.getWaveObjectAtom<Block>(`block:${blockId}`);
188189
this.markdownShowToc = atom(false);
189190
this.filterOutNowsh = atom(true);
190191
this.monacoRef = createRef();
@@ -389,7 +390,7 @@ export class PreviewModel implements ViewModel {
389390
this.connection = atom<Promise<string>>(async (get) => {
390391
const connName = get(this.blockAtom)?.meta?.connection;
391392
try {
392-
await RpcApi.ConnEnsureCommand(TabRpcClient, { connname: connName }, { timeout: 60000 });
393+
await this.env.rpc.ConnEnsureCommand(TabRpcClient, { connname: connName }, { timeout: 60000 });
393394
globalStore.set(this.connectionError, "");
394395
} catch (e) {
395396
globalStore.set(this.connectionError, e as string);
@@ -406,7 +407,7 @@ export class PreviewModel implements ViewModel {
406407
return null;
407408
}
408409
try {
409-
const statFile = await RpcApi.FileInfoCommand(TabRpcClient, {
410+
const statFile = await this.env.rpc.FileInfoCommand(TabRpcClient, {
410411
info: {
411412
path,
412413
},
@@ -436,7 +437,7 @@ export class PreviewModel implements ViewModel {
436437
return null;
437438
}
438439
try {
439-
const file = await RpcApi.FileReadCommand(TabRpcClient, {
440+
const file = await this.env.rpc.FileReadCommand(TabRpcClient, {
440441
info: {
441442
path,
442443
},
@@ -482,7 +483,7 @@ export class PreviewModel implements ViewModel {
482483
this.connStatus = atom((get) => {
483484
const blockData = get(this.blockAtom);
484485
const connName = blockData?.meta?.connection;
485-
const connAtom = getConnStatusAtom(connName);
486+
const connAtom = this.env.getConnStatusAtom(connName);
486487
return get(connAtom);
487488
});
488489

@@ -586,7 +587,7 @@ export class PreviewModel implements ViewModel {
586587
return;
587588
}
588589
const blockOref = WOS.makeORef("block", this.blockId);
589-
await services.ObjectService.UpdateObjectMeta(blockOref, updateMeta);
590+
await this.env.services.ObjectService.UpdateObjectMeta(blockOref, updateMeta);
590591

591592
// Clear the saved file buffers
592593
globalStore.set(this.fileContentSaved, null);
@@ -622,7 +623,7 @@ export class PreviewModel implements ViewModel {
622623
}
623624
updateMeta.edit = false;
624625
const blockOref = WOS.makeORef("block", this.blockId);
625-
await services.ObjectService.UpdateObjectMeta(blockOref, updateMeta);
626+
await this.env.services.ObjectService.UpdateObjectMeta(blockOref, updateMeta);
626627
}
627628

628629
async goHistoryForward() {
@@ -634,13 +635,13 @@ export class PreviewModel implements ViewModel {
634635
}
635636
updateMeta.edit = false;
636637
const blockOref = WOS.makeORef("block", this.blockId);
637-
await services.ObjectService.UpdateObjectMeta(blockOref, updateMeta);
638+
await this.env.services.ObjectService.UpdateObjectMeta(blockOref, updateMeta);
638639
}
639640

640641
async setEditMode(edit: boolean) {
641642
const blockMeta = globalStore.get(this.blockAtom)?.meta;
642643
const blockOref = WOS.makeORef("block", this.blockId);
643-
await services.ObjectService.UpdateObjectMeta(blockOref, { ...blockMeta, edit });
644+
await this.env.services.ObjectService.UpdateObjectMeta(blockOref, { ...blockMeta, edit });
644645
}
645646

646647
async handleFileSave() {
@@ -654,7 +655,7 @@ export class PreviewModel implements ViewModel {
654655
return;
655656
}
656657
try {
657-
await RpcApi.FileWriteCommand(TabRpcClient, {
658+
await this.env.rpc.FileWriteCommand(TabRpcClient, {
658659
info: {
659660
path: await this.formatRemoteUri(filePath, globalStore.get),
660661
},
@@ -699,7 +700,7 @@ export class PreviewModel implements ViewModel {
699700
}
700701

701702
getSettingsMenuItems(): ContextMenuItem[] {
702-
const defaultFontSize = globalStore.get(getSettingsKeyAtom("editor:fontsize")) ?? 12;
703+
const defaultFontSize = globalStore.get(this.env.getSettingsKeyAtom("editor:fontsize")) ?? 12;
703704
const blockData = globalStore.get(this.blockAtom);
704705
const overrideFontSize = blockData?.meta?.["editor:fontsize"];
705706
const menuItems: ContextMenuItem[] = [];
@@ -747,7 +748,7 @@ export class PreviewModel implements ViewModel {
747748
type: "checkbox",
748749
checked: overrideFontSize == fontSize,
749750
click: () => {
750-
RpcApi.SetMetaCommand(TabRpcClient, {
751+
this.env.rpc.SetMetaCommand(TabRpcClient, {
751752
oref: WOS.makeORef("block", this.blockId),
752753
meta: { "editor:fontsize": fontSize },
753754
});
@@ -760,7 +761,7 @@ export class PreviewModel implements ViewModel {
760761
type: "checkbox",
761762
checked: overrideFontSize == null,
762763
click: () => {
763-
RpcApi.SetMetaCommand(TabRpcClient, {
764+
this.env.rpc.SetMetaCommand(TabRpcClient, {
764765
oref: WOS.makeORef("block", this.blockId),
765766
meta: { "editor:fontsize": null },
766767
});
@@ -789,7 +790,7 @@ export class PreviewModel implements ViewModel {
789790
click: () =>
790791
fireAndForget(async () => {
791792
const blockOref = WOS.makeORef("block", this.blockId);
792-
await services.ObjectService.UpdateObjectMeta(blockOref, {
793+
await this.env.services.ObjectService.UpdateObjectMeta(blockOref, {
793794
"editor:wordwrap": !wordWrap,
794795
});
795796
}),

frontend/app/view/preview/preview.tsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
// Copyright 2025, Command Line Inc.
1+
// Copyright 2026, Command Line Inc.
22
// SPDX-License-Identifier: Apache-2.0
33

44
import { CenteredDiv } from "@/app/element/quickelems";
5-
import { RpcApi } from "@/app/store/wshclientapi";
65
import { TabRpcClient } from "@/app/store/wshrpcutil";
76
import { BlockHeaderSuggestionControl } from "@/app/suggestion/suggestion";
7+
import { useWaveEnv } from "@/app/waveenv/waveenv";
88
import { globalStore } from "@/store/global";
99
import { isBlank, makeConnRoute } from "@/util/util";
1010
import { useAtom, useAtomValue, useSetAtom } from "jotai";
@@ -16,6 +16,7 @@ import { ErrorOverlay } from "./preview-error-overlay";
1616
import { MarkdownPreview } from "./preview-markdown";
1717
import type { PreviewModel } from "./preview-model";
1818
import { StreamingPreview } from "./preview-streaming";
19+
import type { PreviewEnv } from "./previewenv";
1920

2021
export type SpecializedViewProps = {
2122
model: PreviewModel;
@@ -64,6 +65,7 @@ const SpecializedView = memo(({ parentRef, model }: SpecializedViewProps) => {
6465
});
6566

6667
const fetchSuggestions = async (
68+
env: PreviewEnv,
6769
model: PreviewModel,
6870
query: string,
6971
reqContext: SuggestionRequestContext
@@ -74,7 +76,7 @@ const fetchSuggestions = async (
7476
route = null;
7577
}
7678
if (reqContext?.dispose) {
77-
RpcApi.DisposeSuggestionsCommand(TabRpcClient, reqContext.widgetid, { noresponse: true, route: route });
79+
env.rpc.DisposeSuggestionsCommand(TabRpcClient, reqContext.widgetid, { noresponse: true, route: route });
7880
return null;
7981
}
8082
const fileInfo = await globalStore.get(model.statFile);
@@ -89,7 +91,7 @@ const fetchSuggestions = async (
8991
reqnum: reqContext.reqnum,
9092
"file:connection": conn,
9193
};
92-
return await RpcApi.FetchSuggestionsCommand(TabRpcClient, sdata, {
94+
return await env.rpc.FetchSuggestionsCommand(TabRpcClient, sdata, {
9395
route: route,
9496
});
9597
};
@@ -104,6 +106,7 @@ function PreviewView({
104106
contentRef: React.RefObject<HTMLDivElement>;
105107
model: PreviewModel;
106108
}) {
109+
const env = useWaveEnv<PreviewEnv>();
107110
const connStatus = useAtomValue(model.connStatus);
108111
const [errorMsg, setErrorMsg] = useAtom(model.errorMsgAtom);
109112
const connection = useAtomValue(model.connectionImmediate);
@@ -140,7 +143,7 @@ function PreviewView({
140143
}
141144
};
142145
const fetchSuggestionsFn = async (query, ctx) => {
143-
return await fetchSuggestions(model, query, ctx);
146+
return await fetchSuggestions(env, model, query, ctx);
144147
};
145148

146149
return (

0 commit comments

Comments
 (0)