kward 0.81.0 → 0.82.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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +29 -0
  3. data/Gemfile.lock +2 -2
  4. data/README.md +1 -1
  5. data/Rakefile +44 -4
  6. data/doc/composer.md +2 -2
  7. data/doc/files.md +8 -2
  8. data/doc/releasing.md +71 -38
  9. data/doc/security.md +1 -1
  10. data/doc/shell.md +3 -3
  11. data/kward.gemspec +1 -1
  12. data/lib/kward/adaptive_pty_output_sink.rb +183 -0
  13. data/lib/kward/cli/git.rb +5 -1
  14. data/lib/kward/cli/rendering.rb +14 -2
  15. data/lib/kward/cli/runtime_helpers.rb +103 -39
  16. data/lib/kward/cli/settings.rb +5 -13
  17. data/lib/kward/cli/tabs.rb +59 -4
  18. data/lib/kward/cli.rb +2 -0
  19. data/lib/kward/image_attachments.rb +98 -15
  20. data/lib/kward/interactive_pty_runner.rb +25 -19
  21. data/lib/kward/prompt_interface/composer_controller.rb +3 -3
  22. data/lib/kward/prompt_interface/composer_renderer.rb +27 -0
  23. data/lib/kward/prompt_interface/editor/controller.rb +6 -6
  24. data/lib/kward/prompt_interface/editor/modes/emacs.rb +2 -2
  25. data/lib/kward/prompt_interface/editor/modes/vibe.rb +2 -2
  26. data/lib/kward/prompt_interface/git_prompt.rb +1 -1
  27. data/lib/kward/prompt_interface/key_handler.rb +22 -0
  28. data/lib/kward/prompt_interface/overlay_renderer.rb +1 -0
  29. data/lib/kward/prompt_interface/project_browser.rb +162 -3
  30. data/lib/kward/prompt_interface/prompt_renderer.rb +15 -6
  31. data/lib/kward/prompt_interface/question_prompt.rb +1 -1
  32. data/lib/kward/prompt_interface/screen.rb +40 -20
  33. data/lib/kward/prompt_interface/selection_prompt.rb +5 -5
  34. data/lib/kward/prompt_interface/transcript_renderer.rb +4 -4
  35. data/lib/kward/prompt_interface.rb +83 -32
  36. data/lib/kward/pty_output_sink.rb +45 -0
  37. data/lib/kward/pty_transcript_normalizer.rb +93 -0
  38. data/lib/kward/session_catalog.rb +87 -0
  39. data/lib/kward/session_store.rb +94 -5
  40. data/lib/kward/terminal_image_support.rb +116 -0
  41. data/lib/kward/terminal_sequences.rb +43 -0
  42. data/lib/kward/version.rb +1 -1
  43. metadata +7 -4
  44. data/.github/workflows/ci.yml +0 -48
  45. data/.github/workflows/pages.yml +0 -48
@@ -0,0 +1,116 @@
1
+ require_relative "terminal_sequences"
2
+
3
+ # Namespace for the Kward CLI agent runtime.
4
+ module Kward
5
+ # Terminal capability detection for inline image protocols.
6
+ module TerminalImageSupport
7
+ KITTY = :kitty
8
+ ITERM2 = :iterm2
9
+ PROBE_TIMEOUT = 0.3
10
+ KITTY_PROBE = "#{TerminalSequences.kitty_query}\e[c".freeze
11
+ KITTY_OK_RESPONSE = "\e_Gi=31;OK\e\\".freeze
12
+ DA1_RESPONSE_PATTERN = /\e\[\?[0-9;]*c/.freeze
13
+
14
+ module_function
15
+
16
+ def detect(env: ENV, input: nil, output: nil, timeout: PROBE_TIMEOUT, probe_result: :auto)
17
+ return static_protocol(env) if input.nil? && output.nil? && probe_result == :auto
18
+ return nil unless tty?(input) && tty?(output)
19
+
20
+ probe_result = probe_kitty(input, output, env: env, timeout: timeout) if probe_result == :auto
21
+ return KITTY if probe_result == true
22
+
23
+ fallback = static_protocol(env)
24
+ return fallback if probe_result.nil? || fallback
25
+
26
+ nil
27
+ end
28
+
29
+ def static_protocol(env)
30
+ return KITTY if kitty_hint?(env)
31
+ return ITERM2 if iterm2_hint?(env)
32
+
33
+ nil
34
+ end
35
+
36
+ def probe_kitty(input, output, env: ENV, timeout: PROBE_TIMEOUT)
37
+ return nil unless tty?(input) && tty?(output)
38
+ return nil unless input.respond_to?(:raw)
39
+ return nil unless output.respond_to?(:write) && output.respond_to?(:flush)
40
+
41
+ query = env["TMUX"].to_s.empty? ? KITTY_PROBE : TerminalSequences.tmux_passthrough(KITTY_PROBE)
42
+ input.raw do
43
+ output.write(query)
44
+ output.flush
45
+ read_probe_response(input, timeout: timeout)
46
+ end
47
+ rescue StandardError
48
+ nil
49
+ end
50
+
51
+ def read_probe_response(input, timeout: PROBE_TIMEOUT)
52
+ buffer = +""
53
+ deadline = monotonic_now + timeout.to_f
54
+ loop do
55
+ break if da1_response?(buffer)
56
+
57
+ remaining = deadline - monotonic_now
58
+ break if remaining <= 0
59
+
60
+ ready = IO.select([input], nil, nil, remaining)
61
+ break unless ready
62
+
63
+ chunk = input.read_nonblock(256, exception: false)
64
+ break if chunk == :wait_readable || chunk.nil? || chunk.empty?
65
+
66
+ buffer << chunk
67
+ end
68
+
69
+ kitty_probe_success?(buffer)
70
+ rescue IOError, SystemCallError
71
+ false
72
+ end
73
+
74
+ def kitty_probe_success?(response)
75
+ da1_response?(response) && response.to_s.include?(KITTY_OK_RESPONSE)
76
+ end
77
+
78
+ def da1_response?(response)
79
+ response.to_s.match?(DA1_RESPONSE_PATTERN)
80
+ end
81
+
82
+ def kitty_hint?(env)
83
+ value = normalized_environment(env)
84
+ return true if value["KITTY_WINDOW_ID"] && !value["KITTY_WINDOW_ID"].empty?
85
+
86
+ program = value.fetch("TERM_PROGRAM", "")
87
+ term = value.fetch("TERM", "")
88
+ program == "kitty" || program == "ghostty" || term.include?("kitty") || term.include?("ghostty")
89
+ end
90
+
91
+ def iterm2_hint?(env)
92
+ value = normalized_environment(env)
93
+ program = value.fetch("TERM_PROGRAM", "")
94
+ return true if program == "iterm.app" || program == "wezterm"
95
+ return true if value.fetch("TERM_FEATURES", "").include?("f")
96
+
97
+ value["WEZTERM_PANE"] && !value["WEZTERM_PANE"].empty?
98
+ end
99
+
100
+ def normalized_environment(env)
101
+ env.to_h.each_with_object({}) do |(key, value), normalized|
102
+ normalized[key.to_s] = value.to_s.strip.downcase
103
+ end
104
+ end
105
+
106
+ def tty?(io)
107
+ io.respond_to?(:tty?) && io.tty?
108
+ rescue IOError, SystemCallError
109
+ false
110
+ end
111
+
112
+ def monotonic_now
113
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
114
+ end
115
+ end
116
+ end
@@ -18,6 +18,7 @@ module Kward
18
18
  CURSOR_SHAPE_BAR = "\e[6 q".freeze
19
19
  MOUSE_REPORTING_ENABLE = "\e[?1003h\e[?1006h".freeze
20
20
  MOUSE_REPORTING_DISABLE = "\e[?1006l\e[?1003l".freeze
21
+ SGR_RESET = "\e[0m".freeze
21
22
  SGR_INVERSE = "\e[7m".freeze
22
23
  SGR_INVERSE_OFF = "\e[27m".freeze
23
24
 
@@ -35,8 +36,50 @@ module Kward
35
36
  "\e[#{row};#{col}H"
36
37
  end
37
38
 
39
+ def move_to_column(column)
40
+ "\e[#{column}G"
41
+ end
42
+
38
43
  def osc52(text)
39
44
  "\e]52;c;#{Base64.strict_encode64(text.to_s)}\a"
40
45
  end
46
+
47
+ def kitty_graphics(data, image_id:, columns: nil, rows: nil, move_cursor: true)
48
+ payload = data.to_s
49
+ chunks = payload.scan(/.{1,4096}/m)
50
+ chunks = [""] if chunks.empty?
51
+
52
+ chunks.each_with_index.map do |chunk, index|
53
+ more = index < chunks.length - 1 ? 1 : 0
54
+ params = index.zero? ? ["a=T", "f=100", "t=d"] : []
55
+ params << "i=#{image_id}" if index.zero? && image_id
56
+ params << "c=#{columns.to_i}" if index.zero? && columns
57
+ params << "r=#{rows.to_i}" if index.zero? && rows
58
+ params << "C=1" if index.zero? && !move_cursor
59
+ params << "q=2" if index == chunks.length - 1
60
+ params << "m=#{more}"
61
+ "\e_G#{params.join(",")};#{chunk}\e\\"
62
+ end.join
63
+ end
64
+
65
+ def kitty_query(image_id = 31)
66
+ "\e_Gi=#{image_id},s=1,v=1,a=q,t=d,f=24;AAAA\e\\"
67
+ end
68
+
69
+ def kitty_delete(image_id)
70
+ "\e_Ga=d,d=I,i=#{image_id},q=2;\e\\"
71
+ end
72
+
73
+ def tmux_passthrough(sequence)
74
+ "\ePtmux;#{sequence.to_s.gsub("\e", "\e\e")}\e\\"
75
+ end
76
+
77
+ def iterm2_image(data, name: nil, width: nil, height: nil)
78
+ params = ["inline=1", "preserveAspectRatio=1"]
79
+ params << "width=#{width}" if width
80
+ params << "height=#{height}" if height
81
+ params << "name=#{Base64.strict_encode64(File.basename(name))}" if name
82
+ "\e]1337;File=#{params.join(";")}:#{data}\a"
83
+ end
41
84
  end
42
85
  end
data/lib/kward/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # Namespace for the Kward CLI agent runtime.
2
2
  module Kward
3
3
  # Current gem version.
4
- VERSION = "0.81.0"
4
+ VERSION = "0.82.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kward
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.81.0
4
+ version: 0.82.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kai Wood
@@ -116,8 +116,6 @@ executables:
116
116
  extensions: []
117
117
  extra_rdoc_files: []
118
118
  files:
119
- - ".github/workflows/ci.yml"
120
- - ".github/workflows/pages.yml"
121
119
  - ".yardopts"
122
120
  - CHANGELOG.md
123
121
  - Gemfile
@@ -170,6 +168,7 @@ files:
170
168
  - exe/kward
171
169
  - kward.gemspec
172
170
  - lib/kward.rb
171
+ - lib/kward/adaptive_pty_output_sink.rb
173
172
  - lib/kward/agent.rb
174
173
  - lib/kward/ansi.rb
175
174
  - lib/kward/auth/anthropic_oauth.rb
@@ -315,6 +314,8 @@ files:
315
314
  - lib/kward/prompts.rb
316
315
  - lib/kward/prompts/commands.rb
317
316
  - lib/kward/prompts/templates.rb
317
+ - lib/kward/pty_output_sink.rb
318
+ - lib/kward/pty_transcript_normalizer.rb
318
319
  - lib/kward/question_contract.rb
319
320
  - lib/kward/rpc/attachment_normalizer.rb
320
321
  - lib/kward/rpc/auth_manager.rb
@@ -346,6 +347,7 @@ files:
346
347
  - lib/kward/sandbox/runner_factory.rb
347
348
  - lib/kward/sandbox/unavailable_runner.rb
348
349
  - lib/kward/scratchpad_runner.rb
350
+ - lib/kward/session_catalog.rb
349
351
  - lib/kward/session_diff.rb
350
352
  - lib/kward/session_naming.rb
351
353
  - lib/kward/session_store.rb
@@ -363,6 +365,7 @@ files:
363
365
  - lib/kward/tab_store.rb
364
366
  - lib/kward/telemetry/logger.rb
365
367
  - lib/kward/telemetry/stats.rb
368
+ - lib/kward/terminal_image_support.rb
366
369
  - lib/kward/terminal_keys.rb
367
370
  - lib/kward/terminal_sequences.rb
368
371
  - lib/kward/text_boundary.rb
@@ -436,7 +439,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
436
439
  - !ruby/object:Gem::Version
437
440
  version: '0'
438
441
  requirements: []
439
- rubygems_version: 4.0.6
442
+ rubygems_version: 3.6.9
440
443
  specification_version: 4
441
444
  summary: An extendable Ruby CLI coding agent.
442
445
  test_files: []
@@ -1,48 +0,0 @@
1
- name: CI
2
-
3
- on:
4
- push:
5
- branches:
6
- - main
7
- pull_request:
8
- workflow_dispatch:
9
-
10
- permissions:
11
- contents: read
12
-
13
- jobs:
14
- test:
15
- runs-on: ubuntu-latest
16
- strategy:
17
- fail-fast: false
18
- matrix:
19
- ruby-version: ['3.4', 'ruby']
20
- steps:
21
- - name: Checkout
22
- uses: actions/checkout@v4
23
-
24
- - name: Set up Ruby
25
- uses: ruby/setup-ruby@v1
26
- with:
27
- ruby-version: ${{ matrix.ruby-version }}
28
- bundler-cache: true
29
-
30
- - name: Install sandbox test dependency
31
- run: sudo apt-get update && sudo apt-get install --yes bubblewrap
32
-
33
- - name: Enable Bubblewrap namespaces
34
- run: |
35
- sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
36
- sudo sysctl -w kernel.unprivileged_userns_clone=1
37
- bwrap --ro-bind / / --unshare-net true
38
-
39
- - name: Check syntax
40
- run: find lib test -name '*.rb' -print0 | xargs -0 ruby -c
41
-
42
- - name: Check runtime warnings
43
- run: bundle exec ruby -w -Ilib -e 'require "kward"'
44
-
45
- - name: Run tests
46
- env:
47
- KWARD_RUN_BUBBLEWRAP_TESTS: "1"
48
- run: bundle exec rake test
@@ -1,48 +0,0 @@
1
- name: Deploy documentation
2
-
3
- on:
4
- push:
5
- branches:
6
- - main
7
- workflow_dispatch:
8
-
9
- permissions:
10
- contents: read
11
- pages: write
12
- id-token: write
13
-
14
- concurrency:
15
- group: pages
16
- cancel-in-progress: false
17
-
18
- jobs:
19
- build:
20
- runs-on: ubuntu-latest
21
- steps:
22
- - name: Checkout
23
- uses: actions/checkout@v4
24
-
25
- - name: Set up Ruby
26
- uses: ruby/setup-ruby@v1
27
- with:
28
- ruby-version: '3.4'
29
- bundler-cache: true
30
-
31
- - name: Build documentation
32
- run: bundle exec rake docs:build
33
-
34
- - name: Upload Pages artifact
35
- uses: actions/upload-pages-artifact@v3
36
- with:
37
- path: _yardoc
38
-
39
- deploy:
40
- environment:
41
- name: github-pages
42
- url: ${{ steps.deployment.outputs.page_url }}
43
- runs-on: ubuntu-latest
44
- needs: build
45
- steps:
46
- - name: Deploy to GitHub Pages
47
- id: deployment
48
- uses: actions/deploy-pages@v4