samagotchi 0.170.0 → 0.171.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +29 -1
- data/docs/architecture.md +2 -2
- data/docs/cli.md +37 -23
- data/docs/configuration.md +1 -1
- data/docs/internals/block-renderers.md +2 -2
- data/docs/internals/bridge.md +5 -4
- data/docs/internals/context-telemetry.md +13 -1
- data/docs/internals/engine.md +4 -4
- data/docs/internals/inline-widgets.md +1 -1
- data/docs/internals/objectives.md +1 -1
- data/docs/internals/participants.md +235 -119
- data/docs/internals/web-frontend.md +164 -33
- data/docs/internals/working-set.md +69 -5
- data/docs/releasing.md +2 -1
- data/docs/sessions.md +6 -3
- data/lib/samagotchi/bridge/turn_accumulator.rb +8 -4
- data/lib/samagotchi/bridge.rb +39 -12
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/delivery.rb +13 -0
- data/lib/samagotchi/engine.rb +5 -1
- data/lib/samagotchi/event_fanout.rb +22 -1
- data/lib/samagotchi/grants.rb +5 -0
- data/lib/samagotchi/guardrails/review_paths.rb +2 -2
- data/lib/samagotchi/image_store.rb +51 -12
- data/lib/samagotchi/kernel_loop.rb +7 -1
- data/lib/samagotchi/llm/stray_tool_call.rb +7 -4
- data/lib/samagotchi/llm_context_stale.rb +4 -1
- data/lib/samagotchi/participant_messages.rb +39 -0
- data/lib/samagotchi/participant_revise.rb +61 -0
- data/lib/samagotchi/participant_uploads.rb +98 -0
- data/lib/samagotchi/reply_wait/poll.rb +5 -1
- data/lib/samagotchi/revise_anchor.rb +9 -0
- data/lib/samagotchi/send_command.rb +5 -0
- data/lib/samagotchi/session_inbox.rb +20 -12
- data/lib/samagotchi/session_manager/delivery.rb +7 -0
- data/lib/samagotchi/session_stores.rb +2 -0
- data/lib/samagotchi/tool_runner.rb +39 -5
- data/lib/samagotchi/tools/memory.rb +26 -7
- data/lib/samagotchi/turn_anchors.rb +0 -3
- data/lib/samagotchi/turn_stages/verify_anchors.rb +13 -1
- data/lib/samagotchi/verify_anchor.rb +6 -0
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +42 -77
- data/lib/samagotchi/web/participant_access.rb +129 -26
- data/lib/samagotchi/web/participant_images.rb +185 -0
- data/lib/samagotchi/web/participant_review.rb +19 -138
- data/lib/samagotchi/web/participant_show.rb +240 -0
- data/lib/samagotchi/web/participant_stream_filter.rb +25 -2
- data/lib/samagotchi/web/participant_view.rb +331 -24
- data/lib/samagotchi/web/public/annotate_bar.css +1 -2
- data/lib/samagotchi/web/public/annotate_bar.js +1 -2
- data/lib/samagotchi/web/public/app.js +124 -88
- data/lib/samagotchi/web/public/block_host.js +7 -4
- data/lib/samagotchi/web/public/bubbles.js +9 -1
- data/lib/samagotchi/web/public/cards_offscreen.js +69 -0
- data/lib/samagotchi/web/public/chip_popover.js +5 -4
- data/lib/samagotchi/web/public/claim_link.js +4 -4
- data/lib/samagotchi/web/public/composer.css +4 -6
- data/lib/samagotchi/web/public/composer_mode.js +11 -6
- data/lib/samagotchi/web/public/composer_mount.js +4 -4
- data/lib/samagotchi/web/public/ctx.js +12 -3
- data/lib/samagotchi/web/public/delivery_line.js +34 -5
- data/lib/samagotchi/web/public/history_shape.js +5 -1
- data/lib/samagotchi/web/public/images.js +7 -1
- data/lib/samagotchi/web/public/index.html +40 -4
- data/lib/samagotchi/web/public/info_bar_model.js +2 -1
- data/lib/samagotchi/web/public/markdown_place.js +5 -4
- data/lib/samagotchi/web/public/owner_claim.js +3 -3
- data/lib/samagotchi/web/public/owner_verify.js +0 -2
- data/lib/samagotchi/web/public/participant_bar.js +108 -0
- data/lib/samagotchi/web/public/participant_main.js +289 -0
- data/lib/samagotchi/web/public/participant_model.js +5 -292
- data/lib/samagotchi/web/public/participant_names.js +6 -1
- data/lib/samagotchi/web/public/participant_revise.js +66 -0
- data/lib/samagotchi/web/public/participants_panel.js +3 -12
- data/lib/samagotchi/web/public/question_cards.js +14 -3
- data/lib/samagotchi/web/public/refs.js +53 -36
- data/lib/samagotchi/web/public/reload_check.js +68 -11
- data/lib/samagotchi/web/public/revise_stack.js +21 -10
- data/lib/samagotchi/web/public/send_turn.js +24 -6
- data/lib/samagotchi/web/public/session_fields.js +1 -1
- data/lib/samagotchi/web/public/session_state.js +12 -4
- data/lib/samagotchi/web/public/session_sync.js +9 -2
- data/lib/samagotchi/web/public/session_view.js +17 -7
- data/lib/samagotchi/web/public/stage_asking.js +6 -4
- data/lib/samagotchi/web/public/stage_owner_wait.js +35 -0
- data/lib/samagotchi/web/public/stage_view.js +18 -4
- data/lib/samagotchi/web/public/stage_waiting.js +74 -6
- data/lib/samagotchi/web/public/storage.js +22 -0
- data/lib/samagotchi/web/public/stream_handlers.js +2 -1
- data/lib/samagotchi/web/public/tokens.css +1 -1
- data/lib/samagotchi/web/public/turn_block.js +3 -2
- data/lib/samagotchi/web/public/turn_events.js +15 -6
- data/lib/samagotchi/web/public/turn_html.js +9 -2
- data/lib/samagotchi/web/public/turn_range.js +1 -2
- data/lib/samagotchi/web/public/verify_target.js +2 -4
- data/lib/samagotchi/web/public/viewer.js +59 -0
- data/lib/samagotchi/web/public/viewer_self.js +53 -0
- data/lib/samagotchi/web/public/working_set.js +11 -5
- data/lib/samagotchi/web/public/working_set_view.js +53 -8
- data/lib/samagotchi/web/public/ws_cards.js +89 -0
- data/lib/samagotchi/web/public/ws_peek.js +106 -0
- data/lib/samagotchi/web/public/ws_preview.css +126 -0
- data/lib/samagotchi/web/public/ws_reader.js +319 -0
- data/lib/samagotchi/web/public/ws_timeline.js +200 -0
- data/lib/samagotchi/web/public/ws_way_in.js +25 -0
- data/lib/samagotchi/web/session_control.rb +39 -14
- data/lib/samagotchi/web/session_create.rb +1 -1
- data/lib/samagotchi/web/session_show.rb +75 -28
- data/lib/samagotchi/web/viewer.rb +70 -0
- data/lib/samagotchi/web/working_set_routes.rb +132 -0
- data/lib/samagotchi/worker.rb +19 -14
- data/lib/samagotchi/working_set.rb +260 -8
- metadata +20 -6
- data/lib/samagotchi/web/public/participant.html +0 -218
- data/lib/samagotchi/web/public/participant.js +0 -719
- data/lib/samagotchi/web/public/participant_annotate.js +0 -76
- data/lib/samagotchi/web/public/review_history.js +0 -72
- data/lib/samagotchi/web/public/tools_overview.js +0 -42
|
@@ -7,7 +7,7 @@ export const CLAIM_ROOTS = ".bubble.output, .bubble.user .user-message, .gen-tex
|
|
|
7
7
|
// The owner page's claim links (used by app.js; claim_link.js does the text
|
|
8
8
|
// and DOM work): a claim link opened on this page (#/s/<id>?t=…&q=…, or the
|
|
9
9
|
// participant's #s=<id>&t=… opened in this browser) shown once its session is
|
|
10
|
-
// drawn, and annotate's "Copy link" for a selection.
|
|
10
|
+
// drawn, and annotate's "Copy link" for a selection. A participant's page has
|
|
11
11
|
// its own copies on purpose (docs/internals/web-frontend.md, Two pages).
|
|
12
12
|
//
|
|
13
13
|
// +historyEl+: #history. +store+: the page's session store (selected,
|
|
@@ -22,7 +22,7 @@ export function createOwnerClaim({
|
|
|
22
22
|
// A claim link waiting for its session to be drawn (selectFromHash sets it).
|
|
23
23
|
let pendingClaim = null;
|
|
24
24
|
|
|
25
|
-
// How
|
|
25
|
+
// How a participant's page shows this page's text (claim_link.js).
|
|
26
26
|
function opts() {
|
|
27
27
|
return { home, cwd: store.current.dir || "" };
|
|
28
28
|
}
|
|
@@ -50,7 +50,7 @@ export function createOwnerClaim({
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
// Annotate's "Copy link" for a selection in a turn's answer, step or own
|
|
53
|
-
// message:
|
|
53
|
+
// message: a participant's page's link when the session has a person
|
|
54
54
|
// active (it opens in the browser their review link was opened in: one
|
|
55
55
|
// link per browser, the cookie), else this page's own.
|
|
56
56
|
function linkFor(source, range) {
|
|
@@ -12,8 +12,6 @@ import { OWNER_FINAL, verifyTurnOf } from "./verify_target.js";
|
|
|
12
12
|
// verdict's card (verdict_fresh.js), Verify's claim from a selection, and
|
|
13
13
|
// the composer's armed claim (anchor/set/clear: the one holder; the next
|
|
14
14
|
// send makes it a Verify turn).
|
|
15
|
-
// The participant page has its own copies on purpose
|
|
16
|
-
// (docs/internals/web-frontend.md, Two pages).
|
|
17
15
|
//
|
|
18
16
|
// +historyEl+: #history. +store+: the page's session store (it writes
|
|
19
17
|
// current.verdicts and current.verifyFresh through store.update; reads
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// What the main page shows a participant in the owner's bar's places
|
|
2
|
+
// (the participant main page, scenes 1-5), as data and lit views, no page
|
|
3
|
+
// state: participant_main.js feeds it.
|
|
4
|
+
//
|
|
5
|
+
// the top row their name ("Alex · invited") and, on a desktop, when the
|
|
6
|
+
// link ends ("link ends 20 Nov")
|
|
7
|
+
// the bottom the composer card's footer: the status, context %, the
|
|
8
|
+
// row speed (the turn's time while one runs), their spending
|
|
9
|
+
// ("$0.42 of $2.00 used"), and on a desktop what their
|
|
10
|
+
// turns may do ("chi acts under the owner's rules")
|
|
11
|
+
// the closed the composer given way to one line: their limit reached,
|
|
12
|
+
// line the session stopped, the link ended
|
|
13
|
+
//
|
|
14
|
+
// Never the owner's cost, memories, the model or a path: only what the
|
|
15
|
+
// show's viewer (ParticipantShow#participant_main_viewer) and the
|
|
16
|
+
// session's numbers carry. See docs/internals/web-frontend.md (The
|
|
17
|
+
// participant's page).
|
|
18
|
+
import { html, nothing } from "./vendor/lit-html.js";
|
|
19
|
+
import { ctxBarText, speedText, costText } from "./ctx.js";
|
|
20
|
+
import { formatDuration } from "./timing.js";
|
|
21
|
+
|
|
22
|
+
// "20 Nov" (the viewer's locale's day and month) of an ISO time; "" when
|
|
23
|
+
// it doesn't parse.
|
|
24
|
+
export function endsText(iso) {
|
|
25
|
+
const at = Date.parse(iso || "");
|
|
26
|
+
if (!Number.isFinite(at)) return "";
|
|
27
|
+
return new Date(at).toLocaleDateString(undefined, { day: "numeric", month: "short" });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// "$0.42 of $2.00 used", "$0.42 used" without a cap; "" when their grant's
|
|
31
|
+
// exposure doesn't show the spend.
|
|
32
|
+
export function spentText(usage) {
|
|
33
|
+
const spent = Number(usage?.spent);
|
|
34
|
+
if (usage?.spent == null || !Number.isFinite(spent)) return "";
|
|
35
|
+
const shown = spent > 0 ? costText(spent) : "$0.00";
|
|
36
|
+
const cap = Number(usage?.cap);
|
|
37
|
+
return usage?.cap != null && Number.isFinite(cap) ? `${shown} of ${money(cap)} used` : `${shown} used`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const money = (value) => `$${Number(value).toFixed(2)}`;
|
|
41
|
+
|
|
42
|
+
// What their turns may do, by the grant's mode (pair: they act; review:
|
|
43
|
+
// they only read).
|
|
44
|
+
export const RULES_TEXT = { pair: "chi acts under the owner's rules", review: "your turns only read" };
|
|
45
|
+
|
|
46
|
+
// The bottom row's items: [{key, text, tip}].
|
|
47
|
+
// status "idle" | "running" | "waiting"
|
|
48
|
+
// ctxPct the context's fill (store.current.ctxPct), windowSource its window's source
|
|
49
|
+
// speed {decode_tps, source} of the last generation, or null
|
|
50
|
+
// turnMs the running turn's time so far, or null
|
|
51
|
+
// usage the show's viewer.usage
|
|
52
|
+
// mode the show's viewer.mode
|
|
53
|
+
export function barItems({ status, ctxPct = null, windowSource = null, speed = null, turnMs = null, usage = null, mode = null }) {
|
|
54
|
+
const items = [{ key: "status", text: status, tip: STATUS_TIPS[status] || "" }];
|
|
55
|
+
items.push({ key: "ctx", text: ctxBarText(ctxPct, windowSource), tip: "How full the session's context is" });
|
|
56
|
+
if (turnMs != null) items.push({ key: "time", text: formatDuration(turnMs), tip: "This turn's time so far" });
|
|
57
|
+
else {
|
|
58
|
+
const tps = speedText(speed?.decode_tps, speed?.source);
|
|
59
|
+
if (tps) items.push({ key: "speed", text: tps, tip: "Decode speed of the last generation" });
|
|
60
|
+
}
|
|
61
|
+
const spent = spentText(usage);
|
|
62
|
+
if (spent) items.push({ key: "spent", text: spent, tip: "What your turns have cost, of the limit the owner set for you" });
|
|
63
|
+
const rules = RULES_TEXT[mode] || "";
|
|
64
|
+
return { items, rules };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const STATUS_TIPS = {
|
|
68
|
+
idle: "No turn runs now",
|
|
69
|
+
running: "A turn runs now",
|
|
70
|
+
waiting: "The running turn waits for the owner",
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export function barTemplate({ items, rules }) {
|
|
74
|
+
return html`${items.map((item) => html`<span class=${`pb-pill pp-${item.key}`} data-key=${item.key} title=${item.tip || nothing}>${item.text}</span>`)}<span class="pb-gap"></span>${rules ? html`<span class="pb-rules">${rules}</span>` : nothing}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// The top row's chip and the link's end.
|
|
78
|
+
// +color+: their author colour (participant_model.js authorColor), as their badges.
|
|
79
|
+
export function whoTemplate({ name, endsAt, color = null }) {
|
|
80
|
+
const ends = endsText(endsAt);
|
|
81
|
+
return html`<span class="pb-who" title="You were invited to this session by its owner" style=${color ? `--who-color:${color}` : nothing}><b>${name || "you"}</b> · invited</span>${ends ? html`<span class="pb-ends" title=${`This link stops working on ${ends}`}>link ends ${ends}</span>` : nothing}`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// The closed composer's line, or null when the composer stays:
|
|
85
|
+
// capReached the show's viewer.cap_reached (or a send refused for it)
|
|
86
|
+
// usage the show's viewer.usage (the cap's amount)
|
|
87
|
+
// refusal {code, text}: the server's words for a send refused as
|
|
88
|
+
// session_stopped, or the link that ended (token_revoked,
|
|
89
|
+
// token_expired): shown as they are
|
|
90
|
+
// → {kind, title, text}
|
|
91
|
+
export function closedLine({ capReached = false, usage = null, refusal = null } = {}) {
|
|
92
|
+
if (refusal?.text) {
|
|
93
|
+
const text = String(refusal.text);
|
|
94
|
+
return { kind: refusal.code || "refused", title: `${text[0].toUpperCase()}${text.slice(1)}${/[.!?]$/.test(text) ? "" : "."}`, text: "You can keep reading." };
|
|
95
|
+
}
|
|
96
|
+
if (!capReached) return null;
|
|
97
|
+
const cap = Number(usage?.cap);
|
|
98
|
+
const amount = usage?.cap != null && Number.isFinite(cap) ? `the ${money(cap)}` : "the limit";
|
|
99
|
+
return { kind: "cap_reached", title: `You have used ${amount} the owner set for you.`, text: "You can keep reading. Ask the owner to raise the limit to send more." };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function closedTemplate(line) {
|
|
103
|
+
return html`<b>${line.title}</b>${line.text}`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// The codes of a refused send that close the composer (the rest are a
|
|
107
|
+
// toast with the server's words): their state, not this message's.
|
|
108
|
+
export const CLOSING_CODES = Object.freeze(["cap_reached", "session_stopped", "token_revoked", "token_expired"]);
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
// The main page for a participant (an invited person's link): what is
|
|
2
|
+
// theirs on the owner's page, wired from app.js in a few lines. The
|
|
3
|
+
// owner's page never makes it (app.js: viewer.kind() is "participant").
|
|
4
|
+
//
|
|
5
|
+
// - The grant's session (<body data-session>) is the page's one session,
|
|
6
|
+
// whatever the hash says; no start page, no list.
|
|
7
|
+
// - Who they are (viewer_self.js), from each show's `viewer`: their own
|
|
8
|
+
// turns and prompts, the owner's labelled "owner".
|
|
9
|
+
// - The top row's name and the link's end, the bottom row in the bar's
|
|
10
|
+
// place, the closed composer (participant_bar.js).
|
|
11
|
+
// - The running turn: whose it is (turn_started's author and `mine`), so
|
|
12
|
+
// Stop turn shows on their own only and the delivery line says where a
|
|
13
|
+
// message goes; and whether it waits for the owner (question_requested
|
|
14
|
+
// / continue_offered's `kind`; stage_owner_wait.js draws it).
|
|
15
|
+
// - Their usage and limit, read again from the show at each turn's end
|
|
16
|
+
// (the light re-reads carry no viewer).
|
|
17
|
+
// - A link that ended mid-session (a 401 from chi web): the composer gives
|
|
18
|
+
// way to the server's sentence, instead of the owner's signed-out banner.
|
|
19
|
+
//
|
|
20
|
+
// See docs/internals/web-frontend.md (The participant's page).
|
|
21
|
+
import { render } from "./vendor/lit-html.js";
|
|
22
|
+
import { setViewerSelf } from "./viewer_self.js";
|
|
23
|
+
import { authorColor, participantAuthor } from "./participant_model.js";
|
|
24
|
+
import { barItems, barTemplate, CLOSING_CODES, closedLine, closedTemplate, whoTemplate } from "./participant_bar.js";
|
|
25
|
+
|
|
26
|
+
// The worker features a participant's composer reads: their message goes
|
|
27
|
+
// into their own running turn at its next step (Delivery::OWN_TURN_STEER_FEATURE).
|
|
28
|
+
export const PARTICIPANT_STEER = "participant_steer";
|
|
29
|
+
// A 401 whose words the page couldn't read (an auth proxy's own answer).
|
|
30
|
+
export const LINK_ENDED = "this link no longer opens the session";
|
|
31
|
+
|
|
32
|
+
// The events that end a turn: whose turn it was and its wait go, and the
|
|
33
|
+
// viewer is read again (their spend moved).
|
|
34
|
+
const TURN_ENDS = ["turn_completed", "turn_canceled", "turn_failed"];
|
|
35
|
+
// The owner's question, approval or Continue offer, and their answers:
|
|
36
|
+
// chi web sends a participant the kind alone, never a card's content.
|
|
37
|
+
const WAITS = ["question_requested", "continue_offered"];
|
|
38
|
+
const WAIT_ENDS = ["question_answered", "question_cancelled", "continue_resolved"];
|
|
39
|
+
|
|
40
|
+
// @param body the page's <body> (data-session, data-session-title)
|
|
41
|
+
// @param topBar #topBar: the logo, title, their name, the link's end
|
|
42
|
+
// @param infoBar #infoBar: the bottom row takes its place
|
|
43
|
+
// @param composer #composer: given way to the closed line
|
|
44
|
+
// @param store the session store (store.current: ctxPct, timing, workerFeatures)
|
|
45
|
+
// @param running() a turn runs (liveTurn.running)
|
|
46
|
+
// @param stage the stage view (turnDom): redraw()
|
|
47
|
+
// @param read(id) the session's show (data.js getSession)
|
|
48
|
+
// @param probe(id) a fetch Response of the session's brief read, whose 401 carries the ended link's words
|
|
49
|
+
// @param onClosed() the composer closed or opened again (the Revise buttons follow)
|
|
50
|
+
export function createParticipantMain({ body, topBar, infoBar, composer, store, running, stage, read, onClosed = () => {},
|
|
51
|
+
probe = (id) => fetch(`/api/sessions/${encodeURIComponent(id)}?brief=1`, { headers: { Accept: "application/json" } }) }) {
|
|
52
|
+
const sessionId = body.dataset.session || null;
|
|
53
|
+
let viewer = null;
|
|
54
|
+
// The running turn's author: {author, name, mine}; null between turns.
|
|
55
|
+
let turnBy = null;
|
|
56
|
+
// What the running turn waits for the owner on (a kind), or null.
|
|
57
|
+
let waitKind = null;
|
|
58
|
+
// The last generation's speed ({decode_tps, source}).
|
|
59
|
+
let speed = null;
|
|
60
|
+
// A refusal that closed the composer ({code, text}).
|
|
61
|
+
let refusal = null;
|
|
62
|
+
let reading = null;
|
|
63
|
+
// A second turn ended while a re-read was out: one more after it.
|
|
64
|
+
let readAgain = false;
|
|
65
|
+
// What the last show said the session waits for the owner on: a reload
|
|
66
|
+
// or a resync mid-wait gets no question event (no cards for them), only
|
|
67
|
+
// this; the replay's turn_started keeps it.
|
|
68
|
+
let showWait = null;
|
|
69
|
+
// Live turn starts so far: a re-read begun before one is stale about the wait.
|
|
70
|
+
let starts = 0;
|
|
71
|
+
// The session's title as the last show read it (chi web's participant title).
|
|
72
|
+
let title = null;
|
|
73
|
+
|
|
74
|
+
// The top row: a plain logo (not the owner's new-session button), their
|
|
75
|
+
// name and the link's end after the title.
|
|
76
|
+
const logo = document.createElement("span");
|
|
77
|
+
logo.className = "brand plain-brand";
|
|
78
|
+
logo.textContent = "chi";
|
|
79
|
+
topBar.prepend(logo);
|
|
80
|
+
const who = document.createElement("span");
|
|
81
|
+
who.id = "participantWho";
|
|
82
|
+
topBar.querySelector("#sessionTitleRow")?.after(who);
|
|
83
|
+
// The bottom row, in the owner's info line's place.
|
|
84
|
+
const row = document.createElement("div");
|
|
85
|
+
row.id = "participantBar";
|
|
86
|
+
infoBar.prepend(row);
|
|
87
|
+
infoBar.querySelector("#infoText")?.setAttribute("hidden", "");
|
|
88
|
+
const closed = document.createElement("div");
|
|
89
|
+
closed.id = "participantClosed";
|
|
90
|
+
closed.hidden = true;
|
|
91
|
+
composer.after(closed);
|
|
92
|
+
body.classList.add("participant-view");
|
|
93
|
+
|
|
94
|
+
const redraw = () => store.touch("infoBar");
|
|
95
|
+
|
|
96
|
+
function drawTop() {
|
|
97
|
+
const color = viewer?.id ? authorColor(participantAuthor(viewer.id)) : null;
|
|
98
|
+
render(whoTemplate({ name: viewer?.name, endsAt: viewer?.usage?.expires_at, color }), who);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function drawClosed() {
|
|
102
|
+
const line = closedLine({ capReached: !!viewer?.cap_reached, usage: viewer?.usage, refusal });
|
|
103
|
+
const flips = closed.hidden === !!line;
|
|
104
|
+
closed.hidden = !line;
|
|
105
|
+
composer.hidden = !!line;
|
|
106
|
+
body.classList.toggle("participant-closed", !!line);
|
|
107
|
+
if (line) {
|
|
108
|
+
closed.dataset.kind = line.kind;
|
|
109
|
+
render(closedTemplate(line), closed);
|
|
110
|
+
}
|
|
111
|
+
if (flips) onClosed();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// A show's viewer: who they are, their usage, whether the session waits.
|
|
115
|
+
// +fresh+ false: a re-read a turn started after (its wait is stale).
|
|
116
|
+
function shown(next, { fresh = true } = {}) {
|
|
117
|
+
if (!next || next.kind !== "participant") return;
|
|
118
|
+
viewer = next;
|
|
119
|
+
setViewerSelf(next);
|
|
120
|
+
// A session the owner started again (it runs, or was read not stopped)
|
|
121
|
+
// takes the stopped line back.
|
|
122
|
+
if (refusal?.code === "session_stopped" && !next.session_stopped) refusal = null;
|
|
123
|
+
if (fresh) {
|
|
124
|
+
showWait = next.waiting_for_owner || null;
|
|
125
|
+
if (running()) waitKind = showWait;
|
|
126
|
+
}
|
|
127
|
+
drawTop();
|
|
128
|
+
drawClosed();
|
|
129
|
+
stage.redraw?.();
|
|
130
|
+
redraw();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// The viewer again from the show (its usage moved, or its cap).
|
|
134
|
+
async function reread() {
|
|
135
|
+
if (!sessionId) return null;
|
|
136
|
+
if (reading) {
|
|
137
|
+
readAgain = true;
|
|
138
|
+
return reading;
|
|
139
|
+
}
|
|
140
|
+
const at = starts;
|
|
141
|
+
reading = read(sessionId).then((data) => {
|
|
142
|
+
if (typeof data?.session?.title === "string" && data.session.title) title = data.session.title;
|
|
143
|
+
shown(data?.viewer, { fresh: at === starts });
|
|
144
|
+
}, () => {}).finally(() => {
|
|
145
|
+
reading = null;
|
|
146
|
+
if (readAgain) {
|
|
147
|
+
readAgain = false;
|
|
148
|
+
reread();
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
return reading;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function started(data) {
|
|
155
|
+
turnBy = { author: data?.author || null, name: data?.author_name || null, mine: !!data?.mine };
|
|
156
|
+
// A replayed start (a load or resync mid-turn) is the turn the show read:
|
|
157
|
+
// its wait stands. A live one starts unwaited.
|
|
158
|
+
if (data?.replayed) waitKind = showWait;
|
|
159
|
+
else {
|
|
160
|
+
starts += 1;
|
|
161
|
+
waitKind = null;
|
|
162
|
+
showWait = null;
|
|
163
|
+
}
|
|
164
|
+
if (refusal?.code === "session_stopped") {
|
|
165
|
+
refusal = null;
|
|
166
|
+
drawClosed();
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// The stream's handlers (stream_handlers.js) with a participant's own
|
|
171
|
+
// reading of the events chi web sends them bare or cut down.
|
|
172
|
+
function wrapHandlers(handlers) {
|
|
173
|
+
const wrapped = { ...handlers };
|
|
174
|
+
wrapped.turn_started = (data) => {
|
|
175
|
+
started(data);
|
|
176
|
+
handlers.turn_started?.(data);
|
|
177
|
+
stage.redraw?.();
|
|
178
|
+
};
|
|
179
|
+
for (const type of TURN_ENDS) {
|
|
180
|
+
wrapped[type] = (data) => {
|
|
181
|
+
waitKind = null;
|
|
182
|
+
if (!data?.replayed) showWait = null;
|
|
183
|
+
const out = handlers[type]?.(data);
|
|
184
|
+
turnBy = null;
|
|
185
|
+
reread();
|
|
186
|
+
redraw();
|
|
187
|
+
return out;
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
// No card: the stage's waiting look says for what kind of thing.
|
|
191
|
+
for (const type of WAITS) {
|
|
192
|
+
wrapped[type] = (data) => {
|
|
193
|
+
waitKind = data?.kind || "question";
|
|
194
|
+
stage.redraw?.();
|
|
195
|
+
redraw();
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
for (const type of WAIT_ENDS) {
|
|
199
|
+
wrapped[type] = (data) => {
|
|
200
|
+
waitKind = null;
|
|
201
|
+
if (!data?.replayed) showWait = null;
|
|
202
|
+
stage.redraw?.();
|
|
203
|
+
redraw();
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
wrapped.generation_completed = (data) => {
|
|
207
|
+
if (data?.speed && Number.isFinite(Number(data.speed.decode_tps))) speed = data.speed;
|
|
208
|
+
return handlers.generation_completed?.(data);
|
|
209
|
+
};
|
|
210
|
+
return wrapped;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function lastSpeed() {
|
|
214
|
+
if (speed) return speed;
|
|
215
|
+
const records = store.current.timing?.turnRecords || [];
|
|
216
|
+
const last = [...records].reverse().find((r) => Number(r.decode_tps) > 0);
|
|
217
|
+
return last ? { decode_tps: last.decode_tps, source: last.tps_source } : null;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function drawBar() {
|
|
221
|
+
const isRunning = running();
|
|
222
|
+
const status = isRunning ? (waitKind ? "waiting" : "running") : "idle";
|
|
223
|
+
const startedAt = Date.parse(store.current.timing?.activeTurn?.started_at || "");
|
|
224
|
+
const turnMs = isRunning && Number.isFinite(startedAt) ? Math.max(0, Date.now() - startedAt) : null;
|
|
225
|
+
render(barTemplate(barItems({
|
|
226
|
+
status, ctxPct: store.current.ctxPct, windowSource: store.current.ctxWindowSource, speed: lastSpeed(), turnMs,
|
|
227
|
+
usage: viewer?.usage, mode: viewer?.mode,
|
|
228
|
+
})), row);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// A refused send (send_turn.js): the codes that close the composer
|
|
232
|
+
// close it (the cap: from the show read again); @return whether it was one.
|
|
233
|
+
function refused(error) {
|
|
234
|
+
const code = error?.code;
|
|
235
|
+
if (!CLOSING_CODES.includes(code)) return false;
|
|
236
|
+
if (code === "cap_reached") {
|
|
237
|
+
viewer = viewer ? { ...viewer, cap_reached: true } : viewer;
|
|
238
|
+
drawClosed();
|
|
239
|
+
reread();
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
242
|
+
refusal = { code, text: error.body?.detail || error.message };
|
|
243
|
+
drawClosed();
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// chi web refused the page's credential (data.js onAuthLost): their link
|
|
248
|
+
// ended. The server's sentence: a refused send's own (refused), else the
|
|
249
|
+
// 401 of a brief read.
|
|
250
|
+
let ended = false;
|
|
251
|
+
async function linkEnded() {
|
|
252
|
+
if (ended) return;
|
|
253
|
+
ended = true;
|
|
254
|
+
let found = null;
|
|
255
|
+
try {
|
|
256
|
+
const res = await probe(sessionId);
|
|
257
|
+
const answer = await res.json();
|
|
258
|
+
if (res.status === 401 && answer?.detail) found = { code: answer.error || "token_revoked", text: answer.detail };
|
|
259
|
+
} catch (_) { /* unreachable or not JSON: the words below */ }
|
|
260
|
+
if (refusal?.code?.startsWith("token_")) return;
|
|
261
|
+
refusal = found || { code: "token_revoked", text: LINK_ENDED };
|
|
262
|
+
drawClosed();
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return {
|
|
266
|
+
sessionId,
|
|
267
|
+
shown,
|
|
268
|
+
wrapHandlers,
|
|
269
|
+
drawBar,
|
|
270
|
+
refused,
|
|
271
|
+
linkEnded,
|
|
272
|
+
// The session title the top row shows: the one chi web gave the page.
|
|
273
|
+
title: () => title || body.dataset.sessionTitle || "",
|
|
274
|
+
// Whether their composer is closed (no Revise then: participant_revise.js).
|
|
275
|
+
closed: () => !closed.hidden,
|
|
276
|
+
// Stop turn: on their own turn only.
|
|
277
|
+
canCancel: () => !!turnBy?.mine,
|
|
278
|
+
// What the stage's waiting look says, while a turn runs.
|
|
279
|
+
ownerWait: () => (running() ? waitKind : null),
|
|
280
|
+
// The delivery line's input (delivery_line.js viewerTurn): whose turn
|
|
281
|
+
// runs, and whether a message goes into it (their own, on a worker that
|
|
282
|
+
// takes it).
|
|
283
|
+
deliveryTurn: () => ({
|
|
284
|
+
own: !!turnBy?.mine,
|
|
285
|
+
steer: !!turnBy?.mine && (store.current.workerFeatures || []).includes(PARTICIPANT_STEER),
|
|
286
|
+
by: turnBy?.author && !turnBy.mine ? turnBy.name || "another participant" : null,
|
|
287
|
+
}),
|
|
288
|
+
};
|
|
289
|
+
}
|