kward 0.72.0 → 0.73.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +30 -0
  3. data/CHANGELOG.md +53 -0
  4. data/Gemfile.lock +2 -2
  5. data/doc/configuration.md +1 -1
  6. data/doc/editor.md +23 -2
  7. data/doc/git.md +1 -0
  8. data/doc/rpc.md +2 -2
  9. data/doc/shell.md +56 -10
  10. data/doc/usage.md +27 -1
  11. data/lib/kward/ansi.rb +62 -23
  12. data/lib/kward/cli/plugins.rb +1 -1
  13. data/lib/kward/cli/rendering.rb +4 -1
  14. data/lib/kward/cli/runtime_helpers.rb +141 -7
  15. data/lib/kward/cli/settings.rb +0 -1
  16. data/lib/kward/cli/slash_commands.rb +213 -0
  17. data/lib/kward/cli/tabs.rb +34 -4
  18. data/lib/kward/cli/tool_summaries.rb +6 -0
  19. data/lib/kward/cli.rb +4 -12
  20. data/lib/kward/clipboard.rb +2 -3
  21. data/lib/kward/compactor.rb +7 -19
  22. data/lib/kward/config_files.rb +26 -4
  23. data/lib/kward/ekwsh.rb +239 -42
  24. data/lib/kward/image_attachments.rb +3 -1
  25. data/lib/kward/interactive_pty_runner.rb +151 -0
  26. data/lib/kward/local_command_runner.rb +155 -0
  27. data/lib/kward/local_pty_command_runner.rb +171 -0
  28. data/lib/kward/model/context_usage.rb +2 -2
  29. data/lib/kward/model/payloads.rb +2 -5
  30. data/lib/kward/prompt_history.rb +5 -3
  31. data/lib/kward/prompt_interface/editor/auto_indent.rb +5 -4
  32. data/lib/kward/prompt_interface/editor/controller.rb +262 -62
  33. data/lib/kward/prompt_interface/editor/modes/emacs.rb +21 -21
  34. data/lib/kward/prompt_interface/editor/modes/modern.rb +38 -37
  35. data/lib/kward/prompt_interface/editor/modes/vibe.rb +23 -173
  36. data/lib/kward/prompt_interface/editor/modes/vibe_insert_readline.rb +166 -0
  37. data/lib/kward/prompt_interface/editor/renderer.rb +6 -5
  38. data/lib/kward/prompt_interface/editor/state.rb +28 -6
  39. data/lib/kward/prompt_interface/editor/syntax_highlighter.rb +5 -3
  40. data/lib/kward/prompt_interface/git_prompt.rb +12 -23
  41. data/lib/kward/prompt_interface/interactive/controller.rb +1 -1
  42. data/lib/kward/prompt_interface/key_handler.rb +93 -51
  43. data/lib/kward/prompt_interface/question_prompt.rb +1 -6
  44. data/lib/kward/prompt_interface/screen.rb +3 -3
  45. data/lib/kward/prompt_interface/selection_prompt.rb +3 -6
  46. data/lib/kward/prompt_interface/slash_overlay.rb +2 -0
  47. data/lib/kward/prompt_interface.rb +87 -221
  48. data/lib/kward/prompts/commands.rb +4 -0
  49. data/lib/kward/rpc/memory_methods.rb +83 -0
  50. data/lib/kward/rpc/server.rb +130 -83
  51. data/lib/kward/rpc/session_manager.rb +10 -74
  52. data/lib/kward/rpc/tool_metadata.rb +11 -0
  53. data/lib/kward/rpc/transcript_normalizer.rb +4 -39
  54. data/lib/kward/scratchpad_runner.rb +56 -0
  55. data/lib/kward/session_diff.rb +20 -3
  56. data/lib/kward/session_naming.rb +11 -0
  57. data/lib/kward/terminal_keys.rb +84 -0
  58. data/lib/kward/terminal_sequences.rb +42 -0
  59. data/lib/kward/tools/context_for_task.rb +2 -0
  60. data/lib/kward/version.rb +1 -1
  61. data/lib/kward/workers/git_guard.rb +25 -0
  62. data/lib/kward/workers/job.rb +99 -0
  63. data/lib/kward/workers/queue_runner.rb +166 -0
  64. data/lib/kward/workers/queue_store.rb +112 -0
  65. data/lib/kward/workers.rb +3 -0
  66. data/lib/kward/workspace.rb +15 -63
  67. data/templates/default/fulldoc/html/css/kward.css +33 -0
  68. data/templates/default/fulldoc/html/images/kward_screen_1.png +0 -0
  69. data/templates/default/fulldoc/html/setup.rb +1 -0
  70. data/templates/default/layout/html/layout.erb +19 -32
  71. metadata +15 -1
@@ -0,0 +1,151 @@
1
+ require "io/console"
2
+ require "pty"
3
+ require "timeout"
4
+ require_relative "local_pty_command_runner"
5
+
6
+ # Namespace for the Kward CLI agent runtime.
7
+ module Kward
8
+ # Runs a command in a PTY while forwarding caller-owned input and output IOs.
9
+ # This is intentionally low level: UI orchestration decides when terminal
10
+ # ownership is handed to the child process and how the result is presented.
11
+ class InteractivePtyRunner
12
+ Result = Struct.new(:exit_status, keyword_init: true)
13
+
14
+ READ_SIZE = 4096
15
+
16
+ def initialize(window_size_provider: nil)
17
+ @window_size_provider = window_size_provider
18
+ @window_size = nil
19
+ end
20
+
21
+ def run(*command, env: {}, cwd: Dir.pwd, input: $stdin, output: $stdout)
22
+ pid = nil
23
+ status = nil
24
+
25
+ PTY.spawn(env.to_h, *command, chdir: cwd.to_s) do |reader, writer, child_pid|
26
+ pid = child_pid
27
+ update_window_size(reader, pid)
28
+ with_raw_input(input) do
29
+ drain_initial_input(input, writer)
30
+ loop do
31
+ update_window_size(reader, pid)
32
+ readable = IO.select([reader, input], nil, nil, 0.02)&.first || []
33
+ forward_pty_output(reader, output) if readable.include?(reader)
34
+ forward_input(input, writer) if readable.include?(input)
35
+ if (finished_status = finished_status(pid))
36
+ status = finished_status
37
+ break
38
+ end
39
+ rescue Errno::EIO, IOError
40
+ break
41
+ end
42
+ end
43
+ status ||= wait_for_status(pid)
44
+ ensure
45
+ writer&.close unless writer&.closed?
46
+ end
47
+
48
+ Result.new(exit_status: exit_status(status))
49
+ end
50
+
51
+ private
52
+
53
+ def with_raw_input(input)
54
+ return yield unless input.respond_to?(:raw)
55
+
56
+ input.raw { yield }
57
+ rescue Errno::ENOTTY
58
+ yield
59
+ end
60
+
61
+ def drain_initial_input(input, writer)
62
+ loop do
63
+ chunk = input.read_nonblock(READ_SIZE, exception: false)
64
+ break if chunk.nil? || chunk == :wait_readable
65
+
66
+ writer.write(chunk)
67
+ end
68
+ writer.flush
69
+ rescue Errno::EIO, Errno::EPIPE, IOError
70
+ nil
71
+ end
72
+
73
+ def forward_pty_output(reader, output)
74
+ chunk = reader.read_nonblock(READ_SIZE, exception: false)
75
+ return if chunk.nil? || chunk == :wait_readable
76
+
77
+ output.write(chunk)
78
+ output.flush if output.respond_to?(:flush)
79
+ end
80
+
81
+ def forward_input(input, writer)
82
+ chunk = input.read_nonblock(READ_SIZE, exception: false)
83
+ return if chunk.nil? || chunk == :wait_readable
84
+
85
+ writer.write(chunk)
86
+ writer.flush
87
+ rescue Errno::EIO, Errno::EPIPE, IOError
88
+ nil
89
+ end
90
+
91
+ def update_window_size(reader, pid)
92
+ window_size = terminal_window_size
93
+ return if window_size == @window_size
94
+
95
+ @window_size = window_size
96
+ reader.winsize = window_size
97
+ signal_process("WINCH", -pid) || signal_process("WINCH", pid)
98
+ rescue StandardError
99
+ nil
100
+ end
101
+
102
+ def terminal_window_size
103
+ rows, columns = @window_size_provider ? @window_size_provider.call : IO.console&.winsize
104
+ rows = LocalPtyCommandRunner::DEFAULT_ROWS unless rows.to_i.positive?
105
+ columns = LocalPtyCommandRunner::DEFAULT_COLUMNS unless columns.to_i.positive?
106
+ [rows, columns]
107
+ rescue StandardError
108
+ [LocalPtyCommandRunner::DEFAULT_ROWS, LocalPtyCommandRunner::DEFAULT_COLUMNS]
109
+ end
110
+
111
+ def finished_status(pid)
112
+ return unless pid
113
+
114
+ finished_pid, status = Process.wait2(pid, Process::WNOHANG)
115
+ status if finished_pid
116
+ rescue Errno::ECHILD
117
+ nil
118
+ end
119
+
120
+ def wait_for_status(pid)
121
+ return unless pid
122
+
123
+ _, status = Process.wait2(pid)
124
+ status
125
+ rescue Errno::ECHILD
126
+ nil
127
+ end
128
+
129
+ def exit_status(status)
130
+ return 1 unless status
131
+ return status.exitstatus if status.exited?
132
+ return 128 + status.termsig if status.signaled?
133
+
134
+ 1
135
+ end
136
+
137
+ def process_running?(pid)
138
+ Process.kill(0, pid)
139
+ true
140
+ rescue Errno::ESRCH
141
+ false
142
+ end
143
+
144
+ def signal_process(signal, pid)
145
+ Process.kill(signal, pid)
146
+ true
147
+ rescue Errno::ESRCH, Errno::EINVAL, Errno::EPERM
148
+ false
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,155 @@
1
+ require "open3"
2
+ require "thread"
3
+ require "timeout"
4
+ require_relative "cancellation"
5
+
6
+ # Namespace for the Kward CLI agent runtime.
7
+ module Kward
8
+ # Low-level local process runner with bounded capture, timeout, cancellation,
9
+ # and optional output streaming. Callers own command semantics and formatting.
10
+ class LocalCommandRunner
11
+ Result = Struct.new(:stdout, :stderr, :exit_status, :timed_out, :truncated, keyword_init: true)
12
+
13
+ READ_SIZE = 4096
14
+
15
+ def initialize(timeout_seconds:, max_output_bytes:, terminate_on_output_limit: false)
16
+ @timeout_seconds = timeout_seconds.to_i.positive? ? timeout_seconds.to_i : 30
17
+ @max_output_bytes = max_output_bytes.to_i.positive? ? max_output_bytes.to_i : 128 * 1024
18
+ @terminate_on_output_limit = terminate_on_output_limit
19
+ end
20
+
21
+ def run(*command, env: {}, cwd: Dir.pwd, cancellation: nil, &block)
22
+ cancellation&.raise_if_cancelled!
23
+ stdout_buffer = +""
24
+ stderr_buffer = +""
25
+ captured_bytes = 0
26
+ truncated = false
27
+ timed_out = false
28
+ queue = Queue.new
29
+
30
+ Open3.popen3(env.to_h, *command, chdir: cwd.to_s, pgroup: true) do |stdin, stdout, stderr, wait_thread|
31
+ stdin.close
32
+ readers = [
33
+ read_stream(stdout, :stdout, queue),
34
+ read_stream(stderr, :stderr, queue)
35
+ ]
36
+ cancellation&.on_cancel { terminate_process_group(wait_thread.pid) }
37
+
38
+ status = wait_for_process(wait_thread, readers, queue, cancellation: cancellation) do |stream, chunk|
39
+ captured_bytes, truncated, captured_chunk = capture_chunk(
40
+ stream,
41
+ chunk,
42
+ stdout_buffer,
43
+ stderr_buffer,
44
+ captured_bytes,
45
+ truncated
46
+ )
47
+ block&.call(stream, captured_chunk) unless captured_chunk.empty?
48
+ terminate_process_group(wait_thread.pid) if truncated && @terminate_on_output_limit
49
+ end
50
+
51
+ join_readers(readers)
52
+ drain_queue(queue) do |stream, chunk|
53
+ captured_bytes, truncated, captured_chunk = capture_chunk(
54
+ stream,
55
+ chunk,
56
+ stdout_buffer,
57
+ stderr_buffer,
58
+ captured_bytes,
59
+ truncated
60
+ )
61
+ block&.call(stream, captured_chunk) unless captured_chunk.empty?
62
+ end
63
+
64
+ Result.new(stdout: stdout_buffer, stderr: stderr_buffer, exit_status: status.exitstatus || 1, timed_out: false, truncated: truncated)
65
+ rescue Timeout::Error
66
+ timed_out = true
67
+ terminate_process_group(wait_thread.pid)
68
+ join_readers(readers)
69
+ Result.new(stdout: stdout_buffer, stderr: stderr_buffer, exit_status: nil, timed_out: timed_out, truncated: truncated)
70
+ ensure
71
+ readers&.each { |reader| reader.kill if reader&.alive? }
72
+ end
73
+ end
74
+
75
+ private
76
+
77
+ def read_stream(io, stream, queue)
78
+ Thread.new do
79
+ loop do
80
+ queue << [stream, io.readpartial(READ_SIZE)]
81
+ rescue EOFError
82
+ break
83
+ end
84
+ end
85
+ end
86
+
87
+ def wait_for_process(wait_thread, readers, queue, cancellation:)
88
+ deadline = Time.now + @timeout_seconds
89
+ loop do
90
+ cancellation&.raise_if_cancelled!
91
+ drain_queue(queue) { |stream, chunk| yield(stream, chunk) }
92
+ if wait_thread.join(0.02)
93
+ cancellation&.raise_if_cancelled!
94
+ return wait_thread.value
95
+ end
96
+ raise Timeout::Error if Time.now >= deadline
97
+ end
98
+ rescue Cancellation::CancelledError
99
+ terminate_process_group(wait_thread.pid)
100
+ join_readers(readers)
101
+ raise
102
+ end
103
+
104
+ def drain_queue(queue)
105
+ loop do
106
+ stream, chunk = queue.pop(true)
107
+ yield(stream, chunk)
108
+ rescue ThreadError
109
+ break
110
+ end
111
+ end
112
+
113
+ def capture_chunk(stream, chunk, stdout_buffer, stderr_buffer, captured_bytes, truncated)
114
+ return [captured_bytes, truncated, ""] if truncated
115
+
116
+ remaining = @max_output_bytes - captured_bytes
117
+ if chunk.bytesize > remaining
118
+ truncated = true
119
+ chunk = remaining.positive? ? chunk.byteslice(0, remaining).to_s : ""
120
+ end
121
+
122
+ stream == :stderr ? stderr_buffer << chunk : stdout_buffer << chunk
123
+ [captured_bytes + chunk.bytesize, truncated, chunk]
124
+ end
125
+
126
+ def join_readers(readers)
127
+ readers.to_a.each { |reader| reader.join(0.1) }
128
+ end
129
+
130
+ def terminate_process_group(pid)
131
+ signal_process("TERM", -pid) || signal_process("TERM", pid)
132
+ deadline = Time.now + 0.2
133
+ while Time.now < deadline
134
+ return unless process_running?(pid)
135
+
136
+ sleep 0.02
137
+ end
138
+ signal_process("KILL", -pid) || signal_process("KILL", pid)
139
+ end
140
+
141
+ def process_running?(pid)
142
+ Process.kill(0, pid)
143
+ true
144
+ rescue Errno::ESRCH
145
+ false
146
+ end
147
+
148
+ def signal_process(signal, pid)
149
+ Process.kill(signal, pid)
150
+ true
151
+ rescue Errno::ESRCH, Errno::EINVAL, Errno::EPERM
152
+ false
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,171 @@
1
+ require "io/console"
2
+ require "pty"
3
+ require "timeout"
4
+ require_relative "cancellation"
5
+ require_relative "local_command_runner"
6
+
7
+ # Namespace for the Kward CLI agent runtime.
8
+ module Kward
9
+ # Low-level pseudo-terminal command runner with bounded capture, timeout,
10
+ # cancellation, and optional output streaming. This gives child processes a
11
+ # TTY without trying to emulate an interactive terminal.
12
+ class LocalPtyCommandRunner
13
+ Result = LocalCommandRunner::Result
14
+
15
+ READ_SIZE = 4096
16
+ DEFAULT_ROWS = 24
17
+ DEFAULT_COLUMNS = 80
18
+
19
+ def initialize(timeout_seconds:, max_output_bytes:, terminate_on_output_limit: false, window_size_provider: nil)
20
+ @timeout_seconds = timeout_seconds.to_i.positive? ? timeout_seconds.to_i : 30
21
+ @max_output_bytes = max_output_bytes.to_i.positive? ? max_output_bytes.to_i : 128 * 1024
22
+ @terminate_on_output_limit = terminate_on_output_limit
23
+ @window_size_provider = window_size_provider
24
+ @window_size = nil
25
+ end
26
+
27
+ def run(*command, env: {}, cwd: Dir.pwd, cancellation: nil, &block)
28
+ cancellation&.raise_if_cancelled!
29
+ output = +""
30
+ captured_bytes = 0
31
+ truncated = false
32
+ timed_out = false
33
+ cancelled = false
34
+ pid = nil
35
+ status = nil
36
+
37
+ PTY.spawn(env.to_h, *command, chdir: cwd.to_s) do |reader, _writer, child_pid|
38
+ pid = child_pid
39
+ update_window_size(reader, pid)
40
+ cancellation&.on_cancel do
41
+ cancelled = true
42
+ terminate_process_group(pid)
43
+ end
44
+
45
+ begin
46
+ deadline = Time.now + @timeout_seconds
47
+ loop do
48
+ cancellation&.raise_if_cancelled!
49
+ raise Timeout::Error if Time.now >= deadline
50
+
51
+ update_window_size(reader, pid)
52
+ readable, = IO.select([reader], nil, nil, 0.02)
53
+ next unless readable
54
+
55
+ chunk = read_chunk(reader)
56
+ break if chunk.nil?
57
+
58
+ chunk = normalize_line_endings(chunk)
59
+ captured_bytes, truncated, captured_chunk = capture_chunk(chunk, output, captured_bytes, truncated)
60
+ block&.call(:stdout, captured_chunk) unless captured_chunk.empty?
61
+ terminate_process_group(pid) if truncated && @terminate_on_output_limit
62
+ end
63
+ rescue Errno::EIO
64
+ nil
65
+ end
66
+
67
+ status = wait_for_status(pid)
68
+ end
69
+
70
+ cancellation&.raise_if_cancelled! if cancelled
71
+ Result.new(stdout: output, stderr: "", exit_status: exit_status(status), timed_out: false, truncated: truncated)
72
+ rescue Timeout::Error
73
+ timed_out = true
74
+ terminate_process_group(pid) if pid
75
+ wait_for_status(pid) if pid
76
+ Result.new(stdout: output, stderr: "", exit_status: nil, timed_out: timed_out, truncated: truncated)
77
+ rescue Cancellation::CancelledError
78
+ terminate_process_group(pid) if pid
79
+ wait_for_status(pid) if pid
80
+ raise
81
+ end
82
+
83
+ private
84
+
85
+ def update_window_size(reader, pid)
86
+ window_size = terminal_window_size
87
+ return if window_size == @window_size
88
+
89
+ @window_size = window_size
90
+ reader.winsize = window_size
91
+ signal_process("WINCH", -pid) || signal_process("WINCH", pid)
92
+ rescue StandardError
93
+ nil
94
+ end
95
+
96
+ def terminal_window_size
97
+ rows, columns = @window_size_provider ? @window_size_provider.call : IO.console&.winsize
98
+ rows = DEFAULT_ROWS unless rows.to_i.positive?
99
+ columns = DEFAULT_COLUMNS unless columns.to_i.positive?
100
+ [rows, columns]
101
+ rescue StandardError
102
+ [DEFAULT_ROWS, DEFAULT_COLUMNS]
103
+ end
104
+
105
+ def read_chunk(reader)
106
+ reader.read_nonblock(READ_SIZE, exception: false).tap do |chunk|
107
+ return nil if chunk.nil?
108
+ return nil if chunk == :wait_readable
109
+ end
110
+ end
111
+
112
+ def normalize_line_endings(chunk)
113
+ chunk.gsub("\r\r\n", "\n").gsub("\r\n", "\n")
114
+ end
115
+
116
+ def capture_chunk(chunk, output, captured_bytes, truncated)
117
+ return [captured_bytes, truncated, ""] if truncated
118
+
119
+ remaining = @max_output_bytes - captured_bytes
120
+ if chunk.bytesize > remaining
121
+ truncated = true
122
+ chunk = remaining.positive? ? chunk.byteslice(0, remaining).to_s : ""
123
+ end
124
+
125
+ output << chunk
126
+ [captured_bytes + chunk.bytesize, truncated, chunk]
127
+ end
128
+
129
+ def wait_for_status(pid)
130
+ return unless pid
131
+
132
+ _, status = Process.wait2(pid)
133
+ status
134
+ rescue Errno::ECHILD
135
+ nil
136
+ end
137
+
138
+ def exit_status(status)
139
+ return 1 unless status
140
+ return status.exitstatus if status.exited?
141
+ return 128 + status.termsig if status.signaled?
142
+
143
+ 1
144
+ end
145
+
146
+ def terminate_process_group(pid)
147
+ signal_process("TERM", -pid) || signal_process("TERM", pid)
148
+ deadline = Time.now + 0.2
149
+ while Time.now < deadline
150
+ return unless process_running?(pid)
151
+
152
+ sleep 0.02
153
+ end
154
+ signal_process("KILL", -pid) || signal_process("KILL", pid)
155
+ end
156
+
157
+ def process_running?(pid)
158
+ Process.kill(0, pid)
159
+ true
160
+ rescue Errno::ESRCH
161
+ false
162
+ end
163
+
164
+ def signal_process(signal, pid)
165
+ Process.kill(signal, pid)
166
+ true
167
+ rescue Errno::ESRCH, Errno::EINVAL, Errno::EPERM
168
+ false
169
+ end
170
+ end
171
+ end
@@ -12,7 +12,7 @@ module Kward
12
12
  def call(provider:, model:, context_window:, context_parts:)
13
13
  return nil unless context_window
14
14
 
15
- parts = redact_image_data(stringify_keys(context_parts || {}))
15
+ parts = redact_image_data(stringify_top_level_keys(context_parts || {}))
16
16
  return nil unless contains_session_content?(parts)
17
17
 
18
18
  payload = prompt_payload(parts)
@@ -71,7 +71,7 @@ module Kward
71
71
  ["data", "image_url"].include?(key.to_s)
72
72
  end
73
73
 
74
- def stringify_keys(value)
74
+ def stringify_top_level_keys(value)
75
75
  return value unless value.is_a?(Hash)
76
76
 
77
77
  value.each_with_object({}) { |(key, item), result| result[key.to_s] = item }
@@ -1,6 +1,7 @@
1
1
  require "json"
2
2
  require_relative "../image_attachments"
3
3
  require_relative "../message_access"
4
+ require_relative "../tools/tool_call"
4
5
  require_relative "model_info"
5
6
 
6
7
  # Namespace for the Kward CLI agent runtime.
@@ -255,11 +256,7 @@ module Kward
255
256
  end
256
257
 
257
258
  def parse_tool_arguments(arguments)
258
- return arguments if arguments.is_a?(Hash)
259
-
260
- JSON.parse(arguments.to_s.empty? ? "{}" : arguments.to_s)
261
- rescue JSON::ParserError
262
- {}
259
+ ToolCall.parse_arguments(arguments)
263
260
  end
264
261
 
265
262
  def anthropic_tool_schema(tool)
@@ -11,13 +11,14 @@ module Kward
11
11
 
12
12
  Entry = Struct.new(:value, :timestamp, keyword_init: true)
13
13
 
14
- def initialize(config_dir: ConfigFiles.config_dir, cwd: Dir.pwd, limit: DEFAULT_LIMIT)
14
+ def initialize(config_dir: ConfigFiles.config_dir, cwd: Dir.pwd, limit: DEFAULT_LIMIT, kind: "prompt")
15
15
  @config_dir = config_dir
16
16
  @cwd = ConfigFiles.canonical_workspace_root(cwd)
17
17
  @limit = limit.to_i.positive? ? limit.to_i : DEFAULT_LIMIT
18
+ @kind = kind.to_s.empty? ? "prompt" : kind.to_s
18
19
  end
19
20
 
20
- attr_reader :cwd, :limit
21
+ attr_reader :cwd, :limit, :kind
21
22
 
22
23
  def values
23
24
  entries.map(&:value)
@@ -35,7 +36,7 @@ module Kward
35
36
  end
36
37
 
37
38
  def path
38
- ConfigFiles.prompt_history_path(@cwd, config_dir: @config_dir)
39
+ ConfigFiles.prompt_history_path(@cwd, config_dir: @config_dir, kind: @kind)
39
40
  end
40
41
 
41
42
  private
@@ -73,6 +74,7 @@ module Kward
73
74
  {
74
75
  type: "prompt_history_header",
75
76
  version: 1,
77
+ kind: @kind,
76
78
  workspace: @cwd,
77
79
  workspaceHash: File.basename(path, ".jsonl"),
78
80
  limit: limit
@@ -12,8 +12,8 @@ module Kward
12
12
  LUA_INDENT_KEYWORDS = %w[do else elseif for function if repeat then while].freeze
13
13
  SHELL_DEDENT_KEYWORDS = %w[fi done esac].freeze
14
14
  PUNCTUATION_PAIRS = { "}" => "{", "]" => "[", ")" => "(" }.freeze
15
- EDITOR_TAB_SEQUENCES = ["\t", "\e[9u", "\e[9;1u", "\e[27;1;9~"].freeze
16
- EDITOR_SHIFT_TAB_SEQUENCES = ["\e[Z", "\e[1;2Z", "\e[9;2u", "\e[27;2;9~"].freeze
15
+ EDITOR_TAB_SEQUENCES = TerminalKeys::TAB
16
+ EDITOR_SHIFT_TAB_SEQUENCES = TerminalKeys::SHIFT_TAB
17
17
 
18
18
  private
19
19
 
@@ -268,9 +268,10 @@ module Kward
268
268
  end
269
269
 
270
270
  def editor_indent_unit
271
+ path = @editor_state.path || @editor_state.display_path
271
272
  @editor_indent_unit_path ||= nil
272
- if @editor_indent_unit_path != @editor_state.path
273
- @editor_indent_unit_path = @editor_state.path
273
+ if @editor_indent_unit_path != path
274
+ @editor_indent_unit_path = path
274
275
  @editor_indent_unit = detect_editor_indent_unit
275
276
  end
276
277
  @editor_indent_unit