From e8bc012f2b7a4ae39db0877b1e2ec3a8946bd949 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 20 Oct 2025 10:15:38 -0700 Subject: [PATCH 01/16] implement osc 16162;M for bash --- .../shellutil/shellintegration/bash_bashrc.sh | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/pkg/util/shellutil/shellintegration/bash_bashrc.sh b/pkg/util/shellutil/shellintegration/bash_bashrc.sh index f8960f06d6..749b19d081 100644 --- a/pkg/util/shellutil/shellintegration/bash_bashrc.sh +++ b/pkg/util/shellutil/shellintegration/bash_bashrc.sh @@ -30,6 +30,8 @@ if type _init_completion &>/dev/null; then source <(wsh completion bash) fi +_WAVETERM_SI_FIRSTPROMPT=1 + # shell integration _waveterm_si_blocked() { [[ -n "$TMUX" || -n "$STY" || "$TERM" == tmux* || "$TERM" == screen* ]] @@ -55,9 +57,19 @@ _waveterm_si_osc7() { printf '\033]7;file://%s%s\007' "$HOSTNAME" "$encoded_pwd" } -# Hook OSC 7 into PROMPT_COMMAND _waveterm_si_prompt_command() { - _waveterm_si_osc7 + local _waveterm_si_status=$? + _waveterm_si_blocked && return + if [ "$_WAVETERM_SI_FIRSTPROMPT" -eq 1 ]; then + local uname_info + uname_info=$(uname -smr 2>/dev/null) + printf '\033]16162;M;{"shell":"bash","shellversion":"%s","uname":"%s"}\007' "$BASH_VERSION" "$uname_info" + _waveterm_si_osc7 + else + printf '\033]16162;D;{"exitcode":%d}\007' $_waveterm_si_status + fi + printf '\033]16162;A\007' + _WAVETERM_SI_FIRSTPROMPT=0 } # Append _waveterm_si_prompt_command to PROMPT_COMMAND (v3-safe) From 85ddfb0f8a1208cecfe911f8fb66ab5eac70f13c Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 20 Oct 2025 10:47:28 -0700 Subject: [PATCH 02/16] implement more osc 16162 for fish. A, C, D, M --- .../shellintegration/fish_wavefish.sh | 34 +++++++++++++++++-- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/pkg/util/shellutil/shellintegration/fish_wavefish.sh b/pkg/util/shellutil/shellintegration/fish_wavefish.sh index de0b623265..9a83fd9144 100644 --- a/pkg/util/shellutil/shellintegration/fish_wavefish.sh +++ b/pkg/util/shellutil/shellintegration/fish_wavefish.sh @@ -9,6 +9,8 @@ set -e WAVETERM_SWAPTOKEN # Load Wave completions wsh completion fish | source +set -g _WAVETERM_SI_FIRSTPROMPT 1 + # shell integration function _waveterm_si_blocked # Check if we're in tmux or screen (using fish-native checks) @@ -18,13 +20,39 @@ end function _waveterm_si_osc7 _waveterm_si_blocked; and return # Use fish-native URL encoding - set -l encoded_pwd (string escape --style=url -- $PWD) + set -l encoded_pwd (string escape --style=url -- "$PWD") printf '\033]7;file://%s%s\007' $hostname $encoded_pwd end -# Hook OSC 7 to prompt and directory changes function _waveterm_si_prompt --on-event fish_prompt - _waveterm_si_osc7 + set -l _waveterm_si_status $status + _waveterm_si_blocked; and return + if test $_WAVETERM_SI_FIRSTPROMPT -eq 1 + set -l uname_info (uname -smr 2>/dev/null) + printf '\033]16162;M;{"shell":"fish","shellversion":"%s","uname":"%s"}\007' $FISH_VERSION "$uname_info" + _waveterm_si_osc7 + else + printf '\033]16162;D;{"exitcode":%d}\007' $_waveterm_si_status + end + printf '\033]16162;A\007' + set -g _WAVETERM_SI_FIRSTPROMPT 0 +end + +function _waveterm_si_preexec --on-event fish_preexec + _waveterm_si_blocked; and return + set -l cmd (string join -- ' ' $argv) + set -l cmd_length (string length -- "$cmd") + if test $cmd_length -gt 8192 + set -l cmd64 (printf '# command too large (%d bytes)' $cmd_length | base64 2>/dev/null | string replace -a '\n' '' | string replace -a '\r' '') + printf '\033]16162;C;{"cmd64":"%s"}\007' "$cmd64" + else + set -l cmd64 (printf '%s' "$cmd" | base64 2>/dev/null | string replace -a '\n' '' | string replace -a '\r' '') + if test -n "$cmd64" + printf '\033]16162;C;{"cmd64":"%s"}\007' "$cmd64" + else + printf '\033]16162;C\007' + end + end end # Also update on directory change From 4930a81d9cf962f4cfbe0873a905f81cc1888d26 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 20 Oct 2025 11:02:57 -0700 Subject: [PATCH 03/16] implement OSC 16162;M for pwsh --- pkg/util/shellutil/shellintegration/pwsh_wavepwsh.sh | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/pkg/util/shellutil/shellintegration/pwsh_wavepwsh.sh b/pkg/util/shellutil/shellintegration/pwsh_wavepwsh.sh index 2dfe45703d..3cf9e06262 100644 --- a/pkg/util/shellutil/shellintegration/pwsh_wavepwsh.sh +++ b/pkg/util/shellutil/shellintegration/pwsh_wavepwsh.sh @@ -12,6 +12,8 @@ Remove-Item Env:WAVETERM_SWAPTOKEN # Load Wave completions wsh completion powershell | Out-String | Invoke-Expression +$Global:_WAVETERM_SI_FIRSTPROMPT = $true + # shell integration function Global:_waveterm_si_blocked { # Check if we're in tmux or screen @@ -34,8 +36,16 @@ function Global:_waveterm_si_osc7 { Write-Host -NoNewline "`e]7;file://$hostname/$encoded_pwd`a" } -# Hook OSC 7 to prompt function Global:_waveterm_si_prompt { + if (_waveterm_si_blocked) { return } + + if ($Global:_WAVETERM_SI_FIRSTPROMPT) { + # not sending uname + $shellversion = $PSVersionTable.PSVersion.ToString() + Write-Host -NoNewline "`e]16162;M;{`"shell`":`"pwsh`",`"shellversion`":`"$shellversion`"}`a" + $Global:_WAVETERM_SI_FIRSTPROMPT = $false + } + _waveterm_si_osc7 } From 481040ccf1c3ddb0e4c82e06df3a3d9f33d53352 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 20 Oct 2025 11:19:40 -0700 Subject: [PATCH 04/16] fix UNC parsing... look for /\\ and then remove leading slash. keep backslashes for UNC --- frontend/app/view/term/termwrap.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/frontend/app/view/term/termwrap.ts b/frontend/app/view/term/termwrap.ts index 7f623cf39b..a2bba1112a 100644 --- a/frontend/app/view/term/termwrap.ts +++ b/frontend/app/view/term/termwrap.ts @@ -141,6 +141,12 @@ function handleOsc7Command(data: string, blockId: string, loaded: boolean): bool // Strip leading slash and normalize to forward slashes pathPart = pathPart.substring(1).replace(/\\/g, "/"); } + + // Handle UNC paths (e.g., /\\server\share) + if (pathPart.startsWith("/\\\\")) { + // Strip leading slash but keep backslashes for UNC + pathPart = pathPart.substring(1); + } } catch (e) { console.log("Invalid OSC 7 command received (parse error)", data, e); return true; From 7e689db9b77e4746ebbee61e8077c643eb3e96df Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 20 Oct 2025 11:32:31 -0700 Subject: [PATCH 05/16] add bash preexec library (v0.6.0), w/ copyright + license --- .../shellintegration/bash_preexec.sh | 402 ++++++++++++++++++ 1 file changed, 402 insertions(+) create mode 100644 pkg/util/shellutil/shellintegration/bash_preexec.sh diff --git a/pkg/util/shellutil/shellintegration/bash_preexec.sh b/pkg/util/shellutil/shellintegration/bash_preexec.sh new file mode 100644 index 0000000000..3cab537b3a --- /dev/null +++ b/pkg/util/shellutil/shellintegration/bash_preexec.sh @@ -0,0 +1,402 @@ +# License for bash-preexec.sh follows below from https://github.com/rcaloras/bash-preexec v0.6.0 +# ----------------------------------------------------------------------------- +# The MIT License +# +# Copyright (c) 2017 Ryan Caloras and contributors (see https://github.com/rcaloras/bash-preexec) +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +# ----------------------------------------------------------------------------- +# +# bash-preexec.sh -- Bash support for ZSH-like 'preexec' and 'precmd' functions. +# https://github.com/rcaloras/bash-preexec +# +# +# 'preexec' functions are executed before each interactive command is +# executed, with the interactive command as its argument. The 'precmd' +# function is executed before each prompt is displayed. +# +# Author: Ryan Caloras (ryan@bashhub.com) +# Forked from Original Author: Glyph Lefkowitz +# +# V0.6.0 +# + +# General Usage: +# +# 1. Source this file at the end of your bash profile so as not to interfere +# with anything else that's using PROMPT_COMMAND. +# +# 2. Add any precmd or preexec functions by appending them to their arrays: +# e.g. +# precmd_functions+=(my_precmd_function) +# precmd_functions+=(some_other_precmd_function) +# +# preexec_functions+=(my_preexec_function) +# +# 3. Consider changing anything using the DEBUG trap or PROMPT_COMMAND +# to use preexec and precmd instead. Preexisting usages will be +# preserved, but doing so manually may be less surprising. +# +# Note: This module requires two Bash features which you must not otherwise be +# using: the "DEBUG" trap, and the "PROMPT_COMMAND" variable. If you override +# either of these after bash-preexec has been installed it will most likely break. + +# Tell shellcheck what kind of file this is. +# shellcheck shell=bash + +# Make sure this is bash that's running and return otherwise. +# Use POSIX syntax for this line: +if [ -z "${BASH_VERSION-}" ]; then + return 1 +fi + +# We only support Bash 3.1+. +# Note: BASH_VERSINFO is first available in Bash-2.0. +if [[ -z "${BASH_VERSINFO-}" ]] || (( BASH_VERSINFO[0] < 3 || (BASH_VERSINFO[0] == 3 && BASH_VERSINFO[1] < 1) )); then + return 1 +fi + +# Avoid duplicate inclusion +if [[ -n "${bash_preexec_imported:-}" || -n "${__bp_imported:-}" ]]; then + return 0 +fi +bash_preexec_imported="defined" + +# WARNING: This variable is no longer used and should not be relied upon. +# Use ${bash_preexec_imported} instead. +# shellcheck disable=SC2034 +__bp_imported="${bash_preexec_imported}" + +# Should be available to each precmd and preexec +# functions, should they want it. $? and $_ are available as $? and $_, but +# $PIPESTATUS is available only in a copy, $BP_PIPESTATUS. +# TODO: Figure out how to restore PIPESTATUS before each precmd or preexec +# function. +__bp_last_ret_value="$?" +BP_PIPESTATUS=("${PIPESTATUS[@]}") +__bp_last_argument_prev_command="$_" + +__bp_inside_precmd=0 +__bp_inside_preexec=0 + +# Initial PROMPT_COMMAND string that is removed from PROMPT_COMMAND post __bp_install +__bp_install_string=$'__bp_trap_string="$(trap -p DEBUG)"\ntrap - DEBUG\n__bp_install' + +# Fails if any of the given variables are readonly +# Reference https://stackoverflow.com/a/4441178 +__bp_require_not_readonly() { + local var + for var; do + if ! ( unset "$var" 2> /dev/null ); then + echo "bash-preexec requires write access to ${var}" >&2 + return 1 + fi + done +} + +# Remove ignorespace and or replace ignoreboth from HISTCONTROL +# so we can accurately invoke preexec with a command from our +# history even if it starts with a space. +__bp_adjust_histcontrol() { + local histcontrol + histcontrol="${HISTCONTROL:-}" + histcontrol="${histcontrol//ignorespace}" + # Replace ignoreboth with ignoredups + if [[ "$histcontrol" == *"ignoreboth"* ]]; then + histcontrol="ignoredups:${histcontrol//ignoreboth}" + fi + export HISTCONTROL="$histcontrol" +} + +# This variable describes whether we are currently in "interactive mode"; +# i.e. whether this shell has just executed a prompt and is waiting for user +# input. It documents whether the current command invoked by the trace hook is +# run interactively by the user; it's set immediately after the prompt hook, +# and unset as soon as the trace hook is run. +__bp_preexec_interactive_mode="" + +# These arrays are used to add functions to be run before, or after, prompts. +declare -a precmd_functions +declare -a preexec_functions + +# Trims leading and trailing whitespace from $2 and writes it to the variable +# name passed as $1 +__bp_trim_whitespace() { + local var=${1:?} text=${2:-} + text="${text#"${text%%[![:space:]]*}"}" # remove leading whitespace characters + text="${text%"${text##*[![:space:]]}"}" # remove trailing whitespace characters + printf -v "$var" '%s' "$text" +} + + +# Trims whitespace and removes any leading or trailing semicolons from $2 and +# writes the resulting string to the variable name passed as $1. Used for +# manipulating substrings in PROMPT_COMMAND +__bp_sanitize_string() { + local var=${1:?} text=${2:-} sanitized + __bp_trim_whitespace sanitized "$text" + sanitized=${sanitized%;} + sanitized=${sanitized#;} + __bp_trim_whitespace sanitized "$sanitized" + printf -v "$var" '%s' "$sanitized" +} + +# This function is installed as part of the PROMPT_COMMAND; +# It sets a variable to indicate that the prompt was just displayed, +# to allow the DEBUG trap to know that the next command is likely interactive. +__bp_interactive_mode() { + __bp_preexec_interactive_mode="on" +} + + +# This function is installed as part of the PROMPT_COMMAND. +# It will invoke any functions defined in the precmd_functions array. +__bp_precmd_invoke_cmd() { + # Save the returned value from our last command, and from each process in + # its pipeline. Note: this MUST be the first thing done in this function. + # BP_PIPESTATUS may be unused, ignore + # shellcheck disable=SC2034 + + __bp_last_ret_value="$?" BP_PIPESTATUS=("${PIPESTATUS[@]}") + + # Don't invoke precmds if we are inside an execution of an "original + # prompt command" by another precmd execution loop. This avoids infinite + # recursion. + if (( __bp_inside_precmd > 0 )); then + return + fi + local __bp_inside_precmd=1 + + # Invoke every function defined in our function array. + local precmd_function + for precmd_function in "${precmd_functions[@]}"; do + + # Only execute this function if it actually exists. + # Test existence of functions with: declare -[Ff] + if type -t "$precmd_function" 1>/dev/null; then + __bp_set_ret_value "$__bp_last_ret_value" "$__bp_last_argument_prev_command" + # Quote our function invocation to prevent issues with IFS + "$precmd_function" + fi + done + + __bp_set_ret_value "$__bp_last_ret_value" +} + +# Sets a return value in $?. We may want to get access to the $? variable in our +# precmd functions. This is available for instance in zsh. We can simulate it in bash +# by setting the value here. +__bp_set_ret_value() { + return ${1:+"$1"} +} + +__bp_in_prompt_command() { + + local prompt_command_array IFS=$'\n;' + read -rd '' -a prompt_command_array <<< "${PROMPT_COMMAND[*]:-}" + + local trimmed_arg + __bp_trim_whitespace trimmed_arg "${1:-}" + + local command trimmed_command + for command in "${prompt_command_array[@]:-}"; do + __bp_trim_whitespace trimmed_command "$command" + if [[ "$trimmed_command" == "$trimmed_arg" ]]; then + return 0 + fi + done + + return 1 +} + +# This function is installed as the DEBUG trap. It is invoked before each +# interactive prompt display. Its purpose is to inspect the current +# environment to attempt to detect if the current command is being invoked +# interactively, and invoke 'preexec' if so. +__bp_preexec_invoke_exec() { + + # Save the contents of $_ so that it can be restored later on. + # https://stackoverflow.com/questions/40944532/bash-preserve-in-a-debug-trap#40944702 + __bp_last_argument_prev_command="${1:-}" + # Don't invoke preexecs if we are inside of another preexec. + if (( __bp_inside_preexec > 0 )); then + return + fi + local __bp_inside_preexec=1 + + # Checks if the file descriptor is not standard out (i.e. '1') + # __bp_delay_install checks if we're in test. Needed for bats to run. + # Prevents preexec from being invoked for functions in PS1 + if [[ ! -t 1 && -z "${__bp_delay_install:-}" ]]; then + return + fi + + if [[ -n "${COMP_POINT:-}" || -n "${READLINE_POINT:-}" ]]; then + # We're in the middle of a completer or a keybinding set up by "bind + # -x". This obviously can't be an interactively issued command. + return + fi + if [[ -z "${__bp_preexec_interactive_mode:-}" ]]; then + # We're doing something related to displaying the prompt. Let the + # prompt set the title instead of me. + return + else + # If we're in a subshell, then the prompt won't be re-displayed to put + # us back into interactive mode, so let's not set the variable back. + # In other words, if you have a subshell like + # (sleep 1; sleep 2) + # You want to see the 'sleep 2' as a set_command_title as well. + if [[ 0 -eq "${BASH_SUBSHELL:-}" ]]; then + __bp_preexec_interactive_mode="" + fi + fi + + if __bp_in_prompt_command "${BASH_COMMAND:-}"; then + # If we're executing something inside our prompt_command then we don't + # want to call preexec. Bash prior to 3.1 can't detect this at all :/ + __bp_preexec_interactive_mode="" + return + fi + + local this_command + this_command=$(LC_ALL=C HISTTIMEFORMAT='' builtin history 1) + this_command="${this_command#*[[:digit:]][* ] }" + + # Sanity check to make sure we have something to invoke our function with. + if [[ -z "$this_command" ]]; then + return + fi + + # Invoke every function defined in our function array. + local preexec_function + local preexec_function_ret_value + local preexec_ret_value=0 + for preexec_function in "${preexec_functions[@]:-}"; do + + # Only execute each function if it actually exists. + # Test existence of function with: declare -[fF] + if type -t "$preexec_function" 1>/dev/null; then + __bp_set_ret_value "${__bp_last_ret_value:-}" + # Quote our function invocation to prevent issues with IFS + "$preexec_function" "$this_command" + preexec_function_ret_value="$?" + if [[ "$preexec_function_ret_value" != 0 ]]; then + preexec_ret_value="$preexec_function_ret_value" + fi + fi + done + + # Restore the last argument of the last executed command, and set the return + # value of the DEBUG trap to be the return code of the last preexec function + # to return an error. + # If `extdebug` is enabled a non-zero return value from any preexec function + # will cause the user's command not to execute. + # Run `shopt -s extdebug` to enable + __bp_set_ret_value "$preexec_ret_value" "$__bp_last_argument_prev_command" +} + +__bp_install() { + # Exit if we already have this installed. + if [[ "${PROMPT_COMMAND[*]:-}" == *"__bp_precmd_invoke_cmd"* ]]; then + return 1 + fi + + trap '__bp_preexec_invoke_exec "$_"' DEBUG + + # Preserve any prior DEBUG trap as a preexec function + eval "local trap_argv=(${__bp_trap_string:-})" + local prior_trap=${trap_argv[2]:-} + unset __bp_trap_string + if [[ -n "$prior_trap" ]]; then + eval '__bp_original_debug_trap() { + '"$prior_trap"' + }' + preexec_functions+=(__bp_original_debug_trap) + fi + + # Adjust our HISTCONTROL Variable if needed. + __bp_adjust_histcontrol + + # Issue #25. Setting debug trap for subshells causes sessions to exit for + # backgrounded subshell commands (e.g. (pwd)& ). Believe this is a bug in Bash. + # + # Disabling this by default. It can be enabled by setting this variable. + if [[ -n "${__bp_enable_subshells:-}" ]]; then + + # Set so debug trap will work be invoked in subshells. + set -o functrace > /dev/null 2>&1 + shopt -s extdebug > /dev/null 2>&1 + fi + + local existing_prompt_command + # Remove setting our trap install string and sanitize the existing prompt command string + existing_prompt_command="${PROMPT_COMMAND:-}" + # Edge case of appending to PROMPT_COMMAND + existing_prompt_command="${existing_prompt_command//$__bp_install_string/:}" # no-op + existing_prompt_command="${existing_prompt_command//$'\n':$'\n'/$'\n'}" # remove known-token only + existing_prompt_command="${existing_prompt_command//$'\n':;/$'\n'}" # remove known-token only + __bp_sanitize_string existing_prompt_command "$existing_prompt_command" + if [[ "${existing_prompt_command:-:}" == ":" ]]; then + existing_prompt_command= + fi + + # Install our hooks in PROMPT_COMMAND to allow our trap to know when we've + # actually entered something. + PROMPT_COMMAND='__bp_precmd_invoke_cmd' + PROMPT_COMMAND+=${existing_prompt_command:+$'\n'$existing_prompt_command} + if (( BASH_VERSINFO[0] > 5 || (BASH_VERSINFO[0] == 5 && BASH_VERSINFO[1] >= 1) )); then + PROMPT_COMMAND+=('__bp_interactive_mode') + else + # shellcheck disable=SC2179 # PROMPT_COMMAND is not an array in bash <= 5.0 + PROMPT_COMMAND+=$'\n__bp_interactive_mode' + fi + + # Add two functions to our arrays for convenience + # of definition. + precmd_functions+=(precmd) + preexec_functions+=(preexec) + + # Invoke our two functions manually that were added to $PROMPT_COMMAND + __bp_precmd_invoke_cmd + __bp_interactive_mode +} + +# Sets an installation string as part of our PROMPT_COMMAND to install +# after our session has started. This allows bash-preexec to be included +# at any point in our bash profile. +__bp_install_after_session_init() { + # bash-preexec needs to modify these variables in order to work correctly + # if it can't, just stop the installation + __bp_require_not_readonly PROMPT_COMMAND HISTCONTROL HISTTIMEFORMAT || return + + local sanitized_prompt_command + __bp_sanitize_string sanitized_prompt_command "${PROMPT_COMMAND:-}" + if [[ -n "$sanitized_prompt_command" ]]; then + # shellcheck disable=SC2178 # PROMPT_COMMAND is not an array in bash <= 5.0 + PROMPT_COMMAND=${sanitized_prompt_command}$'\n' + fi + # shellcheck disable=SC2179 # PROMPT_COMMAND is not an array in bash <= 5.0 + PROMPT_COMMAND+=${__bp_install_string} +} + +# Run our install so long as we're not delaying it. +if [[ -z "${__bp_delay_install:-}" ]]; then + __bp_install_after_session_init +fi \ No newline at end of file From 4023833f66488cc7e46a1116a12e40a51f857e52 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 20 Oct 2025 12:28:41 -0700 Subject: [PATCH 06/16] add shell + version to terminal block desc --- pkg/aiusechat/tools.go | 50 ++++++++++++++++++++++++++---------------- 1 file changed, 31 insertions(+), 19 deletions(-) diff --git a/pkg/aiusechat/tools.go b/pkg/aiusechat/tools.go index 2a504caa2e..3ac5895d48 100644 --- a/pkg/aiusechat/tools.go +++ b/pkg/aiusechat/tools.go @@ -17,6 +17,36 @@ import ( "github.com/wavetermdev/waveterm/pkg/wstore" ) +func makeTerminalBlockDesc(block *waveobj.Block) string { + connection, hasConnection := block.Meta["connection"].(string) + cwd, hasCwd := block.Meta["cmd:cwd"].(string) + + blockORef := waveobj.MakeORef(waveobj.OType_Block, block.OID) + rtInfo := wstore.GetRTInfo(blockORef) + hasCurCwd := rtInfo != nil && rtInfo.CmdHasCurCwd + + var desc string + if hasConnection && connection != "" { + desc = fmt.Sprintf("CLI terminal on %q", connection) + } else { + desc = "local CLI terminal" + } + + if rtInfo != nil && rtInfo.ShellType != "" { + desc += fmt.Sprintf(" (%s", rtInfo.ShellType) + if rtInfo.ShellVersion != "" { + desc += fmt.Sprintf(" %s", rtInfo.ShellVersion) + } + desc += ")" + } + + if hasCurCwd && hasCwd && cwd != "" { + desc += fmt.Sprintf(" in directory %q", cwd) + } + + return desc +} + func MakeBlockShortDesc(block *waveobj.Block) string { if block.Meta == nil { return "" @@ -29,25 +59,7 @@ func MakeBlockShortDesc(block *waveobj.Block) string { switch viewType { case "term": - connection, hasConnection := block.Meta["connection"].(string) - cwd, hasCwd := block.Meta["cmd:cwd"].(string) - - blockORef := waveobj.MakeORef(waveobj.OType_Block, block.OID) - rtInfo := wstore.GetRTInfo(blockORef) - hasCurCwd := rtInfo != nil && rtInfo.CmdHasCurCwd - - var desc string - if hasConnection && connection != "" { - desc = fmt.Sprintf("CLI terminal on %q", connection) - } else { - desc = "local CLI terminal" - } - - if hasCurCwd && hasCwd && cwd != "" { - desc += fmt.Sprintf(" in directory %q", cwd) - } - - return desc + return makeTerminalBlockDesc(block) case "preview": file, hasFile := block.Meta["file"].(string) connection, hasConnection := block.Meta["connection"].(string) From e30633e93a211b402a11f4e883222a961b21bbe9 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 20 Oct 2025 12:35:30 -0700 Subject: [PATCH 07/16] write bash precmd to shell integration directory, load it in our bashrc and use it, fix cmd64 when base64 doesn't exist (rare) --- .../shellutil/shellintegration/bash_bashrc.sh | 92 +++++++++++-------- .../shellintegration/fish_wavefish.sh | 1 + .../shellutil/shellintegration/zsh_zshrc.sh | 21 ++--- pkg/util/shellutil/shellutil.go | 7 ++ 4 files changed, 74 insertions(+), 47 deletions(-) diff --git a/pkg/util/shellutil/shellintegration/bash_bashrc.sh b/pkg/util/shellutil/shellintegration/bash_bashrc.sh index 749b19d081..93a5e84813 100644 --- a/pkg/util/shellutil/shellintegration/bash_bashrc.sh +++ b/pkg/util/shellutil/shellintegration/bash_bashrc.sh @@ -30,54 +30,74 @@ if type _init_completion &>/dev/null; then source <(wsh completion bash) fi +# Source bash-preexec for proper preexec/precmd hook support +if [ -z "${bash_preexec_imported:-}" ]; then + _WAVETERM_SI_BASHRC_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + if [ -f "$_WAVETERM_SI_BASHRC_DIR/bash_preexec.sh" ]; then + source "$_WAVETERM_SI_BASHRC_DIR/bash_preexec.sh" + fi + unset _WAVETERM_SI_BASHRC_DIR +fi + _WAVETERM_SI_FIRSTPROMPT=1 -# shell integration +# Wave Terminal Shell Integration _waveterm_si_blocked() { - [[ -n "$TMUX" || -n "$STY" || "$TERM" == tmux* || "$TERM" == screen* ]] + [[ -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" + local s="$1" + s="${s//%/%25}" + 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" + _waveterm_si_blocked && return + local encoded_pwd=$(_waveterm_si_urlencode "$PWD") + printf '\033]7;file://%s%s\007' "$HOSTNAME" "$encoded_pwd" } -_waveterm_si_prompt_command() { - local _waveterm_si_status=$? - _waveterm_si_blocked && return - if [ "$_WAVETERM_SI_FIRSTPROMPT" -eq 1 ]; then - local uname_info - uname_info=$(uname -smr 2>/dev/null) - printf '\033]16162;M;{"shell":"bash","shellversion":"%s","uname":"%s"}\007' "$BASH_VERSION" "$uname_info" +_waveterm_si_precmd() { + local _waveterm_si_status=$? + _waveterm_si_blocked && return + + if [ "$_WAVETERM_SI_FIRSTPROMPT" -eq 1 ]; then + local uname_info + uname_info=$(uname -smr 2>/dev/null) + printf '\033]16162;M;{"shell":"bash","shellversion":"%s","uname":"%s"}\007' "$BASH_VERSION" "$uname_info" + else + printf '\033]16162;D;{"exitcode":%d}\007' "$_waveterm_si_status" + fi + # OSC 7 sent on every prompt - bash has no chpwd hook for directory changes _waveterm_si_osc7 - else - printf '\033]16162;D;{"exitcode":%d}\007' $_waveterm_si_status - fi - printf '\033]16162;A\007' - _WAVETERM_SI_FIRSTPROMPT=0 + printf '\033]16162;A\007' + _WAVETERM_SI_FIRSTPROMPT=0 } -# 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_preexec() { + _waveterm_si_blocked && return + + local cmd="$1" + local cmd_length=${#cmd} + if [ "$cmd_length" -gt 8192 ]; then + cmd=$(printf '# command too large (%d bytes)' "$cmd_length") + fi + local cmd64 + cmd64=$(printf '%s' "$cmd" | 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 } -_waveterm_si_append_pc \ No newline at end of file + +# Add our functions to the bash-preexec arrays +precmd_functions+=(_waveterm_si_precmd) +preexec_functions+=(_waveterm_si_preexec) \ No newline at end of file diff --git a/pkg/util/shellutil/shellintegration/fish_wavefish.sh b/pkg/util/shellutil/shellintegration/fish_wavefish.sh index 9a83fd9144..e76ba9cd1b 100644 --- a/pkg/util/shellutil/shellintegration/fish_wavefish.sh +++ b/pkg/util/shellutil/shellintegration/fish_wavefish.sh @@ -30,6 +30,7 @@ function _waveterm_si_prompt --on-event fish_prompt if test $_WAVETERM_SI_FIRSTPROMPT -eq 1 set -l uname_info (uname -smr 2>/dev/null) printf '\033]16162;M;{"shell":"fish","shellversion":"%s","uname":"%s"}\007' $FISH_VERSION "$uname_info" + # OSC 7 only sent on first prompt - chpwd hook handles directory changes _waveterm_si_osc7 else printf '\033]16162;D;{"exitcode":%d}\007' $_waveterm_si_status diff --git a/pkg/util/shellutil/shellintegration/zsh_zshrc.sh b/pkg/util/shellutil/shellintegration/zsh_zshrc.sh index 89054f39d2..449083c216 100644 --- a/pkg/util/shellutil/shellintegration/zsh_zshrc.sh +++ b/pkg/util/shellutil/shellintegration/zsh_zshrc.sh @@ -58,27 +58,26 @@ _waveterm_si_precmd() { else local uname_info=$(uname -smr 2>/dev/null) printf '\033]16162;M;{"shell":"zsh","shellversion":"%s","uname":"%s"}\007' "$ZSH_VERSION" "$uname_info" + # OSC 7 only sent on first prompt - chpwd hook handles directory changes _waveterm_si_osc7 fi - printf '\033]16162;A\007' # start of new prompt + printf '\033]16162;A\007' _WAVETERM_SI_FIRSTPRECMD=0 } _waveterm_si_preexec() { _waveterm_si_blocked && return - local cmd_length=${#1} + local cmd="$1" + local cmd_length=${#cmd} 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') + cmd=$(printf '# command too large (%d bytes)' "$cmd_length") + fi + local cmd64 + cmd64=$(printf '%s' "$cmd" | base64 2>/dev/null | tr -d '\n\r') + if [ -n "$cmd64" ]; then printf '\033]16162;C;{"cmd64":"%s"}\007' "$cmd64" else - 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 + printf '\033]16162;C\007' fi } diff --git a/pkg/util/shellutil/shellutil.go b/pkg/util/shellutil/shellutil.go index f6b60b4d81..dd9fe697a1 100644 --- a/pkg/util/shellutil/shellutil.go +++ b/pkg/util/shellutil/shellutil.go @@ -39,6 +39,9 @@ var ( //go:embed shellintegration/bash_bashrc.sh BashStartup_Bashrc string + //go:embed shellintegration/bash_preexec.sh + BashStartup_Preexec string + //go:embed shellintegration/fish_wavefish.sh FishStartup_Wavefish string @@ -281,6 +284,10 @@ func InitRcFiles(waveHome string, absWshBinDir string) error { if err != nil { return fmt.Errorf("error writing bash-integration .bashrc: %v", err) } + err = os.WriteFile(filepath.Join(bashDir, "bash_preexec.sh"), []byte(BashStartup_Preexec), 0644) + if err != nil { + return fmt.Errorf("error writing bash-integration bash_preexec.sh: %v", err) + } err = utilfn.WriteTemplateToFile(filepath.Join(fishDir, "wave.fish"), FishStartup_Wavefish, params) if err != nil { return fmt.Errorf("error writing fish-integration wave.fish: %v", err) From 449091abf0255682dd9f06bec4fb45a73102aa6d Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 20 Oct 2025 12:53:53 -0700 Subject: [PATCH 08/16] small change to guard against extdebug --- pkg/util/shellutil/shellintegration/bash_bashrc.sh | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pkg/util/shellutil/shellintegration/bash_bashrc.sh b/pkg/util/shellutil/shellintegration/bash_bashrc.sh index 93a5e84813..c7fdd7b719 100644 --- a/pkg/util/shellutil/shellintegration/bash_bashrc.sh +++ b/pkg/util/shellutil/shellintegration/bash_bashrc.sh @@ -30,6 +30,12 @@ if type _init_completion &>/dev/null; then source <(wsh completion bash) fi +# extdebug breaks bash-preexec semantics; bail out cleanly +if shopt -q extdebug; then + # printf 'wave si: disabled (bash extdebug enabled)\n' >&2 + return 0 +fi + # Source bash-preexec for proper preexec/precmd hook support if [ -z "${bash_preexec_imported:-}" ]; then _WAVETERM_SI_BASHRC_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" From c4b5b224d99943b4fa827d1309db4dc6c4186afe Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 20 Oct 2025 14:22:07 -0700 Subject: [PATCH 09/16] add ready state, change "on" to "connected to" for remote shells. --- pkg/aiusechat/tools.go | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/pkg/aiusechat/tools.go b/pkg/aiusechat/tools.go index 3ac5895d48..1445471b80 100644 --- a/pkg/aiusechat/tools.go +++ b/pkg/aiusechat/tools.go @@ -6,6 +6,7 @@ package aiusechat import ( "context" "fmt" + "log" "os/user" "strings" @@ -27,7 +28,7 @@ func makeTerminalBlockDesc(block *waveobj.Block) string { var desc string if hasConnection && connection != "" { - desc = fmt.Sprintf("CLI terminal on %q", connection) + desc = fmt.Sprintf("CLI terminal connected to %q", connection) } else { desc = "local CLI terminal" } @@ -40,8 +41,21 @@ func makeTerminalBlockDesc(block *waveobj.Block) string { desc += ")" } + if rtInfo != nil { + var stateStr string + switch rtInfo.ShellState { + case "ready": + stateStr = "waiting for input" + case "running-command": + stateStr = "running command" + default: + stateStr = "state unknown" + } + desc += fmt.Sprintf(", %s", stateStr) + } + if hasCurCwd && hasCwd && cwd != "" { - desc += fmt.Sprintf(" in directory %q", cwd) + desc += fmt.Sprintf(", in directory %q", cwd) } return desc @@ -123,6 +137,7 @@ func GenerateTabStateAndTools(ctx context.Context, tabid string, widgetAccess bo } } tabState := GenerateCurrentTabStatePrompt(blocks, widgetAccess) + log.Printf("TABPROMPT %s\n", tabState) var tools []uctypes.ToolDefinition if widgetAccess { tools = append(tools, GetCaptureScreenshotToolDefinition(tabid)) From 761c8159a1326296d2fa01650453fb6df57e9d4d Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 20 Oct 2025 14:38:57 -0700 Subject: [PATCH 10/16] clear rtinfo if we stop a block controller --- pkg/blockcontroller/blockcontroller.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/blockcontroller/blockcontroller.go b/pkg/blockcontroller/blockcontroller.go index 777798cc40..77b7178a94 100644 --- a/pkg/blockcontroller/blockcontroller.go +++ b/pkg/blockcontroller/blockcontroller.go @@ -94,6 +94,7 @@ func registerController(blockId string, controller Controller) { if existingController != nil { existingController.Stop(false, Status_Done) + wstore.DeleteRTInfo(waveobj.MakeORef(waveobj.OType_Block, blockId)) } } @@ -243,6 +244,7 @@ func StopBlockController(blockId string) { return } controller.Stop(true, Status_Done) + wstore.DeleteRTInfo(waveobj.MakeORef(waveobj.OType_Block, blockId)) } func StopBlockControllerAndSetStatus(blockId string, newStatus string) { @@ -251,6 +253,7 @@ func StopBlockControllerAndSetStatus(blockId string, newStatus string) { return } controller.Stop(true, newStatus) + wstore.DeleteRTInfo(waveobj.MakeORef(waveobj.OType_Block, blockId)) } func SendInput(blockId string, inputUnion *BlockInputUnion) error { @@ -268,6 +271,7 @@ func StopAllBlockControllers() { if status != nil && status.ShellProcStatus == Status_Running { go func(id string, c Controller) { c.Stop(true, Status_Done) + wstore.DeleteRTInfo(waveobj.MakeORef(waveobj.OType_Block, id)) }(blockId, controller) } } From 65d0dd2e4559c58739a3b57b0839820da6910029 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 20 Oct 2025 14:40:11 -0700 Subject: [PATCH 11/16] add new shell:integration flag... set to true for bash, zsh, and fish, false for pwsh (no A, C, D tracking yet) --- aiprompts/wave-osc-16162.md | 1 + frontend/app/view/term/termwrap.ts | 5 ++++- frontend/types/gotypes.d.ts | 1 + pkg/aiusechat/tools.go | 22 +++++++++++-------- .../shellutil/shellintegration/bash_bashrc.sh | 10 ++++++++- .../shellintegration/fish_wavefish.sh | 2 +- .../shellintegration/pwsh_wavepwsh.sh | 4 ++-- .../shellutil/shellintegration/zsh_zshrc.sh | 2 +- pkg/waveobj/blockrtinfo.go | 1 + 9 files changed, 33 insertions(+), 15 deletions(-) diff --git a/aiprompts/wave-osc-16162.md b/aiprompts/wave-osc-16162.md index 7d403606d8..fe9c8c8352 100644 --- a/aiprompts/wave-osc-16162.md +++ b/aiprompts/wave-osc-16162.md @@ -72,6 +72,7 @@ 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") + integration?: boolean; // Whether shell integration is active (true) or disabled (false) } ``` diff --git a/frontend/app/view/term/termwrap.ts b/frontend/app/view/term/termwrap.ts index a2bba1112a..bf0e99116d 100644 --- a/frontend/app/view/term/termwrap.ts +++ b/frontend/app/view/term/termwrap.ts @@ -176,7 +176,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; uname?: string } } + | { command: "M"; data: { shell?: string; shellversion?: string; uname?: string; integration?: boolean } } | { command: "D"; data: { exitcode?: number } } | { command: "I"; data: { inputempty?: boolean } } | { command: "R"; data: {} }; @@ -236,6 +236,9 @@ function handleOsc16162Command(data: string, blockId: string, loaded: boolean, t if (cmd.data.uname) { rtInfo["shell:uname"] = cmd.data.uname; } + if (cmd.data.integration != null) { + rtInfo["shell:integration"] = cmd.data.integration; + } break; case "D": if (cmd.data.exitcode != null) { diff --git a/frontend/types/gotypes.d.ts b/frontend/types/gotypes.d.ts index 722f54c55f..eeaf24f970 100644 --- a/frontend/types/gotypes.d.ts +++ b/frontend/types/gotypes.d.ts @@ -715,6 +715,7 @@ declare global { "shell:type"?: string; "shell:version"?: string; "shell:uname"?: string; + "shell:integration"?: boolean; "shell:inputempty"?: boolean; "shell:lastcmd"?: string; "shell:lastcmdexitcode"?: number; diff --git a/pkg/aiusechat/tools.go b/pkg/aiusechat/tools.go index 1445471b80..a092318b5a 100644 --- a/pkg/aiusechat/tools.go +++ b/pkg/aiusechat/tools.go @@ -42,16 +42,20 @@ func makeTerminalBlockDesc(block *waveobj.Block) string { } if rtInfo != nil { - var stateStr string - switch rtInfo.ShellState { - case "ready": - stateStr = "waiting for input" - case "running-command": - stateStr = "running command" - default: - stateStr = "state unknown" + if rtInfo.ShellIntegration { + var stateStr string + switch rtInfo.ShellState { + case "ready": + stateStr = "waiting for input" + case "running-command": + stateStr = "running command" + default: + stateStr = "state unknown" + } + desc += fmt.Sprintf(", %s", stateStr) + } else { + desc += ", no shell integration" } - desc += fmt.Sprintf(", %s", stateStr) } if hasCurCwd && hasCwd && cwd != "" { diff --git a/pkg/util/shellutil/shellintegration/bash_bashrc.sh b/pkg/util/shellutil/shellintegration/bash_bashrc.sh index c7fdd7b719..b9c68e6e62 100644 --- a/pkg/util/shellutil/shellintegration/bash_bashrc.sh +++ b/pkg/util/shellutil/shellintegration/bash_bashrc.sh @@ -33,6 +33,7 @@ fi # extdebug breaks bash-preexec semantics; bail out cleanly if shopt -q extdebug; then # printf 'wave si: disabled (bash extdebug enabled)\n' >&2 + printf '\033]16162;M;{"integration":false}\007' return 0 fi @@ -45,6 +46,13 @@ if [ -z "${bash_preexec_imported:-}" ]; then unset _WAVETERM_SI_BASHRC_DIR fi +# Check if bash-preexec was successfully imported +if [ -z "${bash_preexec_imported:-}" ]; then + # bash-preexec failed to import, disable shell integration + printf '\033]16162;M;{"integration":false}\007' + return 0 +fi + _WAVETERM_SI_FIRSTPROMPT=1 # Wave Terminal Shell Integration @@ -77,7 +85,7 @@ _waveterm_si_precmd() { if [ "$_WAVETERM_SI_FIRSTPROMPT" -eq 1 ]; then local uname_info uname_info=$(uname -smr 2>/dev/null) - printf '\033]16162;M;{"shell":"bash","shellversion":"%s","uname":"%s"}\007' "$BASH_VERSION" "$uname_info" + printf '\033]16162;M;{"shell":"bash","shellversion":"%s","uname":"%s","integration":true}\007' "$BASH_VERSION" "$uname_info" else printf '\033]16162;D;{"exitcode":%d}\007' "$_waveterm_si_status" fi diff --git a/pkg/util/shellutil/shellintegration/fish_wavefish.sh b/pkg/util/shellutil/shellintegration/fish_wavefish.sh index e76ba9cd1b..d6aa9d5000 100644 --- a/pkg/util/shellutil/shellintegration/fish_wavefish.sh +++ b/pkg/util/shellutil/shellintegration/fish_wavefish.sh @@ -29,7 +29,7 @@ function _waveterm_si_prompt --on-event fish_prompt _waveterm_si_blocked; and return if test $_WAVETERM_SI_FIRSTPROMPT -eq 1 set -l uname_info (uname -smr 2>/dev/null) - printf '\033]16162;M;{"shell":"fish","shellversion":"%s","uname":"%s"}\007' $FISH_VERSION "$uname_info" + printf '\033]16162;M;{"shell":"fish","shellversion":"%s","uname":"%s","integration":true}\007' $FISH_VERSION "$uname_info" # OSC 7 only sent on first prompt - chpwd hook handles directory changes _waveterm_si_osc7 else diff --git a/pkg/util/shellutil/shellintegration/pwsh_wavepwsh.sh b/pkg/util/shellutil/shellintegration/pwsh_wavepwsh.sh index 3cf9e06262..32f54c9b67 100644 --- a/pkg/util/shellutil/shellintegration/pwsh_wavepwsh.sh +++ b/pkg/util/shellutil/shellintegration/pwsh_wavepwsh.sh @@ -41,8 +41,8 @@ function Global:_waveterm_si_prompt { if ($Global:_WAVETERM_SI_FIRSTPROMPT) { # not sending uname - $shellversion = $PSVersionTable.PSVersion.ToString() - Write-Host -NoNewline "`e]16162;M;{`"shell`":`"pwsh`",`"shellversion`":`"$shellversion`"}`a" + $shellversion = $PSVersionTable.PSVersion.ToString() + Write-Host -NoNewline "`e]16162;M;{`"shell`":`"pwsh`",`"shellversion`":`"$shellversion`",`"integration`":false}`a" $Global:_WAVETERM_SI_FIRSTPROMPT = $false } diff --git a/pkg/util/shellutil/shellintegration/zsh_zshrc.sh b/pkg/util/shellutil/shellintegration/zsh_zshrc.sh index 449083c216..5df358bb1c 100644 --- a/pkg/util/shellutil/shellintegration/zsh_zshrc.sh +++ b/pkg/util/shellutil/shellintegration/zsh_zshrc.sh @@ -57,7 +57,7 @@ _waveterm_si_precmd() { printf '\033]16162;D;{"exitcode":%d}\007' $_waveterm_si_status else local uname_info=$(uname -smr 2>/dev/null) - printf '\033]16162;M;{"shell":"zsh","shellversion":"%s","uname":"%s"}\007' "$ZSH_VERSION" "$uname_info" + printf '\033]16162;M;{"shell":"zsh","shellversion":"%s","uname":"%s","integration":true}\007' "$ZSH_VERSION" "$uname_info" # OSC 7 only sent on first prompt - chpwd hook handles directory changes _waveterm_si_osc7 fi diff --git a/pkg/waveobj/blockrtinfo.go b/pkg/waveobj/blockrtinfo.go index dc72762d36..855fc55e9e 100644 --- a/pkg/waveobj/blockrtinfo.go +++ b/pkg/waveobj/blockrtinfo.go @@ -14,6 +14,7 @@ type ObjRTInfo struct { ShellType string `json:"shell:type,omitempty"` ShellVersion string `json:"shell:version,omitempty"` ShellUname string `json:"shell:uname,omitempty"` + ShellIntegration bool `json:"shell:integration,omitempty"` ShellInputEmpty bool `json:"shell:inputempty,omitempty"` ShellLastCmd string `json:"shell:lastcmd,omitempty"` ShellLastCmdExitCode int `json:"shell:lastcmdexitcode,omitempty"` From d76270a833ac3b9e2768d2ab605a7479b04428ce Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 20 Oct 2025 14:58:58 -0700 Subject: [PATCH 12/16] update name to shell:hascurcwd instead of cmd to be more consistent. also drop two set to null paths since we clear on connection change in blockcontroller.go now. --- cmd/wsh/cmd/wshcmd-ssh.go | 12 ------------ frontend/app/modals/conntypeahead.tsx | 8 -------- frontend/app/view/term/termwrap.ts | 2 +- frontend/types/gotypes.d.ts | 2 +- pkg/aiusechat/tools.go | 2 +- pkg/waveobj/blockrtinfo.go | 3 +-- 6 files changed, 4 insertions(+), 25 deletions(-) diff --git a/cmd/wsh/cmd/wshcmd-ssh.go b/cmd/wsh/cmd/wshcmd-ssh.go index ec011ddd2c..e8859cf771 100644 --- a/cmd/wsh/cmd/wshcmd-ssh.go +++ b/cmd/wsh/cmd/wshcmd-ssh.go @@ -89,18 +89,6 @@ func sshRun(cmd *cobra.Command, args []string) (rtnErr error) { if err != nil { return fmt.Errorf("setting connection in block: %w", err) } - - // Clear the cmd:hascurcwd rtinfo field - rtInfoData := wshrpc.CommandSetRTInfoData{ - ORef: waveobj.MakeORef(waveobj.OType_Block, blockId), - Data: map[string]any{ - "cmd:hascurcwd": nil, - }, - } - err = wshclient.SetRTInfoCommand(RpcClient, rtInfoData, nil) - if err != nil { - return fmt.Errorf("setting RTInfo in block: %w", err) - } WriteStderr("switched connection to %q\n", sshArg) return nil } diff --git a/frontend/app/modals/conntypeahead.tsx b/frontend/app/modals/conntypeahead.tsx index 9605478942..bee43cb03d 100644 --- a/frontend/app/modals/conntypeahead.tsx +++ b/frontend/app/modals/conntypeahead.tsx @@ -404,14 +404,6 @@ const ChangeConnectionBlockModal = React.memo( meta: { connection: connName, file: newFile, "cmd:cwd": null }, }); - const rtInfo = { "cmd:hascurcwd": null }; - const rtInfoData: CommandSetRTInfoData = { - oref: WOS.makeORef("block", blockId), - data: rtInfo - }; - RpcApi.SetRTInfoCommand(TabRpcClient, rtInfoData).catch((e) => - console.log("error setting RT info", e) - ); try { await RpcApi.ConnEnsureCommand( TabRpcClient, diff --git a/frontend/app/view/term/termwrap.ts b/frontend/app/view/term/termwrap.ts index bf0e99116d..20088ae23a 100644 --- a/frontend/app/view/term/termwrap.ts +++ b/frontend/app/view/term/termwrap.ts @@ -158,7 +158,7 @@ function handleOsc7Command(data: string, blockId: string, loaded: boolean): bool "cmd:cwd": pathPart, }); - const rtInfo = { "cmd:hascurcwd": true }; + const rtInfo = { "shell:hascurcwd": true }; const rtInfoData: CommandSetRTInfoData = { oref: WOS.makeORef("block", blockId), data: rtInfo, diff --git a/frontend/types/gotypes.d.ts b/frontend/types/gotypes.d.ts index eeaf24f970..5a44c802a2 100644 --- a/frontend/types/gotypes.d.ts +++ b/frontend/types/gotypes.d.ts @@ -710,7 +710,7 @@ declare global { "tsunami:title"?: string; "tsunami:shortdesc"?: string; "tsunami:schemas"?: any; - "cmd:hascurcwd"?: boolean; + "shell:hascurcwd"?: boolean; "shell:state"?: string; "shell:type"?: string; "shell:version"?: string; diff --git a/pkg/aiusechat/tools.go b/pkg/aiusechat/tools.go index a092318b5a..3fdcefbb1a 100644 --- a/pkg/aiusechat/tools.go +++ b/pkg/aiusechat/tools.go @@ -24,7 +24,7 @@ func makeTerminalBlockDesc(block *waveobj.Block) string { blockORef := waveobj.MakeORef(waveobj.OType_Block, block.OID) rtInfo := wstore.GetRTInfo(blockORef) - hasCurCwd := rtInfo != nil && rtInfo.CmdHasCurCwd + hasCurCwd := rtInfo != nil && rtInfo.ShellHasCurCwd var desc string if hasConnection && connection != "" { diff --git a/pkg/waveobj/blockrtinfo.go b/pkg/waveobj/blockrtinfo.go index 855fc55e9e..1a5a1f7b5f 100644 --- a/pkg/waveobj/blockrtinfo.go +++ b/pkg/waveobj/blockrtinfo.go @@ -8,8 +8,7 @@ type ObjRTInfo struct { TsunamiShortDesc string `json:"tsunami:shortdesc,omitempty"` TsunamiSchemas any `json:"tsunami:schemas,omitempty"` - CmdHasCurCwd bool `json:"cmd:hascurcwd,omitempty"` - + ShellHasCurCwd bool `json:"shell:hascurcwd,omitempty"` ShellState string `json:"shell:state,omitempty"` ShellType string `json:"shell:type,omitempty"` ShellVersion string `json:"shell:version,omitempty"` From 4f8887989f3958bd9e2de0a5c1b4ce9827556807 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 20 Oct 2025 15:30:24 -0700 Subject: [PATCH 13/16] fix weird stdout/stderr race condition detectting the connserver version. --- pkg/aiusechat/tools.go | 4 ++-- pkg/remote/conncontroller/conncontroller.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/aiusechat/tools.go b/pkg/aiusechat/tools.go index 3fdcefbb1a..1497a0b80c 100644 --- a/pkg/aiusechat/tools.go +++ b/pkg/aiusechat/tools.go @@ -6,7 +6,6 @@ package aiusechat import ( "context" "fmt" - "log" "os/user" "strings" @@ -141,7 +140,8 @@ func GenerateTabStateAndTools(ctx context.Context, tabid string, widgetAccess bo } } tabState := GenerateCurrentTabStatePrompt(blocks, widgetAccess) - log.Printf("TABPROMPT %s\n", tabState) + // for debugging + // log.Printf("TABPROMPT %s\n", tabState) var tools []uctypes.ToolDefinition if widgetAccess { tools = append(tools, GetCaptureScreenshotToolDefinition(tabid)) diff --git a/pkg/remote/conncontroller/conncontroller.go b/pkg/remote/conncontroller/conncontroller.go index 9738048450..8d2d400f44 100644 --- a/pkg/remote/conncontroller/conncontroller.go +++ b/pkg/remote/conncontroller/conncontroller.go @@ -73,7 +73,7 @@ type SSHConn struct { var ConnServerCmdTemplate = strings.TrimSpace( strings.Join([]string{ - "%s version 2> /dev/null || (echo -n \"not-installed \"; uname -sm);", + "%s version 2> /dev/null || (echo -n \"not-installed \"; uname -sm; exit 0);", "exec %s connserver", }, "\n")) From f2d3a4a12a4ae1cd3caf72a93b18c1f73969127b Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 20 Oct 2025 15:47:41 -0700 Subject: [PATCH 14/16] surface currently running command if we know it. --- pkg/aiusechat/tools.go | 9 +++++++++ pkg/util/utilfn/marshal.go | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/pkg/aiusechat/tools.go b/pkg/aiusechat/tools.go index 1497a0b80c..32e0f4ddb3 100644 --- a/pkg/aiusechat/tools.go +++ b/pkg/aiusechat/tools.go @@ -12,6 +12,7 @@ import ( "github.com/google/uuid" "github.com/wavetermdev/waveterm/pkg/aiusechat/uctypes" "github.com/wavetermdev/waveterm/pkg/blockcontroller" + "github.com/wavetermdev/waveterm/pkg/util/utilfn" "github.com/wavetermdev/waveterm/pkg/wavebase" "github.com/wavetermdev/waveterm/pkg/waveobj" "github.com/wavetermdev/waveterm/pkg/wstore" @@ -48,6 +49,14 @@ func makeTerminalBlockDesc(block *waveobj.Block) string { stateStr = "waiting for input" case "running-command": stateStr = "running command" + if rtInfo.ShellLastCmd != "" { + cmdStr := rtInfo.ShellLastCmd + if len(cmdStr) > 30 { + cmdStr = cmdStr[:27] + "..." + } + cmdJSON := utilfn.MarshalJsonString(cmdStr) + stateStr = fmt.Sprintf("running command %s", cmdJSON) + } default: stateStr = "state unknown" } diff --git a/pkg/util/utilfn/marshal.go b/pkg/util/utilfn/marshal.go index c284260ae9..c7cf7e62a2 100644 --- a/pkg/util/utilfn/marshal.go +++ b/pkg/util/utilfn/marshal.go @@ -202,6 +202,15 @@ func DecodeDataURL(dataURL string) (mimeType string, data []byte, err error) { return mimeType, []byte(decoded), nil } +// MarshalJsonString marshals a string to JSON format, returning the properly escaped JSON string. +// Returns empty string if there's an error (rare). +func MarshalJsonString(s string) string { + jsonBytes, err := json.Marshal(s) + if err != nil { + return "" + } + return string(jsonBytes) +} // ContainsBinaryData checks if the provided data contains binary (non-text) content func ContainsBinaryData(data []byte) bool { From db81907d80bb2670b14c2db44acc469fe79c0988 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 20 Oct 2025 16:16:09 -0700 Subject: [PATCH 15/16] add last_command to output when the AI requests scrollback from terminal --- frontend/app/view/term/termwrap.ts | 2 ++ pkg/aiusechat/tools.go | 1 - pkg/aiusechat/tools_term.go | 46 ++++++++++++++++++++++++------ 3 files changed, 39 insertions(+), 10 deletions(-) diff --git a/frontend/app/view/term/termwrap.ts b/frontend/app/view/term/termwrap.ts index 20088ae23a..5d9e64b564 100644 --- a/frontend/app/view/term/termwrap.ts +++ b/frontend/app/view/term/termwrap.ts @@ -225,6 +225,8 @@ function handleOsc16162Command(data: string, blockId: string, loaded: boolean, t } else { rtInfo["shell:lastcmd"] = null; } + // also clear lastcmdexitcode (since we've now started a new command) + rtInfo["shell:lastcmdexitcode"] = null; break; case "M": if (cmd.data.shell) { diff --git a/pkg/aiusechat/tools.go b/pkg/aiusechat/tools.go index 32e0f4ddb3..48be6e02ef 100644 --- a/pkg/aiusechat/tools.go +++ b/pkg/aiusechat/tools.go @@ -216,7 +216,6 @@ func GenerateCurrentTabStatePrompt(blocks []*waveobj.Block, widgetAccess bool) s } prompt.WriteString("") rtn := prompt.String() - // log.Printf("%s\n", rtn) return rtn } diff --git a/pkg/aiusechat/tools_term.go b/pkg/aiusechat/tools_term.go index ad28bcc895..dfbc764855 100644 --- a/pkg/aiusechat/tools_term.go +++ b/pkg/aiusechat/tools_term.go @@ -11,10 +11,12 @@ import ( "time" "github.com/wavetermdev/waveterm/pkg/aiusechat/uctypes" + "github.com/wavetermdev/waveterm/pkg/waveobj" "github.com/wavetermdev/waveterm/pkg/wcore" "github.com/wavetermdev/waveterm/pkg/wshrpc" "github.com/wavetermdev/waveterm/pkg/wshrpc/wshclient" "github.com/wavetermdev/waveterm/pkg/wshutil" + "github.com/wavetermdev/waveterm/pkg/wstore" ) type TermGetScrollbackToolInput struct { @@ -23,15 +25,22 @@ type TermGetScrollbackToolInput struct { Count int `json:"count,omitempty"` } +type CommandInfo struct { + Command string `json:"command"` + Status string `json:"status"` + ExitCode *int `json:"exitcode,omitempty"` +} + type TermGetScrollbackToolOutput struct { - TotalLines int `json:"total_lines"` - LineStart int `json:"line_start"` - LineEnd int `json:"line_end"` - ReturnedLines int `json:"returned_lines"` - Content string `json:"content"` - SinceLastOutputSec *int `json:"since_last_output_sec,omitempty"` - HasMore bool `json:"has_more"` - NextStart *int `json:"next_start"` + TotalLines int `json:"totallines"` + LineStart int `json:"linestart"` + LineEnd int `json:"lineend"` + ReturnedLines int `json:"returnedlines"` + Content string `json:"content"` + SinceLastOutputSec *int `json:"sincelastoutputsec,omitempty"` + HasMore bool `json:"hasmore"` + NextStart *int `json:"nextstart"` + LastCommand *CommandInfo `json:"lastcommand,omitempty"` } func parseTermGetScrollbackInput(input any) (*TermGetScrollbackToolInput, error) { @@ -76,7 +85,7 @@ func GetTermGetScrollbackToolDefinition(tabId string) uctypes.ToolDefinition { return uctypes.ToolDefinition{ Name: "term_get_scrollback", DisplayName: "Get Terminal Scrollback", - Description: "Fetch terminal scrollback from a widget as plain text. Index 0 is the most recent line; indices increase going upward (older lines).", + Description: "Fetch terminal scrollback from a widget as plain text. Index 0 is the most recent line; indices increase going upward (older lines). Also returns last command and exit code if shell integration is enabled.", ToolLogName: "term:getscrollback", InputSchema: map[string]any{ "type": "object", @@ -155,6 +164,24 @@ func GetTermGetScrollbackToolDefinition(tabId string) uctypes.ToolDefinition { nextStart = &effectiveLineEnd } + blockORef := waveobj.MakeORef(waveobj.OType_Block, fullBlockId) + rtInfo := wstore.GetRTInfo(blockORef) + + var lastCommand *CommandInfo + if rtInfo != nil && rtInfo.ShellIntegration && rtInfo.ShellLastCmd != "" { + cmdInfo := &CommandInfo{ + Command: rtInfo.ShellLastCmd, + } + if rtInfo.ShellState == "running-command" { + cmdInfo.Status = "running" + } else if rtInfo.ShellState == "ready" { + cmdInfo.Status = "completed" + exitCode := rtInfo.ShellLastCmdExitCode + cmdInfo.ExitCode = &exitCode + } + lastCommand = cmdInfo + } + return &TermGetScrollbackToolOutput{ TotalLines: result.TotalLines, LineStart: result.LineStart, @@ -164,6 +191,7 @@ func GetTermGetScrollbackToolDefinition(tabId string) uctypes.ToolDefinition { SinceLastOutputSec: sinceLastOutputSec, HasMore: hasMore, NextStart: nextStart, + LastCommand: lastCommand, }, nil }, } From 87f0db6d6ef2b1c178e91f10fe3a19b6ecb09587 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 20 Oct 2025 17:27:59 -0700 Subject: [PATCH 16/16] fix name case --- pkg/aiusechat/tools.go | 2 +- pkg/util/utilfn/marshal.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/aiusechat/tools.go b/pkg/aiusechat/tools.go index 48be6e02ef..b038848ce9 100644 --- a/pkg/aiusechat/tools.go +++ b/pkg/aiusechat/tools.go @@ -54,7 +54,7 @@ func makeTerminalBlockDesc(block *waveobj.Block) string { if len(cmdStr) > 30 { cmdStr = cmdStr[:27] + "..." } - cmdJSON := utilfn.MarshalJsonString(cmdStr) + cmdJSON := utilfn.MarshalJSONString(cmdStr) stateStr = fmt.Sprintf("running command %s", cmdJSON) } default: diff --git a/pkg/util/utilfn/marshal.go b/pkg/util/utilfn/marshal.go index c7cf7e62a2..6106e70604 100644 --- a/pkg/util/utilfn/marshal.go +++ b/pkg/util/utilfn/marshal.go @@ -202,9 +202,9 @@ func DecodeDataURL(dataURL string) (mimeType string, data []byte, err error) { return mimeType, []byte(decoded), nil } -// MarshalJsonString marshals a string to JSON format, returning the properly escaped JSON string. +// MarshalJSONString marshals a string to JSON format, returning the properly escaped JSON string. // Returns empty string if there's an error (rare). -func MarshalJsonString(s string) string { +func MarshalJSONString(s string) string { jsonBytes, err := json.Marshal(s) if err != nil { return ""