Skip to content
Merged
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
34 changes: 31 additions & 3 deletions emain/emain-window.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@ import {
setWasInFg,
} from "./emain-activity";
import { log } from "./emain-log";
import { getElectronAppBasePath, unamePlatform } from "./emain-platform";
import { getElectronAppBasePath, isDev, unamePlatform } from "./emain-platform";
import { getOrCreateWebViewForTab, getWaveTabViewByWebContentsId, WaveTabView } from "./emain-tabview";
import { delay, ensureBoundsAreVisible, waveKeyToElectronKey } from "./emain-util";
import { ElectronWshClient } from "./emain-wsh";
import { updater } from "./updater";

const DevInitTimeoutMs = 5000;

export type WindowOpts = {
unamePlatform: NodeJS.Platform;
isPrimaryStartupWindow?: boolean;
Expand Down Expand Up @@ -389,7 +391,7 @@ export class WaveBrowserWindow extends BaseWindow {

private async initializeTab(tabView: WaveTabView, primaryStartupTab: boolean) {
const clientId = await getClientId();
await tabView.initPromise;
await this.awaitWithDevTimeout(tabView.initPromise, "initPromise", tabView.waveTabId);
this.contentView.addChildView(tabView);
const initOpts: WaveInitOpts = {
tabId: tabView.waveTabId,
Expand All @@ -410,10 +412,36 @@ export class WaveBrowserWindow extends BaseWindow {
primaryStartupTab ? "(primary startup)" : ""
);
tabView.webContents.send("wave-init", initOpts);
await tabView.waveReadyPromise;
await this.awaitWithDevTimeout(tabView.waveReadyPromise, "waveReadyPromise", tabView.waveTabId);
console.log("wave-ready init time", Date.now() - startTime + "ms");
}

private async awaitWithDevTimeout<T>(promise: Promise<T>, name: string, tabId: string): Promise<T> {
if (!isDev) {
return promise;
}
let timeoutHandle: ReturnType<typeof setTimeout> = null;
const timeoutPromise = new Promise<never>((_, reject) => {
timeoutHandle = setTimeout(() => {
console.log(
`[dev] ${name} timed out after ${DevInitTimeoutMs}ms for tab ${tabId}, showing window for devtools`
);
if (!this.isDestroyed() && !this.isVisible()) {
this.show();
}
if (this.activeTabView?.webContents && !this.activeTabView.webContents.isDevToolsOpened()) {
this.activeTabView.webContents.openDevTools();
}
reject(new Error(`[dev] ${name} timed out after ${DevInitTimeoutMs}ms`));
}, DevInitTimeoutMs);
});
try {
return await Promise.race([promise, timeoutPromise]);
} finally {
clearTimeout(timeoutHandle);
}
}

private async setTabViewIntoWindow(tabView: WaveTabView, tabInitialized: boolean, primaryStartupTab = false) {
if (this.activeTabView == tabView) {
return;
Expand Down
1 change: 0 additions & 1 deletion frontend/app/store/keymodel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -419,7 +419,6 @@ function appHandleKeyDown(waveEvent: WaveKeyboardEvent): boolean {
}
const nativeEvent = (waveEvent as any).nativeEvent;
if (lastHandledEvent != null && nativeEvent != null && lastHandledEvent === nativeEvent) {
console.log("lastHandledEvent return false");
return false;
}
lastHandledEvent = nativeEvent;
Expand Down
88 changes: 85 additions & 3 deletions frontend/app/view/term/termwrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
getOverrideConfigAtom,
getSettingsKeyAtom,
globalStore,
isDev,
openLink,
setTabIndicator,
WOS,
Expand Down Expand Up @@ -43,6 +44,7 @@ const TermCacheFileName = "cache:term:full";
const MinDataProcessedForCache = 100 * 1024;
export const SupportsImageInput = true;
const IMEDedupWindowMs = 20;
const MaxRepaintTransactionMs = 2000;

// detect webgl support
function detectWebGLSupport(): boolean {
Expand Down Expand Up @@ -104,9 +106,23 @@ export class TermWrap {
// xterm.js paste() method triggers onData event, which can cause duplicate sends
lastPasteData: string = "";
lastPasteTime: number = 0;

// for scrollToBottom support during a resize
lastAtBottomTime: number = Date.now();
lastScrollAtBottom: boolean = true;
cachedAtBottomForResize: boolean | null = null;
viewportScrollTop: number = 0;

// dev only (for debugging)
recentWrites: { idx: number; data: string; ts: number }[] = [];
recentWritesCounter: number = 0;

// for repaint transaction scrolling behavior
lastClearScrollbackTs: number = 0;
lastMode2026SetTs: number = 0;
lastMode2026ResetTs: number = 0;
inSyncTransaction: boolean = false;
inRepaintTransaction: boolean = false;

constructor(
tabId: string,
Expand Down Expand Up @@ -187,6 +203,44 @@ export class TermWrap {
this.terminal.parser.registerOscHandler(16162, (data: string) => {
return handleOsc16162Command(data, this.blockId, this.loaded, this);
});
this.toDispose.push(
this.terminal.parser.registerCsiHandler({ final: "J" }, (params) => {
if (params[0] === 3) {
this.lastClearScrollbackTs = Date.now();
if (this.inSyncTransaction) {
console.log("[termwrap] repaint transaction starting");
this.inRepaintTransaction = true;
}
}
return false;
})
);
this.toDispose.push(
this.terminal.parser.registerCsiHandler({ prefix: "?", final: "h" }, (params) => {
if (params[0] === 2026) {
this.lastMode2026SetTs = Date.now();
this.inSyncTransaction = true;
}
return false;
})
);
this.toDispose.push(
this.terminal.parser.registerCsiHandler({ prefix: "?", final: "l" }, (params) => {
if (params[0] === 2026) {
this.lastMode2026ResetTs = Date.now();
this.inSyncTransaction = false;
const wasRepaint = this.inRepaintTransaction;
this.inRepaintTransaction = false;
if (wasRepaint && Date.now() - this.lastClearScrollbackTs <= MaxRepaintTransactionMs) {
setTimeout(() => {
console.log("[termwrap] repaint transaction complete, scrolling to bottom");
this.terminal.scrollToBottom();
}, 20);
}
}
return false;
})
);
Comment on lines +206 to +243
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Gate instrumentation console.log calls behind isDev() to avoid production log spam.

Lines 211, 236, 584-591, and 601 add high-frequency logs on repaint/resize paths. These should be dev-gated like the rest of the debugging instrumentation.

Proposed fix
-                        console.log("[termwrap] repaint transaction starting");
+                        if (isDev()) {
+                            console.log("[termwrap] repaint transaction starting");
+                        }

...
-                            console.log("[termwrap] repaint transaction complete, scrolling to bottom");
+                            if (isDev()) {
+                                console.log("[termwrap] repaint transaction complete, scrolling to bottom");
+                            }
                             this.terminal.scrollToBottom();

...
-            console.log(
-                "[termwrap] resize",
-                `${oldRows}x${oldCols}`,
-                "->",
-                `${this.terminal.rows}x${this.terminal.cols}`,
-                "atBottom:",
-                atBottom
-            );
+            if (isDev()) {
+                console.log(
+                    "[termwrap] resize",
+                    `${oldRows}x${oldCols}`,
+                    "->",
+                    `${this.terminal.rows}x${this.terminal.cols}`,
+                    "atBottom:",
+                    atBottom
+                );
+            }

...
-                console.log("[termwrap] resize scroll-to-bottom");
+                if (isDev()) {
+                    console.log("[termwrap] resize scroll-to-bottom");
+                }

Also applies to: 584-602

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/app/view/term/termwrap.ts` around lines 206 - 243, Wrap the
high-frequency console.log calls in the CSI handlers so they only run in
development mode (use the existing isDev() guard used elsewhere); specifically,
gate the logs inside the handlers registered via
terminal.parser.registerCsiHandler (the handler for { final: "J" } that logs
"[termwrap] repaint transaction starting" and the delayed handler that logs
"[termwrap] repaint transaction complete, scrolling to bottom") so they only
execute when isDev() is true, leaving the logic that sets
this.lastClearScrollbackTs, this.inSyncTransaction, this.inRepaintTransaction,
this.lastMode2026SetTs, this.lastMode2026ResetTs and the call to
this.terminal.scrollToBottom() intact.

this.toDispose.push(
this.terminal.onBell(() => {
if (!this.loaded) {
Expand Down Expand Up @@ -231,9 +285,8 @@ export class TermWrap {
});
const viewportElem = this.connectElem.querySelector(".xterm-viewport") as HTMLElement;
if (viewportElem) {
const scrollHandler = () => {
const atBottom = viewportElem.scrollTop + viewportElem.clientHeight >= viewportElem.scrollHeight - 20;
this.setAtBottom(atBottom);
const scrollHandler = (e: any) => {
this.handleViewportScroll(viewportElem);
};
viewportElem.addEventListener("scroll", scrollHandler);
this.toDispose.push({
Expand Down Expand Up @@ -416,6 +469,13 @@ export class TermWrap {
}

doTerminalWrite(data: string | Uint8Array, setPtyOffset?: number): Promise<void> {
if (isDev() && this.loaded) {
const dataStr = data instanceof Uint8Array ? new TextDecoder().decode(data) : data;
this.recentWrites.push({ idx: this.recentWritesCounter++, ts: Date.now(), data: dataStr });
if (this.recentWrites.length > 50) {
this.recentWrites.shift();
}
}
let resolve: () => void = null;
let prtn = new Promise<void>((presolve, _) => {
resolve = presolve;
Expand Down Expand Up @@ -498,6 +558,19 @@ export class TermWrap {
return Date.now() - this.lastAtBottomTime <= 1000;
}

handleViewportScroll(viewportElem: HTMLElement) {
const { scrollTop, scrollHeight, clientHeight } = viewportElem;
const atBottom = scrollTop + clientHeight >= scrollHeight - clientHeight * 0.5;
this.setAtBottom(atBottom);
const delta = this.viewportScrollTop - scrollTop;
if (isDev() && delta >= 500) {
console.log(
`[termwrap] large-scroll blockId=${this.blockId} delta=${Math.round(delta)}px scrollTop=${scrollTop} wasNearBottom=${atBottom}`
);
}
this.viewportScrollTop = scrollTop;
}

handleResize() {
const oldRows = this.terminal.rows;
const oldCols = this.terminal.cols;
Expand All @@ -508,6 +581,14 @@ export class TermWrap {
this.fitAddon.fit();
if (oldRows !== this.terminal.rows || oldCols !== this.terminal.cols) {
const termSize: TermSize = { rows: this.terminal.rows, cols: this.terminal.cols };
console.log(
"[termwrap] resize",
`${oldRows}x${oldCols}`,
"->",
`${this.terminal.rows}x${this.terminal.cols}`,
"atBottom:",
atBottom
);
RpcApi.ControllerInputCommand(TabRpcClient, { blockid: this.blockId, termsize: termSize });
}
dlog("resize", `${this.terminal.rows}x${this.terminal.cols}`, `${oldRows}x${oldCols}`, this.hasResized);
Expand All @@ -517,6 +598,7 @@ export class TermWrap {
}
if (atBottom) {
setTimeout(() => {
console.log("[termwrap] resize scroll-to-bottom");
this.cachedAtBottomForResize = null;
this.terminal.scrollToBottom();
this.setAtBottom(true);
Expand Down
2 changes: 1 addition & 1 deletion pkg/blockcontroller/blockcontroller.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ const (
)

const (
DefaultTermMaxFileSize = 256 * 1024
DefaultTermMaxFileSize = 2 * 1024 * 1024
DefaultHtmlMaxFileSize = 256 * 1024
MaxInitScriptSize = 50 * 1024
)
Expand Down
10 changes: 5 additions & 5 deletions pkg/telemetry/telemetrydata/telemetrydata.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,11 +126,11 @@ type TEventProps struct {
WshCmd string `json:"wsh:cmd,omitempty"`
WshHadError bool `json:"wsh:haderror,omitempty"`

ConnType string `json:"conn:conntype,omitempty"`
ConnWshErrorCode string `json:"conn:wsherrorcode,omitempty"`
ConnErrorCode string `json:"conn:errorcode,omitempty"`
ConnSubErrorCode string `json:"conn:suberrorcode,omitempty"`
ConnContextError bool `json:"conn:contexterror,omitempty"`
ConnType string `json:"conn:conntype,omitempty"`
ConnWshErrorCode string `json:"conn:wsherrorcode,omitempty"`
ConnErrorCode string `json:"conn:errorcode,omitempty"`
ConnSubErrorCode string `json:"conn:suberrorcode,omitempty"`
ConnContextError bool `json:"conn:contexterror,omitempty"`

OnboardingFeature string `json:"onboarding:feature,omitempty" tstype:"\"waveai\" | \"durable\" | \"magnify\" | \"wsh\""`
OnboardingVersion string `json:"onboarding:version,omitempty"`
Expand Down
Loading