kward 0.76.0 → 0.78.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/.github/workflows/ci.yml +8 -1
- data/CHANGELOG.md +62 -0
- data/Gemfile.lock +2 -2
- data/README.md +13 -4
- data/doc/agent-tools.md +0 -3
- data/doc/api.md +4 -0
- data/doc/code-search.md +9 -6
- data/doc/composer.md +219 -0
- data/doc/configuration.md +114 -27
- data/doc/editor.md +18 -2
- data/doc/files.md +1 -1
- data/doc/getting-started.md +4 -0
- data/doc/git.md +2 -2
- data/doc/lifecycle-hooks.md +2 -14
- data/doc/pan.md +167 -0
- data/doc/permissions.md +179 -0
- data/doc/plugins.md +58 -0
- data/doc/rpc.md +61 -22
- data/doc/security.md +184 -0
- data/doc/tabs.md +4 -3
- data/doc/usage.md +15 -32
- data/doc/web-search.md +4 -2
- data/lib/kward/ansi.rb +2 -2
- data/lib/kward/auth/anthropic_oauth.rb +2 -2
- data/lib/kward/auth/github_oauth.rb +3 -3
- data/lib/kward/auth/oauth_helpers.rb +4 -2
- data/lib/kward/auth/openai_oauth.rb +2 -1
- data/lib/kward/cancellation.rb +2 -2
- data/lib/kward/cli/commands.rb +1 -3
- data/lib/kward/cli/interactive_turn.rb +0 -17
- data/lib/kward/cli/plugins.rb +24 -7
- data/lib/kward/cli/prompt_interface.rb +1 -0
- data/lib/kward/cli/rendering.rb +7 -1
- data/lib/kward/cli/runtime_helpers.rb +11 -35
- data/lib/kward/cli/sessions.rb +14 -5
- data/lib/kward/cli/settings.rb +22 -0
- data/lib/kward/cli/slash_commands.rb +1 -561
- data/lib/kward/cli/tabs.rb +154 -36
- data/lib/kward/cli.rb +30 -26
- data/lib/kward/compactor.rb +3 -3
- data/lib/kward/config_files.rb +28 -0
- data/lib/kward/ekwsh.rb +0 -4
- data/lib/kward/hooks/catalog.rb +0 -5
- data/lib/kward/hooks/http_handler.rb +2 -1
- data/lib/kward/http.rb +18 -0
- data/lib/kward/image_attachments.rb +1 -1
- data/lib/kward/model/client.rb +33 -11
- data/lib/kward/model/payloads.rb +6 -1
- data/lib/kward/openrouter_model_cache.rb +2 -1
- data/lib/kward/pan/index.html.erb +459 -64
- data/lib/kward/pan/kward_logo.png +0 -0
- data/lib/kward/pan/server.rb +248 -23
- data/lib/kward/permissions/policy.rb +171 -0
- data/lib/kward/plugin_registry.rb +71 -1
- data/lib/kward/prompt_interface/approval_prompt.rb +62 -0
- data/lib/kward/prompt_interface/editor/auto_indent.rb +31 -23
- data/lib/kward/prompt_interface/editor/modes/vibe.rb +72 -3
- data/lib/kward/prompt_interface/layout.rb +0 -4
- data/lib/kward/prompt_interface/project_browser.rb +40 -1
- data/lib/kward/prompt_interface/question_prompt.rb +12 -3
- data/lib/kward/prompt_interface/runtime_state.rb +4 -0
- data/lib/kward/prompt_interface/selection_prompt.rb +0 -1
- data/lib/kward/prompt_interface/transcript_renderer.rb +0 -12
- data/lib/kward/prompt_interface.rb +20 -1
- data/lib/kward/prompts/commands.rb +1 -3
- data/lib/kward/prompts.rb +20 -0
- data/lib/kward/rpc/plugin_chat_manager.rb +299 -0
- data/lib/kward/rpc/server.rb +48 -41
- data/lib/kward/rpc/session_manager.rb +76 -25
- data/lib/kward/rpc/tool_event_normalizer.rb +2 -0
- data/lib/kward/rpc/tool_metadata.rb +23 -0
- data/lib/kward/rpc/transcript_normalizer.rb +18 -5
- data/lib/kward/session_store.rb +55 -16
- data/lib/kward/skills/registry.rb +21 -1
- data/lib/kward/starter_pack_installer.rb +3 -1
- data/lib/kward/tab_driver.rb +87 -0
- data/lib/kward/tab_store.rb +74 -12
- data/lib/kward/tools/base.rb +14 -0
- data/lib/kward/tools/code_search.rb +8 -2
- data/lib/kward/tools/fetch_content.rb +4 -2
- data/lib/kward/tools/fetch_raw.rb +3 -1
- data/lib/kward/tools/registry.rb +45 -17
- data/lib/kward/tools/search/code.rb +47 -13
- data/lib/kward/tools/search/web.rb +61 -18
- data/lib/kward/tools/search/web_fetch.rb +206 -38
- data/lib/kward/tools/tool_call.rb +0 -4
- data/lib/kward/tools/web_search.rb +3 -1
- data/lib/kward/transcript_export.rb +1 -1
- data/lib/kward/update_check.rb +2 -1
- data/lib/kward/version.rb +1 -1
- data/templates/default/kward_navigation.rb +7 -2
- data/templates/default/layout/html/layout.erb +2 -2
- data/templates/default/layout/html/setup.rb +1 -0
- metadata +11 -12
- data/lib/kward/workers/git_guard.rb +0 -93
- data/lib/kward/workers/job.rb +0 -99
- data/lib/kward/workers/live_view.rb +0 -49
- data/lib/kward/workers/manager.rb +0 -328
- data/lib/kward/workers/queue_runner.rb +0 -166
- data/lib/kward/workers/queue_store.rb +0 -112
- data/lib/kward/workers/store.rb +0 -72
- data/lib/kward/workers/tool_policy.rb +0 -23
- data/lib/kward/workers/worker.rb +0 -82
- data/lib/kward/workers/write_lock.rb +0 -38
- data/lib/kward/workers.rb +0 -10
data/doc/usage.md
CHANGED
|
@@ -78,7 +78,7 @@ For several commands, enter the embedded Kward shell:
|
|
|
78
78
|
/shell
|
|
79
79
|
```
|
|
80
80
|
|
|
81
|
-
`/shell` opens `ekwsh`, a Kward-native command mode
|
|
81
|
+
`/shell` opens `ekwsh`, a Kward-native command mode that keeps the tab bar and transcript visible. It preserves state such as the current directory, environment variables, and aliases between commands. Use `pty git log` or `/pty git log` when you intentionally want to hand the terminal to an interactive tool such as `less` or `vim`. See [Embedded shell](shell.md) for built-ins, completion, configuration, ANSI handling, PTY passthrough, and limitations.
|
|
82
82
|
|
|
83
83
|
## Shell commands
|
|
84
84
|
|
|
@@ -113,7 +113,7 @@ kward --skip-config edit ~/.kward/config.json
|
|
|
113
113
|
|
|
114
114
|
## Interactive slash commands
|
|
115
115
|
|
|
116
|
-
Slash commands run local actions in the current session. Most do not send a prompt to the model; exceptions like `/git`
|
|
116
|
+
Slash commands run local actions in the current session. Most do not send a prompt to the model; exceptions like `/git` orchestrate local flows that may then trigger model work.
|
|
117
117
|
|
|
118
118
|
| Command | Use it when you want to... |
|
|
119
119
|
| --- | --- |
|
|
@@ -125,7 +125,7 @@ Slash commands run local actions in the current session. Most do not send a prom
|
|
|
125
125
|
| `/files` | browse project files in a nested tree and open them in the editor. |
|
|
126
126
|
| `/shell` | run workspace commands in the embedded Kward shell. |
|
|
127
127
|
| `/pty <command>` | hand the terminal to an interactive command such as `git log`/`less` or `vim`. |
|
|
128
|
-
| `/settings` | configure
|
|
128
|
+
| `/settings` | configure models, accounts, memory, interface, tools, context, personalization, and logging. |
|
|
129
129
|
| `/status` | see session, model, and context status. |
|
|
130
130
|
| `/new` | start a fresh session in the current tab. |
|
|
131
131
|
| `/tab 2` | switch to tab 2. |
|
|
@@ -140,6 +140,7 @@ Slash commands run local actions in the current session. Most do not send a prom
|
|
|
140
140
|
| `/session name <name>` | name or clear the current session. |
|
|
141
141
|
| `/rename <name>` | rename the current session. |
|
|
142
142
|
| `/clone` | copy the current session into a new branch. |
|
|
143
|
+
| `/fork` | fork from an earlier prompt into a new session. |
|
|
143
144
|
| `/rewind` | revisit an earlier prompt and try a different direction. |
|
|
144
145
|
| `/tree` | inspect and navigate the full technical session tree. |
|
|
145
146
|
| `/copy last` | copy the latest assistant answer. |
|
|
@@ -147,39 +148,21 @@ Slash commands run local actions in the current session. Most do not send a prom
|
|
|
147
148
|
| `/export notes.md` | write the transcript to a Markdown file. |
|
|
148
149
|
| `/compact [instructions]` | summarize older context so a long chat can continue. |
|
|
149
150
|
| `/memory ...` | manage opt-in memory. |
|
|
151
|
+
| `/skill <name>` | activate a configured skill explicitly for the current session. |
|
|
152
|
+
| `/stats [range]` | summarize enabled local telemetry. |
|
|
153
|
+
| `/hooks ...` | inspect, diagnose, trust, or untrust lifecycle hooks. |
|
|
154
|
+
| `/scratchpad [text|markdown|ruby]` | open an unsaved editor buffer. |
|
|
150
155
|
| `/redraw` | fix terminal drawing after resize or glitches. |
|
|
151
156
|
| `/reload` | reload installed plugins. |
|
|
152
|
-
| `/workers` | open the experimental worker pipeline (`new`, `do <task>`, or `list`). |
|
|
153
|
-
| `/queue` | manage the experimental tab-backed worker queue (`add`, `list`, `open <id>`, `run`, `suspend <id>`, or `resume <id>`). |
|
|
154
157
|
| `/exit` | leave Kward. |
|
|
158
|
+
| `/quit` | alias for `/exit`. |
|
|
155
159
|
|
|
156
|
-
Prompt templates and plugins can add more slash commands.
|
|
157
|
-
|
|
158
|
-
### Experimental tab-backed worker queue
|
|
159
|
-
|
|
160
|
-
Start Kward with `--experimental-workers` to try the tab-backed worker queue. The queue is an MVP for turning an existing tab/session into implementation work that can be reviewed later.
|
|
161
|
-
|
|
162
|
-
Typical flow:
|
|
163
|
-
|
|
164
|
-
```text
|
|
165
|
-
/plan Add retry handling to webhook delivery
|
|
166
|
-
/queue add
|
|
167
|
-
/queue run
|
|
168
|
-
/queue open <id>
|
|
169
|
-
```
|
|
170
|
-
|
|
171
|
-
`/queue add` stores the current tab session as a queued worker job. `/queue run` drains queued jobs one at a time. Each job continues its saved session as an implementation worker, starts only from a clean git workspace, commits any resulting changes, and then becomes `ready_for_review`. Use `/queue open <id>` to inspect the worker session, test the feature yourself, and continue the same session if a follow-up fix is needed.
|
|
172
|
-
|
|
173
|
-
Current MVP limitations:
|
|
174
|
-
|
|
175
|
-
- The queue is manual: run it with `/queue run` when you want it to work.
|
|
176
|
-
- Jobs run sequentially and stop when one becomes `blocked` or `failed`.
|
|
177
|
-
- A job will not start if the workspace is dirty; clean, commit, or stash your changes first.
|
|
178
|
-
- `/queue suspend <id>` and `/queue resume <id>` provide explicit stash-based parking primitives, but automatic foreground-tool-triggered yielding is not wired yet.
|
|
179
|
-
- The queue is experimental and stores metadata locally in Kward's config directory.
|
|
160
|
+
Prompt templates, configured skills, and plugins can add more slash commands. Their commands appear in the interactive slash-command picker alongside built-ins.
|
|
180
161
|
|
|
181
162
|
## Prompt history
|
|
182
163
|
|
|
164
|
+
The [Interactive composer](composer.md) guide covers multiline editing, slash and file completion, reasoning shortcuts, busy input, cancellation, attachments, and terminal compatibility.
|
|
165
|
+
|
|
183
166
|
In interactive mode, Kward keeps prompt history per workspace under `~/.kward/history/`. Press Up/Down to recall previous prompts across restarts.
|
|
184
167
|
|
|
185
168
|
Press `Ctrl-R` to search history. Type a fuzzy query in the composer, use Up/Down to choose a result from the overlay, then press Enter to place it back in the composer for editing or resubmission. Press Esc or Ctrl-C to cancel the search and restore the draft.
|
|
@@ -260,7 +243,7 @@ Important guardrails:
|
|
|
260
243
|
|
|
261
244
|
## Images
|
|
262
245
|
|
|
263
|
-
If the active model supports images, Kward can attach image paths, Markdown image links, `file://` URLs, or image data URLs pasted into the composer. Supported formats are GIF, JPEG, PNG, and WebP, up to 20 MB per image.
|
|
246
|
+
If the active model supports images, Kward can attach image paths, Markdown image links, `file://` URLs, or image data URLs pasted into the composer. Supported formats are GIF, JPEG, PNG, and WebP, up to 20 MB per image. See [Interactive composer](composer.md#Attach_images) for attachment badges, removal, and terminal previews.
|
|
264
247
|
|
|
265
248
|
Use this for tasks such as:
|
|
266
249
|
|
|
@@ -270,13 +253,13 @@ This screenshot shows the broken layout. Find the likely CSS issue.
|
|
|
270
253
|
|
|
271
254
|
## Pan mode
|
|
272
255
|
|
|
273
|
-
Pan mode starts a
|
|
256
|
+
Pan mode starts a mobile-friendly LAN web UI:
|
|
274
257
|
|
|
275
258
|
```bash
|
|
276
259
|
kward --working-directory ~/code/project pan
|
|
277
260
|
```
|
|
278
261
|
|
|
279
|
-
Use it only on trusted networks. It exposes
|
|
262
|
+
Use it only on trusted networks. It exposes file, shell, web, and configured extension tools through a browser UI and requires credentials configured in `config.json`. Pan saves conversations to the normal workspace session store; its session drawer can create, resume, rename, and delete sessions. Session changes are disabled while turns are active or queued. See [Pan mode](pan.md) for setup, browser workflows, security, and limitations.
|
|
280
263
|
|
|
281
264
|
## RPC backend
|
|
282
265
|
|
data/doc/web-search.md
CHANGED
|
@@ -85,7 +85,9 @@ Arguments:
|
|
|
85
85
|
- `max_bytes`: default 16384, capped at 131072.
|
|
86
86
|
- `extract`: optional `auto`, `text`, or `markdown`.
|
|
87
87
|
|
|
88
|
-
In `auto` mode (default), Kward detects HTML content and extracts readable text
|
|
88
|
+
In `auto` mode (default), Kward detects HTML content and extracts readable text while stripping scripts, styles, navigation prose, and forms. It preserves headings, paragraphs, ordered and unordered lists, code blocks, blockquotes, simple tables, and inline link destinations. A bounded `Links` section lists discovered main-content and navigation destinations so an agent can continue through a site without raw HTML. Relative links are resolved against the final fetched URL. Non-HTML content is returned as cleaned text. Use `markdown` to retain Markdown structure, or `text` for plain text with link destinations in parentheses.
|
|
89
|
+
|
|
90
|
+
Kward downloads and parses up to 2 MB of a page before applying `max_bytes` to the extracted result. This lets pages with large scripts or metadata before their main content remain readable while keeping model-facing output bounded. Responses above the download limit are rejected rather than parsed as incomplete HTML.
|
|
89
91
|
|
|
90
92
|
Fetches follow up to 5 redirects and use a 10-second HTTP timeout.
|
|
91
93
|
|
|
@@ -99,4 +101,4 @@ Arguments:
|
|
|
99
101
|
- `max_bytes`: default 16384, capped at 131072.
|
|
100
102
|
- `accept`: optional HTTP `Accept` header.
|
|
101
103
|
|
|
102
|
-
Fetches follow up to 5 redirects and use a 10-second HTTP timeout.
|
|
104
|
+
Fetches follow up to 5 redirects, enforce the requested byte limit while reading the response, and use a 10-second HTTP timeout.
|
data/lib/kward/ansi.rb
CHANGED
|
@@ -262,8 +262,8 @@ module Kward
|
|
|
262
262
|
end
|
|
263
263
|
|
|
264
264
|
def inline_links(text, enabled: enabled?)
|
|
265
|
-
text.split(/(\[[^\
|
|
266
|
-
if (match = part.match(/\A\[([^\
|
|
265
|
+
text.split(/(\[[^\n\]]+\]\([^)\s]+\))/).map do |part|
|
|
266
|
+
if (match = part.match(/\A\[([^\n\]]+)\]\(([^)\s]+)\)\z/))
|
|
267
267
|
"#{colorize(match[1], :cyan, enabled: enabled)} (#{colorize(match[2], :dim, enabled: enabled)})"
|
|
268
268
|
else
|
|
269
269
|
inline_emphasis(part, enabled: enabled)
|
|
@@ -202,8 +202,8 @@ module Kward
|
|
|
202
202
|
parse_successful_json(response, "Anthropic OAuth token exchange")
|
|
203
203
|
end
|
|
204
204
|
|
|
205
|
-
def post_json(uri, params)
|
|
206
|
-
super(uri, params, headers: { "Accept" => "application/json" })
|
|
205
|
+
def post_json(uri, params = nil, **keyword_params)
|
|
206
|
+
super(uri, params, headers: { "Accept" => "application/json" }, **keyword_params)
|
|
207
207
|
end
|
|
208
208
|
end
|
|
209
209
|
end
|
|
@@ -4,6 +4,7 @@ require "time"
|
|
|
4
4
|
require "uri"
|
|
5
5
|
require_relative "file"
|
|
6
6
|
require_relative "../config_files"
|
|
7
|
+
require_relative "../http"
|
|
7
8
|
|
|
8
9
|
# Namespace for the Kward CLI agent runtime.
|
|
9
10
|
module Kward
|
|
@@ -15,7 +16,6 @@ module Kward
|
|
|
15
16
|
DEFAULT_SCOPE = "read:user"
|
|
16
17
|
DEFAULT_CLIENT_ID = "Iv1.b507a08c87ecfe98"
|
|
17
18
|
COPILOT_HEADERS = {
|
|
18
|
-
"User-Agent" => "GitHubCopilotChat/0.35.0",
|
|
19
19
|
"Editor-Version" => "vscode/1.107.0",
|
|
20
20
|
"Editor-Plugin-Version" => "copilot-chat/0.35.0",
|
|
21
21
|
"Copilot-Integration-Id" => "vscode-chat"
|
|
@@ -150,7 +150,7 @@ module Kward
|
|
|
150
150
|
end
|
|
151
151
|
|
|
152
152
|
def post_form(uri, params)
|
|
153
|
-
request = Net::HTTP::Post.new(uri)
|
|
153
|
+
request = Http.apply_user_agent(Net::HTTP::Post.new(uri))
|
|
154
154
|
request["Content-Type"] = "application/x-www-form-urlencoded"
|
|
155
155
|
request["Accept"] = "application/json"
|
|
156
156
|
request.body = URI.encode_www_form(params)
|
|
@@ -160,7 +160,7 @@ module Kward
|
|
|
160
160
|
end
|
|
161
161
|
|
|
162
162
|
def get_json(uri, headers = {})
|
|
163
|
-
request = Net::HTTP::Get.new(uri)
|
|
163
|
+
request = Http.apply_user_agent(Net::HTTP::Get.new(uri))
|
|
164
164
|
request["Accept"] = "application/json"
|
|
165
165
|
COPILOT_HEADERS.merge(headers).each { |key, value| request[key] = value }
|
|
166
166
|
|
|
@@ -6,6 +6,7 @@ require "securerandom"
|
|
|
6
6
|
require "socket"
|
|
7
7
|
require "time"
|
|
8
8
|
require "uri"
|
|
9
|
+
require_relative "../http"
|
|
9
10
|
|
|
10
11
|
# Namespace for the Kward CLI agent runtime.
|
|
11
12
|
module Kward
|
|
@@ -58,8 +59,9 @@ module Kward
|
|
|
58
59
|
socket&.close
|
|
59
60
|
end
|
|
60
61
|
|
|
61
|
-
def post_json(uri, params, headers: {})
|
|
62
|
-
|
|
62
|
+
def post_json(uri, params = nil, headers: {}, **keyword_params)
|
|
63
|
+
params = (params || {}).merge(keyword_params)
|
|
64
|
+
request = Http.apply_user_agent(Net::HTTP::Post.new(uri))
|
|
63
65
|
request["Content-Type"] = "application/json"
|
|
64
66
|
headers.each { |key, value| request[key] = value }
|
|
65
67
|
request.body = JSON.dump(params)
|
|
@@ -5,6 +5,7 @@ require "time"
|
|
|
5
5
|
require "uri"
|
|
6
6
|
require_relative "file"
|
|
7
7
|
require_relative "oauth_helpers"
|
|
8
|
+
require_relative "../http"
|
|
8
9
|
|
|
9
10
|
# Namespace for the Kward CLI agent runtime.
|
|
10
11
|
module Kward
|
|
@@ -215,7 +216,7 @@ module Kward
|
|
|
215
216
|
end
|
|
216
217
|
|
|
217
218
|
def post_form(uri, params)
|
|
218
|
-
request = Net::HTTP::Post.new(uri)
|
|
219
|
+
request = Http.apply_user_agent(Net::HTTP::Post.new(uri))
|
|
219
220
|
request["Content-Type"] = "application/x-www-form-urlencoded"
|
|
220
221
|
request.body = URI.encode_www_form(params)
|
|
221
222
|
|
data/lib/kward/cancellation.rb
CHANGED
|
@@ -2,9 +2,9 @@ require "thread"
|
|
|
2
2
|
|
|
3
3
|
# Namespace for the Kward CLI agent runtime.
|
|
4
4
|
module Kward
|
|
5
|
-
# Cooperative cancellation token shared by model calls, tools, and
|
|
5
|
+
# Cooperative cancellation token shared by model calls, tools, and background operations.
|
|
6
6
|
class Cancellation
|
|
7
|
-
# Cooperative cancellation token shared by model calls, tools, and
|
|
7
|
+
# Cooperative cancellation token shared by model calls, tools, and background operations.
|
|
8
8
|
class CancelledError < StandardError; end
|
|
9
9
|
|
|
10
10
|
def initialize
|
data/lib/kward/cli/commands.rb
CHANGED
|
@@ -156,7 +156,7 @@ module Kward
|
|
|
156
156
|
},
|
|
157
157
|
"pan" => {
|
|
158
158
|
usage: "kward pan",
|
|
159
|
-
description: "Start Pan mode, a
|
|
159
|
+
description: "Start Pan mode, a mobile-friendly LAN web UI with persistent sessions.",
|
|
160
160
|
examples: ["kward pan", "kward --working-directory ~/code/project pan"]
|
|
161
161
|
},
|
|
162
162
|
"rpc" => {
|
|
@@ -220,8 +220,6 @@ module Kward
|
|
|
220
220
|
@prompt_delimited = true
|
|
221
221
|
remaining.concat(arguments[(index + 1)..] || [])
|
|
222
222
|
break
|
|
223
|
-
when "--experimental-workers"
|
|
224
|
-
@experimental_workers = true
|
|
225
223
|
when "--skip-config"
|
|
226
224
|
@skip_config = true
|
|
227
225
|
when "--filter"
|
|
@@ -7,7 +7,6 @@ module Kward
|
|
|
7
7
|
private
|
|
8
8
|
|
|
9
9
|
def run_interactive_turn(agent, input, display_input: nil)
|
|
10
|
-
stop_live_worker_view if respond_to?(:stop_live_worker_view, true)
|
|
11
10
|
prepare_memory_context(agent.conversation, input) if agent.respond_to?(:conversation)
|
|
12
11
|
print_user_transcript(input, display_input: display_input) if prompt_interface?
|
|
13
12
|
return run_blocking_interactive_turn(agent, input, display_input: display_input) unless prompt_interface?
|
|
@@ -235,22 +234,6 @@ module Kward
|
|
|
235
234
|
["/exit", "/quit", "/new"].include?(input.to_s.strip)
|
|
236
235
|
end
|
|
237
236
|
|
|
238
|
-
def handle_busy_worker_input(input, agent, queued_inputs)
|
|
239
|
-
return false unless agent
|
|
240
|
-
|
|
241
|
-
command = input.to_s.strip
|
|
242
|
-
return false unless command == "/workers" || command.start_with?("/workers ")
|
|
243
|
-
|
|
244
|
-
_handled, replacement_agent = handle_local_slash_command(command, agent, @session_store)
|
|
245
|
-
@busy_replacement_agent = replacement_agent if replacement_agent?(replacement_agent)
|
|
246
|
-
restore_busy_input_prompt
|
|
247
|
-
true
|
|
248
|
-
rescue StandardError => e
|
|
249
|
-
runtime_output("Error: #{e.message}")
|
|
250
|
-
restore_busy_input_prompt
|
|
251
|
-
true
|
|
252
|
-
end
|
|
253
|
-
|
|
254
237
|
def replacement_agent?(object)
|
|
255
238
|
object.respond_to?(:conversation) && object.respond_to?(:ask)
|
|
256
239
|
end
|
data/lib/kward/cli/plugins.rb
CHANGED
|
@@ -32,6 +32,7 @@ module Kward
|
|
|
32
32
|
|
|
33
33
|
def reload_plugins(conversation)
|
|
34
34
|
@plugin_registry = PluginRegistry.load(reserved_commands: reserved_slash_command_names)
|
|
35
|
+
@prompt.update_slash_commands(slash_command_entries) if @prompt.respond_to?(:update_slash_commands)
|
|
35
36
|
conversation.plugin_registry = @plugin_registry if conversation.respond_to?(:plugin_registry=)
|
|
36
37
|
conversation.refresh_system_message! if conversation.respond_to?(:refresh_system_message!)
|
|
37
38
|
runtime_output("Plugins reloaded.")
|
|
@@ -101,6 +102,17 @@ module Kward
|
|
|
101
102
|
end
|
|
102
103
|
end
|
|
103
104
|
|
|
105
|
+
def update_plugin_tab_slash_commands(tab)
|
|
106
|
+
return unless @prompt.respond_to?(:update_slash_commands)
|
|
107
|
+
|
|
108
|
+
entries = if tab&.driver&.respond_to?(:slash_command_entries)
|
|
109
|
+
tab.driver.slash_command_entries
|
|
110
|
+
else
|
|
111
|
+
[]
|
|
112
|
+
end
|
|
113
|
+
@prompt.update_slash_commands(slash_command_entries + Array(entries))
|
|
114
|
+
end
|
|
115
|
+
|
|
104
116
|
def slash_command_entries
|
|
105
117
|
prompt_entries = prompt_templates.map do |template|
|
|
106
118
|
{
|
|
@@ -126,19 +138,13 @@ module Kward
|
|
|
126
138
|
end
|
|
127
139
|
|
|
128
140
|
def builtin_slash_commands
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
BUILTIN_SLASH_COMMANDS.reject { |command| %w[workers queue].include?(command[:name]) }
|
|
141
|
+
BUILTIN_SLASH_COMMANDS
|
|
132
142
|
end
|
|
133
143
|
|
|
134
144
|
def builtin_slash_command_names
|
|
135
145
|
builtin_slash_commands.map { |command| command[:name] }
|
|
136
146
|
end
|
|
137
147
|
|
|
138
|
-
def experimental_workers_enabled?
|
|
139
|
-
@experimental_workers == true
|
|
140
|
-
end
|
|
141
|
-
|
|
142
148
|
def expand_prompt_template(input)
|
|
143
149
|
PromptCommands.expand(input, templates: prompt_templates, reserved_commands: builtin_slash_command_names)
|
|
144
150
|
end
|
|
@@ -197,6 +203,17 @@ module Kward
|
|
|
197
203
|
plugin_registry.notify_transcript_event(event, plugin_context(conversation, ""))
|
|
198
204
|
end
|
|
199
205
|
|
|
206
|
+
def notify_plugin_tab_transcript_event(event, driver)
|
|
207
|
+
return if plugin_registry.transcript_event_handlers.empty?
|
|
208
|
+
|
|
209
|
+
context = PluginRegistry::Context.new(
|
|
210
|
+
conversation: driver,
|
|
211
|
+
workspace_root: current_workspace_root,
|
|
212
|
+
say_callback: lambda { |message| runtime_output(message) }
|
|
213
|
+
)
|
|
214
|
+
plugin_registry.notify_transcript_event(event, context)
|
|
215
|
+
end
|
|
216
|
+
|
|
200
217
|
end
|
|
201
218
|
end
|
|
202
219
|
end
|
|
@@ -18,6 +18,7 @@ module Kward
|
|
|
18
18
|
@prompt = prompt_interface.new(
|
|
19
19
|
slash_commands: slash_command_entries,
|
|
20
20
|
overlay_settings: ConfigFiles.overlay_settings,
|
|
21
|
+
project_browser_icon_theme: ConfigFiles.project_browser_icon_theme,
|
|
21
22
|
footer: prompt_footer_renderer,
|
|
22
23
|
composer_status: method(:composer_status_text),
|
|
23
24
|
busy_help: ConfigFiles.composer_busy_help?,
|
data/lib/kward/cli/rendering.rb
CHANGED
|
@@ -7,9 +7,15 @@ module Kward
|
|
|
7
7
|
private
|
|
8
8
|
|
|
9
9
|
def render_conversation_transcript(conversation)
|
|
10
|
+
render_transcript_messages(conversation.messages)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Renders a transcript supplied by either a Kward conversation or a
|
|
14
|
+
# plugin-owned tab driver.
|
|
15
|
+
def render_transcript_messages(messages)
|
|
10
16
|
tool_calls_by_id = {}
|
|
11
17
|
@prompt.say("\n#{colored("Transcript", :gray, :bold)}\n")
|
|
12
|
-
|
|
18
|
+
Array(messages).each do |message|
|
|
13
19
|
role = message_role(message)
|
|
14
20
|
next if role == "system"
|
|
15
21
|
|
|
@@ -43,23 +43,14 @@ module Kward
|
|
|
43
43
|
end
|
|
44
44
|
|
|
45
45
|
def build_interactive_agent(conversation)
|
|
46
|
-
@active_worker_role = "implementation"
|
|
47
|
-
set_visible_worker("implementation", status: "active")
|
|
48
|
-
build_worker_agent(conversation, role: "implementation")
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def build_worker_agent(conversation, role: "implementation")
|
|
52
46
|
conversation.plugin_registry ||= plugin_registry if conversation.respond_to?(:plugin_registry)
|
|
53
47
|
workspace = configured_workspace(root: conversation.workspace_root)
|
|
54
|
-
writer_id = worker_writer_id(role)
|
|
55
48
|
hook_manager = lifecycle_hook_manager(conversation)
|
|
56
49
|
hook_context = lifecycle_hook_context(conversation)
|
|
57
50
|
tool_registry = ToolRegistry.new(
|
|
58
51
|
workspace: workspace,
|
|
59
52
|
prompt: @prompt,
|
|
60
|
-
|
|
61
|
-
write_lock: @worker_write_lock,
|
|
62
|
-
writer_id: writer_id,
|
|
53
|
+
tool_approval: interactive_tool_approval_callback,
|
|
63
54
|
hook_manager: hook_manager,
|
|
64
55
|
hook_context: hook_context
|
|
65
56
|
)
|
|
@@ -74,32 +65,17 @@ module Kward
|
|
|
74
65
|
)
|
|
75
66
|
end
|
|
76
67
|
|
|
77
|
-
def
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
@visible_worker = worker
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
def worker_writer_id(role)
|
|
84
|
-
return nil unless Workers::ToolPolicy.write_capable?(role)
|
|
85
|
-
|
|
86
|
-
@worker_write_lock ||= Workers::WriteLock.new
|
|
87
|
-
owner_id = role.to_s.empty? ? "implementation" : role.to_s
|
|
88
|
-
return owner_id if @worker_write_lock.acquire(owner_id)
|
|
68
|
+
def interactive_tool_approval_callback
|
|
69
|
+
return nil unless @prompt.respond_to?(:ask_tool_approval)
|
|
70
|
+
return nil unless ConfigFiles.permission_policy(safely_read_config.to_h).enabled?
|
|
89
71
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
build_interactive_agent(agent.conversation)
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
def release_implementation_writer
|
|
102
|
-
@worker_write_lock&.release("implementation")
|
|
72
|
+
lambda do |tool_call:, name:, args:, cancellation:|
|
|
73
|
+
@prompt.ask_tool_approval(
|
|
74
|
+
tool_name: name,
|
|
75
|
+
args: args,
|
|
76
|
+
reason: args["hook_message"] || args[:hook_message]
|
|
77
|
+
)
|
|
78
|
+
end
|
|
103
79
|
end
|
|
104
80
|
|
|
105
81
|
def handle_interactive_shell_command(input, agent)
|
data/lib/kward/cli/sessions.rb
CHANGED
|
@@ -80,6 +80,15 @@ module Kward
|
|
|
80
80
|
@cleanup_sessions.clear
|
|
81
81
|
end
|
|
82
82
|
|
|
83
|
+
def live_session_paths
|
|
84
|
+
(Array(@tabs).filter_map { |tab| tab.session&.path } + [@active_session&.path]).compact.uniq
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def recent_sessions(session_store, tree: false, limit: nil)
|
|
88
|
+
method = tree ? :recent_tree : :recent
|
|
89
|
+
session_store.public_send(method, limit: limit, keep_empty_path: live_session_paths)
|
|
90
|
+
end
|
|
91
|
+
|
|
83
92
|
def cleanup_replaced_session(previous_session)
|
|
84
93
|
return unless previous_session
|
|
85
94
|
return if @active_session && File.expand_path(previous_session.path) == File.expand_path(@active_session.path)
|
|
@@ -590,8 +599,8 @@ module Kward
|
|
|
590
599
|
end
|
|
591
600
|
|
|
592
601
|
def insert_session_copy(session_store, sessions, labels, source, copy_path)
|
|
593
|
-
copy_info = session_store
|
|
594
|
-
copy_info ||= session_store
|
|
602
|
+
copy_info = recent_sessions(session_store, tree: true).find { |session| File.expand_path(session.path) == File.expand_path(copy_path) }
|
|
603
|
+
copy_info ||= recent_sessions(session_store).find { |session| File.expand_path(session.path) == File.expand_path(copy_path) }
|
|
595
604
|
return nil unless copy_info
|
|
596
605
|
|
|
597
606
|
source_index = sessions.index(source) || 0
|
|
@@ -618,7 +627,7 @@ module Kward
|
|
|
618
627
|
return nil unless source
|
|
619
628
|
|
|
620
629
|
session_store.load(source.path).first.rename(name)
|
|
621
|
-
updated = session_store
|
|
630
|
+
updated = recent_sessions(session_store, tree: true)
|
|
622
631
|
sessions.replace(updated)
|
|
623
632
|
labels.replace(session_picker_labels(sessions))
|
|
624
633
|
index = sessions.index { |session| File.expand_path(session.path) == File.expand_path(source.path) } || 0
|
|
@@ -716,7 +725,7 @@ module Kward
|
|
|
716
725
|
end
|
|
717
726
|
|
|
718
727
|
def select_session_path(session_store)
|
|
719
|
-
select_session_path_from_sessions(session_store
|
|
728
|
+
select_session_path_from_sessions(recent_sessions(session_store, tree: true), session_store: session_store)
|
|
720
729
|
end
|
|
721
730
|
|
|
722
731
|
def reopen_sessions_after_fork(session_store, source_path, source_label)
|
|
@@ -724,7 +733,7 @@ module Kward
|
|
|
724
733
|
fork_session_from_picker(session_store, source_path)
|
|
725
734
|
end
|
|
726
735
|
|
|
727
|
-
sessions = session_store
|
|
736
|
+
sessions = recent_sessions(session_store, tree: true)
|
|
728
737
|
labels = session_picker_labels(sessions)
|
|
729
738
|
initial_index = if fork_path
|
|
730
739
|
sessions.index { |session| File.expand_path(session.path) == File.expand_path(fork_path) }
|
data/lib/kward/cli/settings.rb
CHANGED
|
@@ -243,6 +243,8 @@ module Kward
|
|
|
243
243
|
next unless width
|
|
244
244
|
|
|
245
245
|
update_overlay_settings("width" => width)
|
|
246
|
+
when /\Afile icons/
|
|
247
|
+
configure_project_browser_icon_theme
|
|
246
248
|
when /\Ashow busy help/, /\Ahide busy help/
|
|
247
249
|
set_composer_busy_help(!composer_busy_help?)
|
|
248
250
|
runtime_output("Busy help #{composer_busy_help? ? "enabled" : "disabled"}. Restart the TUI to apply this setting.")
|
|
@@ -277,6 +279,7 @@ module Kward
|
|
|
277
279
|
[
|
|
278
280
|
"Overlay alignment (#{settings["alignment"]})",
|
|
279
281
|
"Overlay width (#{settings["width"]})",
|
|
282
|
+
"File icons (#{project_browser_icon_theme})",
|
|
280
283
|
"#{composer_busy_help? ? "Hide" : "Show"} busy help (currently #{on_off(composer_busy_help?)})",
|
|
281
284
|
"Tab keybindings (#{composer_tab_keybindings})",
|
|
282
285
|
"Editor mode (#{editor_mode})",
|
|
@@ -290,6 +293,25 @@ module Kward
|
|
|
290
293
|
]
|
|
291
294
|
end
|
|
292
295
|
|
|
296
|
+
def project_browser_icon_theme
|
|
297
|
+
ConfigFiles.project_browser_icon_theme(safely_read_config.to_h)
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
def configure_project_browser_icon_theme
|
|
301
|
+
selected = @prompt.select("File icons", project_browser_icon_theme_choices, title: "Settings")
|
|
302
|
+
theme = selected.to_s.split.first.to_s.downcase
|
|
303
|
+
return unless ConfigFiles::PROJECT_BROWSER_ICON_THEMES.include?(theme)
|
|
304
|
+
|
|
305
|
+
update_nested_config("project_browser", "icons" => theme)
|
|
306
|
+
@prompt.update_project_browser_icon_theme(theme) if @prompt.respond_to?(:update_project_browser_icon_theme)
|
|
307
|
+
runtime_output("File icons set to #{theme}.")
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def project_browser_icon_theme_choices
|
|
311
|
+
current = project_browser_icon_theme
|
|
312
|
+
ConfigFiles::PROJECT_BROWSER_ICON_THEMES.map { |theme| theme == current ? "#{theme} (current)" : theme }
|
|
313
|
+
end
|
|
314
|
+
|
|
293
315
|
def composer_busy_help?
|
|
294
316
|
ConfigFiles.composer_busy_help?(safely_read_config.to_h)
|
|
295
317
|
end
|