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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +29 -0
- data/Gemfile.lock +2 -2
- data/README.md +1 -1
- data/Rakefile +44 -4
- data/doc/composer.md +2 -2
- data/doc/files.md +8 -2
- data/doc/releasing.md +71 -38
- data/doc/security.md +1 -1
- data/doc/shell.md +3 -3
- data/kward.gemspec +1 -1
- data/lib/kward/adaptive_pty_output_sink.rb +183 -0
- data/lib/kward/cli/git.rb +5 -1
- data/lib/kward/cli/rendering.rb +14 -2
- data/lib/kward/cli/runtime_helpers.rb +103 -39
- data/lib/kward/cli/settings.rb +5 -13
- data/lib/kward/cli/tabs.rb +59 -4
- data/lib/kward/cli.rb +2 -0
- data/lib/kward/image_attachments.rb +98 -15
- data/lib/kward/interactive_pty_runner.rb +25 -19
- data/lib/kward/prompt_interface/composer_controller.rb +3 -3
- data/lib/kward/prompt_interface/composer_renderer.rb +27 -0
- data/lib/kward/prompt_interface/editor/controller.rb +6 -6
- data/lib/kward/prompt_interface/editor/modes/emacs.rb +2 -2
- data/lib/kward/prompt_interface/editor/modes/vibe.rb +2 -2
- data/lib/kward/prompt_interface/git_prompt.rb +1 -1
- data/lib/kward/prompt_interface/key_handler.rb +22 -0
- data/lib/kward/prompt_interface/overlay_renderer.rb +1 -0
- data/lib/kward/prompt_interface/project_browser.rb +162 -3
- data/lib/kward/prompt_interface/prompt_renderer.rb +15 -6
- data/lib/kward/prompt_interface/question_prompt.rb +1 -1
- data/lib/kward/prompt_interface/screen.rb +40 -20
- data/lib/kward/prompt_interface/selection_prompt.rb +5 -5
- data/lib/kward/prompt_interface/transcript_renderer.rb +4 -4
- data/lib/kward/prompt_interface.rb +83 -32
- data/lib/kward/pty_output_sink.rb +45 -0
- data/lib/kward/pty_transcript_normalizer.rb +93 -0
- data/lib/kward/session_catalog.rb +87 -0
- data/lib/kward/session_store.rb +94 -5
- data/lib/kward/terminal_image_support.rb +116 -0
- data/lib/kward/terminal_sequences.rb +43 -0
- data/lib/kward/version.rb +1 -1
- metadata +7 -4
- data/.github/workflows/ci.yml +0 -48
- data/.github/workflows/pages.yml +0 -48
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e4e40d786a99770eef30a7eb7fec41470d927f9d889160d1c6db9f8fd1ebec39
|
|
4
|
+
data.tar.gz: 1cfaf680bf9b5ef313e0a0f71a3fb33ea394eb7bc2f5d19c5bfee4c38898ac49
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: efe8e4a599791fee93d0329c7d0ec4e87c9d45c9ad9f99165d9172d44410ccc83de8014802cc76481d37952e89427b80cac1022a2f7fc17d0bd3c05356d44391
|
|
7
|
+
data.tar.gz: 9b1ad11cfd582d78d4834cf5b6f168ca8e2f987abe99804678d5e58c2e3000fcff6ebbf30448a71aa1f0ca7a94e9e9585be7b0643d6c88e3ed931e3ab6f3eada
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,35 @@ All notable changes to Kward will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.82.0] - 2026-08-23
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Added `script/release` for validated, one-command release preparation from an argument or standard input, including changelog/version updates, preflight checks, an annotated tag, and an atomic push.
|
|
12
|
+
- Added a tag-triggered GitHub Actions release workflow using RubyGems trusted publishing, published-artifact checksum verification, changelog-based GitHub Releases, and safe reruns after partial publication.
|
|
13
|
+
- Added `+` and `-` controls for resizing inline `/files` image previews within the available overlay space.
|
|
14
|
+
- Added inline read-only previews for PNG, JPEG, GIF, and WebP files opened from `/files` in terminals that support Kitty or iTerm2 image sequences, with local conversion for Kitty-compatible terminals when needed.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- Excluded GitHub workflow and local release-script files from packaged gems and added explicit release metadata and package-content checks.
|
|
19
|
+
- Changed interactive PTY handoff to render conservative line/progress output above a frozen composer, with a permanent switch to exclusive full-terminal passthrough when the child emits screen-oriented or unknown controls.
|
|
20
|
+
- Kept safe, normalized shell output as explicit tab-local transient state so completed `/shell` and `!command` output can be reconstructed after tab switches without entering session history or model context.
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- Made the test environment explicitly disable terminal color and made unified diff-viewer tests select that mode explicitly, keeping release preflight assertions deterministic across terminal capabilities and widths.
|
|
25
|
+
- Kept `/files` image previews bounded within a fully cleared, bordered file-list overlay above the visible composer and preserved their aspect ratio in Kitty-compatible terminals.
|
|
26
|
+
- Fixed inline image protocol detection and viewer handoff for Ghostty and other Kitty-compatible terminals by retrying inconclusive probes, honoring recognized Kitty hints, suppressing Kitty acknowledgements, and using valid, chunked graphics sequences.
|
|
27
|
+
- Kept synchronized-output updates such as Homebrew downloads in the inline PTY region instead of resetting the cursor through an unnecessary exclusive handoff.
|
|
28
|
+
- Retained the final visible state of carriage-return and horizontal-cursor progress output from commands such as `git push` and Homebrew when reconstructing the transcript after PTY handback.
|
|
29
|
+
- Cached rebuildable session-list summaries so opening `/session` no longer reparses every unchanged conversation log, while retaining automatic fallback for existing, changed, or corrupt session data.
|
|
30
|
+
- Refreshed composer status immediately after model runtime changes and `/git` returns, so selected model details and Git dirty-state color do not remain stale until another input.
|
|
31
|
+
- Kept echoed passwords, OTPs, and other child input out of transient tab state by stopping safe PTY output retention before the first forwarded input byte.
|
|
32
|
+
- Reconstructed safe transient shell output as part of the PTY ownership handback frame, avoiding an extra clear and redraw after commands such as `ls`.
|
|
33
|
+
- Preserved the active `/shell` prompt and composer when Ctrl+L clears transient shell output, and avoided a redundant preliminary redraw.
|
|
34
|
+
- Reconstructed the complete Kward screen after interactive PTY handoff, including failure paths, instead of relying on the child process cursor position.
|
|
35
|
+
|
|
7
36
|
## [0.81.0] - 2026-08-20
|
|
8
37
|
|
|
9
38
|
### Added
|
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
kward (0.
|
|
4
|
+
kward (0.82.0)
|
|
5
5
|
base64
|
|
6
6
|
nokogiri
|
|
7
7
|
tiktoken_ruby
|
|
@@ -146,7 +146,7 @@ CHECKSUMS
|
|
|
146
146
|
html-proofer (5.2.1) sha256=fdd958a7cbf9c3255fb96fe7cfc4e611f64e2706e469488a3326309ad007d2fd
|
|
147
147
|
io-event (1.16.2) sha256=9f9cb0a96ea5c3850a672606c65f27bc96d7621399ef6196acbfe2be0cd1279c
|
|
148
148
|
json (2.19.9) sha256=9b9025b7cdddafa38d316eca0b2358488e42d417045c1b90d216a9fefe46b79a
|
|
149
|
-
kward (0.
|
|
149
|
+
kward (0.82.0)
|
|
150
150
|
logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203
|
|
151
151
|
metrics (0.15.0) sha256=61ded5bac95118e995b1bc9ed4a5f19bc9814928a312a85b200abbdac9039072
|
|
152
152
|
minitest (6.0.6) sha256=153ea36d1d987a62942382b61075745042a2b3123b1cd48f4c3675af9cc7d6f1
|
data/README.md
CHANGED
|
@@ -92,7 +92,7 @@ Extend and integrate:
|
|
|
92
92
|
- [Plugins](doc/plugins.md): trusted Ruby plugins for commands, footer UI, prompt context, transcript events, and RPC clients.
|
|
93
93
|
- [Lifecycle hooks](doc/lifecycle-hooks.md): deterministic runtime hooks for policy, approvals, automation, and command-hook integrations.
|
|
94
94
|
- [RPC protocol](doc/rpc.md): JSON-RPC backend mode for trusted local UI clients.
|
|
95
|
-
- [Releasing](doc/releasing.md):
|
|
95
|
+
- [Releasing](doc/releasing.md): prepare a version and publish it through RubyGems and GitHub Releases.
|
|
96
96
|
|
|
97
97
|
Reference guides:
|
|
98
98
|
|
data/Rakefile
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
require "bundler/gem_tasks"
|
|
1
2
|
require "fileutils"
|
|
2
3
|
require "html-proofer"
|
|
4
|
+
require "open3"
|
|
3
5
|
require "rdoc/task"
|
|
4
6
|
require "rubygems/package"
|
|
5
7
|
require "webrick"
|
|
@@ -33,6 +35,36 @@ def packaged_gem_files(gem_name)
|
|
|
33
35
|
gem.spec.files.sort
|
|
34
36
|
end
|
|
35
37
|
|
|
38
|
+
def verify_release_metadata
|
|
39
|
+
version = Kward::VERSION
|
|
40
|
+
tag = "v#{version}"
|
|
41
|
+
changelog = File.read("CHANGELOG.md")
|
|
42
|
+
abort("CHANGELOG.md is missing a [#{version}] release heading") unless changelog.match?(/^## \[#{Regexp.escape(version)}\] - \d{4}-\d{2}-\d{2}$/)
|
|
43
|
+
|
|
44
|
+
if ENV["GITHUB_REF_TYPE"] == "tag" && ENV["GITHUB_REF_NAME"] != tag
|
|
45
|
+
abort("GitHub tag #{ENV["GITHUB_REF_NAME"]} does not match gem version #{version}")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
tag_commit, tag_status = Open3.capture2e("git", "rev-parse", "--verify", "#{tag}^{commit}")
|
|
49
|
+
return unless tag_status.success?
|
|
50
|
+
|
|
51
|
+
head_commit = `git rev-parse HEAD`.strip
|
|
52
|
+
abort("Tag #{tag} does not point at HEAD") unless tag_commit.strip == head_commit
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def verify_packaged_gem(gem_name)
|
|
56
|
+
files = packaged_gem_files(gem_name)
|
|
57
|
+
required = ["exe/kward", "lib/kward/version.rb"]
|
|
58
|
+
missing = required - files
|
|
59
|
+
abort("Packaged gem is missing: #{missing.join(", ")}") unless missing.empty?
|
|
60
|
+
|
|
61
|
+
forbidden_prefixes = [".github/", "script/", "test/", "plan/"]
|
|
62
|
+
forbidden = files.select { |file| forbidden_prefixes.any? { |prefix| file.start_with?(prefix) } }
|
|
63
|
+
abort("Packaged gem includes development files: #{forbidden.join(", ")}") unless forbidden.empty?
|
|
64
|
+
|
|
65
|
+
files
|
|
66
|
+
end
|
|
67
|
+
|
|
36
68
|
def rewrite_yard_markdown_links
|
|
37
69
|
guide_names = Dir.glob("doc/*.md").map { |path| File.basename(path, ".md") }
|
|
38
70
|
|
|
@@ -75,15 +107,23 @@ YARD::Rake::YardocTask.new do |yard|
|
|
|
75
107
|
end
|
|
76
108
|
|
|
77
109
|
namespace :release do
|
|
110
|
+
desc "Verify the version, changelog, and release tag agree"
|
|
111
|
+
task :verify do
|
|
112
|
+
verify_release_metadata
|
|
113
|
+
end
|
|
114
|
+
|
|
78
115
|
desc "Run release checks and build a local gem"
|
|
79
|
-
task preflight: [:test, "docs:check"] do
|
|
80
|
-
gem_name = "kward-#{Kward::VERSION}.gem"
|
|
116
|
+
task preflight: ["release:verify", :test, "docs:check"] do
|
|
117
|
+
gem_name = File.join("pkg", "kward-#{Kward::VERSION}.gem")
|
|
81
118
|
FileUtils.rm_f(gem_name)
|
|
82
|
-
|
|
83
|
-
puts
|
|
119
|
+
Rake::Task["build"].invoke
|
|
120
|
+
puts verify_packaged_gem(gem_name)
|
|
84
121
|
end
|
|
85
122
|
end
|
|
86
123
|
|
|
124
|
+
Rake::Task["release:source_control_push"].enhance(["release:verify"])
|
|
125
|
+
Rake::Task["release:rubygem_push"].enhance(["release:verify"])
|
|
126
|
+
|
|
87
127
|
namespace :docs do
|
|
88
128
|
desc "Serve the built YARD documentation site locally and rebuild on changes"
|
|
89
129
|
task serve: :build do
|
data/doc/composer.md
CHANGED
|
@@ -186,7 +186,7 @@ Example:
|
|
|
186
186
|
|
|
187
187
|
If the cursor is at the start of an otherwise empty draft, Backspace removes the most recently added pending attachment. Submitting the draft clears its pending attachments.
|
|
188
188
|
|
|
189
|
-
The active model must support image input. iTerm2 and Kitty-compatible terminals can render submitted images inline;
|
|
189
|
+
The active model must support image input. iTerm2 and Kitty-compatible terminals can render submitted images inline; Kward uses active Kitty capability detection when running on a real TTY and falls back to conservative terminal hints otherwise. Other terminals still show attachment badges and send the image to the model without an inline preview.
|
|
190
190
|
|
|
191
191
|
## Understand composer status
|
|
192
192
|
|
|
@@ -213,4 +213,4 @@ If rendering becomes corrupted after resize or after an external program writes
|
|
|
213
213
|
|
|
214
214
|
You can also press `Ctrl+L`; it rebuilds the durable current-tab view without deleting the conversation transcript, removing transient shell output. If a shortcut never reaches Kward, prefer its slash-command equivalent or change `composer.tab_keybindings` in `/settings`.
|
|
215
215
|
|
|
216
|
-
External commands inside `/shell` and one-shot `!command` input receive an interactive PTY by default,
|
|
216
|
+
External commands inside `/shell` and one-shot `!command` input receive an interactive PTY by default. They begin inline above a frozen composer, then switch to full-terminal passthrough if they emit screen-oriented or unknown terminal controls; full-screen programs such as Vim or `less` can therefore temporarily own the terminal. Use `capture <command>` inside `/shell` or `/capture <command>` from the normal composer for bounded, sanitized transcript output.
|
data/doc/files.md
CHANGED
|
@@ -12,9 +12,9 @@ From an interactive Kward session, run:
|
|
|
12
12
|
/files
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
Kward opens the project file browser. Use the arrow keys or `j`/`k` to move through the tree, then press `Enter` on a file to open it in the integrated editor.
|
|
15
|
+
Kward opens the project file browser. Use the arrow keys or `j`/`k` to move through the tree, then press `Enter` on a file to open it in the integrated editor. Supported images (PNG, JPEG, GIF, and WebP) open as read-only inline previews when the terminal supports Kitty or iTerm2 image sequences.
|
|
16
16
|
|
|
17
|
-
When you quit the editor, Kward returns to the file browser at the same position so you can keep browsing nearby files.
|
|
17
|
+
When you quit the editor or close an image preview, Kward returns to the file browser at the same position so you can keep browsing nearby files.
|
|
18
18
|
|
|
19
19
|
## What appears in the browser
|
|
20
20
|
|
|
@@ -40,6 +40,8 @@ Outside Git, Kward scans the workspace directory and skips common noisy director
|
|
|
40
40
|
| `/` | Start search |
|
|
41
41
|
| `Backspace` | Delete the last search character |
|
|
42
42
|
| `Esc` | Leave search; press again to close the browser |
|
|
43
|
+
| `Q` | Close an image preview |
|
|
44
|
+
| `+` / `-` | Zoom an image preview in / out |
|
|
43
45
|
| `@` | Insert the selected file as an `@path` mention |
|
|
44
46
|
|
|
45
47
|
Directories use `▸` and `▾` markers to show collapsed and expanded state. Files are shown under their containing directory with indentation. File-type icons are off by default; users with a compatible Nerd Font can enable them under Interface in `/settings`. See [Configuration](configuration.md#project-browser-icons).
|
|
@@ -92,6 +94,10 @@ Kward remembers the expanded folders and selected path for each workspace. The n
|
|
|
92
94
|
|
|
93
95
|
Search itself is temporary. Closing search returns to the normal tree, and closing the browser leaves your chat session intact.
|
|
94
96
|
|
|
97
|
+
## Image previews
|
|
98
|
+
|
|
99
|
+
Image previews are read-only and replace the file-list overlay while leaving the prompt composer and tabs visible. Press `+` or `-` to make the image larger or smaller within the available overlay space. Press `Esc` or `Q` to return to `/files`. Kward probes Kitty-compatible terminals when it has a real interactive TTY, uses recognized terminal identity hints when probing is inconclusive, and retries transient detection failures. Terminals without reachable Kitty or iTerm2 inline-image support keep the selection in the browser and show a status message instead of opening binary data in the text editor. Kitty-compatible terminals use PNG data; JPEG, GIF, and WebP previews require an available local image converter.
|
|
100
|
+
|
|
95
101
|
## Notes and limitations
|
|
96
102
|
|
|
97
103
|
- `/files` is only available in the interactive prompt.
|
data/doc/releasing.md
CHANGED
|
@@ -1,74 +1,107 @@
|
|
|
1
1
|
# Releasing Kward
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Kward releases are prepared locally and published by GitHub Actions. The local command updates the version and changelog, runs every release check, creates the release commit and annotated tag, then pushes both atomically. The tag-triggered workflow publishes the gem to RubyGems.org and creates a GitHub Release with the same gem attached.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## One-time setup
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Kward publishes through [RubyGems trusted publishing](https://guides.rubygems.org/trusted-publishing/), so the repository does not need a long-lived RubyGems API key.
|
|
8
8
|
|
|
9
|
-
Before
|
|
9
|
+
Before the first automated release:
|
|
10
10
|
|
|
11
|
-
1.
|
|
12
|
-
2.
|
|
13
|
-
|
|
11
|
+
1. In the GitHub repository, create an Actions environment named `release`. Add required reviewers if releases should have a manual approval gate.
|
|
12
|
+
2. On the RubyGems.org page for the `kward` gem, add a trusted publisher with:
|
|
13
|
+
- Repository owner: `kaiwood`
|
|
14
|
+
- Repository name: `kward`
|
|
15
|
+
- Workflow filename: `release.yml`
|
|
16
|
+
- Environment: `release`
|
|
17
|
+
3. Confirm GitHub Actions can write repository contents. The release job requests only `contents: write` for the GitHub Release and `id-token: write` for RubyGems OIDC authentication.
|
|
14
18
|
|
|
15
|
-
|
|
16
|
-
bundle exec rake release:preflight
|
|
17
|
-
```
|
|
19
|
+
GitHub Packages is intentionally not used. RubyGems.org remains the canonical package registry; the built `.gem` is also attached to each GitHub Release.
|
|
18
20
|
|
|
19
|
-
|
|
21
|
+
## Prepare a release
|
|
20
22
|
|
|
21
|
-
|
|
23
|
+
Keep notable changes under the appropriate `Added`, `Changed`, `Fixed`, or `Removed` heading in the `[Unreleased]` section of `CHANGELOG.md`. Do not add the version heading by hand.
|
|
22
24
|
|
|
23
|
-
|
|
24
|
-
ruby -Itest test/test_cli.rb
|
|
25
|
-
```
|
|
25
|
+
Before releasing, make sure `main` is clean, pushed, and synchronized with `origin/main`. Then run:
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
```bash
|
|
28
|
+
script/release 0.82.0
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The version can also come from standard input:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
printf '0.82.0\n' | script/release
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The command fails closed unless:
|
|
28
38
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
39
|
+
- The version is a valid RubyGems version newer than `Kward::VERSION`.
|
|
40
|
+
- The working tree is clean and checked out on `main`.
|
|
41
|
+
- Local `main` exactly matches `origin/main`.
|
|
42
|
+
- The tag does not exist locally or on GitHub.
|
|
43
|
+
- RubyGems.org does not already contain the version.
|
|
44
|
+
- `[Unreleased]` contains at least one changelog entry.
|
|
32
45
|
|
|
33
|
-
|
|
46
|
+
If validation succeeds, it:
|
|
34
47
|
|
|
35
|
-
|
|
48
|
+
1. Updates `Kward::VERSION` in `lib/kward/version.rb`.
|
|
49
|
+
2. Refreshes `Gemfile.lock` with `bundle lock --local`.
|
|
50
|
+
3. Moves the unreleased changelog entries under a dated version heading.
|
|
51
|
+
4. Runs `bundle exec rake release:preflight`.
|
|
52
|
+
5. Commits the three release files as `Release v0.82.0`.
|
|
53
|
+
6. Creates an annotated `v0.82.0` tag.
|
|
54
|
+
7. Atomically pushes `main` and the tag to `origin`.
|
|
36
55
|
|
|
37
|
-
|
|
38
|
-
bundle exec rake docs:build
|
|
39
|
-
bundle exec rake docs:check
|
|
40
|
-
```
|
|
56
|
+
If preparation fails before the commit, the command restores the version, lockfile, and changelog. If tagging or pushing fails after the commit, it leaves the release commit in place and prints the failed command; inspect the repository before retrying rather than creating another release commit.
|
|
41
57
|
|
|
42
|
-
|
|
58
|
+
## What the release workflow does
|
|
43
59
|
|
|
44
|
-
|
|
60
|
+
A pushed `v*` tag starts `.github/workflows/release.yml`. The workflow:
|
|
45
61
|
|
|
46
|
-
|
|
62
|
+
1. Checks that the tag, gem version, and changelog heading agree.
|
|
63
|
+
2. Runs the full test suite and generated-documentation checks against Ruby 3.4.
|
|
64
|
+
3. Builds the gem and verifies its packaged files.
|
|
65
|
+
4. Publishes through RubyGems trusted publishing.
|
|
66
|
+
5. Verifies the local gem checksum against the artifact served by RubyGems.org.
|
|
67
|
+
6. Creates `Kward VERSION` as a GitHub Release using that version's changelog section and attaches the verified gem.
|
|
47
68
|
|
|
48
|
-
|
|
69
|
+
The publishing job uses the protected `release` environment. If RubyGems.org already has the version after a partially completed workflow, a rerun rebuilds the gem and verifies that it exactly matches the published checksum before continuing. For an existing GitHub Release, the workflow downloads and compares the gem, uploads it when missing, and publishes an unfinished draft. This makes normal workflow reruns safe without silently replacing mismatched artifacts.
|
|
49
70
|
|
|
50
|
-
|
|
71
|
+
Follow the run from the repository's **Actions → Release** page. Installation can be checked after publication with:
|
|
51
72
|
|
|
52
73
|
```bash
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
git push && git push --tags
|
|
74
|
+
gem install kward --version 0.82.0
|
|
75
|
+
kward --version
|
|
56
76
|
```
|
|
57
77
|
|
|
58
|
-
|
|
78
|
+
## Run checks without releasing
|
|
79
|
+
|
|
80
|
+
After updating to an unreleased version, run the complete preflight directly with:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
bundle exec rake release:preflight
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
This verifies release metadata, runs tests, builds and checks the generated documentation, builds `pkg/kward-VERSION.gem`, rejects development-only packaged files, and prints the final gem contents.
|
|
87
|
+
|
|
88
|
+
Individual checks remain available:
|
|
59
89
|
|
|
60
90
|
```bash
|
|
61
|
-
|
|
91
|
+
bundle exec rake test
|
|
92
|
+
bundle exec rake docs:check
|
|
93
|
+
bundle exec rake release:verify
|
|
94
|
+
bundle exec rake build
|
|
62
95
|
```
|
|
63
96
|
|
|
64
|
-
|
|
97
|
+
Use `bundle exec rake docs:serve` to preview documentation locally.
|
|
65
98
|
|
|
66
99
|
## If you need to yank a release
|
|
67
100
|
|
|
68
|
-
If a published gem has a serious problem,
|
|
101
|
+
If a published gem has a serious problem, yank it within 24 hours of pushing:
|
|
69
102
|
|
|
70
103
|
```bash
|
|
71
104
|
gem yank kward --version VERSION
|
|
72
105
|
```
|
|
73
106
|
|
|
74
|
-
Yanking removes the gem from the default install index but does not delete the version
|
|
107
|
+
Yanking removes the gem from the default install index but does not delete the version. Fix the issue, choose a new version, and run the normal release command again. Never move or reuse a published version tag.
|
data/doc/security.md
CHANGED
|
@@ -58,7 +58,7 @@ These protections reduce accidental edits. They do not contain the whole process
|
|
|
58
58
|
|
|
59
59
|
- With `sandbox.mode: off` (the default), `run_shell_command`, `!command`, `/capture`, `/shell`, and `/pty` run with your user permissions.
|
|
60
60
|
- A non-off [command sandbox](sandboxing.md) restricts only model-requested `run_shell_command` workers and their descendants. It does not cover `!command`, `/capture`, `/shell`, or `/pty`.
|
|
61
|
-
- External `/shell` commands, `!command`, and `/pty`
|
|
61
|
+
- External `/shell` commands, `!command`, and `/pty` receive an interactive PTY. Kward forwards a conservative set of line-oriented controls into the inline region, then grants full-terminal passthrough when a child emits screen-oriented or unknown controls. Full passthrough bypasses transcript control-sequence sanitization, so run only commands you trust with terminal access. `capture <command>` and `/capture <command>` sanitize their captured output.
|
|
62
62
|
- Plugins, command hooks, and MCP servers are local processes with the same general operating-system access.
|
|
63
63
|
- Read-before-edit applies to Kward's file tools, not to arbitrary shell commands or extension code.
|
|
64
64
|
|
data/doc/shell.md
CHANGED
|
@@ -18,11 +18,11 @@ Prefix a command with `!` in the normal composer:
|
|
|
18
18
|
!less README.md
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
The command runs from the active workspace root and
|
|
21
|
+
The command runs from the active workspace root and begins in an inline PTY region above a frozen composer. Line-oriented output, single-line carriage-return progress, and synchronized-output update brackets scroll the transcript area naturally while keyboard input belongs to the child process. If the child emits alternate-screen, clear-screen, absolute cursor, or unknown terminal controls, Kward conservatively hides the composer and switches permanently to full-terminal passthrough for the rest of that command. Pagers and full-screen applications therefore retain the complete terminal without relying on a command-name allowlist.
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
When an inline command exits without reading input, safe output is mirrored into the transient transcript view so a repaint cannot hide it. Carriage-return and horizontal-cursor progress redraws are reduced to their final visible lines, while an unterminated synchronized-output update is closed before Kward redraws. If the child reads input, Kward retains only output captured before the first forwarded input byte; this prevents echoed passwords, OTPs, or other input from entering tab state. Shell output is never added to the AI conversation or sent to the model.
|
|
24
24
|
|
|
25
|
-
Shell output can leave transient text in the transcript area. **After the command finishes, press Ctrl+L to redraw the durable conversation and clear that transient `!command` output.** While an interactive command is still running, keyboard input—including Ctrl+L and Kward's tab shortcuts—belongs to the child process.
|
|
25
|
+
Shell output can leave transient text in the transcript area. **After the command finishes, press Ctrl+L to redraw the durable conversation and clear that transient `!command` output.** While an interactive command is still running, the composer remains frozen and keyboard input—including Ctrl+L and Kward's tab shortcuts—belongs to the child process.
|
|
26
26
|
|
|
27
27
|
Configured `ekwsh.yml` aliases also work after `!`:
|
|
28
28
|
|
data/kward.gemspec
CHANGED
|
@@ -20,7 +20,7 @@ Gem::Specification.new do |spec|
|
|
|
20
20
|
|
|
21
21
|
spec.files = Dir.chdir(__dir__) do
|
|
22
22
|
`git ls-files -z`.split("\x0").reject do |file|
|
|
23
|
-
file.start_with?(".ruby-lsp/", "test/", "plan/") || [".gitignore", "AGENTS.md"].include?(file)
|
|
23
|
+
file.start_with?(".github/", ".ruby-lsp/", "script/", "test/", "plan/") || [".gitignore", "AGENTS.md"].include?(file)
|
|
24
24
|
end
|
|
25
25
|
end
|
|
26
26
|
spec.bindir = "exe"
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
require_relative "terminal_sequences"
|
|
2
|
+
|
|
3
|
+
# Namespace for the Kward CLI agent runtime.
|
|
4
|
+
module Kward
|
|
5
|
+
# Streams a conservative subset of PTY output into Kward's inline terminal
|
|
6
|
+
# region and permanently switches to exclusive passthrough before forwarding
|
|
7
|
+
# screen-oriented or unknown terminal controls.
|
|
8
|
+
class AdaptivePtyOutputSink
|
|
9
|
+
MAX_SEQUENCE_BYTES = 4096
|
|
10
|
+
SAFE_CONTROLS = [0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d].freeze
|
|
11
|
+
TERMINAL_STRING_INTRODUCERS = [0x5d, 0x50, 0x5e, 0x5f, 0x58].freeze
|
|
12
|
+
|
|
13
|
+
attr_reader :captured_output
|
|
14
|
+
|
|
15
|
+
def initialize(output:, on_exclusive:, max_capture_bytes: nil)
|
|
16
|
+
@output = output
|
|
17
|
+
@on_exclusive = on_exclusive
|
|
18
|
+
@max_capture_bytes = max_capture_bytes
|
|
19
|
+
@captured_output = max_capture_bytes ? +"".b : nil
|
|
20
|
+
@capture_open = true
|
|
21
|
+
@truncated = false
|
|
22
|
+
@mode = :inline
|
|
23
|
+
@sequence = +"".b
|
|
24
|
+
@synchronized_output = false
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def write(chunk)
|
|
28
|
+
value = chunk.to_s.b
|
|
29
|
+
capture(value)
|
|
30
|
+
return @output.write(value) if exclusive?
|
|
31
|
+
|
|
32
|
+
write_inline(value)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def flush
|
|
36
|
+
@output.flush if @output.respond_to?(:flush)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def finish
|
|
40
|
+
switch_to_exclusive(@sequence) unless @sequence.empty? || exclusive?
|
|
41
|
+
end_synchronized_output if inline?
|
|
42
|
+
flush
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def input_forwarded
|
|
46
|
+
@capture_open = false
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def inline?
|
|
50
|
+
@mode == :inline
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def transcript_safe?
|
|
54
|
+
inline? && @sequence.empty?
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def pre_input_capture_only?
|
|
58
|
+
true
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def truncated?
|
|
62
|
+
@truncated
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
def exclusive?
|
|
68
|
+
@mode == :exclusive
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def write_inline(value)
|
|
72
|
+
safe_output = +"".b
|
|
73
|
+
index = 0
|
|
74
|
+
while index < value.bytesize
|
|
75
|
+
byte = value.getbyte(index)
|
|
76
|
+
if @sequence.empty?
|
|
77
|
+
if byte == 0x1b
|
|
78
|
+
@sequence << byte
|
|
79
|
+
elsif byte >= 0x20 || SAFE_CONTROLS.include?(byte)
|
|
80
|
+
safe_output << byte
|
|
81
|
+
else
|
|
82
|
+
flush_safe_output(safe_output)
|
|
83
|
+
switch_to_exclusive(value.byteslice(index..))
|
|
84
|
+
return
|
|
85
|
+
end
|
|
86
|
+
else
|
|
87
|
+
@sequence << byte
|
|
88
|
+
status = sequence_status
|
|
89
|
+
if status == :safe
|
|
90
|
+
track_safe_sequence(@sequence)
|
|
91
|
+
safe_output << @sequence
|
|
92
|
+
@sequence.clear
|
|
93
|
+
elsif status == :exclusive
|
|
94
|
+
flush_safe_output(safe_output)
|
|
95
|
+
remainder = value.byteslice((index + 1)..).to_s.b
|
|
96
|
+
switch_to_exclusive(@sequence + remainder)
|
|
97
|
+
@sequence.clear
|
|
98
|
+
return
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
index += 1
|
|
102
|
+
end
|
|
103
|
+
flush_safe_output(safe_output)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def sequence_status
|
|
107
|
+
return :exclusive if @sequence.bytesize > MAX_SEQUENCE_BYTES
|
|
108
|
+
return :pending if @sequence.bytesize == 1
|
|
109
|
+
|
|
110
|
+
second = @sequence.getbyte(1)
|
|
111
|
+
return csi_status if second == "[".ord
|
|
112
|
+
return :exclusive if TERMINAL_STRING_INTRODUCERS.include?(second)
|
|
113
|
+
return escape_intermediate_status if second.between?(0x20, 0x2f)
|
|
114
|
+
|
|
115
|
+
:exclusive
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def csi_status
|
|
119
|
+
return :pending if @sequence.bytesize == 2
|
|
120
|
+
|
|
121
|
+
byte = @sequence.getbyte(-1)
|
|
122
|
+
return safe_csi? ? :safe : :exclusive if byte.between?(0x40, 0x7e)
|
|
123
|
+
return :pending if byte.between?(0x20, 0x3f)
|
|
124
|
+
|
|
125
|
+
:exclusive
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def escape_intermediate_status
|
|
129
|
+
byte = @sequence.getbyte(-1)
|
|
130
|
+
return :pending if byte.between?(0x20, 0x2f)
|
|
131
|
+
|
|
132
|
+
:exclusive
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def safe_csi?
|
|
136
|
+
value = @sequence
|
|
137
|
+
value.match?(/\A\e\[[0-9:;]*m\z/) ||
|
|
138
|
+
value.match?(/\A\e\[[0-2]?K\z/) ||
|
|
139
|
+
value.match?(/\A\e\[[0-9;]*[CDG`]\z/) ||
|
|
140
|
+
value.match?(/\A\e\[\?25[hl]\z/) ||
|
|
141
|
+
value.match?(/\A\e\[\?(?:2004|2026)[hl]\z/) ||
|
|
142
|
+
value.match?(/\A\e\[[0-9;]* q\z/)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def track_safe_sequence(sequence)
|
|
146
|
+
@synchronized_output = true if sequence == TerminalSequences::SYNCHRONIZED_OUTPUT_ENABLE
|
|
147
|
+
@synchronized_output = false if sequence == TerminalSequences::SYNCHRONIZED_OUTPUT_DISABLE
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def end_synchronized_output
|
|
151
|
+
return unless @synchronized_output
|
|
152
|
+
|
|
153
|
+
@output.write(TerminalSequences::SYNCHRONIZED_OUTPUT_DISABLE)
|
|
154
|
+
@synchronized_output = false
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def flush_safe_output(value)
|
|
158
|
+
return if value.empty?
|
|
159
|
+
|
|
160
|
+
@output.write(value)
|
|
161
|
+
value.clear
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def switch_to_exclusive(value)
|
|
165
|
+
end_synchronized_output
|
|
166
|
+
@on_exclusive.call
|
|
167
|
+
@mode = :exclusive
|
|
168
|
+
@output.write(value) unless value.empty?
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def capture(value)
|
|
172
|
+
return unless @captured_output && @capture_open
|
|
173
|
+
|
|
174
|
+
remaining = @max_capture_bytes - @captured_output.bytesize
|
|
175
|
+
if value.bytesize > remaining
|
|
176
|
+
@captured_output << value.byteslice(0, remaining) if remaining.positive?
|
|
177
|
+
@truncated = true
|
|
178
|
+
else
|
|
179
|
+
@captured_output << value
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
data/lib/kward/cli/git.rb
CHANGED
|
@@ -30,12 +30,16 @@ module Kward
|
|
|
30
30
|
status = result.is_a?(Hash) && result.key?(:status_lines) ? result[:status_lines] : result
|
|
31
31
|
result
|
|
32
32
|
end
|
|
33
|
-
|
|
33
|
+
if message.nil?
|
|
34
|
+
refresh_composer_status
|
|
35
|
+
return
|
|
36
|
+
end
|
|
34
37
|
|
|
35
38
|
result = run_busy_local_command_and_requeue(activity: "committing") do
|
|
36
39
|
git_commit(git_root, message)
|
|
37
40
|
end
|
|
38
41
|
print_git_commit_result(result)
|
|
42
|
+
refresh_composer_status
|
|
39
43
|
ensure
|
|
40
44
|
@git_hook_conversation = previous_git_hook_conversation
|
|
41
45
|
end
|
data/lib/kward/cli/rendering.rb
CHANGED
|
@@ -12,7 +12,12 @@ module Kward
|
|
|
12
12
|
|
|
13
13
|
def redraw_interactive_prompt
|
|
14
14
|
tab = active_tab if respond_to?(:active_tab, true)
|
|
15
|
-
if tab && (
|
|
15
|
+
clear_active_tab_transient_shell_output if tab && respond_to?(:clear_active_tab_transient_shell_output, true)
|
|
16
|
+
if tab&.shell
|
|
17
|
+
render_tab(tab, restore_composer: false)
|
|
18
|
+
return
|
|
19
|
+
end
|
|
20
|
+
if tab&.running?
|
|
16
21
|
@prompt.redraw if @prompt.respond_to?(:redraw)
|
|
17
22
|
return
|
|
18
23
|
end
|
|
@@ -25,6 +30,7 @@ module Kward
|
|
|
25
30
|
|
|
26
31
|
restore_prompt_transcript do
|
|
27
32
|
tab ? render_transcript_messages(tab.driver.messages) : render_conversation_transcript(conversation)
|
|
33
|
+
render_tab_transient_shell_entries(tab) if tab && respond_to?(:render_tab_transient_shell_entries, true)
|
|
28
34
|
end
|
|
29
35
|
end
|
|
30
36
|
|
|
@@ -283,8 +289,14 @@ module Kward
|
|
|
283
289
|
# Writes the pasted images output for the terminal CLI flow.
|
|
284
290
|
def print_pasted_images(input, image_parts: nil)
|
|
285
291
|
parts = image_parts || Kward::ImageAttachments.image_parts_from_text(input)
|
|
292
|
+
protocol = @prompt.inline_image_protocol if @prompt.respond_to?(:inline_image_protocol)
|
|
286
293
|
parts.each do |part|
|
|
287
|
-
|
|
294
|
+
next if @prompt.respond_to?(:inline_image_protocol) && protocol.nil?
|
|
295
|
+
|
|
296
|
+
part = Kward::ImageAttachments.terminal_image_part(part, protocol) if protocol
|
|
297
|
+
next if protocol && part.nil?
|
|
298
|
+
|
|
299
|
+
sequence = Kward::ImageAttachments.terminal_image_sequence(part, protocol: protocol)
|
|
288
300
|
next unless sequence
|
|
289
301
|
|
|
290
302
|
if @prompt.respond_to?(:say_visual)
|