samagotchi 0.78.0 → 0.80.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 +44 -1
- data/README.md +7 -5
- data/docs/cli.md +24 -7
- data/docs/configuration.md +11 -6
- data/docs/internals/anthropic.md +12 -5
- data/docs/internals/bundles.md +4 -0
- data/docs/internals/engine.md +15 -5
- data/docs/plugins.md +144 -3
- data/docs/releasing.md +2 -2
- data/docs/sessions.md +1 -1
- data/docs/testing.md +32 -2
- data/lib/samagotchi/bootstrap/config_writer.rb +13 -3
- data/lib/samagotchi/bootstrap/probe.rb +153 -31
- data/lib/samagotchi/bootstrap_command.rb +92 -15
- data/lib/samagotchi/bridge.rb +1 -1
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/self_map.md +1 -1
- data/lib/samagotchi/continue_offer.rb +5 -5
- data/lib/samagotchi/engine.rb +40 -37
- data/lib/samagotchi/guardrails/approval.rb +6 -4
- data/lib/samagotchi/guardrails/gate.rb +7 -3
- data/lib/samagotchi/hooks/bundle_loader.rb +31 -16
- data/lib/samagotchi/hooks/registry.rb +44 -3
- data/lib/samagotchi/host_registry.rb +12 -4
- data/lib/samagotchi/llm/adapter_common.rb +8 -4
- data/lib/samagotchi/llm/anthropic_binding_recovery.rb +198 -0
- data/lib/samagotchi/llm/anthropic_messages.rb +39 -178
- data/lib/samagotchi/llm/anthropic_wire.rb +7 -1
- data/lib/samagotchi/llm/chat_loop.rb +21 -12
- data/lib/samagotchi/llm/http.rb +12 -7
- data/lib/samagotchi/llm/model_listing.rb +29 -1
- data/lib/samagotchi/llm/refusal_memo.rb +3 -2
- data/lib/samagotchi/plugin/card.rb +44 -0
- data/lib/samagotchi/plugin/context.rb +53 -8
- data/lib/samagotchi/plugin/http.rb +154 -0
- data/lib/samagotchi/plugin/http_transport.rb +77 -0
- data/lib/samagotchi/plugin/loader.rb +29 -10
- data/lib/samagotchi/question_answer.rb +29 -0
- data/lib/samagotchi/question_desk.rb +30 -22
- data/lib/samagotchi/reasoning_history.rb +2 -1
- data/lib/samagotchi/relay_verifier.rb +1 -1
- data/lib/samagotchi/testing/boundary.rb +156 -0
- data/lib/samagotchi/testing/bundle.rb +349 -0
- data/lib/samagotchi/testing/fake_executable.rb +42 -0
- data/lib/samagotchi/testing/fake_http.rb +137 -0
- data/lib/samagotchi/testing/helpers.rb +56 -0
- data/lib/samagotchi/testing/host.rb +76 -0
- data/lib/samagotchi/testing/minitest.rb +31 -0
- data/lib/samagotchi/testing/recorder.rb +138 -0
- data/lib/samagotchi/testing/recording_api.rb +27 -0
- data/lib/samagotchi/testing/rspec.rb +31 -0
- data/lib/samagotchi/testing/sandbox.rb +163 -0
- data/lib/samagotchi/testing/sessions.rb +77 -0
- data/lib/samagotchi/testing/verdict.rb +28 -0
- data/lib/samagotchi/testing/waiting.rb +36 -0
- data/lib/samagotchi/testing.rb +26 -0
- data/lib/samagotchi/tools/delegate_relay.rb +7 -6
- data/lib/samagotchi/tools/tool_index.rb +3 -0
- data/lib/samagotchi/turn_state.rb +28 -1
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/public/app.js +79 -245
- data/lib/samagotchi/web/public/composer_grip.js +231 -0
- data/lib/samagotchi/web/public/images.js +33 -0
- data/lib/samagotchi/web/public/index.html +73 -40
- data/lib/samagotchi/web/public/level_chip.js +11 -11
- data/lib/samagotchi/web/public/lists_view.js +19 -14
- data/lib/samagotchi/web/public/llm_context_chip.js +15 -15
- data/lib/samagotchi/web/public/model_picker.js +5 -6
- data/lib/samagotchi/web/public/scope.js +3 -3
- data/lib/samagotchi/web/public/stage_view.js +3 -2
- data/lib/samagotchi/web/public/storage.js +23 -0
- data/lib/samagotchi/web/public/strip.js +35 -9
- data/lib/samagotchi/web/public/strip_grip.js +76 -0
- data/lib/samagotchi/web/public/thinking_chip.js +5 -5
- data/lib/samagotchi/web/public/toasts.js +5 -4
- metadata +24 -1
|
@@ -21,14 +21,13 @@
|
|
|
21
21
|
// confirmDelete(id), setArchived(id, archive), archiveOne(id, archive)
|
|
22
22
|
// updateInfoBar(), updateSessionTitle(), showToast(text, action)
|
|
23
23
|
// allViewOpen(), openAllView(), leaveAllView(hash)
|
|
24
|
-
// setStripHidden(hidden)
|
|
25
24
|
|
|
26
25
|
import { ownerBadge, relativeTime, delegatedBy } from "./format.js";
|
|
27
26
|
import { html, nothing, render } from "./vendor/lit-html.js";
|
|
28
27
|
import { cardCtxText, ctxBarTip } from "./ctx.js";
|
|
29
28
|
import { allScopeHref, cardFolder } from "./scope.js";
|
|
30
29
|
import { sessionHash } from "./route.js";
|
|
31
|
-
import { stripColumns,
|
|
30
|
+
import { stripColumns, stripStatusParts } from "./strip.js";
|
|
32
31
|
import { createFamilyPop, familyChipTemplate, familyRowsTemplate } from "./families_view.js";
|
|
33
32
|
import { allViewChains, chainChipTemplate, chainRowsTemplate, chainTitle, foldChains } from "./chains.js";
|
|
34
33
|
import {
|
|
@@ -192,21 +191,23 @@ function ownerBadgeTemplate(s) {
|
|
|
192
191
|
return badge ? html`<span class=${`owner ${badge.kind}`} title=${badge.title}>${badge.text}</span>` : nothing;
|
|
193
192
|
}
|
|
194
193
|
|
|
195
|
-
// The strip's end: the All sessions tile (with its count)
|
|
196
|
-
//
|
|
197
|
-
//
|
|
194
|
+
// The strip's end: the All sessions tile (with its count), the strip's own
|
|
195
|
+
// last cell (the grip on the edge is the strip's handle now, not a button
|
|
196
|
+
// in here). entering / anim: as a card's (cardTemplate). id and title are
|
|
197
|
+
// bound, as class is, so the attributes keep their order.
|
|
198
198
|
export function stripTileTemplate(count, { entering = false, anim = "" } = {}) {
|
|
199
|
-
return html`<
|
|
199
|
+
return html`<button type="button" class=${entering ? "card entering" : "card"} id=${"allTile"} style=${anim || nothing} title=${"All sessions, with search"}>All sessions →<span class="count">${count}</span></button>`;
|
|
200
200
|
}
|
|
201
201
|
|
|
202
|
-
// The hidden strip's pill: "
|
|
203
|
-
// (strip.js
|
|
202
|
+
// The hidden strip's status pill: "5 sessions · ● 1 live · ● 1 waiting"
|
|
203
|
+
// (strip.js stripStatusParts). Not a toggle any more (the grip hides and
|
|
204
|
+
// shows the strip): a click opens the all-sessions view, as the tile does.
|
|
204
205
|
// The count (and the dot after it) in its own span: a phone's narrow top
|
|
205
206
|
// bar drops it when live or waiting shows (index.html).
|
|
206
207
|
export function stripPillTemplate({ count, live, waiting }) {
|
|
207
208
|
const liveT = live ? html`<span class="live"><span class="live-dot" aria-hidden="true"></span>${live}</span>` : nothing;
|
|
208
209
|
const waitingT = waiting ? html`${live ? " · " : ""}<span class="waiting"><span class="waiting-dot" aria-hidden="true"></span>${waiting}</span>` : nothing;
|
|
209
|
-
return html`<span class="pill-
|
|
210
|
+
return html`<span class="pill-count">${count}${live || waiting ? " · " : ""}</span>${liveT}${waitingT}`;
|
|
210
211
|
}
|
|
211
212
|
|
|
212
213
|
// Draws +template+ into +el+ as new nodes, as an innerHTML write did: no
|
|
@@ -222,10 +223,15 @@ export function createListsView(deps) {
|
|
|
222
223
|
const {
|
|
223
224
|
doc, win, topStripEl, allListEl, sessions, selected, scope, scopeError, eventsLive, refresh, select,
|
|
224
225
|
confirmDelete, setArchived, archiveOne, updateInfoBar, updateSessionTitle, showToast,
|
|
225
|
-
allViewOpen, openAllView, leaveAllView,
|
|
226
|
+
allViewOpen, openAllView, leaveAllView,
|
|
226
227
|
} = deps;
|
|
227
228
|
const $ = (s) => doc.querySelector(s);
|
|
228
229
|
|
|
230
|
+
// The hidden strip's status pill opens the all-sessions view (as the tile
|
|
231
|
+
// does), saving the step of showing the strip first. Bound once: the pill
|
|
232
|
+
// element stays, only its content is drawn again (draw()).
|
|
233
|
+
$("#stripStatus").addEventListener("click", openAllView);
|
|
234
|
+
|
|
229
235
|
// The strip, its count and the hidden strip's pill leave archived sessions
|
|
230
236
|
// out; the all view shows them with "include archived". sessions() keeps
|
|
231
237
|
// them all (parent chips, the open session).
|
|
@@ -469,7 +475,7 @@ export function createListsView(deps) {
|
|
|
469
475
|
// to all of them.
|
|
470
476
|
function renderTop(cards) {
|
|
471
477
|
topStripEl.classList.remove("loading");
|
|
472
|
-
|
|
478
|
+
renderStripStatus();
|
|
473
479
|
stripFamilies = new Map(cards.map((f) => [f.head.id, f]));
|
|
474
480
|
noteChainFamilies(cards);
|
|
475
481
|
const refused = scopeError();
|
|
@@ -501,7 +507,6 @@ export function createListsView(deps) {
|
|
|
501
507
|
},
|
|
502
508
|
});
|
|
503
509
|
$("#allTile").addEventListener("click", openAllView);
|
|
504
|
-
$("#stripHide").addEventListener("click", () => setStripHidden(true));
|
|
505
510
|
familyPop.refresh();
|
|
506
511
|
if (chainFrom === topStripEl) chainPop.refresh();
|
|
507
512
|
}
|
|
@@ -517,8 +522,8 @@ export function createListsView(deps) {
|
|
|
517
522
|
// The hidden strip's pill, following the list: its sessions, the number
|
|
518
523
|
// the All sessions tile shows, and the live and waiting ones (a waiting
|
|
519
524
|
// delegate too).
|
|
520
|
-
function
|
|
521
|
-
draw($("#
|
|
525
|
+
function renderStripStatus() {
|
|
526
|
+
draw($("#stripStatus"), stripPillTemplate(stripStatusParts(shownSessions())));
|
|
522
527
|
}
|
|
523
528
|
|
|
524
529
|
// A parent's card holds its delegates (families, by the newest member: a
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// worker's snapshot, a command_ran, or the server's own reading of the
|
|
8
8
|
// session file).
|
|
9
9
|
//
|
|
10
|
-
// The start page has one too (kind "new
|
|
10
|
+
// The start page has one too (kind "new session"): it shows the picked
|
|
11
11
|
// model's values (GET /api/models' llm_context) and its form keeps a
|
|
12
12
|
// choice for the next create only (POST /api/sessions' llm_context), never
|
|
13
13
|
// remembered: nothing is sent unless the user sets one.
|
|
@@ -22,7 +22,7 @@ const STRATEGIES = [
|
|
|
22
22
|
];
|
|
23
23
|
export const APPLIES = ["payoff", "next_request", "turn_end"];
|
|
24
24
|
export const SESSION = "session";
|
|
25
|
-
export const
|
|
25
|
+
export const NEW_SESSION = "new session";
|
|
26
26
|
|
|
27
27
|
const DEFAULT = "default";
|
|
28
28
|
const LAYER_ORDER = ["stale", "forget"];
|
|
@@ -43,16 +43,16 @@ export function ownValues(summary) {
|
|
|
43
43
|
return { strategy, apply: own.apply || DEFAULT, budget };
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
// The start page's choice for the new
|
|
46
|
+
// The start page's choice for the new session ({strategy: [layers], apply,
|
|
47
47
|
// budget_tokens}, the session file's shape; 0 is off) laid over the
|
|
48
48
|
// model's summary: what the chip shows, with the chosen fields' source
|
|
49
|
-
// "session" ("the new
|
|
49
|
+
// "session" ("the new session"). The model's own summary without a choice.
|
|
50
50
|
export function withChoice(summary, choice) {
|
|
51
51
|
if (!summary || !choice) return summary;
|
|
52
52
|
const merged = { ...summary, own: choice };
|
|
53
53
|
// (the budget's source and where are budget_source / budget_where)
|
|
54
54
|
const set = (field, key, value) => Object.assign(merged, { [key]: value, [`${field}_source`]: SESSION,
|
|
55
|
-
[`${field}_where`]: "the new
|
|
55
|
+
[`${field}_where`]: "the new session" });
|
|
56
56
|
if (Array.isArray(choice.strategy)) set("strategy", "strategy", strategyValue(choice.strategy));
|
|
57
57
|
if (choice.apply) set("apply", "apply", choice.apply);
|
|
58
58
|
if (typeof choice.budget_tokens === "number") {
|
|
@@ -61,13 +61,13 @@ export function withChoice(summary, choice) {
|
|
|
61
61
|
return merged;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
// The chip: its text, tooltip and whether the session (or the new
|
|
64
|
+
// The chip: its text, tooltip and whether the session (or the new session)
|
|
65
65
|
// set its own; null without a summary (an older worker).
|
|
66
66
|
export function chipModel(summary, { kind = SESSION } = {}) {
|
|
67
67
|
if (!summary || !summary.strategy) return null;
|
|
68
68
|
const own = summary.strategy_source === "session" || !!summary.own;
|
|
69
69
|
const budget = summary.budget_tokens ? `${summary.budget_tokens} tokens` : "off";
|
|
70
|
-
const what = kind ===
|
|
70
|
+
const what = kind === NEW_SESSION ? "the new session" : "this session";
|
|
71
71
|
const title = `LLM context strategy: ${summary.strategy} (${summary.strategy_where}); apply ${summary.apply} ` +
|
|
72
72
|
`(${summary.apply_where}); budget ${budget} (${summary.budget_where}). Click to change it for ${what}.`;
|
|
73
73
|
return { text: `llm ctx ${summary.strategy}${own ? ` · ${kind}` : ""}`, title, own };
|
|
@@ -107,7 +107,7 @@ export function parseBudget(text) {
|
|
|
107
107
|
return tokens >= 4000 && tokens <= 10_000_000 ? tokens : null;
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
// The new
|
|
110
|
+
// The new session's choice after a Set: +choice+ with the fields the user
|
|
111
111
|
// changed (vs +initial+, the values the form opened with) applied, a field
|
|
112
112
|
// set back to default dropped; null when nothing is left. Throws on a
|
|
113
113
|
// budget that isn't one.
|
|
@@ -160,8 +160,8 @@ export function nowText(summary) {
|
|
|
160
160
|
`budget ${budget} (${summary.budget_where})`;
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
-
// The popover's form (a lit-html template). For the new
|
|
164
|
-
//
|
|
163
|
+
// The popover's form (a lit-html template). For the new session (kind
|
|
164
|
+
// NEW_SESSION), +summary+ is the model's and +choice+ the values the form
|
|
165
165
|
// shows; +now+ is the summary its "Now:" line reads (refresh passes a newer
|
|
166
166
|
// one, the rest stays as the form opened, so lit leaves what the user typed
|
|
167
167
|
// alone); +error+ a failed Set's message. The selects take their value as
|
|
@@ -169,14 +169,14 @@ export function nowText(summary) {
|
|
|
169
169
|
// alone, the first pass renderForm makes.
|
|
170
170
|
export function formTemplate(summary, { running = false, kind = SESSION, choice = null, now = summary, error = null,
|
|
171
171
|
unset = false } = {}) {
|
|
172
|
-
const fresh = kind ===
|
|
172
|
+
const fresh = kind === NEW_SESSION;
|
|
173
173
|
const own = fresh ? ownValues({ own: choice }) : ownValues(summary);
|
|
174
174
|
const follow = (field) => (summary[`${field}_source`] === "session" ? "follow the model" : `follow the model (${summary[field]})`);
|
|
175
|
-
const when = fresh ? "For the new
|
|
175
|
+
const when = fresh ? "For the new session's first turn on; not remembered for the next one."
|
|
176
176
|
: "From the next turn's start. Turning forget on or off re-reads the whole prompt once.";
|
|
177
177
|
const chosen = (value) => (unset ? nothing : value);
|
|
178
178
|
return html`<form class="llmctx-form"><div class="ctx-pop-head"><strong>LLM context</strong
|
|
179
|
-
><span class="ctx-pop-kind">${fresh ?
|
|
179
|
+
><span class="ctx-pop-kind">${fresh ? NEW_SESSION : "this session"}</span
|
|
180
180
|
><button type="button" class="ctx-pop-close" data-act="close" aria-label="Close">×</button></div
|
|
181
181
|
><div class="ctx-pop-meta llmctx-now">${nowText(now)}</div
|
|
182
182
|
><label class="llmctx-field"><span>Strategy</span><select name="strategy" .value=${chosen(own.strategy)}>${
|
|
@@ -206,7 +206,7 @@ export function renderForm(target, summary, opts = {}) {
|
|
|
206
206
|
// (#infoText, info_bar.js: clicks are delegated); summary(): the open
|
|
207
207
|
// session's llm_context; session(): the open session's id; running(): a
|
|
208
208
|
// turn runs; run(line): sends the command.
|
|
209
|
-
// The start page's (kind
|
|
209
|
+
// The start page's (kind NEW_SESSION): summary() is the picked model's,
|
|
210
210
|
// choice() the pending choice, and choose(next) takes a Set's (null for
|
|
211
211
|
// "follow the model") instead of a command running. id and anchor (the
|
|
212
212
|
// chip's selector) keep the two controls apart.
|
|
@@ -214,7 +214,7 @@ export function createLlmContextControl({ el, summary, session = () => null, run
|
|
|
214
214
|
kind = SESSION, choice = () => null, choose = null,
|
|
215
215
|
id = "llmContextPopover", anchor = ".llmctx-chip",
|
|
216
216
|
doc = document, win = window }) {
|
|
217
|
-
const fresh = kind ===
|
|
217
|
+
const fresh = kind === NEW_SESSION;
|
|
218
218
|
// The values the form opened with.
|
|
219
219
|
let initial = null;
|
|
220
220
|
// What the open form was drawn from: the summary, the form's options and
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
// ↑/↓ PageUp/PageDown move, ⏎ picks, Esc and Tab close without a change.
|
|
11
11
|
|
|
12
12
|
import { moveSelection } from "./command_complete.js";
|
|
13
|
+
import { readStored, writeStored } from "./storage.js";
|
|
13
14
|
import {
|
|
14
15
|
MODEL_KEY, RECENT_KEY, groupRows, matchModels, modelRows, pickModel, pushRecent, recentRows, rowNote, rowTitle,
|
|
15
16
|
} from "./model_pick.js";
|
|
@@ -39,7 +40,7 @@ function headTemplate(label, note = "") {
|
|
|
39
40
|
|
|
40
41
|
function readRecent() {
|
|
41
42
|
try {
|
|
42
|
-
const list = JSON.parse(
|
|
43
|
+
const list = JSON.parse(readStored(RECENT_KEY) || "[]");
|
|
43
44
|
return Array.isArray(list) ? list : [];
|
|
44
45
|
} catch (_) {
|
|
45
46
|
return [];
|
|
@@ -47,7 +48,7 @@ function readRecent() {
|
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
// +onPick+ runs after a pick (the panel is closed by then). +remember+:
|
|
50
|
-
// whether a pick is this browser's choice for a new
|
|
51
|
+
// whether a pick is this browser's choice for a new session (stored, and in
|
|
51
52
|
// Recent); false on a fresh chain link, where a pick is that link's only.
|
|
52
53
|
export function createModelPicker({ button, panel, input, list, composer, center, onPick, remember = () => true }) {
|
|
53
54
|
let rows = [];
|
|
@@ -163,10 +164,8 @@ export function createModelPicker({ button, panel, input, list, composer, center
|
|
|
163
164
|
if (!row) return;
|
|
164
165
|
setChosen(row.name);
|
|
165
166
|
if (remember()) {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
localStorage.setItem(RECENT_KEY, JSON.stringify(pushRecent(readRecent(), row.name)));
|
|
169
|
-
} catch (_) {}
|
|
167
|
+
writeStored(MODEL_KEY, row.name);
|
|
168
|
+
writeStored(RECENT_KEY, JSON.stringify(pushRecent(readRecent(), row.name)));
|
|
170
169
|
}
|
|
171
170
|
close();
|
|
172
171
|
onPick?.(row.name);
|
|
@@ -47,7 +47,7 @@ export function cardFolder(workingDirectory, projectName) {
|
|
|
47
47
|
|
|
48
48
|
// The header's scope chip: "<project> · all" in a project view (all links
|
|
49
49
|
// to the same place without ?dir); "all projects" otherwise, with a link
|
|
50
|
-
// back to the project it came from and where a new
|
|
50
|
+
// back to the project it came from and where a new session starts on the
|
|
51
51
|
// start page. +scope+: { dir, projectName, projectDir, serverDir, backDir,
|
|
52
52
|
// backName }; +error+: why the ?dir= folder can't be shown; +where+:
|
|
53
53
|
// () => { hash, search } (the page's location, read again at a click: the
|
|
@@ -63,6 +63,6 @@ export function scopeTemplate(scope, { error = null, where = () => ({ hash: "",
|
|
|
63
63
|
const back = scope.backName
|
|
64
64
|
? html`<a class="scope-all scope-back" href=${projectScopeHref(scope.backDir, at.hash, at.search)} title=${`Sessions of ${scope.backName} only`} @click=${(e) => { e.currentTarget.href = backHref(); }}>← ${scope.backName}</a>`
|
|
65
65
|
: nothing;
|
|
66
|
-
const
|
|
67
|
-
return html`<span class="scope-name" title="Sessions of every project">all projects</span>${back}${
|
|
66
|
+
const newSession = dir ? html`<span class="scope-where" title=${`New sessions start in ${dir}`}>new session in<span class="path"><bdi>${dir}</bdi></span></span>` : nothing;
|
|
67
|
+
return html`<span class="scope-name" title="Sessions of every project">all projects</span>${back}${newSession}`;
|
|
68
68
|
}
|
|
@@ -22,6 +22,7 @@ import { createStageFrame, reducedMotion } from "./stage_frame.js";
|
|
|
22
22
|
import { createInUseWatch } from "./stage_watch.js";
|
|
23
23
|
import { formatDuration } from "./timing.js";
|
|
24
24
|
import { easeScrollTo } from "./scroll.js";
|
|
25
|
+
import { readStored, writeStored } from "./storage.js";
|
|
25
26
|
|
|
26
27
|
const COLLAPSE_KEY = "chi_stage_collapsed";
|
|
27
28
|
// The turn-end read (markdown, answer_display) settles within this, or the
|
|
@@ -41,7 +42,7 @@ const PENDING = ".bubble.question:not(.answered):not(.cancelled), .bubble.contin
|
|
|
41
42
|
const ASKS = ".plugin-card[data-in-turn] .card-action";
|
|
42
43
|
|
|
43
44
|
function readCollapsed() {
|
|
44
|
-
|
|
45
|
+
return readStored(COLLAPSE_KEY) === "1";
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
// +historyNearBottom+: the history's own follow check (the hand-off scrolls
|
|
@@ -289,7 +290,7 @@ export function createStageView({
|
|
|
289
290
|
function setCollapsed(value) {
|
|
290
291
|
collapsed = value;
|
|
291
292
|
revealed = false;
|
|
292
|
-
|
|
293
|
+
writeStored(COLLAPSE_KEY, value ? "1" : "0");
|
|
293
294
|
applyCollapsed();
|
|
294
295
|
}
|
|
295
296
|
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// The page's per-browser choices in localStorage, guarded: storage can be
|
|
2
|
+
// missing or throw (a private window, blocked site data, a full quota), and
|
|
3
|
+
// a choice that can't be read or written is just not remembered. Reads
|
|
4
|
+
// globalThis.localStorage at each call, never at import (the specs install
|
|
5
|
+
// their own).
|
|
6
|
+
|
|
7
|
+
// The stored string, or +fallback+ when there is none or it can't be read.
|
|
8
|
+
export function readStored(key, fallback = null) {
|
|
9
|
+
try {
|
|
10
|
+
const value = globalThis.localStorage.getItem(key);
|
|
11
|
+
return value === null ? fallback : value;
|
|
12
|
+
} catch (_) {
|
|
13
|
+
return fallback;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function writeStored(key, value) {
|
|
18
|
+
try { globalThis.localStorage.setItem(key, value); } catch (_) {}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function removeStored(key) {
|
|
22
|
+
try { globalThis.localStorage.removeItem(key); } catch (_) {}
|
|
23
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { GRIP_OPEN_PX } from "./composer_size.js";
|
|
1
2
|
import { waitingOn } from "./notify.js";
|
|
2
3
|
|
|
3
4
|
// How many session cards the top strip shows for its width in px: a card
|
|
@@ -12,15 +13,16 @@ export function stripColumns(width) {
|
|
|
12
13
|
return Math.min(6, Math.max(3, fit));
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
// The hidden strip's
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
// Plain text parts; live and
|
|
21
|
-
// the bare label. The count is
|
|
22
|
-
// number the "All sessions" tile
|
|
23
|
-
|
|
16
|
+
// The hidden strip's status pill carries the list's count, and how many of
|
|
17
|
+
// those sessions a worker runs (the cards' green "live" badge), so a user
|
|
18
|
+
// who hid the strip still sees there is a live session to go back to. And
|
|
19
|
+
// how many wait on the user (notify.js waitingOn: a question, an approval, a
|
|
20
|
+
// card), so a hidden strip still says one needs an answer; a click on it
|
|
21
|
+
// opens the all-sessions view (lists_view.js). Plain text parts; live and
|
|
22
|
+
// waiting are "" when none. Zero sessions keep the bare label. The count is
|
|
23
|
+
// sessions (a folded delegate counts), the number the "All sessions" tile
|
|
24
|
+
// shows.
|
|
25
|
+
export function stripStatusParts(sessions) {
|
|
24
26
|
const list = sessions || [];
|
|
25
27
|
const n = list.length;
|
|
26
28
|
const liveN = list.filter((s) => s && s.owner === "worker").length;
|
|
@@ -37,6 +39,30 @@ export function stripShowParts(sessions) {
|
|
|
37
39
|
// cut off below the strip.
|
|
38
40
|
export const SHORT_WINDOW_QUERY = "(max-height: 700px)";
|
|
39
41
|
|
|
42
|
+
// The strip's grip: the handle on the edge between the strip and the chat
|
|
43
|
+
// (the composer's grip's twin, mirrored: that one folds the box down, this
|
|
44
|
+
// one puts the strip away up). +dy+ is the release y minus the press y, so a
|
|
45
|
+
// drag up is negative. Decided on release only (the strip snaps, it does not
|
|
46
|
+
// follow the pointer): |dy| below GRIP_OPEN_PX is a tap, which toggles either
|
|
47
|
+
// way (a hidden strip's grip has no other way to come back, and a shown
|
|
48
|
+
// one's neither); a longer drag only fires the way it points: up (past
|
|
49
|
+
// STRIP_HIDE_PX) hides the shown strip, down (past GRIP_OPEN_PX) shows the
|
|
50
|
+
// hidden one. Anything else is null: a short drag the wrong way does nothing.
|
|
51
|
+
export const STRIP_HIDE_PX = 32; // a drag up this far hides the shown strip
|
|
52
|
+
export function stripGripAction({ hidden, dy }) {
|
|
53
|
+
if (Math.abs(dy) < GRIP_OPEN_PX) return hidden ? "show" : "hide"; // a tap
|
|
54
|
+
if (hidden) return dy >= GRIP_OPEN_PX ? "show" : null;
|
|
55
|
+
return dy <= -STRIP_HIDE_PX ? "hide" : null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// The grip's title (D7: the grip owns it; composer_grip.js's drawFoldControls
|
|
59
|
+
// writes the composer's the same way). willToggle: the drag in progress would
|
|
60
|
+
// act on release.
|
|
61
|
+
export function stripGripTitle({ hidden, willToggle }) {
|
|
62
|
+
if (willToggle) return hidden ? "Release to show the latest sessions" : "Release to hide the latest sessions";
|
|
63
|
+
return hidden ? "Drag down or click to show the latest sessions" : "Drag up or click to hide the latest sessions";
|
|
64
|
+
}
|
|
65
|
+
|
|
40
66
|
// Whether the strip is hidden: the user's own hide (saved), or a short
|
|
41
67
|
// window unless the user showed it (the pill) since it got short.
|
|
42
68
|
export function stripAutoHidden({ saved, short, shownWhileShort }) {
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { GRIP_OPEN_PX } from "./composer_size.js";
|
|
2
|
+
import { stripGripAction, stripGripTitle } from "./strip.js";
|
|
3
|
+
|
|
4
|
+
// The latest-sessions strip's grip (used by app.js): the handle on the edge
|
|
5
|
+
// between the strip and the chat, the composer's grip's twin, mirrored. A
|
|
6
|
+
// drag up (or a tap) hides the strip, a drag down (or a tap) shows it, and
|
|
7
|
+
// Enter or Space toggles too. The strip snaps: unlike the composer's box it
|
|
8
|
+
// does not follow the pointer, so nothing is measured while dragging — only
|
|
9
|
+
// whether the release would act (.will-toggle, the pill in the accent).
|
|
10
|
+
// The grip owns its title and aria-expanded (composer_grip.js's
|
|
11
|
+
// drawFoldControls does the same for the composer's): app.js calls the
|
|
12
|
+
// returned refresh() from applyStripHidden, and nothing else writes them.
|
|
13
|
+
// No click listener: a click still fires after pointerdown's preventDefault
|
|
14
|
+
// and would toggle twice. No document listener either: the composer's grip
|
|
15
|
+
// needs one for typing on the page, this one acts only on itself.
|
|
16
|
+
// +setHidden(true)+ hides the strip, +isHidden()+ says whether it is hidden.
|
|
17
|
+
export function createStripGrip({ gripEl, isHidden, setHidden }) {
|
|
18
|
+
let willToggle = false;
|
|
19
|
+
|
|
20
|
+
function refresh() {
|
|
21
|
+
const hidden = isHidden();
|
|
22
|
+
gripEl.setAttribute("aria-expanded", hidden ? "false" : "true");
|
|
23
|
+
gripEl.title = stripGripTitle({ hidden, willToggle });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function setWillToggle(will) {
|
|
27
|
+
if (will === willToggle) return;
|
|
28
|
+
willToggle = will;
|
|
29
|
+
gripEl.classList.toggle("will-toggle", will);
|
|
30
|
+
refresh();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
gripEl.addEventListener("pointerdown", (e) => {
|
|
34
|
+
e.preventDefault();
|
|
35
|
+
gripEl.setPointerCapture(e.pointerId);
|
|
36
|
+
gripEl.classList.add("dragging");
|
|
37
|
+
const startY = e.clientY;
|
|
38
|
+
// Past a tap's size, and the release would act: the pill promises. A
|
|
39
|
+
// tap-sized move never flickers it (that stays a tap).
|
|
40
|
+
const willAfter = (dy) => Math.abs(dy) >= GRIP_OPEN_PX && stripGripAction({ hidden: isHidden(), dy }) !== null;
|
|
41
|
+
const move = (ev) => setWillToggle(willAfter(ev.clientY - startY));
|
|
42
|
+
const end = () => {
|
|
43
|
+
gripEl.classList.remove("dragging");
|
|
44
|
+
gripEl.removeEventListener("pointermove", move);
|
|
45
|
+
gripEl.removeEventListener("pointerup", up);
|
|
46
|
+
gripEl.removeEventListener("pointercancel", cancel);
|
|
47
|
+
};
|
|
48
|
+
const up = (ev) => {
|
|
49
|
+
const action = stripGripAction({ hidden: isHidden(), dy: ev.clientY - startY });
|
|
50
|
+
end();
|
|
51
|
+
setWillToggle(false);
|
|
52
|
+
if (action) setHidden(action === "hide");
|
|
53
|
+
refresh();
|
|
54
|
+
};
|
|
55
|
+
const cancel = () => {
|
|
56
|
+
end();
|
|
57
|
+
setWillToggle(false);
|
|
58
|
+
refresh();
|
|
59
|
+
};
|
|
60
|
+
gripEl.addEventListener("pointermove", move);
|
|
61
|
+
gripEl.addEventListener("pointerup", up);
|
|
62
|
+
gripEl.addEventListener("pointercancel", cancel);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Enter or Space on the focused handle toggles (preventDefault: Space
|
|
66
|
+
// would scroll the page).
|
|
67
|
+
gripEl.addEventListener("keydown", (e) => {
|
|
68
|
+
if (e.key !== "Enter" && e.key !== " ") return;
|
|
69
|
+
e.preventDefault();
|
|
70
|
+
setHidden(!isHidden());
|
|
71
|
+
refresh();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
refresh();
|
|
75
|
+
return refresh;
|
|
76
|
+
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// {level, source, own}; the worker's snapshot, a command_ran, or the
|
|
7
7
|
// server's own reading of the session file). A config on level_chip.js.
|
|
8
8
|
//
|
|
9
|
-
// The start page has one too (kind "new
|
|
9
|
+
// The start page has one too (kind "new session"): it shows the picked
|
|
10
10
|
// model's level (GET /api/models' thinking) and keeps a choice for the
|
|
11
11
|
// next create only (POST /api/sessions' thinking), never remembered:
|
|
12
12
|
// nothing is sent unless the user sets one.
|
|
@@ -26,20 +26,20 @@ export const CONFIG = Object.freeze({
|
|
|
26
26
|
formClass: "think-form",
|
|
27
27
|
popoverClass: "think-popover",
|
|
28
28
|
nowClass: "think-now",
|
|
29
|
-
when: (fresh) => (fresh ? "For the new
|
|
29
|
+
when: (fresh) => (fresh ? "For the new session's first turn on; not remembered for the next one."
|
|
30
30
|
: "From the next turn's start. On Gemma the whole prompt is read again; a hosted API reads the whole conversation again."),
|
|
31
31
|
});
|
|
32
32
|
|
|
33
33
|
export const { nowText } = level;
|
|
34
34
|
|
|
35
|
-
// The new
|
|
35
|
+
// The new session's choice (a level, or null) laid over the model's summary:
|
|
36
36
|
// what the chip shows. The model's own summary without a choice.
|
|
37
37
|
export function withChoice(summary, choice) {
|
|
38
38
|
if (!summary || !choice) return summary;
|
|
39
39
|
return { ...summary, level: choice, source: SESSION, own: choice };
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
// The chip: its text, tooltip and whether the session (or the new
|
|
42
|
+
// The chip: its text, tooltip and whether the session (or the new session)
|
|
43
43
|
// set its own; null without a summary (an older worker). The default with
|
|
44
44
|
// nothing set anywhere shows too ("think default"): the chip is where the
|
|
45
45
|
// level is set.
|
|
@@ -67,7 +67,7 @@ export function formTemplate(summary, opts) {
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
// The chip's popover (level_chip.js createLevelControl); the start page's
|
|
70
|
-
// passes kind
|
|
70
|
+
// passes kind NEW_SESSION, choice and choose.
|
|
71
71
|
export function createThinkingControl({ id = "thinkingPopover", ...opts }) {
|
|
72
72
|
return level.createLevelControl({ cfg: CONFIG, id, ...opts });
|
|
73
73
|
}
|
|
@@ -37,7 +37,7 @@ export function createToasts({ el, onChange = () => {}, doc = globalThis.documen
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
function show(text, action = null, { sticky = false } = {}) {
|
|
40
|
-
const candidate = { el: doc.createElement("div"), timer: null, sticky, key: null };
|
|
40
|
+
const candidate = { el: doc.createElement("div"), timer: null, sticky, action, key: null };
|
|
41
41
|
candidate.el.className = "toast-item";
|
|
42
42
|
draw(candidate, text, action, sticky);
|
|
43
43
|
candidate.key = candidate.el.querySelector(".toast-message").textContent;
|
|
@@ -46,12 +46,13 @@ export function createToasts({ el, onChange = () => {}, doc = globalThis.documen
|
|
|
46
46
|
if (duplicate) {
|
|
47
47
|
if (duplicate.timer !== null) clearTimeout(duplicate.timer);
|
|
48
48
|
duplicate.timer = null;
|
|
49
|
-
duplicate.sticky = sticky;
|
|
49
|
+
duplicate.sticky = sticky || duplicate.sticky;
|
|
50
|
+
duplicate.action = action || duplicate.action;
|
|
50
51
|
duplicate.key = candidate.key;
|
|
51
|
-
draw(duplicate, text, action, sticky);
|
|
52
|
+
draw(duplicate, text, duplicate.action, duplicate.sticky);
|
|
52
53
|
entries.push(...entries.splice(entries.indexOf(duplicate), 1));
|
|
53
54
|
el.appendChild(duplicate.el);
|
|
54
|
-
if (!sticky) duplicate.timer = setTimeout(() => remove(duplicate), TOAST_MS);
|
|
55
|
+
if (!duplicate.sticky) duplicate.timer = setTimeout(() => remove(duplicate), TOAST_MS);
|
|
55
56
|
sync();
|
|
56
57
|
return;
|
|
57
58
|
}
|
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.80.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dmitry Dedov
|
|
@@ -337,6 +337,7 @@ files:
|
|
|
337
337
|
- lib/samagotchi/llm/adapter_common.rb
|
|
338
338
|
- lib/samagotchi/llm/adapters.rb
|
|
339
339
|
- lib/samagotchi/llm/anthropic_assembly.rb
|
|
340
|
+
- lib/samagotchi/llm/anthropic_binding_recovery.rb
|
|
340
341
|
- lib/samagotchi/llm/anthropic_messages.rb
|
|
341
342
|
- lib/samagotchi/llm/anthropic_wire.rb
|
|
342
343
|
- lib/samagotchi/llm/api_key.rb
|
|
@@ -416,7 +417,10 @@ files:
|
|
|
416
417
|
- lib/samagotchi/pending_question.rb
|
|
417
418
|
- lib/samagotchi/plugin/api.rb
|
|
418
419
|
- lib/samagotchi/plugin/attached_context.rb
|
|
420
|
+
- lib/samagotchi/plugin/card.rb
|
|
419
421
|
- lib/samagotchi/plugin/context.rb
|
|
422
|
+
- lib/samagotchi/plugin/http.rb
|
|
423
|
+
- lib/samagotchi/plugin/http_transport.rb
|
|
420
424
|
- lib/samagotchi/plugin/loader.rb
|
|
421
425
|
- lib/samagotchi/plugin/peer.rb
|
|
422
426
|
- lib/samagotchi/plugin/service.rb
|
|
@@ -434,6 +438,7 @@ files:
|
|
|
434
438
|
- lib/samagotchi/prompt_literal_guard.rb
|
|
435
439
|
- lib/samagotchi/prompt_note.rb
|
|
436
440
|
- lib/samagotchi/prompt_warmup.rb
|
|
441
|
+
- lib/samagotchi/question_answer.rb
|
|
437
442
|
- lib/samagotchi/question_desk.rb
|
|
438
443
|
- lib/samagotchi/queued_command.rb
|
|
439
444
|
- lib/samagotchi/reasoning_history.rb
|
|
@@ -500,6 +505,21 @@ files:
|
|
|
500
505
|
- lib/samagotchi/terminal_ui/surface.rb
|
|
501
506
|
- lib/samagotchi/terminal_ui/thinking_line.rb
|
|
502
507
|
- lib/samagotchi/terminal_ui/version_lines.rb
|
|
508
|
+
- lib/samagotchi/testing.rb
|
|
509
|
+
- lib/samagotchi/testing/boundary.rb
|
|
510
|
+
- lib/samagotchi/testing/bundle.rb
|
|
511
|
+
- lib/samagotchi/testing/fake_executable.rb
|
|
512
|
+
- lib/samagotchi/testing/fake_http.rb
|
|
513
|
+
- lib/samagotchi/testing/helpers.rb
|
|
514
|
+
- lib/samagotchi/testing/host.rb
|
|
515
|
+
- lib/samagotchi/testing/minitest.rb
|
|
516
|
+
- lib/samagotchi/testing/recorder.rb
|
|
517
|
+
- lib/samagotchi/testing/recording_api.rb
|
|
518
|
+
- lib/samagotchi/testing/rspec.rb
|
|
519
|
+
- lib/samagotchi/testing/sandbox.rb
|
|
520
|
+
- lib/samagotchi/testing/sessions.rb
|
|
521
|
+
- lib/samagotchi/testing/verdict.rb
|
|
522
|
+
- lib/samagotchi/testing/waiting.rb
|
|
503
523
|
- lib/samagotchi/text_diff.rb
|
|
504
524
|
- lib/samagotchi/thinking.rb
|
|
505
525
|
- lib/samagotchi/thinking_command.rb
|
|
@@ -595,6 +615,7 @@ files:
|
|
|
595
615
|
- lib/samagotchi/web/public/commands.js
|
|
596
616
|
- lib/samagotchi/web/public/composer.js
|
|
597
617
|
- lib/samagotchi/web/public/composer_fold.js
|
|
618
|
+
- lib/samagotchi/web/public/composer_grip.js
|
|
598
619
|
- lib/samagotchi/web/public/composer_size.js
|
|
599
620
|
- lib/samagotchi/web/public/context_chips.js
|
|
600
621
|
- lib/samagotchi/web/public/copy.js
|
|
@@ -647,8 +668,10 @@ files:
|
|
|
647
668
|
- lib/samagotchi/web/public/stage_slots.js
|
|
648
669
|
- lib/samagotchi/web/public/stage_view.js
|
|
649
670
|
- lib/samagotchi/web/public/stage_watch.js
|
|
671
|
+
- lib/samagotchi/web/public/storage.js
|
|
650
672
|
- lib/samagotchi/web/public/stream_handlers.js
|
|
651
673
|
- lib/samagotchi/web/public/strip.js
|
|
674
|
+
- lib/samagotchi/web/public/strip_grip.js
|
|
652
675
|
- lib/samagotchi/web/public/tally.js
|
|
653
676
|
- lib/samagotchi/web/public/task_stop.js
|
|
654
677
|
- lib/samagotchi/web/public/thinking_chip.js
|