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.
Files changed (155) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +34 -0
  3. data/Gemfile.lock +2 -2
  4. data/README.md +2 -2
  5. data/doc/agent-tools.md +13 -1
  6. data/doc/api.md +17 -2
  7. data/doc/composer.md +1 -1
  8. data/doc/configuration.md +27 -4
  9. data/doc/extensibility.md +2 -1
  10. data/doc/git.md +3 -1
  11. data/doc/pan.md +6 -0
  12. data/doc/permissions.md +4 -4
  13. data/doc/plugins.md +464 -15
  14. data/doc/rpc.md +154 -16
  15. data/doc/sandboxing.md +11 -5
  16. data/doc/security.md +7 -1
  17. data/doc/session-management.md +5 -4
  18. data/doc/tabs.md +6 -2
  19. data/doc/transports.md +15 -0
  20. data/doc/usage.md +4 -1
  21. data/doc/workspace-tools.md +9 -0
  22. data/examples/plugins/space_invaders.rb +1 -1
  23. data/examples/plugins/stardate_footer.rb +2 -2
  24. data/examples/plugins/telegram/plugin.rb +1 -1
  25. data/lib/kward/agent.rb +24 -11
  26. data/lib/kward/cli/compaction.rb +9 -3
  27. data/lib/kward/cli/plugins.rb +81 -12
  28. data/lib/kward/cli/prompt_interface.rb +25 -5
  29. data/lib/kward/cli/rendering.rb +3 -0
  30. data/lib/kward/cli/runtime_helpers.rb +121 -27
  31. data/lib/kward/cli/sessions.rb +9 -5
  32. data/lib/kward/cli/settings/menus.rb +745 -0
  33. data/lib/kward/cli/settings/model.rb +327 -0
  34. data/lib/kward/cli/settings.rb +6 -1055
  35. data/lib/kward/cli/slash_commands.rb +45 -4
  36. data/lib/kward/cli/tabs.rb +167 -27
  37. data/lib/kward/{cli_transcript_formatter.rb → cli/transcript_formatter.rb} +3 -3
  38. data/lib/kward/cli/worktrees.rb +65 -2
  39. data/lib/kward/cli.rb +26 -24
  40. data/lib/kward/compactor.rb +18 -7
  41. data/lib/kward/config/core.rb +389 -0
  42. data/lib/kward/config/extensions.rb +96 -0
  43. data/lib/kward/config/prompts.rb +313 -0
  44. data/lib/kward/config/settings.rb +250 -0
  45. data/lib/kward/config_files.rb +14 -1008
  46. data/lib/kward/conversation.rb +31 -2
  47. data/lib/kward/image_attachments.rb +1 -1
  48. data/lib/kward/model/client.rb +2 -2
  49. data/lib/kward/model/copilot_models.rb +2 -2
  50. data/lib/kward/model/model_info.rb +20 -3
  51. data/lib/kward/{openrouter_model_cache.rb → model/openrouter_model_cache.rb} +3 -3
  52. data/lib/kward/model/payloads.rb +12 -3
  53. data/lib/kward/model/typesafe_client.rb +78 -0
  54. data/lib/kward/pan/server.rb +10 -7
  55. data/lib/kward/permissions/policy.rb +6 -2
  56. data/lib/kward/plugin_registry.rb +2 -659
  57. data/lib/kward/plugins/actions.rb +453 -0
  58. data/lib/kward/plugins/chat_contract.rb +121 -0
  59. data/lib/kward/{plugin_chat_runtime.rb → plugins/chat_runtime.rb} +62 -18
  60. data/lib/kward/plugins/host.rb +232 -0
  61. data/lib/kward/plugins/registry.rb +1190 -0
  62. data/lib/kward/plugins/resources.rb +206 -0
  63. data/lib/kward/plugins/turn_request.rb +36 -0
  64. data/lib/kward/plugins/ui.rb +219 -0
  65. data/lib/kward/prompt_interface/composer_renderer.rb +1 -1
  66. data/lib/kward/prompt_interface/composer_state.rb +1 -1
  67. data/lib/kward/prompt_interface/editor/controller.rb +1 -1
  68. data/lib/kward/prompt_interface/editor/runner.rb +1 -1
  69. data/lib/kward/prompt_interface/editor/state.rb +1 -1
  70. data/lib/kward/prompt_interface/layout.rb +1 -1
  71. data/lib/kward/prompt_interface/overlay_renderer.rb +1 -1
  72. data/lib/kward/prompt_interface/plugin_ui_requests.rb +82 -0
  73. data/lib/kward/prompt_interface/runtime_state.rb +6 -1
  74. data/lib/kward/prompt_interface/screen.rb +9 -2
  75. data/lib/kward/prompt_interface.rb +60 -11
  76. data/lib/kward/prompts/commands.rb +2 -1
  77. data/lib/kward/{adaptive_pty_output_sink.rb → pty/adaptive_output_sink.rb} +1 -1
  78. data/lib/kward/{interactive_pty_runner.rb → pty/interactive_runner.rb} +2 -2
  79. data/lib/kward/{local_command_runner.rb → pty/local_command_runner.rb} +1 -1
  80. data/lib/kward/{local_pty_command_runner.rb → pty/local_pty_runner.rb} +3 -9
  81. data/lib/kward/{pty_output_sink.rb → pty/output_sink.rb} +9 -4
  82. data/lib/kward/{pty_transcript_normalizer.rb → pty/transcript_normalizer.rb} +1 -1
  83. data/lib/kward/rpc/plugin_chat_manager.rb +30 -10
  84. data/lib/kward/rpc/prompt_bridge.rb +25 -0
  85. data/lib/kward/rpc/server.rb +91 -12
  86. data/lib/kward/rpc/session_manager.rb +147 -44
  87. data/lib/kward/rpc/session_tree_rows.rb +2 -2
  88. data/lib/kward/rpc/tool_metadata.rb +1 -1
  89. data/lib/kward/sandbox/command_runner.rb +1 -1
  90. data/lib/kward/{session_catalog.rb → sessions/catalog.rb} +1 -1
  91. data/lib/kward/{session_store.rb → sessions/store.rb} +9 -9
  92. data/lib/kward/{session_tree_nodes.rb → sessions/tree_nodes.rb} +3 -3
  93. data/lib/kward/{session_tree_renderer.rb → sessions/tree_renderer.rb} +4 -4
  94. data/lib/kward/{session_tree_tool_display.rb → sessions/tree_tool_display.rb} +1 -1
  95. data/lib/kward/{kwsh.rb → shell/kwsh.rb} +3 -3
  96. data/lib/kward/{persistent_shell_session.rb → shell/persistent_session.rb} +3 -3
  97. data/lib/kward/{shell_prompt_session.rb → shell/prompt_session.rb} +1 -1
  98. data/lib/kward/skills/trust_store.rb +1 -1
  99. data/lib/kward/tabs/driver.rb +194 -0
  100. data/lib/kward/{tab_store.rb → tabs/store.rb} +2 -2
  101. data/lib/kward/{clipboard.rb → terminal/clipboard.rb} +1 -1
  102. data/lib/kward/{terminal_image_support.rb → terminal/image_support.rb} +1 -1
  103. data/lib/kward/tools/base.rb +18 -0
  104. data/lib/kward/tools/context_for_task.rb +15 -6
  105. data/lib/kward/tools/edit_file.rb +9 -6
  106. data/lib/kward/tools/git_commit.rb +13 -7
  107. data/lib/kward/tools/list_directory.rb +4 -4
  108. data/lib/kward/tools/plugin_tool.rb +41 -0
  109. data/lib/kward/tools/prepare_shell_command.rb +1 -1
  110. data/lib/kward/tools/read_file.rb +7 -6
  111. data/lib/kward/tools/registry.rb +96 -13
  112. data/lib/kward/tools/run_shell_command.rb +10 -8
  113. data/lib/kward/tools/search/code.rb +1 -1
  114. data/lib/kward/tools/summarize_file_structure.rb +5 -5
  115. data/lib/kward/tools/tool_call.rb +2 -1
  116. data/lib/kward/tools/typesafe_evaluate.rb +81 -0
  117. data/lib/kward/tools/workspace_targets.rb +58 -0
  118. data/lib/kward/tools/write_file.rb +9 -6
  119. data/lib/kward/{export_path.rb → transcripts/export_path.rb} +1 -1
  120. data/lib/kward/{markdown_transcript.rb → transcripts/markdown_transcript.rb} +2 -2
  121. data/lib/kward/transport/contracts.rb +200 -0
  122. data/lib/kward/transport/gateway.rb +79 -35
  123. data/lib/kward/transport/plugin_chat_gateway.rb +3 -2
  124. data/lib/kward/transport.rb +1 -200
  125. data/lib/kward/version.rb +1 -1
  126. data/lib/kward/{workspace_factory.rb → workspace/factory.rb} +2 -2
  127. data/lib/kward/{git_worktree_manager.rb → workspace/git_worktree_manager.rb} +28 -0
  128. data/lib/kward/{workspace.rb → workspace/workspace.rb} +3 -3
  129. data/templates/default/fulldoc/html/css/kward.css +125 -0
  130. data/templates/default/fulldoc/html/images/kward_screen_1.png +0 -0
  131. data/templates/default/fulldoc/html/setup.rb +1 -1
  132. data/templates/default/layout/html/layout.erb +16 -4
  133. metadata +67 -48
  134. data/lib/kward/tab_driver.rb +0 -90
  135. data/templates/default/fulldoc/html/images/kward_workflow.svg +0 -52
  136. /data/lib/kward/{editor_prompt.rb → cli/editor_prompt.rb} +0 -0
  137. /data/lib/kward/{editor_prompt_session.rb → cli/editor_prompt_session.rb} +0 -0
  138. /data/lib/kward/{diff_view_mode.rb → prompt_interface/editor/diff_view_mode.rb} +0 -0
  139. /data/lib/kward/{editor_mode.rb → prompt_interface/editor/editor_mode.rb} +0 -0
  140. /data/lib/kward/{markdown_code_block.rb → prompt_interface/editor/markdown_code_block.rb} +0 -0
  141. /data/lib/kward/{scratchpad_languages.rb → prompt_interface/editor/scratchpad_languages.rb} +0 -0
  142. /data/lib/kward/{scratchpad_runner.rb → prompt_interface/editor/scratchpad_runner.rb} +0 -0
  143. /data/lib/kward/{detached_run.rb → pty/detached_run.rb} +0 -0
  144. /data/lib/kward/{session_diff.rb → sessions/diff.rb} +0 -0
  145. /data/lib/kward/{session_naming.rb → sessions/naming.rb} +0 -0
  146. /data/lib/kward/{session_trash.rb → sessions/trash.rb} +0 -0
  147. /data/lib/kward/{kwshrc.rb → shell/kwshrc.rb} +0 -0
  148. /data/lib/kward/{shell_prompt.rb → shell/prompt.rb} +0 -0
  149. /data/lib/kward/{ansi.rb → terminal/ansi.rb} +0 -0
  150. /data/lib/kward/{terminal_keys.rb → terminal/keys.rb} +0 -0
  151. /data/lib/kward/{terminal_sequences.rb → terminal/sequences.rb} +0 -0
  152. /data/lib/kward/{terminal_text.rb → terminal/text.rb} +0 -0
  153. /data/lib/kward/{transcript_export.rb → transcripts/transcript_export.rb} +0 -0
  154. /data/lib/kward/{project_files.rb → workspace/files.rb} +0 -0
  155. /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 a footer
472
+ ## Add status to the footer
108
473
 
109
- A footer can show compact local status in the terminal UI:
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'} • #{ctx.transcript.messages.length} messages"
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, storage, model behavior, and any global state; it does not
194
- need to use a Kward workspace session.
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 "example", id: "com.example.chat", title: "Example", singleton: :global do |host, descriptor|
199
- ExampleChat.new(client: host.client, descriptor: descriptor)
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: commands, footers, prompt context renderers, and transcript event observers. `ctx.say` outputs to the active frontend (terminal or RPC) wherever it is called.
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