samagotchi 0.21.0 → 0.23.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 +62 -1
- data/bin/chi +9 -4
- data/docs/architecture.md +3 -2
- data/docs/configuration.md +15 -4
- data/docs/releasing.md +1 -1
- data/docs/sessions.md +1 -1
- data/docs/sub-agent.md +11 -5
- data/docs/testing.md +6 -0
- data/lib/samagotchi/answer_command.rb +11 -5
- 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/sse_writer.rb +0 -6
- data/lib/samagotchi/bridge/turn_accumulator.rb +2 -2
- data/lib/samagotchi/bridge.rb +12 -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 +2 -0
- 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/cli/parent_wait.rb +9 -2
- data/lib/samagotchi/client.rb +3 -5
- data/lib/samagotchi/commands/registry.rb +2 -2
- data/lib/samagotchi/config.rb +38 -14
- data/lib/samagotchi/context_note.rb +1 -1
- data/lib/samagotchi/context_status.rb +6 -6
- data/lib/samagotchi/continue_offer.rb +44 -12
- 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 +49 -19
- data/lib/samagotchi/gem_update.rb +2 -1
- 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 +2 -2
- data/lib/samagotchi/guardrails/shell_paths.rb +2 -2
- 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 +18 -15
- data/lib/samagotchi/idle_reminders.rb +6 -3
- data/lib/samagotchi/image_store.rb +5 -2
- data/lib/samagotchi/kernel_loop.rb +31 -31
- data/lib/samagotchi/llm/chat_loop.rb +9 -6
- data/lib/samagotchi/llm/errors.rb +33 -2
- data/lib/samagotchi/llm/http.rb +12 -12
- data/lib/samagotchi/llm/openai_chat.rb +53 -30
- data/lib/samagotchi/llm/prompt_cache.rb +83 -0
- data/lib/samagotchi/log.rb +2 -2
- data/lib/samagotchi/log_subscriber.rb +2 -1
- 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/parent_report.rb +24 -7
- 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 +68 -12
- data/lib/samagotchi/prompt_literal_guard.rb +1 -1
- data/lib/samagotchi/question_desk.rb +7 -13
- 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/reply_wait.rb +12 -2
- 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/system_prompt.rb +19 -8
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +5 -5
- data/lib/samagotchi/terminal_ui/attached_loop.rb +2 -2
- data/lib/samagotchi/terminal_ui/event_renderer.rb +1 -2
- data/lib/samagotchi/terminal_ui/formatting.rb +10 -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/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 +9 -5
- data/lib/samagotchi/text_diff.rb +12 -12
- data/lib/samagotchi/thinking.rb +6 -2
- data/lib/samagotchi/thought_stream_splitter.rb +6 -3
- 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/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/turn_state.rb +22 -0
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/vision_support.rb +2 -2
- data/lib/samagotchi/web/app.rb +13 -6
- data/lib/samagotchi/web/markdown_renderer.rb +2 -1
- data/lib/samagotchi/web/qr.rb +7 -1
- data/lib/samagotchi/web/server.rb +6 -2
- data/lib/samagotchi/worker.rb +10 -2
- metadata +21 -20
|
@@ -197,6 +197,7 @@ module Samagotchi
|
|
|
197
197
|
return usage_error("--wait takes one session") if options[:wait] && options[:ids].uniq.size > 1
|
|
198
198
|
return usage_error("at most #{MAX_IMAGES} images") if options[:images].size > MAX_IMAGES
|
|
199
199
|
return new_options(options) if options[:new]
|
|
200
|
+
|
|
200
201
|
%i[dir model].each { |key| return usage_error("--#{key} needs --new") if options[key] }
|
|
201
202
|
return usage_error("give session ids") if options[:ids].empty?
|
|
202
203
|
|
|
@@ -229,6 +230,7 @@ module Samagotchi
|
|
|
229
230
|
# caller never closes still hangs, as it would for cat.)
|
|
230
231
|
def read_stdin
|
|
231
232
|
return nil if @stdin.respond_to?(:tty?) && @stdin.tty?
|
|
233
|
+
|
|
232
234
|
if @stdin.respond_to?(:stat)
|
|
233
235
|
stat = @stdin.stat
|
|
234
236
|
return nil unless stat.pipe? || stat.file?
|
|
@@ -328,8 +330,8 @@ module Samagotchi
|
|
|
328
330
|
|
|
329
331
|
# The session as it was before the message went in (ReplyWait's
|
|
330
332
|
# baseline).
|
|
331
|
-
def baseline_of(session, **
|
|
332
|
-
ReplyWait.baseline_of(session, **
|
|
333
|
+
def baseline_of(session, **)
|
|
334
|
+
ReplyWait.baseline_of(session, **)
|
|
333
335
|
end
|
|
334
336
|
|
|
335
337
|
# A new idle session's first turn, with the images. Its worker's Bridge
|
data/lib/samagotchi/session.rb
CHANGED
|
@@ -35,8 +35,8 @@ module Samagotchi
|
|
|
35
35
|
SORT_ORDERS = %w[asc desc].freeze
|
|
36
36
|
|
|
37
37
|
attr_accessor :id, :metadata_version, :mode, :model_name, :working_directory, :messages,
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
:created_at, :updated_at, :status, :last_prompt, :first_preview, :test_run,
|
|
39
|
+
:pending_question, :used_memory_names
|
|
40
40
|
# Memories the session was started with (--memory) and memories hidden
|
|
41
41
|
# from it (--mute): the worker rebuilds the same prompt on a respawn.
|
|
42
42
|
# Names as given; the engine normalizes them.
|
|
@@ -47,7 +47,11 @@ module Samagotchi
|
|
|
47
47
|
bad = @argv.find { |arg| arg.start_with?("-") }
|
|
48
48
|
return usage_error(bad ? "unknown option #{bad}" : "give session ids") if bad || @argv.empty?
|
|
49
49
|
|
|
50
|
-
@argv.uniq.map { |given| @action == "archive" ? archive(given) : unarchive(given) }.all?
|
|
50
|
+
if @argv.uniq.map { |given| @action == "archive" ? archive(given) : unarchive(given) }.all?
|
|
51
|
+
0
|
|
52
|
+
else
|
|
53
|
+
1
|
|
54
|
+
end
|
|
51
55
|
end
|
|
52
56
|
|
|
53
57
|
private
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "set"
|
|
4
|
-
|
|
5
3
|
require_relative "config"
|
|
6
4
|
require_relative "model_profile"
|
|
7
5
|
require_relative "served_model"
|
|
@@ -65,7 +63,7 @@ module Samagotchi
|
|
|
65
63
|
guardrails(text.delete_prefix(GUARDRAILS_COMMAND).strip)
|
|
66
64
|
end
|
|
67
65
|
registry.register(MODEL_COMMAND, "show or switch the model",
|
|
68
|
-
match: ->(text) { text.match?(
|
|
66
|
+
match: ->(text) { text.match?(%r{\A/model(?:\s+.*)?\z}) }) { |text| model(text) }
|
|
69
67
|
registry.register(HELP_COMMAND, "list the commands, the bundles' too", anytime: true) { |_text| reply(help_listing) }
|
|
70
68
|
registry.register("/stats", "show the session's stats", local: true)
|
|
71
69
|
registry.register("/recap", "show the session's recap", local: true)
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
|
|
2
1
|
# frozen_string_literal: true
|
|
3
2
|
|
|
4
3
|
require "fileutils"
|
|
@@ -272,7 +271,7 @@ module Samagotchi
|
|
|
272
271
|
root = cwd && folder_path(cwd)
|
|
273
272
|
roots = {}
|
|
274
273
|
summaries = Session.list(state_dir: sd, sort: sort || "updated_at", order: order || "desc", project_root: project_root,
|
|
275
|
-
|
|
274
|
+
include_archived: include_archived).lazy
|
|
276
275
|
.reject { |s| (!include_tests && s.test_run) || s.id == exclude }
|
|
277
276
|
.select { |s| root.nil? || in_folder?(s.working_directory, root) }
|
|
278
277
|
.filter_map do |s|
|
|
@@ -703,7 +702,7 @@ module Samagotchi
|
|
|
703
702
|
Dir.children(dir).all? do |name|
|
|
704
703
|
path = File.join(dir, name)
|
|
705
704
|
if EMPTY_SKELETON_DIRS.include?(name)
|
|
706
|
-
File.directory?(path) && (!EMPTY_DIRS.include?(name) || Dir.
|
|
705
|
+
File.directory?(path) && (!EMPTY_DIRS.include?(name) || Dir.empty?(path))
|
|
707
706
|
elsif name == "analytics.json"
|
|
708
707
|
Array(JSON.parse(File.read(path))["turn_records"]).empty?
|
|
709
708
|
# Cards and notices from before any turn (Bridge::CardStore).
|
|
@@ -1054,4 +1053,3 @@ module Samagotchi
|
|
|
1054
1053
|
end
|
|
1055
1054
|
end
|
|
1056
1055
|
end
|
|
1057
|
-
|
|
@@ -137,9 +137,9 @@ module Samagotchi
|
|
|
137
137
|
|
|
138
138
|
tokens = data["tokens"].is_a?(Hash) ? data["tokens"] : {}
|
|
139
139
|
count = ->(key) { tokens[key].is_a?(Numeric) ? tokens[key] : 0 }
|
|
140
|
-
SavedSummary.new(ctx_pct: saved_pct(data["context"]), prompt_sum: count.("prompt_sum"),
|
|
141
|
-
completion_sum: count.("completion_sum"), cached_sum: count.("cached_sum"),
|
|
142
|
-
reasoning_sum: count.("reasoning_sum"), cost_sum: count.("cost_sum"))
|
|
140
|
+
SavedSummary.new(ctx_pct: saved_pct(data["context"]), prompt_sum: count.call("prompt_sum"),
|
|
141
|
+
completion_sum: count.call("completion_sum"), cached_sum: count.call("cached_sum"),
|
|
142
|
+
reasoning_sum: count.call("reasoning_sum"), cost_sum: count.call("cost_sum"))
|
|
143
143
|
rescue JSON::ParserError, SystemCallError, TypeError
|
|
144
144
|
nil
|
|
145
145
|
end
|
|
@@ -379,15 +379,15 @@ module Samagotchi
|
|
|
379
379
|
# Caller holds the mutex.
|
|
380
380
|
def tokens_block(totals, turn)
|
|
381
381
|
add = ->(key) { totals[key] + (turn&.public_send(key) || 0) }
|
|
382
|
-
decode_ms = add.(:decode_ms_sum)
|
|
383
|
-
decode_tokens = add.(:decode_tokens_sum)
|
|
382
|
+
decode_ms = add.call(:decode_ms_sum)
|
|
383
|
+
decode_tokens = add.call(:decode_tokens_sum)
|
|
384
384
|
speed_turn = turn&.last_decode_tps ? turn : nil
|
|
385
385
|
{
|
|
386
|
-
prompt_sum: add.(:prompt_sum),
|
|
387
|
-
completion_sum: add.(:completion_sum),
|
|
388
|
-
cached_sum: add.(:cached_sum),
|
|
389
|
-
reasoning_sum: add.(:reasoning_sum),
|
|
390
|
-
cost_sum: add.(:cost_sum),
|
|
386
|
+
prompt_sum: add.call(:prompt_sum),
|
|
387
|
+
completion_sum: add.call(:completion_sum),
|
|
388
|
+
cached_sum: add.call(:cached_sum),
|
|
389
|
+
reasoning_sum: add.call(:reasoning_sum),
|
|
390
|
+
cost_sum: add.call(:cost_sum),
|
|
391
391
|
decode_ms_sum: decode_ms.round,
|
|
392
392
|
decode_tokens_sum: decode_tokens,
|
|
393
393
|
avg_decode_tps: decode_ms.positive? ? (decode_tokens * 1000.0 / decode_ms).round(1) : nil,
|
|
@@ -70,6 +70,7 @@ module Samagotchi
|
|
|
70
70
|
# already-unsubscribed never raises)
|
|
71
71
|
def unsubscribe(handle:)
|
|
72
72
|
return false unless handle.is_a?(SubscribedObserver)
|
|
73
|
+
|
|
73
74
|
@mutex.synchronize { !!@observers.delete(handle) }
|
|
74
75
|
end
|
|
75
76
|
|
|
@@ -110,8 +111,8 @@ module Samagotchi
|
|
|
110
111
|
# Run the block while no event is being numbered or delivered, so it sees
|
|
111
112
|
# (and can change) state consistently with the event log. Reentrant: the
|
|
112
113
|
# block may notify. Keep it short; every emitter waits for it.
|
|
113
|
-
def synchronize(&
|
|
114
|
-
@mutex.synchronize(&
|
|
114
|
+
def synchronize(&)
|
|
115
|
+
@mutex.synchronize(&)
|
|
115
116
|
end
|
|
116
117
|
end
|
|
117
118
|
end
|
|
@@ -171,7 +171,7 @@ module Samagotchi
|
|
|
171
171
|
end
|
|
172
172
|
|
|
173
173
|
now = Time.now
|
|
174
|
-
cutoff = days.to_i.positive? ? now - days.to_i * 86_400 : nil
|
|
174
|
+
cutoff = days.to_i.positive? ? now - (days.to_i * 86_400) : nil
|
|
175
175
|
max = max_count.to_i
|
|
176
176
|
|
|
177
177
|
deleted = []
|
|
@@ -249,7 +249,7 @@ module Samagotchi
|
|
|
249
249
|
begin
|
|
250
250
|
FileUtils.rm_f(path)
|
|
251
251
|
sidecar = File.join(state_dir, session.id)
|
|
252
|
-
FileUtils.rm_rf(sidecar)
|
|
252
|
+
FileUtils.rm_rf(sidecar)
|
|
253
253
|
PluginSessionState.remove(session.id)
|
|
254
254
|
deleted << session.id
|
|
255
255
|
rescue StandardError
|
|
@@ -118,6 +118,7 @@ module Samagotchi
|
|
|
118
118
|
return usage_error("unknown format #{format.inspect}: use --format text|json|tsv")
|
|
119
119
|
end
|
|
120
120
|
return usage_error("unknown scope #{scope.inspect}: use --scope=project|all") unless scope.nil? || %w[project all].include?(scope)
|
|
121
|
+
|
|
121
122
|
# The current git project's sessions (Samagotchi::ProjectScope), like
|
|
122
123
|
# chi web's; --scope=all, a folder in no repo or an explicit --cwd: every
|
|
123
124
|
# project's.
|
|
@@ -175,13 +176,17 @@ module Samagotchi
|
|
|
175
176
|
end
|
|
176
177
|
# A delegated session points at its parent.
|
|
177
178
|
child = summary[:parent_short_id] ? " ↳ #{summary[:parent_short_id]}" : ""
|
|
178
|
-
flag = summary[:scratch]
|
|
179
|
+
flag = if summary[:scratch]
|
|
180
|
+
" [scratch]"
|
|
181
|
+
else
|
|
182
|
+
(summary[:test_run] ? " [test]" : "")
|
|
183
|
+
end
|
|
179
184
|
flag += " [archived]" if summary[:archived]
|
|
180
185
|
ctx = Samagotchi::SessionMetrics.context_label(summary[:ctx_pct])
|
|
181
186
|
@stdout.puts "#{summary[:id]} #{state.ljust(8)} #{ctx.ljust(8)} #{summary[:updated_at]} #{desc}#{flag}#{child}"
|
|
182
187
|
end
|
|
183
188
|
if project
|
|
184
|
-
@stdout.puts "#{summaries.empty?
|
|
189
|
+
@stdout.puts "#{"\n" unless summaries.empty?}#{scope_note.call(summaries.size)}"
|
|
185
190
|
else
|
|
186
191
|
@stdout.puts summaries.empty? ? "No sessions." : "\n#{summaries.size} session(s)"
|
|
187
192
|
end
|
|
@@ -204,7 +209,11 @@ module Samagotchi
|
|
|
204
209
|
end
|
|
205
210
|
# A delegated session points at its parent: ↳ <parent's short id>.
|
|
206
211
|
row = lambda do |s|
|
|
207
|
-
flag = s.scratch
|
|
212
|
+
flag = if s.scratch
|
|
213
|
+
" [scratch]"
|
|
214
|
+
else
|
|
215
|
+
(s.test_run ? " [test]" : "")
|
|
216
|
+
end
|
|
208
217
|
flag += " [archived]" if s.archived
|
|
209
218
|
child = s.parent_id ? " ↳ #{s.parent_id[0, 8]}" : ""
|
|
210
219
|
# How full the context was after the last turn: "ctx 12%", blank when unknown.
|
|
@@ -222,7 +231,7 @@ module Samagotchi
|
|
|
222
231
|
end
|
|
223
232
|
if project
|
|
224
233
|
sessions.each { |s| @stdout.puts row.call(s) }
|
|
225
|
-
@stdout.puts "#{sessions.empty?
|
|
234
|
+
@stdout.puts "#{"\n" unless sessions.empty?}#{scope_note.call(sessions.size)}"
|
|
226
235
|
elsif sessions.empty?
|
|
227
236
|
@stdout.puts "No sessions."
|
|
228
237
|
else
|
|
@@ -276,7 +285,7 @@ module Samagotchi
|
|
|
276
285
|
next false
|
|
277
286
|
rescue Samagotchi::SessionManager::RestartRefused => e
|
|
278
287
|
@stdout.flush
|
|
279
|
-
@stderr.puts
|
|
288
|
+
@stderr.puts %i[not_running unsupported].include?(e.reason) ? e.message : "session #{id}: #{e.message}"
|
|
280
289
|
next false
|
|
281
290
|
rescue ArgumentError => e
|
|
282
291
|
@stdout.flush
|
|
@@ -79,9 +79,10 @@ module Samagotchi
|
|
|
79
79
|
@built = nil
|
|
80
80
|
end
|
|
81
81
|
|
|
82
|
-
# The base prompt (specs, plugins' declarations)
|
|
82
|
+
# The base prompt (specs, plugins' declarations), with Gemma's tool
|
|
83
|
+
# declarations at its end as #build has them.
|
|
83
84
|
def base(chat: false, thinking: nil)
|
|
84
|
-
assist_system_prompt(chat: chat, thinking: thinking)
|
|
85
|
+
with_tail_declarations(assist_system_prompt(chat: chat, thinking: thinking), chat: chat)
|
|
85
86
|
end
|
|
86
87
|
|
|
87
88
|
# Names activated via preloaded --memory entries during prompt
|
|
@@ -141,15 +142,18 @@ module Samagotchi
|
|
|
141
142
|
def assist_system_prompt(chat: false, thinking: nil)
|
|
142
143
|
return chat_system_prompt if chat
|
|
143
144
|
|
|
144
|
-
declarations = tool_declarations
|
|
145
145
|
hint = tool_call_hint
|
|
146
146
|
turn_preamble = turn_preamble_instruction(thinking)
|
|
147
|
+
# Gemma 4's template declares the tools at the end of the system turn
|
|
148
|
+
# (#system_prompt_with_index puts them there).
|
|
149
|
+
tools = if gemma_tail_declarations?
|
|
150
|
+
"Your tools are declared at the end of this system prompt."
|
|
151
|
+
else
|
|
152
|
+
"You have access to the following tools:\n\n#{tool_declarations}\n"
|
|
153
|
+
end
|
|
147
154
|
|
|
148
155
|
<<~SYS
|
|
149
|
-
You are Chi (pronounced "chee"), the friendly name for the Samagotchi assistant harness.
|
|
150
|
-
|
|
151
|
-
#{declarations}
|
|
152
|
-
|
|
156
|
+
You are Chi (pronounced "chee"), the friendly name for the Samagotchi assistant harness. #{tools}
|
|
153
157
|
#{hint}
|
|
154
158
|
You may make multiple tool calls. After seeing tool results, continue reasoning or answer the user.
|
|
155
159
|
#{turn_preamble}
|
|
@@ -234,9 +238,16 @@ module Samagotchi
|
|
|
234
238
|
"Project memories:\n#{project_index}",
|
|
235
239
|
"System memories:\n#{system_index}"
|
|
236
240
|
].join("\n\n")
|
|
237
|
-
[thinking_token + base, rg_guidance, project_description, project_location, current_model, current_session, memory_sections, system_identity_section, explicit_memory_section].compact.join("\n")
|
|
241
|
+
prompt = [thinking_token + base, rg_guidance, project_description, project_location, current_model, current_session, memory_sections, system_identity_section, explicit_memory_section].compact.join("\n")
|
|
242
|
+
with_tail_declarations(prompt, chat: chat)
|
|
238
243
|
end
|
|
239
244
|
|
|
245
|
+
def with_tail_declarations(prompt, chat:)
|
|
246
|
+
chat || !gemma_tail_declarations? ? prompt : prompt.rstrip + tool_declarations
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def gemma_tail_declarations? = profile.name == "gemma4"
|
|
250
|
+
|
|
240
251
|
# B-light: auto-preload the built-in identity memory.
|
|
241
252
|
# The file is installed by SystemBundle.ensure! as a normal system memory,
|
|
242
253
|
# but its body is injected here so the agent has it without an extra tool call.
|
|
@@ -46,10 +46,10 @@ module Samagotchi
|
|
|
46
46
|
first_command = model && (attach || resume) ? "/model #{model}" : nil
|
|
47
47
|
surface = open_surface
|
|
48
48
|
attached = AttachedLoop.new(client: client, screen: surface, client_id: "tui:#{Process.pid}", first_prompt: prompt,
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
49
|
+
first_command: first_command, no_interrupt: no_interrupt,
|
|
50
|
+
default_input: default_input && !prompt && !attach && !resume,
|
|
51
|
+
wait_at_eof: !$stdin.tty?,
|
|
52
|
+
parent_answers: Guardrails::ParentApprovals.parent_process?)
|
|
53
53
|
ended = begin
|
|
54
54
|
attached.run
|
|
55
55
|
ensure
|
|
@@ -144,7 +144,7 @@ module Samagotchi
|
|
|
144
144
|
return if flags.empty?
|
|
145
145
|
|
|
146
146
|
verb = flags.size == 1 ? "applies" : "apply"
|
|
147
|
-
err.puts "(#{flags.join(
|
|
147
|
+
err.puts "(#{flags.join(" and ")} #{verb} to a new session; #{session_id}'s prompt is already built)"
|
|
148
148
|
end
|
|
149
149
|
|
|
150
150
|
# @return [BridgeClient, nil] the running worker's, or nil when none runs
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "set"
|
|
4
3
|
require_relative "event_renderer"
|
|
5
4
|
require_relative "formatting"
|
|
6
5
|
require_relative "../prompt_history"
|
|
@@ -1174,7 +1173,8 @@ module Samagotchi
|
|
|
1174
1173
|
Bridge::TurnAccumulator.replay_events(turn).each do |event|
|
|
1175
1174
|
case event[:type]
|
|
1176
1175
|
# A new step starts its line over, as live (a join during a hold).
|
|
1177
|
-
when :generation_started then tail
|
|
1176
|
+
when :generation_started then tail = nil
|
|
1177
|
+
lane = :writing
|
|
1178
1178
|
when :generation_chunk
|
|
1179
1179
|
tail, lane = event[:thinking].to_s.empty? ? [event[:text], :writing] : [event[:thinking], :thinking]
|
|
1180
1180
|
when :tool_call_started
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "set"
|
|
4
3
|
require_relative "formatting"
|
|
5
4
|
|
|
6
5
|
module Samagotchi
|
|
@@ -188,7 +187,7 @@ module Samagotchi
|
|
|
188
187
|
Array(event[:steers]).each { |steer| @view.print_line(@view.format_steer_line(source: steer[:source], text: steer[:text])) }
|
|
189
188
|
end
|
|
190
189
|
|
|
191
|
-
private
|
|
190
|
+
private
|
|
192
191
|
|
|
193
192
|
def render_streamed_tool_activity(activity, duration_ms:, images: nil, diff: nil)
|
|
194
193
|
return if activity.nil?
|
|
@@ -15,7 +15,7 @@ module Samagotchi
|
|
|
15
15
|
params_suffix = params.empty? ? "" : " #{paint(params, 90)}"
|
|
16
16
|
status = activity[:status].to_s
|
|
17
17
|
elapsed_suffix = duration_ms.nil? ? "" : " (#{format_elapsed_duration(duration_ms)})"
|
|
18
|
-
"#{paint(
|
|
18
|
+
"#{paint("tool>", 36)} #{activity[:action]} (#{activity[:tool]}#{params_suffix}): #{paint(status, status_color(status))}#{elapsed_suffix}"
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
# Green ok, yellow stopped (a wait the user's Stop ended), red the rest.
|
|
@@ -75,7 +75,7 @@ module Samagotchi
|
|
|
75
75
|
return "" unless diff.is_a?(Hash)
|
|
76
76
|
|
|
77
77
|
get = ->(key) { diff[key] || diff[key.to_s] }
|
|
78
|
-
" #{paint("+#{get.(:added).to_i}", 32)} #{paint("\u2212#{get.(:removed).to_i}", 31)}"
|
|
78
|
+
" #{paint("+#{get.call(:added).to_i}", 32)} #{paint("\u2212#{get.call(:removed).to_i}", 31)}"
|
|
79
79
|
end
|
|
80
80
|
|
|
81
81
|
# Prompt labels by the sender's client_id prefix (turn_events.js
|
|
@@ -113,7 +113,7 @@ module Samagotchi
|
|
|
113
113
|
params = part[:params].to_s.strip
|
|
114
114
|
params_suffix = params.empty? ? "" : " #{paint(params, 90)}"
|
|
115
115
|
status = part[:status].to_s
|
|
116
|
-
"#{paint(
|
|
116
|
+
"#{paint("tool>", 36)} #{part[:tool]}#{params_suffix}: #{paint(status, status_color(status))}" \
|
|
117
117
|
"#{format_tool_image_suffix(part[:images])}#{format_tool_diff_suffix(part[:diff])}"
|
|
118
118
|
end
|
|
119
119
|
|
|
@@ -233,8 +233,8 @@ module Samagotchi
|
|
|
233
233
|
value = tps.to_f
|
|
234
234
|
return "" unless value.positive?
|
|
235
235
|
|
|
236
|
-
number = value >= 1000 ? "#{format(
|
|
237
|
-
"#{
|
|
236
|
+
number = value >= 1000 ? "#{format("%.1f", (value / 100).round / 10.0)}k" : value.round.to_s
|
|
237
|
+
"#{"~" if source.to_s == "estimate"}#{number} tok/s"
|
|
238
238
|
end
|
|
239
239
|
|
|
240
240
|
# "$0.42"; a cost under a cent keeps four decimals ("$0.0012"); "" for
|
|
@@ -297,7 +297,7 @@ module Samagotchi
|
|
|
297
297
|
def status_rows(segments, width)
|
|
298
298
|
return [] if segments.empty? || width <= 0
|
|
299
299
|
|
|
300
|
-
row = cap_preview_text("status> #{segments.join(
|
|
300
|
+
row = cap_preview_text("status> #{segments.join(" | ")}", width)
|
|
301
301
|
[color_output? ? paint(row, 90) : row]
|
|
302
302
|
end
|
|
303
303
|
|
|
@@ -336,7 +336,7 @@ module Samagotchi
|
|
|
336
336
|
|
|
337
337
|
visible = names.first(limit)
|
|
338
338
|
suffix = names.length > visible.length ? ", +#{names.length - visible.length}" : ""
|
|
339
|
-
"#{label}: #{visible.join(
|
|
339
|
+
"#{label}: #{visible.join(", ")}#{suffix}"
|
|
340
340
|
end
|
|
341
341
|
|
|
342
342
|
def cap_preview_text(text, width)
|
|
@@ -411,8 +411,8 @@ module Samagotchi
|
|
|
411
411
|
return nil unless last
|
|
412
412
|
|
|
413
413
|
source = tokens[:tps_source].to_s
|
|
414
|
-
text =
|
|
415
|
-
text << ", #{speed_text(tokens[:last_prefill_tps],
|
|
414
|
+
text = "#{speed_text(last, source)} out"
|
|
415
|
+
text << ", #{speed_text(tokens[:last_prefill_tps], "server")} prompt" if tokens[:last_prefill_tps]
|
|
416
416
|
text << " (last, #{source})"
|
|
417
417
|
text << ", avg #{speed_text(tokens[:avg_decode_tps], source)}" if tokens[:avg_decode_tps]
|
|
418
418
|
text
|
|
@@ -428,7 +428,7 @@ module Samagotchi
|
|
|
428
428
|
return "#{seconds.round}s" if seconds < 60
|
|
429
429
|
|
|
430
430
|
total_seconds = seconds.round
|
|
431
|
-
"#{total_seconds / 60}m #{format(
|
|
431
|
+
"#{total_seconds / 60}m #{format("%02d", total_seconds % 60)}s"
|
|
432
432
|
end
|
|
433
433
|
|
|
434
434
|
def paint(text, code)
|
|
@@ -39,7 +39,7 @@ module Samagotchi
|
|
|
39
39
|
end
|
|
40
40
|
|
|
41
41
|
def alive? = @thread&.alive? || false
|
|
42
|
-
|
|
42
|
+
|
|
43
43
|
# Drop the open read and start it again with the current prompt.
|
|
44
44
|
# @param keep_text [Boolean] with what is typed there now
|
|
45
45
|
# @param prefill [String, nil] with this text instead
|
|
@@ -48,7 +48,7 @@ module Samagotchi
|
|
|
48
48
|
@prefill = text unless text.to_s.empty?
|
|
49
49
|
@thread&.raise(Reprompt)
|
|
50
50
|
end
|
|
51
|
-
|
|
51
|
+
|
|
52
52
|
# Type +text+ into the next read (from the sink, between two reads).
|
|
53
53
|
def prefill_next(text)
|
|
54
54
|
@prefill = text
|
|
@@ -57,7 +57,7 @@ module Samagotchi
|
|
|
57
57
|
# @return [String, nil] what the open read holds
|
|
58
58
|
def typed_text
|
|
59
59
|
return nil unless $stdin.tty?
|
|
60
|
-
|
|
60
|
+
|
|
61
61
|
Reline.line_buffer.to_s
|
|
62
62
|
rescue StandardError
|
|
63
63
|
nil
|
|
@@ -33,29 +33,29 @@ module Samagotchi
|
|
|
33
33
|
# @param pending [Hash] Engine#pending_question (symbol or string keys)
|
|
34
34
|
def initialize(pending)
|
|
35
35
|
field = ->(key) { pending[key] || pending[key.to_s] }
|
|
36
|
-
approval = field.(:approval)
|
|
36
|
+
approval = field.call(:approval)
|
|
37
37
|
@preview = approval.is_a?(Hash) ? (approval[:preview] || approval["preview"]) : nil
|
|
38
|
-
@id = field.(:id).to_s
|
|
39
|
-
@question = field.(:question).to_s
|
|
38
|
+
@id = field.call(:id).to_s
|
|
39
|
+
@question = field.call(:question).to_s
|
|
40
40
|
# A delegate's approval relayed here: the delegate's own text is the
|
|
41
41
|
# question, who asks a dim line (#note).
|
|
42
|
-
relay = field.(:relay)
|
|
42
|
+
relay = field.call(:relay)
|
|
43
43
|
if relay.is_a?(Hash)
|
|
44
44
|
get = ->(key) { relay[key] || relay[key.to_s] }
|
|
45
|
-
chain = Array(get.(:chain)).map(&:to_s)
|
|
46
|
-
chain = [get.(:child_id).to_s[0, 8]] if chain.empty?
|
|
45
|
+
chain = Array(get.call(:chain)).map(&:to_s)
|
|
46
|
+
chain = [get.call(:child_id).to_s[0, 8]] if chain.empty?
|
|
47
47
|
@delegate = chain.reverse.join(" → ")
|
|
48
|
-
@delegate_task = get.(:task).to_s
|
|
49
|
-
@question = get.(:asked).to_s unless get.(:asked).to_s.strip.empty?
|
|
48
|
+
@delegate_task = get.call(:task).to_s
|
|
49
|
+
@question = get.call(:asked).to_s unless get.call(:asked).to_s.strip.empty?
|
|
50
50
|
end
|
|
51
|
-
self.relayed_to = field.(:relayed_to)
|
|
52
|
-
@options = Array(field.(:options)).map { |v| v.to_s.strip }.reject(&:empty?)
|
|
53
|
-
header = field.(:header).to_s.strip
|
|
51
|
+
self.relayed_to = field.call(:relayed_to)
|
|
52
|
+
@options = Array(field.call(:options)).map { |v| v.to_s.strip }.reject(&:empty?)
|
|
53
|
+
header = field.call(:header).to_s.strip
|
|
54
54
|
@header = header.empty? ? nil : header
|
|
55
|
-
@multi = !!field.(:multi_select)
|
|
56
|
-
@free = !!field.(:allow_freeform)
|
|
57
|
-
@approval = field.(:kind).to_s == "approval"
|
|
58
|
-
@continue = field.(:kind).to_s == "continue"
|
|
55
|
+
@multi = !!field.call(:multi_select)
|
|
56
|
+
@free = !!field.call(:allow_freeform)
|
|
57
|
+
@approval = field.call(:kind).to_s == "approval"
|
|
58
|
+
@continue = field.call(:kind).to_s == "continue"
|
|
59
59
|
end
|
|
60
60
|
|
|
61
61
|
def multi? = @multi
|
|
@@ -121,19 +121,19 @@ module Samagotchi
|
|
|
121
121
|
return [] unless preview.is_a?(Hash)
|
|
122
122
|
|
|
123
123
|
get = ->(key) { preview[key] || preview[key.to_s] }
|
|
124
|
-
return [paint.("this edit would fail: #{get.(:error)}", 33)] if get.(:error)
|
|
125
|
-
return [paint.("diff not shown: #{get.(:skipped)}", 2)] if get.(:skipped)
|
|
124
|
+
return [paint.call("this edit would fail: #{get.call(:error)}", 33)] if get.call(:error)
|
|
125
|
+
return [paint.call("diff not shown: #{get.call(:skipped)}", 2)] if get.call(:skipped)
|
|
126
126
|
|
|
127
|
-
lines = get.(:text).to_s.split("\n")
|
|
127
|
+
lines = get.call(:text).to_s.split("\n")
|
|
128
128
|
return [] if lines.empty?
|
|
129
129
|
|
|
130
130
|
# TextDiff's own cut note ("… N more lines") counts toward the rest.
|
|
131
131
|
cut = lines.last.to_s[/\A… (\d+) more lines\z/, 1]
|
|
132
132
|
lines.pop if cut
|
|
133
133
|
hidden = [lines.size - PREVIEW_LINES, 0].max + cut.to_i
|
|
134
|
-
shown = lines.first(PREVIEW_LINES).map { |line| paint.(line, diff_code(line)) }
|
|
135
|
-
shown.unshift(paint.("new file", 2)) if get.(:new_file)
|
|
136
|
-
shown << paint.("… #{hidden} more lines (full diff on the web)", 2) if hidden.positive?
|
|
134
|
+
shown = lines.first(PREVIEW_LINES).map { |line| paint.call(line, diff_code(line)) }
|
|
135
|
+
shown.unshift(paint.call("new file", 2)) if get.call(:new_file)
|
|
136
|
+
shown << paint.call("… #{hidden} more lines (full diff on the web)", 2) if hidden.positive?
|
|
137
137
|
shown
|
|
138
138
|
end
|
|
139
139
|
|
|
@@ -142,7 +142,7 @@ module Samagotchi
|
|
|
142
142
|
# @param outcome [String] the answer (#answer_text) or what closed it
|
|
143
143
|
def summary(outcome, paint: ->(text, _code) { text })
|
|
144
144
|
who = @delegate ? "#{@delegate}: " : ""
|
|
145
|
-
"#{paint.("#{mark}#{who}#{question.lines.first.to_s.chomp}", approval? ? 33 : 94)} → #{outcome}"
|
|
145
|
+
"#{paint.call("#{mark}#{who}#{question.lines.first.to_s.chomp}", approval? ? 33 : 94)} → #{outcome}"
|
|
146
146
|
end
|
|
147
147
|
|
|
148
148
|
# @param answer [Answer] an accepted one
|
|
@@ -166,7 +166,7 @@ module Samagotchi
|
|
|
166
166
|
raw = raw.to_s.strip
|
|
167
167
|
# "1,3; my text": the first ';' separates the selection from freeform text.
|
|
168
168
|
sel_part, free_part = raw.include?(";") ? raw.split(";", 2).map(&:strip) : [raw, nil]
|
|
169
|
-
free_part = nil if free_part
|
|
169
|
+
free_part = nil if free_part && free_part.empty?
|
|
170
170
|
# Models forget to flag freeform: accept it anyway, with a note.
|
|
171
171
|
note = free_part && !free? ? "(note: freeform not flagged but accepting '#{free_part}')" : nil
|
|
172
172
|
|
|
@@ -31,7 +31,7 @@ module Samagotchi
|
|
|
31
31
|
|
|
32
32
|
# The line a continue offer leaves in the scrollback.
|
|
33
33
|
def self.continue_summary(outcome, paint: ->(text, _code) { text })
|
|
34
|
-
"#{paint.("? #{CONTINUE_QUESTION}", 33)} → #{outcome}"
|
|
34
|
+
"#{paint.call("? #{CONTINUE_QUESTION}", 33)} → #{outcome}"
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
# @param question [String] the first row's text, wrapped when there is room
|
|
@@ -77,14 +77,14 @@ module Samagotchi
|
|
|
77
77
|
|
|
78
78
|
def layout(width, hint:, details:, header:, folded:)
|
|
79
79
|
rows = []
|
|
80
|
-
rows << @paint.(@header, 1) if header && @header
|
|
81
|
-
rows << @paint.(@note, 2) if header && @note
|
|
80
|
+
rows << @paint.call(@header, 1) if header && @header
|
|
81
|
+
rows << @paint.call(@note, 2) if header && @note
|
|
82
82
|
question_rows = wrap(@question, width, first: @mark, indent: " " * @mark.size)
|
|
83
83
|
question_rows = question_rows.first(1) unless details
|
|
84
|
-
rows.concat(question_rows.map { |row| @paint.(row, @question_code) })
|
|
85
|
-
rows.concat(@details.map { |row| @paint.(row, @question_code) }) if details
|
|
86
|
-
rows.concat(folded ? folded_options(width) : @options.map { |o| @paint.(" #{o.text}", 92) })
|
|
87
|
-
rows << @paint.(" [#{@hint}]", 90) if hint && @hint
|
|
84
|
+
rows.concat(question_rows.map { |row| @paint.call(row, @question_code) })
|
|
85
|
+
rows.concat(@details.map { |row| @paint.call(row, @question_code) }) if details
|
|
86
|
+
rows.concat(folded ? folded_options(width) : @options.map { |o| @paint.call(" #{o.text}", 92) })
|
|
87
|
+
rows << @paint.call(" [#{@hint}]", 90) if hint && @hint
|
|
88
88
|
rows
|
|
89
89
|
end
|
|
90
90
|
|
|
@@ -97,13 +97,13 @@ module Samagotchi
|
|
|
97
97
|
candidate = line.empty? ? " #{item}" : "#{line} · #{item}"
|
|
98
98
|
if !line.empty? && display_width(candidate) > width
|
|
99
99
|
rows << line
|
|
100
|
-
line =
|
|
100
|
+
line = " #{item}"
|
|
101
101
|
else
|
|
102
102
|
line = +candidate
|
|
103
103
|
end
|
|
104
104
|
end
|
|
105
105
|
rows << line unless line.empty?
|
|
106
|
-
rows.map { |row| @paint.(row, 92) }
|
|
106
|
+
rows.map { |row| @paint.call(row, 92) }
|
|
107
107
|
end
|
|
108
108
|
|
|
109
109
|
# Word wrap at +width+ columns, the first row after +first+, the others
|
|
@@ -122,7 +122,7 @@ module Samagotchi
|
|
|
122
122
|
while display_width(line.rstrip) > width
|
|
123
123
|
head, = Reline::Unicode.take_mbchar_range(line, 0, width, padding: false)
|
|
124
124
|
rows << head
|
|
125
|
-
line =
|
|
125
|
+
line = "#{indent}#{line[head.size..]}"
|
|
126
126
|
end
|
|
127
127
|
end
|
|
128
128
|
rows << line.rstrip unless line.strip.empty? && !rows.empty?
|
|
@@ -216,7 +216,7 @@ module Samagotchi
|
|
|
216
216
|
# The prompt and the text as render_finished writes them: one line per
|
|
217
217
|
# input line, with a trailing space when a line fills the width exactly.
|
|
218
218
|
def seam_final_lines
|
|
219
|
-
@buffer_of_lines.size
|
|
219
|
+
Array.new(@buffer_of_lines.size) do |i|
|
|
220
220
|
line = Reline::Unicode.strip_non_printing_start_end(prompt_list[i]) + modified_lines[i]
|
|
221
221
|
split_line_by_width(line, screen_width).last.empty? ? "#{line} " : line
|
|
222
222
|
end
|
|
@@ -43,6 +43,7 @@ module Samagotchi
|
|
|
43
43
|
def <<(item)
|
|
44
44
|
@lock.synchronize do
|
|
45
45
|
next @answers << item if @answers
|
|
46
|
+
|
|
46
47
|
taken = @turn && item.first == :line && @turn.call(item.last)
|
|
47
48
|
# Not now: the line goes back into the prompt.
|
|
48
49
|
next @reader&.prefill_next(item.last) if taken == :back
|
|
@@ -130,8 +131,8 @@ module Samagotchi
|
|
|
130
131
|
end
|
|
131
132
|
end
|
|
132
133
|
|
|
133
|
-
def with_surface_lock(&
|
|
134
|
-
@surface.respond_to?(:synchronize) ? @surface.synchronize(&
|
|
134
|
+
def with_surface_lock(&)
|
|
135
|
+
@surface.respond_to?(:synchronize) ? @surface.synchronize(&) : yield
|
|
135
136
|
end
|
|
136
137
|
end
|
|
137
138
|
end
|