samagotchi 0.130.1 → 0.131.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 +30 -1
- data/docs/cli.md +3 -1
- data/docs/configuration.md +4 -1
- data/docs/internals/participants.md +23 -6
- data/docs/internals/prompt-caching.md +5 -2
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/cache_confirm.rb +29 -9
- data/lib/samagotchi/engine.rb +31 -2
- data/lib/samagotchi/level_setting_command.rb +3 -2
- data/lib/samagotchi/session_commands.rb +4 -4
- data/lib/samagotchi/toolkit_command.rb +2 -2
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/participant_view.rb +22 -6
- data/lib/samagotchi/web/public/annotate_bar.js +6 -1
- data/lib/samagotchi/web/public/app.js +14 -5
- data/lib/samagotchi/web/public/participant.js +24 -6
- data/lib/samagotchi/web/public/verdict_card.js +48 -4
- data/lib/samagotchi/web/public/verdict_fresh.js +8 -4
- data/lib/samagotchi/web/public/verify_target.js +73 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 37f2a54062786637083ade10186ebb751a1cfd7a7996540defa03da018b29533
|
|
4
|
+
data.tar.gz: 6836e60f2a87358698adaf000592ac68af5ce8fe8f3c26799825fa26641d3f1a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b908ad2c276d4dabb1c8bd64ad8435a2f9ab5408663243f8cd0f24e25ca2e4647cb356228a7a37027c1c5f2895936c4df8424af5203d8c121b5403577e2a2456
|
|
7
|
+
data.tar.gz: a3c96161fc7cb55e200367417e1dac93442845e0a868ad02d58ecca15a504af6824c419e8188252c9100daf405892be203e75fd6d7baaca1406c20186a4ff6f4
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,34 @@ and commands may change between minor versions. How releases are made:
|
|
|
7
7
|
[docs/releasing.md](docs/releasing.md).
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.131.0] - 2026-10-29
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- Cache: confirmations for prompt-cache changes estimate the reread size when known and warn that Anthropic drops
|
|
15
|
+
preserved thinking after a full-prefix change.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- Web: a Verify verdict card sits right after the answer it quotes, on your page and a participant's, instead of
|
|
20
|
+
under the turn's prompt, screens above the claim in a long turn. A quote the page can't find yet puts the card at
|
|
21
|
+
the turn's end.
|
|
22
|
+
- Web: Verify on a claim in a model's reply on a participant's page records its verdict and shows the card again;
|
|
23
|
+
the claim was sent without its turn, so the check ran as a plain comment.
|
|
24
|
+
- Web: after a fresh verdict lands on an unclear card, its button reads **Verify fresh again**: each press starts
|
|
25
|
+
another paid check (a participant's counts against their cap).
|
|
26
|
+
- Web: Verify is offered only on a finished turn's final answer, on your page and a participant's: the one place a
|
|
27
|
+
verdict card can come from. Selecting an earlier reply, a step, a message, a line added mid-turn or a turn still
|
|
28
|
+
running no longer offers a Verify that silently ran as a plain comment; the bar keeps its other actions there.
|
|
29
|
+
- Web: on a participant's page, a turn a delegate's report or a context note woke opens as "An automatic turn"
|
|
30
|
+
(still without the report or note). Its answer read as one more reply of the turn before: Verify was offered on
|
|
31
|
+
it and refused, and hidden on that turn's real final answer.
|
|
32
|
+
- Web: Verify on a claim that names a file in the session's folder or your home folder (or holds a key) records its
|
|
33
|
+
verdict: the pages quote such a path relative, or with `~`, and the check compared it against the answer's full
|
|
34
|
+
path, so it ran as a plain comment with no card.
|
|
35
|
+
|
|
36
|
+
Update with `chi update`, then restart `chi web` and the session workers (`chi sessions restart`).
|
|
37
|
+
|
|
10
38
|
## [0.130.1] - 2026-10-29
|
|
11
39
|
|
|
12
40
|
### Fixed
|
|
@@ -4964,7 +4992,8 @@ and long-lived sessions.
|
|
|
4964
4992
|
- A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
|
|
4965
4993
|
hotkey panel that send selected text or the clipboard to your sessions.
|
|
4966
4994
|
|
|
4967
|
-
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.
|
|
4995
|
+
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.131.0...HEAD
|
|
4996
|
+
[0.131.0]: https://github.com/dm1try/samagotchi/compare/v0.130.1...v0.131.0
|
|
4968
4997
|
[0.130.1]: https://github.com/dm1try/samagotchi/compare/v0.130.0...v0.130.1
|
|
4969
4998
|
[0.130.0]: https://github.com/dm1try/samagotchi/compare/v0.129.0...v0.130.0
|
|
4970
4999
|
[0.129.0]: https://github.com/dm1try/samagotchi/compare/v0.128.0...v0.129.0
|
data/docs/cli.md
CHANGED
|
@@ -1214,7 +1214,9 @@ pills there (a review turn refuses `/` lines).
|
|
|
1214
1214
|
|
|
1215
1215
|
After the presets, both pages have a built-in **Verify**: the quote, then
|
|
1216
1216
|
an ask to check the claim against the code and end on a line `Verdict:
|
|
1217
|
-
confirmed|refuted|unclear — <reason>`.
|
|
1217
|
+
confirmed|refuted|unclear — <reason>`. Verify is offered only on text in a finished turn's final answer, the
|
|
1218
|
+
claim a verdict card can come from; elsewhere (an earlier reply, a step, a message, a line added mid-turn, a turn
|
|
1219
|
+
still running) the bar keeps its other actions. A session whose worker predates Verify (started by an older chi) refuses it (409
|
|
1218
1220
|
`verify_unavailable`); a participant's page disables Verify and Verify fresh with a hint to restart the session. On your page a selection in a turn
|
|
1219
1221
|
also offers **Copy link**: a link to that turn and quote for a participant's
|
|
1220
1222
|
browser (the one their review link was opened in), which opens their page
|
data/docs/configuration.md
CHANGED
|
@@ -838,7 +838,10 @@ Gemma's thinking switch, a remote chat host's thinking fields ("Thinking"), a ch
|
|
|
838
838
|
reply has thinking ("Reasoning history"), and any change to the declared tools, which start every request
|
|
839
839
|
("Toolkits"). They ask only when what the next turn runs under really changes and the session has a model reply.
|
|
840
840
|
|
|
841
|
-
- The reply says what it costs and changes nothing yet
|
|
841
|
+
- The reply says what it costs and changes nothing yet. When known, it estimates the reread size: the whole
|
|
842
|
+
conversation for full-prefix changes, or from the first changed reply for reasoning history. On `api: anthropic`,
|
|
843
|
+
a full-prefix change also says preserved thinking in replies is dropped from then on.
|
|
844
|
+
- `Nothing changed yet: send /thinking low --yes to apply.` names the line that applies it.
|
|
842
845
|
- `--yes` (or `-y`, anywhere after the command) applies it at once.
|
|
843
846
|
- The terminal (the REPL and an attached TUI) puts the `--yes` line in the next input: Enter applies it.
|
|
844
847
|
- The web's reply bubble has an **Apply (restarts the cache)** button. The session bar's chips are already an explicit
|
|
@@ -375,7 +375,10 @@ fields: a field added to an event later reaches nobody until it is named there.
|
|
|
375
375
|
so the owner's `!` line and its output, a delegate's report (a wake turn's too), and the notes chi adds (a declined
|
|
376
376
|
Continue) never show. A turn no person sent (its record's `automatic`, `SessionMetrics`: a task's or a plugin's,
|
|
377
377
|
read from analytics.json and the live worker's records; the running turn's by its start; a delegate child's first
|
|
378
|
-
turn, its parent's task) shows as an automatic turn without its prompt, as the stream does
|
|
378
|
+
turn, its parent's task) shows as an automatic turn without its prompt, as the stream does; so does a wake turn's
|
|
379
|
+
start (`.wake_start`: the delegate report or the context note a wake turn opens with is dropped, its `turn_id` and
|
|
380
|
+
`at` kept), so its answer is never read as one more reply of the turn before (Verify, verdict cards and claim
|
|
381
|
+
links group by `[data-turn-id]`). A raw loop's saved
|
|
379
382
|
reply is cut before the display reads it (`.visible_raw`): what came before a bare `</think>` and everything from
|
|
380
383
|
an unclosed thought or tool call on, which the display would only untag. Each
|
|
381
384
|
prompt carries its author and an image count; replies their text and their step's tool rows. Notes, steers and
|
|
@@ -428,6 +431,13 @@ fields: a field added to an event later reaches nobody until it is named there.
|
|
|
428
431
|
After the presets comes the built-in **Verify** (`verify_preset.js`, on the owner's page too): the quote, then
|
|
429
432
|
"Verify this claim against the code. If this session's context doesn't settle it, say so. End with a line
|
|
430
433
|
`Verdict: confirmed|refuted|unclear — <one-line reason>`." It fills the box like any preset and never sends.
|
|
434
|
+
It is offered only where the Engine will take the claim (`verify_target.js`, the bar's `offeredPreset`): a
|
|
435
|
+
selection in the final answer of an ended turn, the last answer bubble (the owner's `.bubble.output` that isn't a
|
|
436
|
+
delegate report's reply; a participant's last `.p-msg.reply > .p-bubble`) between the turn's opening
|
|
437
|
+
(`[data-turn-id]`) and the next one (or, on the owner's page, a context note: a wake turn opens with one; a
|
|
438
|
+
participant's page gets a wake turn's opening as an automatic turn).
|
|
439
|
+
Anywhere else (an earlier reply, a step, a prompt, a line merged mid-turn, the Plan, the running turn) the button
|
|
440
|
+
is hidden and the bar keeps its other actions.
|
|
431
441
|
- **Deep links to a claim** (`claim_link.js`). The owner selects text in a turn's answer, step or own message and
|
|
432
442
|
Annotate's **Copy link** copies `<base>/#s=<id>&t=<turn8>&q=<quote>&p=<prefix>`: `base` the participants list's
|
|
433
443
|
`link_base` (`ParticipantsAdmin#participant_base`: `web.public_url`'s origin, else the LAN address, else the Host),
|
|
@@ -454,13 +464,18 @@ fields: a field added to an event later reaches nobody until it is named there.
|
|
|
454
464
|
with the submitted turn, without changing its prompt text. A Verify turn that fails fast and is rolled back hands
|
|
455
465
|
the claim back with its prompt (`prompt_restored`'s `verify`), so the owner's resend from the composer is a Verify
|
|
456
466
|
turn again. Before the Engine stamps the anchor or records a verdict, it checks that the source turn exists and
|
|
457
|
-
its assistant reply contains the quote. Matching is case-sensitive, collapses whitespace, and strips common
|
|
467
|
+
its final assistant reply (`AnswerTail`: the turn's last assistant message with text) contains the quote. Matching is case-sensitive, collapses whitespace, and strips common
|
|
458
468
|
Markdown presentation (links, headings, list/blockquote prefixes, emphasis and code delimiters) from both sides;
|
|
459
|
-
this matches the rendered text the page selects without treating different casing as the same claim.
|
|
469
|
+
this matches the rendered text the page selects without treating different casing as the same claim. The quote may
|
|
470
|
+
also be in the answer as the pages show it (`Engine#verify_quote_in?`, `ParticipantView::Redact.text`: the session's
|
|
471
|
+
folder relative, home `~`, secret shapes `[redacted]`): a participant's page has only that text, and the owner's
|
|
472
|
+
page quotes it the same way (`claimOpts()`, `normalizeClaim`; `spec/fixtures/redact_cases.json` keeps the two
|
|
473
|
+
sides alike), so a claim naming a path is taken on both. A fresh child
|
|
460
474
|
checks the same anchor against its parent's history. A completed Verify reply whose final non-empty line has
|
|
461
475
|
a valid `Verdict: confirmed|refuted|unclear — <reason>` appends a verdict to the session. Both pages place its card
|
|
462
|
-
|
|
463
|
-
|
|
476
|
+
in the source turn, right after the bubble that holds the quote (the answer's, matched as a claim link reads the
|
|
477
|
+
page; a quote not found yet goes at the turn's end), name its author, show the reason and quote, and link to the
|
|
478
|
+
checking turn; a live completion adds the card without a reload. The participant view receives only the card fields through its allowlist, with the
|
|
464
479
|
quote and reason redacted by the view's normal rules, never the full reply.
|
|
465
480
|
- **Verify fresh** (`VerifyFresh`, `verdict_fresh.js`). An `unclear` card has a **Verify fresh** button (the owner's
|
|
466
481
|
page always; a participant's while `can_comment`, i.e. while their grant could run a turn now; never in the owner's
|
|
@@ -520,7 +535,9 @@ fields: a field added to an event later reaches nobody until it is named there.
|
|
|
520
535
|
`:verdict_recorded`; chi web shows the ones still waiting (`VerifyFresh.verdicts_of`: the summary, the review
|
|
521
536
|
data). The card shows "Verifying fresh…" while the child runs (the summary's `verify_fresh` `{of, child_id}`,
|
|
522
537
|
the review data's `fresh_running`, ids only) and the fresh verdict as a `fresh: ✅ confirmed — …` line on the same
|
|
523
|
-
card. The
|
|
538
|
+
card. The original stays unclear, so the button stays, as **Verify fresh again**: each press is another paid
|
|
539
|
+
child (a participant's against their cap). The participant view passes `fresh` and `of`, never the child's id (a
|
|
540
|
+
participant can't open it). No push
|
|
524
541
|
for a fresh verdict yet (`Attention` pushes a verdict at a turn's end).
|
|
525
542
|
- **The comment box** is the owner's composer (`composer_mount.js`, styled by the shared `composer.css`), mounted by
|
|
526
543
|
`participant.js` with a subset on. It has: the height that grows with the text, the grip (drag to resize,
|
|
@@ -232,8 +232,11 @@ is prefilled once. The native and the chat loop use the same plan.
|
|
|
232
232
|
classifier: `Thinking.cache_cost` (`:full` for Gemma, `:provider` for a remote chat host, `:tail` otherwise) and
|
|
233
233
|
`ReasoningHistory.cache_cost` (`:history` for a change to or from `all` once an earlier reply has thinking).
|
|
234
234
|
`CacheConfirm.needed?` asks only for `:full`, `:provider` and `:history`, when the effective level changes and
|
|
235
|
-
the session has a model reply.
|
|
236
|
-
|
|
235
|
+
the session has a model reply. The confirmation estimates the reread size from the latest known context count:
|
|
236
|
+
the whole conversation for `:full` / `:provider`, or up to the first changed reply for `:history`; it omits the
|
|
237
|
+
estimate when no count is known. On `api: anthropic`, a `:full` / `:provider` change also warns that preserved
|
|
238
|
+
thinking in replies is dropped from then on. A new setting with a cost (a `/model` across templates,
|
|
239
|
+
`/llm-context` turning forget on) adds its classifier and goes through the same step.
|
|
237
240
|
7. **No wall-clock time rendered at wire time.** A history entry's `at` (`MessageTime::KEY`, when it happened) is
|
|
238
241
|
display-only: no wire format reads it, and a guard spec checks that `ChatLoop#wire_messages` and
|
|
239
242
|
`Prompt.format_with_images` give the same bytes with and without it (`spec/message_time_history_spec.rb`). A
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Samagotchi
|
|
4
|
-
# The confirm step before a
|
|
5
|
-
#
|
|
6
|
-
# without --yes from a person at a web tab or a chi TUI, changes nothing
|
|
4
|
+
# The confirm step before a command that restarts the prompt cache: sent
|
|
5
|
+
# without --yes by a person at a web tab or a chi TUI, it changes nothing
|
|
7
6
|
# and replies with what it costs and the line that applies it. Two steps,
|
|
8
7
|
# no stored state: the reply carries a Confirm (SessionCommands::Result,
|
|
9
8
|
# the worker's command_ran), the TUI prefills its line, the web draws an
|
|
@@ -43,12 +42,33 @@ module Samagotchi
|
|
|
43
42
|
changed && history && COSTLY.include?(cost)
|
|
44
43
|
end
|
|
45
44
|
|
|
46
|
-
#
|
|
47
|
-
#
|
|
48
|
-
# @
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
45
|
+
# Whether a cache-changing command should ask, and its reply line.
|
|
46
|
+
# Kept independent of any UI so commands and plugins can share it.
|
|
47
|
+
# @param line [String] the full command line without --yes
|
|
48
|
+
# @param cost [Symbol, nil] the change's cache cost
|
|
49
|
+
# @param changed [Boolean] whether the effective setting changes
|
|
50
|
+
# @param engine [Engine] supplies history, used tokens and model host
|
|
51
|
+
# @return [Confirm, nil]
|
|
52
|
+
def ask(line:, cost:, changed:, engine:)
|
|
53
|
+
return nil unless needed?(cost: cost, history: engine.history?, changed: changed)
|
|
54
|
+
|
|
55
|
+
apply_line = "#{line.to_s.strip} --yes"
|
|
56
|
+
notes = []
|
|
57
|
+
tokens = engine.cache_restart_tokens
|
|
58
|
+
if tokens
|
|
59
|
+
size = tokens >= 1000 ? "#{format("%.1f", tokens / 1000.0).delete_suffix(".0")}k" : tokens.to_s
|
|
60
|
+
notes << case cost
|
|
61
|
+
when :full, :provider
|
|
62
|
+
"The next request reads the whole conversation again (about #{size} tokens)."
|
|
63
|
+
when :history
|
|
64
|
+
"The next request reads again from the first changed reply (up to about #{size} tokens)."
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
if %i[full provider].include?(cost) && engine.cache_restart_anthropic?
|
|
68
|
+
notes << "On an `api: anthropic` host, preserved thinking in replies is dropped from then on."
|
|
69
|
+
end
|
|
70
|
+
notes << "Nothing changed yet: send `#{apply_line}` to apply."
|
|
71
|
+
Confirm.new(line: apply_line, text: notes.compact.join("\n"))
|
|
52
72
|
end
|
|
53
73
|
end
|
|
54
74
|
end
|
data/lib/samagotchi/engine.rb
CHANGED
|
@@ -16,6 +16,7 @@ require_relative "turn_note"
|
|
|
16
16
|
require_relative "turn_endings"
|
|
17
17
|
require_relative "turn_origin"
|
|
18
18
|
require_relative "verify_anchor"
|
|
19
|
+
require_relative "web/participant_view"
|
|
19
20
|
require_relative "verdict"
|
|
20
21
|
require_relative "model_profile"
|
|
21
22
|
require_relative "effective_model"
|
|
@@ -1352,6 +1353,19 @@ module Samagotchi
|
|
|
1352
1353
|
Array(@session&.messages).any? { |entry| entry[:role].to_s == "model" }
|
|
1353
1354
|
end
|
|
1354
1355
|
|
|
1356
|
+
# The latest known context size used by a request, for a cache-restart
|
|
1357
|
+
# confirmation; nil before any request reports a count.
|
|
1358
|
+
# @return [Integer, nil]
|
|
1359
|
+
def cache_restart_tokens
|
|
1360
|
+
used = @metrics.snapshot.dig(:context, :used_tokens)
|
|
1361
|
+
used if used.is_a?(Integer)
|
|
1362
|
+
end
|
|
1363
|
+
|
|
1364
|
+
# Whether a cache restart can invalidate preserved thinking blocks.
|
|
1365
|
+
def cache_restart_anthropic?
|
|
1366
|
+
effective_model.entry&.api.to_s == "anthropic"
|
|
1367
|
+
end
|
|
1368
|
+
|
|
1355
1369
|
# What a thinking level change costs the effective model's prompt
|
|
1356
1370
|
# cache (Thinking.cache_cost): :full, :provider or :tail.
|
|
1357
1371
|
# @return [Symbol]
|
|
@@ -1646,13 +1660,28 @@ module Samagotchi
|
|
|
1646
1660
|
return reject_verify_anchor(anchor, :reply_missing) unless reply
|
|
1647
1661
|
|
|
1648
1662
|
quote_text = verify_comparison_text(anchor.quote)
|
|
1649
|
-
|
|
1650
|
-
return reject_verify_anchor(anchor, :quote_missing)
|
|
1663
|
+
quoted = !quote_text.empty? && verify_quote_in?(quote_text, (reply[:content] || reply["content"]).to_s, source)
|
|
1664
|
+
return reject_verify_anchor(anchor, :quote_missing) unless quoted
|
|
1651
1665
|
|
|
1652
1666
|
anchor
|
|
1653
1667
|
end
|
|
1654
1668
|
private :verified_anchor
|
|
1655
1669
|
|
|
1670
|
+
# Whether +quote_text+ is in +answer+ as the model wrote it, or as the
|
|
1671
|
+
# pages show it: a page quotes its own text, which reads the session's
|
|
1672
|
+
# folder relative, the home folder as ~ and a secret shape as
|
|
1673
|
+
# [redacted] (a participant's page gets it so, ParticipantView::Redact;
|
|
1674
|
+
# the owner's page rewrites its quote the same way, claim_link.js
|
|
1675
|
+
# normalizeClaim, so claim links match both pages). +source+ is the
|
|
1676
|
+
# session whose folder that is.
|
|
1677
|
+
def verify_quote_in?(quote_text, answer, source)
|
|
1678
|
+
return true if verify_comparison_text(answer).include?(quote_text)
|
|
1679
|
+
|
|
1680
|
+
viewer = Web::ParticipantView::Viewer.new(cwd: source.working_directory)
|
|
1681
|
+
verify_comparison_text(Web::ParticipantView::Redact.text(answer, viewer)).include?(quote_text)
|
|
1682
|
+
end
|
|
1683
|
+
private :verify_quote_in?
|
|
1684
|
+
|
|
1656
1685
|
def verify_anchor_source_session(session, anchor)
|
|
1657
1686
|
return session unless session.verify_fresh
|
|
1658
1687
|
|
|
@@ -81,9 +81,10 @@ module Samagotchi
|
|
|
81
81
|
return nil unless before && would
|
|
82
82
|
|
|
83
83
|
cost = spec.cache_cost.call(@engine, before.level, would.level)
|
|
84
|
-
|
|
84
|
+
step = CacheConfirm.ask(line: "#{spec.name} #{word}", cost: cost, changed: would.level != before.level,
|
|
85
|
+
engine: @engine)
|
|
86
|
+
return nil unless step
|
|
85
87
|
|
|
86
|
-
step = CacheConfirm.confirm(spec.name, word)
|
|
87
88
|
[[listing(before), "Switching to #{would.level} restarts the prompt cache: #{spec.cost_notes[cost]}", step.text].join("\n"),
|
|
88
89
|
false, step]
|
|
89
90
|
end
|
|
@@ -59,7 +59,7 @@ module Samagotchi
|
|
|
59
59
|
# /help's note per listed mid_turn (the web's command list says the same).
|
|
60
60
|
MID_TURN_NOTES = { "anytime" => "mid-turn too", "depends" => "show: mid-turn too" }.freeze
|
|
61
61
|
|
|
62
|
-
#
|
|
62
|
+
# Commands that may ask before a change that restarts the prompt cache
|
|
63
63
|
# (CacheConfirm): #run passes them its confirm flag.
|
|
64
64
|
CONFIRMING = %i[thinking reasoning_history toolkit].freeze
|
|
65
65
|
|
|
@@ -287,8 +287,8 @@ module Samagotchi
|
|
|
287
287
|
# How help and errors name an entry (the shell's "!" is "!<cmd>").
|
|
288
288
|
def self.display_name(entry) = entry.name == SHELL_BANG_PREFIX ? "#{SHELL_BANG_PREFIX}<cmd>" : entry.name
|
|
289
289
|
|
|
290
|
-
# @param confirm [Boolean] a costly
|
|
291
|
-
#
|
|
290
|
+
# @param confirm [Boolean] a costly setting change asks first
|
|
291
|
+
# (CacheConfirm): true for a person at a web tab or
|
|
292
292
|
# a TUI; false applies it at once (chi send, a delegate, a plugin,
|
|
293
293
|
# chi -p)
|
|
294
294
|
# @param as_parent [Boolean] a parent agent sent it (client cli:answer):
|
|
@@ -304,7 +304,7 @@ module Samagotchi
|
|
|
304
304
|
return nil unless entry&.handler
|
|
305
305
|
return run_bundle_command(entry, text) unless entry.source == "core"
|
|
306
306
|
|
|
307
|
-
#
|
|
307
|
+
# Only commands that may ask take the flag.
|
|
308
308
|
return send(entry.id, text, confirm: confirm) if CONFIRMING.include?(entry.id)
|
|
309
309
|
# Who sent it goes to its handler per call, never through an ivar:
|
|
310
310
|
# an anytime command runs on another thread against this instance.
|
|
@@ -54,8 +54,8 @@ module Samagotchi
|
|
|
54
54
|
end
|
|
55
55
|
|
|
56
56
|
cost = Toolkit.cache_cost(before.prompt_key, after.prompt_key)
|
|
57
|
-
|
|
58
|
-
|
|
57
|
+
step = CacheConfirm.ask(line: "#{NAME} #{words}", cost: cost, changed: true, engine: @engine) if confirm && !yes
|
|
58
|
+
if step
|
|
59
59
|
return [[change_line(before, after), reread_line(before, after), step.text].join("\n"), false, step]
|
|
60
60
|
end
|
|
61
61
|
|
data/lib/samagotchi/version.rb
CHANGED
|
@@ -211,7 +211,8 @@ module Samagotchi
|
|
|
211
211
|
# @return [Array<Hash>] {role: "user", content, turn_id, author,
|
|
212
212
|
# author_name, mine, automatic, images, at} and {role: "assistant",
|
|
213
213
|
# content, tools: [{tool, path}], at}; no notes, steers, automatic
|
|
214
|
-
# input or empty-answer markers
|
|
214
|
+
# input or empty-answer markers, but a wake turn's start as an
|
|
215
|
+
# automatic turn (#wake_start). html is added by the caller, from
|
|
215
216
|
# this (redacted) content.
|
|
216
217
|
def messages(shown, viewer, automatic: [])
|
|
217
218
|
Array(shown).filter_map do |message|
|
|
@@ -220,6 +221,7 @@ module Samagotchi
|
|
|
220
221
|
out = case field(message, :role).to_s
|
|
221
222
|
when "user" then user_message(message, viewer, automatic)
|
|
222
223
|
when "assistant" then assistant_message(message, viewer)
|
|
224
|
+
when "note" then wake_start(message)
|
|
223
225
|
end
|
|
224
226
|
out && Redact.deep(out, viewer)
|
|
225
227
|
end
|
|
@@ -228,15 +230,15 @@ module Samagotchi
|
|
|
228
230
|
# A user message a participant sees: a turn's prompt (it has the
|
|
229
231
|
# turn's id) or a person's line merged into a turn. Every other user
|
|
230
232
|
# message is dropped: the owner's `!` line and its output, a
|
|
231
|
-
# delegate's report (a wake turn's input
|
|
232
|
-
#
|
|
233
|
-
#
|
|
233
|
+
# delegate's report (a wake turn's input: only its turn's start is
|
|
234
|
+
# kept, #wake_start), the notes chi adds (a Continue the owner
|
|
235
|
+
# declined). Any other message from a source that isn't a person's is
|
|
236
|
+
# dropped whatever it is.
|
|
234
237
|
def user_message(message, viewer, automatic = [])
|
|
235
238
|
source = field(message, :source).to_s
|
|
236
|
-
return nil unless PERSON_SOURCES.include?(source)
|
|
237
|
-
|
|
238
239
|
turn_id = field(message, :turn_id)
|
|
239
240
|
merged = field(message, :merged)
|
|
241
|
+
return (turn_id && !merged ? wake_start(message) : nil) unless PERSON_SOURCES.include?(source)
|
|
240
242
|
return nil unless turn_id || merged
|
|
241
243
|
|
|
242
244
|
out = { "role" => "user", **time_field(message) }
|
|
@@ -254,6 +256,20 @@ module Samagotchi
|
|
|
254
256
|
out
|
|
255
257
|
end
|
|
256
258
|
|
|
259
|
+
# A wake turn's start (its prompt a delegate's report or a context
|
|
260
|
+
# note, MessageDisplay's turn_start note): the turn's boundary only,
|
|
261
|
+
# as an automatic turn with no text, so its answer is never read as
|
|
262
|
+
# one more reply of the turn before (Verify, verdict cards, claim
|
|
263
|
+
# links group by these). Nothing of the report or the note. nil for
|
|
264
|
+
# a message that starts no turn.
|
|
265
|
+
def wake_start(message)
|
|
266
|
+
turn_id = field(message, :turn_id)
|
|
267
|
+
return nil if turn_id.to_s.empty?
|
|
268
|
+
return nil if field(message, :role).to_s == "note" && field(message, :turn_start) != true
|
|
269
|
+
|
|
270
|
+
{ "role" => "user", **time_field(message), "content" => "", "automatic" => true, "turn_id" => turn_id.to_s }
|
|
271
|
+
end
|
|
272
|
+
|
|
257
273
|
def assistant_message(message, viewer)
|
|
258
274
|
parts = field(message, :parts)
|
|
259
275
|
tools = parts.is_a?(Hash) ? Array(field(parts, :tools)) : []
|
|
@@ -33,6 +33,9 @@ import { actionEnabled, actionTitle, sameActions, selectionFields, splitInline }
|
|
|
33
33
|
// @param onQuote (text) runs with the quote (and a preset's note) to append
|
|
34
34
|
// to the composer; the selection is cleared and the bar hidden first
|
|
35
35
|
// @param disabledPreset (preset) → a reason to disable it, else null
|
|
36
|
+
// @param offeredPreset (preset, source, range) → whether the selection
|
|
37
|
+
// offers it (default: every preset); one not offered is hidden, not
|
|
38
|
+
// disabled (Verify outside a turn's final answer: verify_target.js)
|
|
36
39
|
// @param actions () => the open session's annotation actions
|
|
37
40
|
// (annotate_actions.js annotationActions), read on every selection
|
|
38
41
|
// @param running () => whether a turn runs (a send pill may wait for it)
|
|
@@ -58,6 +61,7 @@ import { actionEnabled, actionTitle, sameActions, selectionFields, splitInline }
|
|
|
58
61
|
// next frame (call it on scroll)
|
|
59
62
|
export function createAnnotateBar({
|
|
60
63
|
presets, canAnnotate, roots, isLive, onQuote, onPreset = () => {}, disabledPreset = () => null,
|
|
64
|
+
offeredPreset = () => true,
|
|
61
65
|
actions = () => [], running = () => false, onAction = () => {},
|
|
62
66
|
dock = () => null, source: sourceOf = annotationSource, quote = quoteBlock, link = () => null, onLink = () => {},
|
|
63
67
|
doc = globalThis.document, win = globalThis.window,
|
|
@@ -85,6 +89,7 @@ export function createAnnotateBar({
|
|
|
85
89
|
const presetButtons = [...bar.children].slice(1, presets.length + 1);
|
|
86
90
|
function markPresets() {
|
|
87
91
|
presetButtons.forEach((btn, i) => {
|
|
92
|
+
btn.hidden = !!pendingSource && !offeredPreset(presets[i], pendingSource, pendingRange);
|
|
88
93
|
const reason = disabledPreset(presets[i]);
|
|
89
94
|
btn.disabled = !!reason;
|
|
90
95
|
btn.title = reason || presetParts(presets[i]).text;
|
|
@@ -259,7 +264,7 @@ export function createAnnotateBar({
|
|
|
259
264
|
return;
|
|
260
265
|
}
|
|
261
266
|
const preset = btn.dataset.index === undefined ? null : presets[Number(btn.dataset.index)];
|
|
262
|
-
if (btn.disabled || (preset && disabledPreset(preset))) return;
|
|
267
|
+
if (btn.disabled || btn.hidden || (preset && disabledPreset(preset))) return;
|
|
263
268
|
const text = preset ? presetNote(pendingQuote, presetParts(preset).text) : pendingQuote;
|
|
264
269
|
const fields = { ...pendingFields, source: pendingSource, range: pendingRange };
|
|
265
270
|
win.getSelection()?.removeAllRanges();
|
|
@@ -65,6 +65,7 @@ import { authorBadge, nameUnnamedBadges } from "./participant_names.js";
|
|
|
65
65
|
import { parsePresets } from "./annotate_presets.js";
|
|
66
66
|
import { createAnnotateBar } from "./annotate_bar.js";
|
|
67
67
|
import { VERIFY_PRESET, VERIFY_UNAVAILABLE, verifyWorkerUnavailable, withVerify } from "./verify_preset.js";
|
|
68
|
+
import { OWNER_FINAL, verifyTurnOf } from "./verify_target.js";
|
|
68
69
|
import { actionLine, annotationActions, composerWithAction } from "./annotate_actions.js";
|
|
69
70
|
import { copyText, installCopy } from "./copy.js";
|
|
70
71
|
import { createQuestionCards } from "./question_cards.js";
|
|
@@ -1351,6 +1352,8 @@ function placeVerdict(record) {
|
|
|
1351
1352
|
placeVerdictWithFresh(historyEl, record, {
|
|
1352
1353
|
who: badge ? badge.name : "You",
|
|
1353
1354
|
author: record.by || null,
|
|
1355
|
+
roots: CLAIM_ROOTS,
|
|
1356
|
+
opts: claimOpts(),
|
|
1354
1357
|
onMissingCheck: (missing) => {
|
|
1355
1358
|
const hash = ownerCheckHash(allSessions, selected, missing.check_turn_id);
|
|
1356
1359
|
if (hash) location.hash = hash;
|
|
@@ -1959,8 +1962,9 @@ const updateSheet = createUpdateSheet({
|
|
|
1959
1962
|
function showToast(text, action = null, options = {}) { toasts.show(text, action, options); }
|
|
1960
1963
|
|
|
1961
1964
|
// Annotate (annotate_bar.js): a selection in the conversation offers
|
|
1962
|
-
// "Annotate" and the presets (then the built-in Verify: verify_preset.js
|
|
1963
|
-
//
|
|
1965
|
+
// "Annotate" and the presets (then the built-in Verify: verify_preset.js,
|
|
1966
|
+
// only in an ended turn's final answer: verify_target.js), which append a
|
|
1967
|
+
// quote to the composer, the session's plugins' pills
|
|
1964
1968
|
// (chi.annotation, annotate_actions.js), which fill the composer with their
|
|
1965
1969
|
// command line and the quote, or send it at once, and Copy link in a turn
|
|
1966
1970
|
// (claimLinkFor).
|
|
@@ -1978,6 +1982,7 @@ annotateBar = createAnnotateBar({
|
|
|
1978
1982
|
presets: withVerify(parsePresets(document.body.dataset.annotatePresets)),
|
|
1979
1983
|
disabledPreset: (preset) => preset === VERIFY_PRESET
|
|
1980
1984
|
&& verifyWorkerUnavailable(current.workerFeatures, current.bridgeUp) ? VERIFY_UNAVAILABLE : null,
|
|
1985
|
+
offeredPreset: (preset, source, range) => preset !== VERIFY_PRESET || !!verifyAnchorFor({ source, range }),
|
|
1981
1986
|
canAnnotate: () => !!selected && current.owner !== "tui",
|
|
1982
1987
|
roots: turnRoots,
|
|
1983
1988
|
isLive: (root) => liveTurn.running && turnDom.containsLive(root),
|
|
@@ -1997,10 +2002,14 @@ annotateBar = createAnnotateBar({
|
|
|
1997
2002
|
dock: () => $("#dock"),
|
|
1998
2003
|
});
|
|
1999
2004
|
|
|
2005
|
+
// Verify's claim: a selection in an ended turn's final answer (the only
|
|
2006
|
+
// text Engine#verified_anchor takes; verify_target.js), else null and no
|
|
2007
|
+
// Verify on the bar.
|
|
2000
2008
|
function verifyAnchorFor({ source, range }) {
|
|
2001
|
-
if (
|
|
2002
|
-
const
|
|
2003
|
-
const
|
|
2009
|
+
if (source?.kind !== "answer" || !range) return null;
|
|
2010
|
+
const running = liveTurn.running ? { turnId: liveTurn.timing.turnId || null } : null;
|
|
2011
|
+
const turnId = verifyTurnOf(historyEl, source.root, OWNER_FINAL, { running });
|
|
2012
|
+
const quote = turnId ? selectionClaim(source.root, range, claimOpts()).q : "";
|
|
2004
2013
|
return turnId && quote ? { turn_id: turnId, quote } : null;
|
|
2005
2014
|
}
|
|
2006
2015
|
historyEl.addEventListener("scroll", annotateBar.schedule);
|
|
@@ -23,6 +23,7 @@ import { REPLACE_SESSION_TEXT, fragmentCode, tradeFragmentCode } from "./token_f
|
|
|
23
23
|
import { createAnnotateBar } from "./annotate_bar.js";
|
|
24
24
|
import { parsePresets } from "./annotate_presets.js";
|
|
25
25
|
import { VERIFY_PRESET, VERIFY_UNAVAILABLE, withVerify } from "./verify_preset.js";
|
|
26
|
+
import { PARTICIPANT_FINAL, verifyTurnOf } from "./verify_target.js";
|
|
26
27
|
import { appendQuote } from "./annotations.js";
|
|
27
28
|
import { messageAuthorData, participantQuote, participantSource } from "./participant_annotate.js";
|
|
28
29
|
import { claimFromHash, selectionClaim, showClaim } from "./claim_link.js";
|
|
@@ -76,6 +77,8 @@ let drawnMessages = [];
|
|
|
76
77
|
// A claim link's turn and quote (claim_link.js: #s=…&t=…&q=…), shown once
|
|
77
78
|
// the thread is drawn; a later hash change shows another.
|
|
78
79
|
let pendingClaim = claimFromHash(globalThis.location?.hash);
|
|
80
|
+
// Where a claim's text can be on this page (a claim link, a verdict's card).
|
|
81
|
+
const CLAIM_ROOTS = ".p-msg > .p-bubble";
|
|
79
82
|
let claimNoteTimer = null;
|
|
80
83
|
|
|
81
84
|
function el(tag, cls, text) {
|
|
@@ -259,7 +262,7 @@ function showPendingClaim() {
|
|
|
259
262
|
return showClaimNote(`This link is for another session; this browser holds your link for ${title}.`);
|
|
260
263
|
}
|
|
261
264
|
setView("thread");
|
|
262
|
-
if (showClaim($("thread"), claim, { roots:
|
|
265
|
+
if (showClaim($("thread"), claim, { roots: CLAIM_ROOTS })) return;
|
|
263
266
|
$("main").scrollTop = 0;
|
|
264
267
|
showClaimNote("The linked turn isn't in this session.");
|
|
265
268
|
}
|
|
@@ -330,6 +333,8 @@ function setView(view) {
|
|
|
330
333
|
$("segThread").setAttribute("aria-selected", String(shown === "thread"));
|
|
331
334
|
}
|
|
332
335
|
|
|
336
|
+
const AUTOMATIC_TURN = "An automatic turn";
|
|
337
|
+
|
|
333
338
|
function messageNode(message) {
|
|
334
339
|
const user = message.role === "user";
|
|
335
340
|
const node = el("div", `p-msg ${user ? "user" : "reply"}${message.mine ? " mine" : ""}`);
|
|
@@ -346,7 +351,10 @@ function messageNode(message) {
|
|
|
346
351
|
if (message.merged) who.append(el("span", "p-tag", "added mid-turn"));
|
|
347
352
|
if (isParticipantAuthor(message.author)) who.append(el("span", "p-tag", modeCopy(message.participant_mode).tag));
|
|
348
353
|
node.append(who);
|
|
349
|
-
|
|
354
|
+
// An automatic turn (a task's, a wake turn's: a delegate report or a
|
|
355
|
+
// context note started it) opens its turn without its prompt.
|
|
356
|
+
const content = message.automatic && !message.content ? AUTOMATIC_TURN : message.content;
|
|
357
|
+
const text = content + (message.images ? `\n[${message.images} image${message.images > 1 ? "s" : ""}]` : "");
|
|
350
358
|
node.append(el("div", "p-bubble", text));
|
|
351
359
|
return node;
|
|
352
360
|
}
|
|
@@ -423,6 +431,7 @@ function renderThread() {
|
|
|
423
431
|
placeVerdictWithFresh(thread, record, {
|
|
424
432
|
who,
|
|
425
433
|
author,
|
|
434
|
+
roots: CLAIM_ROOTS,
|
|
426
435
|
onMissingCheck: () => showClaimNote(CHECK_NOT_ON_PAGE),
|
|
427
436
|
}, verifyFreshState());
|
|
428
437
|
}
|
|
@@ -477,7 +486,7 @@ function currentTurnNodes() {
|
|
|
477
486
|
const turn = data.current_turn;
|
|
478
487
|
const nodes = [];
|
|
479
488
|
if (turn.prompt !== undefined || turn.automatic) {
|
|
480
|
-
nodes.push(messageNode({ role: "user", content: turn.automatic ?
|
|
489
|
+
nodes.push(messageNode({ role: "user", content: turn.automatic ? AUTOMATIC_TURN : turn.prompt, author: turn.author,
|
|
481
490
|
author_name: turn.author_name, mine: turn.mine, automatic: turn.automatic, at: turn.at, turn_id: turn.turn_id }));
|
|
482
491
|
}
|
|
483
492
|
const reply = el("div", "p-msg reply live");
|
|
@@ -653,7 +662,8 @@ for (const button of $("viewSeg").querySelectorAll("button")) {
|
|
|
653
662
|
|
|
654
663
|
// Annotate (annotate_bar.js, participant_annotate.js): a selection in the
|
|
655
664
|
// Plan or the thread offers "Annotate" and the presets
|
|
656
|
-
// (web.annotate_presets, then the built-in Verify: verify_preset.js
|
|
665
|
+
// (web.annotate_presets, then the built-in Verify: verify_preset.js, only
|
|
666
|
+
// in an ended turn's final reply: verify_target.js), which
|
|
657
667
|
// append a quote to the comment box; nothing is sent. No plugin pills: a
|
|
658
668
|
// review turn refuses `/` lines. Not in the owner's participant view, nor
|
|
659
669
|
// while this participant can't comment.
|
|
@@ -661,6 +671,7 @@ const annotateBar = createAnnotateBar({
|
|
|
661
671
|
presets: withVerify(parsePresets(body.annotatePresets)),
|
|
662
672
|
disabledPreset: (preset) => preset === VERIFY_PRESET
|
|
663
673
|
&& data?.session?.verify_unavailable === true ? VERIFY_UNAVAILABLE : null,
|
|
674
|
+
offeredPreset: (preset, source, range) => preset !== VERIFY_PRESET || !!verifyAnchorFor({ source, range }),
|
|
664
675
|
canAnnotate: () => !ownerView && !ended && !!data?.viewer?.can_comment,
|
|
665
676
|
roots: () => [$("planDoc"), $("thread")],
|
|
666
677
|
isLive: (root) => !!root.closest?.(".p-msg.live"),
|
|
@@ -671,9 +682,16 @@ const annotateBar = createAnnotateBar({
|
|
|
671
682
|
dock: () => $("dock"),
|
|
672
683
|
});
|
|
673
684
|
|
|
685
|
+
// Verify's claim: a selection in an ended turn's final reply (the last
|
|
686
|
+
// reply bubble after the turn's opening, as the owner's page and
|
|
687
|
+
// placeVerdict count turns: verify_target.js), the only text
|
|
688
|
+
// Engine#verified_anchor takes; else null and no Verify on the bar. Not an
|
|
689
|
+
// earlier reply, a line merged mid-turn, a prompt, the running turn or the
|
|
690
|
+
// Plan.
|
|
674
691
|
function verifyAnchorFor({ source, range }) {
|
|
675
|
-
|
|
676
|
-
const
|
|
692
|
+
if (source?.kind !== "reply" || !range) return null;
|
|
693
|
+
const running = data?.current_turn ? { turnId: data.current_turn.turn_id || null } : null;
|
|
694
|
+
const turnId = verifyTurnOf($("thread"), source.root, PARTICIPANT_FINAL, { running });
|
|
677
695
|
if (!turnId) return null;
|
|
678
696
|
const quote = selectionClaim(source.root, range).q;
|
|
679
697
|
return quote ? { turn_id: turnId, quote } : null;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ownerClaimHash } from "./claim_link.js";
|
|
1
|
+
import { findClaim, ownerClaimHash } from "./claim_link.js";
|
|
2
2
|
|
|
3
3
|
const TYPES = Object.freeze({
|
|
4
4
|
confirmed: { icon: "✅", label: "confirmed" },
|
|
@@ -17,7 +17,14 @@ export function ownerCheckHash(sessions, currentSessionId, checkTurnId) {
|
|
|
17
17
|
return session ? ownerClaimHash(session.id, { turnId: checkTurnId }) : null;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
// A verdict's card on its source turn: after the bubble that holds the
|
|
21
|
+
// quote (+roots+, the page's claim roots selector; +opts+, how it reads the
|
|
22
|
+
// page's text, claim_link.js), the answer's first; a quote not found (a
|
|
23
|
+
// bubble not drawn yet): at the turn's end. Cards on one bubble keep their
|
|
24
|
+
// order; a card already on the page is returned as it is.
|
|
25
|
+
export function placeVerdict(root, record, {
|
|
26
|
+
who = "Owner", author = null, onMissingCheck = null, roots = null, opts = {},
|
|
27
|
+
} = {}) {
|
|
21
28
|
const anchor = record?.anchor;
|
|
22
29
|
const type = TYPES[record?.verdict];
|
|
23
30
|
if (!root || !type || typeof anchor?.turn_id !== "string" || typeof anchor?.quote !== "string"
|
|
@@ -67,11 +74,48 @@ export function placeVerdict(root, record, { who = "Owner", author = null, onMis
|
|
|
67
74
|
});
|
|
68
75
|
card.append(heading, quote, reason, link);
|
|
69
76
|
|
|
70
|
-
|
|
71
|
-
(previous || source).after(card);
|
|
77
|
+
afterCards(quotedElement(source, anchor.quote, roots, opts) || turnEnd(source)).after(card);
|
|
72
78
|
return card;
|
|
73
79
|
}
|
|
74
80
|
|
|
81
|
+
// The element at +source+'s level that holds the quote: of the turn's
|
|
82
|
+
// roots after its prompt, the last match first (the answer the Engine
|
|
83
|
+
// checked the quote against comes after the turn's steps).
|
|
84
|
+
function quotedElement(source, quote, selector, opts) {
|
|
85
|
+
if (!selector || !source.parentElement) return null;
|
|
86
|
+
const level = source.parentElement;
|
|
87
|
+
const candidates = turnElements(source).flatMap((element) =>
|
|
88
|
+
[...(element.matches(selector) ? [element] : []), ...element.querySelectorAll(selector)]).reverse();
|
|
89
|
+
const hit = findClaim(candidates.map((candidate) => candidate.textContent), { q: quote }, opts);
|
|
90
|
+
if (!hit) return null;
|
|
91
|
+
let element = candidates[hit.at];
|
|
92
|
+
while (element && element.parentElement !== level) element = element.parentElement;
|
|
93
|
+
return element || null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// +source+'s turn: its siblings after it up to the next turn's opening
|
|
97
|
+
// (or a gap divider before it).
|
|
98
|
+
function turnElements(source) {
|
|
99
|
+
const elements = [];
|
|
100
|
+
for (let next = source.nextElementSibling; next; next = next.nextElementSibling) {
|
|
101
|
+
if (next.matches("[data-turn-id]") || next.querySelector("[data-turn-id]")) break;
|
|
102
|
+
elements.push(next);
|
|
103
|
+
}
|
|
104
|
+
while (elements.at(-1)?.classList.contains("time-gap")) elements.pop();
|
|
105
|
+
return elements;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function turnEnd(source) {
|
|
109
|
+
return turnElements(source).at(-1) || source;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// +element+, or the last of the verdict cards right after it.
|
|
113
|
+
function afterCards(element) {
|
|
114
|
+
let last = element;
|
|
115
|
+
while (last.nextElementSibling?.classList.contains("verify-verdict")) last = last.nextElementSibling;
|
|
116
|
+
return last;
|
|
117
|
+
}
|
|
118
|
+
|
|
75
119
|
export function nameVerdictCards(root, nameFor) {
|
|
76
120
|
for (const card of root?.querySelectorAll?.(".verify-verdict[data-author]") || []) {
|
|
77
121
|
const name = nameFor(card.dataset.author);
|
|
@@ -51,7 +51,8 @@ export function redrawFresh(root, fresh = {}) {
|
|
|
51
51
|
|
|
52
52
|
// The card's Verify fresh part, drawn again from +fresh+: the fresh
|
|
53
53
|
// verdicts, the running check, else the button (an unclear verdict, and a
|
|
54
|
-
// viewer who could run a turn now
|
|
54
|
+
// viewer who could run a turn now; "Verify fresh again" after a fresh
|
|
55
|
+
// verdict, each press another paid check).
|
|
55
56
|
export function renderFresh(card, fresh = {}) {
|
|
56
57
|
const doc = card.ownerDocument;
|
|
57
58
|
const of = card.dataset.checkTurnId;
|
|
@@ -68,7 +69,10 @@ export function renderFresh(card, fresh = {}) {
|
|
|
68
69
|
return link;
|
|
69
70
|
};
|
|
70
71
|
|
|
71
|
-
|
|
72
|
+
const results = (fresh.verdicts || []).filter((r) => r?.fresh && r.of === of);
|
|
73
|
+
// Once a fresh verdict is in, the button says a press runs another one.
|
|
74
|
+
const label = results.length ? "Verify fresh again" : "Verify fresh";
|
|
75
|
+
for (const result of results) {
|
|
72
76
|
const line = doc.createElement("p");
|
|
73
77
|
line.className = `verify-fresh-result ${ICONS[result.verdict] ? result.verdict : ""}`.trim();
|
|
74
78
|
line.textContent = `fresh: ${ICONS[result.verdict] || ""} ${result.verdict} — ${result.reason || ""}`;
|
|
@@ -89,7 +93,7 @@ export function renderFresh(card, fresh = {}) {
|
|
|
89
93
|
const button = doc.createElement("button");
|
|
90
94
|
button.type = "button";
|
|
91
95
|
button.className = "verify-fresh-btn";
|
|
92
|
-
button.textContent =
|
|
96
|
+
button.textContent = label;
|
|
93
97
|
button.title = fresh.unavailableReason;
|
|
94
98
|
button.disabled = true;
|
|
95
99
|
const note = doc.createElement("p");
|
|
@@ -100,7 +104,7 @@ export function renderFresh(card, fresh = {}) {
|
|
|
100
104
|
const button = doc.createElement("button");
|
|
101
105
|
button.type = "button";
|
|
102
106
|
button.className = "verify-fresh-btn";
|
|
103
|
-
button.textContent =
|
|
107
|
+
button.textContent = label;
|
|
104
108
|
button.title = "A new agent checks this claim against the files, with none of this session's context";
|
|
105
109
|
button.addEventListener("click", () => startFresh(card, button, fresh));
|
|
106
110
|
part.append(button);
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Where the Verify preset is offered (verify_preset.js), on both pages: only
|
|
2
|
+
// on text in an ended turn's final answer, the one place the server takes a
|
|
3
|
+
// claim from. Engine#verified_anchor records a Verify turn's card only when
|
|
4
|
+
// its quote is in the last answer of the source turn (AnswerTail.answer?:
|
|
5
|
+
// the turn's last assistant message with text, from its prompt up to the
|
|
6
|
+
// next prompt, merged lines and steers inside); anything else (an earlier
|
|
7
|
+
// reply, a step, a person's line, a reply still running) is refused with
|
|
8
|
+
// `quote_missing` and the turn runs as a plain comment, no card. So the
|
|
9
|
+
// page offers Verify where the server will accept it, and nowhere else.
|
|
10
|
+
//
|
|
11
|
+
// The turn is the page's own grouping (claim_link.js turnIdOf,
|
|
12
|
+
// verdict_card.js turnElements): the last turn opening ([data-turn-id]) at
|
|
13
|
+
// or before the selection, and its elements up to the next opening. Its
|
|
14
|
+
// final answer is the last of them matching the page's answer selector.
|
|
15
|
+
//
|
|
16
|
+
// Pure DOM reads (Element, compareDocumentPosition), so it runs under
|
|
17
|
+
// `node --test` with happy-dom. A module of its own: static JS is cached at
|
|
18
|
+
// unversioned URLs, and a new export on an old module would blank the page.
|
|
19
|
+
|
|
20
|
+
const PRECEDING = 2;
|
|
21
|
+
|
|
22
|
+
// The owner's page: an answer bubble (not a delegate report's reply, which
|
|
23
|
+
// a turn shows before its answer); a context note ends a turn's elements
|
|
24
|
+
// (a wake turn starts with one, and its bubble carries no turn id).
|
|
25
|
+
export const OWNER_FINAL = Object.freeze({ answers: ".bubble.output:not(.report-reply)", boundary: ".bubble.note" });
|
|
26
|
+
// The participant page: a reply's bubble.
|
|
27
|
+
export const PARTICIPANT_FINAL = Object.freeze({ answers: ".p-msg.reply > .p-bubble", boundary: null });
|
|
28
|
+
|
|
29
|
+
// The turn opening +node+ is in (the last [data-turn-id] at or before it in
|
|
30
|
+
// +container+) and whether it is the newest one: {anchor, last}, or null.
|
|
31
|
+
function openingOf(container, node) {
|
|
32
|
+
const anchors = [...container.querySelectorAll("[data-turn-id]")];
|
|
33
|
+
let at = -1;
|
|
34
|
+
anchors.forEach((anchor, i) => {
|
|
35
|
+
if (anchor === node || anchor.contains(node) || (node.compareDocumentPosition(anchor) & PRECEDING)) at = i;
|
|
36
|
+
});
|
|
37
|
+
return at < 0 ? null : { anchor: anchors[at], last: at === anchors.length - 1 };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// The final answer of the turn whose opening is +anchor+: the last element
|
|
41
|
+
// matching +answers+ among the opening's siblings after it, up to the next
|
|
42
|
+
// opening or a +boundary+ element. null when it has none.
|
|
43
|
+
function finalAnswer(container, anchor, { answers, boundary }) {
|
|
44
|
+
let top = anchor;
|
|
45
|
+
while (top && top.parentElement !== container) top = top.parentElement;
|
|
46
|
+
if (!top) return null;
|
|
47
|
+
let final = null;
|
|
48
|
+
for (let el = top.nextElementSibling; el; el = el.nextElementSibling) {
|
|
49
|
+
if (el.matches("[data-turn-id]") || el.querySelector("[data-turn-id]")) break;
|
|
50
|
+
if (boundary && el.matches(boundary)) break;
|
|
51
|
+
const inside = [...el.querySelectorAll(answers)];
|
|
52
|
+
if (inside.length) final = inside[inside.length - 1];
|
|
53
|
+
else if (el.matches(answers)) final = el;
|
|
54
|
+
}
|
|
55
|
+
return final;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// The turn id Verify anchors to for a selection in +root+ (the annotate
|
|
59
|
+
// source's root) in +container+, or null where Verify isn't offered: not in
|
|
60
|
+
// a turn, not in its final answer, or in the running turn. +rule+:
|
|
61
|
+
// OWNER_FINAL or PARTICIPANT_FINAL. +running+: null when no turn runs, else
|
|
62
|
+
// {turnId} (the running turn's id; null when unknown, then the newest turn
|
|
63
|
+
// counts as the running one) — a running turn's answer isn't final yet.
|
|
64
|
+
export function verifyTurnOf(container, root, rule, { running = null } = {}) {
|
|
65
|
+
if (!container || !root || !container.contains(root)) return null;
|
|
66
|
+
const opening = openingOf(container, root);
|
|
67
|
+
if (!opening) return null;
|
|
68
|
+
const turnId = opening.anchor.dataset.turnId || null;
|
|
69
|
+
if (!turnId) return null;
|
|
70
|
+
if (running && (opening.last || (running.turnId && running.turnId === turnId))) return null;
|
|
71
|
+
const final = finalAnswer(container, opening.anchor, rule);
|
|
72
|
+
return final && (final === root || final.contains(root)) ? turnId : null;
|
|
73
|
+
}
|
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.131.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dmitry Dedov
|
|
@@ -893,6 +893,7 @@ files:
|
|
|
893
893
|
- lib/samagotchi/web/public/verdict_fresh.js
|
|
894
894
|
- lib/samagotchi/web/public/verify_card.css
|
|
895
895
|
- lib/samagotchi/web/public/verify_preset.js
|
|
896
|
+
- lib/samagotchi/web/public/verify_target.js
|
|
896
897
|
- lib/samagotchi/web/public/working_set.js
|
|
897
898
|
- lib/samagotchi/web/public/working_set_rail.js
|
|
898
899
|
- lib/samagotchi/web/public/working_set_view.js
|