kward 0.83.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 +102 -15
- data/CONTRIBUTING.md +74 -0
- data/Gemfile.lock +8 -2
- data/README.md +21 -1
- data/Rakefile +46 -2
- data/SECURITY.md +31 -0
- data/doc/agent-tools.md +13 -1
- data/doc/api.md +21 -2
- data/doc/composer.md +2 -2
- data/doc/configuration.md +95 -25
- data/doc/editor.md +28 -13
- data/doc/extensibility.md +2 -1
- data/doc/files.md +8 -4
- data/doc/getting-started.md +3 -0
- data/doc/git.md +3 -1
- data/doc/pan.md +25 -15
- data/doc/permissions.md +4 -4
- data/doc/platform-support.md +48 -0
- data/doc/plugins.md +464 -15
- data/doc/rpc.md +154 -16
- data/doc/sandboxing.md +11 -5
- data/doc/security.md +10 -3
- data/doc/session-management.md +5 -4
- data/doc/shell.md +62 -45
- data/doc/tabs.md +6 -2
- data/doc/transports.md +15 -0
- data/doc/troubleshooting.md +12 -2
- data/doc/usage.md +9 -6
- 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/kward.gemspec +5 -4
- data/lib/kward/agent.rb +30 -14
- data/lib/kward/cli/auth_commands.rb +34 -13
- data/lib/kward/cli/commands.rb +83 -62
- data/lib/kward/cli/compaction.rb +9 -3
- data/lib/kward/cli/doctor.rb +39 -17
- data/lib/kward/cli/hook_commands.rb +22 -12
- data/lib/kward/cli/interactive_turn.rb +48 -7
- data/lib/kward/cli/plugins.rb +81 -12
- data/lib/kward/cli/project_skills_commands.rb +8 -4
- data/lib/kward/cli/prompt_interface.rb +52 -5
- data/lib/kward/cli/rendering.rb +18 -9
- data/lib/kward/cli/runtime_helpers.rb +228 -71
- 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 +56 -17
- data/lib/kward/cli/tabs.rb +244 -33
- data/lib/kward/cli/tool_summaries.rb +14 -0
- data/lib/kward/{cli_transcript_formatter.rb → cli/transcript_formatter.rb} +14 -7
- data/lib/kward/cli/worktrees.rb +65 -2
- data/lib/kward/cli.rb +70 -30
- 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 -994
- data/lib/kward/conversation.rb +31 -2
- data/lib/kward/image_attachments.rb +1 -1
- data/lib/kward/model/client.rb +36 -24
- 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/provider_catalog.rb +5 -0
- data/lib/kward/model/stream_parser.rb +20 -4
- data/lib/kward/model/typesafe_client.rb +78 -0
- data/lib/kward/pan/index.html.erb +3 -3
- data/lib/kward/pan/server.rb +33 -10
- 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 +44 -40
- data/lib/kward/prompt_interface/composer_state.rb +33 -24
- data/lib/kward/prompt_interface/editor/auto_indent.rb +24 -22
- data/lib/kward/prompt_interface/editor/controller.rb +30 -33
- data/lib/kward/prompt_interface/editor/endwise.rb +13 -4
- data/lib/kward/prompt_interface/editor/markdown_code_block.rb +136 -0
- data/lib/kward/prompt_interface/editor/modes/vibe.rb +289 -44
- data/lib/kward/prompt_interface/editor/renderer.rb +108 -6
- data/lib/kward/prompt_interface/editor/runner.rb +362 -0
- data/lib/kward/prompt_interface/editor/runner_state.rb +78 -0
- data/lib/kward/prompt_interface/editor/scratchpad_languages.rb +74 -0
- data/lib/kward/prompt_interface/editor/scratchpad_runner.rb +182 -0
- data/lib/kward/prompt_interface/editor/state.rb +11 -11
- data/lib/kward/prompt_interface/editor/syntax_highlighter.rb +68 -6
- data/lib/kward/prompt_interface/editor/vibe_state.rb +3 -3
- data/lib/kward/prompt_interface/file_overlay.rb +71 -15
- data/lib/kward/prompt_interface/key_handler.rb +67 -0
- data/lib/kward/prompt_interface/layout.rb +1 -1
- data/lib/kward/prompt_interface/overlay_renderer.rb +7 -5
- data/lib/kward/prompt_interface/plugin_ui_requests.rb +82 -0
- data/lib/kward/prompt_interface/project_browser.rb +415 -14
- data/lib/kward/prompt_interface/runtime_state.rb +56 -2
- data/lib/kward/prompt_interface/screen.rb +11 -4
- data/lib/kward/prompt_interface/selection_prompt.rb +3 -1
- data/lib/kward/prompt_interface/slash_overlay.rb +19 -4
- data/lib/kward/prompt_interface/transcript_renderer.rb +12 -7
- data/lib/kward/prompt_interface.rb +151 -27
- data/lib/kward/prompts/commands.rb +3 -2
- data/lib/kward/prompts.rb +1 -1
- data/lib/kward/{adaptive_pty_output_sink.rb → pty/adaptive_output_sink.rb} +1 -1
- data/lib/kward/pty/detached_run.rb +44 -0
- data/lib/kward/{interactive_pty_runner.rb → pty/interactive_runner.rb} +104 -30
- 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} +52 -0
- 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/rpc/transcript_normalizer.rb +7 -3
- 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/{ekwsh.rb → shell/kwsh.rb} +38 -19
- data/lib/kward/shell/kwshrc.rb +233 -0
- data/lib/kward/{persistent_shell_session.rb → shell/persistent_session.rb} +121 -28
- data/lib/kward/{shell_prompt.rb → shell/prompt.rb} +2 -0
- 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/{ansi.rb → terminal/ansi.rb} +110 -10
- 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/{terminal_keys.rb → terminal/keys.rb} +12 -0
- data/lib/kward/terminal/text.rb +121 -0
- data/lib/kward/text_matcher.rb +18 -0
- 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/open_editor.rb +41 -0
- 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 +109 -17
- 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 +3 -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/{project_files.rb → workspace/files.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/kward_navigation.rb +1 -0
- data/templates/default/layout/html/footer.erb +10 -0
- data/templates/default/layout/html/headers.erb +23 -0
- data/templates/default/layout/html/layout.erb +2 -2
- data/templates/default/layout/html/setup.rb +41 -2
- metadata +94 -47
- data/lib/kward/scratchpad_runner.rb +0 -56
- data/lib/kward/tab_driver.rb +0 -90
- /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/{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/{terminal_sequences.rb → terminal/sequences.rb} +0 -0
- /data/lib/kward/{transcript_export.rb → transcripts/transcript_export.rb} +0 -0
- /data/lib/kward/{path_guard.rb → workspace/path_guard.rb} +0 -0
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
require_relative "../prompt_interface/editor/scratchpad_languages"
|
|
2
|
+
|
|
1
3
|
# Namespace for the Kward CLI agent runtime.
|
|
2
4
|
module Kward
|
|
3
5
|
# Command-line frontend that coordinates terminal interaction, sessions, tools, and model turns.
|
|
@@ -48,7 +50,7 @@ module Kward
|
|
|
48
50
|
open_project_files_browser
|
|
49
51
|
[true, nil]
|
|
50
52
|
when "shell"
|
|
51
|
-
|
|
53
|
+
run_kwsh(agent)
|
|
52
54
|
[true, nil]
|
|
53
55
|
when "capture"
|
|
54
56
|
run_captured_shell_command(argument, agent)
|
|
@@ -62,6 +64,9 @@ module Kward
|
|
|
62
64
|
[true, nil]
|
|
63
65
|
when "tab"
|
|
64
66
|
[true, handle_tab_command(argument, session_store)]
|
|
67
|
+
when "name"
|
|
68
|
+
rename_active_session_and_tab(argument)
|
|
69
|
+
[true, nil]
|
|
65
70
|
when "worktree"
|
|
66
71
|
handle_worktree_command(argument)
|
|
67
72
|
[true, active_tab&.agent]
|
|
@@ -79,7 +84,9 @@ module Kward
|
|
|
79
84
|
configure_reasoning(agent.conversation)
|
|
80
85
|
[true, nil]
|
|
81
86
|
when "reload"
|
|
82
|
-
|
|
87
|
+
strict_worktree = active_tab&.driver&.respond_to?(:worktree) && active_tab.driver.worktree&.active?
|
|
88
|
+
registry = strict_worktree ? nil : agent.tool_registry
|
|
89
|
+
run_busy_local_command_and_requeue { reload_plugins(agent.conversation, tool_registry: registry) }
|
|
83
90
|
[true, nil]
|
|
84
91
|
when "new"
|
|
85
92
|
[true, run_busy_local_command_and_requeue { start_new_session(session_store) }]
|
|
@@ -111,7 +118,11 @@ module Kward
|
|
|
111
118
|
run_busy_local_command_and_requeue { export_session(agent.conversation, argument) }
|
|
112
119
|
[true, nil]
|
|
113
120
|
when "compact"
|
|
114
|
-
|
|
121
|
+
origin_tab = active_tab
|
|
122
|
+
result = run_busy_local_command_and_requeue(activity: "compacting") do |cancellation|
|
|
123
|
+
compact_context(agent, argument, cancellation: cancellation)
|
|
124
|
+
end
|
|
125
|
+
render_compaction_summary(result) if !origin_tab || origin_tab == active_tab
|
|
115
126
|
[true, nil]
|
|
116
127
|
else
|
|
117
128
|
if name.to_s.start_with?("skill:")
|
|
@@ -120,7 +131,7 @@ module Kward
|
|
|
120
131
|
elsif interactive_command_for(name) && prompt_interface? && @prompt.respond_to?(:start_interactive)
|
|
121
132
|
run_interactive_command(name, argument, agent)
|
|
122
133
|
elsif plugin_command_for(name)
|
|
123
|
-
|
|
134
|
+
run_plugin_command_and_turn(name, argument, agent)
|
|
124
135
|
else
|
|
125
136
|
[false, nil]
|
|
126
137
|
end
|
|
@@ -131,6 +142,38 @@ module Kward
|
|
|
131
142
|
PromptCommands.parse(command) || [nil, ""]
|
|
132
143
|
end
|
|
133
144
|
|
|
145
|
+
def rename_active_session_and_tab(argument)
|
|
146
|
+
name = argument.to_s.strip
|
|
147
|
+
return runtime_output("Usage: /name <name>") if name.empty?
|
|
148
|
+
|
|
149
|
+
tab = active_tab
|
|
150
|
+
unless @active_session && (!tab || tab.session.equal?(@active_session))
|
|
151
|
+
return runtime_output("The /name command is only available in a session tab.")
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
errors = []
|
|
155
|
+
begin
|
|
156
|
+
rename_session(name, require_name: true, announce: false)
|
|
157
|
+
rescue StandardError => error
|
|
158
|
+
errors << "session: #{error.message}"
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
if tab
|
|
162
|
+
begin
|
|
163
|
+
rename_active_tab(name)
|
|
164
|
+
rescue StandardError => error
|
|
165
|
+
errors << "tab: #{error.message}"
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
if errors.empty?
|
|
170
|
+
label = tab ? "Named session and tab: #{name}" : "Named session: #{name}"
|
|
171
|
+
runtime_output(label)
|
|
172
|
+
else
|
|
173
|
+
runtime_output("Name update incomplete (#{errors.join("; ")}).")
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
134
177
|
def handle_sandbox_command(argument, workspace_root)
|
|
135
178
|
action, value = argument.to_s.strip.split(/\s+/, 2)
|
|
136
179
|
case action
|
|
@@ -304,11 +347,16 @@ module Kward
|
|
|
304
347
|
end
|
|
305
348
|
|
|
306
349
|
def open_scratchpad_command(argument)
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
else
|
|
310
|
-
runtime_output("The scratchpad is only available in the interactive prompt.")
|
|
350
|
+
unless @prompt.respond_to?(:scratchpad)
|
|
351
|
+
return runtime_output("The scratchpad is only available in the interactive prompt.")
|
|
311
352
|
end
|
|
353
|
+
|
|
354
|
+
value = argument.to_s.strip
|
|
355
|
+
return runtime_output(ScratchpadLanguages.help_text) if value.casecmp("help").zero?
|
|
356
|
+
|
|
357
|
+
@prompt.scratchpad(ScratchpadLanguages.normalize(value))
|
|
358
|
+
rescue ArgumentError => e
|
|
359
|
+
runtime_output(e.message)
|
|
312
360
|
end
|
|
313
361
|
|
|
314
362
|
def queue_editor_prompt_action(result)
|
|
@@ -318,15 +366,6 @@ module Kward
|
|
|
318
366
|
@pending_inputs.unshift(result)
|
|
319
367
|
end
|
|
320
368
|
|
|
321
|
-
def scratchpad_language_argument(argument)
|
|
322
|
-
value = argument.to_s.strip.downcase
|
|
323
|
-
return :text if value.empty? || value == "text"
|
|
324
|
-
return :markdown if ["markdown", "md"].include?(value)
|
|
325
|
-
return :ruby if ["ruby", "rb"].include?(value)
|
|
326
|
-
|
|
327
|
-
:text
|
|
328
|
-
end
|
|
329
|
-
|
|
330
369
|
def open_project_files_browser
|
|
331
370
|
if @prompt.respond_to?(:open_project_browser)
|
|
332
371
|
@prompt.open_project_browser
|
data/lib/kward/cli/tabs.rb
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require "json"
|
|
2
|
+
require "securerandom"
|
|
2
3
|
require "thread"
|
|
3
4
|
require_relative "../cancellation"
|
|
4
5
|
|
|
@@ -10,14 +11,16 @@ module Kward
|
|
|
10
11
|
module Tabs
|
|
11
12
|
WORKTREE_AGENT_CONTEXT = <<~PROMPT.strip.freeze
|
|
12
13
|
This is an active Git worktree tab. The workspace is sandboxed separately from the host repository metadata.
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
Workspace tools default to the active worktree. When they advertise `target`, use `target: "origin"` to inspect the verified host repository worktree without leaving this tab. Read the origin's applicable `AGENTS.md` before changing it.
|
|
15
|
+
When the user explicitly asks you to commit, use the advertised `git_commit` tool. Set `target: "origin"` only when the user asks to commit there. Do not use `run_shell_command` for `git add` or `git commit`, because shared Git metadata is intentionally protected.
|
|
16
|
+
`git_commit` requires a commit message and accepts optional target-relative paths; omit paths to include all current changes in the selected worktree. Do not create a commit unless the user requested one.
|
|
15
17
|
PROMPT
|
|
16
18
|
|
|
17
19
|
TabRuntime = Struct.new(
|
|
18
20
|
:session,
|
|
19
21
|
:agent,
|
|
20
22
|
:driver,
|
|
23
|
+
:plugin_host,
|
|
21
24
|
:diff,
|
|
22
25
|
:snapshot,
|
|
23
26
|
:status,
|
|
@@ -33,12 +36,15 @@ module Kward
|
|
|
33
36
|
:markdown_chunks,
|
|
34
37
|
:label,
|
|
35
38
|
:unread,
|
|
39
|
+
:attention,
|
|
36
40
|
:pending_question,
|
|
37
41
|
:shell,
|
|
38
42
|
:shell_agent,
|
|
43
|
+
:background_run,
|
|
39
44
|
:transient_shell_entries,
|
|
40
45
|
:error_reported,
|
|
41
46
|
:local_busy_activity,
|
|
47
|
+
:pending_runtime_outputs,
|
|
42
48
|
keyword_init: true
|
|
43
49
|
) do
|
|
44
50
|
TRANSIENT_SHELL_OUTPUT_LIMIT = 200_000
|
|
@@ -51,6 +57,11 @@ module Kward
|
|
|
51
57
|
!local_busy_activity.to_s.empty?
|
|
52
58
|
end
|
|
53
59
|
|
|
60
|
+
def append_pending_runtime_output(text)
|
|
61
|
+
self.pending_runtime_outputs ||= []
|
|
62
|
+
pending_runtime_outputs << text.to_s
|
|
63
|
+
end
|
|
64
|
+
|
|
54
65
|
def idle?
|
|
55
66
|
!running? && !local_busy?
|
|
56
67
|
end
|
|
@@ -106,6 +117,10 @@ module Kward
|
|
|
106
117
|
@cli.send(:ask_tab_user_question, @tab, questions, cancellation: cancellation)
|
|
107
118
|
end
|
|
108
119
|
|
|
120
|
+
def edit_file(path, base_dir:, allow_new:)
|
|
121
|
+
@cli.send(:open_editor_for_agent, path, base_dir: base_dir, allow_new: allow_new)
|
|
122
|
+
end
|
|
123
|
+
|
|
109
124
|
def ask_tool_approval(tool_name:, args:, reason: nil)
|
|
110
125
|
answers = ask_user_question([
|
|
111
126
|
{
|
|
@@ -199,14 +214,23 @@ module Kward
|
|
|
199
214
|
end
|
|
200
215
|
|
|
201
216
|
tab_type = plugin_registry.tab_type_for_id(descriptor["plugin_tab_type"])
|
|
217
|
+
host = nil
|
|
202
218
|
driver = if tab_type&.local
|
|
203
|
-
|
|
204
|
-
tab_type.
|
|
219
|
+
descriptor["scope_key"] ||= plugin_tab_scope_key(tab_type)
|
|
220
|
+
host = build_plugin_tab_host(tab_type, workspace_root: session_store.cwd, scope_key: descriptor["scope_key"])
|
|
221
|
+
begin
|
|
222
|
+
created_driver = tab_type.handler.call(host, descriptor)
|
|
223
|
+
raise "Plugin tab #{descriptor["plugin_tab_type"].inspect} did not return a tab driver." unless created_driver
|
|
224
|
+
|
|
225
|
+
tab_type.capabilities.validate_driver!(created_driver)
|
|
226
|
+
rescue StandardError
|
|
227
|
+
host.shutdown
|
|
228
|
+
raise
|
|
229
|
+
end
|
|
205
230
|
else
|
|
206
231
|
UnavailableTabDriver.new(descriptor: descriptor, message: "Plugin tab #{descriptor["plugin_tab_type"].inspect} is unavailable.")
|
|
207
232
|
end
|
|
208
|
-
|
|
209
|
-
build_tab(nil, nil, driver: driver, label: label || descriptor["label"] || tab_type&.title)
|
|
233
|
+
build_tab(nil, nil, driver: driver, plugin_host: host, label: label || descriptor["label"] || tab_type&.title)
|
|
210
234
|
end
|
|
211
235
|
|
|
212
236
|
def open_plugin_tab(name, session_store)
|
|
@@ -219,12 +243,20 @@ module Kward
|
|
|
219
243
|
|
|
220
244
|
save_active_tab_state
|
|
221
245
|
stop_tab_live_view
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
246
|
+
scope_key = plugin_tab_scope_key(tab_type)
|
|
247
|
+
descriptor = { "kind" => "plugin", "plugin_tab_type" => tab_type.id, "label" => tab_type.title, "scope_key" => scope_key }
|
|
248
|
+
host = build_plugin_tab_host(tab_type, workspace_root: session_store.cwd, scope_key: scope_key)
|
|
249
|
+
begin
|
|
250
|
+
driver = tab_type.handler.call(host, descriptor)
|
|
251
|
+
raise "Plugin tab #{name.inspect} did not return a tab driver." unless driver
|
|
252
|
+
|
|
253
|
+
tab_type.capabilities.validate_driver!(driver)
|
|
254
|
+
rescue StandardError
|
|
255
|
+
host.shutdown
|
|
256
|
+
raise
|
|
257
|
+
end
|
|
226
258
|
|
|
227
|
-
@tabs << build_tab(nil, nil, driver: driver, label: tab_type.title)
|
|
259
|
+
@tabs << build_tab(nil, nil, driver: driver, plugin_host: host, label: tab_type.title)
|
|
228
260
|
@active_tab_index = @tabs.length - 1
|
|
229
261
|
activate_tab(@active_tab_index)
|
|
230
262
|
rescue StandardError => e
|
|
@@ -248,21 +280,30 @@ module Kward
|
|
|
248
280
|
strict = worktree&.active? == true
|
|
249
281
|
update_worktree_agent_context(conversation, strict)
|
|
250
282
|
workspace = configured_workspace(root: conversation.workspace_root, strict: strict)
|
|
283
|
+
workspace_targets = if strict
|
|
284
|
+
origin_workspace = configured_workspace(root: worktree.origin_root, strict: true)
|
|
285
|
+
Tools::WorkspaceTargets.new(active: workspace, targets: { origin: origin_workspace })
|
|
286
|
+
end
|
|
251
287
|
prompt = TabQuestionPrompt.new(self)
|
|
252
288
|
hook_manager = strict ? nil : lifecycle_hook_manager(conversation)
|
|
253
289
|
hook_context = strict ? nil : lifecycle_hook_context(conversation)
|
|
254
290
|
git_committer = if strict
|
|
255
|
-
|
|
291
|
+
lambda do |message:, paths:, target: Tools::WorkspaceTargets::ACTIVE|
|
|
292
|
+
commit_workspace = target == "origin" ? worktree.origin_root : workspace.root.to_s
|
|
293
|
+
git_commit_for_agent(commit_workspace, message: message, paths: paths)
|
|
294
|
+
end
|
|
256
295
|
end
|
|
257
296
|
project_skill_paths = project_skill_paths_for(conversation.workspace_root) || []
|
|
258
297
|
tool_registry = ToolRegistry.new(
|
|
259
298
|
workspace: workspace,
|
|
299
|
+
workspace_targets: workspace_targets,
|
|
260
300
|
prompt: prompt,
|
|
261
301
|
skills: ConfigFiles.skills(workspace_root: conversation.workspace_root, project_skill_paths: project_skill_paths),
|
|
262
302
|
tool_approval: tab_tool_approval_callback(prompt),
|
|
263
303
|
hook_manager: hook_manager,
|
|
264
304
|
hook_context: hook_context,
|
|
265
305
|
mcp_clients: strict ? [] : nil,
|
|
306
|
+
plugin_tools: strict ? [] : plugin_registry.tools,
|
|
266
307
|
git_committer: git_committer
|
|
267
308
|
)
|
|
268
309
|
@footer_conversation = conversation
|
|
@@ -295,12 +336,13 @@ module Kward
|
|
|
295
336
|
end
|
|
296
337
|
end
|
|
297
338
|
|
|
298
|
-
def build_tab(session, agent, driver: nil, label: nil)
|
|
339
|
+
def build_tab(session, agent, driver: nil, plugin_host: nil, label: nil)
|
|
299
340
|
driver ||= SessionTabDriver.new(session: session, agent: agent)
|
|
300
341
|
TabRuntime.new(
|
|
301
342
|
session: session,
|
|
302
343
|
agent: agent,
|
|
303
344
|
driver: driver,
|
|
345
|
+
plugin_host: plugin_host,
|
|
304
346
|
diff: session&.path ? SessionDiff.from_session_file(session.path) : SessionDiff.new,
|
|
305
347
|
snapshot: nil,
|
|
306
348
|
status: "idle",
|
|
@@ -316,12 +358,15 @@ module Kward
|
|
|
316
358
|
markdown_chunks: [],
|
|
317
359
|
label: label,
|
|
318
360
|
unread: false,
|
|
361
|
+
attention: nil,
|
|
319
362
|
pending_question: nil,
|
|
320
363
|
shell: nil,
|
|
321
364
|
shell_agent: nil,
|
|
365
|
+
background_run: nil,
|
|
322
366
|
transient_shell_entries: [],
|
|
323
367
|
error_reported: false,
|
|
324
|
-
local_busy_activity: nil
|
|
368
|
+
local_busy_activity: nil,
|
|
369
|
+
pending_runtime_outputs: []
|
|
325
370
|
).tap { |tab| assign_tab_question_prompt(agent, tab) }
|
|
326
371
|
end
|
|
327
372
|
|
|
@@ -420,16 +465,17 @@ module Kward
|
|
|
420
465
|
end
|
|
421
466
|
|
|
422
467
|
if @tabs.length <= 1
|
|
423
|
-
|
|
468
|
+
close_kwsh_session(tab.shell) if tab&.shell && respond_to?(:close_kwsh_session, true)
|
|
469
|
+
close_plugin_tab(tab)
|
|
424
470
|
@tabs.clear
|
|
425
471
|
persist_tabs
|
|
426
472
|
return PromptInterface::EXIT_INPUT
|
|
427
473
|
end
|
|
428
474
|
|
|
429
475
|
stop_tab_live_view
|
|
430
|
-
|
|
476
|
+
close_kwsh_session(tab.shell) if tab&.shell && respond_to?(:close_kwsh_session, true)
|
|
431
477
|
tab.session&.delete_if_unused if tab&.session.respond_to?(:delete_if_unused)
|
|
432
|
-
tab
|
|
478
|
+
close_plugin_tab(tab)
|
|
433
479
|
@tabs.delete_at(@active_tab_index)
|
|
434
480
|
@active_tab_index = [@active_tab_index, @tabs.length - 1].min
|
|
435
481
|
activate_tab(@active_tab_index)
|
|
@@ -461,12 +507,13 @@ module Kward
|
|
|
461
507
|
tab.error = nil
|
|
462
508
|
tab.answer = nil
|
|
463
509
|
tab.unread = false
|
|
510
|
+
tab.attention = nil
|
|
464
511
|
tab.error_reported = false
|
|
465
512
|
tab.event_history.clear
|
|
466
513
|
tab.seen_events = 0
|
|
467
514
|
tab.queued_inputs.clear
|
|
468
515
|
tab.steering = nil
|
|
469
|
-
|
|
516
|
+
close_kwsh_session(tab.shell) if tab.shell && respond_to?(:close_kwsh_session, true)
|
|
470
517
|
tab.shell = nil
|
|
471
518
|
tab.shell_agent = nil
|
|
472
519
|
tab.clear_transient_shell_entries
|
|
@@ -501,6 +548,7 @@ module Kward
|
|
|
501
548
|
end
|
|
502
549
|
|
|
503
550
|
def activate_tab(index, render: true)
|
|
551
|
+
service_background_tab_runs
|
|
504
552
|
tab = @tabs[index]
|
|
505
553
|
return nil unless tab
|
|
506
554
|
|
|
@@ -517,6 +565,7 @@ module Kward
|
|
|
517
565
|
@prompt.update_assistant_label(assistant_prompt_name) if @prompt.respond_to?(:update_assistant_label)
|
|
518
566
|
end
|
|
519
567
|
tab.unread = false
|
|
568
|
+
tab.attention = nil
|
|
520
569
|
restore_tab_composer_snapshot(tab.snapshot)
|
|
521
570
|
update_prompt_workspace_root(current_workspace_root)
|
|
522
571
|
@prompt.refresh_composer_status if @prompt.respond_to?(:refresh_composer_status)
|
|
@@ -544,8 +593,23 @@ module Kward
|
|
|
544
593
|
end
|
|
545
594
|
report_tab_runtime_error(tab) if %w[failed cancelled].include?(tab.status.to_s)
|
|
546
595
|
render_tab_transient_shell_entries(tab)
|
|
596
|
+
render_background_tab_output(tab)
|
|
547
597
|
end
|
|
548
598
|
restore_tab_composer_snapshot(tab.snapshot) if restore_composer
|
|
599
|
+
render_pending_tab_runtime_outputs(tab)
|
|
600
|
+
end
|
|
601
|
+
|
|
602
|
+
def render_pending_tab_runtime_outputs(tab)
|
|
603
|
+
outputs = Array(tab.pending_runtime_outputs)
|
|
604
|
+
tab.pending_runtime_outputs = []
|
|
605
|
+
outputs.each { |output| runtime_output(output) }
|
|
606
|
+
end
|
|
607
|
+
|
|
608
|
+
def flush_pending_runtime_outputs(tab)
|
|
609
|
+
return false unless tab && tab == active_tab
|
|
610
|
+
|
|
611
|
+
render_pending_tab_runtime_outputs(tab)
|
|
612
|
+
true
|
|
549
613
|
end
|
|
550
614
|
|
|
551
615
|
def render_tab_transient_shell_entries(tab)
|
|
@@ -558,6 +622,17 @@ module Kward
|
|
|
558
622
|
end
|
|
559
623
|
end
|
|
560
624
|
|
|
625
|
+
def render_background_tab_output(tab)
|
|
626
|
+
output = tab.background_run&.fetch(:rendered_output, "").to_s
|
|
627
|
+
return if output.empty?
|
|
628
|
+
|
|
629
|
+
if @prompt.respond_to?(:say)
|
|
630
|
+
@prompt.say(output)
|
|
631
|
+
elsif @prompt.respond_to?(:write_transcript)
|
|
632
|
+
@prompt.write_transcript(output)
|
|
633
|
+
end
|
|
634
|
+
end
|
|
635
|
+
|
|
561
636
|
def clear_active_tab_transient_shell_entries
|
|
562
637
|
active_tab&.clear_transient_shell_entries
|
|
563
638
|
end
|
|
@@ -603,21 +678,92 @@ module Kward
|
|
|
603
678
|
index ? index + 1 : active_tab_number
|
|
604
679
|
end
|
|
605
680
|
|
|
681
|
+
def register_background_tab_run(tab, run, output_kind: nil, &completion)
|
|
682
|
+
return unless tab && run
|
|
683
|
+
|
|
684
|
+
tab.background_run = {
|
|
685
|
+
run: run,
|
|
686
|
+
completion: completion,
|
|
687
|
+
output_kind: output_kind,
|
|
688
|
+
rendered_output: +""
|
|
689
|
+
}
|
|
690
|
+
tab.local_busy_activity = "running"
|
|
691
|
+
tab.attention = nil
|
|
692
|
+
update_prompt_tabs
|
|
693
|
+
end
|
|
694
|
+
|
|
695
|
+
def service_background_tab_runs
|
|
696
|
+
return unless @tabs
|
|
697
|
+
|
|
698
|
+
@tabs.each do |tab|
|
|
699
|
+
state = tab.background_run
|
|
700
|
+
next unless state
|
|
701
|
+
|
|
702
|
+
complete = state[:run].complete?
|
|
703
|
+
result = state[:run].result if complete
|
|
704
|
+
if state[:output_kind] && respond_to?(:flush_background_tab_output, true)
|
|
705
|
+
flush_background_tab_output(tab, state, result: result)
|
|
706
|
+
end
|
|
707
|
+
next unless complete
|
|
708
|
+
|
|
709
|
+
state[:completion]&.call(state[:run].sink, result, state) if result.is_a?(Struct)
|
|
710
|
+
rendered_output = state[:rendered_output].to_s
|
|
711
|
+
tab.append_transient_shell_entry(rendered_output) unless rendered_output.empty?
|
|
712
|
+
tab.background_run = nil
|
|
713
|
+
finish_local_busy_command_for_tab(tab)
|
|
714
|
+
if tab != active_tab
|
|
715
|
+
tab.unread = true
|
|
716
|
+
tab.attention = :success
|
|
717
|
+
end
|
|
718
|
+
rescue StandardError => e
|
|
719
|
+
tab.background_run = nil
|
|
720
|
+
tab.error = e.message
|
|
721
|
+
finish_local_busy_command_for_tab(tab)
|
|
722
|
+
tab.attention = :failure if tab != active_tab
|
|
723
|
+
end
|
|
724
|
+
update_prompt_tabs
|
|
725
|
+
end
|
|
726
|
+
|
|
727
|
+
def finish_local_busy_command_for_tab(tab)
|
|
728
|
+
tab.local_busy_activity = nil
|
|
729
|
+
@prompt.finish_busy_input if prompt_interface? && tab == active_tab && @prompt.respond_to?(:finish_busy_input)
|
|
730
|
+
end
|
|
731
|
+
|
|
606
732
|
def submit_tab_input(tab, input, display_input: nil)
|
|
607
733
|
return if input.to_s.strip.empty?
|
|
734
|
+
return unless plugin_tab_attachments_allowed?(tab, input)
|
|
608
735
|
|
|
609
736
|
save_active_tab_state
|
|
610
737
|
start_tab_turn(tab, input, display_input: display_input)
|
|
611
738
|
start_tab_live_view(tab) if tab == active_tab
|
|
612
739
|
end
|
|
613
740
|
|
|
741
|
+
def plugin_tab_attachments_allowed?(tab, input)
|
|
742
|
+
capabilities = tab.plugin_host&.capabilities
|
|
743
|
+
return true unless capabilities&.declared?
|
|
744
|
+
return true if capabilities.allows_attachment?(:image)
|
|
745
|
+
|
|
746
|
+
attached = ImageAttachments.references_from_text(input.to_s).any? { |reference| reference[:status] == :attached }
|
|
747
|
+
return true unless attached
|
|
748
|
+
|
|
749
|
+
runtime_output("This plugin chat does not allow image attachments.")
|
|
750
|
+
false
|
|
751
|
+
end
|
|
752
|
+
|
|
614
753
|
def start_tab_turn(tab, input, display_input: nil)
|
|
615
754
|
prepare_memory_context(tab.agent.conversation, input) if tab.agent.respond_to?(:conversation)
|
|
616
|
-
print_user_transcript(input, display_input: display_input) if prompt_interface?
|
|
755
|
+
print_user_transcript(input, display_input: display_input) if prompt_interface? && tab == active_tab
|
|
617
756
|
tab.status = "queued"
|
|
618
757
|
tab.unread = false
|
|
758
|
+
tab.attention = nil
|
|
619
759
|
tab.cancellation = Cancellation.new
|
|
620
|
-
|
|
760
|
+
steering_capability = tab.plugin_host&.capabilities
|
|
761
|
+
driver_supports_steering = if steering_capability&.declared?
|
|
762
|
+
steering_capability.steering?
|
|
763
|
+
else
|
|
764
|
+
tab.driver.supports_steering?
|
|
765
|
+
end
|
|
766
|
+
tab.steering = driver_supports_steering && steering_supported? ? Steering.new : nil
|
|
621
767
|
tab.error = nil
|
|
622
768
|
tab.answer = nil
|
|
623
769
|
tab.error_reported = false
|
|
@@ -639,14 +785,17 @@ module Kward
|
|
|
639
785
|
end
|
|
640
786
|
tab.status = "ready"
|
|
641
787
|
tab.unread = tab != active_tab
|
|
788
|
+
tab.attention = :output if tab != active_tab
|
|
642
789
|
rescue Cancellation::CancelledError
|
|
643
790
|
tab.status = "cancelled"
|
|
644
791
|
tab.unread = false
|
|
792
|
+
tab.attention = :failure if tab != active_tab
|
|
645
793
|
report_tab_runtime_error(tab)
|
|
646
794
|
rescue StandardError => e
|
|
647
795
|
tab.error = e.message
|
|
648
796
|
tab.status = "failed"
|
|
649
797
|
tab.unread = false
|
|
798
|
+
tab.attention = :failure if tab != active_tab
|
|
650
799
|
report_tab_runtime_error(tab)
|
|
651
800
|
ensure
|
|
652
801
|
finish_tab_turn(tab)
|
|
@@ -662,8 +811,9 @@ module Kward
|
|
|
662
811
|
end
|
|
663
812
|
|
|
664
813
|
def poll_active_tab_input
|
|
814
|
+
service_background_tab_runs
|
|
665
815
|
tab = active_tab
|
|
666
|
-
unless tab&.running?
|
|
816
|
+
unless tab&.running? || tab&.local_busy?
|
|
667
817
|
input = @prompt.ask("You>")
|
|
668
818
|
return { tab_action: :close } if input.nil? && tab && @tabs.length > 1
|
|
669
819
|
|
|
@@ -672,6 +822,7 @@ module Kward
|
|
|
672
822
|
|
|
673
823
|
@prompt.begin_busy_input("You>") if @prompt.respond_to?(:begin_busy_input)
|
|
674
824
|
loop do
|
|
825
|
+
service_background_tab_runs
|
|
675
826
|
refresh_active_tab
|
|
676
827
|
if service_active_tab_question
|
|
677
828
|
return next_tab_queued_input(tab) if tab.idle? && !tab.queued_inputs.empty?
|
|
@@ -679,6 +830,10 @@ module Kward
|
|
|
679
830
|
|
|
680
831
|
next
|
|
681
832
|
end
|
|
833
|
+
if @prompt.respond_to?(:modal_active?) && @prompt.modal_active?
|
|
834
|
+
sleep 0.01
|
|
835
|
+
next
|
|
836
|
+
end
|
|
682
837
|
poll_result = @prompt.poll_input
|
|
683
838
|
case poll_result
|
|
684
839
|
when Hash
|
|
@@ -687,8 +842,12 @@ module Kward
|
|
|
687
842
|
return poll_result
|
|
688
843
|
end
|
|
689
844
|
when PromptInterface::CANCEL_INPUT
|
|
690
|
-
tab.
|
|
691
|
-
|
|
845
|
+
if tab.background_run
|
|
846
|
+
tab.background_run[:run].cancel!
|
|
847
|
+
else
|
|
848
|
+
tab.cancellation&.cancel!
|
|
849
|
+
tab.thread&.raise(Cancellation::CancelledError, "cancelled") if tab.thread&.alive?
|
|
850
|
+
end
|
|
692
851
|
when PromptInterface::EXIT_INPUT
|
|
693
852
|
tab.queued_inputs << "/exit"
|
|
694
853
|
@prompt.set_queued_count(tab.queued_inputs.length) if @prompt.respond_to?(:set_queued_count)
|
|
@@ -716,8 +875,12 @@ module Kward
|
|
|
716
875
|
tab.queued_inputs << input
|
|
717
876
|
@prompt.set_queued_count(tab.queued_inputs.length) if @prompt.respond_to?(:set_queued_count)
|
|
718
877
|
return
|
|
719
|
-
elsif busy_tab_command?(input)
|
|
720
|
-
|
|
878
|
+
elsif busy_tab_command?(input) || busy_name_command?(input)
|
|
879
|
+
if busy_tab_command?(input)
|
|
880
|
+
handle_tab_command(input.to_s.strip.sub(%r{\A/tab(?:\s+|\z)}, ""), @session_store)
|
|
881
|
+
else
|
|
882
|
+
handle_local_slash_command(input, tab.agent, @session_store)
|
|
883
|
+
end
|
|
721
884
|
unless active_tab.equal?(tab)
|
|
722
885
|
@prompt.finish_busy_input if @prompt.respond_to?(:finish_busy_input)
|
|
723
886
|
return :active_tab_changed
|
|
@@ -749,6 +912,10 @@ module Kward
|
|
|
749
912
|
input.to_s.strip.match?(%r{\A/tab(?:\s|\z)})
|
|
750
913
|
end
|
|
751
914
|
|
|
915
|
+
def busy_name_command?(input)
|
|
916
|
+
input.to_s.strip.match?(%r{\A/name(?:\s|\z)})
|
|
917
|
+
end
|
|
918
|
+
|
|
752
919
|
def refresh_active_tab
|
|
753
920
|
tab = active_tab
|
|
754
921
|
return unless tab&.thread && !tab.thread.alive? && tab.running?
|
|
@@ -830,15 +997,17 @@ module Kward
|
|
|
830
997
|
return if tab.stream_state[:streamed]
|
|
831
998
|
return if tab.answer.to_s.empty?
|
|
832
999
|
|
|
833
|
-
@prompt.say("\n#{colored(
|
|
1000
|
+
@prompt.say("\n#{colored(transcript_marker("Assistant"), :activity, :bold)} #{render_markdown_transcript(tab.answer)}\n")
|
|
834
1001
|
end
|
|
835
1002
|
|
|
836
1003
|
def new_tab_stream_state(driver)
|
|
837
1004
|
{
|
|
838
1005
|
streamed: false,
|
|
1006
|
+
assistant_arrived: false,
|
|
839
1007
|
last_flush: monotonic_now,
|
|
840
1008
|
stream_block_open: false,
|
|
841
1009
|
markdown_streams: {},
|
|
1010
|
+
tool_elapsed_ms: {},
|
|
842
1011
|
defer_assistant_streaming: driver.respond_to?(:conversation) && defer_assistant_streaming?(driver.agent)
|
|
843
1012
|
}
|
|
844
1013
|
end
|
|
@@ -853,10 +1022,11 @@ module Kward
|
|
|
853
1022
|
end
|
|
854
1023
|
|
|
855
1024
|
def tab_label_color(tab)
|
|
856
|
-
return :
|
|
857
|
-
return :
|
|
858
|
-
return :
|
|
859
|
-
return :
|
|
1025
|
+
return :success if tab.status.to_s == "waiting_for_question"
|
|
1026
|
+
return :caution if tab.running? || tab.local_busy?
|
|
1027
|
+
return :failure if %w[failed cancelled].include?(tab.status.to_s) || tab.attention == :failure
|
|
1028
|
+
return :success if tab.attention == :success
|
|
1029
|
+
return :success if tab.unread
|
|
860
1030
|
|
|
861
1031
|
nil
|
|
862
1032
|
end
|
|
@@ -906,7 +1076,7 @@ module Kward
|
|
|
906
1076
|
end
|
|
907
1077
|
|
|
908
1078
|
def tab_usage
|
|
909
|
-
"/tab 1-n | /tab move 1-n|left|right | /tab close | /tab new | /tab open <plugin-tab> | /tab name <label> | /tab worktree [activate|detach|status|merge|
|
|
1079
|
+
"/tab 1-n | /tab move 1-n|left|right | /tab close | /tab new | /tab open <plugin-tab> | /tab name <label> | /tab worktree [activate|detach|status|merge [resolve|continue|abort]|remove]"
|
|
910
1080
|
end
|
|
911
1081
|
|
|
912
1082
|
def switch_tab_number(number)
|
|
@@ -970,17 +1140,58 @@ module Kward
|
|
|
970
1140
|
tab.thread&.raise(Cancellation::CancelledError, "cancelled") if tab.thread&.alive?
|
|
971
1141
|
tab.thread&.join(0.2)
|
|
972
1142
|
end
|
|
973
|
-
|
|
1143
|
+
if (background_run = tab.background_run&.fetch(:run))
|
|
1144
|
+
background_run.cancel!
|
|
1145
|
+
background_run.join(0.5)
|
|
1146
|
+
end
|
|
1147
|
+
tab.background_run = nil
|
|
1148
|
+
close_kwsh_session(tab.shell) if tab&.shell && respond_to?(:close_kwsh_session, true)
|
|
1149
|
+
close_plugin_tab(tab)
|
|
974
1150
|
rescue StandardError
|
|
975
1151
|
nil
|
|
976
1152
|
end
|
|
977
1153
|
end
|
|
978
1154
|
|
|
1155
|
+
def build_plugin_tab_host(tab_type, workspace_root:, scope_key:)
|
|
1156
|
+
PluginTabHost.new(
|
|
1157
|
+
client: @client,
|
|
1158
|
+
workspace_root: workspace_root,
|
|
1159
|
+
plugin_host: tab_type.plugin_id && plugin_registry.plugin_for(tab_type.plugin_id),
|
|
1160
|
+
type_id: tab_type.id,
|
|
1161
|
+
surface: :local,
|
|
1162
|
+
scope_key: scope_key,
|
|
1163
|
+
capabilities: tab_type.capabilities
|
|
1164
|
+
)
|
|
1165
|
+
end
|
|
1166
|
+
|
|
1167
|
+
def plugin_tab_scope_key(tab_type)
|
|
1168
|
+
tab_type.singleton == :global ? "global" : SecureRandom.uuid
|
|
1169
|
+
end
|
|
1170
|
+
|
|
1171
|
+
def close_plugin_tab(tab)
|
|
1172
|
+
return unless tab&.plugin_host
|
|
1173
|
+
|
|
1174
|
+
if tab.driver.respond_to?(:close)
|
|
1175
|
+
tab.driver.close
|
|
1176
|
+
elsif tab.driver.respond_to?(:shutdown)
|
|
1177
|
+
tab.driver.shutdown
|
|
1178
|
+
end
|
|
1179
|
+
rescue StandardError => e
|
|
1180
|
+
runtime_output("Plugin tab cleanup error: #{e.message}")
|
|
1181
|
+
ensure
|
|
1182
|
+
tab&.plugin_host&.shutdown
|
|
1183
|
+
tab.plugin_host = nil if tab
|
|
1184
|
+
end
|
|
1185
|
+
|
|
979
1186
|
def persist_tabs
|
|
980
1187
|
return unless @tab_store
|
|
981
1188
|
|
|
982
1189
|
@tab_store.save(
|
|
983
|
-
tabs: @tabs.map
|
|
1190
|
+
tabs: @tabs.map do |tab|
|
|
1191
|
+
descriptor = tab.driver.descriptor.merge("label" => tab.label.to_s)
|
|
1192
|
+
descriptor["scope_key"] ||= tab.plugin_host.scope_key if tab.plugin_host
|
|
1193
|
+
descriptor
|
|
1194
|
+
end,
|
|
984
1195
|
active_index: @active_tab_index
|
|
985
1196
|
)
|
|
986
1197
|
end
|