Class: Kward::PersistentShellSession
- Inherits:
-
Object
- Object
- Kward::PersistentShellSession
- Defined in:
- lib/kward/shell/persistent_session.rb
Overview
Owns one interactive shell process and serializes user and agent commands.
The process is kept alive between commands so shell variables, functions, aliases, directory changes, and child-shell state belong to one session.
Defined Under Namespace
Classes: CommandInterrupted, InteractiveResult
Constant Summary collapse
- Result =
Kwsh::Result
- READ_SIZE =
4_096- STARTUP_TIMEOUT_SECONDS =
5- STTY_COMMAND =
if File.executable?("/bin/stty") "/bin/stty" elsif File.executable?("/usr/bin/stty") "/usr/bin/stty" else "stty" end.freeze
- STATEFUL_COMMANDS =
%w[alias cd export pwd unset unalias].freeze
Instance Attribute Summary collapse
-
#cwd ⇒ Object
readonly
Returns the value of attribute cwd.
-
#last_command ⇒ Object
readonly
Returns the value of attribute last_command.
-
#timeout_seconds ⇒ Object
readonly
Returns the value of attribute timeout_seconds.
Instance Method Summary collapse
- #child_env(interactive: false) ⇒ Object
- #close ⇒ Object
- #command_shell ⇒ Object
- #complete(input, cursor) ⇒ Object
-
#context_snapshot(max_output_bytes: Kwsh::CONTEXT_OUTPUT_BYTES) ⇒ Object
Returns bounded shell state for an explicit shell-agent prompt.
- #editor_command_result(command, display_command: command) ⇒ Object
- #expand_alias(command, interactive: false) ⇒ Object
-
#initialize(cwd: Dir.pwd, env: ENV.to_h, shell: Kwsh::DEFAULT_SHELL, configured_env: {}, aliases: {}, timeout_seconds: Kwsh::DEFAULT_TIMEOUT_SECONDS, max_output_bytes: Kwsh::DEFAULT_MAX_OUTPUT_BYTES, window_size_provider: nil) ⇒ PersistentShellSession
constructor
A new instance of PersistentShellSession.
- #prompt_label ⇒ Object
-
#record_interactive_result(output:, exit_status:) ⇒ Object
Records the safe output captured after an interactive terminal handoff.
- #run(input, cancellation: nil, &block) ⇒ Object
-
#run_for_agent(input, timeout_seconds: nil, cancellation: nil) ⇒ Object
Runs a command for the shell assistant without handing it the terminal.
-
#run_interactive(command, input:, sink:, tab_action_handler: nil, on_detach: nil) ⇒ Object
Runs an external command through the same persistent shell process while the caller owns the terminal.
Constructor Details
#initialize(cwd: Dir.pwd, env: ENV.to_h, shell: Kwsh::DEFAULT_SHELL, configured_env: {}, aliases: {}, timeout_seconds: Kwsh::DEFAULT_TIMEOUT_SECONDS, max_output_bytes: Kwsh::DEFAULT_MAX_OUTPUT_BYTES, window_size_provider: nil) ⇒ PersistentShellSession
Returns a new instance of PersistentShellSession.
43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
# File 'lib/kward/shell/persistent_session.rb', line 43 def initialize(cwd: Dir.pwd, env: ENV.to_h, shell: Kwsh::DEFAULT_SHELL, configured_env: {}, aliases: {}, timeout_seconds: Kwsh::DEFAULT_TIMEOUT_SECONDS, max_output_bytes: Kwsh::DEFAULT_MAX_OUTPUT_BYTES, window_size_provider: nil) @cwd = File.(cwd.to_s.empty? ? Dir.pwd : cwd.to_s) @base_env = env.to_h.transform_keys(&:to_s).transform_values(&:to_s) @configured_env = configured_env.to_h.transform_keys(&:to_s).transform_values(&:to_s) @aliases = aliases.to_h.transform_keys(&:to_s).transform_values(&:to_s) @shell = shell.to_s.empty? ? Kwsh::DEFAULT_SHELL : shell.to_s @timeout_seconds = timeout_seconds.to_i.positive? ? timeout_seconds.to_i : Kwsh::DEFAULT_TIMEOUT_SECONDS @max_output_bytes = max_output_bytes.to_i.positive? ? max_output_bytes.to_i : Kwsh::DEFAULT_MAX_OUTPUT_BYTES @window_size_provider = window_size_provider @last_command = nil @last_result = nil @command_mutex = Mutex.new @write_mutex = Mutex.new @marker_counter = 0 @marker_prefix = "__KWARD_SHELL_#{SecureRandom.hex(16)}_" @closed = false @routing_shell = build_routing_shell @env = @routing_shell.child_env start_process initialize_protocol rescue StandardError close raise end |
Instance Attribute Details
#cwd ⇒ Object (readonly)
Returns the value of attribute cwd.
41 42 43 |
# File 'lib/kward/shell/persistent_session.rb', line 41 def cwd @cwd end |
#last_command ⇒ Object (readonly)
Returns the value of attribute last_command.
41 42 43 |
# File 'lib/kward/shell/persistent_session.rb', line 41 def last_command @last_command end |
#timeout_seconds ⇒ Object (readonly)
Returns the value of attribute timeout_seconds.
41 42 43 |
# File 'lib/kward/shell/persistent_session.rb', line 41 def timeout_seconds @timeout_seconds end |
Instance Method Details
#child_env(interactive: false) ⇒ Object
73 74 75 |
# File 'lib/kward/shell/persistent_session.rb', line 73 def child_env(interactive: false) @env.dup end |
#close ⇒ Object
180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 |
# File 'lib/kward/shell/persistent_session.rb', line 180 def close return if @closed @closed = true begin terminate_process_group(@pid) if @pid ensure @write_mutex.synchronize do @reader&.close unless @reader&.closed? @writer&.close unless @writer&.closed? end @process_thread&.join(0.5) @process_thread&.kill if @process_thread&.alive? @pid = nil end rescue StandardError nil end |
#command_shell ⇒ Object
69 70 71 |
# File 'lib/kward/shell/persistent_session.rb', line 69 def command_shell @shell end |
#complete(input, cursor) ⇒ Object
168 169 170 |
# File 'lib/kward/shell/persistent_session.rb', line 168 def complete(input, cursor) completion_shell.complete(input, cursor) end |
#context_snapshot(max_output_bytes: Kwsh::CONTEXT_OUTPUT_BYTES) ⇒ Object
Returns bounded shell state for an explicit shell-agent prompt.
159 160 161 162 163 164 165 166 |
# File 'lib/kward/shell/persistent_session.rb', line 159 def context_snapshot(max_output_bytes: Kwsh::CONTEXT_OUTPUT_BYTES) { cwd: @cwd, last_command: @last_command, exit_status: @last_result&.exit_status, last_output: context_output(@last_result&.output, max_output_bytes) } end |
#editor_command_result(command, display_command: command) ⇒ Object
176 177 178 |
# File 'lib/kward/shell/persistent_session.rb', line 176 def editor_command_result(command, display_command: command) @routing_shell.editor_command_result(command, display_command: display_command) end |
#expand_alias(command, interactive: false) ⇒ Object
172 173 174 |
# File 'lib/kward/shell/persistent_session.rb', line 172 def (command, interactive: false) @routing_shell.(command, interactive: interactive) end |
#prompt_label ⇒ Object
77 78 79 |
# File 'lib/kward/shell/persistent_session.rb', line 77 def prompt_label "Shell #{display_cwd} $" end |
#record_interactive_result(output:, exit_status:) ⇒ Object
Records the safe output captured after an interactive terminal handoff.
152 153 154 155 156 |
# File 'lib/kward/shell/persistent_session.rb', line 152 def record_interactive_result(output:, exit_status:) return unless @last_command @last_result = Result.new(output: ANSI.sanitize_transcript(output.to_s), exit_status: exit_status) end |
#run(input, cancellation: nil, &block) ⇒ Object
81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 |
# File 'lib/kward/shell/persistent_session.rb', line 81 def run(input, cancellation: nil, &block) command = input.to_s.strip return Result.new(output: "", exit_status: 0) if command.empty? = (command) words = shell_words() sync_runtime_state(words) result = if words.empty? Result.new(output: "", exit_status: 0) elsif words.first == "clear" Result.new(output: "", exit_status: 0, clear: true) elsif %w[exit logout].include?(words.first) exit_result(command, words) elsif words.first == "capture" capture_result(command, , cancellation: cancellation, &block) elsif words.first == "pty" interactive_result(command, ) elsif %w[source .].include?(words.first) source_result(command, words) elsif (editor_result = editor_command_result(command)) editor_result elsif stateful_command?(words) execute_command(, display_command: command, cancellation: cancellation, &block) else Result.new(output: command_echo(command), exit_status: 0, interactive_command: ) end remember_result(command, result) result rescue ArgumentError => e result = Result.new(output: "#{command_echo(command)}kwsh: #{e.}\n", exit_status: 2) remember_result(command, result) result end |
#run_for_agent(input, timeout_seconds: nil, cancellation: nil) ⇒ Object
Runs a command for the shell assistant without handing it the terminal.
116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 |
# File 'lib/kward/shell/persistent_session.rb', line 116 def run_for_agent(input, timeout_seconds: nil, cancellation: nil) command = input.to_s.strip return run(command, cancellation: cancellation) if command.empty? result = run(command, cancellation: cancellation) return result unless result.interactive_command result = execute_command( result.interactive_command, display_command: command, timeout_seconds: timeout_seconds, cancellation: cancellation, suppress_git_pager: true ) remember_result(command, result) result end |
#run_interactive(command, input:, sink:, tab_action_handler: nil, on_detach: nil) ⇒ Object
Runs an external command through the same persistent shell process while the caller owns the terminal.
136 137 138 139 140 141 142 143 144 145 146 147 148 149 |
# File 'lib/kward/shell/persistent_session.rb', line 136 def run_interactive(command, input:, sink:, tab_action_handler: nil, on_detach: nil) protocol = with_raw_input(input) do execute_protocol(command, input: input, sink: sink, tab_action_handler: tab_action_handler, on_detach: on_detach) end update_cwd(protocol[:cwd]) unless protocol[:background] InteractiveResult.new( exit_status: protocol[:status] || 1, input_forwarded: protocol[:input_forwarded], tab_action: protocol[:tab_action], background: protocol[:background] ) ensure sink.finish if sink.respond_to?(:finish) end |