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
1 change: 1 addition & 0 deletions docs/docs/config.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ wsh editconfig
| editor:inlinediff | bool | set to true to show diffs inline instead of side-by-side, false for side-by-side (defaults to undefined which uses Monaco's responsive behavior) |
| preview:showhiddenfiles | bool | set to false to disable showing hidden files in the directory preview (defaults to true) |
| preview:defaultsort <VersionBadge version="v0.14.2" /> | string | sets the default sort column for directory preview. `"name"` (default) sorts alphabetically by name ascending; `"modtime"` sorts by last modified time descending (newest first) |
| notes:path <VersionBadge version="v0.14.6" /> | string | path to the notes markdown file (defaults to `~/notes.md`) |
| markdown:fontsize | float64 | font size for the normal text when rendering markdown in preview. headers are scaled up from this size, (default 14px) |
| markdown:fixedfontsize | float64 | font size for the code blocks when rendering markdown in preview (default is 12px) |
| web:openlinksinternally | bool | set to false to open web links in external browser |
Expand Down
2 changes: 2 additions & 0 deletions frontend/app/block/blockregistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { QuickTipsViewModel } from "../view/quicktipsview/quicktipsview";
import { WaveConfigViewModel } from "../view/waveconfig/waveconfig-model";
import { blockViewToIcon, blockViewToName } from "./blockutil";
import { HelpViewModel } from "@/view/helpview/helpview";
import { NotesViewModel } from "@/app/view/notes/notes-model";
import { TermViewModel } from "@/view/term/term-model";
import { WaveAiModel } from "@/view/waveai/waveai";
import { WebViewModel } from "@/view/webview/webview";
Expand All @@ -35,6 +36,7 @@ BlockRegistry.set("tsunami", TsunamiViewModel);
BlockRegistry.set("aifilediff", AiFileDiffViewModel);
BlockRegistry.set("waveconfig", WaveConfigViewModel);
BlockRegistry.set("processviewer", ProcessViewerViewModel);
BlockRegistry.set("notes", NotesViewModel);

function makeDefaultViewModel(viewType: string): ViewModel {
const viewModel: ViewModel = {
Expand Down
110 changes: 110 additions & 0 deletions frontend/app/modals/alertmodal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// Copyright 2026, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0

import { modalsModel } from "@/app/store/modalmodel";
import { makeIconClass } from "@/util/util";
import { ReactNode, useEffect } from "react";
import ReactDOM from "react-dom";

interface AlertModalProps {
children: ReactNode;
title?: string;
icon?: string;
iconClassName?: string;
okLabel?: string;
onClose?: () => void;
}

interface AlertOptions {
title?: string;
message: ReactNode;
icon?: string;
iconClassName?: string;
okLabel?: string;
}

function showAlert(opts: AlertOptions) {
modalsModel.pushModal("AlertModal", {
children: opts.message,
title: opts.title ?? "Alert",
icon: opts.icon,
iconClassName: opts.iconClassName,
okLabel: opts.okLabel,
});
}

function showErrorAlert(message: ReactNode, title?: string) {
showAlert({ title: title ?? "Error", message, icon: "circle-exclamation", iconClassName: "text-error" });
}

const AlertModal = ({ children, title = "Alert", icon = "circle-info", iconClassName, okLabel = "Ok", onClose }: AlertModalProps) => {
function close() {
if (onClose) {
onClose();
} else {
modalsModel.popModal();
}
}

useEffect(() => {
function handleKeyDown(e: KeyboardEvent) {
if (e.key === "Escape" || e.key === "Enter") {
e.preventDefault();
e.stopPropagation();
close();
}
}
window.addEventListener("keydown", handleKeyDown, true);
return () => window.removeEventListener("keydown", handleKeyDown, true);
}, []);

const iconClass = makeIconClass(icon, false);

return ReactDOM.createPortal(
<div
Comment on lines +63 to +64
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Portal targets using `#main` =="
rg -n --type=ts --type=tsx 'createPortal\(|getElementById\("main"\)' frontend

echo "== Definitions of id=\"main\" in frontend markup =="
rg -n 'id=["'"'"']main["'"'"']' frontend

echo "== Preview render containers (sanity check) =="
rg -n --iglob '*preview*' 'id=["'"'"']main["'"'"']|createRoot\(|render\(' frontend/preview

Repository: wavetermdev/waveterm

Length of output: 143


🏁 Script executed:

# Check if the file exists and read its content
head -n 120 frontend/app/modals/alertmodal.tsx | tail -n +55

Repository: wavetermdev/waveterm

Length of output: 2464


🏁 Script executed:

# Search for `#main` element definitions without file type filter
rg 'id=["\047\042]main' frontend --type html

Repository: wavetermdev/waveterm

Length of output: 255


🏁 Script executed:

# Look for other portal usages
rg 'createPortal' frontend -A 2 -B 1

Repository: wavetermdev/waveterm

Length of output: 1262


🏁 Script executed:

# Check preview/test environment setup for main element
fd -e html frontend/preview/ frontend/test/ 2>/dev/null | head -20

Repository: wavetermdev/waveterm

Length of output: 46


🏁 Script executed:

# Search for "main" element definition - try different approaches
rg 'id.*main' frontend --type html -B 1 -A 1

Repository: wavetermdev/waveterm

Length of output: 322


🏁 Script executed:

# Check for main element in tsx/ts files where it might be created
rg 'id=.*main' frontend -B 1 -A 1 --type ts --type tsx | head -30

Repository: wavetermdev/waveterm

Length of output: 92


🏁 Script executed:

# Search for app root or main container setup
rg 'createRoot|render.*main|getElementById.*main' frontend -B 2 -A 2

Repository: wavetermdev/waveterm

Length of output: 3272


🏁 Script executed:

# Look at the main entry point
find frontend -name "index.html" -o -name "main.tsx" -o -name "main.ts" -o -name "app.tsx" | head -10

Repository: wavetermdev/waveterm

Length of output: 113


🏁 Script executed:

# Check if there's a main index.html at root or in build config
find frontend -maxdepth 2 -name "index.html" -o -name "tsconfig.json" | grep -E "(root|tsconfig)"

Repository: wavetermdev/waveterm

Length of output: 46


🏁 Script executed:

# Check what happens in wave.ts if getElementById("main") returns null
cat -n frontend/wave.ts | grep -A 3 -B 3 'getElementById.*main'

Repository: wavetermdev/waveterm

Length of output: 742


🏁 Script executed:

# Verify the exact line numbers in alertmodal.tsx where the createPortal call is
wc -l frontend/app/modals/alertmodal.tsx

Repository: wavetermdev/waveterm

Length of output: 103


🏁 Script executed:

# Get exact content around alertmodal line 103-104
sed -n '100,110p' frontend/app/modals/alertmodal.tsx

Repository: wavetermdev/waveterm

Length of output: 293


Use non-null assertion for consistency with modal.tsx pattern.

While document.getElementById("main") is guaranteed to exist at this point (the app initialization in wave.ts already depends on it), use the non-null assertion ! for consistency with the codebase pattern seen in modal.tsx rather than adding a runtime guard.

-        document.getElementById("main")
+        document.getElementById("main")!

The same applies at lines 103-104.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/app/modals/alertmodal.tsx` around lines 63 - 64, The createPortal
calls in AlertModal use document.getElementById("main") without the project's
non-null assertion pattern; update both occurrences (the one in the return
ReactDOM.createPortal(...) and the later createPortal at lines around 103-104)
to append a non-null assertion (!) to document.getElementById("main") so it
matches how modal.tsx handles the root element and avoids adding a runtime
guard.

className="fixed inset-0 flex items-center justify-center"
style={{ zIndex: "var(--zindex-modal-wrapper)" }}
>
Comment on lines +64 to +67
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add dialog semantics and keyboard-accessible close control.

The modal container is missing role="dialog" / aria-modal, and the close button is removed from tab order (tabIndex={-1}), which hurts accessibility.

💡 Proposed fix
-        <div
+        <div
             className="fixed inset-0 flex items-center justify-center"
             style={{ zIndex: "var(--zindex-modal-wrapper)" }}
+            role="dialog"
+            aria-modal="true"
+            aria-label={title}
         >
@@
-                    <button
+                    <button
                         className="text-muted hover:text-primary transition-colors cursor-pointer p-0.5"
                         onClick={close}
-                        tabIndex={-1}
+                        aria-label="Close alert"
                     >

Also applies to: 80-90

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/app/modals/alertmodal.tsx` around lines 64 - 67, The modal wrapper
in the AlertModal component is missing dialog semantics and the close control is
not keyboard-accessible; update the modal container element (the div currently
rendered as the modal wrapper) to include role="dialog", aria-modal="true" and
an aria-labelledby (pointing to the modal title element id) and/or
aria-describedby as appropriate, remove tabIndex={-1} from the close button so
it is focusable, ensure the close button is a native <button> (or has keyboard
handlers for Enter/Space) and add an Escape key handler on the modal (e.g.,
onKeyDown on the wrapper) that invokes the existing close function to allow
keyboard dismissal.

<div
className="fixed inset-0"
style={{ zIndex: "var(--zindex-modal-backdrop)", backgroundColor: "rgba(21,23,21,0.7)", top: "36px" }}
/>
<div
className="relative flex flex-col rounded-lg shadow-[0px_8px_32px_rgba(0,0,0,0.4)] min-w-[380px] max-w-[560px]"
style={{
zIndex: "var(--zindex-modal)",
background: "var(--modal-bg-color)",
border: "0.5px solid var(--modal-border-color)",
}}
>
<div className="flex items-center gap-3 px-4 py-2.5 border-b border-white/10">
<i className={`${iconClass} text-base ${iconClassName ?? "text-warning"}`} />
<span className="text-sm font-semibold text-primary flex-1">{title}</span>
<button
className="text-muted hover:text-primary transition-colors cursor-pointer p-0.5"
onClick={close}
tabIndex={-1}
>
<i className="fa-sharp fa-solid fa-xmark text-sm" />
</button>
</div>
<div className="px-5 pt-3 pb-2 text-sm text-secondary leading-relaxed">{children}</div>
<div className="flex justify-end px-5 pb-3 pt-1">
<button
className="bg-white/10 text-primary rounded px-5 py-1.5 text-sm font-medium hover:bg-white/20 transition-colors cursor-pointer outline-none"
onClick={close}
autoFocus
>
{okLabel}
</button>
</div>
</div>
</div>,
document.getElementById("main")
);
};

AlertModal.displayName = "AlertModal";

export { AlertModal, showAlert, showErrorAlert };
export type { AlertOptions };
4 changes: 2 additions & 2 deletions frontend/app/modals/modalregistry.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0

import { MessageModal } from "@/app/modals/messagemodal";
import { AlertModal } from "@/app/modals/alertmodal";
import { NewInstallOnboardingModal } from "@/app/onboarding/onboarding";
import { UpgradeOnboardingModal } from "@/app/onboarding/onboarding-upgrade";
import { UpgradeOnboardingPatch } from "@/app/onboarding/onboarding-upgrade-patch";
Expand All @@ -16,7 +16,7 @@ const modalRegistry: { [key: string]: React.ComponentType<any> } = {
[UpgradeOnboardingPatch.displayName || "UpgradeOnboardingPatch"]: UpgradeOnboardingPatch,
[UserInputModal.displayName || "UserInputModal"]: UserInputModal,
[AboutModal.displayName || "AboutModal"]: AboutModal,
[MessageModal.displayName || "MessageModal"]: MessageModal,
[AlertModal.displayName || "AlertModal"]: AlertModal,
[PublishAppModal.displayName || "PublishAppModal"]: PublishAppModal,
[RenameFileModal.displayName || "RenameFileModal"]: RenameFileModal,
[DeleteFileModal.displayName || "DeleteFileModal"]: DeleteFileModal,
Expand Down
12 changes: 12 additions & 0 deletions frontend/app/store/wshclientapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,12 @@ export class RpcApiType {
return client.wshRpcCall("getmeta", data, opts);
}

// command "getnote" [call]
GetNoteCommand(client: WshClient, opts?: RpcOpts): Promise<NoteData> {
if (this.mockClient) return this.mockClient.mockWshRpcCall(client, "getnote", null, opts);
return client.wshRpcCall("getnote", null, opts);
}

// command "getrtinfo" [call]
GetRTInfoCommand(client: WshClient, data: CommandGetRTInfoData, opts?: RpcOpts): Promise<ObjRTInfo> {
if (this.mockClient) return this.mockClient.mockWshRpcCall(client, "getrtinfo", data, opts);
Expand Down Expand Up @@ -1050,6 +1056,12 @@ export class RpcApiType {
return client.wshRpcCall("writeappsecretbindings", data, opts);
}

// command "writenote" [call]
WriteNoteCommand(client: WshClient, data: CommandWriteNoteData, opts?: RpcOpts): Promise<void> {
if (this.mockClient) return this.mockClient.mockWshRpcCall(client, "writenote", data, opts);
return client.wshRpcCall("writenote", data, opts);
}

// command "writetempfile" [call]
WriteTempFileCommand(client: WshClient, data: CommandWriteTempFileData, opts?: RpcOpts): Promise<string> {
if (this.mockClient) return this.mockClient.mockWshRpcCall(client, "writetempfile", data, opts);
Expand Down
Loading
Loading