From 7379b53266e66feae588da2f26118534b99e5ec8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 14 Mar 2026 19:21:53 +0000 Subject: [PATCH 1/3] Initial plan From c539e503066389b14cac3df5774ebe6ea7ae40f3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 14 Mar 2026 19:34:46 +0000 Subject: [PATCH 2/3] Add preview waveenv narrowing Co-authored-by: sawka <2722291+sawka@users.noreply.github.com> --- .../view/preview/preview-directory-utils.tsx | 21 +++++----- .../app/view/preview/preview-directory.tsx | 23 ++++++----- frontend/app/view/preview/preview-model.tsx | 39 ++++++++++--------- frontend/app/view/preview/preview.tsx | 13 ++++--- frontend/app/view/preview/previewenv.test.ts | 18 +++++++++ frontend/app/view/preview/previewenv.ts | 34 ++++++++++++++++ 6 files changed, 105 insertions(+), 43 deletions(-) create mode 100644 frontend/app/view/preview/previewenv.test.ts create mode 100644 frontend/app/view/preview/previewenv.ts diff --git a/frontend/app/view/preview/preview-directory-utils.tsx b/frontend/app/view/preview/preview-directory-utils.tsx index fac6bfff17..e278475cac 100644 --- a/frontend/app/view/preview/preview-directory-utils.tsx +++ b/frontend/app/view/preview/preview-directory-utils.tsx @@ -1,8 +1,7 @@ // Copyright 2026, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 -import { getSettingsKeyAtom, globalStore } from "@/app/store/global"; -import { RpcApi } from "@/app/store/wshclientapi"; +import { globalStore } from "@/app/store/jotaiStore"; import { TabRpcClient } from "@/app/store/wshrpcutil"; import { fireAndForget, isBlank } from "@/util/util"; import dayjs from "dayjs"; @@ -95,7 +94,7 @@ export function handleRename( if (isDir) { srcuri += "/"; } - await RpcApi.FileMoveCommand(TabRpcClient, { + await model.env.rpc.FileMoveCommand(TabRpcClient, { srcuri, desturi: await model.formatRemoteUri(newPath, globalStore.get), }); @@ -121,7 +120,7 @@ export function handleFileDelete( fireAndForget(async () => { const formattedPath = await model.formatRemoteUri(path, globalStore.get); try { - await RpcApi.FileDeleteCommand(TabRpcClient, { + await model.env.rpc.FileDeleteCommand(TabRpcClient, { path: formattedPath, recursive, }); @@ -154,7 +153,7 @@ export function handleFileDelete( } export function makeDirectoryDefaultMenuItems(model: PreviewModel): ContextMenuItem[] { - const defaultSort = globalStore.get(getSettingsKeyAtom("preview:defaultsort")) ?? "name"; + const defaultSort = globalStore.get(model.env.getSettingsKeyAtom("preview:defaultsort")) ?? "name"; const showHiddenFiles = globalStore.get(model.showHiddenFiles) ?? true; return [ { @@ -165,7 +164,9 @@ export function makeDirectoryDefaultMenuItems(model: PreviewModel): ContextMenuI type: "checkbox", checked: defaultSort === "name", click: () => - fireAndForget(() => RpcApi.SetConfigCommand(TabRpcClient, { "preview:defaultsort": "name" })), + fireAndForget(() => + model.env.rpc.SetConfigCommand(TabRpcClient, { "preview:defaultsort": "name" }) + ), }, { label: "Last Modified", @@ -173,7 +174,7 @@ export function makeDirectoryDefaultMenuItems(model: PreviewModel): ContextMenuI checked: defaultSort === "modtime", click: () => fireAndForget(() => - RpcApi.SetConfigCommand(TabRpcClient, { "preview:defaultsort": "modtime" }) + model.env.rpc.SetConfigCommand(TabRpcClient, { "preview:defaultsort": "modtime" }) ), }, ], @@ -187,7 +188,9 @@ export function makeDirectoryDefaultMenuItems(model: PreviewModel): ContextMenuI checked: showHiddenFiles, click: () => { globalStore.set(model.showHiddenFiles, true); - fireAndForget(() => RpcApi.SetConfigCommand(TabRpcClient, { "preview:showhiddenfiles": true })); + fireAndForget(() => + model.env.rpc.SetConfigCommand(TabRpcClient, { "preview:showhiddenfiles": true }) + ); }, }, { @@ -197,7 +200,7 @@ export function makeDirectoryDefaultMenuItems(model: PreviewModel): ContextMenuI click: () => { globalStore.set(model.showHiddenFiles, false); fireAndForget(() => - RpcApi.SetConfigCommand(TabRpcClient, { "preview:showhiddenfiles": false }) + model.env.rpc.SetConfigCommand(TabRpcClient, { "preview:showhiddenfiles": false }) ); }, }, diff --git a/frontend/app/view/preview/preview-directory.tsx b/frontend/app/view/preview/preview-directory.tsx index 1bd0ab9101..cdacd810bd 100644 --- a/frontend/app/view/preview/preview-directory.tsx +++ b/frontend/app/view/preview/preview-directory.tsx @@ -1,9 +1,9 @@ -// Copyright 2025, Command Line Inc. +// Copyright 2026, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 import { ContextMenuModel } from "@/app/store/contextmenu"; -import { atoms, getApi, getSettingsKeyAtom, globalStore } from "@/app/store/global"; -import { RpcApi } from "@/app/store/wshclientapi"; +import { useWaveEnv } from "@/app/waveenv/waveenv"; +import { globalStore } from "@/app/store/jotaiStore"; import { TabRpcClient } from "@/app/store/wshrpcutil"; import { checkKeyPressed, isCharacterKeyEvent } from "@/util/keyutil"; import { PLATFORM, PlatformMacOS } from "@/util/platformutil"; @@ -44,6 +44,7 @@ import { overwriteError, } from "./preview-directory-utils"; import { type PreviewModel } from "./preview-model"; +import type { PreviewEnv } from "./previewenv"; const PageJumpSize = 20; @@ -110,9 +111,10 @@ function DirectoryTable({ newFile, newDirectory, }: DirectoryTableProps) { + const env = useWaveEnv(); const searchActive = useAtomValue(model.directorySearchActive); - const fullConfig = useAtomValue(atoms.fullConfigAtom); - const defaultSort = useAtomValue(getSettingsKeyAtom("preview:defaultsort")) ?? "name"; + const fullConfig = useAtomValue(env.atoms.fullConfigAtom); + const defaultSort = useAtomValue(env.getSettingsKeyAtom("preview:defaultsort")) ?? "name"; const setErrorMsg = useSetAtom(model.errorMsgAtom); const getIconFromMimeType = useCallback( (mimeType: string): string => { @@ -560,6 +562,7 @@ interface DirectoryPreviewProps { } function DirectoryPreview({ model }: DirectoryPreviewProps) { + const env = useWaveEnv(); const [searchText, setSearchText] = useState(""); const [focusIndex, setFocusIndex] = useState(0); const [unfilteredData, setUnfilteredData] = useState([]); @@ -586,7 +589,7 @@ function DirectoryPreview({ model }: DirectoryPreviewProps) { fireAndForget(async () => { let entries: FileInfo[]; try { - const file = await RpcApi.FileReadCommand( + const file = await env.rpc.FileReadCommand( TabRpcClient, { info: { @@ -680,7 +683,7 @@ function DirectoryPreview({ model }: DirectoryPreviewProps) { PLATFORM == PlatformMacOS && !blockData?.meta?.connection ) { - getApi().onQuicklook(selectedPath); + env.electron.onQuicklook(selectedPath); return true; } if (isCharacterKeyEvent(waveEvent)) { @@ -714,7 +717,7 @@ function DirectoryPreview({ model }: DirectoryPreviewProps) { const handleDropCopy = useCallback( async (data: CommandFileCopyData, isDir: boolean) => { try { - await RpcApi.FileCopyCommand(TabRpcClient, data, { timeout: data.opts.timeout }); + await env.rpc.FileCopyCommand(TabRpcClient, data, { timeout: data.opts.timeout }); } catch (e) { console.warn("Copy failed:", e); const copyError = `${e}`; @@ -801,7 +804,7 @@ function DirectoryPreview({ model }: DirectoryPreviewProps) { onSave: (newName: string) => { console.log(`newFile: ${newName}`); fireAndForget(async () => { - await RpcApi.FileCreateCommand( + await env.rpc.FileCreateCommand( TabRpcClient, { info: { @@ -822,7 +825,7 @@ function DirectoryPreview({ model }: DirectoryPreviewProps) { onSave: (newName: string) => { console.log(`newDirectory: ${newName}`); fireAndForget(async () => { - await RpcApi.FileMkdirCommand(TabRpcClient, { + await env.rpc.FileMkdirCommand(TabRpcClient, { info: { path: await model.formatRemoteUri(`${dirPath}/${newName}`, globalStore.get), }, diff --git a/frontend/app/view/preview/preview-model.tsx b/frontend/app/view/preview/preview-model.tsx index 59cbbaca4f..6ebe3c040f 100644 --- a/frontend/app/view/preview/preview-model.tsx +++ b/frontend/app/view/preview/preview-model.tsx @@ -4,10 +4,8 @@ import { BlockNodeModel } from "@/app/block/blocktypes"; import { ContextMenuModel } from "@/app/store/contextmenu"; import type { TabModel } from "@/app/store/tab-model"; -import { RpcApi } from "@/app/store/wshclientapi"; import { TabRpcClient } from "@/app/store/wshrpcutil"; -import { getConnStatusAtom, getOverrideConfigAtom, getSettingsKeyAtom, globalStore, refocusNode } from "@/store/global"; -import * as services from "@/store/services"; +import { getOverrideConfigAtom, globalStore, refocusNode } from "@/store/global"; import * as WOS from "@/store/wos"; import { goHistory, goHistoryBack, goHistoryForward } from "@/util/historyutil"; import { checkKeyPressed } from "@/util/keyutil"; @@ -21,6 +19,7 @@ import type * as MonacoTypes from "monaco-editor"; import { createRef } from "react"; import { PreviewView } from "./preview"; import { makeDirectoryDefaultMenuItems } from "./preview-directory-utils"; +import type { PreviewEnv } from "./previewenv"; // TODO drive this using config const BOOKMARKS: { label: string; path: string }[] = [ @@ -168,13 +167,15 @@ export class PreviewModel implements ViewModel { refreshCallback: () => void; directoryKeyDownHandler: (waveEvent: WaveKeyboardEvent) => boolean; codeEditKeyDownHandler: (waveEvent: WaveKeyboardEvent) => boolean; + env: PreviewEnv; - constructor({ blockId, nodeModel, tabModel }: ViewModelInitType) { + constructor({ blockId, nodeModel, tabModel, waveEnv }: ViewModelInitType) { this.viewType = "preview"; this.blockId = blockId; this.nodeModel = nodeModel; this.tabModel = tabModel; - let showHiddenFiles = globalStore.get(getSettingsKeyAtom("preview:showhiddenfiles")) ?? true; + this.env = waveEnv; + let showHiddenFiles = globalStore.get(this.env.getSettingsKeyAtom("preview:showhiddenfiles")) ?? true; this.showHiddenFiles = atom(showHiddenFiles); this.refreshVersion = atom(0); this.directorySearchActive = atom(false); @@ -184,7 +185,7 @@ export class PreviewModel implements ViewModel { this.openFileError = atom(null) as PrimitiveAtom; this.openFileModalGiveFocusRef = createRef(); this.manageConnection = atom(true); - this.blockAtom = WOS.getWaveObjectAtom(`block:${blockId}`); + this.blockAtom = this.env.wos.getWaveObjectAtom(`block:${blockId}`); this.markdownShowToc = atom(false); this.filterOutNowsh = atom(true); this.monacoRef = createRef(); @@ -389,7 +390,7 @@ export class PreviewModel implements ViewModel { this.connection = atom>(async (get) => { const connName = get(this.blockAtom)?.meta?.connection; try { - await RpcApi.ConnEnsureCommand(TabRpcClient, { connname: connName }, { timeout: 60000 }); + await this.env.rpc.ConnEnsureCommand(TabRpcClient, { connname: connName }, { timeout: 60000 }); globalStore.set(this.connectionError, ""); } catch (e) { globalStore.set(this.connectionError, e as string); @@ -406,7 +407,7 @@ export class PreviewModel implements ViewModel { return null; } try { - const statFile = await RpcApi.FileInfoCommand(TabRpcClient, { + const statFile = await this.env.rpc.FileInfoCommand(TabRpcClient, { info: { path, }, @@ -436,7 +437,7 @@ export class PreviewModel implements ViewModel { return null; } try { - const file = await RpcApi.FileReadCommand(TabRpcClient, { + const file = await this.env.rpc.FileReadCommand(TabRpcClient, { info: { path, }, @@ -482,7 +483,7 @@ export class PreviewModel implements ViewModel { this.connStatus = atom((get) => { const blockData = get(this.blockAtom); const connName = blockData?.meta?.connection; - const connAtom = getConnStatusAtom(connName); + const connAtom = this.env.getConnStatusAtom(connName); return get(connAtom); }); @@ -586,7 +587,7 @@ export class PreviewModel implements ViewModel { return; } const blockOref = WOS.makeORef("block", this.blockId); - await services.ObjectService.UpdateObjectMeta(blockOref, updateMeta); + await this.env.services.ObjectService.UpdateObjectMeta(blockOref, updateMeta); // Clear the saved file buffers globalStore.set(this.fileContentSaved, null); @@ -622,7 +623,7 @@ export class PreviewModel implements ViewModel { } updateMeta.edit = false; const blockOref = WOS.makeORef("block", this.blockId); - await services.ObjectService.UpdateObjectMeta(blockOref, updateMeta); + await this.env.services.ObjectService.UpdateObjectMeta(blockOref, updateMeta); } async goHistoryForward() { @@ -634,13 +635,13 @@ export class PreviewModel implements ViewModel { } updateMeta.edit = false; const blockOref = WOS.makeORef("block", this.blockId); - await services.ObjectService.UpdateObjectMeta(blockOref, updateMeta); + await this.env.services.ObjectService.UpdateObjectMeta(blockOref, updateMeta); } async setEditMode(edit: boolean) { const blockMeta = globalStore.get(this.blockAtom)?.meta; const blockOref = WOS.makeORef("block", this.blockId); - await services.ObjectService.UpdateObjectMeta(blockOref, { ...blockMeta, edit }); + await this.env.services.ObjectService.UpdateObjectMeta(blockOref, { ...blockMeta, edit }); } async handleFileSave() { @@ -654,7 +655,7 @@ export class PreviewModel implements ViewModel { return; } try { - await RpcApi.FileWriteCommand(TabRpcClient, { + await this.env.rpc.FileWriteCommand(TabRpcClient, { info: { path: await this.formatRemoteUri(filePath, globalStore.get), }, @@ -699,7 +700,7 @@ export class PreviewModel implements ViewModel { } getSettingsMenuItems(): ContextMenuItem[] { - const defaultFontSize = globalStore.get(getSettingsKeyAtom("editor:fontsize")) ?? 12; + const defaultFontSize = globalStore.get(this.env.getSettingsKeyAtom("editor:fontsize")) ?? 12; const blockData = globalStore.get(this.blockAtom); const overrideFontSize = blockData?.meta?.["editor:fontsize"]; const menuItems: ContextMenuItem[] = []; @@ -747,7 +748,7 @@ export class PreviewModel implements ViewModel { type: "checkbox", checked: overrideFontSize == fontSize, click: () => { - RpcApi.SetMetaCommand(TabRpcClient, { + this.env.rpc.SetMetaCommand(TabRpcClient, { oref: WOS.makeORef("block", this.blockId), meta: { "editor:fontsize": fontSize }, }); @@ -760,7 +761,7 @@ export class PreviewModel implements ViewModel { type: "checkbox", checked: overrideFontSize == null, click: () => { - RpcApi.SetMetaCommand(TabRpcClient, { + this.env.rpc.SetMetaCommand(TabRpcClient, { oref: WOS.makeORef("block", this.blockId), meta: { "editor:fontsize": null }, }); @@ -789,7 +790,7 @@ export class PreviewModel implements ViewModel { click: () => fireAndForget(async () => { const blockOref = WOS.makeORef("block", this.blockId); - await services.ObjectService.UpdateObjectMeta(blockOref, { + await this.env.services.ObjectService.UpdateObjectMeta(blockOref, { "editor:wordwrap": !wordWrap, }); }), diff --git a/frontend/app/view/preview/preview.tsx b/frontend/app/view/preview/preview.tsx index 89eee74deb..33188ae5b5 100644 --- a/frontend/app/view/preview/preview.tsx +++ b/frontend/app/view/preview/preview.tsx @@ -1,10 +1,10 @@ -// Copyright 2025, Command Line Inc. +// Copyright 2026, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 import { CenteredDiv } from "@/app/element/quickelems"; -import { RpcApi } from "@/app/store/wshclientapi"; import { TabRpcClient } from "@/app/store/wshrpcutil"; import { BlockHeaderSuggestionControl } from "@/app/suggestion/suggestion"; +import { useWaveEnv } from "@/app/waveenv/waveenv"; import { globalStore } from "@/store/global"; import { isBlank, makeConnRoute } from "@/util/util"; import { useAtom, useAtomValue, useSetAtom } from "jotai"; @@ -16,6 +16,7 @@ import { ErrorOverlay } from "./preview-error-overlay"; import { MarkdownPreview } from "./preview-markdown"; import type { PreviewModel } from "./preview-model"; import { StreamingPreview } from "./preview-streaming"; +import type { PreviewEnv } from "./previewenv"; export type SpecializedViewProps = { model: PreviewModel; @@ -64,6 +65,7 @@ const SpecializedView = memo(({ parentRef, model }: SpecializedViewProps) => { }); const fetchSuggestions = async ( + env: PreviewEnv, model: PreviewModel, query: string, reqContext: SuggestionRequestContext @@ -74,7 +76,7 @@ const fetchSuggestions = async ( route = null; } if (reqContext?.dispose) { - RpcApi.DisposeSuggestionsCommand(TabRpcClient, reqContext.widgetid, { noresponse: true, route: route }); + env.rpc.DisposeSuggestionsCommand(TabRpcClient, reqContext.widgetid, { noresponse: true, route: route }); return null; } const fileInfo = await globalStore.get(model.statFile); @@ -89,7 +91,7 @@ const fetchSuggestions = async ( reqnum: reqContext.reqnum, "file:connection": conn, }; - return await RpcApi.FetchSuggestionsCommand(TabRpcClient, sdata, { + return await env.rpc.FetchSuggestionsCommand(TabRpcClient, sdata, { route: route, }); }; @@ -104,6 +106,7 @@ function PreviewView({ contentRef: React.RefObject; model: PreviewModel; }) { + const env = useWaveEnv(); const connStatus = useAtomValue(model.connStatus); const [errorMsg, setErrorMsg] = useAtom(model.errorMsgAtom); const connection = useAtomValue(model.connectionImmediate); @@ -140,7 +143,7 @@ function PreviewView({ } }; const fetchSuggestionsFn = async (query, ctx) => { - return await fetchSuggestions(model, query, ctx); + return await fetchSuggestions(env, model, query, ctx); }; return ( diff --git a/frontend/app/view/preview/previewenv.test.ts b/frontend/app/view/preview/previewenv.test.ts new file mode 100644 index 0000000000..58a5d94aef --- /dev/null +++ b/frontend/app/view/preview/previewenv.test.ts @@ -0,0 +1,18 @@ +// Copyright 2026, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +import { makeMockWaveEnv } from "@/preview/mock/mockwaveenv"; +import { describe, expect, it } from "vitest"; +import type { PreviewEnv } from "./previewenv"; + +describe("PreviewEnv", () => { + it("is satisfied by the existing mock wave env", () => { + const env: PreviewEnv = makeMockWaveEnv(); + + expect(env.rpc.FetchSuggestionsCommand).toBeTypeOf("function"); + expect(env.rpc.FileReadCommand).toBeTypeOf("function"); + expect(env.rpc.SetConfigCommand).toBeTypeOf("function"); + expect(env.electron.onQuicklook).toBeTypeOf("function"); + expect(env.atoms.fullConfigAtom).toBeTruthy(); + }); +}); diff --git a/frontend/app/view/preview/previewenv.ts b/frontend/app/view/preview/previewenv.ts new file mode 100644 index 0000000000..d99dee9cc5 --- /dev/null +++ b/frontend/app/view/preview/previewenv.ts @@ -0,0 +1,34 @@ +// Copyright 2026, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +import { SettingsKeyAtomFnType, WaveEnv, WaveEnvSubset } from "@/app/waveenv/waveenv"; + +export type PreviewEnv = WaveEnvSubset<{ + electron: { + onQuicklook: WaveEnv["electron"]["onQuicklook"]; + }; + rpc: { + ConnEnsureCommand: WaveEnv["rpc"]["ConnEnsureCommand"]; + FileInfoCommand: WaveEnv["rpc"]["FileInfoCommand"]; + FileReadCommand: WaveEnv["rpc"]["FileReadCommand"]; + FileWriteCommand: WaveEnv["rpc"]["FileWriteCommand"]; + FileMoveCommand: WaveEnv["rpc"]["FileMoveCommand"]; + FileDeleteCommand: WaveEnv["rpc"]["FileDeleteCommand"]; + SetConfigCommand: WaveEnv["rpc"]["SetConfigCommand"]; + SetMetaCommand: WaveEnv["rpc"]["SetMetaCommand"]; + FetchSuggestionsCommand: WaveEnv["rpc"]["FetchSuggestionsCommand"]; + DisposeSuggestionsCommand: WaveEnv["rpc"]["DisposeSuggestionsCommand"]; + FileCopyCommand: WaveEnv["rpc"]["FileCopyCommand"]; + FileCreateCommand: WaveEnv["rpc"]["FileCreateCommand"]; + FileMkdirCommand: WaveEnv["rpc"]["FileMkdirCommand"]; + }; + atoms: { + fullConfigAtom: WaveEnv["atoms"]["fullConfigAtom"]; + }; + services: { + ObjectService: WaveEnv["services"]["ObjectService"]; + }; + wos: WaveEnv["wos"]; + getSettingsKeyAtom: SettingsKeyAtomFnType<"preview:showhiddenfiles" | "editor:fontsize" | "preview:defaultsort">; + getConnStatusAtom: WaveEnv["getConnStatusAtom"]; +}>; From 813fe7afe12ae8cec26e04effd2c06ad6e3472a7 Mon Sep 17 00:00:00 2001 From: sawka Date: Sat, 14 Mar 2026 17:34:58 -0700 Subject: [PATCH 3/3] remove previewenv test file --- frontend/app/view/preview/previewenv.test.ts | 18 ------------------ 1 file changed, 18 deletions(-) delete mode 100644 frontend/app/view/preview/previewenv.test.ts diff --git a/frontend/app/view/preview/previewenv.test.ts b/frontend/app/view/preview/previewenv.test.ts deleted file mode 100644 index 58a5d94aef..0000000000 --- a/frontend/app/view/preview/previewenv.test.ts +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright 2026, Command Line Inc. -// SPDX-License-Identifier: Apache-2.0 - -import { makeMockWaveEnv } from "@/preview/mock/mockwaveenv"; -import { describe, expect, it } from "vitest"; -import type { PreviewEnv } from "./previewenv"; - -describe("PreviewEnv", () => { - it("is satisfied by the existing mock wave env", () => { - const env: PreviewEnv = makeMockWaveEnv(); - - expect(env.rpc.FetchSuggestionsCommand).toBeTypeOf("function"); - expect(env.rpc.FileReadCommand).toBeTypeOf("function"); - expect(env.rpc.SetConfigCommand).toBeTypeOf("function"); - expect(env.electron.onQuicklook).toBeTypeOf("function"); - expect(env.atoms.fullConfigAtom).toBeTruthy(); - }); -});