Skip to content

Commit d886b5e

Browse files
authored
more terminal integration for wave ai (#2470)
- mark last command / prompts in xterm.js - split out term model into its own file - try to detect repl/shells/ssh/tmux etc commands that stop wave ai integration - show icons in term headers for whether wave ai integration is available - keep integration status / last command client side (sync with server on reload)
1 parent edacd65 commit d886b5e

File tree

14 files changed

+1244
-823
lines changed

14 files changed

+1244
-823
lines changed

frontend/app/block/block.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import { getWaveObjectAtom, makeORef, useWaveObjectValue } from "@/store/wos";
2727
import { focusedBlockId, getElemAsStr } from "@/util/focusutil";
2828
import { isBlank, useAtomValueSafe } from "@/util/util";
2929
import { HelpViewModel } from "@/view/helpview/helpview";
30-
import { TermViewModel } from "@/view/term/term";
30+
import { TermViewModel } from "@/view/term/term-model";
3131
import { WaveAiModel } from "@/view/waveai/waveai";
3232
import { WebViewModel } from "@/view/webview/webview";
3333
import clsx from "clsx";

frontend/app/element/iconbutton.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { useLongClick } from "@/app/hook/useLongClick";
55
import { makeIconClass } from "@/util/util";
66
import clsx from "clsx";
77
import { atom, useAtom } from "jotai";
8-
import { forwardRef, memo, useMemo, useRef } from "react";
8+
import { CSSProperties, forwardRef, memo, useMemo, useRef } from "react";
99
import "./iconbutton.scss";
1010

1111
type IconButtonProps = { decl: IconButtonDecl; className?: string };
@@ -15,6 +15,10 @@ export const IconButton = memo(
1515
const spin = decl.iconSpin ?? false;
1616
useLongClick(ref, decl.click, decl.longClick, decl.disabled);
1717
const disabled = decl.disabled ?? false;
18+
const styleVal: CSSProperties = {};
19+
if (decl.iconColor) {
20+
styleVal.color = decl.iconColor;
21+
}
1822
return (
1923
<button
2024
ref={ref}
@@ -24,7 +28,7 @@ export const IconButton = memo(
2428
})}
2529
title={decl.title}
2630
aria-label={decl.title}
27-
style={{ color: decl.iconColor ?? "inherit" }}
31+
style={styleVal}
2832
disabled={disabled}
2933
>
3034
{typeof decl.icon === "string" ? <i className={makeIconClass(decl.icon, true, { spin })} /> : decl.icon}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
// Copyright 2025, Command Line Inc.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
// Always block (TUIs / pagers / multiplexers / known interactive UIs)
5+
const ALWAYS_BLOCK = [
6+
// multiplexers
7+
"tmux", "screen", "byobu", "dtach", "abduco", "tmate",
8+
// editors/pagers
9+
"vim", "nvim", "emacs", "nano", "less", "more", "man", "most", "view",
10+
// TUIs / tools
11+
"htop", "top", "btop", "fzf", "ranger", "mc", "nnn", "k9s", "nmtui", "alsamixer",
12+
"tig", "gdb", "lldb",
13+
// mail/irc
14+
"mutt", "neomutt", "alpine", "weechat", "irssi",
15+
// dialog UIs
16+
"dialog", "whiptail",
17+
// DB shells
18+
"psql", "mysql", "sqlite3", "mongo", "redis-cli",
19+
];
20+
21+
// Bare REPLs only block when no args
22+
const BARE_REPLS = [
23+
"python", "python3", "python2", "node", "ruby", "perl", "php", "lua", "ipython", "bpython", "irb",
24+
];
25+
26+
// Shells: block only if interactive/new shell
27+
const SHELLS = [
28+
"bash", "sh", "zsh", "fish", "ksh", "mksh", "dash", "ash", "tcsh", "csh",
29+
"xonsh", "elvish", "nu", "nushell", "pwsh", "powershell", "cmd",
30+
];
31+
32+
// Wrappers to skip
33+
const WRAPPERS = [
34+
"sudo", "doas", "pkexec", "rlwrap", "env", "time", "nice", "nohup",
35+
"chrt", "stdbuf", "script", "scriptreplay", "sshpass",
36+
];
37+
38+
function looksInteractiveShellArgs(args: string[]): boolean {
39+
return (
40+
args.length === 0 ||
41+
args.includes("-i") ||
42+
args.includes("--login") ||
43+
args.includes("-l") ||
44+
args.includes("-s")
45+
);
46+
}
47+
48+
function isNonInteractiveShellExec(args: string[]): boolean {
49+
return (
50+
args.includes("-c") ||
51+
args.some((a) => a === "-Command" || a.startsWith("-Command")) ||
52+
args.some((a) => a.endsWith(".sh") || a.includes("/"))
53+
);
54+
}
55+
56+
function isAttachLike(cmd: string, args: string[]): boolean {
57+
if (cmd === "docker" || cmd === "podman") {
58+
if (args[0] === "attach") return true;
59+
if (args[0] === "exec") return args.some((a) => a === "-it" || a === "-i" || a === "-t");
60+
}
61+
if (cmd === "kubectl" || cmd === "k3s" || cmd === "oc") {
62+
if (args[0] === "attach") return true;
63+
if (args[0] === "exec") return args.some((a) => a === "-it" || a === "-i" || a === "-t");
64+
}
65+
if (cmd === "lxc" && args[0] === "exec") return args.some((a) => a === "-t" || a === "-T");
66+
return false;
67+
}
68+
69+
function isSshInteractive(args: string[]): boolean {
70+
const hasForcedTty = args.includes("-t") || args.includes("-tt");
71+
const hasRemoteCmd = args.some((a) => !a.startsWith("-") && a.includes(" "));
72+
return hasForcedTty || !hasRemoteCmd;
73+
}
74+
75+
export function getBlockingCommand(lastCommand: string | null, inAltBuffer: boolean): string | null {
76+
if (!lastCommand) return null;
77+
78+
let words = lastCommand.trim().split(/\s+/);
79+
if (words.length === 0) return null;
80+
81+
while (words.length && WRAPPERS.includes(words[0])) {
82+
words.shift();
83+
}
84+
if (!words.length) return null;
85+
86+
const first = words[0].split("/").pop()!;
87+
const args = words.slice(1);
88+
89+
if (inAltBuffer) return first;
90+
91+
if (ALWAYS_BLOCK.includes(first)) return first;
92+
93+
if (isAttachLike(first, args)) return first;
94+
95+
if (first === "ssh" || first === "mosh" || first === "telnet" || first === "rlogin") {
96+
if (isSshInteractive(args)) return first;
97+
return null;
98+
}
99+
100+
if (first === "su" || first === "machinectl" || first === "chroot" || first === "nsenter" || first === "lxc") {
101+
if (!args.length || SHELLS.includes(args[args.length - 1]?.split("/").pop() || "")) return first;
102+
return null;
103+
}
104+
105+
if (SHELLS.includes(first)) {
106+
if (looksInteractiveShellArgs(args)) return first;
107+
if (isNonInteractiveShellExec(args)) return null;
108+
return null;
109+
}
110+
111+
if (BARE_REPLS.includes(first)) {
112+
if (args.length === 0) return first;
113+
return null;
114+
}
115+
116+
return null;
117+
}

0 commit comments

Comments
 (0)