kward 0.84.0 → 0.85.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 +34 -0
- data/Gemfile.lock +2 -2
- data/README.md +2 -2
- data/doc/agent-tools.md +13 -1
- data/doc/api.md +17 -2
- data/doc/composer.md +1 -1
- data/doc/configuration.md +27 -4
- data/doc/extensibility.md +2 -1
- data/doc/git.md +3 -1
- data/doc/pan.md +6 -0
- data/doc/permissions.md +4 -4
- data/doc/plugins.md +464 -15
- data/doc/rpc.md +154 -16
- data/doc/sandboxing.md +11 -5
- data/doc/security.md +7 -1
- data/doc/session-management.md +5 -4
- data/doc/tabs.md +6 -2
- data/doc/transports.md +15 -0
- data/doc/usage.md +4 -1
- data/doc/workspace-tools.md +9 -0
- data/examples/plugins/space_invaders.rb +1 -1
- data/examples/plugins/stardate_footer.rb +2 -2
- data/examples/plugins/telegram/plugin.rb +1 -1
- data/lib/kward/agent.rb +24 -11
- data/lib/kward/cli/compaction.rb +9 -3
- data/lib/kward/cli/plugins.rb +81 -12
- data/lib/kward/cli/prompt_interface.rb +25 -5
- data/lib/kward/cli/rendering.rb +3 -0
- data/lib/kward/cli/runtime_helpers.rb +121 -27
- data/lib/kward/cli/sessions.rb +9 -5
- data/lib/kward/cli/settings/menus.rb +745 -0
- data/lib/kward/cli/settings/model.rb +327 -0
- data/lib/kward/cli/settings.rb +6 -1055
- data/lib/kward/cli/slash_commands.rb +45 -4
- data/lib/kward/cli/tabs.rb +167 -27
- data/lib/kward/{cli_transcript_formatter.rb → cli/transcript_formatter.rb} +3 -3
- data/lib/kward/cli/worktrees.rb +65 -2
- data/lib/kward/cli.rb +26 -24
- data/lib/kward/compactor.rb +18 -7
- data/lib/kward/config/core.rb +389 -0
- data/lib/kward/config/extensions.rb +96 -0
- data/lib/kward/config/prompts.rb +313 -0
- data/lib/kward/config/settings.rb +250 -0
- data/lib/kward/config_files.rb +14 -1008
- data/lib/kward/conversation.rb +31 -2
- data/lib/kward/image_attachments.rb +1 -1
- data/lib/kward/model/client.rb +2 -2
- data/lib/kward/model/copilot_models.rb +2 -2
- data/lib/kward/model/model_info.rb +20 -3
- data/lib/kward/{openrouter_model_cache.rb → model/openrouter_model_cache.rb} +3 -3
- data/lib/kward/model/payloads.rb +12 -3
- data/lib/kward/model/typesafe_client.rb +78 -0
- data/lib/kward/pan/server.rb +10 -7
- data/lib/kward/permissions/policy.rb +6 -2
- data/lib/kward/plugin_registry.rb +2 -659
- data/lib/kward/plugins/actions.rb +453 -0
- data/lib/kward/plugins/chat_contract.rb +121 -0
- data/lib/kward/{plugin_chat_runtime.rb → plugins/chat_runtime.rb} +62 -18
- data/lib/kward/plugins/host.rb +232 -0
- data/lib/kward/plugins/registry.rb +1190 -0
- data/lib/kward/plugins/resources.rb +206 -0
- data/lib/kward/plugins/turn_request.rb +36 -0
- data/lib/kward/plugins/ui.rb +219 -0
- data/lib/kward/prompt_interface/composer_renderer.rb +1 -1
- data/lib/kward/prompt_interface/composer_state.rb +1 -1
- data/lib/kward/prompt_interface/editor/controller.rb +1 -1
- data/lib/kward/prompt_interface/editor/runner.rb +1 -1
- data/lib/kward/prompt_interface/editor/state.rb +1 -1
- data/lib/kward/prompt_interface/layout.rb +1 -1
- data/lib/kward/prompt_interface/overlay_renderer.rb +1 -1
- data/lib/kward/prompt_interface/plugin_ui_requests.rb +82 -0
- data/lib/kward/prompt_interface/runtime_state.rb +6 -1
- data/lib/kward/prompt_interface/screen.rb +9 -2
- data/lib/kward/prompt_interface.rb +60 -11
- data/lib/kward/prompts/commands.rb +2 -1
- data/lib/kward/{adaptive_pty_output_sink.rb → pty/adaptive_output_sink.rb} +1 -1
- data/lib/kward/{interactive_pty_runner.rb → pty/interactive_runner.rb} +2 -2
- data/lib/kward/{local_command_runner.rb → pty/local_command_runner.rb} +1 -1
- data/lib/kward/{local_pty_command_runner.rb → pty/local_pty_runner.rb} +3 -9
- data/lib/kward/{pty_output_sink.rb → pty/output_sink.rb} +9 -4
- data/lib/kward/{pty_transcript_normalizer.rb → pty/transcript_normalizer.rb} +1 -1
- data/lib/kward/rpc/plugin_chat_manager.rb +30 -10
- data/lib/kward/rpc/prompt_bridge.rb +25 -0
- data/lib/kward/rpc/server.rb +91 -12
- data/lib/kward/rpc/session_manager.rb +147 -44
- data/lib/kward/rpc/session_tree_rows.rb +2 -2
- data/lib/kward/rpc/tool_metadata.rb +1 -1
- data/lib/kward/sandbox/command_runner.rb +1 -1
- data/lib/kward/{session_catalog.rb → sessions/catalog.rb} +1 -1
- data/lib/kward/{session_store.rb → sessions/store.rb} +9 -9
- data/lib/kward/{session_tree_nodes.rb → sessions/tree_nodes.rb} +3 -3
- data/lib/kward/{session_tree_renderer.rb → sessions/tree_renderer.rb} +4 -4
- data/lib/kward/{session_tree_tool_display.rb → sessions/tree_tool_display.rb} +1 -1
- data/lib/kward/{kwsh.rb → shell/kwsh.rb} +3 -3
- data/lib/kward/{persistent_shell_session.rb → shell/persistent_session.rb} +3 -3
- data/lib/kward/{shell_prompt_session.rb → shell/prompt_session.rb} +1 -1
- data/lib/kward/skills/trust_store.rb +1 -1
- data/lib/kward/tabs/driver.rb +194 -0
- data/lib/kward/{tab_store.rb → tabs/store.rb} +2 -2
- data/lib/kward/{clipboard.rb → terminal/clipboard.rb} +1 -1
- data/lib/kward/{terminal_image_support.rb → terminal/image_support.rb} +1 -1
- data/lib/kward/tools/base.rb +18 -0
- data/lib/kward/tools/context_for_task.rb +15 -6
- data/lib/kward/tools/edit_file.rb +9 -6
- data/lib/kward/tools/git_commit.rb +13 -7
- data/lib/kward/tools/list_directory.rb +4 -4
- data/lib/kward/tools/plugin_tool.rb +41 -0
- data/lib/kward/tools/prepare_shell_command.rb +1 -1
- data/lib/kward/tools/read_file.rb +7 -6
- data/lib/kward/tools/registry.rb +96 -13
- data/lib/kward/tools/run_shell_command.rb +10 -8
- data/lib/kward/tools/search/code.rb +1 -1
- data/lib/kward/tools/summarize_file_structure.rb +5 -5
- data/lib/kward/tools/tool_call.rb +2 -1
- data/lib/kward/tools/typesafe_evaluate.rb +81 -0
- data/lib/kward/tools/workspace_targets.rb +58 -0
- data/lib/kward/tools/write_file.rb +9 -6
- data/lib/kward/{export_path.rb → transcripts/export_path.rb} +1 -1
- data/lib/kward/{markdown_transcript.rb → transcripts/markdown_transcript.rb} +2 -2
- data/lib/kward/transport/contracts.rb +200 -0
- data/lib/kward/transport/gateway.rb +79 -35
- data/lib/kward/transport/plugin_chat_gateway.rb +3 -2
- data/lib/kward/transport.rb +1 -200
- data/lib/kward/version.rb +1 -1
- data/lib/kward/{workspace_factory.rb → workspace/factory.rb} +2 -2
- data/lib/kward/{git_worktree_manager.rb → workspace/git_worktree_manager.rb} +28 -0
- data/lib/kward/{workspace.rb → workspace/workspace.rb} +3 -3
- data/templates/default/fulldoc/html/css/kward.css +125 -0
- data/templates/default/fulldoc/html/images/kward_screen_1.png +0 -0
- data/templates/default/fulldoc/html/setup.rb +1 -1
- data/templates/default/layout/html/layout.erb +16 -4
- metadata +67 -48
- data/lib/kward/tab_driver.rb +0 -90
- data/templates/default/fulldoc/html/images/kward_workflow.svg +0 -52
- /data/lib/kward/{editor_prompt.rb → cli/editor_prompt.rb} +0 -0
- /data/lib/kward/{editor_prompt_session.rb → cli/editor_prompt_session.rb} +0 -0
- /data/lib/kward/{diff_view_mode.rb → prompt_interface/editor/diff_view_mode.rb} +0 -0
- /data/lib/kward/{editor_mode.rb → prompt_interface/editor/editor_mode.rb} +0 -0
- /data/lib/kward/{markdown_code_block.rb → prompt_interface/editor/markdown_code_block.rb} +0 -0
- /data/lib/kward/{scratchpad_languages.rb → prompt_interface/editor/scratchpad_languages.rb} +0 -0
- /data/lib/kward/{scratchpad_runner.rb → prompt_interface/editor/scratchpad_runner.rb} +0 -0
- /data/lib/kward/{detached_run.rb → pty/detached_run.rb} +0 -0
- /data/lib/kward/{session_diff.rb → sessions/diff.rb} +0 -0
- /data/lib/kward/{session_naming.rb → sessions/naming.rb} +0 -0
- /data/lib/kward/{session_trash.rb → sessions/trash.rb} +0 -0
- /data/lib/kward/{kwshrc.rb → shell/kwshrc.rb} +0 -0
- /data/lib/kward/{shell_prompt.rb → shell/prompt.rb} +0 -0
- /data/lib/kward/{ansi.rb → terminal/ansi.rb} +0 -0
- /data/lib/kward/{terminal_keys.rb → terminal/keys.rb} +0 -0
- /data/lib/kward/{terminal_sequences.rb → terminal/sequences.rb} +0 -0
- /data/lib/kward/{terminal_text.rb → terminal/text.rb} +0 -0
- /data/lib/kward/{transcript_export.rb → transcripts/transcript_export.rb} +0 -0
- /data/lib/kward/{project_files.rb → workspace/files.rb} +0 -0
- /data/lib/kward/{path_guard.rb → workspace/path_guard.rb} +0 -0
data/doc/plugins.md
CHANGED
|
@@ -5,6 +5,7 @@ Plugins are trusted local Ruby extensions for Kward. Use them when you need beha
|
|
|
5
5
|
Good plugin use cases:
|
|
6
6
|
|
|
7
7
|
- add a slash command for a personal workflow,
|
|
8
|
+
- expose a local integration as a model-callable tool,
|
|
8
9
|
- show project/session status in the terminal footer,
|
|
9
10
|
- add concise local context to prompts,
|
|
10
11
|
- log or observe transcript events,
|
|
@@ -49,7 +50,7 @@ mkdir -p ~/.kward/plugins
|
|
|
49
50
|
Create `~/.kward/plugins/hello.rb`:
|
|
50
51
|
|
|
51
52
|
```ruby
|
|
52
|
-
Kward.plugin do |plugin|
|
|
53
|
+
Kward.plugin(id: "com.example.hello", version: "1.0.0", api: 1) do |plugin|
|
|
53
54
|
plugin.command "hello", description: "Say hello", argument_hint: "[name]" do |args, ctx|
|
|
54
55
|
name = args.strip.empty? ? "there" : args.strip
|
|
55
56
|
ctx.say("Hello, #{name}.")
|
|
@@ -65,6 +66,112 @@ Start Kward and run:
|
|
|
65
66
|
|
|
66
67
|
When developing plugins or prompt templates, use `/reload` inside Kward to reload configured prompt files and all plugin files without restarting. This picks up prompt edits, changes to existing plugins, and new plugin registrations, then refreshes slash-command completion and rebuilds the system message.
|
|
67
68
|
|
|
69
|
+
## Plugin identity and host services
|
|
70
|
+
|
|
71
|
+
Give a reusable plugin a stable reverse-domain-style `id`, its own `version`, and
|
|
72
|
+
the Kward plugin API it targets. API version `1` is currently supported. Kward
|
|
73
|
+
skips plugins that declare an unsupported API version or duplicate another
|
|
74
|
+
plugin's ID.
|
|
75
|
+
|
|
76
|
+
An identified plugin receives one shared host for configuration, private durable
|
|
77
|
+
storage, secret lookup, and logging:
|
|
78
|
+
|
|
79
|
+
```ruby
|
|
80
|
+
Kward.plugin(id: "com.example.issues", version: "1.2.0", api: 1) do |plugin|
|
|
81
|
+
host = plugin.host
|
|
82
|
+
|
|
83
|
+
plugin.command "issue-server", description: "Show the issue server" do |_args, ctx|
|
|
84
|
+
visits = host.storage.get("visits").to_i + 1
|
|
85
|
+
endpoint = host.config.fetch("endpoint")
|
|
86
|
+
host.storage.put("visits", visits)
|
|
87
|
+
ctx.say("#{endpoint} (visit #{visits})")
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Configure the plugin under its stable ID in `config.json`:
|
|
93
|
+
|
|
94
|
+
```json
|
|
95
|
+
{
|
|
96
|
+
"plugins": {
|
|
97
|
+
"com.example.issues": {
|
|
98
|
+
"endpoint": "https://issues.example.com"
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
The host exposes:
|
|
105
|
+
|
|
106
|
+
- `host.id`, `host.version`, and `host.api_version`;
|
|
107
|
+
- `host.config`, an immutable copy of the plugin's namespaced configuration;
|
|
108
|
+
- `host.storage.get`, `put`, and `delete` for JSON-compatible values;
|
|
109
|
+
- `host.secret(name, env: nil)` for private config or environment lookup;
|
|
110
|
+
- `host.logger`, a standard Ruby logger routed through Kward's diagnostic output.
|
|
111
|
+
|
|
112
|
+
Storage is kept in `plugin_state/<plugin-id>/state.json` under Kward's active
|
|
113
|
+
config directory and survives `/reload`. Secret lookup checks plugin config,
|
|
114
|
+
then the optional explicit environment variable, then a conventional name such
|
|
115
|
+
as `KWARD_PLUGIN_COM_EXAMPLE_ISSUES_TOKEN`. Do not log secret values.
|
|
116
|
+
|
|
117
|
+
Existing `Kward.plugin do ... end` files remain supported, but `plugin.host` is
|
|
118
|
+
`nil` until the plugin declares stable identity metadata.
|
|
119
|
+
|
|
120
|
+
## Lifecycle and owned background work
|
|
121
|
+
|
|
122
|
+
Plugin files are loaded without starting runtime work. Identified plugins can
|
|
123
|
+
register lifecycle callbacks for the points where managed work is safe:
|
|
124
|
+
|
|
125
|
+
```ruby
|
|
126
|
+
Kward.plugin(id: "com.example.watcher", version: "1.0.0", api: 1) do |plugin|
|
|
127
|
+
plugin.on_start do |host|
|
|
128
|
+
subscription = Watcher.subscribe { |event| host.logger.info(event) }
|
|
129
|
+
host.on_cleanup(subscription) { |value| Watcher.unsubscribe(value) }
|
|
130
|
+
|
|
131
|
+
host.background(name: "poller") do |cancellation|
|
|
132
|
+
until cancellation.cancelled?
|
|
133
|
+
Watcher.poll
|
|
134
|
+
sleep 1
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
plugin.on_reload { |host| host.logger.info("Reloading") }
|
|
140
|
+
plugin.on_shutdown { |host| host.logger.info("Stopping") }
|
|
141
|
+
end
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Lifecycle behavior is explicit:
|
|
145
|
+
|
|
146
|
+
- `on_start` runs after loading, when the registry becomes active. A newly loaded
|
|
147
|
+
registry receives `on_start` after `/reload` too.
|
|
148
|
+
- `on_reload` runs on the old plugin instance immediately before Kward cleans up
|
|
149
|
+
its resources.
|
|
150
|
+
- `on_shutdown` runs immediately before final process cleanup.
|
|
151
|
+
- Callback failures are reported as warnings and do not prevent other plugins
|
|
152
|
+
from cleaning up.
|
|
153
|
+
|
|
154
|
+
`host.background` returns a `PluginTask`. Its block may accept a cooperative
|
|
155
|
+
`Cancellation` token. Kward cancels and waits up to a bounded deadline for owned
|
|
156
|
+
tasks during reload or shutdown; blocking work should register cleanup that
|
|
157
|
+
closes the socket, stream, or other resource needed to wake it. Tasks also accept
|
|
158
|
+
an optional parent token with `cancellation:`.
|
|
159
|
+
|
|
160
|
+
`host.on_cleanup(resource) { |resource| ... }` returns an idempotent
|
|
161
|
+
`PluginDisposable`. Call `dispose` to unsubscribe early, or leave it registered
|
|
162
|
+
for automatic cleanup. `host.manage` is an alias. Cleanup runs in reverse
|
|
163
|
+
registration order so dependent resources unwind predictably.
|
|
164
|
+
|
|
165
|
+
Plugin-owned tab hosts expose the same `background`, `on_cleanup`, and `manage`
|
|
166
|
+
methods. Their resources are cleaned up when a local tab closes or its shared
|
|
167
|
+
plugin-chat runtime shuts down. A tab driver may implement `close` (or `shutdown`
|
|
168
|
+
as a fallback) for its own final cleanup; Kward invokes it before closing the tab
|
|
169
|
+
host.
|
|
170
|
+
|
|
171
|
+
Do not open network connections or start threads directly while the plugin file
|
|
172
|
+
is loading. Register them from `on_start`, a command, or a plugin-tab factory so
|
|
173
|
+
Kward can own their lifetime.
|
|
174
|
+
|
|
68
175
|
## Add a slash command
|
|
69
176
|
|
|
70
177
|
Use plugin commands for local actions that should not call the model.
|
|
@@ -82,6 +189,264 @@ Command names do not include `/`. They must start with a letter or number and ma
|
|
|
82
189
|
|
|
83
190
|
A plugin command cannot replace a built-in command or prompt-template command.
|
|
84
191
|
|
|
192
|
+
### Typed command arguments and results
|
|
193
|
+
|
|
194
|
+
Add an object JSON Schema when a command needs validated arguments. Interactive
|
|
195
|
+
slash commands use shell-style `--flags`; RPC clients may send either the same
|
|
196
|
+
text or an argument object. Declare `positionals:` when selected properties may
|
|
197
|
+
be supplied without flags:
|
|
198
|
+
|
|
199
|
+
```ruby
|
|
200
|
+
Kward.plugin(id: "com.example.release", version: "1.0.0", api: 1) do |plugin|
|
|
201
|
+
plugin.command "deploy",
|
|
202
|
+
description: "Deploy a service",
|
|
203
|
+
argument_hint: "SERVICE [--environment NAME] [--dry-run]",
|
|
204
|
+
schema: {
|
|
205
|
+
type: "object",
|
|
206
|
+
properties: {
|
|
207
|
+
service: { type: "string" },
|
|
208
|
+
environment: {
|
|
209
|
+
type: "string",
|
|
210
|
+
enum: %w[staging production],
|
|
211
|
+
default: "staging"
|
|
212
|
+
},
|
|
213
|
+
dry_run: { type: "boolean", default: false },
|
|
214
|
+
labels: { type: "array", items: { type: "string" } }
|
|
215
|
+
},
|
|
216
|
+
required: ["service"]
|
|
217
|
+
},
|
|
218
|
+
positionals: ["service"] do |args, ctx|
|
|
219
|
+
deployment = Release.deploy(
|
|
220
|
+
args.fetch("service"),
|
|
221
|
+
environment: args.fetch("environment"),
|
|
222
|
+
dry_run: args.fetch("dry_run"),
|
|
223
|
+
labels: args.fetch("labels", [])
|
|
224
|
+
)
|
|
225
|
+
ctx.result(
|
|
226
|
+
message: "Queued deployment #{deployment.id}.",
|
|
227
|
+
data: { deployment_id: deployment.id }
|
|
228
|
+
)
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
For example:
|
|
234
|
+
|
|
235
|
+
```text
|
|
236
|
+
/deploy api --environment production --dry-run --labels urgent --labels "release candidate"
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Typed command handlers receive a string-keyed argument hash through both the
|
|
240
|
+
first block argument and `ctx.args`. Supported property types are `string`,
|
|
241
|
+
`integer`, `number`, `boolean`, `array`, and `object`; array items must be scalar.
|
|
242
|
+
Use `--flag` or `--no-flag` for booleans, repeat an array option to collect
|
|
243
|
+
values, and use `--` before positional text that starts with a dash. Unknown,
|
|
244
|
+
missing, duplicate, incorrectly typed, and out-of-enum arguments are rejected
|
|
245
|
+
before plugin code runs. Commands without `schema:` keep receiving their raw
|
|
246
|
+
argument string for compatibility.
|
|
247
|
+
|
|
248
|
+
`ctx.result(message:, data:)` returns optional user-facing text plus optional
|
|
249
|
+
JSON-compatible machine data. The TUI displays `message`. RPC returns the full
|
|
250
|
+
structured result, and asynchronous slash-command turns also emit a
|
|
251
|
+
`pluginCommandResult` event before their normal answer event.
|
|
252
|
+
|
|
253
|
+
## Add a namespaced action
|
|
254
|
+
|
|
255
|
+
Actions are typed operations intended for trusted RPC integrations rather than
|
|
256
|
+
slash-command completion. They require identified plugin metadata and receive a
|
|
257
|
+
stable `<plugin-id>/<action-name>` ID:
|
|
258
|
+
|
|
259
|
+
```ruby
|
|
260
|
+
Kward.plugin(id: "com.example.release", version: "1.0.0", api: 1) do |plugin|
|
|
261
|
+
plugin.action "status",
|
|
262
|
+
description: "Read deployment status",
|
|
263
|
+
schema: {
|
|
264
|
+
type: "object",
|
|
265
|
+
properties: { deployment_id: { type: "integer" } },
|
|
266
|
+
required: ["deployment_id"]
|
|
267
|
+
} do |args, ctx|
|
|
268
|
+
deployment = Release.find(args.fetch("deployment_id"))
|
|
269
|
+
ctx.result(data: { id: deployment.id, state: deployment.state })
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
RPC clients discover actions with `pluginActions/list` and invoke them with
|
|
275
|
+
`pluginActions/run`. Action arguments must be an object and results always use
|
|
276
|
+
the structured `{ message, data }` contract. Synchronous actions support
|
|
277
|
+
`ctx.say`, progress, and notifications, but blocking UI requests fail closed so
|
|
278
|
+
the RPC reader cannot deadlock. Actions are disabled when the session execution
|
|
279
|
+
profile disables plugin commands. They are not exposed as local TUI commands;
|
|
280
|
+
register a typed command as well when people should invoke the operation from
|
|
281
|
+
the composer.
|
|
282
|
+
|
|
283
|
+
## Add a model-callable tool
|
|
284
|
+
|
|
285
|
+
Plugin tools let the model call trusted local Ruby integrations without requiring
|
|
286
|
+
an MCP server. Define a model-facing description and a strict object JSON Schema
|
|
287
|
+
for the arguments:
|
|
288
|
+
|
|
289
|
+
```ruby
|
|
290
|
+
Kward.plugin do |plugin|
|
|
291
|
+
plugin.tool "issue_search",
|
|
292
|
+
description: "Search the local issue tracker",
|
|
293
|
+
schema: {
|
|
294
|
+
type: "object",
|
|
295
|
+
properties: {
|
|
296
|
+
query: { type: "string", description: "Issue search text." },
|
|
297
|
+
limit: { type: "integer", description: "Maximum results." }
|
|
298
|
+
},
|
|
299
|
+
required: ["query"]
|
|
300
|
+
} do |args, ctx|
|
|
301
|
+
ctx.cancellation&.raise_if_cancelled!
|
|
302
|
+
IssueTracker.search(args.fetch("query"), limit: args.fetch("limit", 10))
|
|
303
|
+
.map { |issue| "#{issue.id}: #{issue.title}" }
|
|
304
|
+
.join("\n")
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
The handler receives a parsed argument hash and a normal plugin context. It
|
|
310
|
+
should return model-facing text. `ctx.cancellation` contains the
|
|
311
|
+
active cooperative cancellation token, and `ctx.cancelled?` is a convenient
|
|
312
|
+
boolean check for longer operations.
|
|
313
|
+
|
|
314
|
+
Plugin tool schemas are exposed in normal CLI, RPC, Pan, and transport-backed
|
|
315
|
+
agent turns. Kward forces `additionalProperties: false`, validates required
|
|
316
|
+
property names, reports the tool source as `plugin`, and routes execution through
|
|
317
|
+
the normal permission policy, approval bridge, lifecycle hooks, output
|
|
318
|
+
compaction, and transcript artifact storage. Restricted execution profiles can
|
|
319
|
+
filter plugin tools by name or remove all tools. Editor-scoped prompts,
|
|
320
|
+
shell-agent prompts, and strict worktree agents do not receive plugin tools.
|
|
321
|
+
Plugin-owned chats continue to own their own tool behavior.
|
|
322
|
+
|
|
323
|
+
Plugin tools cannot replace built-in, MCP, or other plugin tools. Duplicate
|
|
324
|
+
plugin registrations are skipped with a warning. Because trusted plugin code can
|
|
325
|
+
perform arbitrary local or network effects, an enabled permission policy treats
|
|
326
|
+
plugin tools as approval-requiring operations unless an explicit allow rule
|
|
327
|
+
matches the tool or `source: plugin`.
|
|
328
|
+
|
|
329
|
+
## Use structured plugin UI
|
|
330
|
+
|
|
331
|
+
Plugin commands and model-callable plugin tools receive `ctx.ui`, a
|
|
332
|
+
frontend-neutral interface for questions, choices, confirmation, text input,
|
|
333
|
+
notifications, and progress:
|
|
334
|
+
|
|
335
|
+
```ruby
|
|
336
|
+
plugin.command "release", description: "Prepare a release" do |_args, ctx|
|
|
337
|
+
environment = ctx.ui.select(
|
|
338
|
+
"Environment",
|
|
339
|
+
[
|
|
340
|
+
{ label: "Staging", value: "staging", description: "Deploy for testing." },
|
|
341
|
+
{ label: "Production", value: "production", description: "Deploy publicly." }
|
|
342
|
+
]
|
|
343
|
+
)
|
|
344
|
+
next unless environment
|
|
345
|
+
next unless ctx.ui.confirm("Release", "Deploy to #{environment}?")
|
|
346
|
+
|
|
347
|
+
tag = ctx.ui.input("Release tag", "For example, v1.2.0")
|
|
348
|
+
next if tag.to_s.empty?
|
|
349
|
+
|
|
350
|
+
ctx.ui.progress(id: "release", message: "Preparing #{tag}", percent: 25)
|
|
351
|
+
# Perform work here.
|
|
352
|
+
ctx.ui.progress(id: "release", message: "Prepared #{tag}", percent: 100, done: true)
|
|
353
|
+
ctx.ui.notify("#{tag} is ready for #{environment}.", level: :success)
|
|
354
|
+
end
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
Available methods:
|
|
358
|
+
|
|
359
|
+
- `ctx.ui.question(questions)` uses the same validated 1-4 question contract as
|
|
360
|
+
`ask_user_question` and returns its answer array or `nil` when cancelled;
|
|
361
|
+
- `ctx.ui.select(title, options, message: nil)` accepts 1-100 strings or
|
|
362
|
+
`{ label:, value:, description: }` objects and returns the selected value;
|
|
363
|
+
- `ctx.ui.confirm(title, message = nil, default: false)` returns a boolean;
|
|
364
|
+
- `ctx.ui.input(title, placeholder = nil, default: nil)` returns text or `nil`;
|
|
365
|
+
- `ctx.ui.progress(id:, message:, percent: nil, done: false)` publishes a
|
|
366
|
+
non-blocking progress update;
|
|
367
|
+
- `ctx.ui.notify(message, level: :info)` publishes an `info`, `success`,
|
|
368
|
+
`warning`, or `error` notification;
|
|
369
|
+
- `ctx.ui.supported?(:select)` and `ctx.ui.capabilities` let a plugin inspect
|
|
370
|
+
the active frontend before requesting interaction.
|
|
371
|
+
|
|
372
|
+
Blocking requests fail closed when the active frontend does not support them:
|
|
373
|
+
questions, selections, and input return `nil`, while confirmation returns
|
|
374
|
+
`false`. Notifications and progress fall back to normal `ctx.say` text. Inputs
|
|
375
|
+
and emitted text are bounded, and plugin-tool cancellation is checked before
|
|
376
|
+
and after blocking requests.
|
|
377
|
+
|
|
378
|
+
The interactive terminal implements all six primitives. RPC implements them for
|
|
379
|
+
plugin commands submitted through `turns/start` and for plugin tools; use that
|
|
380
|
+
asynchronous turn path when a command needs to wait for UI input. Synchronous
|
|
381
|
+
RPC `commands/run` supports notifications and progress but deliberately fails
|
|
382
|
+
closed for blocking requests so the protocol reader cannot deadlock waiting for
|
|
383
|
+
its own response. Pan has no interaction bridge, so blocking requests fail
|
|
384
|
+
closed and non-blocking output falls back to its existing plugin message event.
|
|
385
|
+
Transport gateways expose blocking requests as transport-neutral interactions;
|
|
386
|
+
the transport adapter decides whether and how to render and answer them.
|
|
387
|
+
|
|
388
|
+
## Request a model response from a command
|
|
389
|
+
|
|
390
|
+
Use `ctx.request_turn(system: text)` when a slash command should run the active
|
|
391
|
+
session's model immediately with turn-scoped system instructions. For example,
|
|
392
|
+
save this as `~/.kward/plugins/iddqd.rb` and run `/reload`:
|
|
393
|
+
|
|
394
|
+
```ruby
|
|
395
|
+
Kward.plugin(id: "com.example.iddqd", version: "1.0.0", api: 1) do |plugin|
|
|
396
|
+
plugin.command "iddqd",
|
|
397
|
+
description: "Run a prompt as system instructions",
|
|
398
|
+
argument_hint: "<prompt>" do |text, ctx|
|
|
399
|
+
if text.strip.empty?
|
|
400
|
+
ctx.say("Usage: /iddqd <prompt>")
|
|
401
|
+
next
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
ctx.request_turn(system: text)
|
|
405
|
+
end
|
|
406
|
+
end
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
Then enter `/iddqd Answer in French and explain the current design.` The text
|
|
410
|
+
needs no quoting or flag parsing. Kward streams the response just like an ordinary
|
|
411
|
+
turn, with the same tools, permissions, hooks, cancellation, and tab ownership.
|
|
412
|
+
This does not replace Kward's base system prompt or bypass host security policy.
|
|
413
|
+
|
|
414
|
+
The call stages a request and returns `nil`; the host starts the model only after
|
|
415
|
+
the command successfully returns. A handler error or cancellation discards the
|
|
416
|
+
request. Each command may stage one request, containing nonblank text of at most
|
|
417
|
+
65,536 bytes. Instructions last through tool continuations, prompt refreshes,
|
|
418
|
+
compaction, and retries in that turn, then expire even if the turn fails.
|
|
419
|
+
|
|
420
|
+
Check `ctx.turn_requests_supported?` before offering this behavior on an unknown
|
|
421
|
+
host. It is supported by interactive CLI session commands, asynchronous RPC
|
|
422
|
+
`turns/start` commands, and session-backed transports whose execution profile
|
|
423
|
+
allows plugin commands. Synchronous RPC `commands/run`, plugin actions, plugin
|
|
424
|
+
owned chats, tools, hooks, status renderers, editor/shell prompts, and Pan do not
|
|
425
|
+
provide this capability. Unsupported calls raise an error rather than silently
|
|
426
|
+
launching work or submitting the text as a user prompt. Pan does not dispatch
|
|
427
|
+
plugin slash commands; use the TUI or RPC instead.
|
|
428
|
+
|
|
429
|
+
**History and provider notes:** the invocation and instruction text are stored
|
|
430
|
+
in the session for transcript display and export. Restoring, cloning, or forking
|
|
431
|
+
the session does not reactivate the instructions. Later model requests omit
|
|
432
|
+
these history entries; compaction receives an informational placeholder instead
|
|
433
|
+
of the expired text.
|
|
434
|
+
System-level input uses each provider's native instruction mechanism. On Codex
|
|
435
|
+
(ChatGPT subscription), the command becomes a new `developer` message at its
|
|
436
|
+
position in the conversation: that backend rejects inline `system` messages.
|
|
437
|
+
Direct OpenAI Responses and chat-completions payloads use an ordered `system`
|
|
438
|
+
message. These turn instructions are not folded into the base preamble, so the
|
|
439
|
+
model receives a new instruction after the preceding dialogue.
|
|
440
|
+
|
|
441
|
+
Anthropic and Gemini instead collect system instructions into a separate system
|
|
442
|
+
field; they cannot preserve that ordered-message boundary and require existing
|
|
443
|
+
dialogue. A system-only turn in an empty session is rejected explicitly. Other
|
|
444
|
+
provider/model restrictions surface as normal request errors. No synthetic user
|
|
445
|
+
prompt is inserted.
|
|
446
|
+
|
|
447
|
+
For instructions that should remain active in future turns instead, use prompt
|
|
448
|
+
context below.
|
|
449
|
+
|
|
85
450
|
## Add prompt context
|
|
86
451
|
|
|
87
452
|
Prompt context is short text injected into future model requests.
|
|
@@ -104,20 +469,53 @@ If plugin state changes and Kward should rebuild the active system message, call
|
|
|
104
469
|
ctx.refresh_system_message!
|
|
105
470
|
```
|
|
106
471
|
|
|
107
|
-
## Add
|
|
472
|
+
## Add status to the footer
|
|
108
473
|
|
|
109
|
-
|
|
474
|
+
Identified plugins can contribute compact status without replacing status from
|
|
475
|
+
other plugins. Give each contribution a stable name:
|
|
476
|
+
|
|
477
|
+
```ruby
|
|
478
|
+
Kward.plugin(id: "com.example.session-status", version: "1.0.0", api: 1) do |plugin|
|
|
479
|
+
plugin.status "session", order: 10, priority: :high do |ctx|
|
|
480
|
+
{
|
|
481
|
+
text: ctx.session_name || "unnamed",
|
|
482
|
+
tooltip: "Current Kward session"
|
|
483
|
+
}
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
plugin.status "messages", order: 20, priority: :low do |ctx|
|
|
487
|
+
"#{ctx.transcript.messages.length} messages"
|
|
488
|
+
end
|
|
489
|
+
end
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
Kward joins visible contributions with ` · `. Lower `order` values appear
|
|
493
|
+
first. `priority` may be `:low`, `:normal` (the default), or `:high`. When the
|
|
494
|
+
terminal is too narrow, Kward removes complete low-priority contributions
|
|
495
|
+
first, followed by normal- and high-priority contributions. Among contributions
|
|
496
|
+
with the same priority, later ones are removed first.
|
|
497
|
+
|
|
498
|
+
Return a string for ordinary status, a hash with `text` and optional `tooltip`
|
|
499
|
+
for structured clients, or `nil` to hide the contribution temporarily. One
|
|
500
|
+
renderer failing does not hide status from other plugins. Kward evaluates the
|
|
501
|
+
contributions at most once per second and reuses their values between refreshes.
|
|
502
|
+
|
|
503
|
+
RPC clients receive both the combined `text` fallback and the individual
|
|
504
|
+
structured segments. Terminal footers display the combined text; tooltips are
|
|
505
|
+
available to clients that can render them.
|
|
506
|
+
|
|
507
|
+
The older `plugin.footer` API remains supported. Each legacy footer is treated
|
|
508
|
+
as a normal-priority contribution, so footers from different plugins now
|
|
509
|
+
compose instead of replacing one another:
|
|
110
510
|
|
|
111
511
|
```ruby
|
|
112
512
|
Kward.plugin do |plugin|
|
|
113
513
|
plugin.footer do |ctx|
|
|
114
|
-
"#{ctx.session_name || 'unnamed'}
|
|
514
|
+
"#{ctx.session_name || 'unnamed'}"
|
|
115
515
|
end
|
|
116
516
|
end
|
|
117
517
|
```
|
|
118
518
|
|
|
119
|
-
Only one footer is active. If multiple plugins register footers, the later one replaces the earlier one and Kward prints a warning. Kward evaluates the active footer at most once per second and reuses its last value between refreshes.
|
|
120
|
-
|
|
121
519
|
## Add an interactive command
|
|
122
520
|
|
|
123
521
|
Interactive commands take over the composer region with a Kward-driven render and
|
|
@@ -190,13 +588,25 @@ in piped/non-interactive mode or through RPC.
|
|
|
190
588
|
|
|
191
589
|
A plugin can provide a persistent tab with Kward's normal composer, transcript
|
|
192
590
|
rendering, streaming, image input, cancellation, and tab switching. The plugin
|
|
193
|
-
owns its transcript
|
|
194
|
-
need to use a
|
|
591
|
+
owns its transcript and model behavior, while Kward can provide scoped storage,
|
|
592
|
+
configuration, secrets, logging, and resource cleanup. It does not need to use a
|
|
593
|
+
Kward workspace session.
|
|
195
594
|
|
|
196
595
|
```ruby
|
|
197
|
-
Kward.plugin do |plugin|
|
|
198
|
-
plugin.tab_type
|
|
199
|
-
|
|
596
|
+
Kward.plugin(id: "com.example.plugin", version: "1.0.0", api: 1) do |plugin|
|
|
597
|
+
plugin.tab_type(
|
|
598
|
+
"example",
|
|
599
|
+
id: "com.example.chat",
|
|
600
|
+
title: "Example",
|
|
601
|
+
singleton: :global,
|
|
602
|
+
api: 1,
|
|
603
|
+
capabilities: {
|
|
604
|
+
attachments: [:image],
|
|
605
|
+
steering: false,
|
|
606
|
+
transcript_paging: false
|
|
607
|
+
}
|
|
608
|
+
) do |host, descriptor|
|
|
609
|
+
ExampleChat.new(client: host.client, storage: host.storage, descriptor: descriptor)
|
|
200
610
|
end
|
|
201
611
|
end
|
|
202
612
|
```
|
|
@@ -210,6 +620,39 @@ Open it from interactive Kward:
|
|
|
210
620
|
`id` is a stable persisted identifier: do not change it after release.
|
|
211
621
|
Use `singleton: :global` for one plugin-managed chat shared by all tab views.
|
|
212
622
|
|
|
623
|
+
### Versioned chat contract
|
|
624
|
+
|
|
625
|
+
Pass `api: 1` and `capabilities:` together to opt into the versioned plugin-chat
|
|
626
|
+
contract. The capability object supports:
|
|
627
|
+
|
|
628
|
+
- `attachments`: currently `[]` or `[:image]`;
|
|
629
|
+
- `steering`: whether the driver supports in-flight steering;
|
|
630
|
+
- `transcript_paging`: whether the driver implements `transcript_page`.
|
|
631
|
+
|
|
632
|
+
Kward validates versioned drivers when they are created. A declared driver must
|
|
633
|
+
implement `messages`, `submit`, `descriptor`, `supports_steering?`, and
|
|
634
|
+
`assistant_label`. Its `supports_steering?` result must match the declaration,
|
|
635
|
+
and a driver declaring transcript paging must implement `transcript_page`.
|
|
636
|
+
Omitting both options preserves the legacy method-probing behavior.
|
|
637
|
+
|
|
638
|
+
The factory's `host` exposes:
|
|
639
|
+
|
|
640
|
+
- `host.plugin_id` (nil for a legacy anonymous plugin) and `host.type_id`;
|
|
641
|
+
- `host.surface`, one of `:local`, `:rpc`, `:transport`, or `:shared` when the
|
|
642
|
+
same runtime can serve RPC and transports;
|
|
643
|
+
- `host.scope_key`, persisted for local tabs and stable for RPC/transport scopes;
|
|
644
|
+
- `host.capabilities`, the declared contract;
|
|
645
|
+
- `host.config`, the identified plugin's immutable private configuration;
|
|
646
|
+
- `host.storage`, isolated by plugin, chat type, and scope;
|
|
647
|
+
- `host.secret(name, env: nil)` and `host.logger`;
|
|
648
|
+
- `host.background` and `host.on_cleanup` for work owned by this chat instance.
|
|
649
|
+
|
|
650
|
+
Closing a local tab or shutting down the shared RPC/transport chat runtime calls
|
|
651
|
+
the driver's optional `close` method (or `shutdown` fallback), then cleans up the
|
|
652
|
+
host's managed resources. Scoped storage remains durable across reconstruction.
|
|
653
|
+
Legacy plugins receive the same host surface using their stable tab type ID as
|
|
654
|
+
the configuration namespace.
|
|
655
|
+
|
|
213
656
|
Plugin tabs do not notify global transcript observers by default. Set
|
|
214
657
|
`transcript_events: true` only when the tab explicitly permits its streamed
|
|
215
658
|
content to be delivered to every installed `on_transcript_event` handler, such
|
|
@@ -308,13 +751,15 @@ Handlers receive a `ctx` object. Common methods:
|
|
|
308
751
|
- `ctx.workspace_root`
|
|
309
752
|
- `ctx.args`
|
|
310
753
|
- `ctx.say(message)`
|
|
754
|
+
- `ctx.result(message:, data:)`
|
|
755
|
+
- `ctx.ui`
|
|
311
756
|
- `ctx.transcript.messages`
|
|
312
757
|
- `ctx.session_id`
|
|
313
758
|
- `ctx.session_name`
|
|
314
759
|
- `ctx.session_path`
|
|
315
760
|
- `ctx.refresh_system_message!`
|
|
316
761
|
|
|
317
|
-
These methods are available in all handler types
|
|
762
|
+
These methods are available in all handler types, including model-callable tools, although `ctx.result` is the result contract specifically for typed commands and actions. Typed command and action handlers receive their validated hash through `ctx.args`; legacy commands continue to receive text. Model-callable tools and asynchronous TUI/RPC slash commands expose `ctx.cancellation` and `ctx.cancelled?`. `ctx.say` outputs to the active frontend (terminal or RPC) wherever it is called. Interactive `ctx.ui` methods remain capability-gated because not every handler runs in a frontend context that can wait for an answer.
|
|
318
763
|
|
|
319
764
|
The transcript is read-only. Use context methods instead of mutating Kward internals.
|
|
320
765
|
|
|
@@ -324,15 +769,19 @@ Plugins are available in the CLI and RPC backend.
|
|
|
324
769
|
|
|
325
770
|
RPC clients can:
|
|
326
771
|
|
|
772
|
+
- discover plugin tools through `tools/list`,
|
|
773
|
+
- invoke plugin tools through normal model turns,
|
|
327
774
|
- list plugin commands through `commands/list`,
|
|
328
|
-
- run plugin commands through `commands/run`,
|
|
329
|
-
- run plugin slash commands through `turns/start` input such as `/hello World
|
|
775
|
+
- run plugin commands through `commands/run`, including typed object arguments and structured results,
|
|
776
|
+
- run plugin slash commands through `turns/start` input such as `/hello World`,
|
|
777
|
+
- discover and run namespaced typed actions through `pluginActions/list` and `pluginActions/run`,
|
|
778
|
+
- render and answer structured plugin UI requests advertised through `extensionUi`.
|
|
330
779
|
|
|
331
780
|
Plugin command output is emitted through normal turn events without calling the model.
|
|
332
781
|
|
|
333
782
|
## Security
|
|
334
783
|
|
|
335
|
-
Plugins are local Ruby code. They can read files, write files, run commands, make network requests, and read environment variables as your user.
|
|
784
|
+
Plugins are local Ruby code. They can read files, write files, run commands, make network requests, and read environment variables as your user. Model-callable plugin tools execute in Kward's host process and are not contained by the command sandbox; permission checks decide whether a call starts but do not sandbox trusted plugin code after it begins.
|
|
336
785
|
|
|
337
786
|
Recommended practices:
|
|
338
787
|
|