samagotchi 0.2.0 → 0.4.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 +198 -1
- data/README.md +56 -4
- data/bin/chi +118 -50
- data/docs/cli.md +184 -9
- data/docs/configuration.md +333 -47
- data/docs/desktop.md +45 -4
- data/docs/guardrails.md +11 -0
- data/docs/hooks.md +208 -5
- data/docs/plugins.md +68 -2
- data/docs/releasing.md +23 -13
- data/docs/sessions.md +45 -17
- data/lib/samagotchi/answer_display.rb +95 -0
- data/lib/samagotchi/archive_store.rb +90 -0
- data/lib/samagotchi/bootstrap/config_writer.rb +342 -0
- data/lib/samagotchi/bootstrap/probe.rb +262 -0
- data/lib/samagotchi/bootstrap_command.rb +347 -0
- data/lib/samagotchi/bridge/pending_card.rb +89 -0
- data/lib/samagotchi/bridge/turn_accumulator.rb +15 -3
- data/lib/samagotchi/bridge.rb +13 -1
- data/lib/samagotchi/bridge_client.rb +6 -2
- data/lib/samagotchi/bundles/check-in/manifest.yml +10 -0
- data/lib/samagotchi/bundles/check-in/plugin.rb +244 -0
- data/lib/samagotchi/bundles/source-links/hooks/source_links.rb +531 -0
- data/lib/samagotchi/bundles/source-links/manifest.yml +14 -0
- data/lib/samagotchi/bundles/source-links/source_links.md +5 -0
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +10 -6
- data/lib/samagotchi/bundles/system/delegated.md +6 -7
- data/lib/samagotchi/bundles/system/manifest.yml +4 -4
- data/lib/samagotchi/bundles/system/self_map.md +8 -2
- data/lib/samagotchi/client.rb +81 -19
- data/lib/samagotchi/commands/registry.rb +8 -0
- data/lib/samagotchi/config.rb +252 -48
- data/lib/samagotchi/desktop/macos/App.swift +12 -8
- data/lib/samagotchi/desktop/macos/ChiRunner.swift +17 -9
- data/lib/samagotchi/desktop/macos/Images.swift +113 -0
- data/lib/samagotchi/desktop/macos/Info.plist.erb +6 -0
- data/lib/samagotchi/desktop/macos/Panel.swift +180 -25
- data/lib/samagotchi/desktop/macos.rb +59 -8
- data/lib/samagotchi/desktop_command.rb +6 -3
- data/lib/samagotchi/edit_preview.rb +82 -0
- data/lib/samagotchi/empty_answer_retry.rb +43 -0
- data/lib/samagotchi/engine.rb +434 -140
- data/lib/samagotchi/gem_update.rb +89 -0
- data/lib/samagotchi/guardrails/approval.rb +35 -4
- data/lib/samagotchi/guardrails/load_failures.rb +9 -3
- data/lib/samagotchi/guardrails/scratch_writes.rb +40 -0
- data/lib/samagotchi/guardrails.rb +1 -0
- data/lib/samagotchi/hooks/registry.rb +24 -5
- data/lib/samagotchi/host_registry.rb +9 -12
- data/lib/samagotchi/idle_client.rb +24 -15
- data/lib/samagotchi/idle_recap.rb +5 -1
- data/lib/samagotchi/idle_reminders.rb +2 -2
- data/lib/samagotchi/image_store.rb +10 -6
- data/lib/samagotchi/kernel_loop.rb +73 -94
- data/lib/samagotchi/live_versions.rb +59 -0
- data/lib/samagotchi/llm/api_key.rb +41 -0
- data/lib/samagotchi/llm/chat_loop.rb +132 -29
- data/lib/samagotchi/llm/errors.rb +41 -9
- data/lib/samagotchi/llm/http.rb +57 -17
- data/lib/samagotchi/llm/openai_chat.rb +17 -30
- data/lib/samagotchi/log_subscriber.rb +18 -3
- data/lib/samagotchi/memory_bundle/installer.rb +65 -63
- data/lib/samagotchi/memory_bundle/provenance.rb +51 -12
- data/lib/samagotchi/memory_bundle/shipped_update.rb +157 -0
- data/lib/samagotchi/memory_bundle/status.rb +4 -1
- data/lib/samagotchi/memory_bundle/system_bundle.rb +81 -53
- data/lib/samagotchi/model_profile.rb +24 -1
- data/lib/samagotchi/plugin/context.rb +22 -1
- data/lib/samagotchi/plugin/sessions.rb +3 -1
- data/lib/samagotchi/prompt.rb +4 -2
- data/lib/samagotchi/reminder_store.rb +1 -9
- data/lib/samagotchi/reply_wait.rb +126 -0
- data/lib/samagotchi/sampling_settings.rb +58 -0
- data/lib/samagotchi/self_report.rb +18 -3
- data/lib/samagotchi/send_command.rb +252 -11
- data/lib/samagotchi/session.rb +52 -11
- data/lib/samagotchi/session_archive_command.rb +107 -0
- data/lib/samagotchi/session_commands.rb +46 -7
- data/lib/samagotchi/session_manager.rb +115 -25
- data/lib/samagotchi/session_metrics.rb +222 -106
- data/lib/samagotchi/steer.rb +72 -0
- data/lib/samagotchi/terminal_ui/attached_loop.rb +57 -28
- data/lib/samagotchi/terminal_ui/event_renderer.rb +21 -11
- data/lib/samagotchi/terminal_ui/formatting.rb +40 -8
- data/lib/samagotchi/terminal_ui/input_support.rb +7 -19
- data/lib/samagotchi/terminal_ui/question_prompt.rb +35 -0
- data/lib/samagotchi/terminal_ui.rb +134 -247
- data/lib/samagotchi/text_diff.rb +181 -0
- data/lib/samagotchi/thinking.rb +115 -0
- data/lib/samagotchi/tool_activity.rb +3 -1
- data/lib/samagotchi/tool_runner.rb +34 -1
- data/lib/samagotchi/tools/ask_user_question.rb +41 -33
- data/lib/samagotchi/tools/builtins.rb +15 -4
- data/lib/samagotchi/tools/delegate_wait.rb +26 -69
- data/lib/samagotchi/tools/edit.rb +23 -9
- data/lib/samagotchi/tools/execute.rb +52 -14
- data/lib/samagotchi/tools/task_runtime.rb +19 -0
- data/lib/samagotchi/tools/task_wait.rb +27 -3
- data/lib/samagotchi/tools/write.rb +4 -0
- data/lib/samagotchi/turn_flow.rb +12 -2
- data/lib/samagotchi/turn_note.rb +60 -6
- data/lib/samagotchi/update_command.rb +308 -0
- data/lib/samagotchi/update_hint.rb +59 -0
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/vision_support.rb +7 -9
- data/lib/samagotchi/web/app.rb +91 -7
- data/lib/samagotchi/web/message_parts.rb +8 -3
- data/lib/samagotchi/web/public/activity.js +13 -1
- data/lib/samagotchi/web/public/annotate_presets.js +26 -0
- data/lib/samagotchi/web/public/annotations.js +13 -0
- data/lib/samagotchi/web/public/app.js +472 -111
- data/lib/samagotchi/web/public/card.js +5 -3
- data/lib/samagotchi/web/public/chat_view.js +13 -1
- data/lib/samagotchi/web/public/copy.js +20 -4
- data/lib/samagotchi/web/public/ctx.js +15 -0
- data/lib/samagotchi/web/public/data.js +23 -6
- data/lib/samagotchi/web/public/diff_view.js +58 -0
- data/lib/samagotchi/web/public/format.js +9 -0
- data/lib/samagotchi/web/public/index.html +60 -3
- data/lib/samagotchi/web/public/notify.js +175 -0
- data/lib/samagotchi/web/public/question_card.js +5 -2
- data/lib/samagotchi/web/public/sessions_list.js +7 -0
- data/lib/samagotchi/web/public/timing.js +39 -14
- data/lib/samagotchi/web/public/turn_events.js +75 -5
- data/lib/samagotchi/web/public/turn_view.js +49 -8
- data/lib/samagotchi/web/server.rb +8 -4
- data/lib/samagotchi/web/session_hub.rb +2 -1
- data/lib/samagotchi/web/session_summary.rb +24 -1
- data/lib/samagotchi/worker.rb +16 -4
- metadata +31 -1
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "config"
|
|
4
|
+
require_relative "desktop"
|
|
5
|
+
require_relative "gem_update"
|
|
6
|
+
require_relative "installed_gem"
|
|
7
|
+
require_relative "live_versions"
|
|
8
|
+
require_relative "memory_bundle/shipped_update"
|
|
9
|
+
require_relative "memory_bundle/system_bundle"
|
|
10
|
+
require_relative "session"
|
|
11
|
+
require_relative "version"
|
|
12
|
+
|
|
13
|
+
module Samagotchi
|
|
14
|
+
# `chi update`: brings an installed chi up to date in one go: the system
|
|
15
|
+
# bundle, the shipped bundles the user installed, and the macOS helper.
|
|
16
|
+
# Unedited things update; edited ones are kept (.md) or replaced and
|
|
17
|
+
# reported (hooks, plugin, rules, which didn't load once edited). Live
|
|
18
|
+
# workers and a running chi web are reported, never restarted. Prints one
|
|
19
|
+
# table; a second run changes nothing and says so.
|
|
20
|
+
class UpdateCommand
|
|
21
|
+
USAGE = <<~TEXT
|
|
22
|
+
Usage: chi update [--dry-run] [--no-gem] [--no-bundles] [--no-desktop]
|
|
23
|
+
Updates this chi's install: the gem (from rubygems.org), then the
|
|
24
|
+
system bundle, the bundles chi ships that you installed (chi bundle
|
|
25
|
+
list), and the desktop helper (macOS; rebuilt only when its sources
|
|
26
|
+
changed). Edited memory files are kept and reported; nothing is
|
|
27
|
+
installed that isn't already, and old gem versions stay.
|
|
28
|
+
--dry-run show what would change; change nothing
|
|
29
|
+
--no-gem don't install a newer gem (config update.gem: false)
|
|
30
|
+
--no-bundles leave the shipped bundles alone (config update.bundles: false)
|
|
31
|
+
--no-desktop leave the desktop helper alone (config update.desktop: false)
|
|
32
|
+
Running sessions keep their chi until they idle out (30 min) or
|
|
33
|
+
chi sessions stop ID; a running chi web needs a restart.
|
|
34
|
+
TEXT
|
|
35
|
+
|
|
36
|
+
FLAGS = { "--dry-run" => :dry_run, "--no-gem" => :no_gem, "--no-bundles" => :no_bundles, "--no-desktop" => :no_desktop,
|
|
37
|
+
# Hidden: no pbs/lsregister/open/pkill for a helper under HOME=<tmp>.
|
|
38
|
+
"--no-register" => :no_register }.freeze
|
|
39
|
+
|
|
40
|
+
# A table row. info rows (live workers, chi web) say what runs; they
|
|
41
|
+
# never fail.
|
|
42
|
+
Row = Struct.new(:component, :from, :to, :status, :failed, keyword_init: true)
|
|
43
|
+
|
|
44
|
+
# What a row says after a change, and in a dry run.
|
|
45
|
+
VERBS = { update: ["updated", "would update"], install: ["installed", "would install"],
|
|
46
|
+
restore: ["restored missing files", "would restore missing files"] }.freeze
|
|
47
|
+
|
|
48
|
+
# @param gem_spec [Gem::Specification, nil] the installed gem (nil: a checkout)
|
|
49
|
+
# @param platform [#call] register: → a Desktop::MacOS-like object
|
|
50
|
+
# @param exec [#call] argv → replaces this process (the new chi's update)
|
|
51
|
+
# @param bundler [Boolean] running under Bundler (bundle exec)
|
|
52
|
+
def initialize(argv, stdout: $stdout, stderr: $stderr, gem_spec: InstalledGem.spec,
|
|
53
|
+
shipped_dir: MemoryBundle::SourceNormalizer::SHIPPED_DIR, platform: nil,
|
|
54
|
+
supported: Desktop.supported?, state_dir: Session.default_state_dir, web: nil,
|
|
55
|
+
gem_update: GemUpdate.new, exec: nil, bundler: nil)
|
|
56
|
+
@gem_update = gem_update
|
|
57
|
+
@exec = exec || ->(argv) { Kernel.exec(*argv) }
|
|
58
|
+
@bundler = bundler.nil? ? (defined?(::Bundler) || !ENV["BUNDLE_GEMFILE"].to_s.empty?) : bundler
|
|
59
|
+
@argv = argv.dup
|
|
60
|
+
@stdout = stdout
|
|
61
|
+
@stderr = stderr
|
|
62
|
+
@gem_spec = gem_spec
|
|
63
|
+
@shipped_dir = shipped_dir
|
|
64
|
+
@platform = platform || ->(register:) { Desktop::MacOS.new(register: register) }
|
|
65
|
+
@supported = supported
|
|
66
|
+
@state_dir = state_dir
|
|
67
|
+
@web = web || [Config.get("web.host").to_s, Config.get("web.port").to_i]
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# @return [Integer] 0 nothing failed, 1 a part failed (or a checkout), 2 usage
|
|
71
|
+
def run
|
|
72
|
+
options = parse or return 2
|
|
73
|
+
if options[:help]
|
|
74
|
+
@stdout.puts(USAGE)
|
|
75
|
+
return 0
|
|
76
|
+
end
|
|
77
|
+
unless @gem_spec
|
|
78
|
+
@stderr.puts("chi update: running from a checkout (#{InstalledGem::ROOT}): chi update updates an installed chi; " \
|
|
79
|
+
"git pull, or chi bundle upgrade NAME for one bundle")
|
|
80
|
+
return 1
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
@dry_run = options[:dry_run]
|
|
84
|
+
gem = gem_row(options)
|
|
85
|
+
rows = [gem, system_bundle_row, *bundle_rows(options), desktop_row(options), *live_rows].compact
|
|
86
|
+
print_table(rows)
|
|
87
|
+
footers(options).each { |line| @stdout.puts(line) }
|
|
88
|
+
@stdout.puts(summary(rows))
|
|
89
|
+
rows.any?(&:failed) ? 1 : 0
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
private
|
|
93
|
+
|
|
94
|
+
def parse
|
|
95
|
+
argv = @argv.dup
|
|
96
|
+
options = {}
|
|
97
|
+
while (arg = argv.shift)
|
|
98
|
+
if %w[-h --help help].include?(arg)
|
|
99
|
+
options[:help] = true
|
|
100
|
+
elsif arg == "--gem-from" && argv.first
|
|
101
|
+
# Hidden: the old version, passed by the chi that installed this one.
|
|
102
|
+
options[:gem_from] = argv.shift
|
|
103
|
+
elsif FLAGS.key?(arg)
|
|
104
|
+
options[FLAGS[arg]] = true
|
|
105
|
+
else
|
|
106
|
+
@stderr.puts("chi update: unknown option #{arg}")
|
|
107
|
+
@stderr.puts(USAGE)
|
|
108
|
+
return nil
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
options
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Installs a newer gem and hands over to it (exec: the new code syncs
|
|
115
|
+
# and prints the table), or says why not. A failed install is a failed
|
|
116
|
+
# row; the sync then runs on this version.
|
|
117
|
+
def gem_row(options)
|
|
118
|
+
row = Row.new(component: "chi (gem)", from: VERSION)
|
|
119
|
+
if options[:gem_from]
|
|
120
|
+
row.from = options[:gem_from]
|
|
121
|
+
row.to = VERSION
|
|
122
|
+
return done(row, :update)
|
|
123
|
+
end
|
|
124
|
+
if (why = off(options, "gem"))
|
|
125
|
+
return skipped(row, why)
|
|
126
|
+
end
|
|
127
|
+
return skipped(row, "under Bundler: bundle update samagotchi") if @bundler
|
|
128
|
+
|
|
129
|
+
latest = begin
|
|
130
|
+
@gem_update.latest
|
|
131
|
+
rescue GemUpdate::Error => e
|
|
132
|
+
return row.tap { |r| r.status = "couldn't check (#{e.message})" }
|
|
133
|
+
end
|
|
134
|
+
return up_to_date(row) unless @gem_update.newer?(latest)
|
|
135
|
+
|
|
136
|
+
row.to = latest.version
|
|
137
|
+
return done(row, :update) if @dry_run
|
|
138
|
+
|
|
139
|
+
@stdout.puts("installing samagotchi #{latest.version}…")
|
|
140
|
+
output, ok = @gem_update.install(latest)
|
|
141
|
+
return failed(row, output.lines.map(&:strip).reject(&:empty?).last.to_s) unless ok
|
|
142
|
+
|
|
143
|
+
hand_over(options, row)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# exec the wrapper, which activates the newest installed samagotchi.
|
|
147
|
+
# Without one (installed with --bindir), this version syncs.
|
|
148
|
+
def hand_over(options, row)
|
|
149
|
+
wrapper = InstalledGem.wrapper(@gem_spec)
|
|
150
|
+
return done(row, :update, "run chi update again to sync with it") unless wrapper
|
|
151
|
+
|
|
152
|
+
passed = FLAGS.select { |_, key| options[key] && key != :dry_run }.keys
|
|
153
|
+
@stdout.flush
|
|
154
|
+
@exec.call([RbConfig.ruby, wrapper, "update", "--no-gem", "--gem-from", VERSION, *passed])
|
|
155
|
+
done(row, :update) # only when exec is a spec's stand-in
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Why a part is off for this run, or nil.
|
|
159
|
+
def off(options, part)
|
|
160
|
+
return "--no-#{part}" if options[:"no_#{part}"]
|
|
161
|
+
return "update.#{part}: false" unless Config.get("update.#{part}")
|
|
162
|
+
|
|
163
|
+
nil
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def system_bundle_row
|
|
167
|
+
result = MemoryBundle::SystemBundle.sync(dry_run: @dry_run)
|
|
168
|
+
row = Row.new(component: "system bundle", from: result.from, to: result.to)
|
|
169
|
+
case result.status
|
|
170
|
+
when :installed then done(row, :install)
|
|
171
|
+
when :updated then done(row, :update, kept(result.kept, MemoryBundle::SystemBundle::BUNDLE_NAME))
|
|
172
|
+
when :restored then done(row, :restore).tap { |r| r.to = nil }
|
|
173
|
+
when :up_to_date then up_to_date(row)
|
|
174
|
+
when :newer_installed then skipped(row, "newer than this chi's #{result.to}: left").tap { |r| r.to = nil }
|
|
175
|
+
when :skipped then skipped(row, result.error)
|
|
176
|
+
else failed(row, result.error)
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def bundle_rows(options)
|
|
181
|
+
if (why = off(options, "bundles"))
|
|
182
|
+
return [Row.new(component: "bundles", status: "skipped (#{why})")]
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
rows = MemoryBundle::ShippedUpdate.plan(shipped_dir: @shipped_dir)
|
|
186
|
+
rows = MemoryBundle::ShippedUpdate.apply(rows) unless @dry_run
|
|
187
|
+
rows.map { |r| bundle_row(r) }
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def bundle_row(update)
|
|
191
|
+
row = Row.new(component: update.name, from: update.from, to: update.to)
|
|
192
|
+
case update.status
|
|
193
|
+
when :would_update, :updated
|
|
194
|
+
notes = [kept(update.kept, update.name), replaced(update.replaced)].compact
|
|
195
|
+
done(row, :update, notes.empty? ? nil : notes.join("; "))
|
|
196
|
+
when :up_to_date then up_to_date(row)
|
|
197
|
+
when :skipped then skipped(row, update.note)
|
|
198
|
+
else failed(row, update.note)
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def desktop_row(options)
|
|
203
|
+
return nil unless @supported
|
|
204
|
+
|
|
205
|
+
row = Row.new(component: "Chi Helper")
|
|
206
|
+
if (why = off(options, "desktop"))
|
|
207
|
+
return skipped(row, why)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
helper = @platform.call(register: !options[:no_register])
|
|
211
|
+
return row.tap { |r| r.status = "not installed" } unless helper.installed?
|
|
212
|
+
|
|
213
|
+
row.from = helper.app_version
|
|
214
|
+
if helper.stale?
|
|
215
|
+
row.to = VERSION
|
|
216
|
+
return done(row, :update, @dry_run ? "rebuilds and restarts it" : "restarted") { helper.upgrade { nil } }
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
return up_to_date(row) unless helper.launch_outdated?
|
|
220
|
+
|
|
221
|
+
helper.refresh_launch_file unless @dry_run
|
|
222
|
+
up_to_date(row).tap { |r| r.status += @dry_run ? " (would refresh the launch file)" : " (launch file refreshed)" }
|
|
223
|
+
rescue Desktop::MacOS::Error, SystemCallError => e
|
|
224
|
+
failed(row, e.message.lines.first.to_s.strip)
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def live_rows
|
|
228
|
+
rows = []
|
|
229
|
+
stale = LiveVersions.stale_workers(VERSION, state_dir: @state_dir)
|
|
230
|
+
unless stale.empty?
|
|
231
|
+
versions = stale.map { |w| w.version || "an older chi" }.uniq.join(", ")
|
|
232
|
+
ids = stale.map { |w| w.session_id[0, 8] }.join(" ")
|
|
233
|
+
rows << Row.new(component: "workers",
|
|
234
|
+
status: "#{stale.size} live on #{versions}: they move to #{VERSION} at idle exit (30 min) " \
|
|
235
|
+
"or chi sessions stop #{ids}")
|
|
236
|
+
end
|
|
237
|
+
host, port = @web
|
|
238
|
+
web = LiveVersions.web_version(host, port)
|
|
239
|
+
if web && web != VERSION
|
|
240
|
+
rows << Row.new(component: "chi web :#{port}", from: web,
|
|
241
|
+
status: "running old code: new sessions it starts run #{web} too; restart it")
|
|
242
|
+
end
|
|
243
|
+
rows
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def footers(options)
|
|
247
|
+
return [] if off(options, "bundles")
|
|
248
|
+
|
|
249
|
+
names = MemoryBundle::ShippedUpdate.not_installed(shipped_dir: @shipped_dir).map(&:source)
|
|
250
|
+
names.empty? ? [] : ["Also shipped, not installed: #{names.join(", ")} (chi bundle install NAME)"]
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def summary(rows)
|
|
254
|
+
return "(dry run: nothing was changed)" if @dry_run
|
|
255
|
+
return "some parts failed (see above)" if rows.any?(&:failed)
|
|
256
|
+
|
|
257
|
+
rows.all? { |r| r.status.start_with?("up to date", "skipped", "not installed", "couldn't check") } ? "everything is up to date" : "done"
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# "updated" (or "would update" in a dry run), with a note; the block
|
|
261
|
+
# makes the change (not in a dry run).
|
|
262
|
+
def done(row, verb, note = nil)
|
|
263
|
+
yield if block_given? && !@dry_run
|
|
264
|
+
text = VERBS.fetch(verb)[@dry_run ? 1 : 0]
|
|
265
|
+
row.status = note ? "#{text} (#{note})" : text
|
|
266
|
+
row
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def up_to_date(row)
|
|
270
|
+
row.to = nil
|
|
271
|
+
row.status = "up to date"
|
|
272
|
+
row
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def skipped(row, why)
|
|
276
|
+
row.status = "skipped (#{why})"
|
|
277
|
+
row
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def failed(row, why)
|
|
281
|
+
row.status = "failed (#{why})"
|
|
282
|
+
row.failed = true
|
|
283
|
+
row
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def kept(files, bundle)
|
|
287
|
+
return nil if files.empty?
|
|
288
|
+
|
|
289
|
+
verb = @dry_run ? "would keep" : "kept"
|
|
290
|
+
"#{verb} your edits in #{files.join(", ")}: chi bundle diff #{bundle} #{files.first}"
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def replaced(files)
|
|
294
|
+
return nil if files.empty?
|
|
295
|
+
|
|
296
|
+
"#{@dry_run ? "would replace" : "replaced"} edited #{files.join(", ")} (it wasn't loading)"
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def print_table(rows)
|
|
300
|
+
header = %w[component from to status]
|
|
301
|
+
cells = rows.map { |r| [r.component, r.from.to_s, r.to.to_s, r.status] }
|
|
302
|
+
widths = (0..2).map { |i| ([header] + cells).map { |c| c[i].length }.max }
|
|
303
|
+
([header] + cells).each do |c|
|
|
304
|
+
@stdout.puts((0..2).map { |i| c[i].ljust(widths[i]) }.join(" ") + " " + c[3])
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require_relative "desktop"
|
|
5
|
+
require_relative "memory_bundle/shipped_update"
|
|
6
|
+
require_relative "session"
|
|
7
|
+
require_relative "version"
|
|
8
|
+
|
|
9
|
+
module Samagotchi
|
|
10
|
+
# One line at the first interactive start of a new chi version (REPL,
|
|
11
|
+
# attached, chi web) when something chi update would update is behind:
|
|
12
|
+
# "chi 0.3.1: 2 bundles and the desktop helper can be updated: chi update".
|
|
13
|
+
# Noted once per version in $XDG_STATE_HOME/samagotchi/update_noted,
|
|
14
|
+
# whether or not anything was behind. No network; nothing is changed (the
|
|
15
|
+
# system bundle syncs itself at every start).
|
|
16
|
+
module UpdateHint
|
|
17
|
+
NOTED_FILE = "update_noted"
|
|
18
|
+
|
|
19
|
+
module_function
|
|
20
|
+
|
|
21
|
+
# Only a person starting chi at a terminal: not -p or --non-interactive
|
|
22
|
+
# (a script), not a checkout (git pull updates it), never a worker
|
|
23
|
+
# (workers don't start through bin/chi's launch).
|
|
24
|
+
def wanted?(prompt:, non_interactive:, tty:, installed:)
|
|
25
|
+
!!(tty && installed && !prompt && !non_interactive)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# @param helper [#stale?, nil] the desktop helper (nil off macOS)
|
|
29
|
+
# @return [String, nil] the line shown, if any
|
|
30
|
+
def show(io: $stderr, env: ENV, version: VERSION, helper: default_helper(env),
|
|
31
|
+
shipped_dir: MemoryBundle::SourceNormalizer::SHIPPED_DIR)
|
|
32
|
+
path = File.join(File.dirname(Session.default_state_dir(env: env)), NOTED_FILE)
|
|
33
|
+
noted = File.exist?(path) ? File.readlines(path, chomp: true) : []
|
|
34
|
+
return nil if noted.include?(version)
|
|
35
|
+
|
|
36
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
37
|
+
File.write(path, "#{version}\n", mode: "a")
|
|
38
|
+
line = message(version, shipped_dir: shipped_dir, helper: helper)
|
|
39
|
+
io.puts(line) if line
|
|
40
|
+
line
|
|
41
|
+
rescue StandardError
|
|
42
|
+
nil
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def message(version, shipped_dir:, helper:)
|
|
46
|
+
bundles = MemoryBundle::ShippedUpdate.plan(shipped_dir: shipped_dir).count { |r| r.status == :would_update }
|
|
47
|
+
parts = []
|
|
48
|
+
parts << (bundles == 1 ? "1 bundle" : "#{bundles} bundles") if bundles.positive?
|
|
49
|
+
parts << "the desktop helper" if helper&.stale?
|
|
50
|
+
return nil if parts.empty?
|
|
51
|
+
|
|
52
|
+
"chi #{version}: #{parts.join(" and ")} can be updated: chi update"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def default_helper(env)
|
|
56
|
+
Desktop.supported? ? Desktop::MacOS.new(env: env) : nil
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
data/lib/samagotchi/version.rb
CHANGED
|
@@ -25,10 +25,12 @@ module Samagotchi
|
|
|
25
25
|
# @param profile [ModelProfile, nil] the model's prompt profile (native)
|
|
26
26
|
# @param adapter [LLM::OpenAIChat, nil] the chat host's adapter
|
|
27
27
|
# @param models [Hash, nil] ConfigFile.model_settings (specs)
|
|
28
|
+
# @param names [Array<String>, nil] the model's lookup names, the typed
|
|
29
|
+
# alias first (Engine#model_lookup_names); default: the target's names
|
|
28
30
|
# @return [Answer]
|
|
29
|
-
def self.for(target, profile: nil, adapter: nil, models: nil)
|
|
31
|
+
def self.for(target, profile: nil, adapter: nil, models: nil, names: nil)
|
|
30
32
|
entry = target.entry
|
|
31
|
-
configured = configured(target, entry, models)
|
|
33
|
+
configured = configured(target, entry, models, names)
|
|
32
34
|
return configured if configured&.no?
|
|
33
35
|
|
|
34
36
|
if entry.chat?
|
|
@@ -44,18 +46,14 @@ module Samagotchi
|
|
|
44
46
|
marker.is_a?(String) && !marker.empty? ? marker : nil
|
|
45
47
|
end
|
|
46
48
|
|
|
47
|
-
def self.configured(target, entry, models)
|
|
49
|
+
def self.configured(target, entry, models, names = nil)
|
|
48
50
|
models ||= begin
|
|
49
51
|
ConfigFile.model_settings
|
|
50
52
|
rescue StandardError
|
|
51
53
|
{}
|
|
52
54
|
end
|
|
53
|
-
[target.model, target.bare_model]
|
|
54
|
-
|
|
55
|
-
next if value.nil?
|
|
56
|
-
|
|
57
|
-
return Answer.new(value: value, reason: value ? nil : "models: #{key} sets vision: false")
|
|
58
|
-
end
|
|
55
|
+
key, value = ConfigFile.model_setting(names || [target.model, target.bare_model], :vision, models: models)
|
|
56
|
+
return Answer.new(value: value, reason: value ? nil : "models: #{key} sets vision: false") if key
|
|
59
57
|
return nil if entry.vision.nil?
|
|
60
58
|
|
|
61
59
|
Answer.new(value: entry.vision, reason: entry.vision ? nil : "hosts.#{entry.name} sets vision: false")
|
data/lib/samagotchi/web/app.rb
CHANGED
|
@@ -13,10 +13,12 @@ require_relative "../bridge/bounded_queue"
|
|
|
13
13
|
require_relative "../session"
|
|
14
14
|
require_relative "../session_manager"
|
|
15
15
|
require_relative "../session_commands"
|
|
16
|
+
require_relative "../steer"
|
|
16
17
|
require_relative "../host_registry"
|
|
17
18
|
require_relative "../model_profile"
|
|
18
19
|
require_relative "../project_scope"
|
|
19
20
|
require_relative "../version"
|
|
21
|
+
require_relative "../config"
|
|
20
22
|
require_relative "../output_formatter"
|
|
21
23
|
require_relative "../image_store"
|
|
22
24
|
require_relative "../context_note"
|
|
@@ -71,6 +73,9 @@ module Samagotchi
|
|
|
71
73
|
# @param turn_view [Boolean] the page's per-turn view (web.turn_view,
|
|
72
74
|
# the default); false is the classic row of bubbles; ?view=turn|chat
|
|
73
75
|
# overrides it for one page load
|
|
76
|
+
# @param annotate_presets [String, Array] the quick replies next to
|
|
77
|
+
# Annotate (web.annotate_presets, "|"-separated); "" shows none. The
|
|
78
|
+
# page parses them (annotate_presets.js).
|
|
74
79
|
# @param hub [SessionHub, nil] the session projection GET /api/events
|
|
75
80
|
# streams from; without one the route answers 503
|
|
76
81
|
# @param registry [HostRegistry, nil] the hosts GET /api/models lists
|
|
@@ -79,6 +84,7 @@ module Samagotchi
|
|
|
79
84
|
# waits for the hosts' lists
|
|
80
85
|
def initialize(manager: nil, session_class: nil, state_dir: nil, public_dir: nil,
|
|
81
86
|
bridge_wait_timeout: BRIDGE_WAIT_TIMEOUT, markdown: false, turn_view: true, hub: nil,
|
|
87
|
+
annotate_presets: Config::BY_KEY["web.annotate_presets"].default,
|
|
82
88
|
events_heartbeat: EVENTS_HEARTBEAT, events_queue: EVENTS_QUEUE,
|
|
83
89
|
registry: nil, models_wait_timeout: MODELS_WAIT_TIMEOUT)
|
|
84
90
|
@manager = manager || SessionManager
|
|
@@ -92,6 +98,7 @@ module Samagotchi
|
|
|
92
98
|
@bridge_wait_timeout = bridge_wait_timeout
|
|
93
99
|
@markdown_renderer = MarkdownRenderer.new(enabled: markdown)
|
|
94
100
|
@turn_view = turn_view
|
|
101
|
+
@annotate_presets = annotate_presets.is_a?(Array) ? annotate_presets.join("|") : annotate_presets.to_s
|
|
95
102
|
@hub = hub
|
|
96
103
|
@events_heartbeat = events_heartbeat
|
|
97
104
|
@events_queue = events_queue
|
|
@@ -138,6 +145,9 @@ module Samagotchi
|
|
|
138
145
|
if (m = %r{\A/api/sessions/([^/]+)/stop\z}.match(req.path_info)) && req.post?
|
|
139
146
|
return handle_stop(req, m[1])
|
|
140
147
|
end
|
|
148
|
+
if (m = %r{\A/api/sessions/([^/]+)/(archive|unarchive)\z}.match(req.path_info)) && req.post?
|
|
149
|
+
return m[2] == "archive" ? handle_archive(req, m[1]) : handle_unarchive(req, m[1])
|
|
150
|
+
end
|
|
141
151
|
if (m = %r{\A/api/sessions/([^/]+)/turn\z}.match(req.path_info)) && req.post?
|
|
142
152
|
return handle_turn(req, m[1])
|
|
143
153
|
end
|
|
@@ -224,6 +234,8 @@ module Samagotchi
|
|
|
224
234
|
offset = sanitize_offset(req.params["offset"])
|
|
225
235
|
return list_from_hub(root, sort: sort, order: order, limit: limit, offset: offset) if @hub
|
|
226
236
|
|
|
237
|
+
# Archived ones too, as the hub's: the page filters them at render.
|
|
238
|
+
scope[:include_archived] = true
|
|
227
239
|
sessions = if @state_dir
|
|
228
240
|
@manager.list_sessions(state_dir: @state_dir, sort: sort, order: order, limit: limit, offset: offset,
|
|
229
241
|
**scope)
|
|
@@ -396,11 +408,16 @@ module Samagotchi
|
|
|
396
408
|
# spelled as GET /api/models lists it); blank means the default.
|
|
397
409
|
model = (body["model"] || body["model_name"]).to_s.strip
|
|
398
410
|
folder[:model_name] = model unless model.empty?
|
|
411
|
+
# preview: an idle session's first message, sent as its first turn
|
|
412
|
+
# next; the list and the notifications name the session by it
|
|
413
|
+
# until that turn is saved.
|
|
414
|
+
preview = idle ? body["preview"].to_s.strip : ""
|
|
415
|
+
folder[:title] = preview unless preview.empty?
|
|
399
416
|
begin
|
|
400
417
|
session = @manager.spawn_session(prompt: idle ? nil : prompt.to_s, state_dir: @state_dir, **folder)
|
|
401
|
-
rescue
|
|
402
|
-
|
|
403
|
-
|
|
418
|
+
rescue ModelProfile::MissingModel => e
|
|
419
|
+
# No model configured, or one qualified with an unknown host.
|
|
420
|
+
return error_response(400, "invalid_model", e.message)
|
|
404
421
|
end
|
|
405
422
|
# The worker's Bridge is the single live transport: wait (bounded) for
|
|
406
423
|
# it so the client can attach without client-side polling.
|
|
@@ -464,6 +481,7 @@ module Samagotchi
|
|
|
464
481
|
session: session_json,
|
|
465
482
|
history: read_history(id),
|
|
466
483
|
messages: messages_for_display(raw_messages, parts: req.params["parts"] == "1"),
|
|
484
|
+
failed_turn: failed_turn_for(session, raw_messages),
|
|
467
485
|
current_turn: current_turn,
|
|
468
486
|
queued: turn_snapshot ? Array(turn_snapshot["queued"]) : [],
|
|
469
487
|
recap: turn_snapshot && turn_snapshot["recap"],
|
|
@@ -597,7 +615,8 @@ module Samagotchi
|
|
|
597
615
|
client = live_bridge_client(id)
|
|
598
616
|
return error_response(503, "not_live", "no live bridge for session #{id}") unless client
|
|
599
617
|
|
|
600
|
-
|
|
618
|
+
# card: a card's action; its command events say so (no echo).
|
|
619
|
+
reply = client.post_command(line: line, client_id: body["client_id"], card: body["card"] == true)
|
|
601
620
|
case reply.status
|
|
602
621
|
when 202 then json_response(202, reply.json || { status: "accepted" })
|
|
603
622
|
when 400 then error_response(400, reply.json&.dig("error") || "unknown_command", reply.json&.dig("detail") || "not a session command")
|
|
@@ -805,6 +824,32 @@ module Samagotchi
|
|
|
805
824
|
error_response(404, "not_found", e.message)
|
|
806
825
|
end
|
|
807
826
|
|
|
827
|
+
# POST /api/sessions/:id/archive: hide it (and its delegates) from the
|
|
828
|
+
# lists, keep it for good. A live idle worker is stopped first; a turn
|
|
829
|
+
# running in it or a delegate, a chi REPL's session and a scratch one
|
|
830
|
+
# are refused (409).
|
|
831
|
+
def handle_archive(_req, id)
|
|
832
|
+
result = @manager.archive_session(id, state_dir: @state_dir, wait: STOP_WAIT_SECONDS)
|
|
833
|
+
(result[:archived] + result[:discarded]).each { |sid| @hub&.touch(sid) }
|
|
834
|
+
json_response(200, { status: "archived", session_id: result[:id], archived: result[:archived],
|
|
835
|
+
stopped: result[:stopped], discarded: result[:discarded] })
|
|
836
|
+
rescue SessionManager::OwnedByTUI
|
|
837
|
+
error_response(409, "owned_by_tui", "session #{id} is open in a chi REPL; close it there first")
|
|
838
|
+
rescue SessionManager::ArchiveRefused => e
|
|
839
|
+
error_response(409, e.reason == :scratch ? "scratch" : "busy", e.message)
|
|
840
|
+
rescue ArgumentError => e
|
|
841
|
+
error_response(404, "not_found", e.message)
|
|
842
|
+
end
|
|
843
|
+
|
|
844
|
+
# POST /api/sessions/:id/unarchive: back to the lists, delegates too.
|
|
845
|
+
def handle_unarchive(_req, id)
|
|
846
|
+
result = @manager.unarchive_session(id, state_dir: @state_dir)
|
|
847
|
+
result[:unarchived].each { |sid| @hub&.touch(sid) }
|
|
848
|
+
json_response(200, { status: "unarchived", session_id: result[:id], unarchived: result[:unarchived] })
|
|
849
|
+
rescue ArgumentError => e
|
|
850
|
+
error_response(404, "not_found", e.message)
|
|
851
|
+
end
|
|
852
|
+
|
|
808
853
|
# DELETE /api/sessions/:id: the session for good. A live worker is
|
|
809
854
|
# stopped first (the page's confirm says so); a chi REPL's session is
|
|
810
855
|
# refused.
|
|
@@ -1038,7 +1083,11 @@ module Samagotchi
|
|
|
1038
1083
|
turn_records: Array(source["turn_records"]),
|
|
1039
1084
|
tool_records: Array(source["tool_records"]),
|
|
1040
1085
|
active_turn: source["active_turn"],
|
|
1041
|
-
active_tools: Array(source["active_tools"])
|
|
1086
|
+
active_tools: Array(source["active_tools"]),
|
|
1087
|
+
# The context last seen and the token sums, for the ctx meter
|
|
1088
|
+
# before the next turn streams (live over saved).
|
|
1089
|
+
context: source["context"],
|
|
1090
|
+
tokens: source["tokens"]
|
|
1042
1091
|
}
|
|
1043
1092
|
end
|
|
1044
1093
|
|
|
@@ -1084,12 +1133,19 @@ module Samagotchi
|
|
|
1084
1133
|
return nil unless saved
|
|
1085
1134
|
|
|
1086
1135
|
messages = Array(session.messages)
|
|
1087
|
-
since = messages.drop(saved[:covered].to_i).count { |m| (m
|
|
1136
|
+
since = messages.drop(saved[:covered].to_i).count { |m| Samagotchi::Steer.prompt?(m) }
|
|
1088
1137
|
{ text: saved[:text], turns_since: since }
|
|
1089
1138
|
rescue StandardError
|
|
1090
1139
|
nil
|
|
1091
1140
|
end
|
|
1092
1141
|
|
|
1142
|
+
# The step (iteration) that answered the steer at +index+: one past
|
|
1143
|
+
# the model messages between it and the prompt before it.
|
|
1144
|
+
def steer_step(list, index)
|
|
1145
|
+
list[0...index].reverse_each.take_while { |m| !Samagotchi::Steer.prompt?(m) }
|
|
1146
|
+
.count { |m| %w[model assistant].include?((m[:role] || m["role"]).to_s) } + 1
|
|
1147
|
+
end
|
|
1148
|
+
|
|
1093
1149
|
def read_history(id)
|
|
1094
1150
|
raw = @manager.read_responses(id, since_time: nil, state_dir: @state_dir)
|
|
1095
1151
|
raw.map { |chunk| OutputFormatter.strip(chunk) }.reject(&:empty?)
|
|
@@ -1113,6 +1169,12 @@ module Samagotchi
|
|
|
1113
1169
|
filtered << { role: "note", content: Samagotchi::ContextNote.text_of(m), label: Samagotchi::ContextNote.label_of(m) }
|
|
1114
1170
|
next
|
|
1115
1171
|
end
|
|
1172
|
+
# A plugin's steer: a row of the step that answered it, which the
|
|
1173
|
+
# page finds by `step`.
|
|
1174
|
+
if Samagotchi::Steer.steer?(m)
|
|
1175
|
+
filtered << { role: "steer", content: content, source: (m[:source] || m["source"]).to_s, step: steer_step(list, index) }
|
|
1176
|
+
next
|
|
1177
|
+
end
|
|
1116
1178
|
next if role == "system"
|
|
1117
1179
|
next if role == "tool_response"
|
|
1118
1180
|
|
|
@@ -1129,7 +1191,16 @@ module Samagotchi
|
|
|
1129
1191
|
message = { role: norm_role, content: stripped }
|
|
1130
1192
|
images = m[:images] || m["images"]
|
|
1131
1193
|
message[:images] = Array(images).map { |ref| ImageStore.symbolize(ref).slice(:file, :name, :width, :height) } if norm_role == "user" && images.is_a?(Array) && !images.empty?
|
|
1132
|
-
|
|
1194
|
+
if norm_role == "assistant"
|
|
1195
|
+
# What an after_turn hook presented (AnswerDisplay): rendered and
|
|
1196
|
+
# copied in place of the answer, sanitised the same way; content
|
|
1197
|
+
# stays the model's text, which the page finds the bubble by.
|
|
1198
|
+
display = m[:display] || m["display"]
|
|
1199
|
+
shown = display.is_a?(String) ? Samagotchi::OutputFormatter.strip_markup(display) : ""
|
|
1200
|
+
message[:display] = shown unless shown.strip.empty?
|
|
1201
|
+
rendered = message[:display] || stripped
|
|
1202
|
+
message[:html] = @markdown_renderer.render(rendered) if !rendered.empty? && @markdown_renderer.available?
|
|
1203
|
+
end
|
|
1133
1204
|
message[:parts] = did if did
|
|
1134
1205
|
filtered << message
|
|
1135
1206
|
end
|
|
@@ -1138,6 +1209,17 @@ module Samagotchi
|
|
|
1138
1209
|
[]
|
|
1139
1210
|
end
|
|
1140
1211
|
|
|
1212
|
+
# The last turn failed and its prompt went back to the user: the
|
|
1213
|
+
# prompt (session.last_prompt) and why, for the page to show after the
|
|
1214
|
+
# conversation as it did live. nil otherwise.
|
|
1215
|
+
def failed_turn_for(session, msgs)
|
|
1216
|
+
summary = Samagotchi::TurnNote.restored_failure(msgs)
|
|
1217
|
+
prompt = session.last_prompt.to_s
|
|
1218
|
+
return nil if summary.nil? || prompt.strip.empty?
|
|
1219
|
+
|
|
1220
|
+
{ prompt: prompt, summary: summary }
|
|
1221
|
+
end
|
|
1222
|
+
|
|
1141
1223
|
# The parts of the assistant message at +index+, with the tool_response
|
|
1142
1224
|
# messages right after it.
|
|
1143
1225
|
def message_parts(list, index)
|
|
@@ -1199,6 +1281,8 @@ module Samagotchi
|
|
|
1199
1281
|
def index_data_attributes(req)
|
|
1200
1282
|
attrs = { "sessions-dir" => sessions_dir_label, "server-dir" => home_label(Dir.pwd) }
|
|
1201
1283
|
attrs["turn-view"] = "1" if turn_view?(req.params["view"])
|
|
1284
|
+
# Always there: an empty one means no presets, not the default.
|
|
1285
|
+
attrs["annotate-presets"] = @annotate_presets
|
|
1202
1286
|
dir, = scope_dir(req.params["dir"])
|
|
1203
1287
|
root = dir && ProjectScope.root_for(dir)
|
|
1204
1288
|
if root
|
|
@@ -93,9 +93,10 @@ module Samagotchi
|
|
|
93
93
|
outputs = split_outputs(joined, calls.length)
|
|
94
94
|
shown = saved_list(responses, :tool_params, calls.length)
|
|
95
95
|
labels = saved_list(responses, :tool_labels, calls.length)
|
|
96
|
+
diffs = saved_list(responses, :tool_diffs, calls.length)
|
|
96
97
|
images = split_images(responses, calls.length)
|
|
97
98
|
calls.each_with_index.map do |call, i|
|
|
98
|
-
tool_part(call, outputs[i], registry, shown[i], images[i], label: labels[i])
|
|
99
|
+
tool_part(call, outputs[i], registry, shown[i], images[i], label: labels[i], diff: diffs[i])
|
|
99
100
|
end
|
|
100
101
|
end
|
|
101
102
|
|
|
@@ -124,7 +125,8 @@ module Samagotchi
|
|
|
124
125
|
call = LLM::NativeToolNormalizer.normalize(ref) || { name: ref.name }
|
|
125
126
|
response = by_id[field(raw, :id)] || (field(responses[i], :tool_call_id).nil? ? responses[i] : nil)
|
|
126
127
|
tool_part(call, response && field(response, :content).to_s, registry, field(response, :tool_params),
|
|
127
|
-
response && field(response, :images), label: field(response, :tool_labels)
|
|
128
|
+
response && field(response, :images), label: field(response, :tool_labels),
|
|
129
|
+
diff: field(response, :tool_diffs))
|
|
128
130
|
end
|
|
129
131
|
end
|
|
130
132
|
|
|
@@ -145,7 +147,9 @@ module Samagotchi
|
|
|
145
147
|
end
|
|
146
148
|
|
|
147
149
|
# +shown+ is the saved params line, when it is a String.
|
|
148
|
-
|
|
150
|
+
# +diff+ is what an edit/write changed (EditPreview.change), saved as
|
|
151
|
+
# tool_diffs.
|
|
152
|
+
def tool_part(call, output, registry, shown = nil, images = nil, label: nil, diff: nil)
|
|
149
153
|
name = call[:name].to_s
|
|
150
154
|
params = shown.is_a?(String) ? shown : ToolActivity.tool_activity_params(name, call, registry: registry)
|
|
151
155
|
part = { tool: name, params: params.to_s }
|
|
@@ -156,6 +160,7 @@ module Samagotchi
|
|
|
156
160
|
end
|
|
157
161
|
refs = Array(images).select { |ref| ref.is_a?(Hash) }
|
|
158
162
|
part[:images] = refs.map { |ref| ImageStore.symbolize(ref).slice(:file, :name, :width, :height) } unless refs.empty?
|
|
163
|
+
part[:diff] = diff if diff.is_a?(Hash)
|
|
159
164
|
part
|
|
160
165
|
end
|
|
161
166
|
|