samagotchi 0.156.0 → 0.158.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 +70 -1
- data/README.md +1 -1
- data/bin/chi +4 -1
- data/docs/architecture.md +18 -12
- data/docs/bundle-authoring.md +5 -5
- data/docs/bundles.md +4 -1
- data/docs/cli.md +29 -24
- data/docs/configuration.md +43 -9
- data/docs/context.md +5 -3
- data/docs/guardrails.md +12 -7
- data/docs/hooks.md +53 -83
- data/docs/internals/bridge.md +84 -7
- data/docs/internals/bundles.md +69 -46
- data/docs/internals/context-telemetry.md +4 -3
- data/docs/internals/engine.md +23 -7
- data/docs/internals/participants.md +3 -2
- data/docs/internals/remote-sessions.md +5 -2
- data/docs/internals/web-frontend.md +3 -2
- data/docs/internals/working-set.md +1 -1
- data/docs/memory.md +51 -10
- data/docs/plugins.md +48 -11
- data/docs/releasing.md +6 -5
- data/docs/sessions.md +13 -13
- data/docs/sub-agent.md +17 -2
- data/docs/testing.md +56 -5
- data/lib/samagotchi/answer_command.rb +2 -1
- data/lib/samagotchi/archive_store.rb +4 -3
- data/lib/samagotchi/bootstrap_command.rb +3 -2
- data/lib/samagotchi/bridge/card_places.rb +5 -7
- data/lib/samagotchi/bridge/card_store.rb +3 -0
- data/lib/samagotchi/bridge/turn_accumulator.rb +113 -10
- data/lib/samagotchi/bridge/turn_drawings.rb +271 -0
- data/lib/samagotchi/bridge.rb +31 -6
- data/lib/samagotchi/broadcast/recipients.rb +8 -8
- data/lib/samagotchi/broadcast/triage_model.rb +1 -1
- data/lib/samagotchi/bundle_command/check.rb +41 -10
- data/lib/samagotchi/bundle_command/installed_check.rb +13 -2
- data/lib/samagotchi/bundle_command/try/run.rb +4 -1
- data/lib/samagotchi/bundle_command/try/sandbox.rb +1 -1
- data/lib/samagotchi/bundle_command/try/trace.rb +1 -1
- data/lib/samagotchi/bundle_command.rb +20 -45
- data/lib/samagotchi/bundles/known-names/manifest.yml +4 -7
- data/lib/samagotchi/bundles/known-names/{hooks/known_names.rb → plugin.rb} +17 -0
- data/lib/samagotchi/bundles/source-links/manifest.yml +4 -7
- data/lib/samagotchi/bundles/source-links/{hooks/source_links.rb → plugin.rb} +23 -4
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +1 -1
- data/lib/samagotchi/bundles/system/manifest.yml +4 -4
- data/lib/samagotchi/bundles/system/memory_guide.md +7 -7
- data/lib/samagotchi/bundles/system/self_map.md +1 -1
- data/lib/samagotchi/child_move.rb +6 -6
- data/lib/samagotchi/child_reports.rb +2 -2
- data/lib/samagotchi/children_status.rb +15 -21
- data/lib/samagotchi/cli/parent_wait.rb +8 -22
- data/lib/samagotchi/config.rb +3 -0
- data/lib/samagotchi/context_command.rb +7 -5
- data/lib/samagotchi/context_sources.rb +9 -6
- data/lib/samagotchi/context_window.rb +9 -12
- data/lib/samagotchi/effective_model.rb +57 -22
- data/lib/samagotchi/engine/plugins.rb +1 -1
- data/lib/samagotchi/engine/recap.rb +8 -4
- data/lib/samagotchi/engine.rb +30 -9
- data/lib/samagotchi/events.rb +3 -2
- data/lib/samagotchi/extension_load.rb +26 -44
- data/lib/samagotchi/guardrails/load_failures.rb +3 -2
- data/lib/samagotchi/guardrails/path_resolve.rb +11 -0
- data/lib/samagotchi/guardrails/review_paths.rb +1 -5
- data/lib/samagotchi/guardrails_command.rb +1 -1
- data/lib/samagotchi/hook_routes.rb +2 -2
- data/lib/samagotchi/hooks.rb +2 -3
- data/lib/samagotchi/host_address.rb +52 -0
- data/lib/samagotchi/host_registry.rb +57 -45
- data/lib/samagotchi/kernel_loop.rb +7 -3
- data/lib/samagotchi/llm/chat_loop.rb +6 -3
- data/lib/samagotchi/llm_context_strategy.rb +27 -63
- data/lib/samagotchi/memory_bundle/asset_installer.rb +92 -177
- data/lib/samagotchi/memory_bundle/builder.rb +12 -35
- data/lib/samagotchi/memory_bundle/bundle_hook.rb +6 -31
- data/lib/samagotchi/memory_bundle/file_actions.rb +193 -0
- data/lib/samagotchi/memory_bundle/file_digest.rb +13 -0
- data/lib/samagotchi/memory_bundle/file_state.rb +152 -0
- data/lib/samagotchi/memory_bundle/installed_bundle.rb +1 -1
- data/lib/samagotchi/memory_bundle/installed_copy.rb +13 -19
- data/lib/samagotchi/memory_bundle/installed_manifest.rb +10 -17
- data/lib/samagotchi/memory_bundle/installer.rb +212 -178
- data/lib/samagotchi/memory_bundle/manifest.rb +51 -58
- data/lib/samagotchi/memory_bundle/merger.rb +69 -65
- data/lib/samagotchi/memory_bundle/plugin_ref.rb +6 -5
- data/lib/samagotchi/memory_bundle/profile.rb +5 -5
- data/lib/samagotchi/memory_bundle/provenance.rb +26 -52
- data/lib/samagotchi/memory_bundle/remerge.rb +31 -14
- data/lib/samagotchi/memory_bundle/shas.rb +5 -8
- data/lib/samagotchi/memory_bundle/shipped_update.rb +51 -11
- data/lib/samagotchi/memory_bundle/status.rb +26 -30
- data/lib/samagotchi/memory_bundle/system_bundle.rb +39 -10
- data/lib/samagotchi/memory_bundle/trash.rb +28 -18
- data/lib/samagotchi/memory_bundle/uninstaller.rb +16 -11
- data/lib/samagotchi/memory_orphans.rb +4 -4
- data/lib/samagotchi/memory_paths.rb +102 -13
- data/lib/samagotchi/memory_usage.rb +1 -1
- data/lib/samagotchi/model_lookup.rb +75 -0
- data/lib/samagotchi/model_profile.rb +1 -0
- data/lib/samagotchi/models_command.rb +1 -1
- data/lib/samagotchi/note_command.rb +1 -1
- data/lib/samagotchi/participant_mode.rb +2 -1
- data/lib/samagotchi/plugin/api.rb +42 -10
- data/lib/samagotchi/plugin/loader.rb +75 -15
- data/lib/samagotchi/plugin/sessions.rb +1 -1
- data/lib/samagotchi/project_command.rb +167 -0
- data/lib/samagotchi/project_init.rb +69 -0
- data/lib/samagotchi/project_scope.rb +6 -4
- data/lib/samagotchi/prompt_files.rb +10 -7
- data/lib/samagotchi/reasoning_history.rb +14 -20
- data/lib/samagotchi/recap_place.rb +26 -0
- data/lib/samagotchi/reply_wait/liveness.rb +38 -0
- data/lib/samagotchi/reply_wait/poll.rb +44 -15
- data/lib/samagotchi/reply_wait/target.rb +113 -0
- data/lib/samagotchi/reply_wait.rb +22 -2
- data/lib/samagotchi/run_options.rb +1 -0
- data/lib/samagotchi/sampling_settings.rb +9 -16
- data/lib/samagotchi/self_report.rb +34 -35
- data/lib/samagotchi/send_command.rb +17 -23
- data/lib/samagotchi/session.rb +349 -441
- data/lib/samagotchi/session_access/contract.rb +1 -1
- data/lib/samagotchi/session_access/local.rb +5 -6
- data/lib/samagotchi/session_access/remote.rb +5 -3
- data/lib/samagotchi/session_field.rb +134 -0
- data/lib/samagotchi/session_inbox.rb +49 -1
- data/lib/samagotchi/session_manager/emptiness.rb +2 -1
- data/lib/samagotchi/session_manager/listing.rb +29 -100
- data/lib/samagotchi/session_manager.rb +2 -1
- data/lib/samagotchi/session_metrics.rb +7 -4
- data/lib/samagotchi/session_paths.rb +57 -0
- data/lib/samagotchi/session_retention.rb +2 -2
- data/lib/samagotchi/session_row.rb +200 -0
- data/lib/samagotchi/session_setup.rb +15 -12
- data/lib/samagotchi/sessions_command.rb +11 -129
- data/lib/samagotchi/steer.rb +10 -0
- data/lib/samagotchi/steer_cut_after.rb +7 -14
- data/lib/samagotchi/system_prompt.rb +20 -11
- data/lib/samagotchi/terminal_ui/attached_loop.rb +6 -26
- data/lib/samagotchi/terminal_ui/join_replay.rb +91 -8
- data/lib/samagotchi/terminal_ui/local_link.rb +2 -2
- data/lib/samagotchi/terminal_ui.rb +3 -1
- data/lib/samagotchi/testing/bundle.rb +5 -12
- data/lib/samagotchi/testing/install.rb +5 -4
- data/lib/samagotchi/testing/recorder.rb +1 -1
- data/lib/samagotchi/testing.rb +1 -1
- data/lib/samagotchi/thinking.rb +16 -21
- data/lib/samagotchi/tool_declarations.rb +1 -1
- data/lib/samagotchi/tool_response.rb +5 -3
- data/lib/samagotchi/tool_runner.rb +18 -1
- data/lib/samagotchi/toolkit_binding.rb +1 -4
- data/lib/samagotchi/toolkit_command.rb +1 -4
- data/lib/samagotchi/tools/delegate.rb +60 -51
- data/lib/samagotchi/tools/delegate_relay.rb +5 -5
- data/lib/samagotchi/tools/delegate_result.rb +1 -1
- data/lib/samagotchi/tools/delegate_wait.rb +41 -17
- data/lib/samagotchi/tools/list_sessions.rb +7 -7
- data/lib/samagotchi/update_command.rb +11 -7
- data/lib/samagotchi/verify_fresh.rb +7 -11
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/vision_support.rb +11 -14
- data/lib/samagotchi/web/app.rb +66 -21
- data/lib/samagotchi/web/message_display.rb +57 -7
- data/lib/samagotchi/web/participant_access.rb +1 -1
- data/lib/samagotchi/web/project_list.rb +3 -2
- data/lib/samagotchi/web/public/activity.js +16 -7
- data/lib/samagotchi/web/public/app.js +29 -8
- data/lib/samagotchi/web/public/bubbles.js +8 -3
- data/lib/samagotchi/web/public/cards_place.js +15 -0
- data/lib/samagotchi/web/public/commands.js +5 -1
- data/lib/samagotchi/web/public/data.js +4 -2
- data/lib/samagotchi/web/public/history_shape.js +220 -0
- data/lib/samagotchi/web/public/index.html +6 -1
- data/lib/samagotchi/web/public/project_picker.js +86 -4
- data/lib/samagotchi/web/public/recap_stamp.js +17 -0
- data/lib/samagotchi/web/public/reload_check.js +71 -0
- data/lib/samagotchi/web/public/stage_model.js +7 -5
- data/lib/samagotchi/web/public/stage_view.js +7 -3
- data/lib/samagotchi/web/public/stream_handlers.js +8 -2
- data/lib/samagotchi/web/public/timing.js +43 -2
- data/lib/samagotchi/web/public/turn_block.js +215 -30
- data/lib/samagotchi/web/public/turn_drawing.js +43 -0
- data/lib/samagotchi/web/public/turn_events.js +10 -1
- data/lib/samagotchi/web/public/turn_pieces.js +29 -0
- data/lib/samagotchi/web/reload_check_route.rb +95 -0
- data/lib/samagotchi/web/server.rb +4 -2
- data/lib/samagotchi/web/session_access_routes.rb +3 -3
- data/lib/samagotchi/web/session_control.rb +1 -1
- data/lib/samagotchi/web/session_hub.rb +9 -17
- data/lib/samagotchi/web/session_show.rb +64 -2
- data/lib/samagotchi/web/session_summary.rb +36 -24
- data/lib/samagotchi/worker.rb +28 -5
- data/lib/samagotchi/worker_link.rb +3 -0
- metadata +22 -5
- data/lib/samagotchi/hooks/bundle_loader.rb +0 -191
- data/lib/samagotchi/send_command/wait_target.rb +0 -85
data/lib/samagotchi/web/app.rb
CHANGED
|
@@ -17,6 +17,7 @@ require_relative "../delivery"
|
|
|
17
17
|
require_relative "../answer_tail"
|
|
18
18
|
require_relative "../answer_refs"
|
|
19
19
|
require_relative "../question_asked"
|
|
20
|
+
require_relative "../project_init"
|
|
20
21
|
require_relative "../bridge_client"
|
|
21
22
|
require_relative "../worker_link"
|
|
22
23
|
require_relative "../bridge/bounded_queue"
|
|
@@ -27,6 +28,7 @@ require_relative "../session_metrics"
|
|
|
27
28
|
require_relative "../session_commands"
|
|
28
29
|
require_relative "../steer"
|
|
29
30
|
require_relative "../host_registry"
|
|
31
|
+
require_relative "../effective_model"
|
|
30
32
|
require_relative "../llm_context_override"
|
|
31
33
|
require_relative "../working_set"
|
|
32
34
|
require_relative "../model_catalog"
|
|
@@ -75,6 +77,7 @@ require_relative "session_show"
|
|
|
75
77
|
require_relative "session_create"
|
|
76
78
|
require_relative "session_control"
|
|
77
79
|
require_relative "verify_fresh_routes"
|
|
80
|
+
require_relative "reload_check_route"
|
|
78
81
|
require_relative "push_routes"
|
|
79
82
|
require_relative "block_renderer_routes"
|
|
80
83
|
require_relative "update_check"
|
|
@@ -109,6 +112,7 @@ module Samagotchi
|
|
|
109
112
|
include SessionCreate
|
|
110
113
|
include SessionControl
|
|
111
114
|
include VerifyFreshRoutes
|
|
115
|
+
include ReloadCheckRoute
|
|
112
116
|
include PushRoutes
|
|
113
117
|
include BlockRendererRoutes
|
|
114
118
|
include ProxyGate
|
|
@@ -146,6 +150,9 @@ module Samagotchi
|
|
|
146
150
|
# freshly-spawned worker's bridge before answering POST /api/sessions.
|
|
147
151
|
# @param working_set [Boolean] the stage view's working set
|
|
148
152
|
# (web.working_set); ?working_set=1|0 overrides it for one page load
|
|
153
|
+
# @param reload_check [Boolean] the page checks each finished turn
|
|
154
|
+
# against a reload of it and posts the counts (web.reload_check,
|
|
155
|
+
# opt-in; POST /api/sessions/:id/reload_check logs them)
|
|
149
156
|
# @param annotate_presets [String, Array] the quick replies next to
|
|
150
157
|
# Annotate (web.annotate_presets, "|"-separated); "" shows none. The
|
|
151
158
|
# page parses them (annotate_presets.js).
|
|
@@ -184,6 +191,7 @@ module Samagotchi
|
|
|
184
191
|
events_heartbeat: EVENTS_HEARTBEAT, events_queue: EVENTS_QUEUE,
|
|
185
192
|
registry: nil, models_wait_timeout: MODELS_WAIT_TIMEOUT, lan: nil,
|
|
186
193
|
working_set: Config::BY_KEY["web.working_set"].default, wait_poll_interval: ReplyWait::POLL_INTERVAL,
|
|
194
|
+
reload_check: Config::BY_KEY["web.reload_check"].default,
|
|
187
195
|
grants: nil, public_url: nil, public_token: nil, push: nil, start_dir: nil, update_runner: nil,
|
|
188
196
|
update_blocked: nil, update_ops: nil)
|
|
189
197
|
@start_dir = start_dir
|
|
@@ -199,6 +207,8 @@ module Samagotchi
|
|
|
199
207
|
@bridge_wait_timeout = bridge_wait_timeout
|
|
200
208
|
@markdown_renderer = MarkdownRenderer.new(enabled: markdown)
|
|
201
209
|
@working_set = working_set
|
|
210
|
+
@reload_check = reload_check
|
|
211
|
+
@reload_checks = ReloadCheckRoute::Seen.new
|
|
202
212
|
@annotate_presets = annotate_presets.is_a?(Array) ? annotate_presets.join("|") : annotate_presets.to_s
|
|
203
213
|
@editor_url = Editor.template(editor)
|
|
204
214
|
@hub = hub
|
|
@@ -259,6 +269,7 @@ module Samagotchi
|
|
|
259
269
|
["GET", %r{\A/(?:index\.html)?\z}, :serve_index, :participant],
|
|
260
270
|
["GET", %r{\A/api/sessions\z}, :handle_list, :owner],
|
|
261
271
|
["GET", %r{\A/api/projects\z}, :handle_projects, :owner],
|
|
272
|
+
["POST", %r{\A/api/projects\z}, :handle_project_create, :owner],
|
|
262
273
|
["POST", %r{\A/api/sessions\z}, :handle_create, :owner],
|
|
263
274
|
["GET", %r{\A/api/sessions/resolve\z}, :handle_resolve, :participant],
|
|
264
275
|
["GET", %r{\A/api/sessions/summaries\z}, :handle_summaries, :owner],
|
|
@@ -292,6 +303,7 @@ module Samagotchi
|
|
|
292
303
|
["GET", %r{\A/api/sessions/([^/]+)/worker\z}, :handle_worker, :owner, :session],
|
|
293
304
|
["POST", %r{\A/api/sessions/([^/]+)/exit\z}, :handle_exit, :owner, :session],
|
|
294
305
|
["POST", %r{\A/api/sessions/([^/]+)/recap\z}, :handle_recap, :owner, :session],
|
|
306
|
+
["POST", %r{\A/api/sessions/([^/]+)/reload_check\z}, :handle_reload_check, :owner, :session],
|
|
295
307
|
["POST", %r{\A/api/sessions/([^/]+)/guardrails/wrong\z}, :handle_system_one_wrong, :owner, :session],
|
|
296
308
|
["GET", %r{\A/api/sessions/([^/]+)/stats\z}, :handle_stats, :owner, :session],
|
|
297
309
|
["GET", %r{\A/api/sessions/([^/]+)/children/counts\z}, :handle_children_counts, :owner, :session],
|
|
@@ -996,6 +1008,39 @@ module Samagotchi
|
|
|
996
1008
|
json_response(200, ProjectList.build(@hub.snapshot, start_dir: start_dir, current_root: current_root))
|
|
997
1009
|
end
|
|
998
1010
|
|
|
1011
|
+
def handle_project_create(req)
|
|
1012
|
+
body = parse_json(request_body(req))
|
|
1013
|
+
return error_response(400, "invalid_json", "invalid JSON body") unless body.is_a?(Hash)
|
|
1014
|
+
|
|
1015
|
+
requested = body["dir"]
|
|
1016
|
+
requested = requested.strip if requested.is_a?(String)
|
|
1017
|
+
unless requested.is_a?(String) && !requested.empty? && !requested.include?("\0")
|
|
1018
|
+
return error_response(400, "invalid_dir", "dir must be an absolute folder")
|
|
1019
|
+
end
|
|
1020
|
+
|
|
1021
|
+
home = Dir.home
|
|
1022
|
+
dir = if requested == "~"
|
|
1023
|
+
home
|
|
1024
|
+
elsif requested.start_with?("~/")
|
|
1025
|
+
File.join(home, requested.delete_prefix("~/"))
|
|
1026
|
+
else
|
|
1027
|
+
requested
|
|
1028
|
+
end
|
|
1029
|
+
return error_response(400, "invalid_dir", "dir must be an absolute folder") unless dir.start_with?("/")
|
|
1030
|
+
|
|
1031
|
+
# One normal form for the decision, the mkdir and the realpath (a `..` after a symlink would differ).
|
|
1032
|
+
dir = File.expand_path(dir)
|
|
1033
|
+
outcome = ProjectInit.call(dir, home: home)
|
|
1034
|
+
return error_response(422, outcome.reason.to_s, outcome.message) if outcome.kind == :refused
|
|
1035
|
+
|
|
1036
|
+
if outcome.kind == :git
|
|
1037
|
+
failed = ProjectInit.ensure_folder(dir)
|
|
1038
|
+
return error_response(422, failed.reason.to_s, failed.message) if failed
|
|
1039
|
+
end
|
|
1040
|
+
dir = File.realpath(dir)
|
|
1041
|
+
json_response(200, outcome: outcome.kind.to_s, root: outcome.root, dir: dir)
|
|
1042
|
+
end
|
|
1043
|
+
|
|
999
1044
|
# A scope folder from the page: [dir, nil] (dir nil when none was
|
|
1000
1045
|
# given), or [nil, a 400 response] when it isn't an existing absolute
|
|
1001
1046
|
# folder (a bookmarked worktree deleted since, a typo).
|
|
@@ -1157,30 +1202,29 @@ module Samagotchi
|
|
|
1157
1202
|
|
|
1158
1203
|
settings = ModelSettings.all
|
|
1159
1204
|
models.each do |model|
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
summary = SamplingSettings.summary(target, names: names, models: settings)
|
|
1205
|
+
effective = EffectiveModel.of(registry, ModelChoice.new(name: model[:name], typed: model[:name]), models: settings)
|
|
1206
|
+
summary = SamplingSettings.summary(effective.lookup)
|
|
1163
1207
|
model[:sampling] = summary if summary
|
|
1164
|
-
add_llm_context(model,
|
|
1165
|
-
add_thinking(model,
|
|
1208
|
+
add_llm_context(model, effective)
|
|
1209
|
+
add_thinking(model, effective)
|
|
1166
1210
|
end
|
|
1167
1211
|
end
|
|
1168
1212
|
end
|
|
1169
1213
|
|
|
1170
1214
|
# The LLM context +model+ starts under (the start page's ⚙ panel);
|
|
1171
|
-
# one that can't be read leaves this model without
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1215
|
+
# one that can't be read leaves this model without (the key absent),
|
|
1216
|
+
# not the others.
|
|
1217
|
+
def add_llm_context(model, effective)
|
|
1218
|
+
explained = effective.llm_context_explained({})
|
|
1219
|
+
model[:llm_context] = explained.summary(nil) if explained
|
|
1176
1220
|
end
|
|
1177
1221
|
|
|
1178
1222
|
# The thinking level +model+ starts at, with where it came from (the
|
|
1179
|
-
# start page's ⚙ panel; SettingExplained#summary, no session)
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1223
|
+
# start page's ⚙ panel; SettingExplained#summary, no session); one
|
|
1224
|
+
# that can't be read leaves this model without (the key absent).
|
|
1225
|
+
def add_thinking(model, effective)
|
|
1226
|
+
explained = effective.thinking_explained(own: nil)
|
|
1227
|
+
model[:thinking] = explained.summary if explained
|
|
1184
1228
|
end
|
|
1185
1229
|
|
|
1186
1230
|
# The served model a session's last generation reported (analytics.json,
|
|
@@ -1210,17 +1254,16 @@ module Samagotchi
|
|
|
1210
1254
|
end
|
|
1211
1255
|
end
|
|
1212
1256
|
|
|
1213
|
-
#
|
|
1214
|
-
#
|
|
1257
|
+
# The shared registry (HostRegistry.current: built again when
|
|
1258
|
+
# config.yml's hosts: changed), so the model picker lists a host added
|
|
1215
1259
|
# while chi web runs. An injected registry (specs) stays.
|
|
1216
1260
|
def host_registry
|
|
1217
1261
|
@models_mutex.synchronize do
|
|
1218
1262
|
next @registry if @registry_given
|
|
1219
1263
|
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
@registry =
|
|
1223
|
-
@registry_hosts = hosts
|
|
1264
|
+
current = HostRegistry.current
|
|
1265
|
+
unless current.equal?(@registry)
|
|
1266
|
+
@registry = current
|
|
1224
1267
|
@models_thread = nil # a listing of the old hosts
|
|
1225
1268
|
end
|
|
1226
1269
|
@registry
|
|
@@ -1905,6 +1948,8 @@ module Samagotchi
|
|
|
1905
1948
|
attrs["home"] = Dir.home
|
|
1906
1949
|
# Only when on: the page reads its presence.
|
|
1907
1950
|
attrs["working-set"] = "1" if working_set?(req.params["working_set"])
|
|
1951
|
+
# The opt-in reload check (reload_check.js): only when on.
|
|
1952
|
+
attrs["reload-check"] = "1" if @reload_check
|
|
1908
1953
|
attrs.merge!(annotate_presets_data)
|
|
1909
1954
|
dir, = scope_dir(req.params["dir"])
|
|
1910
1955
|
unless dir
|
|
@@ -9,12 +9,14 @@ require_relative "../message_parts"
|
|
|
9
9
|
require_relative "../message_time"
|
|
10
10
|
require_relative "../objectives"
|
|
11
11
|
require_relative "../output_formatter"
|
|
12
|
+
require_relative "../recap_place"
|
|
12
13
|
require_relative "../recap_store"
|
|
13
14
|
require_relative "../ref"
|
|
14
15
|
require_relative "../revise_anchor"
|
|
15
16
|
require_relative "../steer"
|
|
16
17
|
require_relative "../turn_note"
|
|
17
18
|
require_relative "../bridge/card_store"
|
|
19
|
+
require_relative "../bridge/turn_drawings"
|
|
18
20
|
|
|
19
21
|
module Samagotchi
|
|
20
22
|
module Web
|
|
@@ -47,20 +49,26 @@ module Samagotchi
|
|
|
47
49
|
# a live worker's (it counts against its Engine's messages), else
|
|
48
50
|
# recap.json against the session file. A stopped session gets its
|
|
49
51
|
# recap without a worker being woken.
|
|
50
|
-
#
|
|
51
|
-
#
|
|
52
|
+
# The last turn it covers names its place (opening_id, RecapPlace);
|
|
53
|
+
# an older worker's saved_recap has none: the page counts turns_since.
|
|
54
|
+
# @return [Hash, nil] {text:, turns_since:, opening_id:, created_at:}
|
|
55
|
+
# (created_at: when it was written, UTC ISO; absent from an older
|
|
56
|
+
# recap)
|
|
52
57
|
def saved_recap_for(id, session, turn_snapshot)
|
|
53
58
|
if turn_snapshot
|
|
54
59
|
saved = turn_snapshot["saved_recap"]
|
|
55
|
-
return saved && { text: saved["text"], turns_since: saved["turns_since"].to_i,
|
|
60
|
+
return saved && { text: saved["text"], turns_since: saved["turns_since"].to_i, opening_id: saved["opening_id"],
|
|
61
|
+
created_at: saved["created_at"] }.compact
|
|
56
62
|
end
|
|
57
63
|
|
|
58
64
|
saved = RecapStore.read(@session_class.session_dir(id, state_dir: default_state_dir))
|
|
59
65
|
return nil unless saved
|
|
60
66
|
|
|
61
67
|
messages = Array(session.messages)
|
|
62
|
-
|
|
63
|
-
|
|
68
|
+
covered = saved[:covered].to_i
|
|
69
|
+
since = messages.drop(covered).count { |m| Samagotchi::Steer.turn_opening?(m) }
|
|
70
|
+
{ text: saved[:text], turns_since: since, opening_id: RecapPlace.opening_id(messages, covered),
|
|
71
|
+
created_at: saved[:created_at] }.compact
|
|
64
72
|
rescue StandardError
|
|
65
73
|
nil
|
|
66
74
|
end
|
|
@@ -91,14 +99,22 @@ module Samagotchi
|
|
|
91
99
|
# @param markdown [Boolean] false: no html for a delegate report or an
|
|
92
100
|
# answer (the participant review drops both and renders its replies
|
|
93
101
|
# from their redacted text, ParticipantReview#with_reply_html)
|
|
94
|
-
|
|
102
|
+
# @param drawings [Array<Bridge::TurnDrawings::Line>] the valid lines
|
|
103
|
+
# (TurnDrawings.valid against +msgs+): each item of a kind a drawing
|
|
104
|
+
# draws (#drawn_kind?) from a line's range gets `drawn: <turn_id>`
|
|
105
|
+
# @param strip_drawn [Boolean] a stamped item's parts go (the drawing
|
|
106
|
+
# has them); it keeps its ✂ marks as `marks: {tool_id => edit}`
|
|
107
|
+
def messages_for_display(msgs, parts: false, cwd: nil, markdown: true, drawings: [], strip_drawn: false)
|
|
95
108
|
filtered = []
|
|
96
109
|
list = Array(msgs)
|
|
110
|
+
# Each item's message index, for the drawings' stamps.
|
|
111
|
+
origins = []
|
|
97
112
|
# The outputs' ✂ marks (LLM context edits), by id, for the parts.
|
|
98
113
|
marks = parts ? MessageParts.edit_marks(list) : {}
|
|
99
114
|
# The list before each objectives note, for what its entry says changed.
|
|
100
115
|
objectives = nil
|
|
101
116
|
list.each_with_index do |m, index|
|
|
117
|
+
origins.fill(index - 1, origins.size...filtered.size)
|
|
102
118
|
role = (m[:role] || m["role"]).to_s
|
|
103
119
|
content = (m[:content] || m["content"]).to_s
|
|
104
120
|
if Samagotchi::ContextNote.note?(m)
|
|
@@ -205,11 +221,45 @@ module Samagotchi
|
|
|
205
221
|
message[:parts] = did if did
|
|
206
222
|
filtered << message
|
|
207
223
|
end
|
|
208
|
-
filtered
|
|
224
|
+
origins.fill(list.size - 1, origins.size...filtered.size)
|
|
225
|
+
stamp_drawn(filtered, origins, Array(drawings), strip: strip_drawn)
|
|
209
226
|
rescue StandardError
|
|
210
227
|
[]
|
|
211
228
|
end
|
|
212
229
|
|
|
230
|
+
# +items+ with a drawn turn's stamp on each item of a kind its drawing
|
|
231
|
+
# draws (the opening, steps, steers, merged lines, the empty answer):
|
|
232
|
+
# a context note, a `!cmd`'s record and anything else keep today's
|
|
233
|
+
# place. +origins+: each item's message index.
|
|
234
|
+
def stamp_drawn(items, origins, drawings, strip:)
|
|
235
|
+
return items if drawings.empty?
|
|
236
|
+
|
|
237
|
+
items.each_with_index.map do |item, i|
|
|
238
|
+
line = drawings.find { |l| l.range.cover?(origins[i]) }
|
|
239
|
+
next item unless line && drawn_kind?(item)
|
|
240
|
+
|
|
241
|
+
stamped = item.merge(drawn: line.turn_id)
|
|
242
|
+
strip ? without_parts(stamped) : stamped
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def drawn_kind?(item)
|
|
247
|
+
case item[:role]
|
|
248
|
+
when "note" then item[:turn_start] == true
|
|
249
|
+
when "user" then !(item[:turn_id].nil? && item[:content].match?(/\A!\(.*\)\n/))
|
|
250
|
+
else %w[assistant steer empty_answer].include?(item[:role])
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# A drawn item without its parts: its ✂ marks by output id stay (an
|
|
255
|
+
# overlay on the drawing's rows: later state, as a card's latest body).
|
|
256
|
+
def without_parts(item)
|
|
257
|
+
tools = Array(item.dig(:parts, :tools))
|
|
258
|
+
marks = tools.filter_map { |tool| [tool[:tool_id], tool[:edit]] if tool[:tool_id] && tool[:edit] }.to_h
|
|
259
|
+
out = item.except(:parts)
|
|
260
|
+
marks.empty? ? out : out.merge(marks: marks)
|
|
261
|
+
end
|
|
262
|
+
|
|
213
263
|
# A turn opening's Revise stamp as {revises: {turn_id:}}, checked again
|
|
214
264
|
# (a stored file is never trusted), or nothing.
|
|
215
265
|
def revises_of(message)
|
|
@@ -395,7 +395,7 @@ module Samagotchi
|
|
|
395
395
|
# send --wait, D11): only for a turn of theirs (+turn+, the enqueued
|
|
396
396
|
# id their POST …/turn answered); the body's busy, queue and created
|
|
397
397
|
# don't count. The answer is what their wait on that turn needs
|
|
398
|
-
# (
|
|
398
|
+
# (ReplyWait::Target.wait_only with the turn asked, the wait's
|
|
399
399
|
# baseline), never the owner's wait state: the last turn's end (the
|
|
400
400
|
# baseline's last_turn), whether their turn is known, whether a worker
|
|
401
401
|
# runs it, and last_turn_id only when it is their turn. No cursor
|
|
@@ -6,8 +6,8 @@ require_relative "push/attention"
|
|
|
6
6
|
|
|
7
7
|
module Samagotchi
|
|
8
8
|
module Web
|
|
9
|
-
# Projects represented by the session hub, plus the start
|
|
10
|
-
# when
|
|
9
|
+
# Projects represented by the session hub, plus the start and current
|
|
10
|
+
# projects even when they have no sessions.
|
|
11
11
|
module ProjectList
|
|
12
12
|
module_function
|
|
13
13
|
|
|
@@ -24,6 +24,7 @@ module Samagotchi
|
|
|
24
24
|
(groups[root] ||= []) << summary
|
|
25
25
|
end
|
|
26
26
|
groups[default_root] ||= [] if valid_root?(default_root)
|
|
27
|
+
groups[current_root] ||= [] if valid_root?(current_root)
|
|
27
28
|
|
|
28
29
|
rows = groups.map do |root, sessions|
|
|
29
30
|
default = root == default_root
|
|
@@ -50,8 +50,11 @@ export function newActivity() {
|
|
|
50
50
|
// refs: a plugin tool's refs (the server's RowRefs: [{kind, target,
|
|
51
51
|
// label?, actions}]): the first on the row's title, the others in its
|
|
52
52
|
// menu (refs.js bindRowRefs).
|
|
53
|
+
// tool_id: the id the call's output is saved under (the server's ToolIds,
|
|
54
|
+
// "t41"; not a row field there), so a row names the output a reload's
|
|
55
|
+
// part and a strategy's edit name.
|
|
53
56
|
export const ACTIVITY_FIELDS = Object.freeze(["title", "called_as", "via"]);
|
|
54
|
-
export const EVENT_FIELDS = Object.freeze(["view", "ref", "refs"]);
|
|
57
|
+
export const EVENT_FIELDS = Object.freeze(["view", "ref", "refs", "tool_id"]);
|
|
55
58
|
const ROW_FIELDS = Object.freeze([...ACTIVITY_FIELDS, ...EVENT_FIELDS]);
|
|
56
59
|
|
|
57
60
|
// The +fields+ of +source+ that are set, as an object (for a spread).
|
|
@@ -115,10 +118,10 @@ function markStart(row, event, now) {
|
|
|
115
118
|
|
|
116
119
|
// Update (or synthesize) a row from a :tool_call_completed event. Returns the row.
|
|
117
120
|
// If no matching :tool_call_started was seen (e.g. a replay gap), a row is
|
|
118
|
-
// created on the fly so the completion is never lost. Its duration
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
// the call waited for an approval (waited_ms)
|
|
121
|
+
// created on the fly so the completion is never lost. Its duration: the
|
|
122
|
+
// event's duration_ms (the worker's, the figure a reload's tool record and
|
|
123
|
+
// the terminal show), else (an older worker) from the start the page saw to
|
|
124
|
+
// +now+, less the time the call waited for an approval (waited_ms).
|
|
122
125
|
export function addCompleted(model, event = {}, now = null) {
|
|
123
126
|
const iteration = event.iteration ?? 0;
|
|
124
127
|
const callIndex = event.call_index ?? 0;
|
|
@@ -137,8 +140,10 @@ export function addCompleted(model, event = {}, now = null) {
|
|
|
137
140
|
row = synthesized.row;
|
|
138
141
|
}
|
|
139
142
|
row.status = completedStatus(event.activity?.status);
|
|
140
|
-
|
|
141
|
-
|
|
143
|
+
// A long output's web cut (the worker's preview: the text a reload's row
|
|
144
|
+
// has from the saved output), else the output itself.
|
|
145
|
+
row.output = event.preview ?? (event.output || "");
|
|
146
|
+
row.output_truncated = event.preview != null || !!event.output_truncated;
|
|
142
147
|
if (event.activity?.params) row.params = event.activity.params;
|
|
143
148
|
// The completed event carries the title (ACTIVITY_FIELDS) in its activity.
|
|
144
149
|
Object.assign(row, pickFields(event.activity, ACTIVITY_FIELDS));
|
|
@@ -160,6 +165,10 @@ export function addCompleted(model, event = {}, now = null) {
|
|
|
160
165
|
// What an edit/write changed in its file (a reload draws it from the
|
|
161
166
|
// saved parts, turn_html.js toolRowTemplate).
|
|
162
167
|
if (event.diff) row.diff = event.diff;
|
|
168
|
+
// An LLM context edit's ✂ mark on its output, later state a drawn turn's
|
|
169
|
+
// row takes from its saved item (turn_block.js drawnTurnNodes); no live
|
|
170
|
+
// event carries one.
|
|
171
|
+
if (event.edit) row.edit = event.edit;
|
|
163
172
|
const duration = callDuration(row, event, now);
|
|
164
173
|
if (duration !== null) row.duration_ms = duration;
|
|
165
174
|
return row;
|
|
@@ -53,6 +53,7 @@ import { elementOf, emptyAnswerTemplate } from "./turn_html.js";
|
|
|
53
53
|
import { createBubbles, hintEl, outputBubbleTemplate } from "./bubbles.js";
|
|
54
54
|
import { createCardsPlace } from "./cards_place.js";
|
|
55
55
|
import { createStageView } from "./stage_view.js";
|
|
56
|
+
import { createReloadCheck } from "./reload_check.js";
|
|
56
57
|
import { createWorkingSet, groupItems } from "./working_set_view.js";
|
|
57
58
|
import { createObjectives } from "./objectives_wiring.js";
|
|
58
59
|
import { turnGroups } from "./timing.js";
|
|
@@ -356,6 +357,17 @@ const turnDom = createStageView({
|
|
|
356
357
|
workingSet,
|
|
357
358
|
// Stop turn in its status row (disabled until the request settles).
|
|
358
359
|
onCancel: () => handleCancel(),
|
|
360
|
+
onHandedOff: (handed) => reloadCheck.handedOff(handed),
|
|
361
|
+
});
|
|
362
|
+
// The opt-in reload check (web.reload_check): a handed-off turn against a
|
|
363
|
+
// reload of it, the counts to chi web (reload_check.js).
|
|
364
|
+
const reloadCheck = createReloadCheck({
|
|
365
|
+
enabled: () => document.body.dataset.reloadCheck === "1",
|
|
366
|
+
sessionId: () => store.selected,
|
|
367
|
+
running: () => liveTurn.running,
|
|
368
|
+
read: (id) => getSession(id, { parts: true }),
|
|
369
|
+
render: (data) => turnHistoryNodes(data.messages || [], normalizeTiming(data.timing), historyOpts(data)),
|
|
370
|
+
post: (id, body) => api(`/api/sessions/${id}/reload_check`, { method: "POST", body: JSON.stringify(body) }),
|
|
359
371
|
});
|
|
360
372
|
// The history's bottom reserve and the toast's place follow the composer
|
|
361
373
|
// card and the stage floating over the history (dock_reserve.js).
|
|
@@ -1001,7 +1013,7 @@ function renderSessionView(data, { kept = [] } = {}) {
|
|
|
1001
1013
|
});
|
|
1002
1014
|
liveTurn.timing.ended = store.current.timing.turnRecords.length;
|
|
1003
1015
|
const activeTurnId = store.current.timing.activeTurn?.id || null;
|
|
1004
|
-
if (data.messages && data.messages.length) renderHistory(data.messages);
|
|
1016
|
+
if (data.messages && data.messages.length) renderHistory(data.messages, data);
|
|
1005
1017
|
else renderHistory(data.history || []);
|
|
1006
1018
|
seedWorkingSet(data);
|
|
1007
1019
|
objectives.seed(data.objectives);
|
|
@@ -1029,9 +1041,12 @@ function renderSessionView(data, { kept = [] } = {}) {
|
|
|
1029
1041
|
// The recap saved with the session (also a stopped one's), else a live
|
|
1030
1042
|
// worker's from before saved_recap existed. After the cards: the last
|
|
1031
1043
|
// turn's check-in notice goes to the end, and the recap closes the page.
|
|
1032
|
-
|
|
1044
|
+
const saved = data.saved_recap;
|
|
1045
|
+
if (saved) showRecap(saved.text, saved.turns_since, saved.created_at || null, saved.opening_id || null);
|
|
1033
1046
|
else showRecap(data.recap);
|
|
1034
|
-
|
|
1047
|
+
// chi web's unfold of the join (TurnAccumulator.join_events); the page's
|
|
1048
|
+
// own only for a mixed cache (an older chi web's payload has none).
|
|
1049
|
+
const replay = data.replay ?? snapshotEvents({
|
|
1035
1050
|
current_turn: data.current_turn,
|
|
1036
1051
|
queued: data.queued,
|
|
1037
1052
|
queued_commands: data.queued_commands,
|
|
@@ -1048,7 +1063,8 @@ function renderSessionView(data, { kept = [] } = {}) {
|
|
|
1048
1063
|
turnDom.runningWithoutTurn?.(liveTurn.running);
|
|
1049
1064
|
if (liveTurn.running && store.current.timing.activeTurn?.started_at) liveTurn.timing.start(store.current.timing.activeTurn.started_at, activeTurnId);
|
|
1050
1065
|
} else if (liveTurn.running) {
|
|
1051
|
-
// The
|
|
1066
|
+
// The live line's turn: the running turn's record's (the replayed
|
|
1067
|
+
// turn_started carries the same id, on both unfolds).
|
|
1052
1068
|
liveTurn.timing.turnId = activeTurnId;
|
|
1053
1069
|
}
|
|
1054
1070
|
turnDom.flush();
|
|
@@ -1079,7 +1095,7 @@ function renderSessionView(data, { kept = [] } = {}) {
|
|
|
1079
1095
|
function seedWorkingSet(data) {
|
|
1080
1096
|
if (!workingSet) return;
|
|
1081
1097
|
const items = data.messages || [];
|
|
1082
|
-
const groups = typeof items[0] === "object" && items[0]?.role ? turnGroups(items, store.current.timing).filter((g) => g.kind === "turn") : [];
|
|
1098
|
+
const groups = typeof items[0] === "object" && items[0]?.role ? turnGroups(items, store.current.timing, { drawings: data.turn_drawings }).filter((g) => g.kind === "turn") : [];
|
|
1083
1099
|
if (data.current_turn && groups.length) groups.pop();
|
|
1084
1100
|
workingSet.seed(groups.map((g, i) => {
|
|
1085
1101
|
const text = String(items[g.user]?.content ?? "").replace(/\s+/g, " ").trim();
|
|
@@ -1087,17 +1103,22 @@ function seedWorkingSet(data) {
|
|
|
1087
1103
|
}));
|
|
1088
1104
|
}
|
|
1089
1105
|
|
|
1090
|
-
|
|
1106
|
+
// turnHistoryNodes' options for a read's +data+: its turns' drawings
|
|
1107
|
+
// (turn_drawing.js) and cards (a drawn card's body).
|
|
1108
|
+
const historyOpts = (data = {}) => ({ thumbs: (images) => thumbsTemplate(store.selected, images), workingSet: !!workingSet,
|
|
1109
|
+
drawings: data.turn_drawings || null, cards: data.cards || null, capabilities: pageCapabilities });
|
|
1110
|
+
|
|
1111
|
+
function renderHistory(items, data = {}) {
|
|
1091
1112
|
gapClock.seed(null);
|
|
1092
1113
|
if (!items.length) {
|
|
1093
1114
|
historyEl.replaceChildren(hintEl("No output yet. Send a message."));
|
|
1094
1115
|
return;
|
|
1095
1116
|
}
|
|
1096
1117
|
if (typeof items[0] === "object" && items[0].role) {
|
|
1097
|
-
historyEl.replaceChildren(turnHistoryNodes(items, store.current.timing,
|
|
1118
|
+
historyEl.replaceChildren(turnHistoryNodes(items, store.current.timing, historyOpts(data)));
|
|
1098
1119
|
ownerVerify.renderSaved();
|
|
1099
1120
|
// The live dividers count from the history's last activity.
|
|
1100
|
-
gapClock.seed(lastActivity(turnGroups(items, store.current.timing), items));
|
|
1121
|
+
gapClock.seed(lastActivity(turnGroups(items, store.current.timing, { drawings: data.turn_drawings }), items));
|
|
1101
1122
|
linkTextRefs();
|
|
1102
1123
|
} else {
|
|
1103
1124
|
historyEl.replaceChildren(...items.map((c) => elementOf(outputBubbleTemplate(c))));
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
// notices, plugins' steers, command and note bubbles. The markup is lit
|
|
5
5
|
// (turn_html.js elementOf); createBubbles puts them in the history.
|
|
6
6
|
import { html, nothing } from "./vendor/lit-html.js";
|
|
7
|
-
import { fullTime, normalize, recapLabel
|
|
7
|
+
import { fullTime, normalize, recapLabel } from "./format.js";
|
|
8
|
+
import { recapStampPlace } from "./recap_stamp.js";
|
|
8
9
|
import { BADGES, elementOf, noteBubbleTemplate, stateBadgeTemplate, steerRowTemplate, userBubbleTemplate } from "./turn_html.js";
|
|
9
10
|
import { thumbsTemplate } from "./images.js";
|
|
10
11
|
import { appendAboveLiveTiming, STOPPED_LINE_CLASS } from "./timing.js";
|
|
@@ -254,14 +255,16 @@ export function createBubbles({
|
|
|
254
255
|
// the end of the history. A turn starting leaves it in place, marked
|
|
255
256
|
// earlier; only a new recap replaces it.
|
|
256
257
|
|
|
257
|
-
|
|
258
|
+
// +openingId+: a saved recap's stamp, the last turn it covers (its place;
|
|
259
|
+
// without one, +turnsSince+ counts it).
|
|
260
|
+
function showRecap(text, turnsSince = 0, createdAt = null, openingId = null) {
|
|
258
261
|
if (!text) return;
|
|
259
262
|
removeRecap();
|
|
260
263
|
const details = elementOf(recapTemplate(text, turnsSince, createdAt));
|
|
261
264
|
// A stale one sits where it was written, above the turns it doesn't cover.
|
|
262
265
|
// Never in the stage: a recap is between turns.
|
|
263
266
|
turnDom.settleFold();
|
|
264
|
-
const prompt =
|
|
267
|
+
const prompt = recapStampPlace(turnPromptBubbles(historyEl), openingId, turnsSince);
|
|
265
268
|
// Above that turn's gap divider too: the recap was written before the quiet stretch ended. The
|
|
266
269
|
// snapshot's cards placed at that prompt (placed first) sit between the divider and the prompt.
|
|
267
270
|
// A delegate report's fold row (answer_fold.js) heads its prompt.
|
|
@@ -314,6 +317,8 @@ export function createBubbles({
|
|
|
314
317
|
// Page-only: kept across a resync of this session (keptCommandBubbles).
|
|
315
318
|
const id = selected();
|
|
316
319
|
if (id) div.dataset.keptFor = id;
|
|
320
|
+
// A reminder's: the turn it went into (restoreCommandBubbles).
|
|
321
|
+
if (view.turnId) div.dataset.turnId = view.turnId;
|
|
317
322
|
drawCommandBubble(div, view);
|
|
318
323
|
// When it happened (a reminder's at): the hover, as a reload's bubbles.
|
|
319
324
|
const when = fullTime(view.at);
|
|
@@ -92,6 +92,14 @@ export function createCardsPlace({
|
|
|
92
92
|
function placeSnapshotCards(entries, { before: end = null } = {}) {
|
|
93
93
|
const users = turnPromptBubbles(historyEl);
|
|
94
94
|
for (const entry of entries) {
|
|
95
|
+
// A drawn turn's own card or row is in its drawing, where live drew it
|
|
96
|
+
// (turn_block.js drawnTurnNodes): only its body refreshes, by id. Its
|
|
97
|
+
// question is still placed from here.
|
|
98
|
+
if (drawnEntry(entry)) {
|
|
99
|
+
const el = isCard(entry) ? cardElement(entry.id) : null;
|
|
100
|
+
if (el) refreshCardBody(el, entry);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
95
103
|
const before = snapshotPlace(users, entry) || (entry.after_failed ? null : end);
|
|
96
104
|
if (isCard(entry)) upsertCard(entry, { before });
|
|
97
105
|
else if (entry.type === "question") placeResolvedQuestion(entry, before);
|
|
@@ -101,6 +109,13 @@ export function createCardsPlace({
|
|
|
101
109
|
}
|
|
102
110
|
}
|
|
103
111
|
|
|
112
|
+
// An in-turn entry (not a question) of a turn the history drew from its
|
|
113
|
+
// drawing (its pieces carry data-drawn; the entry's of_turn, CardStore's).
|
|
114
|
+
function drawnEntry(entry) {
|
|
115
|
+
if (!entry?.in_turn || entry.type === "question" || typeof entry.of_turn !== "string") return false;
|
|
116
|
+
return [...historyEl.querySelectorAll("[data-drawn]")].some((el) => el.dataset.drawn === entry.of_turn);
|
|
117
|
+
}
|
|
118
|
+
|
|
104
119
|
// A finished turn's row, a hook's notice or the loop's asking again
|
|
105
120
|
// (+before+: the next turn's prompt, or null for the last turn): a row of
|
|
106
121
|
// its step, above the row of the call it came before, as live; else (a
|
|
@@ -268,11 +268,15 @@ export function keptCommandBubbles(historyEl, id) {
|
|
|
268
268
|
.filter(({ el }) => keepsCommandBubble(el.querySelector(":scope > .command-line")?.textContent));
|
|
269
269
|
}
|
|
270
270
|
|
|
271
|
-
// The kept bubbles back into the redrawn history, each after its turn
|
|
271
|
+
// The kept bubbles back into the redrawn history, each after its turn; a
|
|
272
|
+
// reminder's whose turn the history now draws from its drawing
|
|
273
|
+
// (data-drawn: the drawing has the reminder) stays out.
|
|
272
274
|
export function restoreCommandBubbles(historyEl, kept) {
|
|
273
275
|
if (!kept.length) return;
|
|
274
276
|
historyEl.querySelector(".hint")?.remove();
|
|
277
|
+
const drawn = new Set([...historyEl.querySelectorAll("[data-drawn]")].map((el) => el.dataset.drawn));
|
|
275
278
|
kept.forEach(({ el, anchor }) => {
|
|
279
|
+
if (el.dataset.turnId && drawn.has(el.dataset.turnId)) return;
|
|
276
280
|
const { els, kinds } = historyAnchors(historyEl);
|
|
277
281
|
const at = anchorInsertIndex(kinds, anchor);
|
|
278
282
|
if (at < els.length) els[at].before(el);
|
|
@@ -130,9 +130,11 @@ export function uploadImage(id, file, name, opts = {}) {
|
|
|
130
130
|
// (mergeTiming), and opts.turnId asks for that turn's answer.
|
|
131
131
|
// opts.timing: the whole timing alone (when the merge came up short).
|
|
132
132
|
// opts.parts: each saved message with what it did (thinking, tool calls
|
|
133
|
-
// and their output), for the turn block's reload
|
|
133
|
+
// and their output), for the turn block's reload; with it, drawings=1: a
|
|
134
|
+
// finished turn with a drawing comes as its turn_drawings, its items
|
|
135
|
+
// stamped (drawn) without their parts (turn_drawing.js).
|
|
134
136
|
export function getSession(id, { tail, recent, turnId, timing, parts, cards, ...opts } = {}) {
|
|
135
|
-
let query = cards ? "?cards=1" : tail ? "?tail=1" : timing ? "?timing=1" : parts ? "?parts=1" : "";
|
|
137
|
+
let query = cards ? "?cards=1" : tail ? "?tail=1" : timing ? "?timing=1" : parts ? "?parts=1&drawings=1" : "";
|
|
136
138
|
if (tail && recent) query += "&recent=1";
|
|
137
139
|
if (tail && turnId) query += `&turn_id=${encodeURIComponent(turnId)}`;
|
|
138
140
|
return api(`/api/sessions/${id}${query}`, opts);
|