samagotchi 0.154.1 → 0.155.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 +16 -1
- data/docs/architecture.md +3 -2
- data/docs/bundles.md +21 -0
- data/docs/cli.md +1 -1
- data/docs/guardrails.md +2 -2
- data/docs/hooks.md +1 -1
- data/docs/internals/anthropic.md +4 -4
- data/docs/internals/attached-context.md +8 -6
- data/docs/internals/block-renderers.md +8 -1
- data/docs/internals/engine.md +220 -99
- data/docs/internals/inline-widgets.md +136 -0
- data/docs/internals/objectives.md +8 -6
- data/docs/internals/participants.md +5 -5
- data/docs/internals/prompt-caching.md +9 -5
- data/docs/internals/web-frontend.md +1 -1
- data/docs/internals/working-set.md +2 -1
- data/docs/plugins.md +2 -0
- data/docs/releasing.md +1 -1
- data/docs/sessions.md +2 -2
- data/docs/testing.md +1 -1
- data/lib/samagotchi/block_renderers.rb +5 -0
- data/lib/samagotchi/bundle_command/check.rb +1 -1
- data/lib/samagotchi/bundles/dev/manifest.yml +3 -2
- data/lib/samagotchi/bundles/inline-widgets/manifest.yml +11 -0
- data/lib/samagotchi/bundles/inline-widgets/plugin.rb +76 -0
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/engine/context_judge.rb +19 -125
- data/lib/samagotchi/engine/guardrails.rb +4 -2
- data/lib/samagotchi/engine/messages.rb +9 -6
- data/lib/samagotchi/engine/objectives.rb +22 -90
- data/lib/samagotchi/engine/participants.rb +15 -10
- data/lib/samagotchi/engine/plugins.rb +3 -1
- data/lib/samagotchi/engine/questions.rb +1 -1
- data/lib/samagotchi/engine/side_requests.rb +5 -2
- data/lib/samagotchi/engine/toolkit.rb +33 -190
- data/lib/samagotchi/engine/turn_end.rb +1 -1
- data/lib/samagotchi/engine/turn_helpers.rb +16 -39
- data/lib/samagotchi/engine/verify.rb +7 -143
- data/lib/samagotchi/engine.rb +162 -99
- data/lib/samagotchi/engine_peers.rb +1 -1
- data/lib/samagotchi/event_fanout.rb +18 -12
- data/lib/samagotchi/guardrail_wiring.rb +83 -82
- data/lib/samagotchi/guardrails/gate.rb +41 -40
- data/lib/samagotchi/guardrails/pair_boundary.rb +2 -1
- data/lib/samagotchi/guardrails/review_boundary.rb +2 -1
- data/lib/samagotchi/guardrails/targets.rb +6 -4
- data/lib/samagotchi/guardrails_command.rb +1 -1
- data/lib/samagotchi/inline_widgets.rb +325 -0
- data/lib/samagotchi/kernel_loop.rb +190 -213
- data/lib/samagotchi/llm/backend.rb +3 -1
- data/lib/samagotchi/llm/chat_loop.rb +63 -41
- data/lib/samagotchi/llm/native_backend.rb +3 -2
- data/lib/samagotchi/llm_context_forget.rb +5 -3
- data/lib/samagotchi/model_binding.rb +3 -3
- data/lib/samagotchi/plugin/prompt_sections.rb +7 -0
- data/lib/samagotchi/question_desk.rb +1 -1
- data/lib/samagotchi/revise_anchor.rb +1 -1
- data/lib/samagotchi/system_prompt.rb +37 -30
- data/lib/samagotchi/testing/boundary.rb +7 -0
- data/lib/samagotchi/testing/bundle.rb +7 -5
- data/lib/samagotchi/testing/install.rb +3 -2
- data/lib/samagotchi/tool_activity.rb +8 -6
- data/lib/samagotchi/tool_declarations.rb +4 -4
- data/lib/samagotchi/tool_response.rb +4 -3
- data/lib/samagotchi/tool_runner.rb +91 -100
- data/lib/samagotchi/toolkit_binding.rb +40 -14
- data/lib/samagotchi/tools/deferred_tools.rb +17 -21
- data/lib/samagotchi/tools/registry.rb +43 -40
- data/lib/samagotchi/tools/tool_index.rb +135 -88
- data/lib/samagotchi/tools/tool_surface.rb +173 -0
- data/lib/samagotchi/turn_context.rb +67 -0
- data/lib/samagotchi/turn_stages/context_judge.rb +170 -0
- data/lib/samagotchi/turn_stages/objectives.rb +146 -0
- data/lib/samagotchi/turn_stages/toolkit_suggestion.rb +261 -0
- data/lib/samagotchi/turn_stages/verify_anchors.rb +169 -0
- data/lib/samagotchi/turn_stages/warmup.rb +83 -0
- data/lib/samagotchi/turn_state.rb +46 -4
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/markdown_renderer.rb +39 -14
- data/lib/samagotchi/web/message_display.rb +1 -1
- data/lib/samagotchi/web/public/app.js +5 -1
- data/lib/samagotchi/web/public/block_host.js +74 -199
- data/lib/samagotchi/web/public/block_renderers.css +2 -0
- data/lib/samagotchi/web/public/block_source.js +7 -4
- data/lib/samagotchi/web/public/block_watch.js +256 -0
- data/lib/samagotchi/web/public/composer_fold.js +3 -0
- data/lib/samagotchi/web/public/index.html +28 -9
- data/lib/samagotchi/web/public/inline_widgets.js +146 -0
- data/lib/samagotchi/web/public/owner_verify.js +1 -1
- data/lib/samagotchi/web/public/participant.js +1 -1
- data/lib/samagotchi/web/public/revise_stack.js +1 -1
- data/lib/samagotchi/web/public/stage_watch.js +6 -2
- data/lib/samagotchi/web/public/text_roots.js +1 -1
- data/lib/samagotchi/web/public/verify_target.js +1 -1
- data/lib/samagotchi/web/public/widget_picks.js +130 -0
- data/lib/samagotchi/web/public/widget_walk.js +230 -0
- data/lib/samagotchi/web/public/widgets.css +70 -0
- metadata +17 -1
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# Inline widgets: chi-ui blocks the page draws
|
|
2
|
+
|
|
3
|
+
A model can write a ```` ```chi-ui ```` fence in an answer: YAML, `type:` first, a **picks** (rows of options to choose
|
|
4
|
+
from, which fill the reply) or a **walk** (a scenario stepped through). Core draws it, not a bundle: the server parses
|
|
5
|
+
the YAML once and the page draws from the parsed data. Where it doesn't draw (the terminal, `web.markdown` off, a
|
|
6
|
+
refused block) the YAML reads as text. The server half (the parse) and the page half (the drawers) are below.
|
|
7
|
+
|
|
8
|
+
What tells the model the blocks exist is the shipped inline-widgets bundle
|
|
9
|
+
([bundles.md](../bundles.md#the-inline-widgets-bundle)): two prompt sections, `picks` and `walk`, each with an example
|
|
10
|
+
its spec holds to a strict parse. Without it nothing here changes, and blocks already in a history still draw.
|
|
11
|
+
|
|
12
|
+
## The path
|
|
13
|
+
|
|
14
|
+
1. `Web::MarkdownRenderer#fences` walks the `code_block` nodes, as for block renderers' `data-lang`
|
|
15
|
+
([block-renderers.md](block-renderers.md), The path), keeping each one's language and its literal
|
|
16
|
+
(`string_content`, the source between the fences, also inside a list item or a blockquote) as a `Fence`.
|
|
17
|
+
2. After sanitize's walk, `mark_languages` pairs them with the `<pre>`s by position and, for a `chi-ui` one, calls
|
|
18
|
+
`InlineWidgets.parse(literal)`. The block gets `data-widget` (the JSON of the data with its `type`) or
|
|
19
|
+
`data-widget-error` (the one-line reason), and `data-widget-lenient` when the lenient pass was needed. A count
|
|
20
|
+
that differs sets nothing, as for `data-lang`.
|
|
21
|
+
3. Every HTML path goes through `render`, so answers, cards, delegate reports and `/api/render` all carry the
|
|
22
|
+
attributes; which of them draw is the page's choice (answer texts only).
|
|
23
|
+
4. `chi-ui` is `BlockRenderers::RESERVED`: a bundle's `block_renderers:` can't claim it, so the page never has two
|
|
24
|
+
drawers for one block.
|
|
25
|
+
|
|
26
|
+
**Safety.** The JSON is an attribute value: Nokogiri escapes its quotes, and `<`, `>` and `&` in it are `\u` escapes
|
|
27
|
+
(`JSON_MARKUP`), so the HTML holds no model markup even as attribute text. The page draws its strings as text.
|
|
28
|
+
|
|
29
|
+
## The parse (`Samagotchi::InlineWidgets`)
|
|
30
|
+
|
|
31
|
+
Pure Ruby, no web, no Engine. `parse(source)` returns a `Result`: `ok?`, `type`, `data` (string keys, JSON-ready),
|
|
32
|
+
`lenient?`, `error`, and `widget_json`.
|
|
33
|
+
|
|
34
|
+
- **Strings stay strings.** The YAML is never loaded: `Tree` reads Psych's node tree and takes each scalar's own text.
|
|
35
|
+
YAML 1.1 would make `- no` false (a real eval block has one), `id: 1.10` the float 1.1, and a date a `Date` that
|
|
36
|
+
`safe_load` refuses. A plain empty value, `~` or `null` is absent.
|
|
37
|
+
- **Plain data only.** An anchor, an alias or a tag (`!ruby/object`, `!!str`) is refused, as is a second document, a
|
|
38
|
+
key that isn't a scalar, a root that isn't a mapping, text that isn't valid UTF-8, and nesting past 8 levels
|
|
39
|
+
(`MAX_DEPTH`; the deepest legal block nests 6). A stack overflow is rescued too, as the backstop: it is no
|
|
40
|
+
`StandardError`, so it would escape `render` and break every render of the history.
|
|
41
|
+
- **Strict first, then the lenient pass.** Only a block Psych can't parse gets `Lenient.quote` and a second parse.
|
|
42
|
+
The pass is eval 0013's (`~/.claude/plans/evals/0013-inline-widgets/scripts/validate.rb`): the free-text values
|
|
43
|
+
(`title text why does sees behind when recommended`) and the list items that aren't mappings go in double quotes
|
|
44
|
+
unless they are one clean scalar already. It mends the traps models fell into: a colon in a value
|
|
45
|
+
(`does: Asked: ...`), a colon in an option (`- B: keep 1 h`), a value starting with a quoted word
|
|
46
|
+
(`text: "x" is called`). It is line by line, so a YAML error's line still points at the source. It can't mend a
|
|
47
|
+
comma in a flow list (`options: [A allow, B deny project, allow system]` is three options, so the recommended one
|
|
48
|
+
isn't one of them: eval round 1's failures), and a block that parses strictly into the wrong shape isn't retried.
|
|
49
|
+
- **Folds.** A block that parses strictly can still have an option or a free-text value that YAML read as a mapping
|
|
50
|
+
of one pair of scalars (`- fix now: drop the command`). Where text is wanted (an option, `title`, `text`, `why`,
|
|
51
|
+
`does`, `sees`, `behind`, `when`, `recommended`), that mapping is taken as the text written: the source slice from
|
|
52
|
+
the key to the value's end when both sit on one line, else `key: value`. The Result is then `lenient?`. A mapping
|
|
53
|
+
of several pairs, or one holding a list or a mapping, stays an error, and so does one where an id, an actor, a
|
|
54
|
+
`next` or a `goto` goes. This is a mend, not a second lenient pass (`Tree#folds`, `Schema#text`).
|
|
55
|
+
- **Normalised.** Ids are strings, unknown keys are dropped, optional values are absent rather than null, and text is
|
|
56
|
+
stripped. The page never sees model junk.
|
|
57
|
+
|
|
58
|
+
### The schemas
|
|
59
|
+
|
|
60
|
+
| | Required | Optional | Rules |
|
|
61
|
+
|---|---|---|---|
|
|
62
|
+
| picks | `rows`: rows of `id`, `text`, `options` (2-4 texts) | `title`; on a row `recommended`, `why` | ids unique; a row's options unique; `recommended` is one of the options |
|
|
63
|
+
| walk | `title`, `actors` (2-5 texts), `steps`: steps of `id`, `actor`, `does` or `sees` | on a step `behind`, `next`, `cases` (each `when` + `goto`) | ids unique, none of them `end`; actors unique; `actor` is one of the actors; `next` names a step or is `end`, and not the step itself; every `goto` names a step |
|
|
64
|
+
|
|
65
|
+
Eval round 1's `branches:` (the same shape as `cases:`) is read as `cases`.
|
|
66
|
+
|
|
67
|
+
**`next: end`** ends the walk at that step: a branch that ends early, such as a success step written before the
|
|
68
|
+
error step it must not run into. The normalised JSON keeps `"next": "end"`, and the page shows the step as the last
|
|
69
|
+
one (its "end" tag, then "Start over"). A `goto` can't be `end`. In YAML:
|
|
70
|
+
|
|
71
|
+
```yaml
|
|
72
|
+
- id: s4
|
|
73
|
+
actor: user
|
|
74
|
+
sees: the saved form
|
|
75
|
+
next: end
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### The caps
|
|
79
|
+
|
|
80
|
+
The source is at most 16 KB, checked before the parse. After it: a picks has at most 12 rows, a walk at most 30 steps
|
|
81
|
+
and 2-5 unique actors, and every string is at most 300 characters. An error repeats at most 40 characters of a bad
|
|
82
|
+
`type`, and never a tag.
|
|
83
|
+
|
|
84
|
+
## The page (`inline_widgets.js`)
|
|
85
|
+
|
|
86
|
+
`createWidgetDrawer` is a core drawer of `block_watch.js` (its header has the contract), wired in `app.js` ahead of the
|
|
87
|
+
bundles' frame drawer through `createBlockHost`'s `drawers`. So a `chi-ui` block gets the watcher's paths: a reload, a
|
|
88
|
+
session switch, a turn's end, Revise's clones, the stage's hand-off to `#history`.
|
|
89
|
+
|
|
90
|
+
- **What it draws.** A `<pre>` with `data-widget` gets a light-DOM `<figure class="block-drawn widget">` right before it,
|
|
91
|
+
holding a `.widget-host` and the shared `.block-toggle` (block_source.js `setSourceShown`, the same icon; the
|
|
92
|
+
figure's `data-toggle-picture` makes its other label "Show the widget"). The `<pre>` stays, hidden: the DOM is the state, copy.js gives it its `.code-wrap`
|
|
93
|
+
copy button (it copies the YAML, which is the answer's text), and the toggle shows it. The toggle is in the light DOM
|
|
94
|
+
because a click in a shadow root reaches the page's delegated listeners retargeted at the host.
|
|
95
|
+
- **Shadow DOM.** The host has an open shadow root holding the widget, with `widgets.css` adopted
|
|
96
|
+
(`adoptedStyleSheets`): the page fetches it once into a constructable `CSSStyleSheet` (`loadWidgetSheet`), and `draw`
|
|
97
|
+
returns a Promise that places the widget once it is there, so no widget flashes unstyled and the view held
|
|
98
|
+
(`holdingView`) measures the styled one. A failed fetch draws unstyled and the next draw tries again. Its words are then out of the bubble's `textContent`, which the shown-texts dedupe (`seedSeenFromDom`), copy's
|
|
99
|
+
fallback and an annotation's quote read, as with a renderer's `<img>`-only figure. chi's `:root` tokens reach it as
|
|
100
|
+
inherited custom properties, so light and dark follow the page. Playwright's locators pierce an open shadow root;
|
|
101
|
+
`page.evaluate`'s DOM calls don't. Whatever asks `document.activeElement` whether a field has the focus looks
|
|
102
|
+
through shadow roots (`composer_fold.js editable`, `stage_watch.js`), or typing in a note opens a folded composer.
|
|
103
|
+
- **A clone redraws.** `cloneNode` doesn't copy a shadow root: a Revise clone (`revise_switch.js viewOf`) arrives with
|
|
104
|
+
the figure and an empty host, which `needsDraw` sees, and `draw` fills it again with a fresh widget and its own
|
|
105
|
+
listeners. A widget's position and picks live in the widget, so a clone or a rewrite starts over (accepted for v1).
|
|
106
|
+
- **A refused block** (`data-widget-error`) stays source under `failBlock`'s muted `.block-note` ("Couldn't draw this
|
|
107
|
+
chi-ui block: …", an attribute CSS shows, a click copies it). Bad JSON or an unknown `type` fails the same way, from
|
|
108
|
+
the watcher. A `chi-ui` `<pre>` with neither attribute (an older server) is left alone.
|
|
109
|
+
- **walk** (`widget_walk.js`): the swimlane (one column per actor, one row per step; the current, seen and a hovered
|
|
110
|
+
case's target marked; a click jumps), the step card (actor, `does`, "You see", the dashed "Behind it"), a fork's
|
|
111
|
+
"Which case?" buttons, Back / Next / Start over, the path taken, and ← → while the widget has the focus
|
|
112
|
+
(a move that removes or hides the focused button leaves the focus on the widget; a click on the current step is no
|
|
113
|
+
move). `walkRoutes`
|
|
114
|
+
resolves where a step goes: its `next` (`end`: nowhere), else the step below; a fork's `cases` as written, a case whose `when` is no
|
|
115
|
+
condition (round 1's `always`) reading as the usual path, alone it is the step's next. At a phone's width the
|
|
116
|
+
swimlane scrolls sideways inside the widget; the page doesn't.
|
|
117
|
+
- **picks** (`widget_picks.js`): one row per item, its options as chips (`aria-pressed`), the recommended one marked
|
|
118
|
+
and picked to start with, a "+ note" per row, and "Fill reply".
|
|
119
|
+
|
|
120
|
+
**The only way out is composer text.** Fill reply builds `id: option (note) · …` from the picked rows (a row with no
|
|
121
|
+
pick is left out) and hands it to app.js's `fillComposer`, after a typed draft on a new line (`withDraft`; the draft
|
|
122
|
+
is kept). It never sends: the owner reads the text and sends it or not. A walk has no way out at all. The model's
|
|
123
|
+
strings reach the page through `textContent` and attributes only, never `innerHTML`, so a step reading
|
|
124
|
+
`<img src=x onerror=…>` shows as those characters.
|
|
125
|
+
|
|
126
|
+
**What doesn't draw.** The participant page (`participant.js` wires no block host), `web.markdown` off (no
|
|
127
|
+
`data-lang`), a live answer while it streams (its YAML shows until the turn's end re-renders it as markdown), and
|
|
128
|
+
anything outside `TEXTS`: cards, question replies, the asking slot, delegate reports. The terminal shows the YAML.
|
|
129
|
+
|
|
130
|
+
## The fixtures
|
|
131
|
+
|
|
132
|
+
`spec/fixtures/inline_widgets/` holds eval 0013's 78 real blocks (47 from round 2, 31 from round 1), one file each,
|
|
133
|
+
and `manifest.yml`, the outcome (ok, lenient or error) the eval's `check()` gave each when they were copied, but for
|
|
134
|
+
r2-deepseek-picks-review.2: its options are one-pair mappings, an error in the eval, and a fold (lenient) here.
|
|
135
|
+
`spec/inline_widgets_spec.rb` checks every one gets it: all 47 of round 2's draw, and round 1's errors are the
|
|
136
|
+
comma splits.
|
|
@@ -116,9 +116,9 @@ Bridge's `TurnAccumulator#current_messages`) leaves tool calls out. A move shows
|
|
|
116
116
|
|
|
117
117
|
`action: propose` (`change` add, reword, done or drop; `id`; `text`; `note`, why) checks the change against the list
|
|
118
118
|
(`Objectives::Proposal#refusal`: its line through `Objectives::Edit`, the edit the card's Apply runs, so the tool refuses
|
|
119
|
-
what Apply would) and hands it to `Engine#propose_objective
|
|
120
|
-
as
|
|
121
|
-
failed one drops them
|
|
119
|
+
what Apply would) and hands it to `Engine#propose_objective` (`TurnStages::Objectives#propose`), which keeps it until the turn is over,
|
|
120
|
+
as `TurnStages::ContextJudge`'s cards are kept. `run_turn`'s ensure (`TurnStages::Objectives#finally`) shows them only for a turn that completed: a canceled or
|
|
121
|
+
failed one drops them, so its Apply card never outlives a turn whose call a
|
|
122
122
|
rollback has taken out of the history, and nothing carries over to the next turn. A card is between turns (no
|
|
123
123
|
`in_turn`), so it writes no `pending_card.json`, the stage doesn't say it waits, and it keeps its buttons over a
|
|
124
124
|
restart (`CardStore` takes them only from a card shown in a turn). The same change twice in a turn is one card. A P3
|
|
@@ -139,7 +139,7 @@ any call. An add's text can't split on ` | `. The body names the line, for a ter
|
|
|
139
139
|
|
|
140
140
|
`Objectives.view` (`List#to_view`) is what the page draws: the items, each with its last move, and `pending`, whether
|
|
141
141
|
the newest list is a file still waiting. The session read (`objectives`, no key with no list) and the live
|
|
142
|
-
`:objectives_updated` carry it. `Engine#announce_objectives` emits when the view differs from the last: after an edit,
|
|
142
|
+
`:objectives_updated` carry it. `Engine#announce_objectives` (`TurnStages::Objectives#announce`) emits when the view differs from the last: after an edit,
|
|
143
143
|
once a list note is in (`add_context_note`), at a turn's end (its moves) and after a rollback between turns. A session
|
|
144
144
|
with no list emits nothing. Between two tabs that is all it takes. A participant gets neither: the event is in
|
|
145
145
|
`ParticipantView::DROPPED`.
|
|
@@ -259,7 +259,9 @@ turn isn't on the page until the turn's end (`announce_objectives`), so its tap
|
|
|
259
259
|
|
|
260
260
|
## System 1 in audit
|
|
261
261
|
|
|
262
|
-
`ObjectivesAudit`, off unless `objectives.system_one: audit`.
|
|
262
|
+
`ObjectivesAudit`, off unless `objectives.system_one: audit`. `TurnStages::Objectives` asks it: `incoming` at an owner's
|
|
263
|
+
turn's start (a person's prompt only), `outgoing` after the end event of a completed answer, `note` for a note added
|
|
264
|
+
between turns (docs/internals/engine.md, "Turn stages"). See [system-one.md](../system-one.md#objectives-in-audit).
|
|
263
265
|
|
|
264
266
|
## Not in this version
|
|
265
267
|
|
|
@@ -269,4 +271,4 @@ Taking it out: old notes stay readable framed bubbles and keep being sent, and t
|
|
|
269
271
|
stay as history; with a kit no longer declaring the tool, they are ordinary saved calls. The card's bundle uninstalls. The
|
|
270
272
|
page's pieces are the `objectives_*.js` modules and `objectives.css`, wired in a few lines of `app.js`; without them a list note
|
|
271
273
|
falls back to the default bubble once `noteBubbleTemplate`'s first line goes. Check this is `objectives_check.rb`, its line in the command and
|
|
272
|
-
the `check` key; the audit is `objectives_audit.rb` and three calls in `
|
|
274
|
+
the `check` key; the audit is `objectives_audit.rb` and three calls in `TurnStages::Objectives`. Old `objective_checks.jsonl` files and saved runs are then read by nothing.
|
|
@@ -214,10 +214,10 @@ the list's `mode` (the panel's words) and the grant's stamp: `Grant#mode` / the
|
|
|
214
214
|
turns) stays review until the owner invites them again.
|
|
215
215
|
|
|
216
216
|
A turn's mode is `ParticipantMode.for_turn(session, entry)`: `:pair` only for a stamped entry in a pair session.
|
|
217
|
-
The worker decides it once at the turn's start (`GuardrailWiring#begin_turn` →
|
|
217
|
+
The worker decides it once at the turn's start (`GuardrailWiring#begin_turn` → `TurnStart#mode`, into the turn's `TurnContext#participant_mode`, from the entry
|
|
218
218
|
`SessionParticipants#entry_for` reads), and everything in the turn reads that: the stamp on the opening message
|
|
219
219
|
(`participant_mode`), the boundary in `GuardrailWiring#checks`, `ToolRunner#unreached_group_refusal` (through
|
|
220
|
-
`
|
|
220
|
+
the call's `TurnContext`: a review turn's boundary words the refusal; a pair turn gets the owner's), the
|
|
221
221
|
mid-turn stop and the cap notice's words. The system prompt's line is the pair one once an active participant is
|
|
222
222
|
stamped pair (`SessionParticipants#prompt_mode`); the participant page's `viewer.mode` is their own grant's
|
|
223
223
|
(`ParticipantMode.live_for`).
|
|
@@ -323,7 +323,7 @@ and `ctx.turn_author` names it ([hooks](../hooks.md#participants-turns)).
|
|
|
323
323
|
their $2.00 review cap: their comments are refused until you raise it (chi share cap ID p_… AMOUNT)." (a working
|
|
324
324
|
session's: "Alex reached their $2.00 cap: their turns stop and are refused until you raise it (…)"). A review
|
|
325
325
|
turn's spend counts from its end, so a review turn can finish past the cap. A pair turn stops mid-turn: before each
|
|
326
|
-
generation (`KernelLoop#generation_guard`, both loops) `Engine#guard_participant_spend` asks the worker's check
|
|
326
|
+
generation (`KernelLoop::Services#generation_guard`, both loops) `Engine#guard_participant_spend` asks the worker's check
|
|
327
327
|
(`Worker#participant_turn_stop`: `metrics.spend(author)` counts the running turn's finished generations) and, past
|
|
328
328
|
the cap, stops the turn on the plugins' stop path (`HookRoutes#stop_turn`: "stopped the turn: Alex reached their
|
|
329
329
|
$2.00 cap"), with the owner's notice once per cap. A generation's cost is known only when it ends, so a pair turn
|
|
@@ -469,7 +469,7 @@ fields: a field added to an event later reaches nobody until it is named there.
|
|
|
469
469
|
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
|
|
470
470
|
Markdown presentation (links, headings, list/blockquote prefixes, emphasis and code delimiters) from both sides;
|
|
471
471
|
this matches the rendered text the page selects without treating different casing as the same claim. The quote may
|
|
472
|
-
also be in the answer as the pages show it (`
|
|
472
|
+
also be in the answer as the pages show it (`TurnStages::VerifyAnchors#quote_in?`, `Redact.text`: the session's
|
|
473
473
|
folder relative, home `~`, secret shapes `[redacted]`): a participant's page has only that text, and the owner's
|
|
474
474
|
page quotes it the same way (`owner_claim.js` `opts()`, `normalizeClaim`; `spec/fixtures/redact_cases.json` keeps the two
|
|
475
475
|
sides alike), so a claim naming a path is taken on both. A fresh child
|
|
@@ -510,7 +510,7 @@ fields: a field added to an event later reaches nobody until it is named there.
|
|
|
510
510
|
`pair` only for a grant stamped to act in a working session whose worker runs pair turns, else `review`). Every
|
|
511
511
|
turn of a participant's child is theirs, whoever starts it (the owner typing into it, a continue, a reminder):
|
|
512
512
|
`VerifyFreshOrigin#origin_for` sets the author in `Engine#begin_turn` and in the worker, so their boundary
|
|
513
|
-
(`GuardrailWiring#
|
|
513
|
+
(`GuardrailWiring#begin_turn` reads the saved mode, never a lookup), their System 1 skip, their once-only asks to
|
|
514
514
|
the owner and the stamps on their messages follow. The child has no `participants.json`: its participant lookups
|
|
515
515
|
(`Session#participants_source_id`) read the parent's, so a revoke, an expiry or the parent's archive refuses the
|
|
516
516
|
child's next turn and stops a pair turn mid-turn, as in the parent; their audit lines go to the parent's
|
|
@@ -73,11 +73,15 @@ right-hand column happens.
|
|
|
73
73
|
- The tools layer is shared **per toolkit** (configuration.md, "Toolkits"): sessions on the same toolkit share it,
|
|
74
74
|
and a chain's links keep theirs. A session's toolkit is saved when it is created and never resolved again from
|
|
75
75
|
config. A change (`/toolkit`, `ctx.use_toolkit`) is applied only at a turn's start (`Engine#apply_toolkit!`,
|
|
76
|
-
beside `apply_staged_tools!`, then `reset!`): the chat loop builds `tools:` per request
|
|
77
|
-
|
|
78
|
-
|
|
76
|
+
beside `apply_staged_tools!`, then `reset!`): the chat loop builds `tools:` per request from the turn's
|
|
77
|
+
`Tools::ToolSurface` (built once in `Engine#generate` from the Registry, under the layers the turn's prompt was
|
|
78
|
+
built with), so its requests declare what the turn's prompt does, and a mid-turn change shapes only the next
|
|
79
|
+
turn's surface. The native declarations, the not-loaded line and the kit prompt come from a surface too: each
|
|
80
|
+
`SystemPrompt` build reads the Registry once as `ToolSurface.for(registry, layers:)` (its `declared`, `left_out`
|
|
81
|
+
and `index_only`, its toolkit's prompt), so the three agree and match the turn's (engine.md, "ToolSurface"). Under `full` nothing is filtered and no line is added: the bytes are what they were before
|
|
82
|
+
toolkits.
|
|
79
83
|
- A toolkit suggestion (`toolkit.suggest.mode: on`) is set before `apply_toolkit!` of the turn it was asked on
|
|
80
|
-
(`
|
|
84
|
+
(`TurnStages::ToolkitSuggestion#settle` → `ToolkitBinding#suggest`), and no request goes out before that, so on a
|
|
81
85
|
session's first turn it costs nothing. Its **deferred switch** (a later prompt after `undecided`, within
|
|
82
86
|
`toolkit.suggest.defer_turns` / `defer_tokens`) is engine-side, so `/toolkit`'s CacheConfirm doesn't apply: it
|
|
83
87
|
changes the declared tools at the front of the request, and the server re-reads **system prompt + tools +
|
|
@@ -171,7 +175,7 @@ right-hand column happens.
|
|
|
171
175
|
The next turn's prompt differs from the turn's last request at its tail: the previous answer's thinking is
|
|
172
176
|
stripped (the models' specs require it). A hybrid model on llama.cpp can only resume from a restore point, so the
|
|
173
177
|
next turn prefills from well before the end, more with every turn. `PromptWarmup` moves that work into the time
|
|
174
|
-
the user reads: after a completed turn, `
|
|
178
|
+
the user reads: after a completed turn, `TurnStages::Warmup#after` formats the next prompt up to where the new
|
|
175
179
|
user message starts (`KernelLoop#warmup_prompt`) and sends it in a thread (`Client#warm_up`: native
|
|
176
180
|
`/completion`, `n_predict: 1`, not streamed, no retries, log purpose `warmup`, then a `model warmup` line with
|
|
177
181
|
`cached=`, `prefilled=` and `prefill_ms=`).
|
|
@@ -24,7 +24,7 @@ page. A factory dep a new version stops passing is the same hazard the other way
|
|
|
24
24
|
out of `send_turn.js`, `session_actions.js` and `lists_view.js`); a page loaded before keeps all its old modules
|
|
25
25
|
until a reload, so the risk is a mixed cache, not an open tab. `answer_fold.js`, `details_motion.js`,
|
|
26
26
|
`turn_range.js`, `history_reveal.js`, `latest.js`, `session_payload.js`, `method_api.js`, `session_store.js`,
|
|
27
|
-
`session_fields.js`, `feature_gates.js`, `revise_stack.js`, `revise_switch.js`, `shell_command.js`, `stale_params.js`, `popover_modal.js`, `more_sheet.js`, `hash_intent.js` (what the hash asks for: session, all, start or keep), `worker_features.js`, `version_notices.js`, `owner_claim.js`, `owner_verify.js`, `composer_mode.js`, `strip_hidden.js`, `block_open.js`, `text_roots.js`, `block_source.js`, `block_frame.js`, `block_host.js`, `card_stamps.js` and the `objectives_*.js` modules are new modules for that reason. The last
|
|
27
|
+
`session_fields.js`, `feature_gates.js`, `revise_stack.js`, `revise_switch.js`, `shell_command.js`, `stale_params.js`, `popover_modal.js`, `more_sheet.js`, `hash_intent.js` (what the hash asks for: session, all, start or keep), `worker_features.js`, `version_notices.js`, `owner_claim.js`, `owner_verify.js`, `composer_mode.js`, `strip_hidden.js`, `block_open.js`, `text_roots.js`, `block_source.js`, `block_frame.js`, `block_host.js`, `block_watch.js`, `card_stamps.js`, `inline_widgets.js`, `widget_walk.js`, `widget_picks.js` and the `objectives_*.js` modules are new modules for that reason. The last four `block_*` draw fenced blocks (`block_watch.js` finds and watches them, `block_host.js` is its frame drawer for a bundle's renderers): [block-renderers.md](block-renderers.md). `inline_widgets.js` is its core drawer for `chi-ui` blocks, drawing `widget_walk.js`'s and `widget_picks.js`'s widgets in a shadow root styled by `widgets.css`: [inline-widgets.md](inline-widgets.md).
|
|
28
28
|
|
|
29
29
|
## Two pages, two models
|
|
30
30
|
|
|
@@ -102,7 +102,8 @@ when it has outputs and none is `sent`. An edit-only item, a failed run's file o
|
|
|
102
102
|
pending: [Steering: add and unadd](#steering-add-and-unadd)) (`Engine#emit_working_set`, `EventFanout`'s):
|
|
103
103
|
- after each `:generation_started`, in `build_stream_event_handler` (`EventFanout`), from the running turn's
|
|
104
104
|
conversation (`Engine#live_conversation`, read on the turn's thread) under the turn's strategy
|
|
105
|
-
(
|
|
105
|
+
(`TurnContext#settings`' `llm_context`; `EventFanout#working_set_llm_context`: the running turn's, the last turn's
|
|
106
|
+
until it has its own). Both loops start a generation after `apply_llm_context!`, so the set is
|
|
106
107
|
what that request sends;
|
|
107
108
|
- once in `Engine#end_turn`, after `replace_session_messages` and before the end event, from the kept messages (the
|
|
108
109
|
turn-end apply has run by then);
|
data/docs/plugins.md
CHANGED
|
@@ -1387,6 +1387,8 @@ block_renderers:
|
|
|
1387
1387
|
it refuses with "No memories to build".
|
|
1388
1388
|
- The first installed bundle (by name) with a language draws it. A bundle whose `requires_chi` this chi doesn't meet
|
|
1389
1389
|
draws nothing.
|
|
1390
|
+
- `chi-ui` is reserved: chi draws it itself (inline widgets, [internals/inline-widgets.md](internals/inline-widgets.md)),
|
|
1391
|
+
so a manifest that lists it is refused.
|
|
1390
1392
|
- **What it needs of the page:** `web.markdown` on (a default-off setting, and the commonmarker gem). With it off a
|
|
1391
1393
|
fence has no language in the page and nothing draws, though a hint in the bundle's prompt section still reaches
|
|
1392
1394
|
the model: say so in the bundle's README.
|
data/docs/releasing.md
CHANGED
|
@@ -13,7 +13,7 @@ can run the whole release; the user approves the notes before the tag and the
|
|
|
13
13
|
and `rake release:check` enforce it). The system bundle upgrades itself when
|
|
14
14
|
chi starts.
|
|
15
15
|
- **Every other shipped bundle** (btw, check-in, guardrails, known-names,
|
|
16
|
-
loop-guard, mcp, skills, slow-commands, agents-md, cost-cap, work-cap, source-links, github-pr, coordinator, claude-peer, codex-peer, ruby, model-notes, decision-card, web-search, and the profiles core and dev) has its own semver in its
|
|
16
|
+
loop-guard, mcp, skills, slow-commands, agents-md, cost-cap, work-cap, source-links, github-pr, coordinator, claude-peer, codex-peer, ruby, model-notes, decision-card, inline-widgets, web-search, and the profiles core and dev) has its own semver in its
|
|
17
17
|
`manifest.yml` and, when it needs a newer chi, a `requires_chi:` line. They never upgrade by themselves: users run
|
|
18
18
|
`chi update` (all of them, with the gem) or `chi bundle upgrade NAME`. So:
|
|
19
19
|
- a change to a bundle's files bumps that bundle's `version:`
|
data/docs/sessions.md
CHANGED
|
@@ -6,7 +6,7 @@ Sessions are plain files — no DB. Each session is `~/.local/state/samagotchi/s
|
|
|
6
6
|
|
|
7
7
|
**Verify verdicts:** a web Verify turn stamps its opening user message with `verify: {turn_id, quote}` (the selected source turn and quote; metadata only, never added to the prompt). When that turn completes and the final assistant reply's last non-empty line is a valid `Verdict: confirmed|refuted|unclear — <reason>`, the session file appends `{anchor: {turn_id, quote}, verdict, reason, by, check_turn_id, at}` to its optional `verdicts` array. `by` is the turn's author (nil for the owner), `check_turn_id` names the Verify turn, and `at` is its timestamp. The array is append-only; older files without it have no saved verdicts. An `unclear` verdict can be checked again by **Verify fresh** (a delegate child that gets only the claim; see *Delegating*): its verdict is appended to the parent's `verdicts` with `fresh: true`, `of` (the verdict it checked again) and `child_id`, from the parent's `fresh_verdicts/` dir, which its worker takes between turns. Verdicts are display state: none enters a prompt.
|
|
8
8
|
|
|
9
|
-
**Revise stacks:** a web Revise turn (POST /turn `revises: {turn_id}`, the answer it rewrites) stamps its opening message (its prompt) with `revises: {turn_id}` at the turn's end, only when the turn completed with an answer (`AnswerTail`): a failed, canceled or empty turn revised nothing (`
|
|
9
|
+
**Revise stacks:** a web Revise turn (POST /turn `revises: {turn_id}`, the answer it rewrites) stamps its opening message (its prompt) with `revises: {turn_id}` at the turn's end, only when the turn completed with an answer (`AnswerTail`): a failed, canceled or empty turn revised nothing (`TurnStages::VerifyAnchors#stamp_opening`). A draft no prompt carries (a stale page, a rolled-back turn) is no target: no stamp. A draft a stamped turn already revised is re-targeted to its stack's top, the newest version, so a stack is a chain `v1 ← v2 ← v3`. Like `verify`, it is metadata only, never added to the prompt (no change to a request's bytes); chi web's history passes it on the opening message (and on a wake turn's note), and `:turn_completed` carries the stamped `revises` (the participant view's too). The history also marks a Verify turn's opening `verify: true`, and so does its live end event (`:turn_completed`'s `verify: true`, with its `turn_id`): the page offers no Revise on its answer. The web page draws the stacks from these stamps (docs/cli.md, Revise). Like a Verify turn's, a Revise message waits for the running turn's end (`SessionInbox.turn_end_data?`; the Bridge takes either as `delivery: queue`, so it never cuts and its ack and `:turn_enqueued` say it queues), is sent only to a worker whose `bridge.json` names the `revise` feature (else 409 `revise_unavailable`), and a failed turn gives it back (`:prompt_restored`'s `revises`). Participants can't Revise: chi web and the Bridge drop it from their turns. Both anchors travel as one `TurnAnchors` inside chi; on the wire and in an input file each keeps its own key (`verify`, `revises`).
|
|
10
10
|
|
|
11
11
|
**Input files:** a message for a session's worker waits in `input/<timestamp>.json` (`SessionInbox.write_input`: `prompt`, `client_id`, `enqueued_id`, and `images`, `no_interrupt` and `delivery` when they apply; `delivery` is written only for `"queue"`). While a turn runs, the worker merges them at each step boundary in name order, except a queued one or one with images: those wait for the turn's end and run one turn each, in name order, and a file after them still merges (`Worker#pending_input_drain`). Such a file, with a prompt, from the user, `chi send` or a delegating parent, also ends a running `task_wait` or `delegate_result` and hands a running `execute` over as a task after 3 s (`SessionInbox.steer_waiting?`, a peek the worker's turn polls through `Engine#input_waiting?`; never in a participant's turn): the file stays for the drain. A queue is sent only to a worker whose `bridge.json` names the `delivery` feature; an older one would steer it, so `SessionManager.deliver_turn` refuses it (409 `delivery_unavailable`), and any other 4xx the Bridge answers is a refusal too, never a fallback file (except a 413, a body over its size limit: that one still goes in as a file, its `delivery` kept). A session command recognized in a file runs without a turn; its outcome is kept at `input/settled/<enqueued_id>` (`completed`, `failed` for a command error, `canceled` with `cancel_reason` `busy` or `dropped` for one refused mid-turn or dropped as the worker left; `origin: command`) so `chi send --wait` ends instead of waiting for a turn that will not run, its detail saying `the command failed` or `the command was refused (busy)` rather than a turn's words. **Reply files:** a turn that ends with visible text leaves its reply in `output/<timestamp>.txt`, or `output/<timestamp>-<enqueued_id>.txt` when the turn ran an input file with an id (`SessionInbox.write_output`), and the session's `last_turn` keeps that `enqueued_id`; so a wait for one queued message (`chi send --queue --wait`, `ReplyWait`'s `turn:`) finds its own turn's reply or end, never another turn's. The names still sort by time, and every reader takes `*.txt`.
|
|
12
12
|
|
|
@@ -141,7 +141,7 @@ chi sessions clean --dry-run --days 7 # test sessions older than 7 da
|
|
|
141
141
|
| `parent_id` | The session that delegated this one or forked it (`ctx.sessions.fork`); `null` otherwise. See [Delegating](#delegating). | `SessionManager.spawn_session`; `ChildMove` writes a `parent.json` beside it instead of this file |
|
|
142
142
|
| `scratch` | A `chi scratch` session, deleted when its terminal leaves. | creation |
|
|
143
143
|
| `last_turn` | How the last turn ended (`LastTurn`), for a parent's wait and the web's notifications. | the Engine, at a turn's end |
|
|
144
|
-
| `verdicts` | Verdict cards recorded by completed Verify turns, in append order. | `
|
|
144
|
+
| `verdicts` | Verdict cards recorded by completed Verify turns, in append order. | `TurnStages::VerifyAnchors#record_verdict`, `VerifyFresh` |
|
|
145
145
|
| `delegate` | Started by the `delegate` tool (a fork has a `parent_id` but isn't one). | `Tools::Delegate` via `spawn_session` |
|
|
146
146
|
| `llm_context` | The session's own LLM context values (strategy, apply rule, budget), over the model's; `null` with none. | creation (`--llm-context`), `/llm-context`; copied by a fork and a continue, not a delegate child |
|
|
147
147
|
| `continues` | The previous link of a chain; `null` otherwise. See [Session chains](#session-chains). | `SessionContinue` |
|
data/docs/testing.md
CHANGED
|
@@ -260,7 +260,7 @@ any other file requires it, so a spec file run alone still finds its requires. E
|
|
|
260
260
|
| `reply_wait_looks.rb` | `ReplyWaitLooks`: counts a `ReplyWait`'s polls, so a step waits for the looks instead of a delay |
|
|
261
261
|
| `stamps.rb` | *helper*: `next_stamp`, distinct ordered times for the lib's `now:` seams; the pinned history clock |
|
|
262
262
|
| `task_dirs.rb` | `with_task_dir`: a tasks' folder of its own, every task stopped and its recorders joined after |
|
|
263
|
-
| `test_kernel.rb` | `TestKernel`: `test_kernel` (a real `KernelLoop` to stub `#run` on) and `test_client` for Engine specs |
|
|
263
|
+
| `test_kernel.rb` | `TestKernel`: `test_kernel` (a real `KernelLoop` to stub `#run` on; its options are `KernelLoop::Services` fields), `attach_services` (swap some of a kernel's services), `run_context` and `test_client` for Engine specs |
|
|
264
264
|
| `thinking_off.rb` | shared context "thinking off": `SAMAGOTCHI_THINKING_LEVEL=off` for the example |
|
|
265
265
|
| `virtual_terminal.rb` | `VirtualTerminal`: just enough VT100 to check what a Screen leaves on the terminal |
|
|
266
266
|
| `waiting.rb` | *helper*: `SpecWaiting` (`wait_for`, `wait_until`, `join!`, `pop!`, …), the test kit's helpers ([Waiting and time](#waiting-and-time)) |
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Samagotchi
|
|
4
|
+
autoload :InlineWidgets, File.expand_path("inline_widgets", __dir__)
|
|
4
5
|
autoload :Log, File.expand_path("log", __dir__)
|
|
5
6
|
autoload :VERSION, File.expand_path("version", __dir__)
|
|
6
7
|
module MemoryBundle
|
|
@@ -34,6 +35,9 @@ module Samagotchi
|
|
|
34
35
|
# A language a block can carry: a fence's first word, downcased
|
|
35
36
|
# (MarkdownRenderer sets it as data-lang under this same pattern).
|
|
36
37
|
LANG = /\A[a-z0-9_+-]{1,32}\z/
|
|
38
|
+
# Languages core draws itself: a bundle's renderer for one would be a
|
|
39
|
+
# second drawer for the same block.
|
|
40
|
+
RESERVED = [InlineWidgets::LANG].freeze
|
|
37
41
|
|
|
38
42
|
module_function
|
|
39
43
|
|
|
@@ -60,6 +64,7 @@ module Samagotchi
|
|
|
60
64
|
unless lang.is_a?(String) && lang.match?(LANG)
|
|
61
65
|
raise Invalid, "block_renderers: lang #{lang.inspect} isn't a fence's language (lowercase letters, digits, _ + -; at most 32)"
|
|
62
66
|
end
|
|
67
|
+
raise Invalid, "block_renderers: #{lang} is drawn by chi itself; a bundle can't render it" if RESERVED.include?(lang)
|
|
63
68
|
|
|
64
69
|
load = data["load"]
|
|
65
70
|
unless load.is_a?(Array) && !load.empty? && load.all? { |file| file.is_a?(String) && file.match?(MemoryBundle::Manifest::SCRIPT_FILE) }
|
|
@@ -421,7 +421,7 @@ module Samagotchi
|
|
|
421
421
|
end
|
|
422
422
|
end
|
|
423
423
|
{ "init" => registered.inits, "toolkit" => registered.toolkits, "tool source" => registered.tool_sources,
|
|
424
|
-
"peer source" => registered.peer_sources }.each do |what, names|
|
|
424
|
+
"peer source" => registered.peer_sources, "prompt section" => registered.prompt_sections }.each do |what, names|
|
|
425
425
|
names.each { |name| add(:info, :registered, plugin, "#{what} #{name}") }
|
|
426
426
|
end
|
|
427
427
|
end
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: dev
|
|
3
|
-
version: 0.7.
|
|
3
|
+
version: 0.7.8
|
|
4
4
|
scope: system
|
|
5
5
|
description: "Developer tools"
|
|
6
6
|
trust_level: reviewed
|
|
7
|
-
requires_chi: ">= 0.
|
|
7
|
+
requires_chi: ">= 0.155.0"
|
|
8
8
|
includes:
|
|
9
9
|
- known-names
|
|
10
10
|
- mcp
|
|
@@ -18,3 +18,4 @@ includes:
|
|
|
18
18
|
- ruby
|
|
19
19
|
- model-notes
|
|
20
20
|
- decision-card
|
|
21
|
+
- inline-widgets
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: inline-widgets
|
|
3
|
+
version: 0.1.0
|
|
4
|
+
scope: system
|
|
5
|
+
description: "Teaches the model the chi-ui blocks chi web draws: picks (rows of options that fill your reply) and walk (a scenario stepped through)"
|
|
6
|
+
trust_level: reviewed
|
|
7
|
+
plugin:
|
|
8
|
+
file: plugin.rb
|
|
9
|
+
sha256: sha256:24ad0c3f0ffb592826b4a7389111d76723e9f28ea4d94ab63abda7986d3311bb
|
|
10
|
+
# chi.prompt_section is 0.132.0's; the web draws chi-ui blocks only from the next release
|
|
11
|
+
requires_chi: ">= 0.155.0"
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# The inline-widgets bundle (docs/bundles.md, The inline-widgets bundle): two
|
|
4
|
+
# prompt sections that teach the model the ```chi-ui blocks chi web draws
|
|
5
|
+
# (docs/internals/inline-widgets.md): picks, rows of options that fill the
|
|
6
|
+
# user's reply, and walk, a scenario stepped through. The drawing is core's; this
|
|
7
|
+
# bundle only says the blocks exist. Uninstalled, the prompt loses the sections
|
|
8
|
+
# and blocks already in a history still draw.
|
|
9
|
+
#
|
|
10
|
+
# The text is eval 0013's round-2 hint, split in two. It is static, so the
|
|
11
|
+
# prompt cache keeps: same text at every build. A delegate child's answer is
|
|
12
|
+
# read by its parent, not drawn for a user, and a one-shot (-p --non-interactive)
|
|
13
|
+
# prints to a terminal: neither gets a section.
|
|
14
|
+
class Plugin
|
|
15
|
+
PICKS = <<~TEXT
|
|
16
|
+
The web UI draws fenced ```chi-ui code blocks (YAML inside) as interactive widgets; the terminal shows their source. Use one whenever it helps the user more than prose: a widget replaces the long explanation, it doesn't sit beside one. Never use one for what markdown shows well (tables, lists, key-value). At most one widget per answer; keep the prose around it to a few sentences and don't repeat the widget's content.
|
|
17
|
+
|
|
18
|
+
picks: whenever your answer leaves the user two or more things to decide (review findings, a plan's open decisions, which of several fixes to do), even if they didn't ask you to list them. The user clicks one option per row, can add a note, and the UI fills their reply.
|
|
19
|
+
```chi-ui
|
|
20
|
+
type: picks
|
|
21
|
+
title: What to do with the findings
|
|
22
|
+
rows:
|
|
23
|
+
- id: "1" # unique, short; the user's reply names it
|
|
24
|
+
text: Bridge drops the replay buffer on reset # the item, <= 80 chars
|
|
25
|
+
options: # 2-4 short options, one per line
|
|
26
|
+
- fix now
|
|
27
|
+
- skip
|
|
28
|
+
- later
|
|
29
|
+
recommended: fix now # exactly one of the options
|
|
30
|
+
why: loses events a slow tab still needs # optional, one short line
|
|
31
|
+
```
|
|
32
|
+
Every row id unique; a row's options unique. Quote a value that starts with a quote or contains ": ".
|
|
33
|
+
TEXT
|
|
34
|
+
|
|
35
|
+
WALK = <<~TEXT
|
|
36
|
+
walk is the other ```chi-ui widget (the same rules: the web UI draws it, at most one widget per answer): a scenario the user steps through one step at a time: who does what, what the user sees, what happens behind it. Use it for "when does this happen / how does this work / what does the user go through" questions.
|
|
37
|
+
```chi-ui
|
|
38
|
+
type: walk
|
|
39
|
+
title: A tab reconnects
|
|
40
|
+
actors: [user, web, worker] # 2-5 short ids, unique
|
|
41
|
+
steps:
|
|
42
|
+
- id: s1 # unique
|
|
43
|
+
actor: user # one of actors
|
|
44
|
+
does: reopens the laptop # the action, <= 80 chars
|
|
45
|
+
sees: the page shows "reconnecting" # optional: what the user sees at this step
|
|
46
|
+
behind: the tab sends GET /events with Last-Event-ID # optional: what happens out of sight
|
|
47
|
+
- id: s2
|
|
48
|
+
actor: worker
|
|
49
|
+
does: checks the event id
|
|
50
|
+
cases: # optional: the step forks. List EVERY case, the usual one too
|
|
51
|
+
- when: the id is still buffered
|
|
52
|
+
goto: s3
|
|
53
|
+
- when: the id is too old
|
|
54
|
+
goto: s4
|
|
55
|
+
- id: s3
|
|
56
|
+
actor: worker
|
|
57
|
+
does: replays the missed events
|
|
58
|
+
next: end # optional: where to go after this step (default: the step below); end ends the walk here
|
|
59
|
+
- id: s4
|
|
60
|
+
actor: web
|
|
61
|
+
does: reloads the whole session
|
|
62
|
+
sees: the page redraws from the top
|
|
63
|
+
```
|
|
64
|
+
A step needs `does` or `sees` (or both). Steps run in order unless `next` or `cases` say otherwise. Every id unique; every goto names a step; a next names another step or is `end`. Quote a value that starts with a quote or contains ": ".
|
|
65
|
+
TEXT
|
|
66
|
+
|
|
67
|
+
def register(chi)
|
|
68
|
+
chi.prompt_section("picks") { |ctx| PICKS if drawn?(ctx) }
|
|
69
|
+
chi.prompt_section("walk") { |ctx| WALK if drawn?(ctx) }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
# Whether the session's answers reach a web page that draws them.
|
|
75
|
+
def drawn?(ctx) = ctx.frontend != :one_shot && !ctx.delegate?
|
|
76
|
+
end
|