samagotchi 0.22.0 → 0.24.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 +72 -1
- data/bin/chi +9 -4
- data/docs/cli.md +6 -1
- data/docs/configuration.md +13 -4
- data/docs/hooks.md +20 -6
- data/docs/plugins.md +1 -1
- data/docs/releasing.md +1 -1
- data/docs/sub-agent.md +4 -3
- data/docs/testing.md +6 -0
- data/lib/samagotchi/answer_command.rb +4 -2
- data/lib/samagotchi/bootstrap/config_writer.rb +2 -1
- data/lib/samagotchi/bootstrap/probe.rb +1 -1
- data/lib/samagotchi/bootstrap_command.rb +2 -2
- data/lib/samagotchi/bridge/bounded_queue.rb +0 -2
- data/lib/samagotchi/bridge/card_store.rb +4 -3
- data/lib/samagotchi/bridge/sse_writer.rb +0 -6
- data/lib/samagotchi/bridge/turn_accumulator.rb +6 -3
- data/lib/samagotchi/bridge/turn_notice.rb +1 -0
- data/lib/samagotchi/bridge.rb +19 -9
- data/lib/samagotchi/bridge_client/event_stream.rb +2 -2
- data/lib/samagotchi/bridge_client.rb +1 -1
- data/lib/samagotchi/bundle_command.rb +39 -29
- data/lib/samagotchi/bundles/btw/manifest.yml +2 -2
- data/lib/samagotchi/bundles/btw/plugin.rb +2 -0
- data/lib/samagotchi/bundles/check-in/manifest.yml +2 -2
- data/lib/samagotchi/bundles/check-in/plugin.rb +4 -1
- data/lib/samagotchi/bundles/known-names/hooks/known_names.rb +6 -5
- data/lib/samagotchi/bundles/known-names/manifest.yml +2 -2
- data/lib/samagotchi/bundles/loop-guard/manifest.yml +2 -2
- data/lib/samagotchi/bundles/loop-guard/plugin.rb +2 -0
- data/lib/samagotchi/bundles/mcp/manifest.yml +2 -2
- data/lib/samagotchi/bundles/mcp/plugin.rb +2 -0
- data/lib/samagotchi/bundles/skills/manifest.yml +2 -2
- data/lib/samagotchi/bundles/skills/plugin.rb +5 -3
- data/lib/samagotchi/bundles/source-links/hooks/source_links.rb +7 -7
- data/lib/samagotchi/bundles/source-links/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +1 -1
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/cancellation_controller.rb +1 -1
- data/lib/samagotchi/cli/command.rb +1 -1
- data/lib/samagotchi/client.rb +3 -5
- data/lib/samagotchi/commands/registry.rb +2 -2
- data/lib/samagotchi/config.rb +42 -14
- data/lib/samagotchi/context_note.rb +1 -1
- data/lib/samagotchi/context_status.rb +6 -6
- data/lib/samagotchi/continue_offer.rb +15 -8
- data/lib/samagotchi/debug_log.rb +1 -1
- data/lib/samagotchi/desktop/macos.rb +5 -4
- data/lib/samagotchi/empty_answer_retry.rb +1 -1
- data/lib/samagotchi/engine.rb +97 -18
- data/lib/samagotchi/gem_update.rb +2 -1
- data/lib/samagotchi/generation_phase.rb +83 -0
- data/lib/samagotchi/guardrail_wiring.rb +1 -1
- data/lib/samagotchi/guardrails/approval.rb +5 -5
- data/lib/samagotchi/guardrails/approvals.rb +2 -2
- data/lib/samagotchi/guardrails/gate.rb +2 -2
- data/lib/samagotchi/guardrails/read_only_shell.rb +5 -3
- data/lib/samagotchi/guardrails/shell_git_dirs.rb +1 -1
- data/lib/samagotchi/guardrails/shell_lex.rb +113 -5
- data/lib/samagotchi/guardrails/shell_paths.rb +3 -3
- data/lib/samagotchi/hooks/bundle_loader.rb +9 -6
- data/lib/samagotchi/hooks/loader.rb +18 -16
- data/lib/samagotchi/hooks/registry.rb +10 -2
- data/lib/samagotchi/host_registry.rb +1 -3
- data/lib/samagotchi/idle_recap.rb +22 -17
- data/lib/samagotchi/idle_reminders.rb +6 -3
- data/lib/samagotchi/image_store.rb +5 -2
- data/lib/samagotchi/kernel_loop.rb +50 -40
- data/lib/samagotchi/llm/chat_loop.rb +29 -16
- data/lib/samagotchi/llm/errors.rb +2 -2
- data/lib/samagotchi/llm/http.rb +12 -12
- data/lib/samagotchi/llm/openai_chat.rb +42 -28
- data/lib/samagotchi/llm/prompt_cache.rb +1 -1
- data/lib/samagotchi/log.rb +2 -2
- data/lib/samagotchi/log_subscriber.rb +5 -3
- data/lib/samagotchi/memory_bundle/builder.rb +9 -3
- data/lib/samagotchi/memory_bundle/index_updater.rb +13 -1
- data/lib/samagotchi/memory_bundle/installer.rb +67 -57
- data/lib/samagotchi/memory_bundle/listing.rb +5 -1
- data/lib/samagotchi/memory_bundle/manifest.rb +14 -4
- data/lib/samagotchi/memory_bundle/merger.rb +6 -4
- data/lib/samagotchi/memory_bundle/provenance.rb +16 -13
- data/lib/samagotchi/memory_bundle/source.rb +7 -3
- data/lib/samagotchi/memory_bundle/status.rb +3 -1
- data/lib/samagotchi/memory_bundle/system_bundle.rb +3 -2
- data/lib/samagotchi/memory_bundle/trash.rb +5 -2
- data/lib/samagotchi/memory_bundle/uninstaller.rb +9 -6
- data/lib/samagotchi/memory_bundle.rb +1 -0
- data/lib/samagotchi/model_overlay.rb +3 -5
- data/lib/samagotchi/model_profile.rb +12 -8
- data/lib/samagotchi/note_command.rb +2 -1
- data/lib/samagotchi/output_formatter.rb +18 -17
- data/lib/samagotchi/owner_lock.rb +2 -1
- data/lib/samagotchi/plugin/api.rb +8 -5
- data/lib/samagotchi/plugin/context.rb +1 -3
- data/lib/samagotchi/plugin/loader.rb +1 -1
- data/lib/samagotchi/plugin/service.rb +1 -1
- data/lib/samagotchi/plugin/sessions.rb +1 -0
- data/lib/samagotchi/plugin_tasks.rb +1 -1
- data/lib/samagotchi/prompt.rb +72 -13
- data/lib/samagotchi/prompt_literal_guard.rb +1 -1
- data/lib/samagotchi/question_desk.rb +7 -7
- data/lib/samagotchi/recap_setup.rb +1 -1
- data/lib/samagotchi/relay_desk.rb +1 -1
- data/lib/samagotchi/reminder_store.rb +2 -1
- data/lib/samagotchi/send_command.rb +4 -2
- data/lib/samagotchi/session.rb +2 -2
- data/lib/samagotchi/session_archive_command.rb +5 -1
- data/lib/samagotchi/session_commands.rb +1 -3
- data/lib/samagotchi/session_manager.rb +2 -4
- data/lib/samagotchi/session_metrics.rb +10 -10
- data/lib/samagotchi/session_observer.rb +3 -2
- data/lib/samagotchi/session_retention.rb +2 -2
- data/lib/samagotchi/sessions_command.rb +14 -5
- data/lib/samagotchi/steer.rb +103 -8
- data/lib/samagotchi/system_prompt.rb +19 -8
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +5 -5
- data/lib/samagotchi/terminal_ui/attached_loop.rb +3 -3
- data/lib/samagotchi/terminal_ui/event_renderer.rb +4 -2
- data/lib/samagotchi/terminal_ui/formatting.rb +19 -10
- data/lib/samagotchi/terminal_ui/input_support.rb +2 -2
- data/lib/samagotchi/terminal_ui/line_reader.rb +3 -3
- data/lib/samagotchi/terminal_ui/one_shot_sink.rb +1 -0
- data/lib/samagotchi/terminal_ui/question_prompt.rb +23 -23
- data/lib/samagotchi/terminal_ui/question_slot.rb +10 -10
- data/lib/samagotchi/terminal_ui/reline_seam.rb +1 -1
- data/lib/samagotchi/terminal_ui/repl_input.rb +3 -2
- data/lib/samagotchi/terminal_ui.rb +14 -5
- data/lib/samagotchi/text_diff.rb +12 -12
- data/lib/samagotchi/thinking.rb +6 -2
- data/lib/samagotchi/thought_stream_splitter.rb +15 -6
- data/lib/samagotchi/tool_activity.rb +1 -0
- data/lib/samagotchi/tool_call_parser.rb +13 -14
- data/lib/samagotchi/tool_declarations.rb +36 -35
- data/lib/samagotchi/tool_runner.rb +7 -0
- data/lib/samagotchi/tool_view.rb +40 -0
- data/lib/samagotchi/tools/ask_user_question.rb +3 -4
- data/lib/samagotchi/tools/builtin_calls.rb +1 -1
- data/lib/samagotchi/tools/delegate.rb +1 -1
- data/lib/samagotchi/tools/delegate_relay.rb +1 -1
- data/lib/samagotchi/tools/delegate_wait.rb +0 -1
- data/lib/samagotchi/tools/edit.rb +14 -13
- data/lib/samagotchi/tools/execute.rb +3 -5
- data/lib/samagotchi/tools/list_reminders.rb +1 -1
- data/lib/samagotchi/tools/memory.rb +21 -22
- data/lib/samagotchi/tools/read.rb +7 -7
- data/lib/samagotchi/tools/task_create.rb +1 -1
- data/lib/samagotchi/tools/task_get.rb +1 -1
- data/lib/samagotchi/tools/task_list.rb +1 -1
- data/lib/samagotchi/tools/task_runtime.rb +8 -9
- data/lib/samagotchi/tools/task_stop.rb +1 -1
- data/lib/samagotchi/tools/task_wait.rb +3 -4
- data/lib/samagotchi/tools/web_fetch.rb +2 -4
- data/lib/samagotchi/tools/write.rb +1 -1
- data/lib/samagotchi/turn_flow.rb +8 -8
- data/lib/samagotchi/turn_note.rb +5 -1
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/vision_support.rb +2 -2
- data/lib/samagotchi/waiting_steer.rb +64 -0
- data/lib/samagotchi/web/app.rb +13 -6
- data/lib/samagotchi/web/markdown_renderer.rb +2 -1
- data/lib/samagotchi/web/message_parts.rb +5 -0
- data/lib/samagotchi/web/public/activity.js +5 -0
- data/lib/samagotchi/web/public/app.js +2 -1
- data/lib/samagotchi/web/public/command_view.js +28 -0
- data/lib/samagotchi/web/public/copy.js +7 -0
- data/lib/samagotchi/web/public/index.html +8 -0
- data/lib/samagotchi/web/public/stage_model.js +11 -4
- data/lib/samagotchi/web/public/stage_view.js +5 -2
- data/lib/samagotchi/web/public/turn_events.js +15 -1
- data/lib/samagotchi/web/public/turn_view.js +15 -6
- data/lib/samagotchi/web/qr.rb +7 -1
- data/lib/samagotchi/web/server.rb +6 -2
- data/lib/samagotchi/worker.rb +14 -3
- metadata +24 -20
|
@@ -44,7 +44,8 @@ module Samagotchi
|
|
|
44
44
|
|
|
45
45
|
FileUtils.mkdir_p(task_dir)
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
# The child writes to it; the ensure below closes the parent's copy.
|
|
48
|
+
output_io = File.open(output_path, "a") # rubocop:disable Style/FileOpen
|
|
48
49
|
output_io.sync = true
|
|
49
50
|
|
|
50
51
|
wrapped_command = wrapped_shell_command(normalized_command, exit_code_path)
|
|
@@ -80,7 +81,7 @@ module Samagotchi
|
|
|
80
81
|
|
|
81
82
|
write_record(record)
|
|
82
83
|
[record, nil]
|
|
83
|
-
rescue => e
|
|
84
|
+
rescue StandardError => e
|
|
84
85
|
[nil, "Error: #{e.message}"]
|
|
85
86
|
ensure
|
|
86
87
|
output_io&.close
|
|
@@ -124,7 +125,7 @@ module Samagotchi
|
|
|
124
125
|
return [nil, "Error: task not found: #{task_id}"] unless record
|
|
125
126
|
|
|
126
127
|
[refresh_record(record), nil]
|
|
127
|
-
rescue => e
|
|
128
|
+
rescue StandardError => e
|
|
128
129
|
[nil, "Error: #{e.message}"]
|
|
129
130
|
end
|
|
130
131
|
|
|
@@ -157,9 +158,7 @@ module Samagotchi
|
|
|
157
158
|
end
|
|
158
159
|
|
|
159
160
|
deadline = monotonic_time + STOP_GRACE_SEC
|
|
160
|
-
while process_alive?(pid) && monotonic_time < deadline
|
|
161
|
-
sleep(STOP_POLL_INTERVAL_SEC)
|
|
162
|
-
end
|
|
161
|
+
sleep(STOP_POLL_INTERVAL_SEC) while process_alive?(pid) && monotonic_time < deadline
|
|
163
162
|
|
|
164
163
|
if process_alive?(pid)
|
|
165
164
|
begin
|
|
@@ -176,7 +175,7 @@ module Samagotchi
|
|
|
176
175
|
write_record(updated)
|
|
177
176
|
|
|
178
177
|
[updated, nil]
|
|
179
|
-
rescue => e
|
|
178
|
+
rescue StandardError => e
|
|
180
179
|
[nil, "Error: #{e.message}"]
|
|
181
180
|
end
|
|
182
181
|
|
|
@@ -278,7 +277,7 @@ module Samagotchi
|
|
|
278
277
|
return nil if rubyopt.empty?
|
|
279
278
|
|
|
280
279
|
filtered = rubyopt.split(/\s+/).reject do |arg|
|
|
281
|
-
arg == "-rbundler/setup" || arg.match?(
|
|
280
|
+
arg == "-rbundler/setup" || arg.match?(%r{\A-r.*bundler/setup\z})
|
|
282
281
|
end
|
|
283
282
|
return nil if filtered.empty?
|
|
284
283
|
|
|
@@ -318,7 +317,7 @@ module Samagotchi
|
|
|
318
317
|
return nil if path.to_s.empty? || !File.exist?(path)
|
|
319
318
|
|
|
320
319
|
Integer(File.read(path).strip, exception: false)
|
|
321
|
-
rescue => _e
|
|
320
|
+
rescue StandardError => _e
|
|
322
321
|
nil
|
|
323
322
|
end
|
|
324
323
|
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
|
|
2
1
|
# frozen_string_literal: true
|
|
3
2
|
|
|
4
3
|
require_relative "task_runtime"
|
|
@@ -48,16 +47,17 @@ module Samagotchi
|
|
|
48
47
|
return cancelled_response(task_id, tail_lines, started) if cancelled.call
|
|
49
48
|
|
|
50
49
|
break if monotonic_time > deadline
|
|
50
|
+
|
|
51
51
|
sleep(POLL_INTERVAL)
|
|
52
52
|
end
|
|
53
53
|
|
|
54
54
|
# Timeout reached — do one final refresh to capture the latest status
|
|
55
|
-
refreshed,
|
|
55
|
+
refreshed, = TaskRuntime.get_record(task_id)
|
|
56
56
|
return format_response(refreshed) unless refreshed.fetch("status") == "running"
|
|
57
57
|
|
|
58
58
|
output_tail = TaskRuntime.output_tail_lines(refreshed.fetch("output_path"), tail_lines)
|
|
59
59
|
format_response(refreshed, wait_result: "timeout", output_tail: output_tail)
|
|
60
|
-
rescue => e
|
|
60
|
+
rescue StandardError => e
|
|
61
61
|
"Error: #{e.message}"
|
|
62
62
|
end
|
|
63
63
|
|
|
@@ -125,4 +125,3 @@ module Samagotchi
|
|
|
125
125
|
end
|
|
126
126
|
end
|
|
127
127
|
end
|
|
128
|
-
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
|
|
2
1
|
# frozen_string_literal: true
|
|
3
2
|
|
|
4
3
|
require "net/http"
|
|
@@ -42,7 +41,7 @@ module Samagotchi
|
|
|
42
41
|
"Error: could not resolve hostname"
|
|
43
42
|
rescue Timeout::Error
|
|
44
43
|
"Error: request timed out after #{TIMEOUT_SEC}s"
|
|
45
|
-
rescue => e
|
|
44
|
+
rescue StandardError => e
|
|
46
45
|
"Error: #{e.message}"
|
|
47
46
|
end
|
|
48
47
|
|
|
@@ -59,7 +58,7 @@ module Samagotchi
|
|
|
59
58
|
IPAddr.new("::1/128"),
|
|
60
59
|
IPAddr.new("fc00::/7"),
|
|
61
60
|
IPAddr.new("fe80::/10"),
|
|
62
|
-
IPAddr.new("::ffff:0:0/96")
|
|
61
|
+
IPAddr.new("::ffff:0:0/96")
|
|
63
62
|
]
|
|
64
63
|
|
|
65
64
|
# Try to parse host as IP directly
|
|
@@ -160,4 +159,3 @@ module Samagotchi
|
|
|
160
159
|
end
|
|
161
160
|
end
|
|
162
161
|
end
|
|
163
|
-
|
data/lib/samagotchi/turn_flow.rb
CHANGED
|
@@ -27,10 +27,10 @@ module Samagotchi
|
|
|
27
27
|
return [:resume, nil] if normalized.empty?
|
|
28
28
|
|
|
29
29
|
lowered = normalized.downcase
|
|
30
|
-
return [:resume, nil] if
|
|
31
|
-
return [:abort, nil] if
|
|
30
|
+
return [:resume, nil] if [CONTINUE_COMMAND, "yes", "y"].include?(lowered)
|
|
31
|
+
return [:abort, nil] if %w[no n].include?(lowered)
|
|
32
32
|
|
|
33
|
-
reason_match = normalized.match(/\A(?:no|n)\s*[
|
|
33
|
+
reason_match = normalized.match(/\A(?:no|n)\s*[,:-]\s*(.+)\z/i)
|
|
34
34
|
if reason_match
|
|
35
35
|
reason = reason_match[1].to_s.strip
|
|
36
36
|
return [:abort, nil] if reason.empty?
|
|
@@ -220,11 +220,11 @@ module Samagotchi
|
|
|
220
220
|
lines << "- original_prompt: #{original_prompt.to_s.empty? ? "(unavailable)" : original_prompt}"
|
|
221
221
|
|
|
222
222
|
tool_trace = context ? Array(context[:tool_trace]) : []
|
|
223
|
-
if tool_trace.empty?
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
223
|
+
lines << if tool_trace.empty?
|
|
224
|
+
"- interrupted_tools: (none)"
|
|
225
|
+
else
|
|
226
|
+
"- interrupted_tools: #{tool_trace.join("; ")}"
|
|
227
|
+
end
|
|
228
228
|
|
|
229
229
|
model_intent = context && context[:last_model_intent]
|
|
230
230
|
lines << "- last_model_intent: #{model_intent.to_s.empty? ? "(unavailable)" : model_intent}"
|
data/lib/samagotchi/turn_note.rb
CHANGED
|
@@ -98,7 +98,11 @@ module Samagotchi
|
|
|
98
98
|
# emptyAnswerLine words it the same).
|
|
99
99
|
def empty_answer_line(retries)
|
|
100
100
|
count = retries.to_i
|
|
101
|
-
after = count.positive?
|
|
101
|
+
after = if count.positive?
|
|
102
|
+
" (after #{count} #{count == 1 ? "retry" : "retries"})"
|
|
103
|
+
else
|
|
104
|
+
""
|
|
105
|
+
end
|
|
102
106
|
"no answer: the model returned nothing#{after}"
|
|
103
107
|
end
|
|
104
108
|
|
data/lib/samagotchi/version.rb
CHANGED
|
@@ -45,7 +45,7 @@ module Samagotchi
|
|
|
45
45
|
marker.is_a?(String) && !marker.empty? ? marker : nil
|
|
46
46
|
end
|
|
47
47
|
|
|
48
|
-
def self.configured(
|
|
48
|
+
def self.configured(_target, entry, models, names)
|
|
49
49
|
models ||= begin
|
|
50
50
|
ConfigFile.model_settings
|
|
51
51
|
rescue StandardError
|
|
@@ -58,7 +58,7 @@ module Samagotchi
|
|
|
58
58
|
Answer.new(value: entry.vision, reason: entry.vision ? nil : "hosts.#{entry.name} sets vision: false")
|
|
59
59
|
end
|
|
60
60
|
|
|
61
|
-
def self.native(target,
|
|
61
|
+
def self.native(target, _entry, profile, trust_config:)
|
|
62
62
|
client = target.client
|
|
63
63
|
transport = client.transport.name
|
|
64
64
|
return no("#{transport} hosts take no images (only llama.cpp does)") unless transport == :llama_cpp
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Samagotchi
|
|
4
|
+
# A message for the running turn that may cut its generation but came too
|
|
5
|
+
# early (the generation hadn't streamed only thinking for long enough):
|
|
6
|
+
# it waits here until the thinking passes steer.cut_after (the Engine
|
|
7
|
+
# checks on each thinking chunk), or until a drain hands the message to
|
|
8
|
+
# the model at a boundary, or the turn ends. One cut covers every waiting
|
|
9
|
+
# message: the first one's source names it.
|
|
10
|
+
#
|
|
11
|
+
# The epoch counts the drains that took input. A caller reads it before
|
|
12
|
+
# it queues its message; a drain after that may have taken the message
|
|
13
|
+
# already, so the message doesn't wait (it may be in the model's prompt).
|
|
14
|
+
# Written from the UIs' threads and the turn's; its own Mutex.
|
|
15
|
+
class WaitingSteer
|
|
16
|
+
def initialize
|
|
17
|
+
@mutex = Mutex.new
|
|
18
|
+
@source = nil
|
|
19
|
+
@epoch = 0
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @return [Integer] the drains that took input so far
|
|
23
|
+
def epoch
|
|
24
|
+
@mutex.synchronize { @epoch }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# +source+'s message waits for a cut, unless a drain took input since
|
|
28
|
+
# +epoch+. Returns whether it waits.
|
|
29
|
+
def wait!(source, epoch: nil)
|
|
30
|
+
@mutex.synchronize do
|
|
31
|
+
next false if epoch && epoch != @epoch
|
|
32
|
+
|
|
33
|
+
@source ||= source.to_s
|
|
34
|
+
true
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def waiting?
|
|
39
|
+
@mutex.synchronize { !@source.nil? }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# The waiting source, taken (nil when none waits).
|
|
43
|
+
def take
|
|
44
|
+
@mutex.synchronize do
|
|
45
|
+
source = @source
|
|
46
|
+
@source = nil
|
|
47
|
+
source
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# A drain took input: what waited is delivered.
|
|
52
|
+
def delivered!
|
|
53
|
+
@mutex.synchronize do
|
|
54
|
+
@epoch += 1
|
|
55
|
+
@source = nil
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# The turn ended, or a cut happened: nothing waits.
|
|
60
|
+
def clear!
|
|
61
|
+
@mutex.synchronize { @source = nil }
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
data/lib/samagotchi/web/app.rb
CHANGED
|
@@ -157,6 +157,7 @@ module Samagotchi
|
|
|
157
157
|
req = Rack::Request.new(env)
|
|
158
158
|
return forbidden unless local_host_header?(env)
|
|
159
159
|
return cross_origin if cross_site?(env)
|
|
160
|
+
|
|
160
161
|
if @lan && !LOOPBACK_PEERS.include?(env["REMOTE_ADDR"].to_s)
|
|
161
162
|
denied = token_gate(req)
|
|
162
163
|
return denied if denied
|
|
@@ -506,13 +507,16 @@ module Samagotchi
|
|
|
506
507
|
|
|
507
508
|
def sanitize_limit(val)
|
|
508
509
|
return nil if val.nil? || val.to_s.strip.empty?
|
|
510
|
+
|
|
509
511
|
n = val.to_i
|
|
510
512
|
return nil if n <= 0
|
|
513
|
+
|
|
511
514
|
[n, 1000].min
|
|
512
515
|
end
|
|
513
516
|
|
|
514
517
|
def sanitize_offset(val)
|
|
515
518
|
return 0 if val.nil? || val.to_s.strip.empty?
|
|
519
|
+
|
|
516
520
|
n = val.to_i
|
|
517
521
|
n.positive? ? n : 0
|
|
518
522
|
end
|
|
@@ -522,6 +526,7 @@ module Samagotchi
|
|
|
522
526
|
unless body.is_a?(Hash)
|
|
523
527
|
return error_response(400, "invalid_json", "invalid JSON body")
|
|
524
528
|
end
|
|
529
|
+
|
|
525
530
|
prompt = body["prompt"] || body[:prompt]
|
|
526
531
|
# idle: a session with no first turn, for a first message with
|
|
527
532
|
# images (the page uploads them into it, then sends the turn).
|
|
@@ -529,6 +534,7 @@ module Samagotchi
|
|
|
529
534
|
if prompt.to_s.strip.empty? && !idle
|
|
530
535
|
return error_response(400, "missing_fields", "prompt is required")
|
|
531
536
|
end
|
|
537
|
+
|
|
532
538
|
# dir: the folder the chat starts in (the page's scope); without it,
|
|
533
539
|
# the server's own cwd.
|
|
534
540
|
dir, error = scope_dir(body["dir"])
|
|
@@ -891,11 +897,13 @@ module Samagotchi
|
|
|
891
897
|
unless body.is_a?(Hash)
|
|
892
898
|
return error_response(400, "invalid_json", "invalid JSON body")
|
|
893
899
|
end
|
|
900
|
+
|
|
894
901
|
prompt = body["prompt"] || body[:prompt]
|
|
895
902
|
client_id = body["client_id"] || body[:client_id]
|
|
896
903
|
if prompt.to_s.strip.empty?
|
|
897
904
|
return error_response(400, "missing_fields", "prompt is required")
|
|
898
905
|
end
|
|
906
|
+
|
|
899
907
|
images = turn_images(id, body["images"])
|
|
900
908
|
return error_response(400, "bad_images", images) if images.is_a?(String)
|
|
901
909
|
|
|
@@ -1173,7 +1181,7 @@ module Samagotchi
|
|
|
1173
1181
|
lambda do |io|
|
|
1174
1182
|
query = req.query_string.to_s.empty? ? "" : "?#{req.query_string}"
|
|
1175
1183
|
body = ProxyStreamBody.new(host: DEFAULT_HOST, port: port, session_id: id, query: query, headers: req.env,
|
|
1176
|
-
|
|
1184
|
+
server_running: @server_running)
|
|
1177
1185
|
body.each { |chunk| io.write(chunk) }
|
|
1178
1186
|
rescue Errno::EPIPE, Errno::ECONNRESET, IOError
|
|
1179
1187
|
nil # client went away — end the stream quietly
|
|
@@ -1249,7 +1257,7 @@ module Samagotchi
|
|
|
1249
1257
|
def serve_static(req)
|
|
1250
1258
|
# Serve files under public_dir for /assets/* and bare paths
|
|
1251
1259
|
rel = req.path_info.sub(%r{\A/(assets|public)/}, "")
|
|
1252
|
-
rel = req.path_info.
|
|
1260
|
+
rel = req.path_info.delete_prefix("/") if rel == req.path_info
|
|
1253
1261
|
# Prevent directory traversal
|
|
1254
1262
|
rel = rel.split("/").reject { |p| p == ".." || p.empty? }.join("/")
|
|
1255
1263
|
full = File.join(@public_dir, rel)
|
|
@@ -1483,7 +1491,7 @@ module Samagotchi
|
|
|
1483
1491
|
|
|
1484
1492
|
norm_role = role == "model" ? "assistant" : role
|
|
1485
1493
|
# normalize assistant vs model, keep user as is
|
|
1486
|
-
norm_role = "assistant" if
|
|
1494
|
+
norm_role = "assistant" if %w[assistant model].include?(norm_role)
|
|
1487
1495
|
norm_role = "user" if norm_role == "user"
|
|
1488
1496
|
next unless %w[user assistant].include?(norm_role)
|
|
1489
1497
|
|
|
@@ -1644,7 +1652,6 @@ module Samagotchi
|
|
|
1644
1652
|
json_response(status, { error: code, detail: detail })
|
|
1645
1653
|
end
|
|
1646
1654
|
|
|
1647
|
-
|
|
1648
1655
|
def not_found(path:)
|
|
1649
1656
|
error_response(404, "not_found", "not found: #{path}")
|
|
1650
1657
|
end
|
|
@@ -1740,12 +1747,12 @@ module Samagotchi
|
|
|
1740
1747
|
@server_running = server_running
|
|
1741
1748
|
end
|
|
1742
1749
|
|
|
1743
|
-
def each(&
|
|
1750
|
+
def each(&)
|
|
1744
1751
|
# Forward the browser's auto-reconnect cursor: the bridge prefers the
|
|
1745
1752
|
# Last-Event-ID header over ?from_seq, and the reconnect URL carries a
|
|
1746
1753
|
# stale initial cursor — without this the bridge would replay content
|
|
1747
1754
|
# already delivered (duplicate bubbles).
|
|
1748
|
-
@client.stream(query: @query, last_event_id: @headers["HTTP_LAST_EVENT_ID"], running: @server_running, &
|
|
1755
|
+
@client.stream(query: @query, last_event_id: @headers["HTTP_LAST_EVENT_ID"], running: @server_running, &)
|
|
1749
1756
|
end
|
|
1750
1757
|
end
|
|
1751
1758
|
end
|
|
@@ -34,7 +34,7 @@ module Samagotchi
|
|
|
34
34
|
|
|
35
35
|
html = Commonmarker.to_html(markdown.to_s, options: {
|
|
36
36
|
render: { escape: true }, # escape raw HTML from source; highlighter output is still emitted
|
|
37
|
-
extension: { header_ids: nil }
|
|
37
|
+
extension: { header_ids: nil }
|
|
38
38
|
})
|
|
39
39
|
return nil unless html
|
|
40
40
|
|
|
@@ -71,6 +71,7 @@ module Samagotchi
|
|
|
71
71
|
def sanitize_attributes(node)
|
|
72
72
|
node.attribute_nodes.each do |attribute|
|
|
73
73
|
next if allowed_attribute?(node, attribute.name)
|
|
74
|
+
|
|
74
75
|
node.remove_attribute(attribute.name)
|
|
75
76
|
end
|
|
76
77
|
return unless node.name == "a"
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require_relative "../model_profile"
|
|
4
4
|
require_relative "../tool_call_parser"
|
|
5
5
|
require_relative "../tool_activity"
|
|
6
|
+
require_relative "../tool_view"
|
|
6
7
|
require_relative "../llm/native_tool_normalizer"
|
|
7
8
|
require_relative "../tools/builtins"
|
|
8
9
|
require_relative "../image_store"
|
|
@@ -158,6 +159,10 @@ module Samagotchi
|
|
|
158
159
|
part = { tool: name, params: params.to_s }
|
|
159
160
|
title = ToolActivity.tool_title(name, call, cwd: cwd)
|
|
160
161
|
part[:title] = title if title
|
|
162
|
+
# Built from the saved call (never saved itself), so older sessions
|
|
163
|
+
# get it too.
|
|
164
|
+
view = ToolView.for(name, call)&.to_h
|
|
165
|
+
part[:view] = view if view
|
|
161
166
|
part[:label] = label if label.is_a?(String) && !label.empty?
|
|
162
167
|
unless output.nil?
|
|
163
168
|
part[:output] = output.length > OUTPUT_MAX ? output[0, OUTPUT_MAX] : output
|
|
@@ -50,6 +50,7 @@ export function addStarted(model, event = {}) {
|
|
|
50
50
|
row.label = event.label || row.label;
|
|
51
51
|
row.params = event.params || row.params;
|
|
52
52
|
row.title = event.title || row.title;
|
|
53
|
+
row.view = event.view || row.view;
|
|
53
54
|
return { row, created: false };
|
|
54
55
|
}
|
|
55
56
|
row = {
|
|
@@ -63,6 +64,9 @@ export function addStarted(model, event = {}) {
|
|
|
63
64
|
// project-relative path, a command without its "cd … &&"); rows show it
|
|
64
65
|
// in place of the params when there is one.
|
|
65
66
|
title: event.title || null,
|
|
67
|
+
// The call's full arguments for the expanded row and the hovers (the
|
|
68
|
+
// server's ToolView: an execute/task_create's command, its cwd).
|
|
69
|
+
view: event.view || null,
|
|
66
70
|
status: ROW_STATUS.RUNNING,
|
|
67
71
|
output: "",
|
|
68
72
|
output_truncated: false,
|
|
@@ -87,6 +91,7 @@ export function addCompleted(model, event = {}) {
|
|
|
87
91
|
label: event.label,
|
|
88
92
|
params: event.activity?.params || "",
|
|
89
93
|
title: event.activity?.title,
|
|
94
|
+
view: event.view,
|
|
90
95
|
});
|
|
91
96
|
row = synthesized.row;
|
|
92
97
|
}
|
|
@@ -45,7 +45,7 @@ import {
|
|
|
45
45
|
appendAboveLiveTiming, cancelLineText, elapsedSince, formatDuration, mergeTiming, normalizeTiming, turnTimingText,
|
|
46
46
|
} from "./timing.js";
|
|
47
47
|
import { DONE_MS, applyInitEvent, dropDone, initRowHtml, newInitTasks, seedInitTasks } from "./init_row.js";
|
|
48
|
-
import { clientLabel, commandView, displayWait, dropEarlyRestores, emptyAnswerLine, emptyRetryLine, hookNoticeLabel, initWaitLine, isOwn, sessionCommandLine, keepEarlyRestore, newClientId, noticeLine, promptOps, reminderText, restoreAction, composerAfterAck, restoreOnAck, replaysDrawnTurn, retryStatusLine, snapshotEvents, startPageReplyOrHint, turnOutput, unknownCommandHint, webLocalReply, workerGoneText } from "./turn_events.js";
|
|
48
|
+
import { clientLabel, commandView, displayWait, dropEarlyRestores, emptyAnswerLine, emptyRetryLine, hookNoticeLabel, initWaitLine, isOwn, sessionCommandLine, keepEarlyRestore, newClientId, noticeLine, promptOps, reminderText, restoreAction, composerAfterAck, restoreOnAck, replaysDrawnTurn, retryStatusLine, snapshotEvents, steerCutLine, startPageReplyOrHint, turnOutput, unknownCommandHint, webLocalReply, workerGoneText } from "./turn_events.js";
|
|
49
49
|
|
|
50
50
|
const $ = (s) => document.querySelector(s);
|
|
51
51
|
const topStripEl = $("#topStrip");
|
|
@@ -1861,6 +1861,7 @@ const streamHandlers = {
|
|
|
1861
1861
|
// The loop asks again after an empty answer: a row of the empty step
|
|
1862
1862
|
// (a reload puts it back from the snapshot's cards).
|
|
1863
1863
|
empty_answer_retry: (data) => { turnDom.notice({ line: emptyRetryLine(data) }); },
|
|
1864
|
+
steer_cut: (data) => { turnDom.notice({ line: steerCutLine(data) }); },
|
|
1864
1865
|
// The provider is asked again after an error, or the turn waits for
|
|
1865
1866
|
// plugins' setup: a note on the live timing line until it streams.
|
|
1866
1867
|
generation_retrying: (data) => setLiveTurnNote(retryStatusLine(data)),
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// A tool call's full command in a richer UI than the TUI's params line:
|
|
2
|
+
// the server's view (ToolView: an execute/task_create's command uncut up to
|
|
3
|
+
// 8,000 chars, the call's cwd: argument). Pure HTML builders, shared by the
|
|
4
|
+
// live row (turn_view.js fillActivityRow) and the reloaded one
|
|
5
|
+
// (reloadRowHtml), so the two can't differ.
|
|
6
|
+
import { copyButtonHtml } from "./copy.js";
|
|
7
|
+
import { escapeHtml } from "./format.js";
|
|
8
|
+
|
|
9
|
+
// What a row's title hovers: the full command when the call has one,
|
|
10
|
+
// else the params line when the row shows a title in its place.
|
|
11
|
+
export function rowHover(row) {
|
|
12
|
+
if (row?.view?.command) return row.view.command;
|
|
13
|
+
return row?.title && row?.params ? row.params : "";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// The expanded row's command block: "in <cwd>" when the call gave one (as
|
|
17
|
+
// given: execute's may be relative to the project root), the command as
|
|
18
|
+
// written (whitespace kept, its own scroll), a copy button, and a line
|
|
19
|
+
// saying how much a cut command left out. "" for a call without a view.
|
|
20
|
+
export function commandBlockHtml(view) {
|
|
21
|
+
const command = view?.command;
|
|
22
|
+
if (!command) return "";
|
|
23
|
+
const cwd = view.cwd ? `<div class="activity-command-cwd">in <span>${escapeHtml(view.cwd)}</span></div>` : "";
|
|
24
|
+
const cut = view.truncated
|
|
25
|
+
? `<div class="activity-command-cut">… (${command.length.toLocaleString("en-US")} of ${Number(view.chars || 0).toLocaleString("en-US")} chars)</div>`
|
|
26
|
+
: "";
|
|
27
|
+
return `<div class="activity-command code-wrap">${cwd}<pre><code>${escapeHtml(command)}</code></pre>${copyButtonHtml("code")}${cut}</div>`;
|
|
28
|
+
}
|
|
@@ -88,6 +88,13 @@ const ICON = '<svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true"
|
|
|
88
88
|
const BUBBLES = ".bubble.output:not(.streaming), .bubble.user";
|
|
89
89
|
const CODE = ".bubble.output pre";
|
|
90
90
|
|
|
91
|
+
// The button as markup, for a builder that returns HTML (a tool row's
|
|
92
|
+
// command block, command_view.js); the same click handler answers it.
|
|
93
|
+
export function copyButtonHtml(kind) {
|
|
94
|
+
const title = kind === "code" ? "Copy code" : "Copy";
|
|
95
|
+
return `<button type="button" class="copy-btn copy-${kind}" title="${title}" aria-label="${title}">${ICON}</button>`;
|
|
96
|
+
}
|
|
97
|
+
|
|
91
98
|
function makeButton(doc, kind) {
|
|
92
99
|
const btn = doc.createElement("button");
|
|
93
100
|
btn.type = "button";
|
|
@@ -134,10 +134,18 @@ main{flex:1;display:flex;flex-direction:column;gap:16px;padding:16px;overflow:au
|
|
|
134
134
|
.activity-diff>summary{cursor:pointer;font-size:12px;color:var(--text-muted);width:max-content;font-variant-numeric:tabular-nums}
|
|
135
135
|
.activity-diff>summary:hover{color:var(--text-2)}
|
|
136
136
|
.activity-diff[open]>summary{margin-bottom:4px}
|
|
137
|
+
/* An execute/task_create's full command, above its output and lined up with it: as written, its own scroll. */
|
|
138
|
+
.activity-row>.activity-command{flex:1 1 100%;min-width:0;margin:4px 0 2px 60px}
|
|
139
|
+
.activity-command pre{margin:0;max-height:160px;overflow:auto;white-space:pre-wrap;word-break:break-word;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;line-height:1.45;color:var(--text-2);background:var(--bg-inset);border:1px solid var(--border);border-radius:6px;padding:6px 34px 6px 8px}
|
|
140
|
+
.activity-command-cwd,.activity-command-cut{font-size:11px;color:var(--text-faint)}
|
|
141
|
+
.activity-command-cwd{margin-bottom:2px}
|
|
142
|
+
.activity-command-cwd>span{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;color:var(--text-muted)}
|
|
143
|
+
.activity-command-cut{margin-top:2px}
|
|
137
144
|
@media(max-width:600px){
|
|
138
145
|
.activity-params{flex:1 1 0}
|
|
139
146
|
.activity-output{margin-left:0}
|
|
140
147
|
.activity-diff{margin-left:0}
|
|
148
|
+
.activity-row>.activity-command{margin-left:0}
|
|
141
149
|
}
|
|
142
150
|
.bubble.thinking{border-left:3px solid var(--think);background:color-mix(in srgb,var(--think) 6%,var(--bg-panel));padding:8px 12px}
|
|
143
151
|
.bubble.thinking:not([open]),.bubble.activity:not([open]){padding:3px 10px;background:transparent;border-color:transparent;align-self:flex-start;border-radius:6px;transition:background .12s ease}
|
|
@@ -78,6 +78,12 @@ function rowTitle(row) {
|
|
|
78
78
|
return row.title || row.params || "";
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
// A slot's full command (the row's view), for its hover; none for a tool
|
|
82
|
+
// without one.
|
|
83
|
+
function withCommand(slot, row) {
|
|
84
|
+
return row.view?.command ? { ...slot, command: row.view.command } : slot;
|
|
85
|
+
}
|
|
86
|
+
|
|
81
87
|
// While the first step has no thinking and no tool rows the turn is plain:
|
|
82
88
|
// its text streams in the stage's answer slot, as the turn view's
|
|
83
89
|
// chrome-less block streams like a bubble.
|
|
@@ -98,9 +104,10 @@ const NO_ANSWER = "no answer";
|
|
|
98
104
|
// headline the newest narration sentence (+narration+, the view's sentence
|
|
99
105
|
// feed), else the newest thinking one (+thinking+, its ticker),
|
|
100
106
|
// else "…";
|
|
101
|
-
// tool the running call ({name, title, kind}
|
|
107
|
+
// tool the running call ({name, title, kind}, and command: its full
|
|
108
|
+
// command when it has one) or null;
|
|
102
109
|
// trail the last three finished calls, newest last (a file's title
|
|
103
|
-
// without its dir);
|
|
110
|
+
// without its dir; command as for tool);
|
|
104
111
|
// ticks one per call: {kind, status, step (the gen's index)}.
|
|
105
112
|
// +pending+: a card waits for the user; +ended+: the turn's end kind;
|
|
106
113
|
// +emptyAnswer+: a completed turn ended with no answer
|
|
@@ -123,13 +130,13 @@ export function liveSlots(turn, { narration = "", thinking = "", pending = false
|
|
|
123
130
|
else if (thinking) headline = { text: thinking, thinking: true };
|
|
124
131
|
const trail = rows.filter(({ row }) => row.status !== "running").slice(-TRAIL).map(({ row }) => {
|
|
125
132
|
const title = rowTitle(row);
|
|
126
|
-
return { name: toolName(row), title: FILE_TOOLS.has(row.tool) ? title.split("/").pop() : title, status: row.status };
|
|
133
|
+
return withCommand({ name: toolName(row), title: FILE_TOOLS.has(row.tool) ? title.split("/").pop() : title, status: row.status }, row);
|
|
127
134
|
});
|
|
128
135
|
return {
|
|
129
136
|
phase,
|
|
130
137
|
step: ended ? gens.filter((g) => !g.answer || g.thinking || g.tools.length).length : gens.length,
|
|
131
138
|
headline,
|
|
132
|
-
tool: running ? { name: toolName(running), title: rowTitle(running), kind: toolKind(running.tool) } : null,
|
|
139
|
+
tool: running ? withCommand({ name: toolName(running), title: rowTitle(running), kind: toolKind(running.tool) }, running) : null,
|
|
133
140
|
trail,
|
|
134
141
|
ticks: rows.map(({ row, step }) => ({ kind: toolKind(row.tool), status: row.status, step })),
|
|
135
142
|
};
|
|
@@ -195,6 +195,7 @@ export function createStageView({ historyEl, appendToHistory, isNearBottom: hist
|
|
|
195
195
|
const elapsed = endKind ? formatDuration(endedAt - startedAt) : "";
|
|
196
196
|
chipLabel.textContent = `${slots.step} step${slots.step === 1 ? "" : "s"} · ${calls} tool call${calls === 1 ? "" : "s"}${elapsed ? ` · ${elapsed}` : ""}`;
|
|
197
197
|
miniTool.textContent = collapsed && slots.tool ? `${slots.tool.name} ${slots.tool.title}` : "";
|
|
198
|
+
miniTool.title = (collapsed && slots.tool?.command) || "";
|
|
198
199
|
|
|
199
200
|
live.hidden = plain || !!endKind;
|
|
200
201
|
cloud.hidden = !blockEl || blockEl.parentNode !== cloudBody;
|
|
@@ -222,7 +223,7 @@ export function createStageView({ historyEl, appendToHistory, isNearBottom: hist
|
|
|
222
223
|
|
|
223
224
|
function drawNow(tool, gen) {
|
|
224
225
|
const quiet = gen?.text && !gen.textDone ? "writing…" : "thinking…";
|
|
225
|
-
const key = tool ? `run|${tool.name}|${tool.title}` : `quiet|${quiet}`;
|
|
226
|
+
const key = tool ? `run|${tool.name}|${tool.title}|${tool.command || ""}` : `quiet|${quiet}`;
|
|
226
227
|
if (shown.now === key) return;
|
|
227
228
|
shown.now = key;
|
|
228
229
|
now.replaceChildren();
|
|
@@ -233,7 +234,8 @@ export function createStageView({ historyEl, appendToHistory, isNearBottom: hist
|
|
|
233
234
|
name.textContent = tool.name;
|
|
234
235
|
const title = node("span", "ts-title", now);
|
|
235
236
|
title.textContent = tool.title;
|
|
236
|
-
title
|
|
237
|
+
// The full command when the call has one (the title is its first line).
|
|
238
|
+
title.title = tool.command || tool.title;
|
|
237
239
|
} else {
|
|
238
240
|
const word = node("span", "ts-title quiet", now);
|
|
239
241
|
word.textContent = quiet;
|
|
@@ -251,6 +253,7 @@ export function createStageView({ historyEl, appendToHistory, isNearBottom: hist
|
|
|
251
253
|
const failed = item.status === "error";
|
|
252
254
|
const el = node("span", `ts-trail-item${failed ? " error" : ""}`, trail);
|
|
253
255
|
el.textContent = `${failed ? "✕" : "✓"} ${item.name} ${item.title}`.trim();
|
|
256
|
+
if (item.command) el.title = item.command;
|
|
254
257
|
}
|
|
255
258
|
if (!flashing && !items.length) node("span", "ts-trail-item none", trail).textContent = "no tool calls yet";
|
|
256
259
|
}
|
|
@@ -123,6 +123,17 @@ export function emptyRetryLine({ attempt, of, stopped_by: stoppedBy } = {}) {
|
|
|
123
123
|
return `↻ ${what}, asking again (${attempt}/${of})`;
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
// The row a step gets when a message for the running turn cut a generation
|
|
127
|
+
// that was only thinking (steer_cut; source "" is the user's, as is an
|
|
128
|
+
// unknown one). The TUI words it the same (spec/shared/labels_matrix.json).
|
|
129
|
+
const STEER_CUT_FOR = {
|
|
130
|
+
chi_send: "a message sent with chi send",
|
|
131
|
+
parent_agent: "the parent agent's message",
|
|
132
|
+
};
|
|
133
|
+
export function steerCutLine({ source } = {}) {
|
|
134
|
+
return `↪ cut in for ${STEER_CUT_FOR[source] || "your message"}`;
|
|
135
|
+
}
|
|
136
|
+
|
|
126
137
|
// The one muted line for a turn that ended with no answer (turn_completed's
|
|
127
138
|
// turn_summary.empty_answer, or a reloaded turn's marker): the TUI words
|
|
128
139
|
// it the same (TurnNote.empty_answer_line; spec/shared/labels_matrix.json).
|
|
@@ -136,6 +147,7 @@ export function emptyAnswerLine({ retries = 0 } = {}) {
|
|
|
136
147
|
// notice as "<label>: text", the loop's asking-again row as its line.
|
|
137
148
|
export function noticeLine(entry = {}) {
|
|
138
149
|
if (entry.type === "empty_answer_retry") return emptyRetryLine(entry);
|
|
150
|
+
if (entry.type === "steer_cut") return steerCutLine(entry);
|
|
139
151
|
return `${hookNoticeLabel(entry.hook)}: ${entry.text || ""}`;
|
|
140
152
|
}
|
|
141
153
|
|
|
@@ -451,7 +463,8 @@ export function snapshotEvents({ current_turn: turn = null, queued = [], started
|
|
|
451
463
|
const call = { iteration: part.iteration, call_index: part.call_index, tool: part.tool };
|
|
452
464
|
if (part.label) call.label = part.label;
|
|
453
465
|
const title = part.title ? { title: part.title } : {};
|
|
454
|
-
|
|
466
|
+
const view = part.view ? { view: part.view } : {};
|
|
467
|
+
events.push({ type: "tool_call_started", ...call, params: part.params, ...title, ...view });
|
|
455
468
|
if (part.status !== "running") {
|
|
456
469
|
// The activity as the live event had it; action and duration_ms
|
|
457
470
|
// only in snapshots from a worker that keeps them (the TUI's
|
|
@@ -463,6 +476,7 @@ export function snapshotEvents({ current_turn: turn = null, queued = [], started
|
|
|
463
476
|
};
|
|
464
477
|
if (part.duration_ms != null) completed.duration_ms = part.duration_ms;
|
|
465
478
|
if (part.diff) completed.diff = part.diff;
|
|
479
|
+
if (part.view) completed.view = part.view;
|
|
466
480
|
events.push(withImages(completed, part.images));
|
|
467
481
|
}
|
|
468
482
|
break;
|