From 023c78401495347d1978bb1a5b312cf791c39833 Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 16 Oct 2025 11:26:39 -0700 Subject: [PATCH 01/17] better logging for latency checking --- pkg/wcloud/wcloud.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/wcloud/wcloud.go b/pkg/wcloud/wcloud.go index eb6a64d9f5..01ab8b29d5 100644 --- a/pkg/wcloud/wcloud.go +++ b/pkg/wcloud/wcloud.go @@ -169,7 +169,6 @@ func sendTEventsBatch(clientId string) (bool, int, error) { if len(events) == 0 { return true, 0, nil } - log.Printf("[wcloud] sending %d tevents\n", len(events)) input := TEventsInputType{ ClientId: clientId, Events: events, @@ -178,7 +177,10 @@ func sendTEventsBatch(clientId string) (bool, int, error) { if err != nil { return true, 0, err } + startTime := time.Now() _, err = doRequest(req, nil) + latency := time.Since(startTime) + log.Printf("[wcloud] sent %d tevents (latency: %v)\n", len(events), latency) if err != nil { return true, 0, err } From 26458f1596b2a12997ef7538c19c36d9b3bb1915 Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 16 Oct 2025 14:19:47 -0700 Subject: [PATCH 02/17] working on zsh terminal integration. moved integration scripts to files for easier editing. added _waveterm_si funcs for sending new OSC code 16162 (like OSC 133). also send OSC 7. POC parsing in termwrap. --- Taskfile.yml | 3 + frontend/app/view/term/termwrap.ts | 15 +- .../shellutil/shellintegration/bash_bashrc.sh | 31 ++++ .../shellintegration/fish_wavefish.sh | 10 ++ .../shellintegration/pwsh_wavepwsh.sh | 13 ++ .../shellutil/shellintegration/zsh_zlogin.sh | 7 + .../shellintegration/zsh_zprofile.sh | 2 + .../shellutil/shellintegration/zsh_zshenv.sh | 11 ++ .../shellutil/shellintegration/zsh_zshrc.sh | 48 ++++++ pkg/util/shellutil/shellutil.go | 140 +++--------------- 10 files changed, 162 insertions(+), 118 deletions(-) create mode 100644 pkg/util/shellutil/shellintegration/bash_bashrc.sh create mode 100644 pkg/util/shellutil/shellintegration/fish_wavefish.sh create mode 100644 pkg/util/shellutil/shellintegration/pwsh_wavepwsh.sh create mode 100644 pkg/util/shellutil/shellintegration/zsh_zlogin.sh create mode 100644 pkg/util/shellutil/shellintegration/zsh_zprofile.sh create mode 100644 pkg/util/shellutil/shellintegration/zsh_zshenv.sh create mode 100644 pkg/util/shellutil/shellintegration/zsh_zshrc.sh diff --git a/Taskfile.yml b/Taskfile.yml index 006cbf7289..fa0ad2a3e9 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -157,6 +157,7 @@ tasks: - go.mod - go.sum - pkg/**/*.go + - pkg/**/*.sh - cmd/**/*.go - tsunami/go.mod - tsunami/go.sum @@ -188,6 +189,7 @@ tasks: - "cmd/server/*.go" - "pkg/**/*.go" - "pkg/**/*.json" + - "pkg/**/*.sh" - tsunami/**/*.go generates: - dist/bin/wavesrv.* @@ -215,6 +217,7 @@ tasks: - "cmd/server/*.go" - "pkg/**/*.go" - "pkg/**/*.json" + - "pkg/**/*.sh" - "tsunami/**/*.go" generates: - dist/bin/wavesrv.* diff --git a/frontend/app/view/term/termwrap.ts b/frontend/app/view/term/termwrap.ts index e84f05744e..66bdc85c32 100644 --- a/frontend/app/view/term/termwrap.ts +++ b/frontend/app/view/term/termwrap.ts @@ -129,11 +129,11 @@ function handleOsc7Command(data: string, blockId: string, loaded: boolean): bool await services.ObjectService.UpdateObjectMeta(WOS.makeORef("block", blockId), { "cmd:cwd": data, }); - + const rtInfo = { "cmd:hascurcwd": true }; const rtInfoData: CommandSetRTInfoData = { oref: WOS.makeORef("block", blockId), - data: rtInfo + data: rtInfo, }; await RpcApi.SetRTInfoCommand(TabRpcClient, rtInfoData).catch((e) => console.log("error setting RT info", e) @@ -143,6 +143,14 @@ function handleOsc7Command(data: string, blockId: string, loaded: boolean): bool return true; } +function handleOsc16162Command(data: string, blockId: string, loaded: boolean): boolean { + if (!loaded) { + return false; + } + console.log("OSC 16162 received:", data, "blockId:", blockId); + return true; +} + export class TermWrap { blockId: string; ptyOffset: number; @@ -222,6 +230,9 @@ export class TermWrap { this.terminal.parser.registerOscHandler(7, (data: string) => { return handleOsc7Command(data, this.blockId, this.loaded); }); + this.terminal.parser.registerOscHandler(16162, (data: string) => { + return handleOsc16162Command(data, this.blockId, this.loaded); + }); this.terminal.attachCustomKeyEventHandler(waveOptions.keydownHandler); this.connectElem = connectElem; this.mainFileSubject = null; diff --git a/pkg/util/shellutil/shellintegration/bash_bashrc.sh b/pkg/util/shellutil/shellintegration/bash_bashrc.sh new file mode 100644 index 0000000000..dcddab93a5 --- /dev/null +++ b/pkg/util/shellutil/shellintegration/bash_bashrc.sh @@ -0,0 +1,31 @@ + +# Source /etc/profile if it exists +if [ -f /etc/profile ]; then + . /etc/profile +fi + +WAVETERM_WSHBINDIR={{.WSHBINDIR}} + +# after /etc/profile which is likely to clobber the path +export PATH="$WAVETERM_WSHBINDIR:$PATH" + +# Source the dynamic script from wsh token +eval "$(wsh token "$WAVETERM_SWAPTOKEN" bash 2> /dev/null)" +unset WAVETERM_SWAPTOKEN + +# Source the first of ~/.bash_profile, ~/.bash_login, or ~/.profile that exists +if [ -f ~/.bash_profile ]; then + . ~/.bash_profile +elif [ -f ~/.bash_login ]; then + . ~/.bash_login +elif [ -f ~/.profile ]; then + . ~/.profile +fi + +if [[ ":$PATH:" != *":$WAVETERM_WSHBINDIR:"* ]]; then + export PATH="$WAVETERM_WSHBINDIR:$PATH" +fi +unset WAVETERM_WSHBINDIR +if type _init_completion &>/dev/null; then + source <(wsh completion bash) +fi \ No newline at end of file diff --git a/pkg/util/shellutil/shellintegration/fish_wavefish.sh b/pkg/util/shellutil/shellintegration/fish_wavefish.sh new file mode 100644 index 0000000000..65db91600e --- /dev/null +++ b/pkg/util/shellutil/shellintegration/fish_wavefish.sh @@ -0,0 +1,10 @@ +# this file is sourced with -C +# Add Wave binary directory to PATH +set -x PATH {{.WSHBINDIR}} $PATH + +# Source dynamic script from wsh token (the echo is to prevent fish from complaining about empty input) +wsh token "$WAVETERM_SWAPTOKEN" fish 2>/dev/null | source +set -e WAVETERM_SWAPTOKEN + +# Load Wave completions +wsh completion fish | source \ No newline at end of file diff --git a/pkg/util/shellutil/shellintegration/pwsh_wavepwsh.sh b/pkg/util/shellutil/shellintegration/pwsh_wavepwsh.sh new file mode 100644 index 0000000000..840072c362 --- /dev/null +++ b/pkg/util/shellutil/shellintegration/pwsh_wavepwsh.sh @@ -0,0 +1,13 @@ +# We source this file with -NoExit -File +$env:PATH = {{.WSHBINDIR_PWSH}} + "{{.PATHSEP}}" + $env:PATH + +# Source dynamic script from wsh token +$waveterm_swaptoken_output = wsh token $env:WAVETERM_SWAPTOKEN pwsh 2>$null | Out-String +if ($waveterm_swaptoken_output -and $waveterm_swaptoken_output -ne "") { + Invoke-Expression $waveterm_swaptoken_output +} +Remove-Variable -Name waveterm_swaptoken_output +Remove-Item Env:WAVETERM_SWAPTOKEN + +# Load Wave completions +wsh completion powershell | Out-String | Invoke-Expression \ No newline at end of file diff --git a/pkg/util/shellutil/shellintegration/zsh_zlogin.sh b/pkg/util/shellutil/shellintegration/zsh_zlogin.sh new file mode 100644 index 0000000000..a4bc901414 --- /dev/null +++ b/pkg/util/shellutil/shellintegration/zsh_zlogin.sh @@ -0,0 +1,7 @@ +# Source the original zlogin +[ -f ~/.zlogin ] && source ~/.zlogin + +# Unset ZDOTDIR only if it hasn't been modified +if [ "$ZDOTDIR" = "$WAVETERM_ZDOTDIR" ]; then + unset ZDOTDIR +fi \ No newline at end of file diff --git a/pkg/util/shellutil/shellintegration/zsh_zprofile.sh b/pkg/util/shellutil/shellintegration/zsh_zprofile.sh new file mode 100644 index 0000000000..60ec1e8c37 --- /dev/null +++ b/pkg/util/shellutil/shellintegration/zsh_zprofile.sh @@ -0,0 +1,2 @@ +# Source the original zprofile +[ -f ~/.zprofile ] && source ~/.zprofile \ No newline at end of file diff --git a/pkg/util/shellutil/shellintegration/zsh_zshenv.sh b/pkg/util/shellutil/shellintegration/zsh_zshenv.sh new file mode 100644 index 0000000000..345fdb3a89 --- /dev/null +++ b/pkg/util/shellutil/shellintegration/zsh_zshenv.sh @@ -0,0 +1,11 @@ +# Store the initial ZDOTDIR value +WAVETERM_ZDOTDIR="$ZDOTDIR" + +# Source the original zshenv +[ -f ~/.zshenv ] && source ~/.zshenv + +# Detect if ZDOTDIR has changed +if [ "$ZDOTDIR" != "$WAVETERM_ZDOTDIR" ]; then + # If changed, manually source your custom zshrc from the original WAVETERM_ZDOTDIR + [ -f "$WAVETERM_ZDOTDIR/.zshrc" ] && source "$WAVETERM_ZDOTDIR/.zshrc" +fi \ No newline at end of file diff --git a/pkg/util/shellutil/shellintegration/zsh_zshrc.sh b/pkg/util/shellutil/shellintegration/zsh_zshrc.sh new file mode 100644 index 0000000000..d6b484f1de --- /dev/null +++ b/pkg/util/shellutil/shellintegration/zsh_zshrc.sh @@ -0,0 +1,48 @@ +# add wsh to path, source dynamic script from wsh token +WAVETERM_WSHBINDIR={{.WSHBINDIR}} +export PATH="$WAVETERM_WSHBINDIR:$PATH" +source <(wsh token "$WAVETERM_SWAPTOKEN" zsh 2>/dev/null) +unset WAVETERM_SWAPTOKEN + +# Source the original zshrc only if ZDOTDIR has not been changed +if [ "$ZDOTDIR" = "$WAVETERM_ZDOTDIR" ]; then + [ -f ~/.zshrc ] && source ~/.zshrc +fi + +if [[ ":$PATH:" != *":$WAVETERM_WSHBINDIR:"* ]]; then + export PATH="$WAVETERM_WSHBINDIR:$PATH" +fi +unset WAVETERM_WSHBINDIR + +if [[ -n ${_comps+x} ]]; then + source <(wsh completion zsh) +fi + +typeset -g _WAVETERM_SI_FIRSTPRECMD=1 + +# shell integration +_waveterm_si_blocked() { + [[ -n "$TMUX" || -n "$STY" || "$TERM" == tmux* || "$TERM" == screen* ]] +} + +_waveterm_si_precmd() { + local _waveterm_si_status=$? + _waveterm_si_blocked && return + # D;status for previous command (skip before first prompt) + if (( !_WAVETERM_SI_FIRSTPRECMD )); then + printf '\033]16162;D;%d\007' $_waveterm_si_status + fi + printf '\033]16162;A\007' # start of new prompt + printf '\033]7;file://%s%s\007' "$HOST" "$PWD" # OSC 7 - current directory + _WAVETERM_SI_FIRSTPRECMD=0 +} + +_waveterm_si_preexec() { + _waveterm_si_blocked && return + printf '\033]16162;B\007' # end of prompt + printf '\033]16162;C\007' # start of command output +} + +autoload -U add-zsh-hook +add-zsh-hook precmd _waveterm_si_precmd +add-zsh-hook preexec _waveterm_si_preexec \ No newline at end of file diff --git a/pkg/util/shellutil/shellutil.go b/pkg/util/shellutil/shellutil.go index d9b625a848..1f55e4172d 100644 --- a/pkg/util/shellutil/shellutil.go +++ b/pkg/util/shellutil/shellutil.go @@ -5,6 +5,7 @@ package shellutil import ( "context" + _ "embed" "fmt" "log" "os" @@ -22,6 +23,29 @@ import ( "github.com/wavetermdev/waveterm/pkg/waveobj" ) +var ( + //go:embed shellintegration/zsh_zprofile.sh + ZshStartup_Zprofile string + + //go:embed shellintegration/zsh_zshrc.sh + ZshStartup_Zshrc string + + //go:embed shellintegration/zsh_zlogin.sh + ZshStartup_Zlogin string + + //go:embed shellintegration/zsh_zshenv.sh + ZshStartup_Zshenv string + + //go:embed shellintegration/bash_bashrc.sh + BashStartup_Bashrc string + + //go:embed shellintegration/fish_wavefish.sh + FishStartup_Wavefish string + + //go:embed shellintegration/pwsh_wavepwsh.sh + PwshStartup_wavepwsh string +) + const DefaultTermType = "xterm-256color" const DefaultTermRows = 24 const DefaultTermCols = 80 @@ -47,122 +71,6 @@ const ( PwshIntegrationDir = "shell/pwsh" FishIntegrationDir = "shell/fish" WaveHomeBinDir = "bin" - - ZshStartup_Zprofile = ` -# Source the original zprofile -[ -f ~/.zprofile ] && source ~/.zprofile -` - - ZshStartup_Zshrc = ` -# add wsh to path, source dynamic script from wsh token -WAVETERM_WSHBINDIR={{.WSHBINDIR}} -export PATH="$WAVETERM_WSHBINDIR:$PATH" -source <(wsh token "$WAVETERM_SWAPTOKEN" zsh 2>/dev/null) -unset WAVETERM_SWAPTOKEN - -# Source the original zshrc only if ZDOTDIR has not been changed -if [ "$ZDOTDIR" = "$WAVETERM_ZDOTDIR" ]; then - [ -f ~/.zshrc ] && source ~/.zshrc -fi - -if [[ ":$PATH:" != *":$WAVETERM_WSHBINDIR:"* ]]; then - export PATH="$WAVETERM_WSHBINDIR:$PATH" -fi -unset WAVETERM_WSHBINDIR - -if [[ -n ${_comps+x} ]]; then - source <(wsh completion zsh) -fi -` - - ZshStartup_Zlogin = ` -# Source the original zlogin -[ -f ~/.zlogin ] && source ~/.zlogin - -# Unset ZDOTDIR only if it hasn't been modified -if [ "$ZDOTDIR" = "$WAVETERM_ZDOTDIR" ]; then - unset ZDOTDIR -fi -` - - ZshStartup_Zshenv = ` -# Store the initial ZDOTDIR value -WAVETERM_ZDOTDIR="$ZDOTDIR" - -# Source the original zshenv -[ -f ~/.zshenv ] && source ~/.zshenv - -# Detect if ZDOTDIR has changed -if [ "$ZDOTDIR" != "$WAVETERM_ZDOTDIR" ]; then - # If changed, manually source your custom zshrc from the original WAVETERM_ZDOTDIR - [ -f "$WAVETERM_ZDOTDIR/.zshrc" ] && source "$WAVETERM_ZDOTDIR/.zshrc" -fi - -` - - BashStartup_Bashrc = ` - -# Source /etc/profile if it exists -if [ -f /etc/profile ]; then - . /etc/profile -fi - -WAVETERM_WSHBINDIR={{.WSHBINDIR}} - -# after /etc/profile which is likely to clobber the path -export PATH="$WAVETERM_WSHBINDIR:$PATH" - -# Source the dynamic script from wsh token -eval "$(wsh token "$WAVETERM_SWAPTOKEN" bash 2> /dev/null)" -unset WAVETERM_SWAPTOKEN - -# Source the first of ~/.bash_profile, ~/.bash_login, or ~/.profile that exists -if [ -f ~/.bash_profile ]; then - . ~/.bash_profile -elif [ -f ~/.bash_login ]; then - . ~/.bash_login -elif [ -f ~/.profile ]; then - . ~/.profile -fi - -if [[ ":$PATH:" != *":$WAVETERM_WSHBINDIR:"* ]]; then - export PATH="$WAVETERM_WSHBINDIR:$PATH" -fi -unset WAVETERM_WSHBINDIR -if type _init_completion &>/dev/null; then - source <(wsh completion bash) -fi - -` - - FishStartup_Wavefish = ` -# this file is sourced with -C -# Add Wave binary directory to PATH -set -x PATH {{.WSHBINDIR}} $PATH - -# Source dynamic script from wsh token (the echo is to prevent fish from complaining about empty input) -wsh token "$WAVETERM_SWAPTOKEN" fish 2>/dev/null | source -set -e WAVETERM_SWAPTOKEN - -# Load Wave completions -wsh completion fish | source -` - - PwshStartup_wavepwsh = ` -# We source this file with -NoExit -File -$env:PATH = {{.WSHBINDIR_PWSH}} + "{{.PATHSEP}}" + $env:PATH - -# Source dynamic script from wsh token -$waveterm_swaptoken_output = wsh token $env:WAVETERM_SWAPTOKEN pwsh 2>$null | Out-String -if ($waveterm_swaptoken_output -and $waveterm_swaptoken_output -ne "") { - Invoke-Expression $waveterm_swaptoken_output -} -Remove-Variable -Name waveterm_swaptoken_output -Remove-Item Env:WAVETERM_SWAPTOKEN - -# Load Wave completions -wsh completion powershell | Out-String | Invoke-Expression -` ) func DetectLocalShellPath() string { From ab4491e0d4de963463de54e3aa217f74e553c112 Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 16 Oct 2025 14:45:13 -0700 Subject: [PATCH 03/17] always send OSC 7 --- frontend/app/view/term/termwrap.ts | 43 ++++++++++++------- .../shellutil/shellintegration/zsh_zshrc.sh | 32 ++++++++++++-- 2 files changed, 56 insertions(+), 19 deletions(-) diff --git a/frontend/app/view/term/termwrap.ts b/frontend/app/view/term/termwrap.ts index 66bdc85c32..2f4458f508 100644 --- a/frontend/app/view/term/termwrap.ts +++ b/frontend/app/view/term/termwrap.ts @@ -47,11 +47,11 @@ type TermWrapOptions = { function handleOscWaveCommand(data: string, blockId: string, loaded: boolean): boolean { if (!loaded) { - return false; + return true; } if (!data || data.length === 0) { console.log("Invalid Wave OSC command received (empty)"); - return false; + return true; } // Expected formats: @@ -60,7 +60,7 @@ function handleOscWaveCommand(data: string, blockId: string, loaded: boolean): b const parts = data.split(";"); if (parts[0] !== "setmeta") { console.log("Invalid Wave OSC command received (bad command)", data); - return false; + return true; } let jsonPayload: string; let waveId: string | undefined; @@ -71,7 +71,7 @@ function handleOscWaveCommand(data: string, blockId: string, loaded: boolean): b jsonPayload = parts.slice(2).join(";"); } else { console.log("Invalid Wave OSC command received (1 part)", data); - return false; + return true; } let meta: any; @@ -79,7 +79,7 @@ function handleOscWaveCommand(data: string, blockId: string, loaded: boolean): b meta = JSON.parse(jsonPayload); } catch (e) { console.error("Invalid JSON in Wave OSC command:", e); - return false; + return true; } if (waveId) { @@ -107,27 +107,38 @@ function handleOscWaveCommand(data: string, blockId: string, loaded: boolean): b return true; } +// for xterm handlers, we return true always because we "own" OSC 7. +// even if it is invalid we dont want to propagate to other handlers function handleOsc7Command(data: string, blockId: string, loaded: boolean): boolean { if (!loaded) { - return false; + return true; } if (data == null || data.length == 0) { console.log("Invalid OSC 7 command received (empty)"); - return false; + return true; } - if (data.startsWith("file://")) { - data = data.substring(7); - const nextSlashIdx = data.indexOf("/"); - if (nextSlashIdx == -1) { - console.log("Invalid OSC 7 command received (bad path)", data); - return false; + if (data.length > 1024) { + console.log("Invalid OSC 7, data length too long", data.length); + return true; + } + + let pathPart: string; + try { + const url = new URL(data); + if (url.protocol !== "file:") { + console.log("Invalid OSC 7 command received (non-file protocol)", data); + return true; } - data = data.substring(nextSlashIdx); + pathPart = url.pathname; + } catch (e) { + console.log("Invalid OSC 7 command received (parse error)", data, e); + return true; } + setTimeout(() => { fireAndForget(async () => { await services.ObjectService.UpdateObjectMeta(WOS.makeORef("block", blockId), { - "cmd:cwd": data, + "cmd:cwd": pathPart, }); const rtInfo = { "cmd:hascurcwd": true }; @@ -145,7 +156,7 @@ function handleOsc7Command(data: string, blockId: string, loaded: boolean): bool function handleOsc16162Command(data: string, blockId: string, loaded: boolean): boolean { if (!loaded) { - return false; + return true; } console.log("OSC 16162 received:", data, "blockId:", blockId); return true; diff --git a/pkg/util/shellutil/shellintegration/zsh_zshrc.sh b/pkg/util/shellutil/shellintegration/zsh_zshrc.sh index d6b484f1de..4f8e19e38d 100644 --- a/pkg/util/shellutil/shellintegration/zsh_zshrc.sh +++ b/pkg/util/shellutil/shellintegration/zsh_zshrc.sh @@ -1,4 +1,4 @@ -# add wsh to path, source dynamic script from wsh token +o# add wsh to path, source dynamic script from wsh token WAVETERM_WSHBINDIR={{.WSHBINDIR}} export PATH="$WAVETERM_WSHBINDIR:$PATH" source <(wsh token "$WAVETERM_SWAPTOKEN" zsh 2>/dev/null) @@ -25,15 +25,40 @@ _waveterm_si_blocked() { [[ -n "$TMUX" || -n "$STY" || "$TERM" == tmux* || "$TERM" == screen* ]] } +_waveterm_si_urlencode() { + if (( $+functions[omz_urlencode] )); then + omz_urlencode "$1" + else + local s="$1" + # Escape % first + s=${s//%/%25} + # Common reserved characters in file paths + s=${s// /%20} + s=${s//#/%23} + s=${s//\?/%3F} + s=${s//&/%26} + s=${s//;/\%3B} + s=${s//+/%2B} + printf '%s' "$s" + fi +} + +_waveterm_si_osc7() { + _waveterm_si_blocked && return + local encoded_pwd=$(_waveterm_si_urlencode "$PWD") + printf '\033]7;file://%s%s\007' "$HOST" "$encoded_pwd" # OSC 7 - current directory +} + _waveterm_si_precmd() { local _waveterm_si_status=$? _waveterm_si_blocked && return # D;status for previous command (skip before first prompt) if (( !_WAVETERM_SI_FIRSTPRECMD )); then printf '\033]16162;D;%d\007' $_waveterm_si_status + else + _waveterm_si_osc7 fi printf '\033]16162;A\007' # start of new prompt - printf '\033]7;file://%s%s\007' "$HOST" "$PWD" # OSC 7 - current directory _WAVETERM_SI_FIRSTPRECMD=0 } @@ -45,4 +70,5 @@ _waveterm_si_preexec() { autoload -U add-zsh-hook add-zsh-hook precmd _waveterm_si_precmd -add-zsh-hook preexec _waveterm_si_preexec \ No newline at end of file +add-zsh-hook preexec _waveterm_si_preexec +add-zsh-hook chpwd _waveterm_si_osc7 \ No newline at end of file From 46badf262eeb8a267f1c044be6856d1ea64d8bff Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 16 Oct 2025 16:54:54 -0700 Subject: [PATCH 04/17] fix bug with alternate screen mode... create a conditional disable OSC code. also working on zle integration --- frontend/app/view/term/termwrap.ts | 20 +++++++++- pkg/blockcontroller/shellcontroller.go | 3 +- .../shellutil/shellintegration/zsh_zshrc.sh | 38 +++++++++++++++++-- pkg/util/shellutil/shellutil.go | 7 ++++ 4 files changed, 62 insertions(+), 6 deletions(-) diff --git a/frontend/app/view/term/termwrap.ts b/frontend/app/view/term/termwrap.ts index 2f4458f508..562a53e6fb 100644 --- a/frontend/app/view/term/termwrap.ts +++ b/frontend/app/view/term/termwrap.ts @@ -154,11 +154,27 @@ function handleOsc7Command(data: string, blockId: string, loaded: boolean): bool return true; } -function handleOsc16162Command(data: string, blockId: string, loaded: boolean): boolean { +function handleOsc16162Command(data: string, blockId: string, loaded: boolean, terminal: Terminal): boolean { if (!loaded) { return true; } console.log("OSC 16162 received:", data, "blockId:", blockId); + + if (!data || data.length === 0) { + return true; + } + + const parts = data.split(";"); + const command = parts[0]; + + switch (command) { + case "R": + if (terminal.buffer.active.type === "alternate") { + terminal.write("\x1b[?1049l"); + } + break; + } + return true; } @@ -242,7 +258,7 @@ export class TermWrap { return handleOsc7Command(data, this.blockId, this.loaded); }); this.terminal.parser.registerOscHandler(16162, (data: string) => { - return handleOsc16162Command(data, this.blockId, this.loaded); + return handleOsc16162Command(data, this.blockId, this.loaded, this.terminal); }); this.terminal.attachCustomKeyEventHandler(waveOptions.keydownHandler); this.connectElem = connectElem; diff --git a/pkg/blockcontroller/shellcontroller.go b/pkg/blockcontroller/shellcontroller.go index 2aabec9eaf..04124153f1 100644 --- a/pkg/blockcontroller/shellcontroller.go +++ b/pkg/blockcontroller/shellcontroller.go @@ -200,10 +200,11 @@ func (sc *ShellController) resetTerminalState(logCtx context.Context) { blocklogger.Debugf(logCtx, "[conndebug] resetTerminalState: resetting terminal state\n") // controller type = "shell" var buf bytes.Buffer - // buf.WriteString("\x1b[?1049l") // disable alternative buffer buf.WriteString("\x1b[0m") // reset attributes buf.WriteString("\x1b[?25h") // show cursor buf.WriteString("\x1b[?1000l") // disable mouse tracking + buf.WriteString("\x1b[?1007l") // disable alternate scroll mode + buf.WriteString(shellutil.FormatOSC(16162, "R")) // OSC 16162 "R" - disable alternate screen mode (only if active) buf.WriteString("\r\n\r\n") err := HandleAppendBlockFile(sc.BlockId, wavebase.BlockFile_Term, buf.Bytes()) if err != nil { diff --git a/pkg/util/shellutil/shellintegration/zsh_zshrc.sh b/pkg/util/shellutil/shellintegration/zsh_zshrc.sh index 4f8e19e38d..f14f6938d4 100644 --- a/pkg/util/shellutil/shellintegration/zsh_zshrc.sh +++ b/pkg/util/shellutil/shellintegration/zsh_zshrc.sh @@ -1,4 +1,4 @@ -o# add wsh to path, source dynamic script from wsh token +# add wsh to path, source dynamic script from wsh token WAVETERM_WSHBINDIR={{.WSHBINDIR}} export PATH="$WAVETERM_WSHBINDIR:$PATH" source <(wsh token "$WAVETERM_SWAPTOKEN" zsh 2>/dev/null) @@ -56,6 +56,7 @@ _waveterm_si_precmd() { if (( !_WAVETERM_SI_FIRSTPRECMD )); then printf '\033]16162;D;%d\007' $_waveterm_si_status else + printf '\033]16162;M;{"shell":"zsh","shellversion":"%s"}\007' "$ZSH_VERSION" _waveterm_si_osc7 fi printf '\033]16162;A\007' # start of new prompt @@ -64,10 +65,41 @@ _waveterm_si_precmd() { _waveterm_si_preexec() { _waveterm_si_blocked && return - printf '\033]16162;B\007' # end of prompt - printf '\033]16162;C\007' # start of command output + local cmd64 + cmd64=$(printf '%s' "$1" | base64 2>/dev/null) + if [ -n "$cmd64" ]; then + printf '\033]16162;C;cmd64=%s\007' "$cmd64" + else + printf '\033]16162;C\007' + fi +} + +typeset -g WAVETERM_SI_INPUTEMPTY=1 + +_waveterm_si_inputempty() { + _waveterm_si_blocked && return + + local current_empty=1 + if [[ -n "$BUFFER" ]]; then + current_empty=0 + fi + + if (( current_empty != WAVETERM_SI_INPUTEMPTY )); then + WAVETERM_SI_INPUTEMPTY=$current_empty + if (( current_empty )); then + printf '\033]16162;I;{"inputempty":true}\007' + else + printf '\033]16162;I;{"inputempty":false}\007' + fi + fi } +if (( $+functions[add-zle-hook-widget] )); then + autoload -Uz add-zle-hook-widget + add-zle-hook-widget zle-line-init _waveterm_si_inputempty + add-zle-hook-widget zle-line-pre-redraw _waveterm_si_inputempty +fi + autoload -U add-zsh-hook add-zsh-hook precmd _waveterm_si_precmd add-zsh-hook preexec _waveterm_si_preexec diff --git a/pkg/util/shellutil/shellutil.go b/pkg/util/shellutil/shellutil.go index 1f55e4172d..489aae4a2b 100644 --- a/pkg/util/shellutil/shellutil.go +++ b/pkg/util/shellutil/shellutil.go @@ -345,3 +345,10 @@ func GetShellTypeFromShellPath(shellPath string) string { } return ShellType_unknown } + +func FormatOSC(oscNum int, parts ...string) string { + if len(parts) == 0 { + return fmt.Sprintf("\x1b]%d\x07", oscNum) + } + return fmt.Sprintf("\x1b]%d;%s\x07", oscNum, strings.Join(parts, ";")) +} From 202109cfaec0b86d65ae63e6177b34e584c01177 Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 16 Oct 2025 17:11:39 -0700 Subject: [PATCH 05/17] more consistency for OSC 16162 --- aiprompts/wave-osc-16162.md | 212 ++++++++++++++++++ frontend/app/view/term/termwrap.ts | 34 ++- .../shellutil/shellintegration/zsh_zshrc.sh | 4 +- 3 files changed, 242 insertions(+), 8 deletions(-) create mode 100644 aiprompts/wave-osc-16162.md diff --git a/aiprompts/wave-osc-16162.md b/aiprompts/wave-osc-16162.md new file mode 100644 index 0000000000..74979b26ac --- /dev/null +++ b/aiprompts/wave-osc-16162.md @@ -0,0 +1,212 @@ +# Wave Terminal OSC 16162 Escape Sequences + +Wave Terminal uses a custom OSC (Operating System Command) escape sequence numbered **16162** for shell integration. This allows the shell to communicate its state and events to the terminal. + +## Format + +All commands use this escape sequence format: + +``` +ESC ] 16162 ; command [;] BEL +``` + +Where: +- `ESC` = `\033` (escape character) +- `BEL` = `\007` (bell character) +- `command` = Single letter (A, C, M, D, I, or R) +- `` = Optional JSON payload (depends on command) + +## Commands + +### A - Prompt Start + +Marks the beginning of a new shell prompt. + +**Format:** `A` + +**When:** Sent in `precmd` hook (after previous command completes, before new prompt is displayed) + +**Purpose:** Signals to the terminal that a new prompt is being drawn. This helps Wave Terminal distinguish between prompt output and command output. + +**Example:** +```bash +printf '\033]16162;A\007' +``` + +--- + +### C - Command Execution + +Sent immediately before a command is executed, optionally including the command text. + +**Format:** `C[;]` + +**Data Type:** +```typescript +{ + cmd64?: string; // base64-encoded command text +} +``` + +**When:** Sent in `preexec` hook (after user presses Enter, before command runs) + +**Purpose:** Notifies the terminal that a command is about to execute. The command text is base64-encoded to handle special characters safely. + +**Example:** +```bash +cmd64=$(printf '%s' "ls -la" | base64) +printf '\033]16162;C;{"cmd64":"%s"}\007' "$cmd64" +``` + +--- + +### M - Metadata + +Sends shell metadata information (typically only once at shell initialization). + +**Format:** `M;` + +**Data Type:** +```typescript +{ + shell?: string; // Shell name (e.g., "zsh", "bash") + shellversion?: string; // Version string of the shell +} +``` + +**When:** Sent during first `precmd` hook (on shell startup) + +**Purpose:** Provides Wave Terminal with information about the shell environment. + +**Example:** +```bash +printf '\033]16162;M;{"shell":"zsh","shellversion":"5.9"}\007' +``` + +--- + +### D - Done (Exit Status) + +Reports the exit status of the previously executed command. + +**Format:** `D;` + +**Data Type:** +```typescript +{ + exitcode?: number; // Exit status code of the previous command +} +``` + +**When:** Sent in `precmd` hook (after command completes) + +**Purpose:** Communicates whether the previous command succeeded or failed, allowing Wave Terminal to display success/failure indicators. + +**Example:** +```bash +# After command exits with status 0 +printf '\033]16162;D;{"exitcode":0}\007' + +# After command exits with status 1 +printf '\033]16162;D;{"exitcode":1}\007' +``` + +--- + +### I - Input Status + +Reports the current state of the command line input buffer. + +**Format:** `I;` + +**Data Type:** +```typescript +{ + inputempty?: boolean; // Whether the command line buffer is empty +} +``` + +**When:** Sent during ZLE (Zsh Line Editor) hooks when buffer state changes +- `zle-line-init` - When line editor is initialized +- `zle-line-pre-redraw` - Before line is redrawn + +**Purpose:** Allows Wave Terminal to track the state of the command line input. Currently reports whether the buffer is empty, but may be extended to include additional input state information in the future. + +**Example:** +```bash +# When buffer is empty +I;{"inputempty":true} + +# When buffer has content +I;{"inputempty":false} +``` + +### R - Reset Alternate Buffer + +Resets the terminal if it's in alternate buffer mode. + +**Format:** `R` + +**When:** Can be sent at any time to ensure terminal is not stuck in alternate buffer mode + +**Purpose:** If the terminal is currently displaying the alternate screen buffer, this command switches back to the normal buffer. This is useful for recovering from programs that crash without properly restoring the screen. + +**Behavior:** +- Checks if terminal is in alternate buffer mode (`terminal.buffer.active.type === "alternate"`) +- If in alternate mode, sends `ESC [ ? 1049 l` to exit alternate buffer +- If not in alternate mode, does nothing + +**Example:** +```bash +R +``` + +--- + +## Typical Command Flow + +Here's the typical sequence during shell interaction: + +``` +1. Shell starts + → M; (metadata - shell info) + +2. First prompt appears + → A (prompt start) + +3. User types command and presses Enter + → I;{"inputempty":false} (input no longer empty - sent as user types) + → C;{"cmd64":"..."} (command about to execute) + +4. Command runs and completes + → D;{"exitcode":} (exit status) + → I;{"inputempty":true} (input empty again) + → A (next prompt start) + +5. Repeat from step 3... +``` + +## Implementation Notes + +- Shell integration is **disabled** when running inside tmux or screen (`TMUX`, `STY` environment variables, or `tmux*`/`screen*` TERM values) +- Commands are base64-encoded in the C sequence to safely handle special characters, newlines, and control characters +- The I (input empty) command is only sent when the state changes (not on every keystroke) +- The M (metadata) command is only sent once during the first precmd +- The D (exit status) command is skipped during the first precmd (no previous command to report) + +## Related Files + +- [`pkg/util/shellutil/shellintegration/zsh_zshrc.sh`](pkg/util/shellutil/shellintegration/zsh_zshrc.sh) - Zsh shell integration implementation +- Similar integrations exist for bash and other shells + +## Standard OSC 7 + +Wave Terminal also uses the standard **OSC 7** sequence for reporting the current working directory: + +**Format:** `7;file://` + +This is sent: +- During first precmd (after metadata) +- In the `chpwd` hook (whenever directory changes) + +The path is URL-encoded to safely handle special characters. \ No newline at end of file diff --git a/frontend/app/view/term/termwrap.ts b/frontend/app/view/term/termwrap.ts index 562a53e6fb..483a11d971 100644 --- a/frontend/app/view/term/termwrap.ts +++ b/frontend/app/view/term/termwrap.ts @@ -154,27 +154,49 @@ function handleOsc7Command(data: string, blockId: string, loaded: boolean): bool return true; } +// OSC 16162 - Shell Integration Commands +// See aiprompts/wave-osc-16162.md for full documentation +type Osc16162Command = + | { command: "A"; data: {} } + | { command: "C"; data: { cmd64?: string } } + | { command: "M"; data: { shell?: string; shellversion?: string } } + | { command: "D"; data: { exitcode?: number } } + | { command: "I"; data: { inputempty?: boolean } } + | { command: "R"; data: {} }; + function handleOsc16162Command(data: string, blockId: string, loaded: boolean, terminal: Terminal): boolean { if (!loaded) { return true; } console.log("OSC 16162 received:", data, "blockId:", blockId); - + if (!data || data.length === 0) { return true; } - + const parts = data.split(";"); - const command = parts[0]; - - switch (command) { + const commandStr = parts[0]; + const jsonDataStr = parts.length > 1 ? parts.slice(1).join(";") : null; + let parsedData: Record = {}; + + if (jsonDataStr) { + try { + parsedData = JSON.parse(jsonDataStr); + } catch (e) { + console.error("Error parsing OSC 16162 JSON data:", e); + } + } + + const cmd: Osc16162Command = { command: commandStr, data: parsedData } as Osc16162Command; + + switch (cmd.command) { case "R": if (terminal.buffer.active.type === "alternate") { terminal.write("\x1b[?1049l"); } break; } - + return true; } diff --git a/pkg/util/shellutil/shellintegration/zsh_zshrc.sh b/pkg/util/shellutil/shellintegration/zsh_zshrc.sh index f14f6938d4..9a2c7b4fc1 100644 --- a/pkg/util/shellutil/shellintegration/zsh_zshrc.sh +++ b/pkg/util/shellutil/shellintegration/zsh_zshrc.sh @@ -54,7 +54,7 @@ _waveterm_si_precmd() { _waveterm_si_blocked && return # D;status for previous command (skip before first prompt) if (( !_WAVETERM_SI_FIRSTPRECMD )); then - printf '\033]16162;D;%d\007' $_waveterm_si_status + printf '\033]16162;D;{"exitcode":%d}\007' $_waveterm_si_status else printf '\033]16162;M;{"shell":"zsh","shellversion":"%s"}\007' "$ZSH_VERSION" _waveterm_si_osc7 @@ -68,7 +68,7 @@ _waveterm_si_preexec() { local cmd64 cmd64=$(printf '%s' "$1" | base64 2>/dev/null) if [ -n "$cmd64" ]; then - printf '\033]16162;C;cmd64=%s\007' "$cmd64" + printf '\033]16162;C;{"cmd64":"%s"}\007' "$cmd64" else printf '\033]16162;C\007' fi From a793ddf0017c1541f74a4f24380023d5537c6583 Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 16 Oct 2025 17:27:05 -0700 Subject: [PATCH 06/17] implement the backend of the new osc code... store shell state in block rtinfo --- aiprompts/wave-osc-16162.md | 6 +- frontend/app/view/term/termwrap.ts | 58 ++++++++++++++++++- frontend/types/gotypes.d.ts | 7 +++ .../shellutil/shellintegration/zsh_zshrc.sh | 3 +- pkg/waveobj/blockrtinfo.go | 11 +++- pkg/wstore/blockrtinfo.go | 10 ++++ 6 files changed, 90 insertions(+), 5 deletions(-) diff --git a/aiprompts/wave-osc-16162.md b/aiprompts/wave-osc-16162.md index 74979b26ac..7d403606d8 100644 --- a/aiprompts/wave-osc-16162.md +++ b/aiprompts/wave-osc-16162.md @@ -71,16 +71,18 @@ Sends shell metadata information (typically only once at shell initialization). { shell?: string; // Shell name (e.g., "zsh", "bash") shellversion?: string; // Version string of the shell + uname?: string; // Output of "uname -smr" (e.g., "Darwin 23.0.0 arm64") } ``` **When:** Sent during first `precmd` hook (on shell startup) -**Purpose:** Provides Wave Terminal with information about the shell environment. +**Purpose:** Provides Wave Terminal with information about the shell environment and operating system. **Example:** ```bash -printf '\033]16162;M;{"shell":"zsh","shellversion":"5.9"}\007' +uname_info=$(uname -smr 2>/dev/null) +printf '\033]16162;M;{"shell":"zsh","shellversion":"5.9","uname":"%s"}\007' "$uname_info" ``` --- diff --git a/frontend/app/view/term/termwrap.ts b/frontend/app/view/term/termwrap.ts index 483a11d971..74ef9f27e8 100644 --- a/frontend/app/view/term/termwrap.ts +++ b/frontend/app/view/term/termwrap.ts @@ -159,7 +159,7 @@ function handleOsc7Command(data: string, blockId: string, loaded: boolean): bool type Osc16162Command = | { command: "A"; data: {} } | { command: "C"; data: { cmd64?: string } } - | { command: "M"; data: { shell?: string; shellversion?: string } } + | { command: "M"; data: { shell?: string; shellversion?: string; uname?: string } } | { command: "D"; data: { exitcode?: number } } | { command: "I"; data: { inputempty?: boolean } } | { command: "R"; data: {} }; @@ -189,7 +189,49 @@ function handleOsc16162Command(data: string, blockId: string, loaded: boolean, t const cmd: Osc16162Command = { command: commandStr, data: parsedData } as Osc16162Command; + const rtInfo: ObjRTInfo = {}; + switch (cmd.command) { + case "A": + rtInfo["shell:state"] = "ready"; + break; + case "C": + rtInfo["shell:state"] = "running-command"; + if (cmd.data.cmd64) { + try { + const decodedCmd = atob(cmd.data.cmd64); + rtInfo["shell:lastcmd"] = decodedCmd; + } catch (e) { + console.error("Error decoding cmd64:", e); + rtInfo["shell:lastcmd"] = null; + } + } else { + rtInfo["shell:lastcmd"] = null; + } + break; + case "M": + if (cmd.data.shell) { + rtInfo["shell:type"] = cmd.data.shell; + } + if (cmd.data.shellversion) { + rtInfo["shell:version"] = cmd.data.shellversion; + } + if (cmd.data.uname) { + rtInfo["shell:uname"] = cmd.data.uname; + } + break; + case "D": + if (cmd.data.exitcode != null) { + rtInfo["shell:lastcmdexitcode"] = cmd.data.exitcode; + } else { + rtInfo["shell:lastcmdexitcode"] = null; + } + break; + case "I": + if (cmd.data.inputempty != null) { + rtInfo["shell:inputempty"] = cmd.data.inputempty; + } + break; case "R": if (terminal.buffer.active.type === "alternate") { terminal.write("\x1b[?1049l"); @@ -197,6 +239,20 @@ function handleOsc16162Command(data: string, blockId: string, loaded: boolean, t break; } + if (Object.keys(rtInfo).length > 0) { + setTimeout(() => { + fireAndForget(async () => { + const rtInfoData: CommandSetRTInfoData = { + oref: WOS.makeORef("block", blockId), + data: rtInfo, + }; + await RpcApi.SetRTInfoCommand(TabRpcClient, rtInfoData).catch((e) => + console.log("error setting RT info (OSC 16162)", e) + ); + }); + }, 0); + } + return true; } diff --git a/frontend/types/gotypes.d.ts b/frontend/types/gotypes.d.ts index 123576e21c..2aa6c5f5ff 100644 --- a/frontend/types/gotypes.d.ts +++ b/frontend/types/gotypes.d.ts @@ -710,6 +710,13 @@ declare global { "tsunami:shortdesc"?: string; "tsunami:schemas"?: any; "cmd:hascurcwd"?: boolean; + "shell:state"?: string; + "shell:type"?: string; + "shell:version"?: string; + "shell:uname"?: string; + "shell:inputempty"?: boolean; + "shell:lastcmd"?: string; + "shell:lastcmdexitcode"?: number; }; // iochantypes.Packet diff --git a/pkg/util/shellutil/shellintegration/zsh_zshrc.sh b/pkg/util/shellutil/shellintegration/zsh_zshrc.sh index 9a2c7b4fc1..d077bc5a7d 100644 --- a/pkg/util/shellutil/shellintegration/zsh_zshrc.sh +++ b/pkg/util/shellutil/shellintegration/zsh_zshrc.sh @@ -56,7 +56,8 @@ _waveterm_si_precmd() { if (( !_WAVETERM_SI_FIRSTPRECMD )); then printf '\033]16162;D;{"exitcode":%d}\007' $_waveterm_si_status else - printf '\033]16162;M;{"shell":"zsh","shellversion":"%s"}\007' "$ZSH_VERSION" + local uname_info=$(uname -smr 2>/dev/null) + printf '\033]16162;M;{"shell":"zsh","shellversion":"%s","uname":"%s"}\007' "$ZSH_VERSION" "$uname_info" _waveterm_si_osc7 fi printf '\033]16162;A\007' # start of new prompt diff --git a/pkg/waveobj/blockrtinfo.go b/pkg/waveobj/blockrtinfo.go index 72e1118a43..dc72762d36 100644 --- a/pkg/waveobj/blockrtinfo.go +++ b/pkg/waveobj/blockrtinfo.go @@ -7,5 +7,14 @@ type ObjRTInfo struct { TsunamiTitle string `json:"tsunami:title,omitempty"` TsunamiShortDesc string `json:"tsunami:shortdesc,omitempty"` TsunamiSchemas any `json:"tsunami:schemas,omitempty"` - CmdHasCurCwd bool `json:"cmd:hascurcwd,omitempty"` + + CmdHasCurCwd bool `json:"cmd:hascurcwd,omitempty"` + + ShellState string `json:"shell:state,omitempty"` + ShellType string `json:"shell:type,omitempty"` + ShellVersion string `json:"shell:version,omitempty"` + ShellUname string `json:"shell:uname,omitempty"` + ShellInputEmpty bool `json:"shell:inputempty,omitempty"` + ShellLastCmd string `json:"shell:lastcmd,omitempty"` + ShellLastCmdExitCode int `json:"shell:lastcmdexitcode,omitempty"` } diff --git a/pkg/wstore/blockrtinfo.go b/pkg/wstore/blockrtinfo.go index 434b04abae..3f76d701bf 100644 --- a/pkg/wstore/blockrtinfo.go +++ b/pkg/wstore/blockrtinfo.go @@ -67,6 +67,16 @@ func SetRTInfo(oref waveobj.ORef, info map[string]any) { fieldValue.SetString(valueStr) } else if valueBool, ok := value.(bool); ok && fieldValue.Kind() == reflect.Bool { fieldValue.SetBool(valueBool) + } else if fieldValue.Kind() == reflect.Int { + // Handle int fields - need to convert from various numeric types + switch v := value.(type) { + case int: + fieldValue.SetInt(int64(v)) + case int64: + fieldValue.SetInt(v) + case float64: + fieldValue.SetInt(int64(v)) + } } else if fieldValue.Kind() == reflect.Interface { // Handle any/interface{} fields fieldValue.Set(reflect.ValueOf(value)) From 94e87f0da65460605dc0ad15c95b0ba8bd8f8f37 Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 16 Oct 2025 20:26:38 -0700 Subject: [PATCH 07/17] implement OSC 7 for bash (supports bash v3, v4, and v5) --- .../shellutil/shellintegration/bash_bashrc.sh | 42 ++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/pkg/util/shellutil/shellintegration/bash_bashrc.sh b/pkg/util/shellutil/shellintegration/bash_bashrc.sh index dcddab93a5..f8960f06d6 100644 --- a/pkg/util/shellutil/shellintegration/bash_bashrc.sh +++ b/pkg/util/shellutil/shellintegration/bash_bashrc.sh @@ -28,4 +28,44 @@ fi unset WAVETERM_WSHBINDIR if type _init_completion &>/dev/null; then source <(wsh completion bash) -fi \ No newline at end of file +fi + +# shell integration +_waveterm_si_blocked() { + [[ -n "$TMUX" || -n "$STY" || "$TERM" == tmux* || "$TERM" == screen* ]] +} + +_waveterm_si_urlencode() { + local s="$1" + # Escape % first + s="${s//%/%25}" + # Common reserved characters in file paths + s="${s// /%20}" + s="${s//#/%23}" + s="${s//\?/%3F}" + s="${s//&/%26}" + s="${s//;/%3B}" + s="${s//+/%2B}" + printf '%s' "$s" +} + +_waveterm_si_osc7() { + _waveterm_si_blocked && return + local encoded_pwd=$(_waveterm_si_urlencode "$PWD") + printf '\033]7;file://%s%s\007' "$HOSTNAME" "$encoded_pwd" +} + +# Hook OSC 7 into PROMPT_COMMAND +_waveterm_si_prompt_command() { + _waveterm_si_osc7 +} + +# Append _waveterm_si_prompt_command to PROMPT_COMMAND (v3-safe) +_waveterm_si_append_pc() { + if [[ $(declare -p PROMPT_COMMAND 2>/dev/null) == "declare -a"* ]]; then + PROMPT_COMMAND+=(_waveterm_si_prompt_command) + else + PROMPT_COMMAND="${PROMPT_COMMAND:+$PROMPT_COMMAND$'\n'}_waveterm_si_prompt_command" + fi +} +_waveterm_si_append_pc \ No newline at end of file From 7d6fc62065f1959a0eac3e75618b458b8e824d71 Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 16 Oct 2025 20:42:20 -0700 Subject: [PATCH 08/17] add localshell type/version detection. make startupactivityupdate async. --- cmd/server/main-server.go | 9 ++- pkg/telemetry/telemetrydata/telemetrydata.go | 3 + pkg/util/shellutil/shellutil.go | 69 ++++++++++++++++++++ 3 files changed, 80 insertions(+), 1 deletion(-) diff --git a/cmd/server/main-server.go b/cmd/server/main-server.go index 760faa48e4..8afa873d00 100644 --- a/cmd/server/main-server.go +++ b/cmd/server/main-server.go @@ -218,6 +218,11 @@ func startupActivityUpdate(firstLaunch bool) { } autoUpdateChannel := telemetry.AutoUpdateChannel() autoUpdateEnabled := telemetry.IsAutoUpdateEnabled() + shellType, shellVersion, shellErr := shellutil.DetectShellTypeAndVersion() + if shellErr != nil { + shellType = "error" + shellVersion = "" + } props := telemetrydata.TEventProps{ UserSet: &telemetrydata.TEventUserProps{ ClientVersion: "v" + WaveVersion, @@ -227,6 +232,8 @@ func startupActivityUpdate(firstLaunch bool) { ClientIsDev: wavebase.IsDevMode(), AutoUpdateChannel: autoUpdateChannel, AutoUpdateEnabled: autoUpdateEnabled, + LocalShellType: shellType, + LocalShellVersion: shellVersion, }, UserSetOnce: &telemetrydata.TEventUserProps{ ClientInitialVersion: "v" + WaveVersion, @@ -401,7 +408,7 @@ func main() { go stdinReadWatch() go telemetryLoop() go updateTelemetryCountsLoop() - startupActivityUpdate(firstLaunch) // must be after startConfigWatcher() + go startupActivityUpdate(firstLaunch) // must be after startConfigWatcher() blocklogger.InitBlockLogger() webListener, err := web.MakeTCPListener("web") diff --git a/pkg/telemetry/telemetrydata/telemetrydata.go b/pkg/telemetry/telemetrydata/telemetrydata.go index 5865bedc46..9a68d092af 100644 --- a/pkg/telemetry/telemetrydata/telemetrydata.go +++ b/pkg/telemetry/telemetrydata/telemetrydata.go @@ -62,6 +62,9 @@ type TEventUserProps struct { AutoUpdateChannel string `json:"autoupdate:channel,omitempty"` AutoUpdateEnabled bool `json:"autoupdate:enabled,omitempty"` + LocalShellType string `json:"localshell:type,omitempty"` + LocalShellVersion string `json:"localshell:version,omitempty"` + LocCountryCode string `json:"loc:countrycode,omitempty"` LocRegionCode string `json:"loc:regioncode,omitempty"` diff --git a/pkg/util/shellutil/shellutil.go b/pkg/util/shellutil/shellutil.go index 489aae4a2b..f6b60b4d81 100644 --- a/pkg/util/shellutil/shellutil.go +++ b/pkg/util/shellutil/shellutil.go @@ -346,6 +346,75 @@ func GetShellTypeFromShellPath(shellPath string) string { return ShellType_unknown } +var ( + bashVersionRegexp = regexp.MustCompile(`\bversion\s+(\d+\.\d+)`) + zshVersionRegexp = regexp.MustCompile(`\bzsh\s+(\d+\.\d+)`) + fishVersionRegexp = regexp.MustCompile(`\bversion\s+(\d+\.\d+)`) + pwshVersionRegexp = regexp.MustCompile(`(?:PowerShell\s+)?(\d+\.\d+)`) +) + +func DetectShellTypeAndVersion() (string, string, error) { + shellPath := DetectLocalShellPath() + return DetectShellTypeAndVersionFromPath(shellPath) +} + +func DetectShellTypeAndVersionFromPath(shellPath string) (string, string, error) { + shellType := GetShellTypeFromShellPath(shellPath) + if shellType == ShellType_unknown { + return shellType, "", fmt.Errorf("unknown shell type: %s", shellPath) + } + + shellBase := filepath.Base(shellPath) + if shellType == ShellType_pwsh && strings.Contains(shellBase, "powershell") && !strings.Contains(shellBase, "pwsh") { + return "powershell", "", nil + } + + version, err := getShellVersion(shellPath, shellType) + if err != nil { + return shellType, "", err + } + + return shellType, version, nil +} + +func getShellVersion(shellPath string, shellType string) (string, error) { + ctx, cancelFn := context.WithTimeout(context.Background(), 2*time.Second) + defer cancelFn() + + var cmd *exec.Cmd + var versionRegex *regexp.Regexp + + switch shellType { + case ShellType_bash: + cmd = exec.CommandContext(ctx, shellPath, "--version") + versionRegex = bashVersionRegexp + case ShellType_zsh: + cmd = exec.CommandContext(ctx, shellPath, "--version") + versionRegex = zshVersionRegexp + case ShellType_fish: + cmd = exec.CommandContext(ctx, shellPath, "--version") + versionRegex = fishVersionRegexp + case ShellType_pwsh: + cmd = exec.CommandContext(ctx, shellPath, "--version") + versionRegex = pwshVersionRegexp + default: + return "", fmt.Errorf("unsupported shell type: %s", shellType) + } + + output, err := cmd.CombinedOutput() + if err != nil { + return "", fmt.Errorf("failed to get version for %s: %w", shellType, err) + } + + outputStr := strings.TrimSpace(string(output)) + matches := versionRegex.FindStringSubmatch(outputStr) + if len(matches) < 2 { + return "", fmt.Errorf("failed to parse version from output: %q", outputStr) + } + + return matches[1], nil +} + func FormatOSC(oscNum int, parts ...string) string { if len(parts) == 0 { return fmt.Sprintf("\x1b]%d\x07", oscNum) From f898b23acab048b53013ca53f590eaed1c0c17e6 Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 16 Oct 2025 21:34:29 -0700 Subject: [PATCH 09/17] generate a small system (OS + version + arch) string for AI state, add to tab context prompt. --- cmd/server/main-server.go | 1 + frontend/types/gotypes.d.ts | 4 +++ pkg/aiusechat/tools.go | 8 +++++- pkg/wavebase/wavebase.go | 52 +++++++++++++++++++++++++++++++++++++ 4 files changed, 64 insertions(+), 1 deletion(-) diff --git a/cmd/server/main-server.go b/cmd/server/main-server.go index 8afa873d00..427e6ad2e0 100644 --- a/cmd/server/main-server.go +++ b/cmd/server/main-server.go @@ -410,6 +410,7 @@ func main() { go updateTelemetryCountsLoop() go startupActivityUpdate(firstLaunch) // must be after startConfigWatcher() blocklogger.InitBlockLogger() + go wavebase.GetSystemSummary() // get this cached (used in AI) webListener, err := web.MakeTCPListener("web") if err != nil { diff --git a/frontend/types/gotypes.d.ts b/frontend/types/gotypes.d.ts index 2aa6c5f5ff..0d2270bcd2 100644 --- a/frontend/types/gotypes.d.ts +++ b/frontend/types/gotypes.d.ts @@ -938,6 +938,8 @@ declare global { "client:isdev"?: boolean; "autoupdate:channel"?: string; "autoupdate:enabled"?: boolean; + "localshell:type"?: string; + "localshell:version"?: string; "loc:countrycode"?: string; "loc:regioncode"?: string; "settings:customwidgets"?: number; @@ -1006,6 +1008,8 @@ declare global { "client:isdev"?: boolean; "autoupdate:channel"?: string; "autoupdate:enabled"?: boolean; + "localshell:type"?: string; + "localshell:version"?: string; "loc:countrycode"?: string; "loc:regioncode"?: string; "settings:customwidgets"?: number; diff --git a/pkg/aiusechat/tools.go b/pkg/aiusechat/tools.go index 091d149806..9bc4cbc7f4 100644 --- a/pkg/aiusechat/tools.go +++ b/pkg/aiusechat/tools.go @@ -11,6 +11,7 @@ import ( "github.com/google/uuid" "github.com/wavetermdev/waveterm/pkg/aiusechat/uctypes" "github.com/wavetermdev/waveterm/pkg/blockcontroller" + "github.com/wavetermdev/waveterm/pkg/wavebase" "github.com/wavetermdev/waveterm/pkg/waveobj" "github.com/wavetermdev/waveterm/pkg/wstore" ) @@ -156,9 +157,11 @@ func GenerateCurrentTabStatePrompt(blocks []*waveobj.Block, widgetAccess bool) s var prompt strings.Builder prompt.WriteString("\n") + prompt.WriteString(fmt.Sprintf("Local Machine: %s\n", wavebase.GetSystemSummary())) if len(widgetDescriptions) == 0 { prompt.WriteString("No widgets open\n") } else { + prompt.WriteString("Open Widgets:\n") for _, desc := range widgetDescriptions { prompt.WriteString("* ") prompt.WriteString(desc) @@ -166,7 +169,9 @@ func GenerateCurrentTabStatePrompt(blocks []*waveobj.Block, widgetAccess bool) s } } prompt.WriteString("") - return prompt.String() + rtn := prompt.String() + // log.Printf("%s\n", rtn) + return rtn } func generateToolsForTsunamiBlock(block *waveobj.Block) []uctypes.ToolDefinition { @@ -193,6 +198,7 @@ func generateToolsForTsunamiBlock(block *waveobj.Block) []uctypes.ToolDefinition return tools } +// Used for internal testing of tool loops func GetAdderToolDefinition() uctypes.ToolDefinition { return uctypes.ToolDefinition{ Name: "adder", diff --git a/pkg/wavebase/wavebase.go b/pkg/wavebase/wavebase.go index 1757429956..930b8a5e08 100644 --- a/pkg/wavebase/wavebase.go +++ b/pkg/wavebase/wavebase.go @@ -311,9 +311,61 @@ func UnameKernelRelease() string { return osRelease } +var systemSummaryOnce = &sync.Once{} +var systemSummary string + +func GetSystemSummary() string { + systemSummaryOnce.Do(func() { + ctx, cancelFn := context.WithTimeout(context.Background(), 2*time.Second) + defer cancelFn() + systemSummary = getSystemSummary(ctx) + }) + return systemSummary +} + func ValidateWshSupportedArch(os string, arch string) error { if SupportedWshBinaries[fmt.Sprintf("%s-%s", os, arch)] { return nil } return fmt.Errorf("unsupported wsh platform: %s-%s", os, arch) } + +func getSystemSummary(ctx context.Context) string { + osName := runtime.GOOS + + switch osName { + case "darwin": + out, _ := exec.CommandContext(ctx, "sw_vers", "-productVersion").Output() + return fmt.Sprintf("macOS %s (%s)", strings.TrimSpace(string(out)), runtime.GOARCH) + case "linux": + // Read /etc/os-release directly (standard location since 2012) + data, err := os.ReadFile("/etc/os-release") + var prettyName string + if err == nil { + for _, line := range strings.Split(string(data), "\n") { + line = strings.TrimSpace(line) + if strings.HasPrefix(line, "PRETTY_NAME=") { + prettyName = strings.Trim(strings.TrimPrefix(line, "PRETTY_NAME="), "\"") + break + } + } + } + if prettyName == "" { + prettyName = "Linux" + } else if !strings.Contains(strings.ToLower(prettyName), "linux") { + prettyName = "Linux " + prettyName + } + return fmt.Sprintf("%s (%s)", prettyName, runtime.GOARCH) + case "windows": + var details string + out, err := exec.CommandContext(ctx, "powershell", "-NoProfile", "-NonInteractive", "-Command", "(Get-CimInstance Win32_OperatingSystem).Caption").Output() + if err == nil && len(out) > 0 { + details = strings.TrimSpace(string(out)) + } else { + details = "Windows" + } + return fmt.Sprintf("%s (%s)", details, runtime.GOARCH) + default: + return fmt.Sprintf("%s (%s)", runtime.GOOS, runtime.GOARCH) + } +} From 8c04f012847c49249df9637b1fa925569bcda4ee Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 17 Oct 2025 08:56:28 -0700 Subject: [PATCH 10/17] add username to the prompt, say ~ expansion is allowed --- pkg/aiusechat/tools.go | 8 +++++++- pkg/aiusechat/tools_readdir.go | 2 +- pkg/aiusechat/tools_readfile.go | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/pkg/aiusechat/tools.go b/pkg/aiusechat/tools.go index 9bc4cbc7f4..2a504caa2e 100644 --- a/pkg/aiusechat/tools.go +++ b/pkg/aiusechat/tools.go @@ -6,6 +6,7 @@ package aiusechat import ( "context" "fmt" + "os/user" "strings" "github.com/google/uuid" @@ -157,7 +158,12 @@ func GenerateCurrentTabStatePrompt(blocks []*waveobj.Block, widgetAccess bool) s var prompt strings.Builder prompt.WriteString("\n") - prompt.WriteString(fmt.Sprintf("Local Machine: %s\n", wavebase.GetSystemSummary())) + systemInfo := wavebase.GetSystemSummary() + if currentUser, err := user.Current(); err == nil && currentUser.Username != "" { + prompt.WriteString(fmt.Sprintf("Local Machine: %s, User: %s\n", systemInfo, currentUser.Username)) + } else { + prompt.WriteString(fmt.Sprintf("Local Machine: %s\n", systemInfo)) + } if len(widgetDescriptions) == 0 { prompt.WriteString("No widgets open\n") } else { diff --git a/pkg/aiusechat/tools_readdir.go b/pkg/aiusechat/tools_readdir.go index b887427493..8a25547b93 100644 --- a/pkg/aiusechat/tools_readdir.go +++ b/pkg/aiusechat/tools_readdir.go @@ -192,7 +192,7 @@ func GetReadDirToolDefinition() uctypes.ToolDefinition { "properties": map[string]any{ "path": map[string]any{ "type": "string", - "description": "Path to the directory to read", + "description": "Path to the directory to read. Supports '~' for the user's home directory.", }, "max_entries": map[string]any{ "type": "integer", diff --git a/pkg/aiusechat/tools_readfile.go b/pkg/aiusechat/tools_readfile.go index cab94fd26c..459ed3c4b9 100644 --- a/pkg/aiusechat/tools_readfile.go +++ b/pkg/aiusechat/tools_readfile.go @@ -202,7 +202,7 @@ func GetReadTextFileToolDefinition() uctypes.ToolDefinition { "properties": map[string]any{ "filename": map[string]any{ "type": "string", - "description": "Path to the file to read", + "description": "Path to the file to read. Supports '~' for the user's home directory.", }, "origin": map[string]any{ "type": "string", From 202555a01895484a841a736922bdda33100de64e Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 17 Oct 2025 09:02:37 -0700 Subject: [PATCH 11/17] decode pathname --- frontend/app/view/term/termwrap.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/app/view/term/termwrap.ts b/frontend/app/view/term/termwrap.ts index 74ef9f27e8..9f10b2d3b8 100644 --- a/frontend/app/view/term/termwrap.ts +++ b/frontend/app/view/term/termwrap.ts @@ -129,7 +129,7 @@ function handleOsc7Command(data: string, blockId: string, loaded: boolean): bool console.log("Invalid OSC 7 command received (non-file protocol)", data); return true; } - pathPart = url.pathname; + pathPart = decodeURIComponent(url.pathname); } catch (e) { console.log("Invalid OSC 7 command received (parse error)", data, e); return true; From 2e7ccf4f24fa0693fd033316720afd409b09dd5a Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 17 Oct 2025 11:13:56 -0700 Subject: [PATCH 12/17] decode windows paths correctly for OSC 7 --- frontend/app/view/term/termwrap.ts | 6 ++++++ frontend/util/platformutil.ts | 5 +++++ 2 files changed, 11 insertions(+) diff --git a/frontend/app/view/term/termwrap.ts b/frontend/app/view/term/termwrap.ts index 9f10b2d3b8..c56b9fac28 100644 --- a/frontend/app/view/term/termwrap.ts +++ b/frontend/app/view/term/termwrap.ts @@ -130,6 +130,12 @@ function handleOsc7Command(data: string, blockId: string, loaded: boolean): bool return true; } pathPart = decodeURIComponent(url.pathname); + + // Handle Windows paths (e.g., /C:/... or /D:\...) + if (/^\/[a-zA-Z]:[\\/]/.test(pathPart)) { + // Strip leading slash and normalize to forward slashes + pathPart = pathPart.substring(1).replace(/\\/g, "/"); + } } catch (e) { console.log("Invalid OSC 7 command received (parse error)", data, e); return true; diff --git a/frontend/util/platformutil.ts b/frontend/util/platformutil.ts index 08deefca44..ef4d2f9e0c 100644 --- a/frontend/util/platformutil.ts +++ b/frontend/util/platformutil.ts @@ -1,4 +1,5 @@ export const PlatformMacOS = "darwin"; +export const PlatformWindows = "win32"; export let PLATFORM: NodeJS.Platform = PlatformMacOS; export function setPlatform(platform: NodeJS.Platform) { @@ -9,6 +10,10 @@ export function isMacOS(): boolean { return PLATFORM == PlatformMacOS; } +export function isWindows(): boolean { + return PLATFORM == PlatformWindows; +} + export function makeNativeLabel(isDirectory: boolean) { let managerName: string; if (!isDirectory) { From e9758b5b67a09068ae99803385f2c0d749bffdb2 Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 17 Oct 2025 11:43:27 -0700 Subject: [PATCH 13/17] fix bug in zshrc (bad backslash), maxlength for cmd64 blocks in "C" case of osc 16162 --- frontend/app/view/term/termwrap.ts | 17 +++++++++++------ .../shellutil/shellintegration/zsh_zshrc.sh | 2 +- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/frontend/app/view/term/termwrap.ts b/frontend/app/view/term/termwrap.ts index c56b9fac28..7453a5c58d 100644 --- a/frontend/app/view/term/termwrap.ts +++ b/frontend/app/view/term/termwrap.ts @@ -204,12 +204,17 @@ function handleOsc16162Command(data: string, blockId: string, loaded: boolean, t case "C": rtInfo["shell:state"] = "running-command"; if (cmd.data.cmd64) { - try { - const decodedCmd = atob(cmd.data.cmd64); - rtInfo["shell:lastcmd"] = decodedCmd; - } catch (e) { - console.error("Error decoding cmd64:", e); - rtInfo["shell:lastcmd"] = null; + const decodedLen = Math.ceil(cmd.data.cmd64.length * 0.75); + if (decodedLen > 8192) { + rtInfo["shell:lastcmd"] = `# command too large to store (${decodedLen} bytes)`; + } else { + try { + const decodedCmd = atob(cmd.data.cmd64); + rtInfo["shell:lastcmd"] = decodedCmd; + } catch (e) { + console.error("Error decoding cmd64:", e); + rtInfo["shell:lastcmd"] = null; + } } } else { rtInfo["shell:lastcmd"] = null; diff --git a/pkg/util/shellutil/shellintegration/zsh_zshrc.sh b/pkg/util/shellutil/shellintegration/zsh_zshrc.sh index d077bc5a7d..d6baef6f0c 100644 --- a/pkg/util/shellutil/shellintegration/zsh_zshrc.sh +++ b/pkg/util/shellutil/shellintegration/zsh_zshrc.sh @@ -37,7 +37,7 @@ _waveterm_si_urlencode() { s=${s//#/%23} s=${s//\?/%3F} s=${s//&/%26} - s=${s//;/\%3B} + s=${s//;/%3B} s=${s//+/%2B} printf '%s' "$s" fi From cd7650c8b5967f8efcc02bf55c0c43ba8d9afc80 Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 17 Oct 2025 11:46:35 -0700 Subject: [PATCH 14/17] fix newlines in base64 output --- pkg/util/shellutil/shellintegration/zsh_zshrc.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/util/shellutil/shellintegration/zsh_zshrc.sh b/pkg/util/shellutil/shellintegration/zsh_zshrc.sh index d6baef6f0c..a0b0656f57 100644 --- a/pkg/util/shellutil/shellintegration/zsh_zshrc.sh +++ b/pkg/util/shellutil/shellintegration/zsh_zshrc.sh @@ -67,7 +67,7 @@ _waveterm_si_precmd() { _waveterm_si_preexec() { _waveterm_si_blocked && return local cmd64 - cmd64=$(printf '%s' "$1" | base64 2>/dev/null) + cmd64=$(printf '%s' "$1" | base64 2>/dev/null | tr -d '\n\r') if [ -n "$cmd64" ]; then printf '\033]16162;C;{"cmd64":"%s"}\007' "$cmd64" else From b5ebb4e1d7d35c5ffe253428fa02c0400ad33ecd Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 17 Oct 2025 11:48:31 -0700 Subject: [PATCH 15/17] remove debugging console.log --- frontend/app/view/term/termwrap.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/frontend/app/view/term/termwrap.ts b/frontend/app/view/term/termwrap.ts index 7453a5c58d..b7d7c464ee 100644 --- a/frontend/app/view/term/termwrap.ts +++ b/frontend/app/view/term/termwrap.ts @@ -174,8 +174,6 @@ function handleOsc16162Command(data: string, blockId: string, loaded: boolean, t if (!loaded) { return true; } - console.log("OSC 16162 received:", data, "blockId:", blockId); - if (!data || data.length === 0) { return true; } @@ -184,7 +182,6 @@ function handleOsc16162Command(data: string, blockId: string, loaded: boolean, t const commandStr = parts[0]; const jsonDataStr = parts.length > 1 ? parts.slice(1).join(";") : null; let parsedData: Record = {}; - if (jsonDataStr) { try { parsedData = JSON.parse(jsonDataStr); @@ -194,9 +191,7 @@ function handleOsc16162Command(data: string, blockId: string, loaded: boolean, t } const cmd: Osc16162Command = { command: commandStr, data: parsedData } as Osc16162Command; - const rtInfo: ObjRTInfo = {}; - switch (cmd.command) { case "A": rtInfo["shell:state"] = "ready"; From f14a3e6f83c89b16ef9a52afc8153862600ebf26 Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 17 Oct 2025 11:52:21 -0700 Subject: [PATCH 16/17] use const, make platfomutil consistent across tsunami --- frontend/util/platformutil.ts | 5 ++++- tsunami/frontend/src/util/platformutil.ts | 24 ++++++----------------- 2 files changed, 10 insertions(+), 19 deletions(-) diff --git a/frontend/util/platformutil.ts b/frontend/util/platformutil.ts index ef4d2f9e0c..1a73fce55d 100644 --- a/frontend/util/platformutil.ts +++ b/frontend/util/platformutil.ts @@ -1,3 +1,6 @@ +// Copyright 2025, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + export const PlatformMacOS = "darwin"; export const PlatformWindows = "win32"; export let PLATFORM: NodeJS.Platform = PlatformMacOS; @@ -20,7 +23,7 @@ export function makeNativeLabel(isDirectory: boolean) { managerName = "Default Application"; } else if (PLATFORM === PlatformMacOS) { managerName = "Finder"; - } else if (PLATFORM == "win32") { + } else if (PLATFORM == PlatformWindows) { managerName = "Explorer"; } else { managerName = "File Manager"; diff --git a/tsunami/frontend/src/util/platformutil.ts b/tsunami/frontend/src/util/platformutil.ts index 410f248fb3..78dc143073 100644 --- a/tsunami/frontend/src/util/platformutil.ts +++ b/tsunami/frontend/src/util/platformutil.ts @@ -2,29 +2,17 @@ // SPDX-License-Identifier: Apache-2.0 export const PlatformMacOS = "darwin"; +export const PlatformWindows = "win32"; export let PLATFORM: NodeJS.Platform = PlatformMacOS; export function setPlatform(platform: NodeJS.Platform) { PLATFORM = platform; } -export function makeNativeLabel(isDirectory: boolean) { - let managerName: string; - if (!isDirectory) { - managerName = "Default Application"; - } else if (PLATFORM === PlatformMacOS) { - managerName = "Finder"; - } else if (PLATFORM == "win32") { - managerName = "Explorer"; - } else { - managerName = "File Manager"; - } +export function isMacOS(): boolean { + return PLATFORM == PlatformMacOS; +} - let fileAction: string; - if (isDirectory) { - fileAction = "Reveal"; - } else { - fileAction = "Open File"; - } - return `${fileAction} in ${managerName}`; +export function isWindows(): boolean { + return PLATFORM == PlatformWindows; } From 7fa802769fa564fc016e94494340ef9c378a0208 Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 17 Oct 2025 11:58:52 -0700 Subject: [PATCH 17/17] more zshrc fixups. dont send large commands, and fix autoload ordering --- frontend/app/view/term/termwrap.ts | 2 +- .../shellutil/shellintegration/zsh_zshrc.sh | 17 ++++++++++++----- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/frontend/app/view/term/termwrap.ts b/frontend/app/view/term/termwrap.ts index b7d7c464ee..6409a86bd7 100644 --- a/frontend/app/view/term/termwrap.ts +++ b/frontend/app/view/term/termwrap.ts @@ -201,7 +201,7 @@ function handleOsc16162Command(data: string, blockId: string, loaded: boolean, t if (cmd.data.cmd64) { const decodedLen = Math.ceil(cmd.data.cmd64.length * 0.75); if (decodedLen > 8192) { - rtInfo["shell:lastcmd"] = `# command too large to store (${decodedLen} bytes)`; + rtInfo["shell:lastcmd"] = `# command too large (${decodedLen} bytes)`; } else { try { const decodedCmd = atob(cmd.data.cmd64); diff --git a/pkg/util/shellutil/shellintegration/zsh_zshrc.sh b/pkg/util/shellutil/shellintegration/zsh_zshrc.sh index a0b0656f57..89054f39d2 100644 --- a/pkg/util/shellutil/shellintegration/zsh_zshrc.sh +++ b/pkg/util/shellutil/shellintegration/zsh_zshrc.sh @@ -66,12 +66,19 @@ _waveterm_si_precmd() { _waveterm_si_preexec() { _waveterm_si_blocked && return - local cmd64 - cmd64=$(printf '%s' "$1" | base64 2>/dev/null | tr -d '\n\r') - if [ -n "$cmd64" ]; then + local cmd_length=${#1} + if [ "$cmd_length" -gt 8192 ]; then + local cmd64 + cmd64=$(printf '# command too large (%d bytes)' "$cmd_length" | base64 2>/dev/null | tr -d '\n\r') printf '\033]16162;C;{"cmd64":"%s"}\007' "$cmd64" else - printf '\033]16162;C\007' + local cmd64 + cmd64=$(printf '%s' "$1" | base64 2>/dev/null | tr -d '\n\r') + if [ -n "$cmd64" ]; then + printf '\033]16162;C;{"cmd64":"%s"}\007' "$cmd64" + else + printf '\033]16162;C\007' + fi fi } @@ -95,8 +102,8 @@ _waveterm_si_inputempty() { fi } +autoload -Uz add-zle-hook-widget 2>/dev/null if (( $+functions[add-zle-hook-widget] )); then - autoload -Uz add-zle-hook-widget add-zle-hook-widget zle-line-init _waveterm_si_inputempty add-zle-hook-widget zle-line-pre-redraw _waveterm_si_inputempty fi