diff --git a/frontend/app/tab/tabbar.tsx b/frontend/app/tab/tabbar.tsx index a5cbff3398..9ce5016616 100644 --- a/frontend/app/tab/tabbar.tsx +++ b/frontend/app/tab/tabbar.tsx @@ -2,7 +2,6 @@ // SPDX-License-Identifier: Apache-2.0 import { Tooltip } from "@/app/element/tooltip"; -import { modalsModel } from "@/app/store/modalmodel"; import { TabRpcClient } from "@/app/store/wshrpcutil"; import { useWaveEnv } from "@/app/waveenv/waveenv"; import { WorkspaceLayoutModel } from "@/app/workspace/workspace-layout-model"; @@ -71,68 +70,6 @@ const WaveAIButton = memo(({ divRef }: { divRef?: React.RefObject { - const env = useWaveEnv(); - const fullConfig = useAtomValue(env.atoms.fullConfigAtom); - - if (fullConfig?.configerrors == null || fullConfig?.configerrors.length == 0) { - return ( -
-

Configuration Clean

-

There are no longer any errors detected in your config.

-
- ); - } - if (fullConfig?.configerrors.length == 1) { - const singleError = fullConfig.configerrors[0]; - return ( -
-

Configuration Error

-
- {singleError.file}: {singleError.err} -
-
- ); - } - return ( -
-

Configuration Error

-
    - {fullConfig.configerrors.map((error, index) => ( -
  • - {error.file}: {error.err} -
  • - ))} -
-
- ); -}; - -const ConfigErrorIcon = () => { - const env = useWaveEnv(); - const hasConfigErrors = useAtomValue(env.atoms.hasConfigErrors); - - const handleClick = useCallback(() => { - modalsModel.pushModal("MessageModal", { children: }); - }, []); - - if (!hasConfigErrors) { - return null; - } - return ( - - - - ); -}; - function strArrayIsEqual(a: string[], b: string[]) { // null check if (a == null && b == null) { @@ -192,7 +129,6 @@ const TabBar = memo(({ workspace }: TabBarProps) => { const confirmClose = useAtomValue(env.getSettingsKeyAtom("tab:confirmclose")) ?? false; const hideAiButton = useAtomValue(env.getSettingsKeyAtom("app:hideaibutton")); const appUpdateStatus = useAtomValue(env.atoms.updaterStatusAtom); - const hasConfigErrors = useAtomValue(env.atoms.hasConfigErrors); let prevDelta: number; let prevDragDirection: string; @@ -330,7 +266,7 @@ const TabBar = memo(({ workspace }: TabBarProps) => { }; }, [handleResizeTabs]); - // update layout on changed tabIds, tabsLoaded, newTabId, hideAiButton, appUpdateStatus, hasConfigErrors, or zoomFactor + // update layout on changed tabIds, tabsLoaded, newTabId, hideAiButton, appUpdateStatus, or zoomFactor useEffect(() => { // Check if all tabs are loaded const allLoaded = tabIds.length > 0 && tabIds.every((id) => tabsLoaded[id]); @@ -348,7 +284,6 @@ const TabBar = memo(({ workspace }: TabBarProps) => { saveTabsPosition, hideAiButton, appUpdateStatus, - hasConfigErrors, zoomFactor, showMenuBar, ]); @@ -715,7 +650,6 @@ const TabBar = memo(({ workspace }: TabBarProps) => {
-
{ ); }); -export { ConfigErrorIcon, ConfigErrorMessage, TabBar, WaveAIButton }; +export { TabBar, WaveAIButton }; diff --git a/frontend/app/view/waveconfig/waveconfig-model.ts b/frontend/app/view/waveconfig/waveconfig-model.ts index f41a39eccd..94cd114440 100644 --- a/frontend/app/view/waveconfig/waveconfig-model.ts +++ b/frontend/app/view/waveconfig/waveconfig-model.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { BlockNodeModel } from "@/app/block/blocktypes"; -import { getApi, getBlockMetaKeyAtom, WOS } from "@/app/store/global"; +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"; @@ -11,7 +11,7 @@ import { SecretsContent } from "@/app/view/waveconfig/secretscontent"; import { WaveConfigView } from "@/app/view/waveconfig/waveconfig"; import { isWindows } from "@/util/platformutil"; import { base64ToString, stringToBase64 } from "@/util/util"; -import { atom, type PrimitiveAtom } from "jotai"; +import { atom, type Atom, type PrimitiveAtom } from "jotai"; import type * as MonacoTypes from "monaco-editor"; import * as React from "react"; @@ -156,6 +156,7 @@ export class WaveConfigViewModel implements ViewModel { isMenuOpenAtom: PrimitiveAtom; presetsJsonExistsAtom: PrimitiveAtom; activeTabAtom: PrimitiveAtom<"visual" | "json">; + configErrorFilesAtom: Atom>; configDir: string; saveShortcut: string; editorRef: React.RefObject; @@ -189,6 +190,14 @@ export class WaveConfigViewModel implements ViewModel { this.isMenuOpenAtom = atom(false); this.presetsJsonExistsAtom = atom(false); this.activeTabAtom = atom<"visual" | "json">("visual"); + this.configErrorFilesAtom = atom((get) => { + const fullConfig = get(atoms.fullConfigAtom); + const errorSet = new Set(); + for (const cerr of fullConfig?.configerrors ?? []) { + errorSet.add(cerr.file); + } + return errorSet; + }); this.editorRef = React.createRef(); this.secretNamesAtom = atom([]); diff --git a/frontend/app/view/waveconfig/waveconfig.tsx b/frontend/app/view/waveconfig/waveconfig.tsx index 4e515466f4..00747ff3c1 100644 --- a/frontend/app/view/waveconfig/waveconfig.tsx +++ b/frontend/app/view/waveconfig/waveconfig.tsx @@ -2,6 +2,7 @@ // 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"; @@ -21,6 +22,7 @@ const ConfigSidebar = memo(({ model }: ConfigSidebarProps) => { const [isMenuOpen, setIsMenuOpen] = useAtom(model.isMenuOpenAtom); const configFiles = model.getConfigFiles(); const deprecatedConfigFiles = model.getDeprecatedConfigFiles(); + const configErrorFiles = useAtomValue(model.configErrorFilesAtom); const handleFileSelect = (file: ConfigFile) => { model.loadFile(file); @@ -46,7 +48,12 @@ const ConfigSidebar = memo(({ model }: ConfigSidebarProps) => { selectedFile?.path === file.path ? "bg-accentbg text-primary" : "hover:bg-secondary/50" }`} > -
{file.name}
+
+
{file.name}
+ {configErrorFiles.has(file.path) && ( + + )} +
{file.description && (
{file.description} @@ -75,6 +82,9 @@ const ConfigSidebar = memo(({ model }: ConfigSidebarProps) => { > deprecated + {configErrorFiles.has(file.path) && ( + + )}
))} @@ -96,6 +106,8 @@ const WaveConfigView = memo(({ blockId, model }: ViewComponentProps { @@ -148,7 +160,8 @@ const WaveConfigView = memo(({ blockId, model }: ViewComponentProps +
+
{isMenuOpen && (
setIsMenuOpen(false)} /> )} @@ -284,6 +297,17 @@ const WaveConfigView = memo(({ blockId, model }: ViewComponentProps )}
+
+ {configErrors?.length > 0 && ( +
+ {configErrors.map((cerr, i) => ( +
+ Config Error: + {cerr.file}: {cerr.err} +
+ ))} +
+ )}
); }); diff --git a/frontend/app/workspace/widgets.tsx b/frontend/app/workspace/widgets.tsx index 2ec171953e..f3043e6d98 100644 --- a/frontend/app/workspace/widgets.tsx +++ b/frontend/app/workspace/widgets.tsx @@ -1,4 +1,4 @@ -// Copyright 2025, Command Line Inc. +// Copyright 2026, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 import { Tooltip } from "@/app/element/tooltip"; @@ -30,6 +30,7 @@ export type WidgetsEnv = WaveEnvSubset<{ }; atoms: { fullConfigAtom: WaveEnv["atoms"]["fullConfigAtom"]; + hasConfigErrors: WaveEnv["atoms"]["hasConfigErrors"]; workspaceId: WaveEnv["atoms"]["workspaceId"]; hasCustomAIPresetsAtom: WaveEnv["atoms"]["hasCustomAIPresetsAtom"]; }; @@ -107,10 +108,26 @@ function calculateGridSize(appCount: number): number { return 6; } +function SettingsTooltipContent({ hasConfigErrors }: { hasConfigErrors: boolean }) { + if (!hasConfigErrors) { + return "Settings & Help"; + } + return ( +
+
Settings & Help
+
+ + Config Errors +
+
+ ); +} + type FloatingWindowPropsType = { isOpen: boolean; onClose: () => void; referenceElement: HTMLElement; + hasConfigErrors?: boolean; }; const AppsFloatingWindow = memo(({ isOpen, onClose, referenceElement }: FloatingWindowPropsType) => { @@ -236,118 +253,125 @@ const AppsFloatingWindow = memo(({ isOpen, onClose, referenceElement }: Floating ); }); -const SettingsFloatingWindow = memo(({ isOpen, onClose, referenceElement }: FloatingWindowPropsType) => { - const env = useWaveEnv(); - const { refs, floatingStyles, context } = useFloating({ - open: isOpen, - onOpenChange: onClose, - placement: "left-start", - middleware: [offset(-2), shift({ padding: 12 })], - whileElementsMounted: autoUpdate, - elements: { - reference: referenceElement, - }, - }); +const SettingsFloatingWindow = memo( + ({ isOpen, onClose, referenceElement, hasConfigErrors }: FloatingWindowPropsType) => { + const env = useWaveEnv(); + const { refs, floatingStyles, context } = useFloating({ + open: isOpen, + onOpenChange: onClose, + placement: "left-start", + middleware: [offset(-2), shift({ padding: 12 })], + whileElementsMounted: autoUpdate, + elements: { + reference: referenceElement, + }, + }); - const dismiss = useDismiss(context); - const { getFloatingProps } = useInteractions([dismiss]); + const dismiss = useDismiss(context); + const { getFloatingProps } = useInteractions([dismiss]); - if (!isOpen) return null; + if (!isOpen) return null; - const menuItems = [ - { - icon: "gear", - label: "Settings", - onClick: () => { - const blockDef: BlockDef = { - meta: { - view: "waveconfig", - }, - }; - env.createBlock(blockDef, false, true); - onClose(); + const menuItems = [ + { + icon: "gear", + label: "Settings", + hasError: hasConfigErrors, + onClick: () => { + const blockDef: BlockDef = { + meta: { + view: "waveconfig", + }, + }; + env.createBlock(blockDef, false, true); + onClose(); + }, }, - }, - { - icon: "lightbulb", - label: "Tips", - onClick: () => { - const blockDef: BlockDef = { - meta: { - view: "tips", - }, - }; - env.createBlock(blockDef, true, true); - onClose(); + { + icon: "lightbulb", + label: "Tips", + onClick: () => { + const blockDef: BlockDef = { + meta: { + view: "tips", + }, + }; + env.createBlock(blockDef, true, true); + onClose(); + }, }, - }, - { - icon: "lock", - label: "Secrets", - onClick: () => { - const blockDef: BlockDef = { - meta: { - view: "waveconfig", - file: "secrets", - }, - }; - env.createBlock(blockDef, false, true); - onClose(); + { + icon: "lock", + label: "Secrets", + onClick: () => { + const blockDef: BlockDef = { + meta: { + view: "waveconfig", + file: "secrets", + }, + }; + env.createBlock(blockDef, false, true); + onClose(); + }, }, - }, - { - icon: "book-open", - label: "Release Notes", - onClick: () => { - modalsModel.pushModal("UpgradeOnboardingPatch", { isReleaseNotes: true }); - onClose(); + { + icon: "book-open", + label: "Release Notes", + onClick: () => { + modalsModel.pushModal("UpgradeOnboardingPatch", { isReleaseNotes: true }); + onClose(); + }, }, - }, - { - icon: "circle-question", - label: "Help", - onClick: () => { - const blockDef: BlockDef = { - meta: { - view: "help", - }, - }; - env.createBlock(blockDef); - onClose(); + { + icon: "circle-question", + label: "Help", + onClick: () => { + const blockDef: BlockDef = { + meta: { + view: "help", + }, + }; + env.createBlock(blockDef); + onClose(); + }, }, - }, - ]; + ]; - return ( - -
- {menuItems.map((item, idx) => ( -
-
- + return ( + +
+ {menuItems.map((item, idx) => ( +
+
+ +
+
{item.label}
+ {item.hasError && ( + + )}
-
{item.label}
-
- ))} -
- - ); -}); + ))} +
+ + ); + } +); SettingsFloatingWindow.displayName = "SettingsFloatingWindow"; const Widgets = memo(() => { const env = useWaveEnv(); const fullConfig = useAtomValue(env.atoms.fullConfigAtom); + const hasConfigErrors = useAtomValue(env.atoms.hasConfigErrors); const workspaceId = useAtomValue(env.atoms.workspaceId); const hasCustomAIPresets = useAtomValue(env.atoms.hasCustomAIPresetsAtom); const [mode, setMode] = useState<"normal" | "compact" | "supercompact">("normal"); @@ -471,9 +495,16 @@ const Widgets = memo(() => { className="flex flex-col justify-center items-center w-full py-1.5 pr-0.5 text-secondary text-sm overflow-hidden rounded-sm hover:bg-hoverbg hover:text-white cursor-pointer" onClick={() => setIsSettingsOpen(!isSettingsOpen)} > - -
+ } + placement="left" + disable={isSettingsOpen} + > +
+ {hasConfigErrors && ( + + )}
@@ -510,9 +541,25 @@ const Widgets = memo(() => { className="flex flex-col justify-center items-center w-full py-1.5 pr-0.5 text-secondary text-lg overflow-hidden rounded-sm hover:bg-hoverbg hover:text-white cursor-pointer" onClick={() => setIsSettingsOpen(!isSettingsOpen)} > - -
- + } + placement="left" + disable={isSettingsOpen} + > +
+
+ + {hasConfigErrors && ( + + )} +
+ {mode === "normal" && ( +
+ settings +
+ )}
@@ -539,6 +586,7 @@ const Widgets = memo(() => { isOpen={isSettingsOpen} onClose={() => setIsSettingsOpen(false)} referenceElement={settingsButtonRef.current} + hasConfigErrors={hasConfigErrors} /> )} diff --git a/frontend/preview/previews/widgets.preview.tsx b/frontend/preview/previews/widgets.preview.tsx index 144cace174..440ae03a6a 100644 --- a/frontend/preview/previews/widgets.preview.tsx +++ b/frontend/preview/previews/widgets.preview.tsx @@ -3,11 +3,14 @@ import { useWaveEnv, WaveEnv, WaveEnvContext } from "@/app/waveenv/waveenv"; import { Widgets } from "@/app/workspace/widgets"; -import { atom, useAtom } from "jotai"; +import { atom, useAtom, useAtomValue } from "jotai"; import { useRef } from "react"; import { applyMockEnvOverrides } from "../mock/mockwaveenv"; const resizableHeightAtom = atom(250); +const hasConfigErrorsAtom = atom(false); +const isDevAtom = atom(true); +const mockVersionAtom = atom(0); function makeMockApp(name: string, icon: string, iconcolor: string): AppInfo { return { @@ -84,13 +87,20 @@ const mockWidgets: { [key: string]: WidgetConfigType } = { const fullConfigAtom = atom({ settings: {}, widgets: mockWidgets } as unknown as FullConfigType); -function makeWidgetsEnv(baseEnv: WaveEnv, isDev: boolean, hasCustomAIPresets: boolean, apps?: AppInfo[]) { +function makeWidgetsEnv( + baseEnv: WaveEnv, + isDev: boolean, + hasCustomAIPresets: boolean, + apps?: AppInfo[], + atomOverrides?: Partial +) { return applyMockEnvOverrides(baseEnv, { isDev, rpc: { ListAllAppsCommand: () => Promise.resolve(apps ?? []) }, atoms: { fullConfigAtom, hasCustomAIPresetsAtom: atom(hasCustomAIPresets), + ...atomOverrides, }, }); } @@ -111,7 +121,9 @@ function WidgetsScenario({ const baseEnv = useWaveEnv(); const envRef = useRef(null); if (envRef.current == null) { - envRef.current = makeWidgetsEnv(baseEnv, isDev, hasCustomAIPresets, apps); + envRef.current = makeWidgetsEnv(baseEnv, isDev, hasCustomAIPresets, apps, { + hasConfigErrors: hasConfigErrorsAtom, + }); } return ( @@ -132,18 +144,18 @@ function WidgetsScenario({ ); } -function WidgetsResizable() { +function WidgetsResizable({ isDev }: { isDev: boolean }) { const [height, setHeight] = useAtom(resizableHeightAtom); const baseEnv = useWaveEnv(); const envRef = useRef(null); if (envRef.current == null) { - envRef.current = makeWidgetsEnv(baseEnv, true, true, mockApps); + envRef.current = makeWidgetsEnv(baseEnv, isDev, true, mockApps, { hasConfigErrors: hasConfigErrorsAtom }); } return (
- compact/supercompact — resizable (dev mode, height: {height}px) + compact/supercompact — resizable (height: {height}px) void) { + fn(); + setMockVersion((v) => v + 1); + } + + return ( +
+ preview controls: + + +
+ ); +} + export function WidgetsPreview() { + const isDev = useAtomValue(isDevAtom); + const mockVersion = useAtomValue(mockVersionAtom); + return (
-
- - - - + +
+
+ + + + +
+
-
); }