Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
153 changes: 79 additions & 74 deletions frontend/app/view/waveconfig/waveconfig-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,13 @@
// SPDX-License-Identifier: Apache-2.0

import { BlockNodeModel } from "@/app/block/blocktypes";
import { atoms, getApi, getBlockMetaKeyAtom, WOS } from "@/app/store/global";
import { globalStore } from "@/app/store/jotaiStore";
import type { TabModel } from "@/app/store/tab-model";
import { RpcApi } from "@/app/store/wshclientapi";
import { makeORef } from "@/app/store/wos";
import { TabRpcClient } from "@/app/store/wshrpcutil";
import { SecretsContent } from "@/app/view/waveconfig/secretscontent";
import { WaveConfigView } from "@/app/view/waveconfig/waveconfig";
import { isWindows } from "@/util/platformutil";
import type { WaveConfigEnv } from "@/app/view/waveconfig/waveconfigenv";
import { base64ToString, stringToBase64 } from "@/util/util";
import { atom, type Atom, type PrimitiveAtom } from "jotai";
import type * as MonacoTypes from "monaco-editor";
Expand Down Expand Up @@ -66,55 +65,57 @@ function validateWaveAiJson(parsed: any): ValidationResult {
return { success: true };
}

const configFiles: ConfigFile[] = [
{
name: "General",
path: "settings.json",
language: "json",
docsUrl: "https://docs.waveterm.dev/config",
hasJsonView: true,
},
{
name: "Connections",
path: "connections.json",
language: "json",
docsUrl: "https://docs.waveterm.dev/connections",
description: isWindows() ? "SSH hosts and WSL distros" : "SSH hosts",
hasJsonView: true,
},
{
name: "Sidebar Widgets",
path: "widgets.json",
language: "json",
docsUrl: "https://docs.waveterm.dev/customwidgets",
hasJsonView: true,
},
{
name: "Wave AI Modes",
path: "waveai.json",
language: "json",
description: "Local models and BYOK",
docsUrl: "https://docs.waveterm.dev/waveai-modes",
validator: validateWaveAiJson,
hasJsonView: true,
// visualComponent: WaveAIVisualContent,
},
{
name: "Tab Backgrounds",
path: "presets/bg.json",
language: "json",
docsUrl: "https://docs.waveterm.dev/presets#background-configurations",
validator: validateBgJson,
hasJsonView: true,
},
{
name: "Secrets",
path: "secrets",
isSecrets: true,
hasJsonView: false,
visualComponent: SecretsContent,
},
];
function makeConfigFiles(isWindows: boolean): ConfigFile[] {
return [
{
name: "General",
path: "settings.json",
language: "json",
docsUrl: "https://docs.waveterm.dev/config",
hasJsonView: true,
},
{
name: "Connections",
path: "connections.json",
language: "json",
docsUrl: "https://docs.waveterm.dev/connections",
description: isWindows ? "SSH hosts and WSL distros" : "SSH hosts",
hasJsonView: true,
},
{
name: "Sidebar Widgets",
path: "widgets.json",
language: "json",
docsUrl: "https://docs.waveterm.dev/customwidgets",
hasJsonView: true,
},
{
name: "Wave AI Modes",
path: "waveai.json",
language: "json",
description: "Local models and BYOK",
docsUrl: "https://docs.waveterm.dev/waveai-modes",
validator: validateWaveAiJson,
hasJsonView: true,
// visualComponent: WaveAIVisualContent,
},
{
name: "Tab Backgrounds",
path: "presets/bg.json",
language: "json",
docsUrl: "https://docs.waveterm.dev/presets#background-configurations",
validator: validateBgJson,
hasJsonView: true,
},
{
name: "Secrets",
path: "secrets",
isSecrets: true,
hasJsonView: false,
visualComponent: SecretsContent,
},
];
}

const deprecatedConfigFiles: ConfigFile[] = [
{
Expand Down Expand Up @@ -144,6 +145,7 @@ export class WaveConfigViewModel implements ViewModel {
noPadding = atom(true);
nodeModel: BlockNodeModel;
tabModel: TabModel;
env: WaveConfigEnv;

selectedFileAtom: PrimitiveAtom<ConfigFile>;
fileContentAtom: PrimitiveAtom<string>;
Expand Down Expand Up @@ -171,12 +173,13 @@ export class WaveConfigViewModel implements ViewModel {
storageBackendErrorAtom: PrimitiveAtom<string | null>;
secretValueRef: HTMLTextAreaElement | null = null;

constructor({ blockId, nodeModel, tabModel }: ViewModelInitType) {
constructor({ blockId, nodeModel, tabModel, waveEnv }: ViewModelInitType) {
this.blockId = blockId;
this.nodeModel = nodeModel;
this.tabModel = tabModel;
this.configDir = getApi().getConfigDir();
const platform = getApi().getPlatform();
this.env = waveEnv as WaveConfigEnv;
this.configDir = this.env.electron.getConfigDir();
const platform = this.env.electron.getPlatform();
this.saveShortcut = platform === "darwin" ? "Cmd+S" : "Alt+S";

this.selectedFileAtom = atom(null) as PrimitiveAtom<ConfigFile>;
Expand All @@ -191,7 +194,7 @@ export class WaveConfigViewModel implements ViewModel {
this.presetsJsonExistsAtom = atom(false);
this.activeTabAtom = atom<"visual" | "json">("visual");
this.configErrorFilesAtom = atom((get) => {
const fullConfig = get(atoms.fullConfigAtom);
const fullConfig = get(this.env.atoms.fullConfigAtom);
const errorSet = new Set<string>();
for (const cerr of fullConfig?.configerrors ?? []) {
errorSet.add(cerr.file);
Expand All @@ -216,7 +219,7 @@ export class WaveConfigViewModel implements ViewModel {
async checkPresetsJsonExists() {
try {
const fullPath = `${this.configDir}/presets.json`;
const fileInfo = await RpcApi.FileInfoCommand(TabRpcClient, {
const fileInfo = await this.env.rpc.FileInfoCommand(TabRpcClient, {
info: { path: fullPath },
});
if (!fileInfo.notfound) {
Expand All @@ -230,8 +233,10 @@ export class WaveConfigViewModel implements ViewModel {
initialize() {
const selectedFile = globalStore.get(this.selectedFileAtom);
if (!selectedFile) {
const metaFileAtom = getBlockMetaKeyAtom(this.blockId, "file");
const metaFileAtom = this.env.getBlockMetaKeyAtom(this.blockId, "file");
const savedFilePath = globalStore.get(metaFileAtom);
const configFiles = this.getConfigFiles();
const deprecatedConfigFiles = this.getDeprecatedConfigFiles();

let fileToLoad: ConfigFile | null = null;
if (savedFilePath) {
Expand All @@ -252,7 +257,7 @@ export class WaveConfigViewModel implements ViewModel {
}

getConfigFiles(): ConfigFile[] {
return configFiles;
return makeConfigFiles(this.env.isWindows());
}

getDeprecatedConfigFiles(): ConfigFile[] {
Expand Down Expand Up @@ -280,8 +285,8 @@ export class WaveConfigViewModel implements ViewModel {

if (file.isSecrets) {
globalStore.set(this.selectedFileAtom, file);
RpcApi.SetMetaCommand(TabRpcClient, {
oref: WOS.makeORef("block", this.blockId),
this.env.rpc.SetMetaCommand(TabRpcClient, {
oref: makeORef("block", this.blockId),
meta: { file: file.path },
});
globalStore.set(this.isLoadingAtom, false);
Expand All @@ -292,7 +297,7 @@ export class WaveConfigViewModel implements ViewModel {

try {
const fullPath = `${this.configDir}/${file.path}`;
const fileData = await RpcApi.FileReadCommand(TabRpcClient, {
const fileData = await this.env.rpc.FileReadCommand(TabRpcClient, {
info: { path: fullPath },
});
const content = fileData?.data64 ? base64ToString(fileData.data64) : "";
Expand All @@ -303,8 +308,8 @@ export class WaveConfigViewModel implements ViewModel {
globalStore.set(this.fileContentAtom, content);
}
globalStore.set(this.selectedFileAtom, file);
RpcApi.SetMetaCommand(TabRpcClient, {
oref: WOS.makeORef("block", this.blockId),
this.env.rpc.SetMetaCommand(TabRpcClient, {
oref: makeORef("block", this.blockId),
meta: { file: file.path },
});
} catch (err) {
Expand All @@ -329,7 +334,7 @@ export class WaveConfigViewModel implements ViewModel {

try {
const fullPath = `${this.configDir}/${selectedFile.path}`;
await RpcApi.FileWriteCommand(TabRpcClient, {
await this.env.rpc.FileWriteCommand(TabRpcClient, {
info: { path: fullPath },
data64: stringToBase64(""),
});
Expand Down Expand Up @@ -371,7 +376,7 @@ export class WaveConfigViewModel implements ViewModel {

try {
const fullPath = `${this.configDir}/${selectedFile.path}`;
await RpcApi.FileWriteCommand(TabRpcClient, {
await this.env.rpc.FileWriteCommand(TabRpcClient, {
info: { path: fullPath },
data64: stringToBase64(formatted),
});
Expand Down Expand Up @@ -401,7 +406,7 @@ export class WaveConfigViewModel implements ViewModel {

async checkStorageBackend() {
try {
const backend = await RpcApi.GetSecretsLinuxStorageBackendCommand(TabRpcClient);
const backend = await this.env.rpc.GetSecretsLinuxStorageBackendCommand(TabRpcClient);
if (backend === "basic_text" || backend === "unknown") {
globalStore.set(
this.storageBackendErrorAtom,
Expand All @@ -420,7 +425,7 @@ export class WaveConfigViewModel implements ViewModel {
globalStore.set(this.errorMessageAtom, null);

try {
const names = await RpcApi.GetSecretsNamesCommand(TabRpcClient);
const names = await this.env.rpc.GetSecretsNamesCommand(TabRpcClient);
globalStore.set(this.secretNamesAtom, names || []);
} catch (error) {
globalStore.set(this.errorMessageAtom, `Failed to load secrets: ${error.message}`);
Expand Down Expand Up @@ -452,7 +457,7 @@ export class WaveConfigViewModel implements ViewModel {
globalStore.set(this.errorMessageAtom, null);

try {
const secrets = await RpcApi.GetSecretsCommand(TabRpcClient, [selectedSecret]);
const secrets = await this.env.rpc.GetSecretsCommand(TabRpcClient, [selectedSecret]);
const value = secrets[selectedSecret];
if (value !== undefined) {
globalStore.set(this.secretValueAtom, value);
Expand All @@ -479,8 +484,8 @@ export class WaveConfigViewModel implements ViewModel {
globalStore.set(this.errorMessageAtom, null);

try {
await RpcApi.SetSecretsCommand(TabRpcClient, { [selectedSecret]: secretValue });
RpcApi.RecordTEventCommand(
await this.env.rpc.SetSecretsCommand(TabRpcClient, { [selectedSecret]: secretValue });
this.env.rpc.RecordTEventCommand(
TabRpcClient,
{
event: "action:other",
Expand Down Expand Up @@ -509,7 +514,7 @@ export class WaveConfigViewModel implements ViewModel {
globalStore.set(this.errorMessageAtom, null);

try {
await RpcApi.SetSecretsCommand(TabRpcClient, { [selectedSecret]: null });
await this.env.rpc.SetSecretsCommand(TabRpcClient, { [selectedSecret]: null });
this.closeSecretView();
await this.refreshSecrets();
} catch (error) {
Expand Down Expand Up @@ -560,8 +565,8 @@ export class WaveConfigViewModel implements ViewModel {
globalStore.set(this.errorMessageAtom, null);

try {
await RpcApi.SetSecretsCommand(TabRpcClient, { [name]: value });
RpcApi.RecordTEventCommand(
await this.env.rpc.SetSecretsCommand(TabRpcClient, { [name]: value });
this.env.rpc.RecordTEventCommand(
TabRpcClient,
{
event: "action:other",
Expand Down
6 changes: 4 additions & 2 deletions frontend/app/view/waveconfig/waveconfig.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@
// SPDX-License-Identifier: Apache-2.0

import { Tooltip } from "@/app/element/tooltip";
import { atoms } from "@/app/store/global";
import { globalStore } from "@/app/store/jotaiStore";
import { tryReinjectKey } from "@/app/store/keymodel";
import { CodeEditor } from "@/app/view/codeeditor/codeeditor";
import type { ConfigFile, WaveConfigViewModel } from "@/app/view/waveconfig/waveconfig-model";
import type { WaveConfigEnv } from "@/app/view/waveconfig/waveconfigenv";
import { useWaveEnv } from "@/app/waveenv/waveenv";
import { adaptFromReactOrNativeKeyEvent, checkKeyPressed, keydownWrapper } from "@/util/keyutil";
import { cn } from "@/util/util";
import { useAtom, useAtomValue } from "jotai";
Expand Down Expand Up @@ -97,6 +98,7 @@ const ConfigSidebar = memo(({ model }: ConfigSidebarProps) => {
ConfigSidebar.displayName = "ConfigSidebar";

const WaveConfigView = memo(({ blockId, model }: ViewComponentProps<WaveConfigViewModel>) => {
const env = useWaveEnv<WaveConfigEnv>();
const selectedFile = useAtomValue(model.selectedFileAtom);
const [fileContent, setFileContent] = useAtom(model.fileContentAtom);
const isLoading = useAtomValue(model.isLoadingAtom);
Expand All @@ -106,7 +108,7 @@ const WaveConfigView = memo(({ blockId, model }: ViewComponentProps<WaveConfigVi
const [isMenuOpen, setIsMenuOpen] = useAtom(model.isMenuOpenAtom);
const hasChanges = useAtomValue(model.hasEditedAtom);
const [activeTab, setActiveTab] = useAtom(model.activeTabAtom);
const fullConfig = useAtomValue(atoms.fullConfigAtom);
const fullConfig = useAtomValue(env.atoms.fullConfigAtom);
const configErrors = fullConfig?.configerrors;

const handleContentChange = useCallback(
Expand Down
27 changes: 27 additions & 0 deletions frontend/app/view/waveconfig/waveconfigenv.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Copyright 2026, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0

import type { BlockMetaKeyAtomFnType, WaveEnv, WaveEnvSubset } from "@/app/waveenv/waveenv";

export type WaveConfigEnv = WaveEnvSubset<{
electron: {
getConfigDir: WaveEnv["electron"]["getConfigDir"];
getPlatform: WaveEnv["electron"]["getPlatform"];
};
rpc: {
FileInfoCommand: WaveEnv["rpc"]["FileInfoCommand"];
FileReadCommand: WaveEnv["rpc"]["FileReadCommand"];
FileWriteCommand: WaveEnv["rpc"]["FileWriteCommand"];
SetMetaCommand: WaveEnv["rpc"]["SetMetaCommand"];
GetSecretsLinuxStorageBackendCommand: WaveEnv["rpc"]["GetSecretsLinuxStorageBackendCommand"];
GetSecretsNamesCommand: WaveEnv["rpc"]["GetSecretsNamesCommand"];
GetSecretsCommand: WaveEnv["rpc"]["GetSecretsCommand"];
SetSecretsCommand: WaveEnv["rpc"]["SetSecretsCommand"];
RecordTEventCommand: WaveEnv["rpc"]["RecordTEventCommand"];
};
atoms: {
fullConfigAtom: WaveEnv["atoms"]["fullConfigAtom"];
};
getBlockMetaKeyAtom: BlockMetaKeyAtomFnType<"file">;
isWindows: WaveEnv["isWindows"];
}>;
Loading