samagotchi 0.152.0 → 0.153.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 +18 -1
- data/docs/bundle-authoring.md +1 -1
- data/docs/bundles.md +84 -0
- data/docs/cli.md +3 -3
- data/docs/configuration.md +19 -14
- data/docs/guardrails.md +7 -5
- data/docs/hooks.md +4 -3
- data/docs/internals/anthropic.md +3 -3
- data/docs/internals/attached-context.md +3 -0
- data/docs/internals/engine.md +9 -5
- data/docs/internals/participants.md +1 -1
- data/docs/internals/prompt-caching.md +7 -3
- data/docs/internals/web-frontend.md +20 -17
- data/docs/plugins.md +29 -15
- data/docs/releasing.md +1 -1
- data/docs/sessions.md +1 -1
- data/docs/testing.md +2 -2
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/bundles/web-search/manifest.yml +13 -0
- data/lib/samagotchi/bundles/web-search/plugin.rb +174 -0
- data/lib/samagotchi/engine.rb +10 -5
- data/lib/samagotchi/guardrails/review_boundary.rb +4 -3
- data/lib/samagotchi/llm_context_stale.rb +2 -2
- data/lib/samagotchi/message_parts.rb +3 -2
- data/lib/samagotchi/toolkit.rb +8 -5
- data/lib/samagotchi/toolkit_binding.rb +30 -5
- data/lib/samagotchi/toolkit_report_command.rb +9 -5
- data/lib/samagotchi/toolkit_suggest.rb +12 -6
- data/lib/samagotchi/tools/builtin_calls.rb +14 -10
- data/lib/samagotchi/tools/deferred_tools.rb +2 -2
- data/lib/samagotchi/tools/registry.rb +44 -20
- data/lib/samagotchi/tools/tool_index.rb +27 -21
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/public/app.js +61 -357
- data/lib/samagotchi/web/public/composer_mode.js +110 -0
- data/lib/samagotchi/web/public/owner_claim.js +81 -0
- data/lib/samagotchi/web/public/owner_verify.js +112 -0
- data/lib/samagotchi/web/public/session_fields.js +3 -3
- data/lib/samagotchi/web/public/strip_grip.js +3 -3
- data/lib/samagotchi/web/public/strip_hidden.js +65 -0
- data/lib/samagotchi/web/public/version_notices.js +132 -0
- data/lib/samagotchi/worker.rb +6 -0
- metadata +8 -1
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// The composer's mode on the owner page (used by app.js, the store's
|
|
2
|
+
// `composer` redraw): its placeholder, key hint, action button, the attach
|
|
3
|
+
// button, the start page's empty state and the delivery line's row, from the
|
|
4
|
+
// open session, its owner and whether its turn runs.
|
|
5
|
+
|
|
6
|
+
// A phone's composer fits a few words and has no Shift+Enter: the short
|
|
7
|
+
// placeholder there.
|
|
8
|
+
export const NARROW_QUERY = "(max-width: 600px)";
|
|
9
|
+
|
|
10
|
+
// The key hint under the composer (shown while it has focus; on the start
|
|
11
|
+
// page only: in a session the placeholder carries it, the hint row is gone).
|
|
12
|
+
export const SEND_HINT = "Enter to send · Shift+Enter for a newline · ↑ history";
|
|
13
|
+
|
|
14
|
+
// What the composer shows, pure: +selected+ the open session's id (null: the
|
|
15
|
+
// start page), +owner+ its owner, +running+ whether its turn runs, +narrow+ a
|
|
16
|
+
// phone's width. While a turn runs, Send still sends: the prompt merges into
|
|
17
|
+
// the turn at its next step (steering); Cancel is its own button then.
|
|
18
|
+
// @return {placeholder, hint, button, disabled, empty, clearHistory,
|
|
19
|
+
// attachHidden, delivery}: +disabled+ undefined while a turn runs (the
|
|
20
|
+
// button keeps whatever a send in flight set); +empty+ the start page's
|
|
21
|
+
// empty state shown, +clearHistory+ #history emptied (the start page only);
|
|
22
|
+
// +delivery+ the delivery line's mode (delivery_line.js show).
|
|
23
|
+
export function composerMode({ selected, owner, running, narrow }) {
|
|
24
|
+
const session = !!selected;
|
|
25
|
+
const tui = session && owner === "tui";
|
|
26
|
+
const live = !!running && session;
|
|
27
|
+
const pick = (long, short) => (narrow ? short : long);
|
|
28
|
+
// No images for a session a terminal owns (it takes no message from here);
|
|
29
|
+
// on a phone none while a turn runs either: the row keeps its room for the
|
|
30
|
+
// message box (steering).
|
|
31
|
+
const attachHidden = tui || (live && !!narrow);
|
|
32
|
+
// The delivery line's row in a session: the key hint while idle, where a
|
|
33
|
+
// message goes in while the turn runs. A terminal owns the session: the
|
|
34
|
+
// composer is off, so is its row.
|
|
35
|
+
const delivery = !session || tui ? "off" : running ? "running" : "idle";
|
|
36
|
+
const base = { attachHidden, delivery, empty: false, clearHistory: false };
|
|
37
|
+
if (live) {
|
|
38
|
+
return {
|
|
39
|
+
...base,
|
|
40
|
+
placeholder: pick("Running… a message goes in at the next step · Enter to send · Alt+Enter queues", "Steer the turn…"),
|
|
41
|
+
hint: SEND_HINT, button: "Send", disabled: undefined,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
if (!session) {
|
|
45
|
+
return {
|
|
46
|
+
...base,
|
|
47
|
+
placeholder: "Ask chi anything…", hint: "Enter to start · Shift+Enter for a newline", button: "Start", disabled: false,
|
|
48
|
+
empty: true, clearHistory: true,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
if (tui) {
|
|
52
|
+
return {
|
|
53
|
+
...base,
|
|
54
|
+
placeholder: pick("A terminal chi owns this session · run it with chi --shared to send from here", "A terminal chi owns this session"),
|
|
55
|
+
hint: "", button: "Send", disabled: true,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
// The keys are in the delivery line's row above.
|
|
59
|
+
return { ...base, placeholder: "Message chi…", hint: SEND_HINT, button: "Send", disabled: false };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// The composer's mode drawn: update() is the store's `composer` redraw, and a
|
|
63
|
+
// window crossing the phone's width redraws it too (an input the store
|
|
64
|
+
// doesn't hold, with the composer as its only reader: the bar doesn't read it).
|
|
65
|
+
//
|
|
66
|
+
// +els+: {body, prompt, hint, button, attach, empty, history}. +store+: the
|
|
67
|
+
// page's session store (selected, current.owner; read only). +liveTurn+:
|
|
68
|
+
// live_turn.js's (running). +startPage+: start_page.js's (restoreStartPick,
|
|
69
|
+
// syncModelPick, dropChoices). +deliveryControl+: delivery_line.js's.
|
|
70
|
+
// +moreSheet+: more_sheet.js's (⋯ is a phone's alone). +fitPrompt()+,
|
|
71
|
+
// +placeToast()+: the page's. +win+: the window (matchMedia).
|
|
72
|
+
export function createComposerMode({
|
|
73
|
+
els, store, liveTurn, startPage, deliveryControl, moreSheet, fitPrompt, placeToast, win = globalThis,
|
|
74
|
+
}) {
|
|
75
|
+
const narrowScreen = win.matchMedia(NARROW_QUERY);
|
|
76
|
+
let hadSession = false;
|
|
77
|
+
|
|
78
|
+
function update() {
|
|
79
|
+
// In a session the box is one line: its minimum follows the class.
|
|
80
|
+
if (els.body.classList.toggle("has-session", !!store.selected) !== hadSession) {
|
|
81
|
+
hadSession = !hadSession;
|
|
82
|
+
// Back on the start page: its remembered model, not the last link's.
|
|
83
|
+
if (!hadSession) startPage.restoreStartPick();
|
|
84
|
+
fitPrompt();
|
|
85
|
+
placeToast();
|
|
86
|
+
}
|
|
87
|
+
startPage.syncModelPick();
|
|
88
|
+
// The start page's choice was for a new session: opening a session drops it.
|
|
89
|
+
if (store.selected) startPage.dropChoices();
|
|
90
|
+
const mode = composerMode({
|
|
91
|
+
selected: store.selected, owner: store.current.owner, running: liveTurn.running, narrow: narrowScreen.matches,
|
|
92
|
+
});
|
|
93
|
+
els.attach.hidden = mode.attachHidden;
|
|
94
|
+
deliveryControl.show(mode.delivery);
|
|
95
|
+
els.prompt.placeholder = mode.placeholder;
|
|
96
|
+
els.hint.textContent = mode.hint;
|
|
97
|
+
els.button.textContent = mode.button;
|
|
98
|
+
if (mode.disabled !== undefined) els.button.disabled = mode.disabled;
|
|
99
|
+
els.empty.classList.toggle("hidden", !mode.empty);
|
|
100
|
+
if (mode.clearHistory) els.history.replaceChildren();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
narrowScreen.addEventListener("change", () => {
|
|
104
|
+
update();
|
|
105
|
+
// ⋯ is a phone's alone: a window grown past it takes its sheet along.
|
|
106
|
+
if (!narrowScreen.matches && moreSheet.isOpen()) moreSheet.close();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
return { update };
|
|
110
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { ownerClaimHash, participantClaimLink, selectionClaim, showClaim, turnIdOf } from "./claim_link.js";
|
|
2
|
+
import { sessionHash } from "./route.js";
|
|
3
|
+
|
|
4
|
+
// Where a claim's text can be on the owner's page: answers, own messages, steps.
|
|
5
|
+
export const CLAIM_ROOTS = ".bubble.output, .bubble.user .user-message, .gen-text";
|
|
6
|
+
|
|
7
|
+
// The owner page's claim links (used by app.js; claim_link.js does the text
|
|
8
|
+
// and DOM work): a claim link opened on this page (#/s/<id>?t=…&q=…, or the
|
|
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. The participant page has
|
|
11
|
+
// its own copies on purpose (docs/internals/web-frontend.md, Two pages).
|
|
12
|
+
//
|
|
13
|
+
// +historyEl+: #history. +store+: the page's session store (selected,
|
|
14
|
+
// current.dir, sessions; read only). +claimTarget()+: participants_panel.js's
|
|
15
|
+
// (the people a link would open for, or null). +toast(text, action,
|
|
16
|
+
// options)+, +copyText(text)+ (copy.js), +allViewOpen()+: the page's.
|
|
17
|
+
// +home+: the home folder the page shows as ~ (body data-home). +win+: the
|
|
18
|
+
// window (location, history).
|
|
19
|
+
export function createOwnerClaim({
|
|
20
|
+
historyEl, store, claimTarget, toast, copyText, allViewOpen, home = "", win = globalThis,
|
|
21
|
+
}) {
|
|
22
|
+
// A claim link waiting for its session to be drawn (selectFromHash sets it).
|
|
23
|
+
let pendingClaim = null;
|
|
24
|
+
|
|
25
|
+
// How the participant page shows this page's text (claim_link.js).
|
|
26
|
+
function opts() {
|
|
27
|
+
return { home, cwd: store.current.dir || "" };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function pending(claim) {
|
|
31
|
+
pendingClaim = claim;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// The claim link's quote scrolled to and marked, its text read as the
|
|
35
|
+
// participant page shows it (the session's folder relative, home ~); a
|
|
36
|
+
// quote not found marks its turn; neither found: the top, and a toast.
|
|
37
|
+
// The hash is the session's again (select() already set it when it
|
|
38
|
+
// switched), so the same link opened again is a hashchange.
|
|
39
|
+
function showPending() {
|
|
40
|
+
const claim = pendingClaim;
|
|
41
|
+
if (!claim || claim.sessionId !== store.selected) return;
|
|
42
|
+
pendingClaim = null;
|
|
43
|
+
const { location, history } = win;
|
|
44
|
+
if (!allViewOpen() && location.hash !== sessionHash(store.selected)) {
|
|
45
|
+
history.replaceState(null, "", location.pathname + location.search + sessionHash(store.selected));
|
|
46
|
+
}
|
|
47
|
+
if (showClaim(historyEl, claim, { roots: CLAIM_ROOTS, opts: opts() })) return;
|
|
48
|
+
historyEl.scrollTop = 0;
|
|
49
|
+
toast("The linked turn isn't in this session.");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Annotate's "Copy link" for a selection in a turn's answer, step or own
|
|
53
|
+
// message: the participant page's link when the session has a person
|
|
54
|
+
// active (it opens in the browser their review link was opened in: one
|
|
55
|
+
// link per browser, the cookie), else this page's own.
|
|
56
|
+
function linkFor(source, range) {
|
|
57
|
+
// Not in a Revise stack's switched copy (revise_switch.js: view only, no claim key; Verify isn't offered there either).
|
|
58
|
+
if (!store.selected || !["answer", "step", "user"].includes(source.kind) || !historyEl.contains(source.root)
|
|
59
|
+
|| source.root.closest("[data-stack-view]")) return null;
|
|
60
|
+
const turnId = turnIdOf(historyEl, source.root);
|
|
61
|
+
if (!turnId) return null;
|
|
62
|
+
const claim = { turnId, ...selectionClaim(source.root, range, opts()) };
|
|
63
|
+
if (!claim.q) return null;
|
|
64
|
+
const target = claimTarget();
|
|
65
|
+
if (!target) {
|
|
66
|
+
const { location } = win;
|
|
67
|
+
return { url: `${location.origin}${location.pathname}${location.search}${ownerClaimHash(store.selected, claim)}`,
|
|
68
|
+
title: "A link to this on your page (no person is invited to this session)" };
|
|
69
|
+
}
|
|
70
|
+
const whose = target.names.map((name) => `${name}'s`).join(" or ");
|
|
71
|
+
return { url: participantClaimLink(target.base, store.selected, claim),
|
|
72
|
+
title: `A link to this for ${whose} browser: it opens where their review link was opened (one link per browser). Opened here, it shows your page.` };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function copyLink({ url }) {
|
|
76
|
+
if (await copyText(url)) toast("Link copied");
|
|
77
|
+
else toast(`Couldn't copy: ${url}`, null, { sticky: true });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return { opts, pending, showPending, linkFor, copyLink };
|
|
81
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { selectionClaim } from "./claim_link.js";
|
|
2
|
+
import { CLAIM_ROOTS } from "./owner_claim.js";
|
|
3
|
+
import { authorBadge } from "./participant_names.js";
|
|
4
|
+
import { sessionHash } from "./route.js";
|
|
5
|
+
import { CHECK_NOT_ON_PAGE, ownerCheckHash } from "./verdict_card.js";
|
|
6
|
+
import { freshURL, placeVerdictWithFresh, settleRunning } from "./verdict_fresh.js";
|
|
7
|
+
import { VERIFY_UNAVAILABLE, verifyWorkerUnavailable } from "./verify_preset.js";
|
|
8
|
+
import { OWNER_FINAL, verifyTurnOf } from "./verify_target.js";
|
|
9
|
+
|
|
10
|
+
// The owner page's Verify (used by app.js): its verdict cards in the history
|
|
11
|
+
// (a live verdict, a session's saved ones), Verify fresh on an unclear
|
|
12
|
+
// verdict's card (verdict_fresh.js), Verify's claim from a selection, and
|
|
13
|
+
// the composer's armed claim (anchor/set/clear: the one holder; the next
|
|
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
|
+
//
|
|
18
|
+
// +historyEl+: #history. +store+: the page's session store (it writes
|
|
19
|
+
// current.verdicts and current.verifyFresh through store.update; reads
|
|
20
|
+
// selected, sessions, workerFeatures, bridgeUp). +api(path, opts)+: data.js's.
|
|
21
|
+
// +claim+: owner_claim.js's (its opts()). +liveTurn+: live_turn.js's (whether
|
|
22
|
+
// a turn runs, its id). +toast(text)+: the page's. +win+: the window (a
|
|
23
|
+
// verdict's check in another session opens by its hash).
|
|
24
|
+
export function createOwnerVerify({ historyEl, store, api, claim, liveTurn, toast, win = globalThis }) {
|
|
25
|
+
// Verify fresh's failed starts, by check (verdict_fresh.js keeps them here).
|
|
26
|
+
const freshErrors = {};
|
|
27
|
+
// The composer's armed Verify claim ({turn_id, quote}) or null: set by the
|
|
28
|
+
// Verify preset, a restored Verify turn's draft; let go by an emptied
|
|
29
|
+
// composer, another session, the send's ack.
|
|
30
|
+
let armed = null;
|
|
31
|
+
|
|
32
|
+
function anchor() {
|
|
33
|
+
return armed;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function set(next) {
|
|
37
|
+
armed = next;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// clear() lets go of whatever is armed; clear(a) only of +a+ (a send's ack
|
|
41
|
+
// for the claim it took, not one armed since).
|
|
42
|
+
function clear(...only) {
|
|
43
|
+
if (only.length && only[0] !== armed) return;
|
|
44
|
+
armed = null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// A verdict ({check_turn_id, verdict, fresh?, by?…}) onto its check's
|
|
48
|
+
// card: into the session's list once, and a fresh one ends its check's
|
|
49
|
+
// "verifying fresh".
|
|
50
|
+
function placeVerdict(record) {
|
|
51
|
+
if (!record) return;
|
|
52
|
+
const known = (store.current.verdicts || []).some((item) => item.check_turn_id === record.check_turn_id);
|
|
53
|
+
const patch = {};
|
|
54
|
+
if (!known) patch.verdicts = [...(store.current.verdicts || []), record];
|
|
55
|
+
// A fresh verdict ends its check's "verifying fresh" (the card shows it).
|
|
56
|
+
if (record.fresh) patch.verifyFresh = settleRunning(store.current.verifyFresh, record);
|
|
57
|
+
store.update({ current: patch });
|
|
58
|
+
const badge = record.by && authorBadge(record.by);
|
|
59
|
+
placeVerdictWithFresh(historyEl, record, {
|
|
60
|
+
who: badge ? badge.name : "You",
|
|
61
|
+
author: record.by || null,
|
|
62
|
+
roots: CLAIM_ROOTS,
|
|
63
|
+
opts: claim.opts(),
|
|
64
|
+
onMissingCheck: (missing) => {
|
|
65
|
+
const hash = ownerCheckHash(store.sessions, store.selected, missing.check_turn_id);
|
|
66
|
+
if (hash) win.location.hash = hash;
|
|
67
|
+
else toast(CHECK_NOT_ON_PAGE);
|
|
68
|
+
},
|
|
69
|
+
}, freshState());
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// The owner's Verify fresh on an unclear verdict's card (verdict_fresh.js):
|
|
73
|
+
// always theirs to start; its running check links to the child's session.
|
|
74
|
+
function freshState() {
|
|
75
|
+
const id = store.selected;
|
|
76
|
+
const unavailable = verifyWorkerUnavailable(store.current.workerFeatures, store.current.bridgeUp);
|
|
77
|
+
return {
|
|
78
|
+
verdicts: store.current.verdicts || [],
|
|
79
|
+
running: store.current.verifyFresh || [],
|
|
80
|
+
errors: freshErrors,
|
|
81
|
+
canStart: !!id && !unavailable,
|
|
82
|
+
unavailableReason: unavailable ? VERIFY_UNAVAILABLE : null,
|
|
83
|
+
start: async (of) => {
|
|
84
|
+
try {
|
|
85
|
+
const body = await api(freshURL(id, of), { method: "POST", body: "{}" });
|
|
86
|
+
return { ok: true, child_id: body.child_id || null };
|
|
87
|
+
} catch (error) {
|
|
88
|
+
return { ok: false, error: error.message };
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
childHref: (childId) => sessionHash(childId),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// The session's saved verdicts, onto the history just drawn.
|
|
96
|
+
function renderSaved() {
|
|
97
|
+
for (const record of store.current.verdicts || []) placeVerdict(record);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Verify's claim: a selection in an ended turn's final answer (the only
|
|
101
|
+
// text Engine#verified_anchor takes; verify_target.js), else null and no
|
|
102
|
+
// Verify on the bar.
|
|
103
|
+
function anchorFor({ source, range }) {
|
|
104
|
+
if (source?.kind !== "answer" || !range) return null;
|
|
105
|
+
const running = liveTurn.running ? { turnId: liveTurn.timing.turnId || null } : null;
|
|
106
|
+
const turnId = verifyTurnOf(historyEl, source.root, OWNER_FINAL, { running });
|
|
107
|
+
const quote = turnId ? selectionClaim(source.root, range, claim.opts()).q : "";
|
|
108
|
+
return turnId && quote ? { turn_id: turnId, quote } : null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return { placeVerdict, freshState, renderSaved, anchorFor, anchor, set, clear };
|
|
112
|
+
}
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// - live_timing.js sets `timing().activeTurn` in place (start, finish,
|
|
11
11
|
// finishLine): the running turn's own clock, which mergeTurnTiming keeps.
|
|
12
12
|
// - verdict_fresh.js `startFresh` pushes onto `fresh.running`, which is
|
|
13
|
-
// `verifyFresh` (
|
|
13
|
+
// `verifyFresh` (owner_verify.js freshState hands the array out): returning the
|
|
14
14
|
// entry for an update would leave the card holding the old array.
|
|
15
15
|
//
|
|
16
16
|
// A new module: the page's modules are cached at unversioned URLs, and a
|
|
@@ -46,12 +46,12 @@ export const FIELDS = {
|
|
|
46
46
|
verdicts: {
|
|
47
47
|
source: (d) => list(d.session?.verdicts),
|
|
48
48
|
redraws: [],
|
|
49
|
-
writers: "placeVerdict (its card draws itself: placeVerdictWithFresh)",
|
|
49
|
+
writers: "owner_verify.js placeVerdict (its card draws itself: placeVerdictWithFresh)",
|
|
50
50
|
},
|
|
51
51
|
verifyFresh: {
|
|
52
52
|
source: (d) => list(d.session?.verify_fresh),
|
|
53
53
|
redraws: [],
|
|
54
|
-
writers: "placeVerdict (settleRunning); verdict_fresh.js startFresh pushes in place (D8)",
|
|
54
|
+
writers: "owner_verify.js placeVerdict (settleRunning); verdict_fresh.js startFresh pushes in place (D8)",
|
|
55
55
|
},
|
|
56
56
|
createdAt: { redraws: ["infoBar"], writers: "the payload" },
|
|
57
57
|
ctxPct: { redraws: ["infoBar"], writers: "stream_handlers.js (generation_chunk, context_status)" },
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { GRIP_OPEN_PX } from "./composer_size.js";
|
|
2
2
|
import { stripGripAction, stripGripTitle } from "./strip.js";
|
|
3
3
|
|
|
4
|
-
// The latest-sessions strip's grip (used by
|
|
4
|
+
// The latest-sessions strip's grip (used by strip_hidden.js): the handle on the edge
|
|
5
5
|
// between the strip and the chat, the composer's grip's twin, mirrored. A
|
|
6
6
|
// drag up (or a tap) hides the strip, a drag down (or a tap) shows it, and
|
|
7
7
|
// Enter or Space toggles too. The strip snaps: unlike the composer's box it
|
|
8
8
|
// does not follow the pointer, so nothing is measured while dragging — only
|
|
9
9
|
// whether the release would act (.will-toggle, the pill in the accent).
|
|
10
10
|
// The grip owns its title and aria-expanded (composer_grip.js's
|
|
11
|
-
// drawFoldControls does the same for the composer's):
|
|
12
|
-
// returned refresh() from
|
|
11
|
+
// drawFoldControls does the same for the composer's): strip_hidden.js calls
|
|
12
|
+
// the returned refresh() from its apply, and nothing else writes them.
|
|
13
13
|
// No click listener: a click still fires after pointerdown's preventDefault
|
|
14
14
|
// and would toggle twice. No document listener either: the composer's grip
|
|
15
15
|
// needs one for typing on the page, this one acts only on itself.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { readStored, writeStored } from "./storage.js";
|
|
2
|
+
import { SHORT_WINDOW_QUERY, stripAutoHidden } from "./strip.js";
|
|
3
|
+
import { createStripGrip } from "./strip_grip.js";
|
|
4
|
+
|
|
5
|
+
// The saved choice (storage.js): the strip hidden; chi_zen is zen mode's old
|
|
6
|
+
// name, read when the new key was never written.
|
|
7
|
+
export const STRIP_HIDDEN_KEY = "chi_strip_hidden";
|
|
8
|
+
const ZEN_KEY = "chi_zen";
|
|
9
|
+
|
|
10
|
+
// The latest-sessions strip hidden or shown (used by app.js): the body's
|
|
11
|
+
// strip-hidden class, the grip (strip_grip.js) that toggles it, and the short
|
|
12
|
+
// window's rule. The strip can be hidden for more chat room; the choice stays
|
|
13
|
+
// per browser (it replaced zen mode, so a zen user starts hidden). A short
|
|
14
|
+
// window hides the strip by itself, so the stage keeps room for a card that
|
|
15
|
+
// waits (its question and choices); the grip brings it back until the window
|
|
16
|
+
// is tall again. Not saved: the saved choice is the user's own hide or show.
|
|
17
|
+
//
|
|
18
|
+
// +gripEl+: #stripGrip. +body+: the page's body (the class). +onApply()+:
|
|
19
|
+
// after each change (the top bar's title follows: app.js updateSessionTitle).
|
|
20
|
+
// +closeFamilyPop()+: a hide takes the strip's family popover along. +win+:
|
|
21
|
+
// the window (matchMedia). +storage+: storage.js's guarded read and write.
|
|
22
|
+
// Applied once at creation.
|
|
23
|
+
export function createStripHidden({
|
|
24
|
+
gripEl, body = globalThis.document.body, onApply, closeFamilyPop, win = globalThis,
|
|
25
|
+
storage = { read: readStored, write: writeStored },
|
|
26
|
+
}) {
|
|
27
|
+
const shortWindow = win.matchMedia(SHORT_WINDOW_QUERY);
|
|
28
|
+
let shownWhileShort = false;
|
|
29
|
+
|
|
30
|
+
function saved() {
|
|
31
|
+
const value = storage.read(STRIP_HIDDEN_KEY);
|
|
32
|
+
return value === null ? storage.read(ZEN_KEY) === "1" : value === "1";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// The grip is given the hidden state and the toggle; it speaks its own
|
|
36
|
+
// title and aria-expanded, refreshed after every change.
|
|
37
|
+
const gripRefresh = createStripGrip({
|
|
38
|
+
gripEl,
|
|
39
|
+
isHidden: () => body.classList.contains("strip-hidden"),
|
|
40
|
+
setHidden: (hidden) => setHidden(hidden),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
function apply() {
|
|
44
|
+
body.classList.toggle("strip-hidden", stripAutoHidden({
|
|
45
|
+
saved: saved(), short: shortWindow.matches, shownWhileShort,
|
|
46
|
+
}));
|
|
47
|
+
gripRefresh();
|
|
48
|
+
onApply();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function setHidden(hidden) {
|
|
52
|
+
if (hidden) closeFamilyPop();
|
|
53
|
+
storage.write(STRIP_HIDDEN_KEY, hidden ? "1" : "0");
|
|
54
|
+
shownWhileShort = !hidden && shortWindow.matches;
|
|
55
|
+
apply();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
apply();
|
|
59
|
+
shortWindow.addEventListener("change", () => {
|
|
60
|
+
if (!shortWindow.matches) shownWhileShort = false;
|
|
61
|
+
apply();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
return { setHidden };
|
|
65
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { html } from "./vendor/lit-html.js";
|
|
2
|
+
import { cantText } from "./format.js";
|
|
3
|
+
import { workerBadgeFor } from "./info_bar_model.js";
|
|
4
|
+
import { readStored, writeStored } from "./storage.js";
|
|
5
|
+
import { createUpdateSheet } from "./update_sheet.js";
|
|
6
|
+
import { mergeVersionState, restartConfirmText, updateFrameToast, versionNotice } from "./update_notice.js";
|
|
7
|
+
|
|
8
|
+
// An available release's notice is once per version per browser, not per
|
|
9
|
+
// tab: chi web says it in every snapshot until the owner updates.
|
|
10
|
+
export const AVAILABLE_SHOWN_KEY = "chi_update_available_shown";
|
|
11
|
+
|
|
12
|
+
// The owner page's chi versions (used by app.js): the versions carried by each
|
|
13
|
+
// event snapshot and `chi` frame, the toasts they bring (chi web upgraded, a
|
|
14
|
+
// newer one installed, an aged one available to update to), the update sheet
|
|
15
|
+
// (update_sheet.js) and its `chi_update` frames, and the open session's
|
|
16
|
+
// worker Restart.
|
|
17
|
+
//
|
|
18
|
+
// +store+: the page's session store (selected and current read; it writes no
|
|
19
|
+
// field, only touch("infoBar"): the worker badge and the Restart button read
|
|
20
|
+
// the versions and the restarting set). +toasts+: toasts.js's. +dialog+: the
|
|
21
|
+
// update sheet's <dialog>. +loadedVersion+: the chi this page was loaded
|
|
22
|
+
// from (body data-version). +api+: {updateChi, restartWorkers, restartWeb,
|
|
23
|
+
// restartSession} (data.js). +confirm+, +reload+, +clipboard()+: the
|
|
24
|
+
// browser's; +storage+: storage.js's guarded read and write.
|
|
25
|
+
export function createVersionNotices({
|
|
26
|
+
store, toasts, dialog, loadedVersion, api,
|
|
27
|
+
confirm = (text) => globalThis.confirm(text),
|
|
28
|
+
reload = () => globalThis.location.reload(),
|
|
29
|
+
clipboard = () => globalThis.navigator?.clipboard,
|
|
30
|
+
storage = { read: readStored, write: writeStored },
|
|
31
|
+
}) {
|
|
32
|
+
const toast = (text, action = null, options = {}) => { toasts.show(text, action, options); };
|
|
33
|
+
const sheet = createUpdateSheet({
|
|
34
|
+
dialog,
|
|
35
|
+
update: api.updateChi,
|
|
36
|
+
restartWorkers: api.restartWorkers,
|
|
37
|
+
restartWeb: api.restartWeb,
|
|
38
|
+
});
|
|
39
|
+
// Once per notice per tab: every reconnect's snapshot says it again.
|
|
40
|
+
const versions = { served: null, installed: null };
|
|
41
|
+
// The sessions whose Restart is under way (their button says so): one
|
|
42
|
+
// session's restart never blocks another's.
|
|
43
|
+
const restarting = new Set();
|
|
44
|
+
const shown = new Set();
|
|
45
|
+
const availableShown = storage.read(AVAILABLE_SHOWN_KEY);
|
|
46
|
+
if (availableShown) shown.add(`available:${availableShown}`);
|
|
47
|
+
let versionToast = null;
|
|
48
|
+
|
|
49
|
+
function notice(data) {
|
|
50
|
+
Object.assign(versions, mergeVersionState(versions, data));
|
|
51
|
+
const next = versionNotice({ loaded: loadedVersion, served: versions.served,
|
|
52
|
+
installed: versions.installed, available: versions.available,
|
|
53
|
+
updateBlocked: versions.updateBlocked, auto: versions.auto, restart: versions.restart,
|
|
54
|
+
autoOffers: versions.autoOffers, shown });
|
|
55
|
+
store.touch("infoBar"); // the worker badge compares with the newest installed
|
|
56
|
+
if (!next) return;
|
|
57
|
+
shown.add(next.key);
|
|
58
|
+
if (next.key.startsWith("available:")) storage.write(AVAILABLE_SHOWN_KEY, next.key.slice("available:".length));
|
|
59
|
+
const text = next.releaseUrl
|
|
60
|
+
? html`${next.text} <a href=${next.releaseUrl} target="_blank" rel="noopener noreferrer">RubyGems</a>`
|
|
61
|
+
: next.text;
|
|
62
|
+
const action = next.reload
|
|
63
|
+
? { label: "Reload", run: () => reload() }
|
|
64
|
+
: next.updateVersion
|
|
65
|
+
? { label: "Update", run: () => sheet.install(next.updateVersion) }
|
|
66
|
+
: next.actions
|
|
67
|
+
? next.actions.map((name) => ({
|
|
68
|
+
label: name === "restartWorkers" ? "Restart workers" : "Restart chi web",
|
|
69
|
+
run: () => (name === "restartWorkers" ? sheet.restartWorkers() : sheet.restartWeb()),
|
|
70
|
+
}))
|
|
71
|
+
: next.command
|
|
72
|
+
? { label: "Copy", run: () => copyUpdateCommand(next.command) }
|
|
73
|
+
: null;
|
|
74
|
+
// One version toast at a time: a newer notice (Reload after a restart)
|
|
75
|
+
// supersedes the one before it.
|
|
76
|
+
toasts.dismiss(versionToast);
|
|
77
|
+
versionToast = toasts.show(text, action, { sticky: true });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// A `chi_update` frame (+snapshot+: the one an event snapshot carries).
|
|
81
|
+
function frame(data, { snapshot = false } = {}) {
|
|
82
|
+
const wasOpen = sheet.isOpen();
|
|
83
|
+
if (!sheet.receive(data)) return;
|
|
84
|
+
if (data.operation === "install" && data.state === "done" && data.version) notice({ installed: data.version });
|
|
85
|
+
const text = updateFrameToast(data, { wasOpen, snapshot });
|
|
86
|
+
if (text) toast(text, { label: "View", run: () => sheet.view() }, { sticky: true });
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// An event snapshot's version after Restart chi web: the sheet says done.
|
|
90
|
+
function reconnected(version) {
|
|
91
|
+
if (sheet.reconnected(version)) toast(`chi web reconnected on chi ${version}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function copyUpdateCommand(command) {
|
|
95
|
+
const board = clipboard();
|
|
96
|
+
if (!board?.writeText) {
|
|
97
|
+
toast(`Clipboard unavailable; run ${command}`);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
board.writeText(command).then(
|
|
101
|
+
() => toast(`Copied ${command}`),
|
|
102
|
+
() => toast(`Couldn't copy ${command}`),
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// The open session's worker badge (info_bar_model.js workerBadgeFor).
|
|
107
|
+
function badge() {
|
|
108
|
+
return workerBadgeFor(store.selected, store.current, versions);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Restart: the session moves to a new worker on the newest chi; the page
|
|
112
|
+
// follows it (its bridge_up), the composer's draft stays as it is.
|
|
113
|
+
async function restart() {
|
|
114
|
+
const current = badge();
|
|
115
|
+
if (!current || !current.restart || restarting.has(store.selected)) return;
|
|
116
|
+
if (!confirm(restartConfirmText(store.current.workerVersion, current.newest))) return;
|
|
117
|
+
const id = store.selected;
|
|
118
|
+
restarting.add(id);
|
|
119
|
+
store.touch("infoBar");
|
|
120
|
+
try {
|
|
121
|
+
const result = await api.restartSession(id);
|
|
122
|
+
toast(result.version ? `Restarted on chi ${result.version}` : "Restarting: the new worker is starting");
|
|
123
|
+
} catch (e) {
|
|
124
|
+
toast(cantText("restart", e), null, { sticky: true });
|
|
125
|
+
} finally {
|
|
126
|
+
restarting.delete(id);
|
|
127
|
+
store.touch("infoBar");
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return { notice, frame, reconnected, versions: () => versions, restarting, restart };
|
|
132
|
+
}
|
data/lib/samagotchi/worker.rb
CHANGED
|
@@ -1793,6 +1793,12 @@ module Samagotchi
|
|
|
1793
1793
|
# once) and leaves the recap to the conversation's next real pause.
|
|
1794
1794
|
def left(result)
|
|
1795
1795
|
deleting = result == :exit_requested && @exit_deletes
|
|
1796
|
+
# The attached context goes with this process: a change that landed after
|
|
1797
|
+
# the leaving pass looked at it has no later pass to note it, so note it
|
|
1798
|
+
# here (no wake turn: the worker is on its way out). Not into a session
|
|
1799
|
+
# /exit --delete deletes next: its save could land after the delete and
|
|
1800
|
+
# write the session back.
|
|
1801
|
+
absorb_context unless deleting
|
|
1796
1802
|
# The watch goes with this process: a task that finished since the
|
|
1797
1803
|
# last look (nothing held the exit for it: queue, a note-only task,
|
|
1798
1804
|
# /exit) is a plain note for the next turn, not lost. Not into a
|
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.153.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dmitry Dedov
|
|
@@ -289,6 +289,8 @@ files:
|
|
|
289
289
|
- lib/samagotchi/bundles/system/manifest.yml
|
|
290
290
|
- lib/samagotchi/bundles/system/memory_guide.md
|
|
291
291
|
- lib/samagotchi/bundles/system/self_map.md
|
|
292
|
+
- lib/samagotchi/bundles/web-search/manifest.yml
|
|
293
|
+
- lib/samagotchi/bundles/web-search/plugin.rb
|
|
292
294
|
- lib/samagotchi/bundles/work-cap/manifest.yml
|
|
293
295
|
- lib/samagotchi/bundles/work-cap/plugin.rb
|
|
294
296
|
- lib/samagotchi/cache_confirm.rb
|
|
@@ -855,6 +857,7 @@ files:
|
|
|
855
857
|
- lib/samagotchi/web/public/composer.js
|
|
856
858
|
- lib/samagotchi/web/public/composer_fold.js
|
|
857
859
|
- lib/samagotchi/web/public/composer_grip.js
|
|
860
|
+
- lib/samagotchi/web/public/composer_mode.js
|
|
858
861
|
- lib/samagotchi/web/public/composer_mount.js
|
|
859
862
|
- lib/samagotchi/web/public/composer_size.js
|
|
860
863
|
- lib/samagotchi/web/public/context_chips.js
|
|
@@ -904,6 +907,8 @@ files:
|
|
|
904
907
|
- lib/samagotchi/web/public/objectives_moves.js
|
|
905
908
|
- lib/samagotchi/web/public/objectives_strip.js
|
|
906
909
|
- lib/samagotchi/web/public/objectives_wiring.js
|
|
910
|
+
- lib/samagotchi/web/public/owner_claim.js
|
|
911
|
+
- lib/samagotchi/web/public/owner_verify.js
|
|
907
912
|
- lib/samagotchi/web/public/participant.html
|
|
908
913
|
- lib/samagotchi/web/public/participant.js
|
|
909
914
|
- lib/samagotchi/web/public/participant_annotate.js
|
|
@@ -960,6 +965,7 @@ files:
|
|
|
960
965
|
- lib/samagotchi/web/public/stream_handlers.js
|
|
961
966
|
- lib/samagotchi/web/public/strip.js
|
|
962
967
|
- lib/samagotchi/web/public/strip_grip.js
|
|
968
|
+
- lib/samagotchi/web/public/strip_hidden.js
|
|
963
969
|
- lib/samagotchi/web/public/sw.js
|
|
964
970
|
- lib/samagotchi/web/public/system_one_wrong.js
|
|
965
971
|
- lib/samagotchi/web/public/tally.js
|
|
@@ -993,6 +999,7 @@ files:
|
|
|
993
999
|
- lib/samagotchi/web/public/verify_card.css
|
|
994
1000
|
- lib/samagotchi/web/public/verify_preset.js
|
|
995
1001
|
- lib/samagotchi/web/public/verify_target.js
|
|
1002
|
+
- lib/samagotchi/web/public/version_notices.js
|
|
996
1003
|
- lib/samagotchi/web/public/worker_features.js
|
|
997
1004
|
- lib/samagotchi/web/public/working_set.js
|
|
998
1005
|
- lib/samagotchi/web/public/working_set_rail.js
|