samagotchi 0.127.0 → 0.128.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 +25 -1
- data/docs/architecture.md +3 -0
- data/docs/guardrails.md +5 -0
- data/docs/internals/participants.md +6 -2
- data/docs/testing.md +3 -2
- data/lib/samagotchi/bridge.rb +2 -0
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/engine.rb +56 -0
- data/lib/samagotchi/guardrails/read_only_shell.rb +48 -1
- data/lib/samagotchi/session_manager.rb +5 -0
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +9 -5
- data/lib/samagotchi/terminal_ui/attached_loop/queueing.rb +6 -12
- data/lib/samagotchi/terminal_ui/attached_loop/sending.rb +23 -35
- data/lib/samagotchi/terminal_ui/attached_loop.rb +10 -18
- data/lib/samagotchi/terminal_ui/own_turns.rb +81 -0
- data/lib/samagotchi/verify_anchor.rb +8 -0
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/participant_review.rb +4 -2
- data/lib/samagotchi/web/public/annotate_bar.js +14 -2
- data/lib/samagotchi/web/public/app.js +17 -4
- data/lib/samagotchi/web/public/composer.css +1 -0
- data/lib/samagotchi/web/public/notify.js +2 -1
- data/lib/samagotchi/web/public/participant.html +3 -2
- data/lib/samagotchi/web/public/participant.js +16 -7
- data/lib/samagotchi/web/public/participant_model.js +3 -2
- data/lib/samagotchi/web/public/participant_names.js +2 -2
- data/lib/samagotchi/web/public/question_card.js +4 -1
- data/lib/samagotchi/web/public/session_state.js +2 -0
- data/lib/samagotchi/web/public/stream_handlers.js +4 -2
- data/lib/samagotchi/web/public/verdict_card.js +18 -3
- data/lib/samagotchi/web/public/verdict_fresh.js +11 -0
- data/lib/samagotchi/web/public/verify_card.css +1 -0
- data/lib/samagotchi/web/public/verify_preset.js +6 -0
- data/lib/samagotchi/web/verify_fresh_routes.rb +12 -7
- 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: 9b175cb8468dd1b6dccbb74c3c372b0796aa18126b47034d07d11d6ead1e1e22
|
|
4
|
+
data.tar.gz: e6b6ea98bf78a7a43962a70ab6daf3cd35519c7db1cefdae44ad8021d4274b75
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: adcfe4e020e6829703e1b9e1d2836d53bb3ddc336da1001c598853352eef75a3ca3df25b7e4d07d292bdaf8b413be228e57487a8b570365e2fba4cecd98490f0
|
|
7
|
+
data.tar.gz: d4b57b741cd271740cb0826e0ff50845a5213c1de30b28c9477ba1b126e5448511f5b73861a3380ae2a93e4102e41df9d4093b14a193a61dcbdd7ed6894a11cd
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,29 @@ 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.128.0] - 2026-10-28
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- Verify needs a worker that knows Verify turns: on an older worker chi web disables Verify and Verify fresh with
|
|
15
|
+
"restart this session to use Verify", and refuses anchored turns and fresh checks sent to it.
|
|
16
|
+
- `sqlite3 -readonly -safe …` (only with both flags and read/display options) and `gh`'s reading subcommands
|
|
17
|
+
(`run`/`pr`/`issue` list and view, `pr diff`/`checks`, `repo view`; never `--web`, `api` or `--watch`) count as
|
|
18
|
+
read-only shell commands, so they no longer ask.
|
|
19
|
+
- The shared composer card footer has less vertical padding on phones.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- A Verify turn records a verdict only when its quote is in the reply of the turn it points at; otherwise it runs
|
|
24
|
+
as a plain turn.
|
|
25
|
+
- A verdict's View check opens the earlier session that holds the check (after a continue), or says the check is
|
|
26
|
+
no longer on the page, instead of doing nothing.
|
|
27
|
+
- An attached piped prompt that this terminal removed before it ran says so on stderr (still exit 1) instead of
|
|
28
|
+
"the model gave an empty answer".
|
|
29
|
+
- A participant's key hint wraps to a second line in a narrow comment box instead of being cut after one.
|
|
30
|
+
|
|
31
|
+
Update with `chi update`.
|
|
32
|
+
|
|
10
33
|
## [0.127.0] - 2026-10-28
|
|
11
34
|
|
|
12
35
|
### Added
|
|
@@ -4787,7 +4810,8 @@ and long-lived sessions.
|
|
|
4787
4810
|
- A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
|
|
4788
4811
|
hotkey panel that send selected text or the clipboard to your sessions.
|
|
4789
4812
|
|
|
4790
|
-
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.
|
|
4813
|
+
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.128.0...HEAD
|
|
4814
|
+
[0.128.0]: https://github.com/dm1try/samagotchi/compare/v0.127.0...v0.128.0
|
|
4791
4815
|
[0.127.0]: https://github.com/dm1try/samagotchi/compare/v0.126.0...v0.127.0
|
|
4792
4816
|
[0.126.0]: https://github.com/dm1try/samagotchi/compare/v0.125.0...v0.126.0
|
|
4793
4817
|
[0.125.0]: https://github.com/dm1try/samagotchi/compare/v0.124.0...v0.125.0
|
data/docs/architecture.md
CHANGED
|
@@ -173,6 +173,9 @@ agent logic and can be used without any terminal rendering; the UI is a thin lay
|
|
|
173
173
|
but `RelineSeam` (prepended to `Reline::LineEditor`) sends its drawing to the
|
|
174
174
|
`Screen`. The saved history and cards in a join or resync are rendered by
|
|
175
175
|
`TerminalUI::JoinReplay` with the loop's screen, view, link, gap clock and renderer.
|
|
176
|
+
With input from a pipe (`chi -p X </dev/null`), `TerminalUI::OwnTurns` keeps this
|
|
177
|
+
run's own prompts until their turns end; `#run` distinguishes an empty answer from a
|
|
178
|
+
prompt this terminal removed before it ran (both exit 1), as well as chi's other outcomes.
|
|
176
179
|
Without a capable terminal it falls back to `PlainSurface` (append-only).
|
|
177
180
|
|
|
178
181
|
#### Using the core
|
data/docs/guardrails.md
CHANGED
|
@@ -557,6 +557,11 @@ every command in it (split at `;`, `&&`, `||`, `|`) is `ls`, `cat`, `head`,
|
|
|
557
557
|
writes or runs something (`sed -i`, `sed 's/a/b/w f'`, `find -delete`/`-exec`,
|
|
558
558
|
`rg --pre`, `sort -o`, `awk` with `>`/`|`/`system(`, `git -c`, `git diff
|
|
559
559
|
--ext-diff`, …) and no redirection other than to `/dev/null` or another fd.
|
|
560
|
+
`sqlite3` is read-only only with both `-readonly` and `-safe`; options are
|
|
561
|
+
checked even after the database name, and only its read/display options are
|
|
562
|
+
allowed (unknown options and dot-command arguments are rejected). `gh` allows
|
|
563
|
+
only `run list|view`, `pr list|view|diff|checks`, `issue list|view` and
|
|
564
|
+
`repo view`; `--web`/`-w`, `gh api` and `pr checks --watch` are not read-only.
|
|
560
565
|
`$(…)`, backticks, `$VAR` (other than `$HOME`, `$XDG_*`, `$TMPDIR` and `$PWD`
|
|
561
566
|
at a word's start), an env prefix, `sh -c`, `xargs`, `eval`, `sudo` and the
|
|
562
567
|
like make a command not read-only. It errs towards asking.
|
|
@@ -362,7 +362,7 @@ fields: a field added to an event later reaches nobody until it is named there.
|
|
|
362
362
|
owner's participant view has none. The page draws it as one line in the comment box card's footer
|
|
363
363
|
(`.composer-card-foot`, `usageItems`): "$0.42 of
|
|
364
364
|
$2.00 cap · 1/3 queued · link: 6 d left · model", each with a title in words; warn colours from 80% of the cap, a
|
|
365
|
-
full queue and a link's last day, "over" at the cap. The key hint is the line above the text field (hidden on a
|
|
365
|
+
full queue and a link's last day, "over" at the cap. The key hint is the line above the text field, wrapping to at most two (hidden on a
|
|
366
366
|
phone). The usage is cut at the end (the model first); folded, it stays as the card's footer under the grip. It is redrawn at each re-read (a
|
|
367
367
|
turn's end, a queued or taken-back comment), not while a turn streams.
|
|
368
368
|
- **A fork's note.** A review fork (`chi review --from`, `/coordinate review`) starts with a note holding the source's
|
|
@@ -420,7 +420,11 @@ fields: a field added to an event later reaches nobody until it is named there.
|
|
|
420
420
|
- **Verify verdict cards** (`verdict_card.js`). The built-in Verify preset carries the selected source turn id and quote
|
|
421
421
|
with the submitted turn, without changing its prompt text. A Verify turn that fails fast and is rolled back hands
|
|
422
422
|
the claim back with its prompt (`prompt_restored`'s `verify`), so the owner's resend from the composer is a Verify
|
|
423
|
-
turn again.
|
|
423
|
+
turn again. Before the Engine stamps the anchor or records a verdict, it checks that the source turn exists and
|
|
424
|
+
its assistant reply contains the quote. Matching is case-sensitive, collapses whitespace, and strips common
|
|
425
|
+
Markdown presentation (links, headings, list/blockquote prefixes, emphasis and code delimiters) from both sides;
|
|
426
|
+
this matches the rendered text the page selects without treating different casing as the same claim. A fresh child
|
|
427
|
+
checks the same anchor against its parent's history. A completed Verify reply whose final non-empty line has
|
|
424
428
|
a valid `Verdict: confirmed|refuted|unclear — <reason>` appends a verdict to the session. Both pages place its card
|
|
425
429
|
after the source turn, name its author, show the reason and quote, and link to the checking turn; a live completion
|
|
426
430
|
adds the card without a reload. The participant view receives only the card fields through its allowlist, with the
|
data/docs/testing.md
CHANGED
|
@@ -255,14 +255,15 @@ well formed (unique ids, known keys and labels, a rule that exists in its set).
|
|
|
255
255
|
## While you work: affected specs
|
|
256
256
|
|
|
257
257
|
```sh
|
|
258
|
-
script/affected-specs # the specs for this branch's change, best first, at most
|
|
258
|
+
script/affected-specs # the specs for this branch's change, best first, at most 16
|
|
259
259
|
bundle exec rspec $(script/affected-specs)
|
|
260
260
|
script/affected-specs --why # with the reason for each pick; --all: no cap; --run: runs them
|
|
261
261
|
script/affected-specs A..B # another range (A...B: from their merge-base); a single REV: vs the working tree
|
|
262
262
|
```
|
|
263
263
|
|
|
264
264
|
A builder runs these while working, not the full suite: the specs the change touched, the specs named after the
|
|
265
|
-
files it changed (`lib/samagotchi/foo/bar.rb` → `spec/foo/bar_spec.rb`, `bar_*_spec.rb
|
|
265
|
+
files it changed (`lib/samagotchi/foo/bar.rb` → `spec/foo/bar_spec.rb`, `bar_*_spec.rb`…;
|
|
266
|
+
`lib/samagotchi/foo/bar/baz.rb` → `spec/foo/bar_baz_spec.rb`), the specs that read
|
|
266
267
|
docs or source as text (its `RULES`: add one when a full run catches what the names missed) and the golden of a changed
|
|
267
268
|
file's layer (any `terminal_ui/*.rb` → `spec/terminal_ui_golden_spec.rb`; `turn_flow.rb` and `turn_note.rb`, whose
|
|
268
269
|
words the REPL prints, pick it by a rule). Changes to `tool_declarations.rb` and `system_prompt.rb` also pick the
|
data/lib/samagotchi/bridge.rb
CHANGED
|
@@ -11,6 +11,7 @@ require_relative "atomic_file"
|
|
|
11
11
|
require_relative "bridge_secret"
|
|
12
12
|
require_relative "delivery"
|
|
13
13
|
require_relative "session_inbox"
|
|
14
|
+
require_relative "verify_anchor"
|
|
14
15
|
|
|
15
16
|
require_relative "bridge/bounded_queue"
|
|
16
17
|
require_relative "bridge/card_store"
|
|
@@ -633,6 +634,7 @@ module Samagotchi
|
|
|
633
634
|
# turn (Guardrails::PairBoundary); a worker without it runs it as a
|
|
634
635
|
# review turn, and chi web says so (Delivery::PAIR_FEATURE).
|
|
635
636
|
FEATURES = ["restart", "task_stop", "reparent", Delivery::QUEUE_FEATURE, ReplyWait::TURN_IDS_FEATURE,
|
|
637
|
+
VerifyAnchor::FEATURE,
|
|
636
638
|
SessionInbox::UNQUEUE_FEATURE, Delivery::REVIEW_FEATURE, Delivery::PAIR_FEATURE].freeze
|
|
637
639
|
|
|
638
640
|
# The routes, by method and what follows /session/:id/: a handler takes
|
data/lib/samagotchi/engine.rb
CHANGED
|
@@ -1620,6 +1620,61 @@ module Samagotchi
|
|
|
1620
1620
|
end
|
|
1621
1621
|
private :stamp_verify_anchor
|
|
1622
1622
|
|
|
1623
|
+
def verified_anchor(session, anchor)
|
|
1624
|
+
return unless anchor
|
|
1625
|
+
|
|
1626
|
+
source = verify_anchor_source_session(session, anchor)
|
|
1627
|
+
return unless source
|
|
1628
|
+
|
|
1629
|
+
turn = AnswerTail.turn_slice(Array(source.messages), anchor.turn_id)
|
|
1630
|
+
return reject_verify_anchor(anchor, :turn_missing) unless turn
|
|
1631
|
+
|
|
1632
|
+
reply = turn.reverse.find { |message| AnswerTail.answer?(message) }
|
|
1633
|
+
return reject_verify_anchor(anchor, :reply_missing) unless reply
|
|
1634
|
+
|
|
1635
|
+
quote_text = verify_comparison_text(anchor.quote)
|
|
1636
|
+
answer_text = verify_comparison_text(reply[:content] || reply["content"])
|
|
1637
|
+
return reject_verify_anchor(anchor, :quote_missing) if quote_text.empty? || !answer_text.include?(quote_text)
|
|
1638
|
+
|
|
1639
|
+
anchor
|
|
1640
|
+
end
|
|
1641
|
+
private :verified_anchor
|
|
1642
|
+
|
|
1643
|
+
def verify_anchor_source_session(session, anchor)
|
|
1644
|
+
return session unless session.verify_fresh
|
|
1645
|
+
|
|
1646
|
+
fresh = session.verify_fresh
|
|
1647
|
+
return reject_verify_anchor(anchor, :fresh_anchor_mismatch) unless fresh.anchor == anchor
|
|
1648
|
+
return reject_verify_anchor(anchor, :fresh_parent_missing) unless session.parent_id
|
|
1649
|
+
|
|
1650
|
+
Session.load(session.parent_id, state_dir: session_state_dir)
|
|
1651
|
+
rescue ArgumentError, SystemCallError, IOError => e
|
|
1652
|
+
Log.warn(:turn, "verify_anchor_rejected", turn_id: anchor.turn_id, reason: :source_session_unavailable,
|
|
1653
|
+
error: e.class.name)
|
|
1654
|
+
nil
|
|
1655
|
+
end
|
|
1656
|
+
private :verify_anchor_source_session
|
|
1657
|
+
|
|
1658
|
+
def verify_comparison_text(text)
|
|
1659
|
+
OutputFormatter.strip_markup(text)
|
|
1660
|
+
.gsub(/!\[([^\]]*)\]\([^)]+\)/, '\1')
|
|
1661
|
+
.gsub(/\[([^\]]+)\]\([^)]+\)/, '\1')
|
|
1662
|
+
.gsub(/^[ \t]*(?:\#{1,6}[ \t]+|>[ \t]?|[-+*][ \t]+|\d+[.)][ \t]+)/, "")
|
|
1663
|
+
.gsub(/(\*\*|__|~~|`+)(.+?)\1/m, '\2')
|
|
1664
|
+
.gsub(/(?<!\w)\*(\S(?:.*?\S)?)\*(?!\w)/m, '\1')
|
|
1665
|
+
.gsub(/(?<!\w)_(\S(?:.*?\S)?)_(?!\w)/m, '\1')
|
|
1666
|
+
.tr("|", " ") # a table row's cells; the page copies them without the pipes
|
|
1667
|
+
.gsub(/\s+/, " ")
|
|
1668
|
+
.strip
|
|
1669
|
+
end
|
|
1670
|
+
private :verify_comparison_text
|
|
1671
|
+
|
|
1672
|
+
def reject_verify_anchor(anchor, reason)
|
|
1673
|
+
Log.warn(:turn, "verify_anchor_rejected", turn_id: anchor.turn_id, reason:)
|
|
1674
|
+
nil
|
|
1675
|
+
end
|
|
1676
|
+
private :reject_verify_anchor
|
|
1677
|
+
|
|
1623
1678
|
def record_verdict(turn, outcome)
|
|
1624
1679
|
return unless outcome == "completed" && turn.verify
|
|
1625
1680
|
|
|
@@ -2170,6 +2225,7 @@ module Samagotchi
|
|
|
2170
2225
|
# A participant's Verify fresh child: every turn is theirs (their
|
|
2171
2226
|
# author, boundary and cap), whoever started it.
|
|
2172
2227
|
origin = session.verify_fresh.origin_for(origin) if session.respond_to?(:verify_fresh) && session.verify_fresh
|
|
2228
|
+
verify = verified_anchor(session, VerifyAnchor.from(verify))
|
|
2173
2229
|
# Track the active session for recap and status snapshot.
|
|
2174
2230
|
@session = session
|
|
2175
2231
|
# status is turn state: running now, idle again before the turn's end
|
|
@@ -130,6 +130,53 @@ module Samagotchi
|
|
|
130
130
|
# uniq IN OUT writes OUT.
|
|
131
131
|
def uniq(args) = args.reject { |a| a.start_with?("-") }.size <= 1
|
|
132
132
|
|
|
133
|
+
SQLITE_OPTIONS = %w[-readonly -safe -json -csv -header -noheader -line -column -box -list -separator -bail
|
|
134
|
+
-batch -noinit].freeze
|
|
135
|
+
|
|
136
|
+
# sqlite3 can still write or run programs unless both safety flags are
|
|
137
|
+
# present. Its options may follow the database name, so check every word.
|
|
138
|
+
def sqlite3(args)
|
|
139
|
+
options = []
|
|
140
|
+
positional = []
|
|
141
|
+
index = 0
|
|
142
|
+
while index < args.length
|
|
143
|
+
arg = args[index]
|
|
144
|
+
if arg == "-separator"
|
|
145
|
+
return false if index + 1 == args.length
|
|
146
|
+
|
|
147
|
+
index += 2
|
|
148
|
+
elsif SQLITE_OPTIONS.include?(arg)
|
|
149
|
+
options << arg
|
|
150
|
+
index += 1
|
|
151
|
+
elsif arg.start_with?("-", ".")
|
|
152
|
+
return false
|
|
153
|
+
else
|
|
154
|
+
positional << arg
|
|
155
|
+
index += 1
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
options.include?("-readonly") && options.include?("-safe") && positional.size >= 2
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
GH_READS = {
|
|
163
|
+
"run" => %w[list view],
|
|
164
|
+
"pr" => %w[list view diff checks],
|
|
165
|
+
"issue" => %w[list view],
|
|
166
|
+
"repo" => %w[view]
|
|
167
|
+
}.freeze
|
|
168
|
+
|
|
169
|
+
def gh(args)
|
|
170
|
+
# -w alone or in a bundle of short flags (-cw) opens a browser.
|
|
171
|
+
return false if args.any? { |arg| arg.match?(/\A-[a-zA-Z]*w/) || arg.start_with?("--web") }
|
|
172
|
+
|
|
173
|
+
group, action = args
|
|
174
|
+
return false unless GH_READS.fetch(group, []).include?(action)
|
|
175
|
+
return false if group == "pr" && action == "checks" && args.any? { |arg| arg.start_with?("--watch") }
|
|
176
|
+
|
|
177
|
+
true
|
|
178
|
+
end
|
|
179
|
+
|
|
133
180
|
SED_FLAGS = /\A-[nErsuz]+\z/
|
|
134
181
|
SED_LONG = %w[--quiet --silent --regexp-extended --separate --unbuffered --null-data --posix --debug
|
|
135
182
|
--sandbox].freeze
|
|
@@ -228,7 +275,7 @@ module Samagotchi
|
|
|
228
275
|
CHECKS = {
|
|
229
276
|
"rg" => method(:rg), "find" => method(:find), "tree" => method(:tree), "file" => method(:file),
|
|
230
277
|
"sort" => method(:sort), "uniq" => method(:uniq), "sed" => method(:sed), "awk" => method(:awk),
|
|
231
|
-
"git" => method(:git)
|
|
278
|
+
"git" => method(:git), "sqlite3" => method(:sqlite3), "gh" => method(:gh)
|
|
232
279
|
}.freeze
|
|
233
280
|
|
|
234
281
|
# A sed script parsed far enough to tell that it only reads: its
|
|
@@ -1408,6 +1408,7 @@ module Samagotchi
|
|
|
1408
1408
|
end
|
|
1409
1409
|
manager.resume_session(session_id, state_dir: state_dir)
|
|
1410
1410
|
if (client = bridge.call)
|
|
1411
|
+
return verify_unavailable if verify && !worker_has?(session_dir, VerifyAnchor::FEATURE)
|
|
1411
1412
|
# A worker from before queueing (no "delivery" feature) ignores the
|
|
1412
1413
|
# field or refuses it; either way a file would steer the running turn.
|
|
1413
1414
|
return queue_unavailable if Delivery.queue?(delivery) && !worker_queues?(session_dir)
|
|
@@ -1533,6 +1534,10 @@ module Samagotchi
|
|
|
1533
1534
|
DeliverResult.new(status: :refused, code: 409, ack: Delivery::REVIEW_UNAVAILABLE)
|
|
1534
1535
|
end
|
|
1535
1536
|
|
|
1537
|
+
private_class_method def self.verify_unavailable
|
|
1538
|
+
DeliverResult.new(status: :refused, code: 409, ack: VerifyAnchor::UNAVAILABLE)
|
|
1539
|
+
end
|
|
1540
|
+
|
|
1536
1541
|
private_class_method def self.turn_timeout
|
|
1537
1542
|
DeliverResult.new(status: :timeout, via: :bridge,
|
|
1538
1543
|
ack: { "error" => "worker_timeout",
|
|
@@ -71,7 +71,8 @@ module Samagotchi
|
|
|
71
71
|
# TUI's streams and requests there (#client_id)
|
|
72
72
|
# @return [Symbol] :detached, :closed when the worker went away,
|
|
73
73
|
# :failed when the --model switch didn't go through, or (input from a
|
|
74
|
-
# pipe) :turn_failed / :empty_answer / :unanswered
|
|
74
|
+
# pipe) :turn_failed / :empty_answer / :removed / :unanswered
|
|
75
|
+
# (AttachedLoop#run)
|
|
75
76
|
def run(attach: nil, shared: false, resume: nil, prompt: nil, model: nil, no_interrupt: false, default_input: true,
|
|
76
77
|
memories: [], muted_memories: [], llm_context: {}, thinking: nil, appended_prompt_files: [], toolkit: nil,
|
|
77
78
|
toolkit_flags: [], settings: nil, parent_answers: nil, state_dir: nil, connector: nil)
|
|
@@ -146,11 +147,14 @@ module Samagotchi
|
|
|
146
147
|
err.flush
|
|
147
148
|
end
|
|
148
149
|
|
|
149
|
-
#
|
|
150
|
-
# line on stderr,
|
|
151
|
-
#
|
|
150
|
+
# An own turn with no answer, or a prompt removed before it ran
|
|
151
|
+
# (input from a pipe): one line on stderr, so a script doesn't take
|
|
152
|
+
# the silence for an answer (exit 1).
|
|
152
153
|
def report_ended(ended, err: $stderr)
|
|
153
|
-
|
|
154
|
+
case ended
|
|
155
|
+
when :empty_answer then err.puts(TerminalUI::EMPTY_ANSWER_ERROR)
|
|
156
|
+
when :removed then err.puts("chi: the prompt was removed before it ran")
|
|
157
|
+
end
|
|
154
158
|
end
|
|
155
159
|
|
|
156
160
|
# chi's exit status after #run: 0 detached, 3 a question left waiting
|
|
@@ -7,8 +7,8 @@ module Samagotchi
|
|
|
7
7
|
class AttachedLoop
|
|
8
8
|
# Queue and unqueue handling for the attached loop.
|
|
9
9
|
# Reads @client, @client_id, @first_command_id, @link, @my_queued,
|
|
10
|
-
# @
|
|
11
|
-
# @my_queued, @
|
|
10
|
+
# @own_turns, @reader, @screen, @shown_enqueued, @wait_at_eof; writes
|
|
11
|
+
# @my_queued, @prefilled, @queued_ids,
|
|
12
12
|
# @shown_enqueued.
|
|
13
13
|
# Calls the loop helpers #author_of, #first_command_flag, #mark_activity,
|
|
14
14
|
# #own?, #prompt_line, #send_prompt, #via, and #worker_down.
|
|
@@ -131,19 +131,13 @@ module Samagotchi
|
|
|
131
131
|
|
|
132
132
|
# A queued prompt was taken back before it ran (the web's ×, /unqueue
|
|
133
133
|
# in a terminal). One of this run's (wait_at_eof) will have no turn:
|
|
134
|
-
# it ends the wait
|
|
135
|
-
# line says so.
|
|
134
|
+
# it ends the wait as :removed. Another client's shown line says so.
|
|
136
135
|
def turn_dequeued(event)
|
|
137
136
|
id = event[:enqueued_id]
|
|
138
137
|
@my_queued.delete(id)
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
@screen.commit("(removed before it ran)")
|
|
143
|
-
else
|
|
144
|
-
@own_failed = true
|
|
145
|
-
@screen.commit("(your queued message was removed before it ran)")
|
|
146
|
-
end
|
|
138
|
+
by_us = own?(event[:client_id])
|
|
139
|
+
if @own_turns.dequeued(id, by_us: by_us)
|
|
140
|
+
@screen.commit(by_us ? "(removed before it ran)" : "(your queued message was removed before it ran)")
|
|
147
141
|
elsif @shown_enqueued.delete?(id)
|
|
148
142
|
@screen.commit("(a queued message was removed)")
|
|
149
143
|
end
|
|
@@ -10,19 +10,16 @@ require_relative "../image_input"
|
|
|
10
10
|
module Samagotchi
|
|
11
11
|
class TerminalUI
|
|
12
12
|
class AttachedLoop
|
|
13
|
-
# Bridge sending
|
|
14
|
-
# Reads @as_parent, @client, @client_id, @
|
|
15
|
-
# @
|
|
16
|
-
# @
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
# #
|
|
23
|
-
# #persist_recent_history, #prompt_line, #put_back_busy_command, #prefill_confirm,
|
|
24
|
-
# #queue_needs_newer_worker, #send_command, #send_prompt, #stale_worker,
|
|
25
|
-
# #steer_cut_note, #too_late?, #via, and #worker_down.
|
|
13
|
+
# Bridge sending and first-command launch.
|
|
14
|
+
# Reads @as_parent, @client, @client_id, @first_command_id, @first_command_line,
|
|
15
|
+
# @first_commands, @first_prompt, @link, @my_queued, @no_interrupt, @own_turns,
|
|
16
|
+
# @queued_ids, @reader, @running, @screen, @status;
|
|
17
|
+
# writes @first_command_id, @first_command_line, @first_commands, @first_prompt,
|
|
18
|
+
# @my_queued, @queued_ids.
|
|
19
|
+
# Calls the loop helpers #command_registry, #default_model_name, #detach,
|
|
20
|
+
# #mark_activity, #older_worker_command, #origin_of, #own?, #paint,
|
|
21
|
+
# #persist_recent_history, #prompt_line, #queue_needs_newer_worker,
|
|
22
|
+
# #stale_worker, #steer_cut_note, #too_late?, #via, and #worker_down.
|
|
26
23
|
module Sending
|
|
27
24
|
private
|
|
28
25
|
|
|
@@ -33,7 +30,7 @@ module Samagotchi
|
|
|
33
30
|
if reply.status == 202
|
|
34
31
|
# Input from a pipe ends once its output is in (#command_ran).
|
|
35
32
|
command_id = reply.json&.fetch("command_id", nil)
|
|
36
|
-
@
|
|
33
|
+
@own_turns.command_sent(command_id) if command_id
|
|
37
34
|
return
|
|
38
35
|
end
|
|
39
36
|
return @screen.commit(stale_worker("run commands")) if reply.status == 404
|
|
@@ -49,7 +46,7 @@ module Samagotchi
|
|
|
49
46
|
end
|
|
50
47
|
|
|
51
48
|
def command_ran(event)
|
|
52
|
-
@
|
|
49
|
+
@own_turns.command_ran(event[:command_id])
|
|
53
50
|
@queued_ids.delete(event[:command_id])
|
|
54
51
|
unless own?(event[:client_id]) || event[:anytime] || event[:card] || event[:queued]
|
|
55
52
|
@screen.commit(prompt_line(event[:client_id], event[:line]))
|
|
@@ -147,8 +144,7 @@ module Samagotchi
|
|
|
147
144
|
reply = @client.post_turn(**options)
|
|
148
145
|
if reply.status == 202
|
|
149
146
|
enqueued_id = reply.json&.fetch("enqueued_id", nil)
|
|
150
|
-
@
|
|
151
|
-
@open_ids << enqueued_id if enqueued_id && @wait_at_eof
|
|
147
|
+
@own_turns.sent(enqueued_id) if enqueued_id
|
|
152
148
|
@my_queued << enqueued_id if enqueued_id && reply.json&.fetch("delivery", nil) == Delivery::QUEUE
|
|
153
149
|
note = sent_note(reply.json, delivery: delivery, images: images, running: running, choice: choice)
|
|
154
150
|
@screen.commit(note) if note
|
|
@@ -172,21 +168,15 @@ module Samagotchi
|
|
|
172
168
|
# Whether the input's end must wait: the -p prompt isn't sent yet, a
|
|
173
169
|
# prompt this run sent hasn't had its turn, or its turn's after_turn
|
|
174
170
|
# hooks still run (their notices, source-links' sources:, come next).
|
|
175
|
-
def own_turns_pending? = !@first_prompt.to_s.strip.empty? ||
|
|
171
|
+
def own_turns_pending? = !@first_prompt.to_s.strip.empty? || @own_turns.pending?
|
|
176
172
|
|
|
177
173
|
# A turn ended: this run's prompt (and ours merged into it) had its turn.
|
|
178
174
|
def own_turn_ended(event)
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
@own_empty = true if ours && event[:type] == :turn_completed && event.dig(:turn_summary, :empty_answer)
|
|
185
|
-
# The worker sends answer_display once its after_turn hooks ran.
|
|
186
|
-
@display_pending = true if ours && event[:type] == :turn_completed && event[:display_pending]
|
|
187
|
-
@open_ids.delete(id)
|
|
188
|
-
@open_ids.subtract(@merged_ids)
|
|
189
|
-
@merged_ids.clear
|
|
175
|
+
completed = event[:type] == :turn_completed
|
|
176
|
+
@own_turns.ended(origin_of(event[:origin]).enqueued_id,
|
|
177
|
+
failed: event[:type] == :turn_failed,
|
|
178
|
+
empty: completed && event.dig(:turn_summary, :empty_answer),
|
|
179
|
+
display_pending: completed && event[:display_pending])
|
|
190
180
|
end
|
|
191
181
|
|
|
192
182
|
# Ours merged into the running turn: they end with it.
|
|
@@ -194,22 +184,20 @@ module Samagotchi
|
|
|
194
184
|
Array(event[:origins]).each do |value|
|
|
195
185
|
origin = origin_of(value)
|
|
196
186
|
id = origin.enqueued_id
|
|
197
|
-
@
|
|
187
|
+
@own_turns.merged(id) if own?(origin.client_id)
|
|
198
188
|
end
|
|
199
189
|
nil
|
|
200
190
|
end
|
|
201
191
|
|
|
202
192
|
def own_prompt_failed
|
|
203
|
-
@
|
|
193
|
+
@own_turns.prompt_failed
|
|
204
194
|
nil
|
|
205
195
|
end
|
|
206
196
|
|
|
207
197
|
# The input ended and nothing of ours is left to wait for.
|
|
208
198
|
def end_of_input
|
|
209
|
-
detach("Detached; the session keeps running. Re-attach with: #{via.attach(@client.session_id)}") if @input_ended
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
@own_empty ? :empty_answer : :detached
|
|
199
|
+
detach("Detached; the session keeps running. Re-attach with: #{via.attach(@client.session_id)}") if @own_turns.input_ended?
|
|
200
|
+
@own_turns.outcome
|
|
213
201
|
end
|
|
214
202
|
|
|
215
203
|
# The prompt's `@path` images, stored where the worker reads them
|
|
@@ -14,6 +14,7 @@ require_relative "image_input"
|
|
|
14
14
|
require_relative "line_reader"
|
|
15
15
|
require_relative "question_prompt"
|
|
16
16
|
require_relative "join_replay"
|
|
17
|
+
require_relative "own_turns"
|
|
17
18
|
require_relative "exit_lines"
|
|
18
19
|
require_relative "attached_loop/exit_commands"
|
|
19
20
|
require_relative "attached_loop/questions"
|
|
@@ -68,8 +69,7 @@ module Samagotchi
|
|
|
68
69
|
attr_reader :client_id
|
|
69
70
|
|
|
70
71
|
# @return [String, nil] the model the worker's turns run on, as the
|
|
71
|
-
# last
|
|
72
|
-
# @return [String, nil] the worker's model
|
|
72
|
+
# last snapshot or model change said (the status row's)
|
|
73
73
|
def model_name = @status[:model]
|
|
74
74
|
|
|
75
75
|
# @return [QuestionPrompt, nil] the question waiting for an answer
|
|
@@ -152,21 +152,12 @@ module Samagotchi
|
|
|
152
152
|
@join_replay = JoinReplay.new(screen: @screen, view: @view, link: @link, gaps: @gaps, renderer: @renderer)
|
|
153
153
|
@last_idle_interrupt_at = nil
|
|
154
154
|
@next_input_prefill = nil
|
|
155
|
-
# Prompts this run sent, by enqueued_id: only those come back into
|
|
156
|
-
# the input when their turn fails (a replayed event must not).
|
|
157
|
-
@sent_ids = Set.new
|
|
158
155
|
# This run's prompts queued for after the running turn (their ack
|
|
159
156
|
# said queue), oldest first, until their turn starts or they are
|
|
160
157
|
# removed: /unqueue takes back the newest.
|
|
161
158
|
@my_queued = []
|
|
162
159
|
@wait_at_eof = wait_at_eof
|
|
163
|
-
|
|
164
|
-
# running turn, whether the input has ended, and whether one of this
|
|
165
|
-
# run's prompts failed (wait_at_eof only).
|
|
166
|
-
@open_ids = Set.new
|
|
167
|
-
@merged_ids = Set.new
|
|
168
|
-
@input_ended = false
|
|
169
|
-
@own_failed = false
|
|
160
|
+
@own_turns = OwnTurns.new(active: wait_at_eof)
|
|
170
161
|
# A continue offer is pending: the prompt asks for the answer.
|
|
171
162
|
@continue_offer = nil
|
|
172
163
|
# The /exit ask is open: the next line is its answer (#answer_exit_ask).
|
|
@@ -188,7 +179,8 @@ module Samagotchi
|
|
|
188
179
|
# :failed when the first command (--model) didn't go through, or
|
|
189
180
|
# (wait_at_eof) :turn_failed when one of this run's prompts failed,
|
|
190
181
|
# :empty_answer when one ended with no answer, and :unanswered when
|
|
191
|
-
# a question came after the input ended
|
|
182
|
+
# a question came after the input ended; :removed when this run took
|
|
183
|
+
# back its only queued prompt before it ran
|
|
192
184
|
# @param input [#call, nil] prompt -> line (nil = Ctrl-D, raising
|
|
193
185
|
# Interrupt = Ctrl-C); defaults to Reline
|
|
194
186
|
def run(input: nil)
|
|
@@ -225,7 +217,7 @@ module Samagotchi
|
|
|
225
217
|
|
|
226
218
|
ended = take_early_lines
|
|
227
219
|
return ended if ended
|
|
228
|
-
return end_of_input if @input_ended && !own_turns_pending?
|
|
220
|
+
return end_of_input if @own_turns.input_ended? && !own_turns_pending?
|
|
229
221
|
when :line
|
|
230
222
|
# A pipe is read at once: its lines wait for the joining
|
|
231
223
|
# snapshot, which may bring the question they answer.
|
|
@@ -256,7 +248,7 @@ module Samagotchi
|
|
|
256
248
|
# has nothing left to answer it.
|
|
257
249
|
return unanswered_question(@question_pending) if @question
|
|
258
250
|
|
|
259
|
-
@
|
|
251
|
+
@own_turns.end_input
|
|
260
252
|
return nil
|
|
261
253
|
end
|
|
262
254
|
end_of_input if submit(line) == :detach
|
|
@@ -303,7 +295,7 @@ module Samagotchi
|
|
|
303
295
|
own_turn_ended(event)
|
|
304
296
|
when :prompt_restored then restore_prompt(event)
|
|
305
297
|
# The after_turn hooks are done (their notices came before it).
|
|
306
|
-
when :answer_display then @
|
|
298
|
+
when :answer_display then @own_turns.displayed
|
|
307
299
|
when :context_status, :used_memories_updated then @status.take_event(event)
|
|
308
300
|
when :participants_changed then participants_changed(event)
|
|
309
301
|
# Another client's anytime command: its line now, before the cards
|
|
@@ -333,7 +325,7 @@ module Samagotchi
|
|
|
333
325
|
refresh_children if Array(event[:origins]).any? { |origin| report_origin?(origin_of(origin)) }
|
|
334
326
|
when :question_requested
|
|
335
327
|
# Nothing is left to answer it with.
|
|
336
|
-
return unanswered_question(event[:pending_question]) if @input_ended
|
|
328
|
+
return unanswered_question(event[:pending_question]) if @own_turns.input_ended?
|
|
337
329
|
|
|
338
330
|
ask(event[:pending_question])
|
|
339
331
|
when :question_answered then question_answered(event)
|
|
@@ -515,7 +507,7 @@ module Samagotchi
|
|
|
515
507
|
# goes back into the input, as the REPL restores it for a retry.
|
|
516
508
|
def restore_prompt(event)
|
|
517
509
|
origin = origin_of(event[:origin])
|
|
518
|
-
return unless own?(origin.client_id) && @
|
|
510
|
+
return unless own?(origin.client_id) && @own_turns.sent?(origin.enqueued_id)
|
|
519
511
|
# Lines from a pipe: nobody to retry it (the REPL's rule).
|
|
520
512
|
return if @wait_at_eof
|
|
521
513
|
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Samagotchi
|
|
4
|
+
class TerminalUI
|
|
5
|
+
# This run's own prompts and commands. Sent prompts are always recorded so
|
|
6
|
+
# only this run's failed prompts can return to the input. With pipe input
|
|
7
|
+
# (+active+), this also tracks turns still open, prompts merged into a
|
|
8
|
+
# running turn, whether input ended, and whether a turn failed, was empty, was removed,
|
|
9
|
+
# or still has an answer display pending. #outcome is what #run returns
|
|
10
|
+
# once those turns finish.
|
|
11
|
+
class OwnTurns
|
|
12
|
+
def initialize(active:)
|
|
13
|
+
@active = active
|
|
14
|
+
@sent = Set.new
|
|
15
|
+
@open = Set.new
|
|
16
|
+
@merged = Set.new
|
|
17
|
+
@input_ended = false
|
|
18
|
+
@failed = false
|
|
19
|
+
@empty = false
|
|
20
|
+
@removed = false
|
|
21
|
+
@display_pending = false
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def sent(enqueued_id)
|
|
25
|
+
@sent << enqueued_id
|
|
26
|
+
@open << enqueued_id if @active
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def sent?(enqueued_id) = @sent.include?(enqueued_id)
|
|
30
|
+
|
|
31
|
+
def command_sent(command_id)
|
|
32
|
+
@open << command_id if @active
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def command_ran(command_id) = @open.delete(command_id)
|
|
36
|
+
|
|
37
|
+
def merged(enqueued_id)
|
|
38
|
+
@merged << enqueued_id if @open.include?(enqueued_id)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def ended(enqueued_id, failed:, empty:, display_pending:)
|
|
42
|
+
return unless @active
|
|
43
|
+
|
|
44
|
+
ours = @open.include?(enqueued_id) || !@merged.empty?
|
|
45
|
+
@failed = true if ours && failed
|
|
46
|
+
@empty = true if ours && empty
|
|
47
|
+
@display_pending = true if ours && display_pending
|
|
48
|
+
@open.delete(enqueued_id)
|
|
49
|
+
@open.subtract(@merged)
|
|
50
|
+
@merged.clear
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def displayed = (@display_pending = false)
|
|
54
|
+
|
|
55
|
+
def prompt_failed
|
|
56
|
+
@failed = true if @active
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def dequeued(enqueued_id, by_us:)
|
|
60
|
+
return false unless @open.delete?(enqueued_id)
|
|
61
|
+
|
|
62
|
+
by_us ? (@removed = true) : (@failed = true)
|
|
63
|
+
true
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def end_input = (@input_ended = true)
|
|
67
|
+
|
|
68
|
+
def input_ended? = @input_ended
|
|
69
|
+
|
|
70
|
+
def pending? = !@open.empty? || @display_pending
|
|
71
|
+
|
|
72
|
+
def outcome
|
|
73
|
+
return :turn_failed if @failed
|
|
74
|
+
return :empty_answer if @empty
|
|
75
|
+
return :removed if @removed
|
|
76
|
+
|
|
77
|
+
:detached
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -25,4 +25,12 @@ module Samagotchi
|
|
|
25
25
|
|
|
26
26
|
def to_file = { "turn_id" => turn_id, "quote" => quote }
|
|
27
27
|
end
|
|
28
|
+
|
|
29
|
+
VerifyAnchor::FEATURE = "verify"
|
|
30
|
+
VerifyAnchor::UNAVAILABLE = {
|
|
31
|
+
"error" => "verify_unavailable",
|
|
32
|
+
"detail" => "this session's worker predates Verify turns; restart this session to use Verify"
|
|
33
|
+
}.freeze
|
|
34
|
+
|
|
35
|
+
def VerifyAnchor.worker_unavailable?(features) = !features.nil? && !features.include?(VerifyAnchor::FEATURE)
|
|
28
36
|
end
|
data/lib/samagotchi/version.rb
CHANGED
|
@@ -51,7 +51,8 @@ module Samagotchi
|
|
|
51
51
|
# current_turn, waiting_for_owner, queued: {mine, others}, last_event_id}
|
|
52
52
|
def handle_review(req, id)
|
|
53
53
|
session = @session_class.load(id, state_dir: default_state_dir)
|
|
54
|
-
|
|
54
|
+
features = ParticipantMode.live_features(session, default_state_dir)
|
|
55
|
+
mode = ParticipantMode.of(session, features: features).to_s
|
|
55
56
|
viewer = participant_viewer(req, session)
|
|
56
57
|
live = bridge_get_json(id, "snapshot")
|
|
57
58
|
turn_snapshot = live && live["snapshot"]
|
|
@@ -67,7 +68,8 @@ module Samagotchi
|
|
|
67
68
|
status = Session::STATUS_IDLE if status == Session::STATUS_RUNNING && current.nil?
|
|
68
69
|
json_response(200, {
|
|
69
70
|
viewer: review_viewer(req, id, session: session, state: state, queued: queued, viewer: viewer, mode: mode),
|
|
70
|
-
session: { id: session.id, title: participant_title(session), status: status
|
|
71
|
+
session: { id: session.id, title: participant_title(session), status: status,
|
|
72
|
+
verify_unavailable: VerifyAnchor.worker_unavailable?(features) },
|
|
71
73
|
subjects: review_subjects(session, viewer),
|
|
72
74
|
participants: participant_names(id).map { |entry| entry.slice(:id, :name, :kind) },
|
|
73
75
|
messages: with_reply_html(ParticipantView.messages(shown, viewer, automatic: automatic)),
|
|
@@ -32,6 +32,7 @@ import { actionEnabled, actionTitle, sameActions, selectionFields, splitInline }
|
|
|
32
32
|
// until its step closes)
|
|
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
|
+
// @param disabledPreset (preset) → a reason to disable it, else null
|
|
35
36
|
// @param actions () => the open session's annotation actions
|
|
36
37
|
// (annotate_actions.js annotationActions), read on every selection
|
|
37
38
|
// @param running () => whether a turn runs (a send pill may wait for it)
|
|
@@ -56,7 +57,8 @@ import { actionEnabled, actionTitle, sameActions, selectionFields, splitInline }
|
|
|
56
57
|
// @return {schedule(), hide(), el}; schedule() re-reads the selection on the
|
|
57
58
|
// next frame (call it on scroll)
|
|
58
59
|
export function createAnnotateBar({
|
|
59
|
-
presets, canAnnotate, roots, isLive, onQuote, onPreset = () => {},
|
|
60
|
+
presets, canAnnotate, roots, isLive, onQuote, onPreset = () => {}, disabledPreset = () => null,
|
|
61
|
+
actions = () => [], running = () => false, onAction = () => {},
|
|
60
62
|
dock = () => null, source: sourceOf = annotationSource, quote = quoteBlock, link = () => null, onLink = () => {},
|
|
61
63
|
doc = globalThis.document, win = globalThis.window,
|
|
62
64
|
}) {
|
|
@@ -80,6 +82,14 @@ export function createAnnotateBar({
|
|
|
80
82
|
btn.title = text;
|
|
81
83
|
bar.appendChild(btn);
|
|
82
84
|
});
|
|
85
|
+
const presetButtons = [...bar.children].slice(1, presets.length + 1);
|
|
86
|
+
function markPresets() {
|
|
87
|
+
presetButtons.forEach((btn, i) => {
|
|
88
|
+
const reason = disabledPreset(presets[i]);
|
|
89
|
+
btn.disabled = !!reason;
|
|
90
|
+
btn.title = reason || presetParts(presets[i]).text;
|
|
91
|
+
});
|
|
92
|
+
}
|
|
83
93
|
// The plugin pills: a separator, the inline ones, then ⋯ and the list
|
|
84
94
|
// it opens. Redrawn only when the session's actions change.
|
|
85
95
|
const group = doc.createElement("span");
|
|
@@ -175,6 +185,7 @@ export function createAnnotateBar({
|
|
|
175
185
|
linkBtn.title = pendingLink?.title || "";
|
|
176
186
|
const list = actions() || [];
|
|
177
187
|
if (!sameActions(list, drawn)) drawActions(list);
|
|
188
|
+
markPresets();
|
|
178
189
|
markActions();
|
|
179
190
|
bar.hidden = false;
|
|
180
191
|
if (touch?.matches) return placeDocked();
|
|
@@ -248,6 +259,7 @@ export function createAnnotateBar({
|
|
|
248
259
|
return;
|
|
249
260
|
}
|
|
250
261
|
const preset = btn.dataset.index === undefined ? null : presets[Number(btn.dataset.index)];
|
|
262
|
+
if (btn.disabled || (preset && disabledPreset(preset))) return;
|
|
251
263
|
const text = preset ? presetNote(pendingQuote, presetParts(preset).text) : pendingQuote;
|
|
252
264
|
const fields = { ...pendingFields, source: pendingSource, range: pendingRange };
|
|
253
265
|
win.getSelection()?.removeAllRanges();
|
|
@@ -256,7 +268,7 @@ export function createAnnotateBar({
|
|
|
256
268
|
onQuote(text);
|
|
257
269
|
});
|
|
258
270
|
|
|
259
|
-
return { schedule, hide, el: bar };
|
|
271
|
+
return { schedule, hide, refresh: markPresets, el: bar };
|
|
260
272
|
}
|
|
261
273
|
|
|
262
274
|
// A preset's button label and note: a text is both.
|
|
@@ -59,12 +59,12 @@ import { createTaskStopper } from "./task_stop.js";
|
|
|
59
59
|
import { createSystemOneWrongAction } from "./system_one_wrong.js";
|
|
60
60
|
import { appendQuote } from "./annotations.js";
|
|
61
61
|
import { applyComposerText, applyRowTemplate, fetchApply } from "./review_apply.js";
|
|
62
|
-
import { nameVerdictCards } from "./verdict_card.js";
|
|
62
|
+
import { CHECK_NOT_ON_PAGE, nameVerdictCards, ownerCheckHash } from "./verdict_card.js";
|
|
63
63
|
import { freshURL, placeVerdictWithFresh, settleRunning } from "./verdict_fresh.js";
|
|
64
64
|
import { authorBadge, nameUnnamedBadges } from "./participant_names.js";
|
|
65
65
|
import { parsePresets } from "./annotate_presets.js";
|
|
66
66
|
import { createAnnotateBar } from "./annotate_bar.js";
|
|
67
|
-
import { VERIFY_PRESET, withVerify } from "./verify_preset.js";
|
|
67
|
+
import { VERIFY_PRESET, VERIFY_UNAVAILABLE, verifyWorkerUnavailable, withVerify } from "./verify_preset.js";
|
|
68
68
|
import { actionLine, annotationActions, composerWithAction } from "./annotate_actions.js";
|
|
69
69
|
import { copyText, installCopy } from "./copy.js";
|
|
70
70
|
import { createQuestionCards } from "./question_cards.js";
|
|
@@ -235,6 +235,7 @@ const composeHintEl = $("#composeHint");
|
|
|
235
235
|
|
|
236
236
|
let selected = null;
|
|
237
237
|
let pendingVerifyAnchor = null;
|
|
238
|
+
let annotateBar = null;
|
|
238
239
|
// The session the composer's text was last written in ("" the start page,
|
|
239
240
|
// undefined unknown): the composer is one for every session, and continue →
|
|
240
241
|
// keeps only a draft of its own (fillOpener).
|
|
@@ -590,6 +591,7 @@ function followSelectedSession(sess) {
|
|
|
590
591
|
current.owner = sess.owner || null;
|
|
591
592
|
current.workerVersion = sess.worker_version || null;
|
|
592
593
|
current.workerFeatures = Array.isArray(sess.worker_features) ? sess.worker_features : [];
|
|
594
|
+
current.bridgeUp = sess.bridge_up === true;
|
|
593
595
|
const streaming = !!(streamControl && streamControl.live);
|
|
594
596
|
if (!streaming) current.status = sess.status;
|
|
595
597
|
updateInfoBar();
|
|
@@ -1344,6 +1346,11 @@ function placeVerdict(record) {
|
|
|
1344
1346
|
placeVerdictWithFresh(historyEl, record, {
|
|
1345
1347
|
who: badge ? badge.name : "You",
|
|
1346
1348
|
author: record.by || null,
|
|
1349
|
+
onMissingCheck: (missing) => {
|
|
1350
|
+
const hash = ownerCheckHash(allSessions, selected, missing.check_turn_id);
|
|
1351
|
+
if (hash) location.hash = hash;
|
|
1352
|
+
else showToast(CHECK_NOT_ON_PAGE);
|
|
1353
|
+
},
|
|
1347
1354
|
}, verifyFreshState());
|
|
1348
1355
|
}
|
|
1349
1356
|
|
|
@@ -1351,11 +1358,13 @@ function placeVerdict(record) {
|
|
|
1351
1358
|
// always theirs to start; its running check links to the child's session.
|
|
1352
1359
|
function verifyFreshState() {
|
|
1353
1360
|
const id = selected;
|
|
1361
|
+
const unavailable = verifyWorkerUnavailable(current.workerFeatures, current.bridgeUp);
|
|
1354
1362
|
if (!Array.isArray(current.verifyFresh)) current.verifyFresh = [];
|
|
1355
1363
|
return {
|
|
1356
1364
|
verdicts: current.verdicts || [],
|
|
1357
1365
|
running: current.verifyFresh,
|
|
1358
|
-
canStart: !!id,
|
|
1366
|
+
canStart: !!id && !unavailable,
|
|
1367
|
+
unavailableReason: unavailable ? VERIFY_UNAVAILABLE : null,
|
|
1359
1368
|
start: async (of) => {
|
|
1360
1369
|
try {
|
|
1361
1370
|
const body = await api(freshURL(id, of), { method: "POST", body: "{}" });
|
|
@@ -1421,6 +1430,7 @@ async function restartWorker() {
|
|
|
1421
1430
|
// The session bar (info_bar_model.js sessionBar): its buttons, and the
|
|
1422
1431
|
// info line rendered by info_bar.js.
|
|
1423
1432
|
function updateInfoBar() {
|
|
1433
|
+
annotateBar?.refresh();
|
|
1424
1434
|
const { buttons, bar } = sessionBar({
|
|
1425
1435
|
id: selected, state: current, sessions: allSessions, versions, cardTitle: (s) => lists.cardTitle(s),
|
|
1426
1436
|
continuing: sessionActions.continuing, restarting: restartingIds, streaming: !!streamControl, running: liveTurn.running,
|
|
@@ -1574,6 +1584,7 @@ function workerGone(id, { opened = true } = {}) {
|
|
|
1574
1584
|
if (!stopped) setLiveStatus("idle");
|
|
1575
1585
|
}
|
|
1576
1586
|
if (current.owner === "worker") current.owner = null;
|
|
1587
|
+
current.bridgeUp = false;
|
|
1577
1588
|
// Its card says "live" (the list's owner, or setLiveStatus's) until the
|
|
1578
1589
|
// hub's event says otherwise: the worker is gone now, as far as this
|
|
1579
1590
|
// tab can tell.
|
|
@@ -1956,8 +1967,10 @@ function fillComposer(text) {
|
|
|
1956
1967
|
promptEl.scrollTop = promptEl.scrollHeight;
|
|
1957
1968
|
}
|
|
1958
1969
|
|
|
1959
|
-
|
|
1970
|
+
annotateBar = createAnnotateBar({
|
|
1960
1971
|
presets: withVerify(parsePresets(document.body.dataset.annotatePresets)),
|
|
1972
|
+
disabledPreset: (preset) => preset === VERIFY_PRESET
|
|
1973
|
+
&& verifyWorkerUnavailable(current.workerFeatures, current.bridgeUp) ? VERIFY_UNAVAILABLE : null,
|
|
1961
1974
|
canAnnotate: () => !!selected && current.owner !== "tui",
|
|
1962
1975
|
roots: turnRoots,
|
|
1963
1976
|
isLive: (root) => liveTurn.running && turnDom.containsLive(root),
|
|
@@ -53,6 +53,7 @@ body.has-session.composer-folded #composeGrip::after{opacity:1;width:48px;margin
|
|
|
53
53
|
info bar's buttons while folded, the stage card's bottom in the stage view); it reaches 7 px below the edge, the info bar's
|
|
54
54
|
border and padding, so it never covers a button of the bar. */
|
|
55
55
|
@media(max-width:600px){
|
|
56
|
+
.composer-card-foot{padding-top:4px;padding-bottom:4px}
|
|
56
57
|
#composeGrip{width:96px;height:26px;margin:-19px auto -7px}
|
|
57
58
|
#composeGrip::after{top:17px;opacity:1}
|
|
58
59
|
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// title badge for what it returns, while the tab is not in front.
|
|
4
4
|
|
|
5
5
|
import { clip } from "./format.js";
|
|
6
|
+
import { isParticipantAuthor } from "./participant_model.js";
|
|
6
7
|
|
|
7
8
|
// The bell's state. OS notifications need a secure context: on chi web
|
|
8
9
|
// over http at a LAN address the API is missing or always denied, so the
|
|
@@ -164,7 +165,7 @@ export function attentionText(session, attention) {
|
|
|
164
165
|
const seconds = Math.round(Number(session?.last_turn?.seconds) || 0);
|
|
165
166
|
const verdict = session?.last_verdict;
|
|
166
167
|
// An owner's verdict has no participant author (none, "" or "owner").
|
|
167
|
-
const byParticipant =
|
|
168
|
+
const byParticipant = isParticipantAuthor(verdict?.by);
|
|
168
169
|
const who = verdict?.by_name || (byParticipant ? "A participant" : "You");
|
|
169
170
|
const verdictReason = clip(String(verdict?.reason || ""), VERDICT_REASON_MAX);
|
|
170
171
|
const verdictIcon = { confirmed: "✅", refuted: "❌", unclear: "❓" }[verdict?.verdict] || "";
|
|
@@ -122,9 +122,10 @@ main{flex:1;min-height:0;overflow-y:auto;padding:16px 16px 8px}
|
|
|
122
122
|
#composer textarea{font:inherit;line-height:1.45;padding:2px 0}
|
|
123
123
|
#composer textarea:disabled{cursor:not-allowed}
|
|
124
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}
|
|
125
127
|
/* The participant's own usage (participant_model.js usageItems: the fields their grant's exposure shows), one line in
|
|
126
128
|
the card's footer, the end cut. Folded, it stays under the grip. */
|
|
127
|
-
.p-hint.composer-line{color:var(--text-faint);white-space:nowrap;text-overflow:ellipsis}
|
|
128
129
|
.p-foot{min-width:0}
|
|
129
130
|
.p-foot:has(> .p-usage[hidden]){display:none}
|
|
130
131
|
/* Folded before the usage loads, the card keeps a footer's height under its grip, not a 2 px sliver. */
|
|
@@ -151,7 +152,7 @@ button.small{font-size:12px;padding:2px 8px}
|
|
|
151
152
|
.p-people .p-person:nth-child(n+4){display:none}
|
|
152
153
|
main{padding:12px 16px 6px}
|
|
153
154
|
.p-bubble{padding:9px 12px}
|
|
154
|
-
.p-hint,.p-note{display:none}
|
|
155
|
+
.p-hint.composer-line,.p-note{display:none}
|
|
155
156
|
}
|
|
156
157
|
/* Under 16px iOS Safari zooms the page on focus (and a Home Screen app never zooms back). */
|
|
157
158
|
@media (max-width:600px),(pointer:coarse){#composer textarea{font-size:16px}}
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
// (spend vs cap, queue, link expiry, model). The owner's participant view
|
|
13
13
|
// mounts none.
|
|
14
14
|
import {
|
|
15
|
-
authorColor, participantAuthor, authorLabel, toolLine, refusalText, dequeueText, endedText, streamURL, reloads, endsLiveTurn, liveFrom, applyLive,
|
|
15
|
+
authorColor, participantAuthor, isParticipantAuthor, authorLabel, toolLine, refusalText, dequeueText, endedText, streamURL, reloads, endsLiveTurn, liveFrom, applyLive,
|
|
16
16
|
liveText, withToolSteps, pickSubject, subjectBody, subjectsKey, narrowView, retryDelay, retries, threadGaps, usageItems,
|
|
17
17
|
modeCopy,
|
|
18
18
|
} from "./participant_model.js";
|
|
@@ -22,10 +22,11 @@ import { createAuthLost, lostAnswer } from "./auth_lost.js";
|
|
|
22
22
|
import { REPLACE_SESSION_TEXT, fragmentCode, tradeFragmentCode } from "./token_fragment.js";
|
|
23
23
|
import { createAnnotateBar } from "./annotate_bar.js";
|
|
24
24
|
import { parsePresets } from "./annotate_presets.js";
|
|
25
|
-
import { VERIFY_PRESET, withVerify } from "./verify_preset.js";
|
|
25
|
+
import { VERIFY_PRESET, VERIFY_UNAVAILABLE, withVerify } from "./verify_preset.js";
|
|
26
26
|
import { appendQuote } from "./annotations.js";
|
|
27
27
|
import { messageAuthorData, participantQuote, participantSource } from "./participant_annotate.js";
|
|
28
28
|
import { claimFromHash, selectionClaim, showClaim } from "./claim_link.js";
|
|
29
|
+
import { CHECK_NOT_ON_PAGE } from "./verdict_card.js";
|
|
29
30
|
import { freshURL, placeVerdictWithFresh, settleRunning } from "./verdict_fresh.js";
|
|
30
31
|
import { mountComposer } from "./composer_mount.js";
|
|
31
32
|
import { PROMPT_LINE_PX } from "./composer_size.js";
|
|
@@ -234,7 +235,7 @@ function render() {
|
|
|
234
235
|
const copy = modeCopy(viewer.mode);
|
|
235
236
|
$("note").textContent = ownerView ? "" : copy.note;
|
|
236
237
|
$("hint").textContent = `⏎ sends, ⇧⏎ a new line, ↑ an earlier comment. ${copy.hint}`;
|
|
237
|
-
$("hint").title = $("hint").textContent; //
|
|
238
|
+
$("hint").title = $("hint").textContent; // at most two lines in the card: the whole text on hover
|
|
238
239
|
$("head").hidden = false;
|
|
239
240
|
$("main").hidden = false;
|
|
240
241
|
renderPlan();
|
|
@@ -343,7 +344,7 @@ function messageNode(message) {
|
|
|
343
344
|
const who = el("div", "p-who");
|
|
344
345
|
who.append(el("b", "", authorLabel(message)));
|
|
345
346
|
if (message.merged) who.append(el("span", "p-tag", "added mid-turn"));
|
|
346
|
-
if (message.author) who.append(el("span", "p-tag", modeCopy(message.participant_mode).tag));
|
|
347
|
+
if (isParticipantAuthor(message.author)) who.append(el("span", "p-tag", modeCopy(message.participant_mode).tag));
|
|
347
348
|
node.append(who);
|
|
348
349
|
const text = message.content + (message.images ? `\n[${message.images} image${message.images > 1 ? "s" : ""}]` : "");
|
|
349
350
|
node.append(el("div", "p-bubble", text));
|
|
@@ -413,13 +414,17 @@ function renderThread() {
|
|
|
413
414
|
const author = record.by || null;
|
|
414
415
|
const viewer = data.viewer || {};
|
|
415
416
|
let who = ownerView ? "You" : "Owner";
|
|
416
|
-
if (author
|
|
417
|
+
if (isParticipantAuthor(author)) {
|
|
417
418
|
if (!ownerView && author === participantAuthor(viewer.id)) who = "You";
|
|
418
419
|
else {
|
|
419
420
|
who = (data.participants || []).find((participant) => participantAuthor(participant.id) === author)?.name || "A participant";
|
|
420
421
|
}
|
|
421
422
|
}
|
|
422
|
-
placeVerdictWithFresh(thread, record, {
|
|
423
|
+
placeVerdictWithFresh(thread, record, {
|
|
424
|
+
who,
|
|
425
|
+
author,
|
|
426
|
+
onMissingCheck: () => showClaimNote(CHECK_NOT_ON_PAGE),
|
|
427
|
+
}, verifyFreshState());
|
|
423
428
|
}
|
|
424
429
|
if (atBottom || !thread.dataset.drawn) main.scrollTop = main.scrollHeight;
|
|
425
430
|
thread.dataset.drawn = "1";
|
|
@@ -431,10 +436,12 @@ function renderThread() {
|
|
|
431
436
|
// does, and no one here gets a link to the child's session.
|
|
432
437
|
function verifyFreshState() {
|
|
433
438
|
if (!Array.isArray(data.fresh_running)) data.fresh_running = [];
|
|
439
|
+
const unavailable = data.session?.verify_unavailable === true;
|
|
434
440
|
return {
|
|
435
441
|
verdicts: data.verdicts || [],
|
|
436
442
|
running: data.fresh_running,
|
|
437
|
-
canStart: !ownerView && !ended && !!data.viewer?.can_comment,
|
|
443
|
+
canStart: !unavailable && !ownerView && !ended && !!data.viewer?.can_comment,
|
|
444
|
+
unavailableReason: unavailable ? VERIFY_UNAVAILABLE : null,
|
|
438
445
|
start: async (of) => {
|
|
439
446
|
const { ok, body } = await api(freshURL(sessionId, of), {
|
|
440
447
|
method: "POST", body: "{}", headers: { "Content-Type": "application/json" },
|
|
@@ -650,6 +657,8 @@ for (const button of $("viewSeg").querySelectorAll("button")) {
|
|
|
650
657
|
// while this participant can't comment.
|
|
651
658
|
const annotateBar = createAnnotateBar({
|
|
652
659
|
presets: withVerify(parsePresets(body.annotatePresets)),
|
|
660
|
+
disabledPreset: (preset) => preset === VERIFY_PRESET
|
|
661
|
+
&& data?.session?.verify_unavailable === true ? VERIFY_UNAVAILABLE : null,
|
|
653
662
|
canAnnotate: () => !ownerView && !ended && !!data?.viewer?.can_comment,
|
|
654
663
|
roots: () => [$("planDoc"), $("thread")],
|
|
655
664
|
isLive: (root) => !!root.closest?.(".p-msg.live"),
|
|
@@ -9,6 +9,7 @@ import { fullTime, gapText } from "./format.js";
|
|
|
9
9
|
// (Steer.author_for / PARTICIPANT_PREFIX): the one place the web spells it.
|
|
10
10
|
export const PARTICIPANT_PREFIX = "p:";
|
|
11
11
|
export const participantAuthor = (id) => `${PARTICIPANT_PREFIX}${id}`;
|
|
12
|
+
export const isParticipantAuthor = (by) => typeof by === "string" && by.startsWith(PARTICIPANT_PREFIX);
|
|
12
13
|
|
|
13
14
|
// Six author colours (tokens.css --author-1..6), one per participant id,
|
|
14
15
|
// the same on every page that shows them (the owner's badges too).
|
|
@@ -22,13 +23,13 @@ export function authorSlot(author) {
|
|
|
22
23
|
|
|
23
24
|
// The CSS colour of an author: a participant's slot, else the owner's.
|
|
24
25
|
export function authorColor(author) {
|
|
25
|
-
return author ? `var(--author-${authorSlot(author)})` : "var(--author-owner)";
|
|
26
|
+
return isParticipantAuthor(author) ? `var(--author-${authorSlot(author)})` : "var(--author-owner)";
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
// Who wrote a user message or started a turn, as the page names them.
|
|
29
30
|
export function authorLabel(item) {
|
|
30
31
|
if (item?.mine) return "You";
|
|
31
|
-
if (item?.author) return item.author_name || "A participant";
|
|
32
|
+
if (isParticipantAuthor(item?.author)) return item.author_name || "A participant";
|
|
32
33
|
if (item?.automatic) return "Automatic";
|
|
33
34
|
return "Owner";
|
|
34
35
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// prompt's own author_name (a saved message, turn_started) comes first; a
|
|
5
5
|
// badge drawn before the list came, with neither, says UNNAMED until
|
|
6
6
|
// nameUnnamedBadges names it.
|
|
7
|
-
import { authorColor, participantAuthor } from "./participant_model.js";
|
|
7
|
+
import { authorColor, isParticipantAuthor, participantAuthor } from "./participant_model.js";
|
|
8
8
|
|
|
9
9
|
export const UNNAMED = "a participant";
|
|
10
10
|
|
|
@@ -28,7 +28,7 @@ export function participantName(author) {
|
|
|
28
28
|
// +mode+: the turn's participant_mode stamp ("review", "pair"; the
|
|
29
29
|
// message's, or turn_started's), kept as mode: when known.
|
|
30
30
|
export function authorBadge(author, name = null, mode = null) {
|
|
31
|
-
if (!author
|
|
31
|
+
if (!isParticipantAuthor(author)) return null;
|
|
32
32
|
const known = name || participantName(author);
|
|
33
33
|
return { author, name: known || UNNAMED, color: authorColor(author), ...(known ? {} : { unnamed: true }), ...(mode ? { mode } : {}) };
|
|
34
34
|
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// command or paths, where and why. Its options end in "Deny"; the answer's
|
|
4
4
|
// last option denies. A page that doesn't know approvals still shows the
|
|
5
5
|
// plain question text, which says the same.
|
|
6
|
+
import { isParticipantAuthor, PARTICIPANT_PREFIX } from "./participant_model.js";
|
|
6
7
|
|
|
7
8
|
export function isApproval(pq) {
|
|
8
9
|
return !!pq && pq.kind === "approval";
|
|
@@ -68,7 +69,9 @@ export function relayView(pq) {
|
|
|
68
69
|
export function approvalAsker(pq) {
|
|
69
70
|
const a = (pq && pq.approval) || {};
|
|
70
71
|
if (!a.author) return null;
|
|
71
|
-
|
|
72
|
+
const author = String(a.author);
|
|
73
|
+
return a.author_name ? String(a.author_name)
|
|
74
|
+
: isParticipantAuthor(author) ? author.slice(PARTICIPANT_PREFIX.length) : author;
|
|
72
75
|
}
|
|
73
76
|
|
|
74
77
|
// @return {null | {tool, what, isCommand, where, why, paths?, preview?, delegate?, asker?}};
|
|
@@ -53,6 +53,7 @@ export function emptySessionState() {
|
|
|
53
53
|
// worker_version / worker_features): the info bar's stale-worker badge.
|
|
54
54
|
workerVersion: null,
|
|
55
55
|
workerFeatures: [],
|
|
56
|
+
bridgeUp: false,
|
|
56
57
|
// The LLM context strategy, apply rule and budget the next turn runs
|
|
57
58
|
// under, each with its source (the info bar's llm ctx chip), or null.
|
|
58
59
|
llmContext: null,
|
|
@@ -95,6 +96,7 @@ export function sessionStateFrom(data) {
|
|
|
95
96
|
// not at the session's next hub event (followSelectedSession).
|
|
96
97
|
workerVersion: s.worker_version || null,
|
|
97
98
|
workerFeatures: Array.isArray(s.worker_features) ? s.worker_features : [],
|
|
99
|
+
bridgeUp: s.bridge_up === true,
|
|
98
100
|
timing: normalizeTiming(data.timing),
|
|
99
101
|
};
|
|
100
102
|
// The saved fill, so a reloaded or stopped session shows it before its
|
|
@@ -55,6 +55,7 @@ import {
|
|
|
55
55
|
import { commandView, drawCommandBubble, historyHasCommandBubble } from "./commands.js";
|
|
56
56
|
import { keepEarlyRestore, restoreAction } from "./restore.js";
|
|
57
57
|
import { PAIR, setBadgeMode } from "./participant_names.js";
|
|
58
|
+
import { isParticipantAuthor } from "./participant_model.js";
|
|
58
59
|
|
|
59
60
|
export function createStreamHandlers(deps) {
|
|
60
61
|
const {
|
|
@@ -163,12 +164,13 @@ export function createStreamHandlers(deps) {
|
|
|
163
164
|
// A participant's review turn: its id and author, for Apply at its end
|
|
164
165
|
// (turn_completed names neither). A pair turn acted itself: no Apply.
|
|
165
166
|
const author = data.author || data.origin?.author;
|
|
167
|
+
const participant = isParticipantAuthor(author);
|
|
166
168
|
const pair = data.participant_mode === PAIR;
|
|
167
|
-
liveTurn.review =
|
|
169
|
+
liveTurn.review = participant && data.turn_id && !pair ? { turn_id: data.turn_id, author, author_name: data.author_name } : null;
|
|
168
170
|
// Its prompt drawn while queued takes the turn's mode now.
|
|
169
171
|
if (data.participant_mode) setBadgeMode(promptBubble, data.participant_mode);
|
|
170
172
|
// The owner's input queues behind it (the composer's delivery line).
|
|
171
|
-
liveTurn.participantTurn =
|
|
173
|
+
liveTurn.participantTurn = participant;
|
|
172
174
|
setTurnRunning(true);
|
|
173
175
|
setLiveStatus("running");
|
|
174
176
|
liveTurn.timing.detach();
|
|
@@ -1,10 +1,23 @@
|
|
|
1
|
+
import { ownerClaimHash } from "./claim_link.js";
|
|
2
|
+
|
|
1
3
|
const TYPES = Object.freeze({
|
|
2
4
|
confirmed: { icon: "✅", label: "confirmed" },
|
|
3
5
|
refuted: { icon: "❌", label: "refuted" },
|
|
4
6
|
unclear: { icon: "❓", label: "unclear" },
|
|
5
7
|
});
|
|
6
8
|
|
|
7
|
-
export
|
|
9
|
+
export const CHECK_NOT_ON_PAGE = "That check is no longer on this page.";
|
|
10
|
+
|
|
11
|
+
export function ownerCheckHash(sessions, currentSessionId, checkTurnId) {
|
|
12
|
+
if (!Array.isArray(sessions) || typeof checkTurnId !== "string") return null;
|
|
13
|
+
const session = sessions.find((candidate) =>
|
|
14
|
+
candidate?.id !== currentSessionId
|
|
15
|
+
&& Array.isArray(candidate.verdicts)
|
|
16
|
+
&& candidate.verdicts.some((verdict) => verdict?.check_turn_id === checkTurnId));
|
|
17
|
+
return session ? ownerClaimHash(session.id, { turnId: checkTurnId }) : null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function placeVerdict(root, record, { who = "Owner", author = null, onMissingCheck = null } = {}) {
|
|
8
21
|
const anchor = record?.anchor;
|
|
9
22
|
const type = TYPES[record?.verdict];
|
|
10
23
|
if (!root || !type || typeof anchor?.turn_id !== "string" || typeof anchor?.quote !== "string"
|
|
@@ -47,8 +60,10 @@ export function placeVerdict(root, record, { who = "Owner", author = null } = {}
|
|
|
47
60
|
link.textContent = "View check";
|
|
48
61
|
link.addEventListener("click", (event) => {
|
|
49
62
|
event.preventDefault();
|
|
50
|
-
[...root.querySelectorAll("[data-turn-id]")]
|
|
51
|
-
|
|
63
|
+
const check = [...root.querySelectorAll("[data-turn-id]")]
|
|
64
|
+
.find((element) => element.dataset.turnId === record.check_turn_id);
|
|
65
|
+
if (check) check.scrollIntoView?.({ behavior: "smooth", block: "center" });
|
|
66
|
+
else onMissingCheck?.(record);
|
|
52
67
|
});
|
|
53
68
|
card.append(heading, quote, reason, link);
|
|
54
69
|
|
|
@@ -79,6 +79,17 @@ export function renderFresh(card, fresh = {}) {
|
|
|
79
79
|
const link = childLink(typeof run === "string" ? null : run.child_id);
|
|
80
80
|
if (link) line.append(" ", link);
|
|
81
81
|
part.append(line);
|
|
82
|
+
} else if (card.classList.contains("unclear") && fresh.unavailableReason) {
|
|
83
|
+
const button = doc.createElement("button");
|
|
84
|
+
button.type = "button";
|
|
85
|
+
button.className = "verify-fresh-btn";
|
|
86
|
+
button.textContent = "Verify fresh";
|
|
87
|
+
button.title = fresh.unavailableReason;
|
|
88
|
+
button.disabled = true;
|
|
89
|
+
const note = doc.createElement("p");
|
|
90
|
+
note.className = "verify-fresh-unavailable";
|
|
91
|
+
note.textContent = fresh.unavailableReason;
|
|
92
|
+
part.append(button, note);
|
|
82
93
|
} else if (card.classList.contains("unclear") && fresh.canStart && fresh.start) {
|
|
83
94
|
const button = doc.createElement("button");
|
|
84
95
|
button.type = "button";
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
.verify-fresh-result{font-weight:600;color:var(--text-bright)}
|
|
12
12
|
.verify-fresh-running{color:var(--text-muted);font-style:italic}
|
|
13
13
|
.verify-fresh-error{color:var(--danger-text)}
|
|
14
|
+
.verify-fresh-unavailable{color:var(--text-muted)}
|
|
14
15
|
.verify-fresh-link{color:var(--accent);font-weight:400}
|
|
15
16
|
.verify-fresh-btn{font:inherit;font-size:12px;font-weight:600;padding:3px 12px;border-radius:8px;cursor:pointer;background:var(--accent-bg);color:var(--accent);border:1px solid var(--accent-border)}
|
|
16
17
|
.verify-fresh-btn:hover{border-color:var(--accent)}
|
|
@@ -12,6 +12,12 @@ export const VERIFY_PRESET = Object.freeze({
|
|
|
12
12
|
+ "End with a line `Verdict: confirmed|refuted|unclear — <one-line reason>`.",
|
|
13
13
|
});
|
|
14
14
|
|
|
15
|
+
export const VERIFY_UNAVAILABLE = "restart this session to use Verify";
|
|
16
|
+
|
|
17
|
+
export function verifyWorkerUnavailable(workerFeatures, bridgeUp) {
|
|
18
|
+
return bridgeUp === true && Array.isArray(workerFeatures) && !workerFeatures.includes("verify");
|
|
19
|
+
}
|
|
20
|
+
|
|
15
21
|
// +presets+ (annotate_presets.js parsePresets) with Verify after them.
|
|
16
22
|
export function withVerify(presets) {
|
|
17
23
|
return [...(presets || []), VERIFY_PRESET];
|
|
@@ -33,14 +33,18 @@ module Samagotchi
|
|
|
33
33
|
def handle_verify_fresh(req, id, of)
|
|
34
34
|
clicker = principal(req).owner? ? nil : principal(req).grant
|
|
35
35
|
session = @session_class.load(id, state_dir: default_state_dir)
|
|
36
|
-
if clicker && (refused = participant_cap_refusal(clicker) || fresh_worker_refusal(session))
|
|
36
|
+
if clicker && (refused = participant_cap_refusal(clicker) || fresh_worker_refusal(session, participant: true))
|
|
37
37
|
return verify_fresh_refused(clicker, of, *refused)
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
by = Steer.sender_author(field_of(VerifyFresh.original(session.verdicts, of), :by))
|
|
41
41
|
runner = by ? verdict_author_grant(id, by) : clicker
|
|
42
42
|
return author_link_ended(clicker, id, of, by) if by && runner.nil?
|
|
43
|
-
if runner && runner != clicker && (refused = participant_cap_refusal(runner) ||
|
|
43
|
+
if runner && runner != clicker && (refused = participant_cap_refusal(runner) ||
|
|
44
|
+
fresh_worker_refusal(session, participant: true))
|
|
45
|
+
return verify_fresh_refused(clicker, of, *refused, runner: runner)
|
|
46
|
+
end
|
|
47
|
+
if (refused = fresh_worker_refusal(session))
|
|
44
48
|
return verify_fresh_refused(clicker, of, *refused, runner: runner)
|
|
45
49
|
end
|
|
46
50
|
|
|
@@ -76,12 +80,13 @@ module Samagotchi
|
|
|
76
80
|
|
|
77
81
|
def field_of(record, key) = record && (record[key] || record[key.to_s])
|
|
78
82
|
|
|
79
|
-
# A live worker
|
|
80
|
-
#
|
|
81
|
-
|
|
82
|
-
def fresh_worker_refusal(session)
|
|
83
|
+
# A live worker must support VerifyFresh's parent checks. For a
|
|
84
|
+
# participant's child it must also have the review boundary.
|
|
85
|
+
def fresh_worker_refusal(session, participant: false)
|
|
83
86
|
features = ParticipantMode.live_features(session, default_state_dir)
|
|
84
|
-
return nil if features.nil?
|
|
87
|
+
return nil if features.nil?
|
|
88
|
+
return [409, VerifyAnchor::UNAVAILABLE["error"], VerifyAnchor::UNAVAILABLE["detail"]] if VerifyAnchor.worker_unavailable?(features)
|
|
89
|
+
return nil unless participant && !features.include?(Delivery::REVIEW_FEATURE)
|
|
85
90
|
|
|
86
91
|
[409, Delivery::REVIEW_UNAVAILABLE["error"], Delivery::REVIEW_UNAVAILABLE["detail"]]
|
|
87
92
|
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.128.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dmitry Dedov
|
|
@@ -623,6 +623,7 @@ files:
|
|
|
623
623
|
- lib/samagotchi/terminal_ui/local_link.rb
|
|
624
624
|
- lib/samagotchi/terminal_ui/model_segments_text.rb
|
|
625
625
|
- lib/samagotchi/terminal_ui/one_shot_sink.rb
|
|
626
|
+
- lib/samagotchi/terminal_ui/own_turns.rb
|
|
626
627
|
- lib/samagotchi/terminal_ui/participant_commands.rb
|
|
627
628
|
- lib/samagotchi/terminal_ui/plain_surface.rb
|
|
628
629
|
- lib/samagotchi/terminal_ui/question_prompt.rb
|