Skip to content
Merged
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
45 changes: 19 additions & 26 deletions frontend/app/view/term/termwrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ const TermFileName = "term";
const TermCacheFileName = "cache:term:full";
const MinDataProcessedForCache = 100 * 1024;
export const SupportsImageInput = true;
const IMEDedupWindowMs = 20;

// detect webgl support
function detectWebGLSupport(): boolean {
Expand Down Expand Up @@ -93,8 +94,6 @@ export class TermWrap {
onLinkHover?: (uri: string | null, mouseX: number, mouseY: number) => void;

// IME composition state tracking
// Prevents duplicate input when switching input methods during composition (e.g., using Capslock)
// xterm.js sends data during compositionupdate AND after compositionend, causing duplicates
isComposing: boolean = false;
composingData: string = "";
lastCompositionEnd: number = 0;
Expand Down Expand Up @@ -205,7 +204,15 @@ export class TermWrap {
return true;
})
);
this.terminal.attachCustomKeyEventHandler(waveOptions.keydownHandler);
this.terminal.attachCustomKeyEventHandler((e: KeyboardEvent) => {
if (e.isComposing && !e.ctrlKey && !e.altKey && !e.metaKey) {
return true;
}
if (!waveOptions.keydownHandler) {
return true;
}
return waveOptions.keydownHandler(e);
});
this.connectElem = connectElem;
this.mainFileSubject = null;
this.heldData = [];
Expand Down Expand Up @@ -236,6 +243,9 @@ export class TermWrap {
resetCompositionState() {
this.isComposing = false;
this.composingData = "";
this.lastComposedText = "";
this.lastCompositionEnd = 0;
this.firstDataAfterCompositionSent = false;
}

private handleCompositionStart = (e: CompositionEvent) => {
Expand Down Expand Up @@ -353,30 +363,13 @@ export class TermWrap {
return;
}

// IME Composition Handling
// Block all data during composition - only send the final text after compositionend
// This prevents xterm.js from sending intermediate composition data (e.g., during compositionupdate)
// IME fix: suppress isComposing=true events unless they immediately follow
// a compositionend (within 20ms). This handles CapsLock input method switching
// where the composition buffer gets flushed as a spurious isComposing=true event
if (this.isComposing) {
dlog("Blocked data during composition:", data);
return;
}

// IME Deduplication (for Capslock input method switching)
// When switching input methods with Capslock during composition, some systems send the
// composed text twice. We allow the first send and block subsequent duplicates.
const IMEDedupWindowMs = 50;
const now = Date.now();
const timeSinceCompositionEnd = now - this.lastCompositionEnd;
if (timeSinceCompositionEnd < IMEDedupWindowMs && data === this.lastComposedText && this.lastComposedText) {
if (!this.firstDataAfterCompositionSent) {
// First send after composition - allow it but mark as sent
this.firstDataAfterCompositionSent = true;
dlog("First data after composition, allowing:", data);
} else {
// Second send of the same data - this is a duplicate from Capslock switching, block it
dlog("Blocked duplicate IME data:", data);
this.lastComposedText = ""; // Clear to allow same text to be typed again later
this.firstDataAfterCompositionSent = false;
const timeSinceCompositionEnd = Date.now() - this.lastCompositionEnd;
if (timeSinceCompositionEnd > IMEDedupWindowMs) {
dlog("Suppressed IME data (composing, not near compositionend):", data);
return;
Comment on lines +366 to 373
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 | 🟠 Major

Avoid logging raw IME input payloads.

Line 372 logs user-typed text verbatim. Even under debug logging, this can expose sensitive data/PII. Log metadata only (length/timing/state).

🔒 Suggested safe logging change
-                dlog("Suppressed IME data (composing, not near compositionend):", data);
+                dlog("Suppressed IME data (composing, not near compositionend)", {
+                    length: data.length,
+                    timeSinceCompositionEnd,
+                });
🤖 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 366 - 373, The dlog call
inside the IME suppression branch is logging raw IME payloads via data; change
it to avoid exposing user text by logging only metadata (e.g., data length,
timeSinceCompositionEnd, this.isComposing state, and IMEDedupWindowMs) or a
masked representation instead of the verbatim data. Update the logging call in
the block where isComposing is checked (referencing isComposing,
lastCompositionEnd, IMEDedupWindowMs, and the dlog invocation) to include those
safe fields and remove the raw data value.

}
}
Expand Down
Loading