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/rpc.md CHANGED
@@ -15,7 +15,7 @@ A typical client launches the server, sends `initialize`, creates or resumes a s
15
15
  | Connect and negotiate capabilities | [Launch](#Launch), [Framing](#Framing), and [Initialization](#Initialization) |
16
16
  | Open, resume, branch, or export a conversation | [Session methods](#Session_methods) |
17
17
  | Send input and render a response | [Turn methods](#Turn_methods) and [Turn notifications](#Turn_notifications) |
18
- | Handle tool approval or structured questions | [Tool approval bridge](#Tool_approval_bridge) and [UI question bridge](#UI_question_bridge) |
18
+ | Handle tool approval or structured plugin UI | [Tool approval bridge](#Tool_approval_bridge) and [Structured UI bridge](#Structured_UI_bridge) |
19
19
  | Show model, runtime, auth, or configuration controls | [Runtime methods](#Runtime_methods), [Model methods](#Model_methods), and [Config and auth methods](#Config_and_auth_methods) |
20
20
  | Discover tools, MCP servers, prompts, skills, or plugins | [Tool and prompt methods](#Tool_and_prompt_methods) |
21
21
 
@@ -65,8 +65,10 @@ Read `capabilities` at runtime instead of assuming every feature is available. I
65
65
  - `transcript`: Kward transcript format support, including normalized messages, image/tool support, compaction summaries, and restored assistant reasoning as Pi-compatible `thinking` content blocks.
66
66
  - `sessions`: explicit RPC session mode, JSONL persistence, and methods for listing, auto-resume, live-session discovery, linear forking, compaction, and labeled tree navigation with branch summaries. Import is unsupported. Live session updates are also unsupported but reserve the `session/updated` notification name. Git worktree bindings are reported as interactive-TUI-only.
67
67
  - `turns`: asynchronous turns, per-session concurrency, active and recent turn lists, busy-input steering when the provider supports it, queued follow-ups, best-effort cancellation, and recent in-memory event replay. Per-turn options cover model, reasoning, tool scope, and tool approval, with structured client context for editor integrations.
68
- - `pluginChats`: optional plugin-owned chats. The capability lists opted-in chat types and methods. Clients must explicitly subscribe before receiving `pluginChat/event` notifications; plugin chats are independent from workspace sessions. A type may also report `transport: true` when a trusted external transport is allowed to target it; RPC opt-in and external transport opt-in remain separate.
69
- - `events`: the `turn/event` contract, assistant and reasoning events, normalized tool metadata, tool updates and results, diff support, workspace guardrail status, focused-context and context-budget statistics tools, and explicitly unsupported shell changed-file and session-update flags.
68
+ - `plugins`: the supported plugin API version and public `id`, `version`, and `apiVersion` metadata for identified plugins. Private plugin configuration is never included.
69
+ - `pluginTools`: model-callable tools registered by trusted local plugins, including the registered count, `tools/list` discovery, execution-profile filtering, and permission-policy enforcement.
70
+ - `pluginChats`: optional plugin-owned chats. The capability lists opted-in chat types and methods. Versioned types report declared attachment, steering, and transcript-paging capabilities. Clients must explicitly subscribe before receiving `pluginChat/event` notifications; plugin chats are independent from workspace sessions. A type may also report `transport: true` when a trusted external transport is allowed to target it; RPC opt-in and external transport opt-in remain separate.
71
+ - `events`: the `turn/event` contract, assistant and reasoning events, typed plugin-command results, normalized tool metadata, tool updates and results, diff support, workspace guardrail status, focused-context and context-budget statistics tools, and explicitly unsupported shell changed-file and session-update flags.
70
72
  - `attachments`: supported input attachment contract for `turns/start`, with accepted base64 image MIME types and a stable max byte value.
71
73
  - `models`: model listing, refresh, selection, and exposed metadata across providers. Scoped model selection is not supported.
72
74
  - `runtime`: runtime state, message-count statistics, and OpenAI/Codex context usage. Kward does not yet compute cumulative token or cost statistics.
@@ -74,12 +76,13 @@ Read `capabilities` at runtime instead of assuming every feature is available. I
74
76
  - `runtimeSettings`: live `runtime/updateSetting` support for `defaultModel` and `defaultThinkingLevel`, plus `runtime/reload`.
75
77
  - `auth`: available providers and authentication methods, private API-key storage, sanitized status, and logout. OpenAI and Anthropic OAuth are supported; Copilot OAuth is CLI-only, OpenRouter PKCE is not implemented, and xAI has no supported stable third-party flow.
76
78
  - `memory`: opt-in structured memory support, interactive prompt injection only, JSON/JSONL local storage, and dedicated `memory/*` methods.
77
- - `commands`: supported `commands/list` capability for prompt, skill, and plugin command sources, plus plugin execution through `commands/run` or plugin slash turns.
79
+ - `commands`: supported `commands/list` capability for prompt, skill, and plugin command sources, plus plugin execution through `commands/run` or plugin slash turns. Plugin commands may advertise validated JSON Schema arguments, shell-style text parsing, and structured results while legacy raw-string commands remain supported.
80
+ - `pluginActions`: namespaced typed actions discovered through `pluginActions/list` and synchronously invoked through `pluginActions/run`. They use object arguments and structured results, are not local TUI commands, and cannot make blocking UI requests.
78
81
  - `skillCapture`: capture a reviewed personal `SKILL.md` from any saved session’s active branch through `skills/captureSessions`, `skills/captureDraft`, and `skills/saveCapturedDraft`.
79
82
  - `projectSkillTrust`: explicitly unsupported over RPC. RPC clients cannot answer the interactive Allow/Deny/Review decision, so project skills remain skipped unless the global `skills.trust_project` override is enabled.
80
83
  - `mcp`: local stdio MCP server support through the shared `mcpServers` config. RPC exposes MCP tools to turns and advertises discovery with `methods: ["tools/list", "mcp/status"]`, `toolMetadata: true`, and `serverStatus: true`. It does not support MCP resources, prompts, sampling, or Streamable HTTP.
81
84
  - `startupResources`: supported startup resource listing for context, skills, prompts, and plugins.
82
- - `extensionUi`: question bridge support via `ui/question` and `ui/answerQuestion`, plus plugin footer updates via `ui/footer`; other UI primitives are explicitly unsupported.
85
+ - `extensionUi`: structured question support plus plugin `select`, `confirm`, `input`, `progress`, and `notify` UI through `ui/request`, `ui/answerRequest`, `ui/progress`, and `ui/notification`; plugin footer updates use `ui/footer`. Editor, widget, custom-canvas, and raw terminal-input primitives remain explicitly unsupported.
83
86
  - `composer`: composer-only UI features. Interactive session diff totals are explicitly unsupported over RPC (`composer.sessionDiff.supported: false`) because RPC clients already receive per-tool diff results and no live composer status payload is exposed. Clipboard copy is also unsupported over RPC (`composer.copy.supported: false`) because UI clients own clipboard access. Vibe editor prompts are unsupported over RPC (`composer.editorPrompt.supported: false`) because RPC has no live integrated editor buffer.
84
87
  - `security`: trusted-local behavior and optional per-turn tool approval. By default, RPC turns have no workspace mutation guard or tool approval, so shell commands and file changes can run. Clients can inspect file-tool guardrails through `capabilities.events.tools.workspaceGuardrails` and `runtime/state.workspaceGuardrailsEnabled`. `security.sandbox` reports the command sandbox mode, enforcement backend, and filesystem and network capabilities; session pinning and one-time elevation are unsupported. See [Command sandboxing](sandboxing.md) for the boundary and its limits.
85
88
  - `export`: supported transcript export formats. Currently `markdown` and `html`; default is `markdown`.
@@ -294,7 +297,11 @@ Returns `{ "session": {}, "editorText": "...", "cancelled": false, "aborted": fa
294
297
 
295
298
  ## Plugin chat methods
296
299
 
297
- Plugin chats are optional trusted-plugin capabilities, not Kward workspace sessions. When `initialize.capabilities.pluginChats.supported` is true, use `pluginChats/list` to discover available types.
300
+ Plugin chats are optional trusted-plugin capabilities, not Kward workspace
301
+ sessions. When `initialize.capabilities.pluginChats.supported` is true, use
302
+ `pluginChats/list` to discover available types. Types using the versioned chat
303
+ contract also report `apiVersion` and `capabilities` with `attachments`,
304
+ `steering`, and `transcriptPaging`; legacy types omit those fields.
298
305
 
299
306
  ### `pluginChats/open`
300
307
 
@@ -330,7 +337,10 @@ Params:
330
337
  - `input`;
331
338
  - `attachments`: optional base64 image attachments using the same MIME and size limits as `turns/start`.
332
339
 
333
- Queues a plugin-chat turn and returns `{ id, chatId, status, ... }`. Plugin chat turns are serialized per chat and do not use workspace sessions, agents, or model overrides.
340
+ Queues a plugin-chat turn and returns `{ id, chatId, status, ... }`. Plugin chat
341
+ turns are serialized per chat and do not use workspace sessions, agents, or
342
+ model overrides. A versioned chat whose declared `attachments` capability omits
343
+ `image` rejects image attachments before invoking its driver.
334
344
 
335
345
  ### `pluginChats/turns/cancel`, `pluginChats/turns/status`, `pluginChats/turns/events`, `pluginChats/turns/list`, `pluginChats/turns/listActive`
336
346
 
@@ -439,6 +449,7 @@ Known event types:
439
449
  - `toolCall`
440
450
  - `toolUpdate`
441
451
  - `toolResult`
452
+ - `pluginCommandResult`
442
453
  - `answer`
443
454
  - `turnCancelRequested`
444
455
  - `error`
@@ -462,6 +473,10 @@ Lifecycle payloads include `status` for `turnQueued`, `turnStarted`, and `turnFi
462
473
 
463
474
  `toolResult` additionally includes `result` with `content`, `isError`, optional unified `diff`, optional `changedFiles`, and `images`. Failed or declined tools set `isError: true`.
464
475
 
476
+ A typed plugin slash command emits `pluginCommandResult` with `command` and a
477
+ structured `result` containing optional `message` and `data` fields. It then
478
+ emits the usual text events when the command produced user-facing output.
479
+
465
480
  Examples:
466
481
 
467
482
  - `edit_file`: `toolName: "edit"`, `args: { "path": "...", "edits": [{ "oldText": "...", "newText": "..." }] }`.
@@ -490,11 +505,11 @@ Params:
490
505
 
491
506
  Denied tools are returned to the model as error-like tool results instead of executing the local operation.
492
507
 
493
- ## UI question bridge
508
+ ## Structured UI bridge
494
509
 
495
- Kward supports the structured question bridge and plugin footer updates over RPC. The `extensionUi` capability reports `question.supported: true` with `notification: "ui/question"`, `method: "ui/answerQuestion"`, `maxQuestions: 4`, `multiSelect: false`, and `preview: false`. It also reports `footer.supported: true` with `notification: "ui/footer"`. Other Pi-style extension UI primitives (`select`, `confirm`, `input`, `editor`, `widgets`, `custom`, and `terminalInput`) are explicitly reported as unsupported until Kward has a real plugin/extension consumer for them.
510
+ Kward supports model questions and frontend-neutral plugin UI over RPC. The `extensionUi` capability reports the notification and answer method for each operation. `question` continues to use `ui/question` and `ui/answerQuestion`; plugin `select`, `confirm`, and `input` requests use `ui/request` and `ui/answerRequest`. Non-blocking plugin updates use `ui/progress` and `ui/notification`, while plugin footers use `ui/footer`. Editor, widget, custom-canvas, and raw terminal-input primitives remain explicitly unsupported.
496
511
 
497
- Question requests are validated before notification. Kward accepts 1-4 questions, each with 2-4 options, and rejects unsupported `multiSelect` or option `preview` requests.
512
+ Question requests are validated before notification. Kward accepts 1-4 questions, each with 2-4 options, and rejects unsupported `multiSelect` or option `preview` requests. Plugin selections accept at most 100 options, and plugin text input is bounded to 16,384 bytes.
498
513
 
499
514
  When the model calls `ask_user_question`, RPC emits a `ui/question` notification:
500
515
 
@@ -506,16 +521,37 @@ When the model calls `ask_user_question`, RPC emits a `ui/question` notification
506
521
  }
507
522
  ```
508
523
 
509
- When a loaded Kward plugin registers a footer, RPC emits `ui/footer` after session creation/resume/clone and after each completed turn:
524
+ When loaded Kward plugins contribute footer status, RPC emits `ui/footer` after
525
+ session creation/resume/clone, after each completed turn, and when the status
526
+ changes during its periodic refresh:
510
527
 
511
528
  ```json
512
529
  {
513
530
  "sessionId": "...",
514
- "text": "custom footer text"
531
+ "text": "Bridge · 2 messages",
532
+ "segments": [
533
+ {
534
+ "id": "com.example.session/session",
535
+ "text": "Bridge",
536
+ "tooltip": "Current Kward session",
537
+ "priority": "high",
538
+ "order": 10
539
+ },
540
+ {
541
+ "id": "com.example.session/messages",
542
+ "text": "2 messages",
543
+ "priority": "low",
544
+ "order": 20
545
+ }
546
+ ]
515
547
  }
516
548
  ```
517
549
 
518
- An empty `text` value clears the client footer.
550
+ `text` is the combined fallback for clients that do not render segments.
551
+ `segments` remains display-ordered and lets richer clients show tooltips or
552
+ apply their own width policy. An empty `text` with an empty `segments` array
553
+ clears the client footer. The `extensionUi.footer` capability advertises
554
+ segment, tooltip, and priority support.
519
555
 
520
556
  The UI must respond with `ui/answerQuestion`:
521
557
 
@@ -525,6 +561,40 @@ Params:
525
561
  - `questionRequestId`
526
562
  - `answers`: answer array returned to the tool.
527
563
 
564
+ When a plugin calls `ctx.ui.select`, `ctx.ui.confirm`, or `ctx.ui.input` during an asynchronous plugin-command turn or plugin tool call, RPC emits:
565
+
566
+ ```json
567
+ {
568
+ "method": "ui/request",
569
+ "params": {
570
+ "sessionId": "...",
571
+ "requestId": "...",
572
+ "kind": "select",
573
+ "payload": {
574
+ "title": "Environment",
575
+ "message": "Choose a target",
576
+ "options": [
577
+ { "label": "Staging", "value": "staging", "description": "Deploy for testing." }
578
+ ]
579
+ }
580
+ }
581
+ }
582
+ ```
583
+
584
+ Answer it with `ui/answerRequest`:
585
+
586
+ ```json
587
+ {
588
+ "sessionId": "...",
589
+ "requestId": "...",
590
+ "value": "staging"
591
+ }
592
+ ```
593
+
594
+ For `confirm`, `value` must be a boolean. For `input`, it is a string or `null` when cancelled. For `select`, it is one of the advertised option values or `null`. Submit interactive plugin commands through `turns/start`; synchronous `commands/run` deliberately reports blocking plugin UI as unavailable so the RPC reader never waits for an answer it cannot read.
595
+
596
+ `ctx.ui.progress` emits `ui/progress` with `sessionId`, stable `id`, `message`, optional `percent`, and `done`. `ctx.ui.notify` emits `ui/notification` with `sessionId`, `message`, and `level` (`info`, `success`, `warning`, or `error`). These notifications do not require answers.
597
+
528
598
  ## Runtime methods
529
599
 
530
600
  ### `runtime/state`
@@ -737,11 +807,11 @@ Params:
737
807
 
738
808
  Returns current tool schemas. The existing model-facing schema shape is preserved: each entry still has `type: "function"` and `function` with `name`, `description`, and `parameters`. Entries also include additive metadata for UI discovery:
739
809
 
740
- - `metadata.source`: one of practical source labels such as `builtin`, `mcp`, `web`, `skill`, `ui`, or `unknown`.
810
+ - `metadata.source`: one of practical source labels such as `builtin`, `plugin`, `mcp`, `web`, `skill`, `ui`, or `unknown`.
741
811
  - `metadata.displayName`: human-readable tool label.
742
812
  - MCP tools also include `metadata.serverName` and `metadata.remoteName`. The callable name remains sanitized with a double underscore, for example `github__search_issues`, while `displayName` is `github.search_issues`.
743
813
 
744
- Clients that only read `tools[].function` remain compatible.
814
+ Clients that only read `tools[].function` remain compatible. Model-callable tools registered by trusted local plugins appear here with `metadata.source: "plugin"` and follow the same per-session execution-profile filtering as built-in tools.
745
815
 
746
816
  ### `mcp/status`
747
817
 
@@ -766,7 +836,74 @@ Params:
766
836
 
767
837
  - `sessionId`: active RPC session ID.
768
838
 
769
- Returns frontend-neutral slash command metadata for configured prompt templates, skills, and plugins. Prompt command names omit the leading slash. Skill command names use `skill:<name>`. Plugin command names omit the leading slash and include `executable: true`. Builtin terminal-only commands are omitted. Prompt commands can be submitted directly to `turns/start` as slash commands or expanded first with `prompts/expand`; plugin commands can be submitted to `turns/start` or run explicitly with `commands/run`.
839
+ Returns frontend-neutral slash command metadata for configured prompt templates, skills, and plugins. Prompt command names omit the leading slash. Skill command names use `skill:<name>`. Plugin command names omit the leading slash and include `executable: true`. Typed plugin entries additionally include `typed: true`, their strict object `schema`, declared `positionals`, and `pluginId` when the plugin has stable identity. Builtin terminal-only commands are omitted. Prompt commands can be submitted directly to `turns/start` as slash commands or expanded first with `prompts/expand`; plugin commands can be submitted to `turns/start` or run explicitly with `commands/run`.
840
+
841
+ ### `commands/run`
842
+
843
+ Params:
844
+
845
+ - `sessionId`: active RPC session ID.
846
+ - `name`: plugin command name, with or without the leading slash.
847
+ - `arguments`: shell-style text or, for typed commands, an object matching the advertised schema.
848
+
849
+ Legacy plugin commands return their existing string-or-null `result`. Typed
850
+ commands return `result` as an object containing optional user-facing `message`
851
+ and JSON-compatible `data`; text emitted through `ctx.say` remains in `output`.
852
+ Blocking structured UI requests are unavailable on this synchronous path. Use a
853
+ slash-command `turns/start` call when the command needs interactive UI.
854
+
855
+ Plugin commands can also stage a model response with `ctx.request_turn(system: text)`.
856
+ Submit these through `turns/start`, for example:
857
+
858
+ ```json
859
+ {"sessionId":"session-id","input":"/iddqd Answer in French and explain the current design."}
860
+ ```
861
+
862
+ The same turn emits the normal model, tool, answer, error, and completion events.
863
+ The host retains cancellation, execution-profile restrictions, and tool approvals.
864
+ The instructions supplement the system prompt for this turn only; they are not
865
+ submitted as user text and are not reactivated by restoring the session.
866
+ `commands/run` and `pluginActions/run` reject model-turn requests. Discover this
867
+ contract under `initialize.capabilities.commands.modelTurns`, including the
868
+ 65,536-byte limit, `scope: "turn"`, and `synchronous: false`.
869
+ `inputRole: "system"` describes Kward's internal instruction role;
870
+ `codexInputRole: "developer"` reports the Codex subscription backend's supported
871
+ wire role. Codex receives the command as a new ordered developer message, while
872
+ direct OpenAI Responses receives an ordered system message. Base instructions
873
+ stay separate from these turn-scoped input messages.
874
+
875
+ The invocation is saved as a user-visible history entry with `display_content`
876
+ and `plugin_system_turn` metadata (command, plugin ID, instruction text, request ID,
877
+ and scope). Later model requests omit these history entries; compaction uses an
878
+ informational placeholder, not the expired instructions. Anthropic and Gemini
879
+ use separate system fields rather than ordered system messages and require
880
+ existing dialogue; a system-only request in an empty session fails explicitly
881
+ rather than inserting a synthetic user prompt.
882
+
883
+ ### `pluginActions/list`
884
+
885
+ Params:
886
+
887
+ - `sessionId`: active RPC session ID.
888
+
889
+ Returns identified-plugin actions with `id`, local `name`, `pluginId`,
890
+ `description`, and strict object `schema`. Action IDs use
891
+ `<plugin-id>/<action-name>`.
892
+
893
+ ### `pluginActions/run`
894
+
895
+ Params:
896
+
897
+ - `sessionId`: active RPC session ID.
898
+ - `id`: complete namespaced action ID.
899
+ - `arguments`: object matching the action schema; defaults to an empty object.
900
+
901
+ Returns `action`, `output`, and a structured `result` with optional `message` and
902
+ `data`. Actions run synchronously and therefore expose notifications and progress
903
+ but not blocking questions, selections, confirmations, or input. They are
904
+ rejected when the session execution profile disables plugin commands. Actions
905
+ are intentionally not exposed in local slash completion; a plugin should also
906
+ register a typed command when it needs a human-facing TUI entry point.
770
907
 
771
908
  ### `skills/captureSessions`, `skills/captureDraft`, and `skills/saveCapturedDraft`
772
909
 
@@ -884,4 +1021,5 @@ Returns login status for a login ID.
884
1021
  - RPC is intended for a trusted local UI and can read/write files, run shell commands, update secrets, and use OAuth.
885
1022
  - Workspace roots may be any existing local directory accessible to the process.
886
1023
  - Tool execution matches current CLI behavior; mutating tools are not approval-gated by RPC.
1024
+ - `commands/run` and `pluginActions/run` execute trusted local plugin code. Action schemas validate data shape but are not an authorization boundary; use execution profiles to disable plugin operations for restricted sessions.
887
1025
  - Responses and diagnostics redact secret-looking fields, but clients should still avoid logging full protocol traffic unless necessary.
data/doc/sandboxing.md CHANGED
@@ -67,9 +67,12 @@ after changing the mode.
67
67
 
68
68
  An active worktree-backed tab uses a strict `workspace_write` policy for its
69
69
  model-requested command workers, regardless of the global `sandbox.mode` or
70
- `tools.workspace_guardrails` settings. The writable root is exactly the linked
71
- worktree and no configured additional writable roots are carried into the tab.
72
- The configured child-network setting is preserved.
70
+ `tools.workspace_guardrails` settings. The default writable root is exactly the
71
+ linked worktree and no configured additional writable roots are carried into the
72
+ tab. When a workspace tool explicitly selects the verified `origin` target,
73
+ Kward uses a separate strict command worker whose writable root is exactly that
74
+ original worktree. Arbitrary target paths are not accepted. The configured
75
+ child-network setting is preserved.
73
76
 
74
77
  If the current platform cannot provide filesystem enforcement, Kward refuses to
75
78
  activate the worktree instead of falling back to an unrestricted command
@@ -82,8 +85,11 @@ This does not contain the user-directed `/shell`, `!command`, `/capture`, or
82
85
  model-requested shell commands still cannot write Git metadata. Active worktree tabs additionally
83
86
  expose a narrow `git_commit` tool for explicit agent-requested commits; it runs
84
87
  through the trusted host-side Git workflow rather than widening the shell
85
- sandbox. The interactive `/git` flow remains available for manual review and
86
- commit.
88
+ sandbox. The verified original worktree is available as `target: "origin"` on
89
+ core workspace tools so an agent can inspect and resolve merge conflicts without
90
+ switching tabs. Selecting that target does not add an approval layer; the normal
91
+ configured tool-permission policy applies equally to both worktrees. The
92
+ interactive `/git` flow remains available for manual review and commit.
87
93
 
88
94
  ## Platform support
89
95
 
data/doc/security.md CHANGED
@@ -87,7 +87,7 @@ See [Lifecycle hooks](lifecycle-hooks.md) for policy examples and [RPC protocol]
87
87
 
88
88
  ## Opt-in permission policy
89
89
 
90
- `permissions.enabled` is off by default. When enabled, Kward can allow, ask, or deny model-requested file changes, shell commands, web tools, and MCP tools before execution. The interactive CLI uses an approval overlay; an unavailable approval bridge fails closed. The policy is not a sandbox: after Kward permits a shell command, it still runs with the permissions of your user account. See [Permissions](permissions.md) for workflows, modes, rules, and limits.
90
+ `permissions.enabled` is off by default. When enabled, Kward can allow, ask, or deny model-requested file changes, shell commands, web tools, MCP tools, and plugin tools before execution. The interactive CLI uses an approval overlay; an unavailable approval bridge fails closed. The policy is not a sandbox: after Kward permits a shell command, it still runs with the permissions of your user account. See [Permissions](permissions.md) for workflows, modes, rules, and limits.
91
91
 
92
92
  ## Know what you are trusting
93
93
 
@@ -113,6 +113,11 @@ They can read files and environment variables, write files, run commands, and
113
113
  make network requests. Kward intentionally does not load plugins from a
114
114
  workspace directory.
115
115
 
116
+ Identified-plugin configuration and state remain local to the active config
117
+ directory. RPC capability reports include only plugin IDs and versions, never
118
+ plugin configuration or secret values. Treat `host.logger` output as
119
+ user-visible diagnostics and never write credentials to it.
120
+
116
121
  ### MCP servers
117
122
 
118
123
  Configured MCP servers are local child processes. Their tools can expose application state such as browser pages, console messages, network traffic, or screenshots. Kward currently supports local stdio servers, but local transport does not make an untrusted server safe.
@@ -168,6 +173,7 @@ Kward keeps user data under `~/.kward` by default, or mostly beside `KWARD_CONFI
168
173
  | Memory | `~/.kward/memory/` | Off by default; directory `0700` and files `0600`. |
169
174
  | Telemetry logs | `~/.kward/logs/` | Off by default; redacted metadata, not intentional prompt or file-content logging. |
170
175
  | Plugins | `~/.kward/plugins/` | Trusted Ruby code, not private data storage. |
176
+ | Plugin state | `~/.kward/plugin_state/` | Namespaced by stable plugin ID; state files use mode `0600`. |
171
177
  | Hook audit log and trust records | `~/.kward/logs/`, `~/.kward/trusted_workspace_hooks.json` | Audit records use redacted metadata rather than raw event values. |
172
178
 
173
179
  Private file modes help on normal Unix-like systems but do not protect data from your own account, privileged users, backups, malware, or a compromised machine. Session exports are written to the path you choose and should be protected separately.
@@ -25,13 +25,13 @@ To automatically resume the last active session for the current workspace on sta
25
25
 
26
26
  ## A normal session workflow
27
27
 
28
- Start a chat and give it a useful name:
28
+ Start a chat and give the current session and tab a useful name:
29
29
 
30
30
  ```text
31
- /rename oauth cleanup
31
+ /name oauth cleanup
32
32
  ```
33
33
 
34
- `/rename` requires a name. Use `/session name` without an argument to clear the current session name.
34
+ `/name` requires a name and updates both the active session and its tab label. It can also be used while the active tab's agent is running; it is unavailable in plugin tabs. Use `/rename` when you only want to rename the session. Use `/session name` without an argument to clear the current session name.
35
35
 
36
36
  Work normally:
37
37
 
@@ -227,7 +227,8 @@ This is useful when you want to confirm which session you are in or check whethe
227
227
  | Need | Use |
228
228
  | --- | --- |
229
229
  | Continue earlier work | `/session` or `/resume` |
230
- | Give the current session a better name | `/rename <name>` |
230
+ | Give the current session and tab a better name | `/name <name>` |
231
+ | Give only the current session a better name | `/rename <name>` |
231
232
  | Clear the current session name | `/session name` |
232
233
  | Keep the current state but try another future | `/clone` |
233
234
  | Start a separate session from before an earlier prompt | `/fork` |
data/doc/tabs.md CHANGED
@@ -59,11 +59,13 @@ Inspect, merge, or remove the binding explicitly:
59
59
  /tab worktree remove
60
60
  ```
61
61
 
62
- `/tab worktree merge` merges the active worktree's clean, committed branch directly into the branch currently checked out in its original workspace. Kward shows the source and target revisions and requires confirmation. Both worktrees must be clean. If Git reports conflicts, Kward leaves the original workspace in its normal merge state; resolve the conflicts there or cancel them with `/tab worktree merge abort`.
62
+ `/tab worktree merge` merges the active worktree's clean, committed branch directly into the branch currently checked out in its original workspace. Kward shows the source and target revisions and requires confirmation. Both worktrees must be clean. If Git reports conflicts, Kward leaves the original workspace in its normal merge state while keeping the current tab on the linked branch. The composer shows the target branch and conflict count. Run `/tab worktree merge resolve` (or `/worktree merge resolve`) to start an agent turn that inspects and edits the verified original workspace with `target: "origin"`; the turn leaves committing for review. After the conflicts are resolved, run `/tab worktree merge continue` to stage the resolutions and complete the merge, or use `/tab worktree merge abort` to cancel it.
63
63
 
64
64
  Removal refuses a dirty worktree and keeps its branch. A worktree that is missing or no longer points at the recorded branch is restored as unavailable rather than silently falling back to the original workspace.
65
65
 
66
- Worktree tabs are available for normal session tabs, not plugin-owned tabs. Kward's file tools, `@`/`$` completion, `/files` browser, integrated editor, and model-requested shell workers use the active worktree root. Model operations retain strict workspace guardrails. The user-directed `/shell`, `!command`, `/capture`, and `/pty` features remain host-process operations and are not contained by the model command sandbox; use them only when that is intentional. Generic shell Git writes remain protected. When explicitly asked to commit, the agent can use the active tab's narrow `git_commit` tool; use the interactive `/git` flow when you want to review and commit changes yourself.
66
+ Worktree tabs are available for normal session tabs, not plugin-owned tabs. Kward's file tools, `@`/`$` completion, `/files` browser, integrated editor, and model-requested shell workers use the active worktree root by default. Core model workspace tools advertise an optional `target` selector in an active worktree tab: `active` keeps the normal linked-worktree root, while `origin` selects only that tab's verified original repository worktree. Selecting `origin` does not add a separate approval prompt; the normal configured tool-permission policy applies equally to both targets. Paths remain relative to the selected root, and arbitrary filesystem targets are never accepted.
67
+
68
+ Both targets retain strict workspace guardrails and separate command sandboxes. The user-directed `/shell`, `!command`, `/capture`, and `/pty` features remain host-process operations and are not contained by the model command sandbox; use them only when that is intentional. Generic shell Git writes remain protected. When explicitly asked to commit, the agent can use `git_commit` for the active worktree or set `target: "origin"` for the original worktree; use the interactive `/git` flow when you want to review and commit changes yourself.
67
69
 
68
70
  ## Common workflow
69
71
 
@@ -98,6 +100,8 @@ Tabs keep the conversations separate, so context from one tab does not automatic
98
100
  | `/tab worktree detach` | Return to the original workspace while keeping the linked worktree and branch |
99
101
  | `/tab worktree status` | Show the current tab's worktree binding and Git status |
100
102
  | `/tab worktree merge` | Merge the current worktree branch into the branch checked out in its original workspace |
103
+ | `/tab worktree merge resolve` | Ask the agent to resolve an in-progress merge in the original workspace from the same tab |
104
+ | `/tab worktree merge continue` | Stage resolutions and complete an in-progress merge after confirmation |
101
105
  | `/tab worktree merge abort` | Abort a conflicted worktree merge in the original workspace |
102
106
  | `/tab worktree remove` | Remove a clean linked worktree and keep its branch |
103
107
  | `/worktree …` | Alias for `/tab worktree …` on the active tab |
data/doc/transports.md CHANGED
@@ -104,6 +104,13 @@ Plugin-chat drivers may accept a `context:` keyword on `submit`. Transport turns
104
104
  provide the authenticated actor there. Existing drivers that do not accept the
105
105
  keyword continue to work, but cannot use actor-specific context.
106
106
 
107
+ The plugin-chat factory host reports `surface: :transport`, the stable chat type
108
+ ID and scope key, immutable plugin configuration, scoped durable storage, secret
109
+ lookup, logging, declared chat capabilities, and managed cleanup. Storage is
110
+ isolated by plugin, chat type, and external conversation scope. Versioned chat
111
+ contracts also reject attachments not declared by that chat type before calling
112
+ the driver.
113
+
107
114
  Plugin-chat transport IDs, turn events, transcript storage, and authorization
108
115
  remain separate from normal workspace sessions. A plugin's `singleton: :global`
109
116
  setting also means that all transport conversations share that one plugin
@@ -119,6 +126,14 @@ API.
119
126
  Transports that cannot support interactive approvals must use an explicit
120
127
  configured fallback policy; they must not leave an agent turn waiting forever.
121
128
 
129
+ Structured plugin UI requests use the same transport-neutral interaction path.
130
+ Plugin `question`, `select`, `confirm`, and `input` calls arrive with matching
131
+ interaction kinds, prompts, choices, and metadata, and answers are routed back to the
132
+ waiting plugin handler. An adapter may support only a subset, but it must cancel
133
+ or answer unsupported requests rather than leaving the turn blocked. Plugin
134
+ notifications and progress remain local to the RPC/TUI UI bridge for now and
135
+ are not transport interaction requests.
136
+
122
137
  ## Storage and routing
123
138
 
124
139
  Transport plugins receive namespaced durable storage for state such as:
data/doc/usage.md CHANGED
@@ -136,11 +136,14 @@ Slash commands run local actions in the current session. Most do not send a prom
136
136
  | `/tab close` | close the active tab. |
137
137
  | `/tab new` | open a new tab. |
138
138
  | `/tab name <label>` | rename the active tab label. |
139
+ | `/name <name>` | rename the active session and tab together; available while the tab's agent is running. |
139
140
  | `/tab worktree` | create or activate the active session tab's linked Git worktree. |
140
141
  | `/tab worktree activate` | explicitly create or activate the active session tab's linked Git worktree. |
141
142
  | `/tab worktree detach` | return to the original workspace while keeping the linked worktree and branch. |
142
143
  | `/tab worktree status` | inspect the active tab's worktree binding and changes. |
143
144
  | `/tab worktree merge` | merge a clean worktree branch into the branch checked out in its original workspace. |
145
+ | `/tab worktree merge resolve` | ask the agent to resolve an in-progress merge in the original workspace from the same tab. |
146
+ | `/tab worktree merge continue` | stage resolutions and complete an in-progress merge after confirmation. |
144
147
  | `/tab worktree merge abort` | abort a conflicted worktree merge in the original workspace. |
145
148
  | `/tab worktree remove` | remove a clean linked worktree while keeping its branch. |
146
149
  | `/worktree …` | alias for `/tab worktree …` on the active tab. |
@@ -191,7 +194,7 @@ Use sessions when work spans more than one terminal sitting, or when you want to
191
194
  Typical flow:
192
195
 
193
196
  ```text
194
- /rename oauth cleanup
197
+ /name oauth cleanup
195
198
  # work with Kward
196
199
  /export oauth-notes.md
197
200
  /exit
@@ -21,6 +21,15 @@ Important behavior:
21
21
  - Edits use exact text replacement, so accidental partial or fuzzy changes fail instead of guessing.
22
22
  - With sandboxing off (the default), shell commands run as your operating-system user from the workspace. Enable [command sandboxing](sandboxing.md) to apply an OS boundary to model-requested `run_shell_command` workers. Command output is capped at 128 KB.
23
23
 
24
+ ### Worktree targets
25
+
26
+ In an active linked-worktree tab, `list_directory`, `read_file`, `write_file`, `edit_file`, `run_shell_command`, `summarize_file_structure`, and `context_for_task` also advertise an optional `target` argument:
27
+
28
+ - `active` is the default linked worktree.
29
+ - `origin` is the tab's verified original repository worktree.
30
+
31
+ Kward supplies this fixed role map from the tab binding; the model cannot provide an arbitrary filesystem root. Selecting `origin` does not add a separate approval prompt, so the agent can complete a requested inspect/edit/test/commit workflow without interruption. The normal configured tool-permission policy still applies equally to both targets. Each target keeps its own path guard and strict shell sandbox.
32
+
24
33
  ## Reading the workspace
25
34
 
26
35
  ### `list_directory`
@@ -15,7 +15,7 @@
15
15
  # The game renders colored sprites and particle-burst explosions inside the
16
16
  # composer canvas region using the interactive mode API.
17
17
 
18
- Kward.plugin do |plugin|
18
+ Kward.plugin(id: "com.kward.example.space-invaders", version: "1.0.0", api: 1) do |plugin|
19
19
  plugin.interactive_command "invaders", rows: 18, fps: 30, description: "Space Invaders arcade game" do |ui, ctx|
20
20
  game = SpaceInvadersGame.new(width: ui.width, height: ui.height)
21
21
  ui.on_tick { |ui| game.tick(ui) }
@@ -1,6 +1,6 @@
1
1
  # Displays the current Federation stardate in Kward's interactive footer.
2
- Kward.plugin do |plugin|
3
- plugin.footer do |_ctx|
2
+ Kward.plugin(id: "com.kward.example.stardate-footer", version: "1.0.0", api: 1) do |plugin|
3
+ plugin.status "stardate", priority: :low do |_ctx|
4
4
  now = Time.now.utc
5
5
  reference = Time.utc(1987, 7, 15)
6
6
  stardate = 41_000 + ((now - reference) / (365.25 * 24 * 60 * 60) * 1_000)
@@ -1,6 +1,6 @@
1
1
  require_relative "telegram_transport"
2
2
 
3
- Kward.plugin do |plugin|
3
+ Kward.plugin(id: "com.kward.telegram", version: "1.0.0", api: 1) do |plugin|
4
4
  capabilities = {
5
5
  inbound: %i[text],
6
6
  outbound: %i[text],
data/lib/kward/agent.rb CHANGED
@@ -3,6 +3,7 @@ require_relative "model/chat_invocation"
3
3
  require_relative "compactor"
4
4
  require_relative "model/context_overflow"
5
5
  require_relative "conversation"
6
+ require_relative "plugins/turn_request"
6
7
  require_relative "events"
7
8
  require_relative "deep_copy"
8
9
  require_relative "hooks"
@@ -41,9 +42,9 @@ module Kward
41
42
 
42
43
  attr_reader :conversation, :tool_registry
43
44
 
44
- # Adds a user message, compacts context when needed, and runs the turn.
45
+ # Adds user input or scopes a plugin system request, then runs a normal turn.
45
46
  #
46
- # @param input [String] text sent to the model
47
+ # @param input [String, PluginTurnRequest] user text or host-staged system instructions
47
48
  # @param display_input [String, nil] alternate text kept for transcripts
48
49
  # @yieldparam event [Object] streamed turn event for frontends
49
50
  # @return [String] final assistant answer
@@ -52,19 +53,31 @@ module Kward
52
53
  status = "completed"
53
54
  error = nil
54
55
  cancellation&.raise_if_cancelled!
55
- turn_start = run_hook("turn_start", payload: { input: input, display_input: display_input })
56
+ system_turn = input if input.is_a?(PluginTurnRequest)
57
+ display_input ||= system_turn.to_s if system_turn
58
+ input = system_turn.system if system_turn
59
+ turn_start = run_hook("turn_start", payload: { input: input, display_input: display_input, input_role: system_turn ? "system" : "user" })
56
60
  return hook_denied_answer(turn_start) if turn_start.denied?
57
61
 
58
62
  input = turn_start.payload[:input] || turn_start.payload["input"] || input
59
63
  display_input = turn_start.payload[:display_input] || turn_start.payload["display_input"] || display_input
60
- @conversation.refresh_system_message_if_workspace_agents_changed!
61
- @conversation.append_user(input, display_content: display_input)
62
- run_hook("turn_context_build_before", payload: { message_count: @conversation.messages.length })
63
- auto_compact_if_needed
64
- run_hook("turn_context_build_after", payload: { message_count: @conversation.messages.length })
65
- answer = run_turn(on_reasoning_delta: on_reasoning_delta, on_retry: on_retry, cancellation: cancellation, steering: steering, options: options, tool_registry: tool_registry, &block)
66
- run_hook("turn_end", payload: { input: input, answer: answer })
67
- answer
64
+ if system_turn
65
+ system_turn = PluginTurnRequest.new(system: input, command: system_turn.command, plugin_id: system_turn.plugin_id, id: system_turn.id)
66
+ end
67
+ @conversation.with_system_turn(system_turn) do
68
+ @conversation.refresh_system_message_if_workspace_agents_changed!
69
+ if system_turn
70
+ @conversation.append_system_turn(system_turn)
71
+ else
72
+ @conversation.append_user(input, display_content: display_input)
73
+ end
74
+ run_hook("turn_context_build_before", payload: { message_count: @conversation.messages.length })
75
+ auto_compact_if_needed
76
+ run_hook("turn_context_build_after", payload: { message_count: @conversation.messages.length })
77
+ answer = run_turn(on_reasoning_delta: on_reasoning_delta, on_retry: on_retry, cancellation: cancellation, steering: steering, options: options, tool_registry: tool_registry, &block)
78
+ run_hook("turn_end", payload: { input: input, answer: answer })
79
+ answer
80
+ end
68
81
  rescue StandardError => e
69
82
  status = "failed"
70
83
  error = e
@@ -6,7 +6,7 @@ module Kward
6
6
  module CompactionCommands
7
7
  private
8
8
 
9
- def compact_context(agent, argument)
9
+ def compact_context(agent, argument, cancellation: nil)
10
10
  before = run_lifecycle_hook("session_compact_before", conversation: agent.conversation, payload: { instructions: argument.to_s })
11
11
  if before.denied? || before.approval_required?
12
12
  runtime_output("Declined: #{before.decision.message || "session compaction denied"}")
@@ -17,16 +17,22 @@ module Kward
17
17
  conversation: agent.conversation,
18
18
  client: @client,
19
19
  tool_result_summarizer: lambda { |tool_call, content| tool_result_summary(tool_call, content) }
20
- ).compact(custom_instructions: argument)
20
+ ).compact(custom_instructions: argument, cancellation: cancellation)
21
21
  run_lifecycle_hook("session_compact_after", conversation: agent.conversation, payload: { old_message_count: result.old_message_count, new_message_count: result.new_message_count })
22
22
  runtime_output("Compacted context: #{result.old_message_count} messages -> #{result.new_message_count} messages.")
23
- render_transcript_block("Assistant", result.summary)
23
+ result.summary
24
24
  rescue Compactor::NothingToCompact, Compactor::AlreadyCompacted, Compactor::EmptySummary => e
25
25
  runtime_output(e.message)
26
+ rescue Cancellation::CancelledError
27
+ raise
26
28
  rescue StandardError => e
27
29
  runtime_output("Compaction error: #{e.message}")
28
30
  end
29
31
 
32
+ def render_compaction_summary(summary)
33
+ render_transcript_block("Assistant", summary)
34
+ end
35
+
30
36
  end
31
37
  end
32
38
  end