samagotchi 0.170.0 → 0.171.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 +29 -1
- data/docs/architecture.md +2 -2
- data/docs/cli.md +37 -23
- data/docs/configuration.md +1 -1
- data/docs/internals/block-renderers.md +2 -2
- data/docs/internals/bridge.md +5 -4
- data/docs/internals/context-telemetry.md +13 -1
- data/docs/internals/engine.md +4 -4
- data/docs/internals/inline-widgets.md +1 -1
- data/docs/internals/objectives.md +1 -1
- data/docs/internals/participants.md +235 -119
- data/docs/internals/web-frontend.md +164 -33
- data/docs/internals/working-set.md +69 -5
- data/docs/releasing.md +2 -1
- data/docs/sessions.md +6 -3
- data/lib/samagotchi/bridge/turn_accumulator.rb +8 -4
- data/lib/samagotchi/bridge.rb +39 -12
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/delivery.rb +13 -0
- data/lib/samagotchi/engine.rb +5 -1
- data/lib/samagotchi/event_fanout.rb +22 -1
- data/lib/samagotchi/grants.rb +5 -0
- data/lib/samagotchi/guardrails/review_paths.rb +2 -2
- data/lib/samagotchi/image_store.rb +51 -12
- data/lib/samagotchi/kernel_loop.rb +7 -1
- data/lib/samagotchi/llm/stray_tool_call.rb +7 -4
- data/lib/samagotchi/llm_context_stale.rb +4 -1
- data/lib/samagotchi/participant_messages.rb +39 -0
- data/lib/samagotchi/participant_revise.rb +61 -0
- data/lib/samagotchi/participant_uploads.rb +98 -0
- data/lib/samagotchi/reply_wait/poll.rb +5 -1
- data/lib/samagotchi/revise_anchor.rb +9 -0
- data/lib/samagotchi/send_command.rb +5 -0
- data/lib/samagotchi/session_inbox.rb +20 -12
- data/lib/samagotchi/session_manager/delivery.rb +7 -0
- data/lib/samagotchi/session_stores.rb +2 -0
- data/lib/samagotchi/tool_runner.rb +39 -5
- data/lib/samagotchi/tools/memory.rb +26 -7
- data/lib/samagotchi/turn_anchors.rb +0 -3
- data/lib/samagotchi/turn_stages/verify_anchors.rb +13 -1
- data/lib/samagotchi/verify_anchor.rb +6 -0
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +42 -77
- data/lib/samagotchi/web/participant_access.rb +129 -26
- data/lib/samagotchi/web/participant_images.rb +185 -0
- data/lib/samagotchi/web/participant_review.rb +19 -138
- data/lib/samagotchi/web/participant_show.rb +240 -0
- data/lib/samagotchi/web/participant_stream_filter.rb +25 -2
- data/lib/samagotchi/web/participant_view.rb +331 -24
- data/lib/samagotchi/web/public/annotate_bar.css +1 -2
- data/lib/samagotchi/web/public/annotate_bar.js +1 -2
- data/lib/samagotchi/web/public/app.js +124 -88
- data/lib/samagotchi/web/public/block_host.js +7 -4
- data/lib/samagotchi/web/public/bubbles.js +9 -1
- data/lib/samagotchi/web/public/cards_offscreen.js +69 -0
- data/lib/samagotchi/web/public/chip_popover.js +5 -4
- data/lib/samagotchi/web/public/claim_link.js +4 -4
- data/lib/samagotchi/web/public/composer.css +4 -6
- data/lib/samagotchi/web/public/composer_mode.js +11 -6
- data/lib/samagotchi/web/public/composer_mount.js +4 -4
- data/lib/samagotchi/web/public/ctx.js +12 -3
- data/lib/samagotchi/web/public/delivery_line.js +34 -5
- data/lib/samagotchi/web/public/history_shape.js +5 -1
- data/lib/samagotchi/web/public/images.js +7 -1
- data/lib/samagotchi/web/public/index.html +40 -4
- data/lib/samagotchi/web/public/info_bar_model.js +2 -1
- data/lib/samagotchi/web/public/markdown_place.js +5 -4
- data/lib/samagotchi/web/public/owner_claim.js +3 -3
- data/lib/samagotchi/web/public/owner_verify.js +0 -2
- data/lib/samagotchi/web/public/participant_bar.js +108 -0
- data/lib/samagotchi/web/public/participant_main.js +289 -0
- data/lib/samagotchi/web/public/participant_model.js +5 -292
- data/lib/samagotchi/web/public/participant_names.js +6 -1
- data/lib/samagotchi/web/public/participant_revise.js +66 -0
- data/lib/samagotchi/web/public/participants_panel.js +3 -12
- data/lib/samagotchi/web/public/question_cards.js +14 -3
- data/lib/samagotchi/web/public/refs.js +53 -36
- data/lib/samagotchi/web/public/reload_check.js +68 -11
- data/lib/samagotchi/web/public/revise_stack.js +21 -10
- data/lib/samagotchi/web/public/send_turn.js +24 -6
- data/lib/samagotchi/web/public/session_fields.js +1 -1
- data/lib/samagotchi/web/public/session_state.js +12 -4
- data/lib/samagotchi/web/public/session_sync.js +9 -2
- data/lib/samagotchi/web/public/session_view.js +17 -7
- data/lib/samagotchi/web/public/stage_asking.js +6 -4
- data/lib/samagotchi/web/public/stage_owner_wait.js +35 -0
- data/lib/samagotchi/web/public/stage_view.js +18 -4
- data/lib/samagotchi/web/public/stage_waiting.js +74 -6
- data/lib/samagotchi/web/public/storage.js +22 -0
- data/lib/samagotchi/web/public/stream_handlers.js +2 -1
- data/lib/samagotchi/web/public/tokens.css +1 -1
- data/lib/samagotchi/web/public/turn_block.js +3 -2
- data/lib/samagotchi/web/public/turn_events.js +15 -6
- data/lib/samagotchi/web/public/turn_html.js +9 -2
- data/lib/samagotchi/web/public/turn_range.js +1 -2
- data/lib/samagotchi/web/public/verify_target.js +2 -4
- data/lib/samagotchi/web/public/viewer.js +59 -0
- data/lib/samagotchi/web/public/viewer_self.js +53 -0
- data/lib/samagotchi/web/public/working_set.js +11 -5
- data/lib/samagotchi/web/public/working_set_view.js +53 -8
- data/lib/samagotchi/web/public/ws_cards.js +89 -0
- data/lib/samagotchi/web/public/ws_peek.js +106 -0
- data/lib/samagotchi/web/public/ws_preview.css +126 -0
- data/lib/samagotchi/web/public/ws_reader.js +319 -0
- data/lib/samagotchi/web/public/ws_timeline.js +200 -0
- data/lib/samagotchi/web/public/ws_way_in.js +25 -0
- data/lib/samagotchi/web/session_control.rb +39 -14
- data/lib/samagotchi/web/session_create.rb +1 -1
- data/lib/samagotchi/web/session_show.rb +75 -28
- data/lib/samagotchi/web/viewer.rb +70 -0
- data/lib/samagotchi/web/working_set_routes.rb +132 -0
- data/lib/samagotchi/worker.rb +19 -14
- data/lib/samagotchi/working_set.rb +260 -8
- metadata +20 -6
- data/lib/samagotchi/web/public/participant.html +0 -218
- data/lib/samagotchi/web/public/participant.js +0 -719
- data/lib/samagotchi/web/public/participant_annotate.js +0 -76
- data/lib/samagotchi/web/public/review_history.js +0 -72
- data/lib/samagotchi/web/public/tools_overview.js +0 -42
|
@@ -30,8 +30,9 @@ module Samagotchi
|
|
|
30
30
|
# decide forgotten; edit, write and memory_write add ops and stamps only
|
|
31
31
|
# (their content is in the call's arguments, which no strategy cuts).
|
|
32
32
|
# Stale: the item's latest run left a stamp (ToolRun#stamp) and the file
|
|
33
|
-
# now differs from it in mtime or size, or is gone
|
|
34
|
-
#
|
|
33
|
+
# now differs from it in mtime or size, or is gone - for a memory, its
|
|
34
|
+
# entry or the model overlay read with it. A run with no stamp (an old
|
|
35
|
+
# session's) never makes an item stale.
|
|
35
36
|
module WorkingSet
|
|
36
37
|
FILE = "file"
|
|
37
38
|
MEMORY = "memory"
|
|
@@ -189,6 +190,219 @@ module Samagotchi
|
|
|
189
190
|
touches.select { |touch| touch.content && pinned.include?([touch.kind, touch.key]) }.to_set { |touch| touch.run.ref.id }
|
|
190
191
|
end
|
|
191
192
|
|
|
193
|
+
# The reader's lines of a read: +first+ and +last+ line of the file it
|
|
194
|
+
# holds (+last+ from the text, so an end_line past the file is clamped,
|
|
195
|
+
# nil for a partial read's), whether it asked for the +whole+ file, and
|
|
196
|
+
# whether it is +partial+ (LLMContextStale.file_run's rule: a head/tail
|
|
197
|
+
# preview of a big file, an output ToolRunner cut): such a read is shown
|
|
198
|
+
# as is, kept out of the file strip and of merging.
|
|
199
|
+
Lines = Data.define(:first, :last, :whole, :partial)
|
|
200
|
+
|
|
201
|
+
# A memory_read's file, from its stamp (ToolRunner#memory_stamp): the
|
|
202
|
+
# +scope+ it answered from (project or system), the entry's +path+, the
|
|
203
|
+
# model overlay read with it (+overlay_path+) and that overlay's text as
|
|
204
|
+
# appended to the output (+overlay+; only for a read of one name, the
|
|
205
|
+
# rest of the output its +text+). nil fields for a session saved before
|
|
206
|
+
# memory stamps.
|
|
207
|
+
MemoryPart = Data.define(:scope, :path, :overlay_path, :overlay)
|
|
208
|
+
|
|
209
|
+
# One run of an item, as its timeline shows it: the run's chi +tool_id+
|
|
210
|
+
# (ToolIds; +derived+ for a legacy entry's), the tool_response entry's
|
|
211
|
+
# +index+, its global +step+ (LLMContextStale::PairedRun#step), +op+
|
|
212
|
+
# (read, edit, write, memory_read, memory_write, fetch), whether the
|
|
213
|
+
# model is still +sent+ it (#still_sent?: edits and writes always),
|
|
214
|
+
# +dropped+ (the user's applied drop) or +forgotten+ (not sent, not by a
|
|
215
|
+
# drop), and what it holds: a read's +lines+ and +text+ (the output as
|
|
216
|
+
# stored, without its lead or a clamp note), an edit's or write's saved
|
|
217
|
+
# +diff+ (ToolResponse's tool_diffs, never re-diffed: nil for a no-op or
|
|
218
|
+
# a failed snapshot, maybe truncated, {skipped} or {error}), a memory
|
|
219
|
+
# read's whole +text+ with +shared_with+ (the other names it holds) and
|
|
220
|
+
# +memory+, a memory_write's written text, a fetch's text.
|
|
221
|
+
Event = Data.define(:tool_id, :derived, :index, :step, :op, :sent, :dropped, :forgotten, :lines, :diff, :text,
|
|
222
|
+
:memory, :shared_with) do
|
|
223
|
+
def to_h = super.merge(lines: lines&.to_h, memory: memory&.to_h)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# An item's timeline: its identity and its events in conversation order
|
|
227
|
+
# (the reader shows them newest first).
|
|
228
|
+
Timeline = Data.define(:kind, :key, :events) do
|
|
229
|
+
def to_h = { kind: kind, key: key, events: events.map(&:to_h) }
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# A stale memory's text now against what the model has: the +text+
|
|
233
|
+
# memory_read would answer now for the names the item's latest read
|
|
234
|
+
# read (nil when an entry is gone), and its +diff+ from what that read
|
|
235
|
+
# sent (TextDiff, EditPreview's shape; "" text for no change).
|
|
236
|
+
Now = Data.define(:text, :diff, :gone)
|
|
237
|
+
|
|
238
|
+
OPS = { "read" => "read", "edit" => "edit", "write" => "write", MEMORY_READ => "memory_read",
|
|
239
|
+
MEMORY_WRITE => "memory_write", WEB_FETCH => "fetch" }.freeze
|
|
240
|
+
# A range read past the file's end says so after its lines (Tools::Read).
|
|
241
|
+
CLAMP_NOTE = /\n\[read: end_line \d+ exceeds \d+ lines; returning lines \d+-\d+\]\z/
|
|
242
|
+
# How memory_read appends a model overlay (Tools::MemoryRead.call).
|
|
243
|
+
OVERLAY_HEAD = "#{Tools::MemoryRead::SEPARATOR}Model-specific guidance (".freeze
|
|
244
|
+
|
|
245
|
+
# The timeline of the item +kind+/+key+ (#build's identity): every run
|
|
246
|
+
# that touched it, edits and writes too, by the same walk #build and
|
|
247
|
+
# #outputs_of use (one all_touches), so the projection, the steer and
|
|
248
|
+
# the timeline never disagree on which runs belong to an item. An
|
|
249
|
+
# unknown key has none. Pure; the route adds #memory_now on request.
|
|
250
|
+
# @return [Timeline]
|
|
251
|
+
def timeline_of(conversation, kind:, key:, strategy:, cwd:)
|
|
252
|
+
conversation = Array(conversation)
|
|
253
|
+
view = LLMContextView.new(strategy: strategy)
|
|
254
|
+
events = all_touches(conversation, cwd, view).filter_map do |touch|
|
|
255
|
+
next unless touch.kind == kind && touch.key == key
|
|
256
|
+
|
|
257
|
+
event(touch, conversation, view, cwd)
|
|
258
|
+
end
|
|
259
|
+
Timeline.new(kind: kind, key: key, events: events)
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# What the memory +key+'s latest memory_read would answer now, and its
|
|
263
|
+
# diff from what that read sent; nil when no read of it has a stamp (a
|
|
264
|
+
# session saved before memory stamps). +root+: the memories folder
|
|
265
|
+
# (MemoryPaths.system_dir: the projects' folders are under it); a stamped
|
|
266
|
+
# path (entry or overlay) outside it is never read, nil then. +read+:
|
|
267
|
+
# File.read, or a stand-in.
|
|
268
|
+
# @return [Now, nil]
|
|
269
|
+
def memory_now(conversation, key:, strategy:, cwd:, root:, read: ->(path) { File.read(path, encoding: "UTF-8") })
|
|
270
|
+
conversation = Array(conversation)
|
|
271
|
+
view = LLMContextView.new(strategy: strategy)
|
|
272
|
+
touch = all_touches(conversation, cwd, view).reverse.find do |candidate|
|
|
273
|
+
candidate.kind == MEMORY && candidate.key == key && candidate.op == "read"
|
|
274
|
+
end
|
|
275
|
+
entries = touch && memory_entries(touch.run, conversation)
|
|
276
|
+
return nil unless entries
|
|
277
|
+
|
|
278
|
+
names = found_names(touch.run)
|
|
279
|
+
return nil unless names.all? { |name| entries[name].is_a?(Hash) && memory_paths?(entries[name], root) }
|
|
280
|
+
|
|
281
|
+
had = body_of(touch.run.text)
|
|
282
|
+
bodies = names.map { |name| memory_body(name, entries[name], read) }
|
|
283
|
+
return Now.new(text: nil, diff: nil, gone: true) if bodies.any?(&:nil?)
|
|
284
|
+
|
|
285
|
+
# Here, not at load: the testing kit loads WorkingSet and keeps a file budget (sandbox_spec).
|
|
286
|
+
require_relative "text_diff"
|
|
287
|
+
last = had.split(Tools::MemoryRead::SEPARATOR).last.to_s
|
|
288
|
+
bodies << last if last.match?(MISSING)
|
|
289
|
+
now = bodies.join(Tools::MemoryRead::SEPARATOR)
|
|
290
|
+
Now.new(text: now, diff: TextDiff.unified(had, now).merge(new_file: false), gone: false)
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
# Whether a stamp entry's file and its overlay's are .md files inside the
|
|
294
|
+
# memories folder +root+ (a stamp is the session file's: never read
|
|
295
|
+
# what it names elsewhere).
|
|
296
|
+
def memory_paths?(entry, root)
|
|
297
|
+
overlay = field(entry, :overlay)
|
|
298
|
+
paths = [field(entry, :path), overlay.is_a?(Hash) ? field(overlay, :path) : nil].compact
|
|
299
|
+
base = File.join(File.expand_path(root.to_s), "")
|
|
300
|
+
real = File.exist?(base) ? File.join(File.realpath(base), "") : base
|
|
301
|
+
paths.all? do |path|
|
|
302
|
+
expanded = File.expand_path(path.to_s)
|
|
303
|
+
next false unless expanded.start_with?(base) && expanded.end_with?(".md")
|
|
304
|
+
|
|
305
|
+
!File.exist?(expanded) || File.realpath(expanded).start_with?(real)
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def event(touch, conversation, view, cwd)
|
|
310
|
+
run = touch.run
|
|
311
|
+
name = run.call[:name].to_s
|
|
312
|
+
sent = !touch.content || still_sent?(touch, conversation, view)
|
|
313
|
+
dropped = touch.content && dropped?(touch, conversation)
|
|
314
|
+
text = body_of(run.text)
|
|
315
|
+
lines = diff = memory = nil
|
|
316
|
+
case name
|
|
317
|
+
when "read"
|
|
318
|
+
lines = lines_of(run, cwd, text)
|
|
319
|
+
text = text.sub(CLAMP_NOTE, "")
|
|
320
|
+
when "edit", "write"
|
|
321
|
+
diff = saved_diff(conversation[run.index], run.ref.run)
|
|
322
|
+
when MEMORY_READ
|
|
323
|
+
memory = memory_part(touch, conversation)
|
|
324
|
+
text = text.delete_suffix(overlay_tail(text)) if memory&.overlay
|
|
325
|
+
when MEMORY_WRITE
|
|
326
|
+
text = run.call[:content].to_s
|
|
327
|
+
end
|
|
328
|
+
Event.new(tool_id: run.ref.id, derived: run.ref.derived?, index: run.index, step: run.step, op: OPS[name],
|
|
329
|
+
sent: sent, dropped: dropped, forgotten: !sent && !dropped, lines: lines, diff: diff, text: text,
|
|
330
|
+
memory: memory, shared_with: touch.content ? shared_with(touch, touch.key) : [])
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
# A run's output without its "[name]" lead.
|
|
334
|
+
def body_of(text) = text.to_s.sub(ToolResponse::LEAD, "")
|
|
335
|
+
|
|
336
|
+
# A read's Lines: its range off LLMContextStale.file_run, its last line
|
|
337
|
+
# off +text+ (the lines it holds).
|
|
338
|
+
def lines_of(run, cwd, text)
|
|
339
|
+
file = LLMContextStale.file_run(run.index, run.ref, run.call, run.text, cwd || Dir.pwd, run.step)
|
|
340
|
+
return Lines.new(first: nil, last: nil, whole: true, partial: true) unless file
|
|
341
|
+
|
|
342
|
+
whole = file.lines == LLMContextStale::WHOLE
|
|
343
|
+
return Lines.new(first: whole ? nil : file.lines.begin, last: nil, whole: whole, partial: true) if file.partial
|
|
344
|
+
|
|
345
|
+
count = text.sub(CLAMP_NOTE, "").lines.size
|
|
346
|
+
first = file.lines.begin
|
|
347
|
+
last = first + [count, 1].max - 1
|
|
348
|
+
last = [last, file.lines.end].min unless file.lines.end.infinite?
|
|
349
|
+
Lines.new(first: first, last: last, whole: whole, partial: false)
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
# The diff saved for the run at +place+ of +entry+: ToolResponse.joined
|
|
353
|
+
# saves an array (one per run, nil for none), .single the bare Hash.
|
|
354
|
+
def saved_diff(entry, place)
|
|
355
|
+
diffs = entry[:tool_diffs] || entry["tool_diffs"]
|
|
356
|
+
diff = diffs.is_a?(Array) ? diffs[place] : diffs
|
|
357
|
+
diff.is_a?(Hash) ? diff.transform_keys(&:to_sym) : nil
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
# The stamp's {name => entry} of a memory_read run, or nil.
|
|
361
|
+
def memory_entries(run, conversation)
|
|
362
|
+
stamp = stamp_hash(conversation[run.index], run.ref.run)
|
|
363
|
+
entries = stamp && field(stamp, :memories)
|
|
364
|
+
entries.is_a?(Hash) ? entries.transform_keys(&:to_s) : nil
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
def memory_part(touch, conversation)
|
|
368
|
+
entries = memory_entries(touch.run, conversation)
|
|
369
|
+
entry = entries && entries[touch.key]
|
|
370
|
+
return nil unless entry.is_a?(Hash)
|
|
371
|
+
|
|
372
|
+
path = field(entry, :path).to_s
|
|
373
|
+
overlay = field(entry, :overlay)
|
|
374
|
+
overlay_path = overlay.is_a?(Hash) ? field(overlay, :path).to_s : nil
|
|
375
|
+
tail = touch.share == 1 && overlay_path ? overlay_tail(body_of(touch.run.text)) : ""
|
|
376
|
+
MemoryPart.new(scope: memory_scope(path), path: path, overlay_path: overlay_path,
|
|
377
|
+
overlay: tail.empty? ? nil : tail.sub(/\A#{Regexp.escape(OVERLAY_HEAD)}[^\n]*\n/o, ""))
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
# The overlay memory_read appended to +text+ (its separator and head
|
|
381
|
+
# line included), or "".
|
|
382
|
+
def overlay_tail(text)
|
|
383
|
+
at = text.rindex(OVERLAY_HEAD)
|
|
384
|
+
at ? text[at..] : ""
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
# A memory's scope by where its file is: a project's under
|
|
388
|
+
# memories/projects/<key>/, a system one's in memories/.
|
|
389
|
+
def memory_scope(path) = File.basename(File.dirname(path, 2)) == "projects" ? "project" : "system"
|
|
390
|
+
|
|
391
|
+
# What memory_read would answer now for +name+ (its entry and the overlay
|
|
392
|
+
# read with it, as Tools::MemoryRead.call appends it); nil when a file is
|
|
393
|
+
# gone.
|
|
394
|
+
def memory_body(name, entry, read)
|
|
395
|
+
body = read.call(field(entry, :path).to_s)
|
|
396
|
+
overlay = field(entry, :overlay)
|
|
397
|
+
return body unless overlay.is_a?(Hash)
|
|
398
|
+
|
|
399
|
+
overlay_path = field(overlay, :path).to_s
|
|
400
|
+
model_key = File.basename(overlay_path, ".md").delete_prefix("#{name}.")
|
|
401
|
+
"#{body}#{Tools::MemoryRead::SEPARATOR}Model-specific guidance (#{model_key}):\n#{read.call(overlay_path)}"
|
|
402
|
+
rescue SystemCallError
|
|
403
|
+
nil
|
|
404
|
+
end
|
|
405
|
+
|
|
192
406
|
# Every paired run's touches, with what +view+ sends of its output.
|
|
193
407
|
def all_touches(conversation, cwd, view)
|
|
194
408
|
stored = LLMContextStale.paired_runs(conversation)
|
|
@@ -252,6 +466,8 @@ module Samagotchi
|
|
|
252
466
|
contents.all? { |touch| dropped?(touch, conversation) } ? DROPPED : FORGOTTEN
|
|
253
467
|
elsif kind == FILE && changed?(key, group.last.run, conversation, stat)
|
|
254
468
|
STALE
|
|
469
|
+
elsif kind == MEMORY && memory_changed?(key, group.last.run, conversation, stat)
|
|
470
|
+
STALE
|
|
255
471
|
else
|
|
256
472
|
LIVE
|
|
257
473
|
end
|
|
@@ -291,24 +507,60 @@ module Samagotchi
|
|
|
291
507
|
stamp = stamp_of(conversation[run.index], run.ref.run)
|
|
292
508
|
return false unless stamp
|
|
293
509
|
|
|
510
|
+
differs?(path, stamp, stat)
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
# Whether the memory +key+ has changed since +run+'s stamp (the item's
|
|
514
|
+
# latest run): a memory_read's stamp has one entry per name it read, each
|
|
515
|
+
# with the file that answered and the model overlay read with it. No stamp
|
|
516
|
+
# - a session saved before memory stamps, or a reader that only knows file
|
|
517
|
+
# stamps - means not changed. The memory is gone, or its overlay is, as
|
|
518
|
+
# well: changed.
|
|
519
|
+
def memory_changed?(key, run, conversation, stat)
|
|
520
|
+
stamp = stamp_hash(conversation[run.index], run.ref.run)
|
|
521
|
+
return false unless stamp
|
|
522
|
+
|
|
523
|
+
entries = field(stamp, :memories)
|
|
524
|
+
entry = entries.is_a?(Hash) ? field(entries, key) : nil
|
|
525
|
+
return false unless entry.is_a?(Hash)
|
|
526
|
+
|
|
527
|
+
overlay = field(entry, :overlay)
|
|
528
|
+
differs?(field(entry, :path), entry, stat) ||
|
|
529
|
+
(overlay.is_a?(Hash) && differs?(field(overlay, :path), overlay, stat))
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
# Whether the file at +path+ is not what +entry+ ({mtime_ns:, size:}) says
|
|
533
|
+
# it was, in mtime or size.
|
|
534
|
+
def differs?(path, entry, stat)
|
|
294
535
|
now = stat.call(path)
|
|
295
536
|
mtime_ns = (now.mtime.to_i * 1_000_000_000) + now.mtime.nsec
|
|
296
|
-
mtime_ns != field(
|
|
537
|
+
mtime_ns != field(entry, :mtime_ns) || now.size != field(entry, :size)
|
|
297
538
|
rescue SystemCallError
|
|
298
539
|
# Gone (or unreadable): not what the model saw.
|
|
299
540
|
true
|
|
300
541
|
end
|
|
301
542
|
|
|
302
|
-
# +entry+'s tool_stamps at the run's place,
|
|
303
|
-
#
|
|
304
|
-
def
|
|
543
|
+
# +entry+'s tool_stamps at the run's place, whatever its shape (live has
|
|
544
|
+
# symbol keys, a reloaded session string keys); nil when there is none.
|
|
545
|
+
def stamp_hash(entry, place)
|
|
305
546
|
stamps = entry[:tool_stamps]
|
|
306
547
|
stamp = stamps.is_a?(Array) ? stamps[place] : nil
|
|
307
|
-
stamp.is_a?(Hash)
|
|
548
|
+
stamp.is_a?(Hash) ? stamp : nil
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
# A file run's stamp: {path:, mtime_ns:, size:}. A memory_read's stamp
|
|
552
|
+
# has no mtime_ns at its top ({memories: {name => ...}}), so a reader
|
|
553
|
+
# that knows only file stamps - one from before memory stamps, any
|
|
554
|
+
# strategy's - sees none there and never calls a memory stale.
|
|
555
|
+
def stamp_of(entry, place)
|
|
556
|
+
stamp = stamp_hash(entry, place)
|
|
557
|
+
stamp && field(stamp, :mtime_ns) ? stamp : nil
|
|
308
558
|
end
|
|
309
559
|
|
|
310
560
|
def field(hash, key) = hash.key?(key) ? hash[key] : hash[key.to_s]
|
|
311
561
|
|
|
312
|
-
private_class_method :all_touches, :touches, :failed?, :shared_with, :item, :still_sent?, :dropped?, :with_id,
|
|
562
|
+
private_class_method :all_touches, :touches, :failed?, :shared_with, :item, :still_sent?, :dropped?, :with_id,
|
|
563
|
+
:changed?, :memory_changed?, :differs?, :stamp_hash, :stamp_of, :field, :event, :body_of,
|
|
564
|
+
:lines_of, :saved_diff, :memory_entries, :memory_paths?, :memory_part, :overlay_tail, :memory_scope, :memory_body
|
|
313
565
|
end
|
|
314
566
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: samagotchi
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.171.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dmitry Dedov
|
|
@@ -579,6 +579,8 @@ files:
|
|
|
579
579
|
- lib/samagotchi/participant_messages.rb
|
|
580
580
|
- lib/samagotchi/participant_mode.rb
|
|
581
581
|
- lib/samagotchi/participant_name.rb
|
|
582
|
+
- lib/samagotchi/participant_revise.rb
|
|
583
|
+
- lib/samagotchi/participant_uploads.rb
|
|
582
584
|
- lib/samagotchi/participants.rb
|
|
583
585
|
- lib/samagotchi/participants_watch.rb
|
|
584
586
|
- lib/samagotchi/paths.rb
|
|
@@ -899,7 +901,9 @@ files:
|
|
|
899
901
|
- lib/samagotchi/web/markdown_renderer.rb
|
|
900
902
|
- lib/samagotchi/web/message_display.rb
|
|
901
903
|
- lib/samagotchi/web/participant_access.rb
|
|
904
|
+
- lib/samagotchi/web/participant_images.rb
|
|
902
905
|
- lib/samagotchi/web/participant_review.rb
|
|
906
|
+
- lib/samagotchi/web/participant_show.rb
|
|
903
907
|
- lib/samagotchi/web/participant_stream_filter.rb
|
|
904
908
|
- lib/samagotchi/web/participant_view.rb
|
|
905
909
|
- lib/samagotchi/web/participants_admin.rb
|
|
@@ -927,6 +931,7 @@ files:
|
|
|
927
931
|
- lib/samagotchi/web/public/bubbles.js
|
|
928
932
|
- lib/samagotchi/web/public/card.js
|
|
929
933
|
- lib/samagotchi/web/public/card_stamps.js
|
|
934
|
+
- lib/samagotchi/web/public/cards_offscreen.js
|
|
930
935
|
- lib/samagotchi/web/public/cards_place.js
|
|
931
936
|
- lib/samagotchi/web/public/chains.js
|
|
932
937
|
- lib/samagotchi/web/public/chip_popover.js
|
|
@@ -998,11 +1003,11 @@ files:
|
|
|
998
1003
|
- lib/samagotchi/web/public/objectives_wiring.js
|
|
999
1004
|
- lib/samagotchi/web/public/owner_claim.js
|
|
1000
1005
|
- lib/samagotchi/web/public/owner_verify.js
|
|
1001
|
-
- lib/samagotchi/web/public/
|
|
1002
|
-
- lib/samagotchi/web/public/
|
|
1003
|
-
- lib/samagotchi/web/public/participant_annotate.js
|
|
1006
|
+
- lib/samagotchi/web/public/participant_bar.js
|
|
1007
|
+
- lib/samagotchi/web/public/participant_main.js
|
|
1004
1008
|
- lib/samagotchi/web/public/participant_model.js
|
|
1005
1009
|
- lib/samagotchi/web/public/participant_names.js
|
|
1010
|
+
- lib/samagotchi/web/public/participant_revise.js
|
|
1006
1011
|
- lib/samagotchi/web/public/participants_panel.js
|
|
1007
1012
|
- lib/samagotchi/web/public/plain_line.js
|
|
1008
1013
|
- lib/samagotchi/web/public/popover_modal.js
|
|
@@ -1022,7 +1027,6 @@ files:
|
|
|
1022
1027
|
- lib/samagotchi/web/public/restore.js
|
|
1023
1028
|
- lib/samagotchi/web/public/resync_reach.js
|
|
1024
1029
|
- lib/samagotchi/web/public/review_apply.js
|
|
1025
|
-
- lib/samagotchi/web/public/review_history.js
|
|
1026
1030
|
- lib/samagotchi/web/public/review_start.js
|
|
1027
1031
|
- lib/samagotchi/web/public/revise_stack.js
|
|
1028
1032
|
- lib/samagotchi/web/public/revise_switch.js
|
|
@@ -1050,6 +1054,7 @@ files:
|
|
|
1050
1054
|
- lib/samagotchi/web/public/stage_frame.js
|
|
1051
1055
|
- lib/samagotchi/web/public/stage_model.js
|
|
1052
1056
|
- lib/samagotchi/web/public/stage_opened.js
|
|
1057
|
+
- lib/samagotchi/web/public/stage_owner_wait.js
|
|
1053
1058
|
- lib/samagotchi/web/public/stage_slots.js
|
|
1054
1059
|
- lib/samagotchi/web/public/stage_stall.js
|
|
1055
1060
|
- lib/samagotchi/web/public/stage_view.js
|
|
@@ -1079,7 +1084,6 @@ files:
|
|
|
1079
1084
|
- lib/samagotchi/web/public/token_fragment.js
|
|
1080
1085
|
- lib/samagotchi/web/public/tokens.css
|
|
1081
1086
|
- lib/samagotchi/web/public/toolkit_chip.js
|
|
1082
|
-
- lib/samagotchi/web/public/tools_overview.js
|
|
1083
1087
|
- lib/samagotchi/web/public/turn_block.js
|
|
1084
1088
|
- lib/samagotchi/web/public/turn_drawing.js
|
|
1085
1089
|
- lib/samagotchi/web/public/turn_events.js
|
|
@@ -1101,6 +1105,8 @@ files:
|
|
|
1101
1105
|
- lib/samagotchi/web/public/verify_preset.js
|
|
1102
1106
|
- lib/samagotchi/web/public/verify_target.js
|
|
1103
1107
|
- lib/samagotchi/web/public/version_notices.js
|
|
1108
|
+
- lib/samagotchi/web/public/viewer.js
|
|
1109
|
+
- lib/samagotchi/web/public/viewer_self.js
|
|
1104
1110
|
- lib/samagotchi/web/public/widget_picks.js
|
|
1105
1111
|
- lib/samagotchi/web/public/widget_walk.js
|
|
1106
1112
|
- lib/samagotchi/web/public/widgets.css
|
|
@@ -1108,6 +1114,12 @@ files:
|
|
|
1108
1114
|
- lib/samagotchi/web/public/working_set.js
|
|
1109
1115
|
- lib/samagotchi/web/public/working_set_rail.js
|
|
1110
1116
|
- lib/samagotchi/web/public/working_set_view.js
|
|
1117
|
+
- lib/samagotchi/web/public/ws_cards.js
|
|
1118
|
+
- lib/samagotchi/web/public/ws_peek.js
|
|
1119
|
+
- lib/samagotchi/web/public/ws_preview.css
|
|
1120
|
+
- lib/samagotchi/web/public/ws_reader.js
|
|
1121
|
+
- lib/samagotchi/web/public/ws_timeline.js
|
|
1122
|
+
- lib/samagotchi/web/public/ws_way_in.js
|
|
1111
1123
|
- lib/samagotchi/web/public_url.rb
|
|
1112
1124
|
- lib/samagotchi/web/push.rb
|
|
1113
1125
|
- lib/samagotchi/web/push/attention.rb
|
|
@@ -1134,6 +1146,8 @@ files:
|
|
|
1134
1146
|
- lib/samagotchi/web/update_check.rb
|
|
1135
1147
|
- lib/samagotchi/web/update_ops.rb
|
|
1136
1148
|
- lib/samagotchi/web/verify_fresh_routes.rb
|
|
1149
|
+
- lib/samagotchi/web/viewer.rb
|
|
1150
|
+
- lib/samagotchi/web/working_set_routes.rb
|
|
1137
1151
|
- lib/samagotchi/worker.rb
|
|
1138
1152
|
- lib/samagotchi/worker_boot.rb
|
|
1139
1153
|
- lib/samagotchi/worker_idle_exit.rb
|
|
@@ -1,218 +0,0 @@
|
|
|
1
|
-
<!doctype html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="utf-8">
|
|
5
|
-
<title>chi · review</title>
|
|
6
|
-
<link rel="icon" href="/assets/favicon.svg" type="image/svg+xml">
|
|
7
|
-
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
|
|
8
|
-
<link rel="stylesheet" href="/assets/tokens.css">
|
|
9
|
-
<link rel="stylesheet" href="/assets/verify_card.css">
|
|
10
|
-
<link rel="stylesheet" href="/assets/annotate_bar.css">
|
|
11
|
-
<link rel="stylesheet" href="/assets/composer.css">
|
|
12
|
-
<link rel="stylesheet" href="/assets/forms.css">
|
|
13
|
-
<style>
|
|
14
|
-
/* The participant page (participant.js): the thread with the composer pinned, and for a review session its subjects
|
|
15
|
-
(the Plan): beside the thread on a wide screen, a Plan | Thread toggle on a narrow one (the composer pinned in both).
|
|
16
|
-
The owner's participant view is the same page with no composer (nothing mounted, the dock hidden). Colours are tokens.css's. */
|
|
17
|
-
*{box-sizing:border-box;margin:0;padding:0}
|
|
18
|
-
/* No iOS text autosizing (index.html's twin). */
|
|
19
|
-
html{-webkit-text-size-adjust:100%;text-size-adjust:100%}
|
|
20
|
-
html,body{height:100%;height:100dvh}
|
|
21
|
-
.p-body{flex:1;min-height:0;display:flex}
|
|
22
|
-
.p-col{flex:1;min-width:0;min-height:0;display:flex;flex-direction:column}
|
|
23
|
-
.p-plan{min-height:0;display:flex;flex-direction:column;background:var(--bg-panel);border-right:1px solid var(--border)}
|
|
24
|
-
.p-plan-files{display:flex;gap:4px;flex-wrap:wrap;padding:8px 16px 0}
|
|
25
|
-
.p-plan-files button{font-size:12px;padding:2px 10px;border-radius:999px;background:transparent;color:var(--text-3)}
|
|
26
|
-
.p-plan-files button[aria-pressed="true"]{background:var(--accent-bg);border-color:var(--accent-border);color:var(--accent)}
|
|
27
|
-
.p-plan-path{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;color:var(--text-muted);padding:8px 16px 0}
|
|
28
|
-
.p-plan-scroll{flex:1;min-height:0;overflow-y:auto;padding:4px 16px 24px}
|
|
29
|
-
.p-doc{max-width:72ch;word-break:break-word}
|
|
30
|
-
.p-doc h1,.p-doc h2,.p-doc h3,.p-doc h4{color:var(--text-bright);line-height:1.3;margin:18px 0 8px}
|
|
31
|
-
.p-doc h1{font-size:22px}.p-doc h2{font-size:18px}.p-doc h3{font-size:16px}.p-doc h4{font-size:15px}
|
|
32
|
-
.p-doc p,.p-doc ul,.p-doc ol,.p-doc pre,.p-doc table,.p-doc blockquote{margin:8px 0}
|
|
33
|
-
.p-doc ul,.p-doc ol{padding-left:22px}
|
|
34
|
-
.p-doc li+li{margin-top:2px}
|
|
35
|
-
.p-doc pre{overflow:auto;padding:10px;border-radius:8px;background:var(--bg-inset)}
|
|
36
|
-
.p-doc code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.88em}
|
|
37
|
-
.p-doc blockquote{border-left:3px solid var(--border-strong);padding-left:12px;color:var(--text-3)}
|
|
38
|
-
.p-doc table{border-collapse:collapse}
|
|
39
|
-
.p-doc th,.p-doc td{border:1px solid var(--border);padding:3px 8px}
|
|
40
|
-
.p-doc a{color:var(--accent)}
|
|
41
|
-
.p-doc.plain{white-space:pre-wrap;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12.5px}
|
|
42
|
-
.p-plan-note{font-size:12.5px;color:var(--text-muted);margin:8px 0}
|
|
43
|
-
.p-seg{display:none;border:1px solid var(--border-strong);border-radius:8px;overflow:hidden}
|
|
44
|
-
.p-seg button{border:0;border-radius:0;padding:3px 14px;font-size:13px;background:transparent;color:var(--text-3)}
|
|
45
|
-
.p-seg button[aria-selected="true"]{background:var(--accent-bg);color:var(--accent);font-weight:600}
|
|
46
|
-
@media (min-width:1100px){
|
|
47
|
-
.p-plan{flex:0 0 min(46vw,720px)}
|
|
48
|
-
body.has-plan .p-head-in,body.has-plan .p-thread,body.has-plan .p-dock-in{max-width:none}
|
|
49
|
-
}
|
|
50
|
-
@media (max-width:1099px){
|
|
51
|
-
.p-body{flex-direction:column}
|
|
52
|
-
body.has-plan .p-seg{display:inline-flex}
|
|
53
|
-
.p-plan{flex:1;border-right:0}
|
|
54
|
-
body.view-thread .p-plan{display:none}
|
|
55
|
-
body.view-plan .p-col{flex:0 0 auto}
|
|
56
|
-
body.view-plan main{display:none}
|
|
57
|
-
}
|
|
58
|
-
body{font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;background:var(--bg);color:var(--text);display:flex;flex-direction:column;font-size:15px;line-height:1.55}
|
|
59
|
-
.p-head{position:sticky;top:0;z-index:2;background:color-mix(in srgb,var(--bg-panel) 88%,transparent);-webkit-backdrop-filter:blur(14px);backdrop-filter:blur(14px);border-bottom:1px solid var(--border);padding:10px 16px}
|
|
60
|
-
.p-head-in{max-width:760px;margin:0 auto;display:flex;flex-wrap:wrap;align-items:center;gap:6px 12px}
|
|
61
|
-
.p-title{font-size:16px;font-weight:600;color:var(--text-bright);min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1 1 220px}
|
|
62
|
-
.p-title .p-sub{font-weight:400;color:var(--text-muted)}
|
|
63
|
-
.p-status{font-size:12px;padding:2px 8px;border-radius:999px;background:var(--bg-raised);color:var(--text-3);white-space:nowrap}
|
|
64
|
-
.p-status:empty{display:none}
|
|
65
|
-
.p-status.running{background:var(--accent-bg);color:var(--accent)}
|
|
66
|
-
.p-status.waiting{background:var(--warn-bg);color:var(--warn-text)}
|
|
67
|
-
.p-people{display:flex;flex-wrap:wrap;gap:4px;font-size:12px;color:var(--text-muted)}
|
|
68
|
-
.p-person{display:inline-flex;align-items:center;gap:5px;padding:1px 8px;border-radius:999px;border:1px solid var(--border)}
|
|
69
|
-
.p-dot{width:7px;height:7px;border-radius:50%;background:var(--author-color,var(--author-owner))}
|
|
70
|
-
.p-note{flex-basis:100%;font-size:12.5px;color:var(--text-muted)}
|
|
71
|
-
/* Over the thread's top, out of the sticky header's flow: a read that fails (or recovers) doesn't move the thread. */
|
|
72
|
-
.p-offline{position:absolute;top:calc(100% + 6px);left:16px;right:16px;max-width:760px;margin:0 auto;box-sizing:border-box;box-shadow:var(--toast-shadow);font-size:12.5px;color:var(--warn-text);background:var(--warn-bg);border:1px solid var(--warn-border);border-radius:8px;padding:4px 10px}
|
|
73
|
-
.p-view-banner{flex-basis:100%;font-size:12.5px;color:var(--warn-text);background:var(--warn-bg);border:1px solid var(--warn-border);border-radius:8px;padding:4px 10px}
|
|
74
|
-
main{flex:1;min-height:0;overflow-y:auto;padding:16px 16px 8px}
|
|
75
|
-
.p-thread{max-width:760px;margin:0 auto;display:flex;flex-direction:column;gap:12px}
|
|
76
|
-
.p-empty{color:var(--text-muted);text-align:center;padding:40px 0}
|
|
77
|
-
/* A gap divider (time_gaps.js): one centred dim line where a quiet stretch passed, never wrapped (a phone at 360 px keeps it on one line). Not a .bubble. */
|
|
78
|
-
.time-gap{align-self:stretch;text-align:center;color:var(--time-gap);font-size:12px;line-height:1.4;font-variant-numeric:tabular-nums;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin:2px 0;user-select:none}
|
|
79
|
-
.p-msg{display:flex;flex-direction:column;gap:4px;min-width:0}
|
|
80
|
-
.p-who{font-size:12px;color:var(--text-muted);display:flex;align-items:center;gap:6px}
|
|
81
|
-
.p-who b{font-weight:600;color:var(--author-color,var(--author-owner))}
|
|
82
|
-
.p-bubble{background:var(--bg-panel);border:1px solid var(--border);border-radius:12px;padding:10px 14px;word-break:break-word;min-width:0}
|
|
83
|
-
.p-msg.user .p-bubble{white-space:pre-wrap;border-left:3px solid var(--author-color,var(--author-owner))}
|
|
84
|
-
.p-msg.mine .p-bubble{background:var(--accent-tint)}
|
|
85
|
-
.p-msg.reply .p-bubble{border-left:3px solid var(--accent-strong)}
|
|
86
|
-
.p-msg.reply .p-bubble.plain{white-space:pre-wrap}
|
|
87
|
-
.p-msg.live .p-bubble{border-left-color:var(--accent)}
|
|
88
|
-
.p-bubble pre{overflow:auto;padding:10px;border-radius:8px;background:var(--bg-inset);margin:8px 0}
|
|
89
|
-
.p-bubble code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.88em}
|
|
90
|
-
.p-bubble p+p,.p-bubble ul,.p-bubble ol{margin-top:8px}
|
|
91
|
-
.p-bubble ul,.p-bubble ol{padding-left:22px}
|
|
92
|
-
.p-bubble a{color:var(--accent)}
|
|
93
|
-
.p-bubble table{border-collapse:collapse;margin:8px 0}
|
|
94
|
-
.p-bubble th,.p-bubble td{border:1px solid var(--border);padding:3px 8px}
|
|
95
|
-
.p-tools{font-size:12.5px;color:var(--text-muted)}
|
|
96
|
-
.p-tools summary{cursor:pointer;list-style:none;display:inline-flex;align-items:center;gap:6px;padding:2px 0}
|
|
97
|
-
.p-tools summary::-webkit-details-marker{display:none}
|
|
98
|
-
.p-tools summary::before{content:"▸"}
|
|
99
|
-
.p-tools[open] summary::before{content:"▾"}
|
|
100
|
-
.p-tools ul{list-style:none;padding:2px 0 0 14px;display:flex;flex-direction:column;gap:1px}
|
|
101
|
-
.p-tools li{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
102
|
-
.p-tools li.error{color:var(--danger-text)}
|
|
103
|
-
.p-tools li.running,.p-tool-one.running{color:var(--accent)}
|
|
104
|
-
.p-tool-one{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px}
|
|
105
|
-
.p-tool-one.error{color:var(--danger-text)}
|
|
106
|
-
.p-tag{font-size:11px;padding:0 6px;border-radius:999px;background:var(--bg-raised);color:var(--text-3)}
|
|
107
|
-
.p-waiting{align-self:flex-start;font-size:13px;color:var(--warn-text);background:var(--warn-bg);border:1px solid var(--warn-border);border-radius:999px;padding:3px 12px}
|
|
108
|
-
/* The dock is the owner's composer card (index.html's #dock): a glass card over the page's bottom edge, holding the
|
|
109
|
-
comment box (key hint, text field, usage footer); the queue and errors sit above it. */
|
|
110
|
-
.p-dock{padding:6px 16px calc(16px + env(safe-area-inset-bottom))}
|
|
111
|
-
.p-dock-in{max-width:760px;margin:0 auto;display:flex;flex-direction:column;gap:8px}
|
|
112
|
-
.p-queue{display:flex;flex-direction:column;gap:4px;font-size:13px}
|
|
113
|
-
.p-queued{display:flex;align-items:center;gap:8px;color:var(--text-3)}
|
|
114
|
-
.p-queued span{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
115
|
-
.p-queue-note{font-size:12.5px;color:var(--text-muted)}
|
|
116
|
-
.p-error{font-size:13px;color:var(--danger-text);background:var(--danger-bg-dim);border-radius:8px;padding:6px 10px}
|
|
117
|
-
/* The comment box: the shared composer in the shared card (composer.css, composer_mount.js), as the owner's session
|
|
118
|
-
composer lays it out (participant.js sets body.has-session for a participant): the key hint as the composer's line,
|
|
119
|
-
the text field and Send, the usage as the card's footer. Its grip sits on the card's top edge, as the owner's does;
|
|
120
|
-
folded, the card keeps its footer under the grip, the owner's info bar's way. No `/` list, delivery choices or model
|
|
121
|
-
picker here. */
|
|
122
|
-
#composer textarea{font:inherit;line-height:1.45;padding:2px 0}
|
|
123
|
-
#composer textarea:disabled{cursor:not-allowed}
|
|
124
|
-
#composer .actions-col #send{height:30px;min-height:30px;line-height:30px;padding:0 16px;font-weight:600}
|
|
125
|
-
/* The key hint wraps to at most two lines (a narrow card cuts the second's end); the whole text is its title. */
|
|
126
|
-
.p-hint.composer-line{color:var(--text-faint);height:auto;max-height:36px;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;line-clamp:2}
|
|
127
|
-
/* The participant's own usage (participant_model.js usageItems: the fields their grant's exposure shows), one line in
|
|
128
|
-
the card's footer, the end cut. Folded, it stays under the grip. */
|
|
129
|
-
.p-foot{min-width:0}
|
|
130
|
-
.p-foot:has(> .p-usage[hidden]){display:none}
|
|
131
|
-
/* Folded before the usage loads, the card keeps a footer's height under its grip, not a 2 px sliver. */
|
|
132
|
-
body.composer-folded .p-foot:has(> .p-usage[hidden]){display:block;min-height:18px}
|
|
133
|
-
.p-usage{display:flex;align-items:baseline;min-width:0;max-width:100%;font-size:12px;color:var(--text-muted);font-variant-numeric:tabular-nums;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
134
|
-
.p-usage span{flex:0 0 auto}
|
|
135
|
-
.p-usage span+span::before{content:"·";padding:0 6px;color:var(--text-faint)}
|
|
136
|
-
.p-usage span.model{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis}
|
|
137
|
-
.p-usage .warn{color:var(--warn-text)}
|
|
138
|
-
.p-usage .over{color:var(--danger-text);font-weight:600}
|
|
139
|
-
body.composer-folded .p-foot{border-top-color:transparent}
|
|
140
|
-
button{font:inherit;cursor:pointer;border-radius:8px;border:1px solid var(--border-strong);background:var(--bg-raised);color:var(--text);padding:6px 12px}
|
|
141
|
-
button:hover{background:var(--bg-hover)}
|
|
142
|
-
button.primary{background:var(--primary);border-color:var(--primary);color:var(--on-solid);height:42px;padding:0 18px}
|
|
143
|
-
button.primary:disabled{opacity:.5;cursor:default}
|
|
144
|
-
button.small{font-size:12px;padding:2px 8px}
|
|
145
|
-
.p-ended{max-width:30rem;margin:18vh auto 0;padding:0 16px;text-align:center;display:flex;flex-direction:column;gap:10px}
|
|
146
|
-
.p-ended h1{font-size:20px;color:var(--text-bright)}
|
|
147
|
-
.p-ended p{color:var(--text-3)}
|
|
148
|
-
[hidden]{display:none!important}
|
|
149
|
-
@media (max-width:640px){
|
|
150
|
-
.p-head{padding:8px 16px}
|
|
151
|
-
.p-title{font-size:15px;flex-basis:100%}
|
|
152
|
-
.p-people .p-person:nth-child(n+4){display:none}
|
|
153
|
-
main{padding:12px 16px 6px}
|
|
154
|
-
.p-bubble{padding:9px 12px}
|
|
155
|
-
.p-hint.composer-line,.p-note{display:none}
|
|
156
|
-
}
|
|
157
|
-
/* Under 16px iOS Safari zooms the page on focus (and a Home Screen app never zooms back). */
|
|
158
|
-
@media (max-width:600px),(pointer:coarse){#composer textarea{font-size:16px}}
|
|
159
|
-
</style>
|
|
160
|
-
</head>
|
|
161
|
-
<body>
|
|
162
|
-
<header class="p-head" id="head" hidden>
|
|
163
|
-
<div class="p-head-in">
|
|
164
|
-
<div class="p-title" id="title"></div>
|
|
165
|
-
<span class="p-status" id="status"></span>
|
|
166
|
-
<div class="p-seg" id="viewSeg" role="tablist" aria-label="Show"><button type="button" role="tab" id="segPlan"
|
|
167
|
-
aria-controls="plan" data-view="plan">Plan</button><button type="button" role="tab" id="segThread"
|
|
168
|
-
aria-controls="main" data-view="thread">Thread</button></div>
|
|
169
|
-
<div class="p-people" id="people"></div>
|
|
170
|
-
<div class="p-view-banner" id="viewBanner" hidden>Participant view: exactly what participants see of this session, through the same filter. Read-only.</div>
|
|
171
|
-
<div class="p-note" id="note"></div>
|
|
172
|
-
</div>
|
|
173
|
-
<div class="p-offline" id="offline" role="status" hidden>Can't reach the session — retrying…</div>
|
|
174
|
-
<div class="p-offline" id="claimNote" role="status" hidden></div>
|
|
175
|
-
</header>
|
|
176
|
-
<div class="p-body">
|
|
177
|
-
<aside class="p-plan" id="plan" hidden aria-label="Under review">
|
|
178
|
-
<div class="p-plan-files" id="planFiles" hidden></div>
|
|
179
|
-
<div class="p-plan-path" id="planPath"></div>
|
|
180
|
-
<div class="p-plan-scroll" id="planScroll"><div class="p-doc" id="planDoc"></div></div>
|
|
181
|
-
</aside>
|
|
182
|
-
<div class="p-col">
|
|
183
|
-
<main id="main" hidden>
|
|
184
|
-
<div class="p-thread" id="thread"></div>
|
|
185
|
-
</main>
|
|
186
|
-
<footer class="p-dock" id="dock" hidden>
|
|
187
|
-
<div class="p-dock-in">
|
|
188
|
-
<div class="p-queue" id="queue"></div>
|
|
189
|
-
<div class="p-error" id="error" role="alert" hidden></div>
|
|
190
|
-
<div class="p-card composer-card">
|
|
191
|
-
<div id="composeGrip" class="edge-grip" tabindex="0" aria-label="Comment box" title="Drag to make the comment box taller (double-click resets)"></div>
|
|
192
|
-
<div id="composer">
|
|
193
|
-
<div class="p-hint composer-line" id="hint">⏎ sends, ⇧⏎ a new line, ↑ an earlier comment. The model answers in a read-only turn; the owner sees everything.</div>
|
|
194
|
-
<div class="compose-col">
|
|
195
|
-
<div id="chips" hidden></div>
|
|
196
|
-
<textarea id="comment" name="chi-comment" autocomplete="off" rows="1" placeholder="Comment or ask" aria-label="Comment or ask"></textarea>
|
|
197
|
-
</div>
|
|
198
|
-
<div class="actions-col">
|
|
199
|
-
<button type="button" id="attachBtn" class="attach-btn" title="Add images" aria-label="Add images" hidden></button>
|
|
200
|
-
<input type="file" id="attachInput" accept="image/*" multiple hidden>
|
|
201
|
-
<div class="split"><button type="button" class="primary" id="send">Send</button></div>
|
|
202
|
-
</div>
|
|
203
|
-
</div>
|
|
204
|
-
<div class="p-foot composer-card-foot">
|
|
205
|
-
<div class="p-usage" id="usage" aria-label="Your usage" hidden></div>
|
|
206
|
-
</div>
|
|
207
|
-
</div>
|
|
208
|
-
</div>
|
|
209
|
-
</footer>
|
|
210
|
-
</div>
|
|
211
|
-
</div>
|
|
212
|
-
<section class="p-ended" id="ended" hidden>
|
|
213
|
-
<h1>This review link has ended</h1>
|
|
214
|
-
<p id="endedText"></p>
|
|
215
|
-
</section>
|
|
216
|
-
<script type="module" src="/assets/participant.js"></script>
|
|
217
|
-
</body>
|
|
218
|
-
</html>
|