samagotchi 0.68.0 → 0.70.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 +76 -1
- data/docs/architecture.md +1 -1
- data/docs/cli.md +6 -1
- data/docs/configuration.md +31 -8
- data/docs/context.md +15 -6
- data/docs/guardrails.md +23 -1
- data/docs/hooks.md +5 -1
- data/docs/internals/background-tasks.md +1 -1
- data/docs/internals/bundles.md +1 -1
- data/docs/internals/engine.md +25 -13
- data/docs/internals/prompt-caching.md +9 -2
- data/docs/internals/tool-guardrails.md +61 -1
- data/docs/plugins.md +66 -38
- data/docs/releasing.md +14 -6
- data/docs/sessions.md +5 -5
- data/docs/sub-agent.md +2 -1
- data/docs/testing.md +8 -1
- data/lib/samagotchi/bootstrap/probe.rb +1 -3
- data/lib/samagotchi/bridge/turn_accumulator.rb +2 -1
- data/lib/samagotchi/bundles/claude-peer/manifest.yml +10 -8
- data/lib/samagotchi/bundles/claude-peer/plugin.rb +4 -4
- data/lib/samagotchi/bundles/claude-peer/scripts/chi-peer-SKILL.md +6 -6
- data/lib/samagotchi/bundles/claude-peer/scripts/pair +3 -3
- data/lib/samagotchi/bundles/claude-peer/skill_claude_peer.md +6 -5
- data/lib/samagotchi/bundles/codex-peer/manifest.yml +10 -8
- data/lib/samagotchi/bundles/codex-peer/plugin.rb +4 -4
- data/lib/samagotchi/bundles/codex-peer/scripts/chi-peer-SKILL.md +6 -6
- data/lib/samagotchi/bundles/codex-peer/scripts/pair +3 -3
- data/lib/samagotchi/bundles/codex-peer/skill_codex_peer.md +6 -5
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/self_map.md +1 -1
- data/lib/samagotchi/config.rb +6 -21
- data/lib/samagotchi/context_absorber.rb +35 -21
- data/lib/samagotchi/engine.rb +7 -2
- data/lib/samagotchi/guardrails/targets.rb +9 -2
- data/lib/samagotchi/host_config.rb +1 -29
- data/lib/samagotchi/host_model.rb +0 -7
- data/lib/samagotchi/kernel_loop.rb +34 -7
- data/lib/samagotchi/llm/errors.rb +47 -1
- data/lib/samagotchi/llm/http.rb +20 -2
- data/lib/samagotchi/llm_context_stale.rb +8 -2
- data/lib/samagotchi/log_subscriber.rb +1 -0
- data/lib/samagotchi/memory_bundle/manifest.rb +3 -0
- data/lib/samagotchi/model_price.rb +2 -4
- data/lib/samagotchi/peer_sources.rb +1 -1
- data/lib/samagotchi/plugin/api.rb +3 -2
- data/lib/samagotchi/plugin/peer.rb +35 -16
- data/lib/samagotchi/session_metrics.rb +4 -1
- data/lib/samagotchi/system_prompt.rb +19 -3
- data/lib/samagotchi/terminal_ui/formatting.rb +5 -2
- data/lib/samagotchi/tool_activity.rb +3 -0
- data/lib/samagotchi/tool_output_cut.rb +312 -0
- data/lib/samagotchi/tool_output_files.rb +73 -0
- data/lib/samagotchi/tool_row_fields.rb +4 -2
- data/lib/samagotchi/tool_runner.rb +70 -15
- data/lib/samagotchi/toolkit.rb +27 -14
- data/lib/samagotchi/toolkit_binding.rb +8 -2
- data/lib/samagotchi/toolkit_command.rb +20 -4
- data/lib/samagotchi/tools/builtin_calls.rb +45 -1
- data/lib/samagotchi/tools/delegate.rb +4 -1
- data/lib/samagotchi/tools/delegate_wait.rb +12 -1
- data/lib/samagotchi/tools/ref_text.rb +27 -0
- data/lib/samagotchi/tools/registry.rb +95 -8
- data/lib/samagotchi/tools/task_wait.rb +4 -1
- data/lib/samagotchi/tools/tool_index.rb +223 -72
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +14 -9
- data/lib/samagotchi/web/message_parts.rb +9 -4
- data/lib/samagotchi/web/public/activity.js +3 -1
- data/lib/samagotchi/web/public/app.js +123 -22
- data/lib/samagotchi/web/public/chains.js +19 -2
- data/lib/samagotchi/web/public/families_view.js +15 -0
- data/lib/samagotchi/web/public/index.html +7 -5
- data/lib/samagotchi/web/public/model_picker.js +21 -6
- data/lib/samagotchi/web/public/session_state.js +5 -0
- data/lib/samagotchi/web/public/stream_handlers.js +5 -2
- data/lib/samagotchi/web/public/timing.js +3 -1
- data/lib/samagotchi/web/public/toast_place.js +16 -0
- data/lib/samagotchi/web/public/toolkit_chip.js +11 -4
- data/lib/samagotchi/web/public/turn_html.js +4 -2
- data/lib/samagotchi/worker.rb +2 -2
- data/lib/samagotchi/worker_inbound.rb +3 -0
- metadata +5 -1
|
@@ -44,7 +44,7 @@ import { elementOf, emptyAnswerTemplate } from "./turn_html.js";
|
|
|
44
44
|
import { createBubbles, hintEl, outputBubbleTemplate, setBubbleDelivery } from "./bubbles.js";
|
|
45
45
|
import { createCardsPlace } from "./cards_place.js";
|
|
46
46
|
import { createStageView, FOLD_MS } from "./stage_view.js";
|
|
47
|
-
import { CONTINUE_OPENER, isOpener, continueRefusal, continuedText, openChildrenTemplate } from "./chains.js";
|
|
47
|
+
import { CONTINUE_OPENER, chainNeighbours, isOpener, continueRefusal, continuedText, freshLink, openChildrenTemplate } from "./chains.js";
|
|
48
48
|
import { createTaskStopper } from "./task_stop.js";
|
|
49
49
|
import { appendQuote } from "./annotations.js";
|
|
50
50
|
import { parsePresets } from "./annotate_presets.js";
|
|
@@ -83,6 +83,7 @@ import { createDeliveryControl, QUEUE_NEEDS_WORKER } from "./delivery_line.js";
|
|
|
83
83
|
import { liveSlots } from "./stage_model.js";
|
|
84
84
|
import { currentGen } from "./turn_model.js";
|
|
85
85
|
import { toolName } from "./activity.js";
|
|
86
|
+
import { toastBottom } from "./toast_place.js";
|
|
86
87
|
|
|
87
88
|
const $ = (s) => document.querySelector(s);
|
|
88
89
|
const topStripEl = $("#topStrip");
|
|
@@ -118,7 +119,7 @@ const llmContextControl = createLlmContextControl({
|
|
|
118
119
|
// drops it too, a failed create keeps it).
|
|
119
120
|
const llmContextNewEl = $("#llmContextNew");
|
|
120
121
|
let newChatLlmContext = null;
|
|
121
|
-
const newChatLlmSummary = () => (modelPickEl.hidden ? null : modelPicker.chosenRow()?.llm_context || null);
|
|
122
|
+
const newChatLlmSummary = () => (selected || modelPickEl.hidden ? null : modelPicker.chosenRow()?.llm_context || null);
|
|
122
123
|
const llmContextNewControl = createLlmContextControl({
|
|
123
124
|
el: llmContextNewEl,
|
|
124
125
|
summary: newChatLlmSummary,
|
|
@@ -154,7 +155,7 @@ const reasoningHistoryControl = createReasoningHistoryControl({
|
|
|
154
155
|
// for the next new chat only (sent with its create, then dropped).
|
|
155
156
|
const thinkingNewEl = $("#thinkingNew");
|
|
156
157
|
let newChatThinking = null;
|
|
157
|
-
const newChatThinkingSummary = () => (modelPickEl.hidden ? null : modelPicker.chosenRow()?.thinking || null);
|
|
158
|
+
const newChatThinkingSummary = () => (selected || modelPickEl.hidden ? null : modelPicker.chosenRow()?.thinking || null);
|
|
158
159
|
const thinkingNewControl = createThinkingControl({
|
|
159
160
|
el: thinkingNewEl,
|
|
160
161
|
summary: newChatThinkingSummary,
|
|
@@ -587,11 +588,13 @@ function updateComposerMode() {
|
|
|
587
588
|
// In a session the box is one line: its minimum follows the class.
|
|
588
589
|
if (document.body.classList.toggle("has-session", !!selected) !== hadSession) {
|
|
589
590
|
hadSession = !hadSession;
|
|
591
|
+
// Back on the start page: its remembered model, not the last link's.
|
|
592
|
+
if (!hadSession) restoreStartPick();
|
|
590
593
|
fitPrompt();
|
|
591
594
|
placeToast();
|
|
592
595
|
}
|
|
596
|
+
syncModelPick();
|
|
593
597
|
if (selected) {
|
|
594
|
-
modelPicker.close();
|
|
595
598
|
llmContextNewControl.close();
|
|
596
599
|
// The start page's choice was for a new chat: opening a session drops it.
|
|
597
600
|
if (newChatLlmContext) {
|
|
@@ -648,6 +651,8 @@ function syncDeliveryLine() {
|
|
|
648
651
|
|
|
649
652
|
function setTurnRunning(running) {
|
|
650
653
|
liveTurn.running = !!running;
|
|
654
|
+
// A fresh link's picker goes now, not at the timing re-read after the turn.
|
|
655
|
+
if (running && selected) current.turnStarted = true;
|
|
651
656
|
updateComposerMode();
|
|
652
657
|
updateInfoBar();
|
|
653
658
|
}
|
|
@@ -1096,6 +1101,9 @@ function updateInfoBar() {
|
|
|
1096
1101
|
// mid-press) stay.
|
|
1097
1102
|
renderInfoBar(infoTextEl, bar);
|
|
1098
1103
|
updateSessionTitle();
|
|
1104
|
+
// A fresh link's picker follows what the bar follows: its model, its
|
|
1105
|
+
// owner, a next link continued from elsewhere.
|
|
1106
|
+
syncModelPick();
|
|
1099
1107
|
}
|
|
1100
1108
|
|
|
1101
1109
|
// The open session's title, once on screen (session_title.js): its strip
|
|
@@ -1289,6 +1297,10 @@ const streamHandlers = createStreamHandlers({
|
|
|
1289
1297
|
onInitEvent: (type, data) => onInitEvent(type, data),
|
|
1290
1298
|
upsertCard: (card) => upsertCard(card),
|
|
1291
1299
|
refreshCardBodies: () => refreshCardBodies(),
|
|
1300
|
+
syncModelPick: () => {
|
|
1301
|
+
if (pendingPick) pickRanEarly = true;
|
|
1302
|
+
syncModelPick();
|
|
1303
|
+
},
|
|
1292
1304
|
});
|
|
1293
1305
|
|
|
1294
1306
|
async function refreshTimingFromSession() {
|
|
@@ -1332,6 +1344,9 @@ function handleCreate() {
|
|
|
1332
1344
|
// /queue wins over both.
|
|
1333
1345
|
async function handleSendTurn({ delivery: chosen = null } = {}) {
|
|
1334
1346
|
if (current.owner === "tui") return;
|
|
1347
|
+
// A fresh link's pick first: its /model is queued in the worker once
|
|
1348
|
+
// acked, and the worker runs queued commands before a turn's input.
|
|
1349
|
+
if (pendingPick) await pendingPick;
|
|
1335
1350
|
let prompt = turnText(promptEl.value, chips);
|
|
1336
1351
|
if (!prompt) return;
|
|
1337
1352
|
const armed = deliveryControl.take();
|
|
@@ -1516,10 +1531,24 @@ $("#newBtn").addEventListener("click", () => {
|
|
|
1516
1531
|
// ── The start page's model picker ──────────────────────────────────────────
|
|
1517
1532
|
// The hosts' models from GET /api/models, the choice this browser made last
|
|
1518
1533
|
// preselected while it is still offered (else the server's default). Hidden
|
|
1519
|
-
// until the list is in, and in a session (its model is in the info bar)
|
|
1520
|
-
//
|
|
1521
|
-
//
|
|
1522
|
-
//
|
|
1534
|
+
// until the list is in, and in a session (its model is in the info bar),
|
|
1535
|
+
// but a fresh chain link's (chains.js freshLink): there it shows the link's
|
|
1536
|
+
// model and a pick runs /model in the link, remembered neither as this
|
|
1537
|
+
// browser's choice nor in Recent. A host that failed is the picker's
|
|
1538
|
+
// tooltip; a route that fails leaves the picker hidden and the server's
|
|
1539
|
+
// default in charge. The search list itself is model_picker.js'; a pick
|
|
1540
|
+
// sends the focus to the message box.
|
|
1541
|
+
const START_PICK_TITLE = "The model the new chat starts on";
|
|
1542
|
+
let startPickTitle = START_PICK_TITLE;
|
|
1543
|
+
// The fresh link the picker shows (null: the start page, or a session it
|
|
1544
|
+
// is hidden in), and a link pick's /model still in flight (a send waits).
|
|
1545
|
+
let pickLinkId = null;
|
|
1546
|
+
let pickSelected = null;
|
|
1547
|
+
let pendingPick = null;
|
|
1548
|
+
let pendingPickId = null;
|
|
1549
|
+
// A command_ran came while the pick was in flight (its sync was skipped):
|
|
1550
|
+
// the ack syncs then.
|
|
1551
|
+
let pickRanEarly = false;
|
|
1523
1552
|
const modelPicker = createModelPicker({
|
|
1524
1553
|
button: modelPickEl,
|
|
1525
1554
|
panel: $("#modelPanel"),
|
|
@@ -1527,12 +1556,70 @@ const modelPicker = createModelPicker({
|
|
|
1527
1556
|
list: $("#modelList"),
|
|
1528
1557
|
composer: $("#composer"),
|
|
1529
1558
|
center: $("#center"),
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1559
|
+
remember: () => !selected,
|
|
1560
|
+
onPick: (name) => {
|
|
1561
|
+
if (selected) pickForLink(name);
|
|
1562
|
+
else {
|
|
1563
|
+
renderNewChatLlmContext();
|
|
1564
|
+
renderNewChatThinking();
|
|
1565
|
+
}
|
|
1533
1566
|
promptEl.focus();
|
|
1534
1567
|
},
|
|
1535
1568
|
});
|
|
1569
|
+
// A pick on a fresh link: /model in its worker (its command bubble and the
|
|
1570
|
+
// info bar follow from command_ran); refused, the button goes back to the
|
|
1571
|
+
// link's model, if the page is still on that link.
|
|
1572
|
+
function pickForLink(name) {
|
|
1573
|
+
const id = selected;
|
|
1574
|
+
const run = runSessionCommand(`/model ${name}`).then((ok) => {
|
|
1575
|
+
const ranEarly = pendingPick === run && pickRanEarly;
|
|
1576
|
+
if (pendingPick === run) {
|
|
1577
|
+
pendingPick = null;
|
|
1578
|
+
pendingPickId = null;
|
|
1579
|
+
pickRanEarly = false;
|
|
1580
|
+
}
|
|
1581
|
+
if ((!ok || ranEarly) && selected === id) syncModelPick();
|
|
1582
|
+
return ok;
|
|
1583
|
+
});
|
|
1584
|
+
pendingPick = run;
|
|
1585
|
+
pendingPickId = id;
|
|
1586
|
+
pickRanEarly = false;
|
|
1587
|
+
}
|
|
1588
|
+
// The button's name, tooltip and label for what the page shows: a fresh
|
|
1589
|
+
// link's model, else the start page's (whose choice stays as it is).
|
|
1590
|
+
// Toggles the fresh-link class (the CSS shows the button in a session
|
|
1591
|
+
// only with it); the panel closes when the link changes or stops being fresh.
|
|
1592
|
+
function syncModelPick() {
|
|
1593
|
+
// The list walk only for a link that is fresh otherwise (the info bar
|
|
1594
|
+
// calls this every second while a turn runs).
|
|
1595
|
+
const fresh = !!selected && freshLink(current, { running: liveTurn.running })
|
|
1596
|
+
&& !chainNeighbours(allSessions, selected, { continues: current.continues }).next;
|
|
1597
|
+
document.body.classList.toggle("fresh-link", fresh);
|
|
1598
|
+
const linkId = fresh ? selected : null;
|
|
1599
|
+
// Another session (or the start page), or the link stopped being fresh:
|
|
1600
|
+
// a panel left open would come back open where it no longer belongs.
|
|
1601
|
+
if (selected !== pickSelected || linkId !== pickLinkId) modelPicker.close();
|
|
1602
|
+
pickSelected = selected;
|
|
1603
|
+
pickLinkId = linkId;
|
|
1604
|
+
if (fresh) {
|
|
1605
|
+
// A pick in flight keeps its name until its /model is taken (its
|
|
1606
|
+
// command_ran syncs again then).
|
|
1607
|
+
if (!(pendingPick && pendingPickId === selected)) modelPicker.show(current.model);
|
|
1608
|
+
modelPickEl.title = "The model this link starts on";
|
|
1609
|
+
modelPickEl.setAttribute("aria-label", "Model this link starts on");
|
|
1610
|
+
} else {
|
|
1611
|
+
modelPickEl.title = startPickTitle;
|
|
1612
|
+
modelPickEl.setAttribute("aria-label", "Model for the new chat");
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
// Leaving a session: the start page's remembered model again (no fetch).
|
|
1616
|
+
function restoreStartPick() {
|
|
1617
|
+
let stored = null;
|
|
1618
|
+
try { stored = localStorage.getItem(MODEL_KEY); } catch (_) {}
|
|
1619
|
+
modelPicker.restore(stored);
|
|
1620
|
+
renderNewChatLlmContext();
|
|
1621
|
+
renderNewChatThinking();
|
|
1622
|
+
}
|
|
1536
1623
|
async function loadModels() {
|
|
1537
1624
|
let payload;
|
|
1538
1625
|
try {
|
|
@@ -1543,8 +1630,10 @@ async function loadModels() {
|
|
|
1543
1630
|
let stored = null;
|
|
1544
1631
|
try { stored = localStorage.getItem(MODEL_KEY); } catch (_) {}
|
|
1545
1632
|
if (!modelPicker.setModels(payload, stored)) return;
|
|
1546
|
-
|
|
1633
|
+
startPickTitle = payload.warning ? `${START_PICK_TITLE} · ${payload.warning}` : START_PICK_TITLE;
|
|
1547
1634
|
modelPickEl.hidden = false;
|
|
1635
|
+
// A fresh link open meanwhile shows its own model again, not the stored one.
|
|
1636
|
+
syncModelPick();
|
|
1548
1637
|
renderNewChatLlmContext();
|
|
1549
1638
|
renderNewChatThinking();
|
|
1550
1639
|
}
|
|
@@ -1819,13 +1908,15 @@ async function continueSelected() {
|
|
|
1819
1908
|
markArchived([id], true);
|
|
1820
1909
|
addCreatedSession(created);
|
|
1821
1910
|
const next = created.id;
|
|
1911
|
+
// A model fallback's warning stays until closed.
|
|
1912
|
+
const sticky = !!created.model_warning;
|
|
1822
1913
|
if (selected !== id) {
|
|
1823
|
-
showToast(continuedText(id, created, { opened: false }), { label: "Open", run: () => select(next) });
|
|
1914
|
+
showToast(continuedText(id, created, { opened: false }), { label: "Open", run: () => select(next) }, { sticky });
|
|
1824
1915
|
return;
|
|
1825
1916
|
}
|
|
1826
1917
|
await select(next);
|
|
1827
1918
|
if (selected === next) fillOpener();
|
|
1828
|
-
showToast(continuedText(id, created));
|
|
1919
|
+
showToast(continuedText(id, created), null, { sticky });
|
|
1829
1920
|
} catch (e) {
|
|
1830
1921
|
const refusal = continueRefusal(e, allSessions);
|
|
1831
1922
|
if (refusal.kind === "open") {
|
|
@@ -1912,22 +2003,32 @@ async function setArchived(id, archive, { undo = true } = {}) {
|
|
|
1912
2003
|
|
|
1913
2004
|
// The toast sits above the session's composer card (measured: it grows with
|
|
1914
2005
|
// a wrapping footer or a taller box), so it never covers the card's
|
|
1915
|
-
// controls, and above the stage while it shows
|
|
1916
|
-
//
|
|
2006
|
+
// controls, and above the stage while it shows and a fresh link's open
|
|
2007
|
+
// model panel (toast_place.js); with no card in sight (the start page, all
|
|
2008
|
+
// sessions) the CSS's 24 px from the bottom.
|
|
1917
2009
|
function placeToast() {
|
|
1918
|
-
const gap = 12;
|
|
1919
2010
|
const dock = document.getElementById("dock");
|
|
1920
2011
|
const r = dock.getBoundingClientRect();
|
|
1921
|
-
const
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
2012
|
+
const shownTop = (el) => {
|
|
2013
|
+
const box = el && !el.hidden ? el.getBoundingClientRect() : null;
|
|
2014
|
+
return box && box.height > 0 ? box.top : null;
|
|
2015
|
+
};
|
|
2016
|
+
const px = toastBottom({
|
|
2017
|
+
inSession: document.body.classList.contains("has-session") && r.height > 0,
|
|
2018
|
+
viewportHeight: window.innerHeight,
|
|
2019
|
+
dockTop: r.top,
|
|
2020
|
+
tops: [shownTop(document.getElementById("turnStage")), shownTop(document.getElementById("modelPanel"))],
|
|
2021
|
+
toastHeight: document.getElementById("toast").offsetHeight,
|
|
2022
|
+
});
|
|
2023
|
+
const bottom = px === null ? "" : `${px}px`;
|
|
1925
2024
|
const root = document.documentElement.style;
|
|
1926
2025
|
if (root.getPropertyValue("--toast-bottom") !== bottom) {
|
|
1927
2026
|
if (bottom) root.setProperty("--toast-bottom", bottom);
|
|
1928
2027
|
else root.removeProperty("--toast-bottom");
|
|
1929
2028
|
}
|
|
1930
2029
|
}
|
|
2030
|
+
// The model panel opening, closing or changing height (its list filters).
|
|
2031
|
+
new ResizeObserver(() => placeToast()).observe(document.getElementById("modelPanel"));
|
|
1931
2032
|
|
|
1932
2033
|
// sticky: stays until its action or its ✕ (else gone after 6 s). +text+:
|
|
1933
2034
|
// words, or a template (chains.js openChildrenTemplate: a link in it, a
|
|
@@ -1939,8 +2040,8 @@ function showToast(text, action = null, { sticky = false } = {}) {
|
|
|
1939
2040
|
render(html`<span @click=${(e) => { if (e.target.closest?.("a[href]")) hide(); }}>${text}</span>${action
|
|
1940
2041
|
? html`<button type="button" class="toast-action" @click=${() => { hide(); action.run(); }}>${action.label}</button>`
|
|
1941
2042
|
: nothing}${sticky ? html`<button type="button" class="toast-close" title="Dismiss" aria-label="Dismiss" @click=${hide}>✕</button>` : nothing}`, el);
|
|
1942
|
-
placeToast();
|
|
1943
2043
|
el.hidden = false;
|
|
2044
|
+
placeToast();
|
|
1944
2045
|
clearTimeout(toastTimer);
|
|
1945
2046
|
if (!sticky) toastTimer = setTimeout(hide, 6000);
|
|
1946
2047
|
}
|
|
@@ -235,13 +235,30 @@ export function openChildrenTemplate(prevId, children, hrefFor) {
|
|
|
235
235
|
}
|
|
236
236
|
|
|
237
237
|
// The toast after a continue: the new link, the previous one archived (when
|
|
238
|
-
// the page shows the new one),
|
|
239
|
-
//
|
|
238
|
+
// the page shows the new one), how many open delegates moved to it (the
|
|
239
|
+
// answer's moved), and the model fallback's warning (model_warning: the
|
|
240
|
+
// previous link's host is gone, the link starts on the default).
|
|
240
241
|
export function continuedText(prevId, created, { opened = true } = {}) {
|
|
241
242
|
const short = String(prevId).slice(0, 8);
|
|
242
243
|
const moved = Array.isArray(created?.moved) ? created.moved.length : 0;
|
|
243
244
|
const parts = [`Continued ${short} in ${String(created?.id || "").slice(0, 8)}`];
|
|
244
245
|
if (opened) parts.push(`${short} is archived`);
|
|
245
246
|
if (moved) parts.push(`${moved} open ${moved === 1 ? "delegate" : "delegates"} moved`);
|
|
247
|
+
const warning = typeof created?.model_warning === "string" ? created.model_warning.trim() : "";
|
|
248
|
+
if (warning) parts.push(warning);
|
|
246
249
|
return parts.join("; ");
|
|
247
250
|
}
|
|
251
|
+
|
|
252
|
+
// A fresh link: a chain link (its `continues`) with no turn yet, so its
|
|
253
|
+
// composer offers the model picker (a pick runs /model in it). However the
|
|
254
|
+
// link was made (continue →, chi send --continues, a reload). Not fresh once
|
|
255
|
+
// a turn started (+state.turnStarted+, set live before the timing re-read
|
|
256
|
+
// brings its record; a failed or canceled first turn counts too), while one
|
|
257
|
+
// runs, when a terminal chi owns it (sending is refused there) or when it is
|
|
258
|
+
// +continued+ itself (a /model there would wake an archived link).
|
|
259
|
+
export function freshLink(state, { running = false, continued = false } = {}) {
|
|
260
|
+
if (!state?.continues || running || continued || state.turnStarted) return false;
|
|
261
|
+
if (state.owner === "tui") return false;
|
|
262
|
+
const timing = state.timing || {};
|
|
263
|
+
return !(timing.turnRecords || []).length && !timing.activeTurn;
|
|
264
|
+
}
|
|
@@ -73,6 +73,20 @@ export function familyRowsTemplate(f, { activeId = null, matchIds = null, archiv
|
|
|
73
73
|
// The widest window index.html lays out as a phone (@media (max-width:600px)).
|
|
74
74
|
const PHONE_MAX_PX = 600;
|
|
75
75
|
|
|
76
|
+
// The status dots' pulse in +root+ on the document timeline's clock (each
|
|
77
|
+
// animation's start time 0), the phase the cards' --pulse-delay gives
|
|
78
|
+
// theirs (lists_view.js cardAnimStyle): a popover keeps its row nodes
|
|
79
|
+
// across draws, reused by position, so a dot that moves to another node
|
|
80
|
+
// or starts pulsing there (a member inserted above, a status flip) would
|
|
81
|
+
// otherwise start its pulse over, out of step with the rest.
|
|
82
|
+
export function syncPulses(root) {
|
|
83
|
+
root.querySelectorAll(".dot").forEach((dot) => {
|
|
84
|
+
for (const a of dot.getAnimations?.() || []) {
|
|
85
|
+
if (a.startTime !== 0) a.startTime = 0;
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
76
90
|
// The strip's family popover: one #familyPop in body (position: fixed, so
|
|
77
91
|
// the strip's sideways scroll on a phone doesn't clip it), under the chip
|
|
78
92
|
// it was opened from and as wide as that card (300 px at least, inside the
|
|
@@ -128,6 +142,7 @@ export function createFamilyPop({ id = "familyPop", doc, win, rowsFor, anchorFor
|
|
|
128
142
|
}
|
|
129
143
|
// A row inserted before the focused one: the focus stays on its session.
|
|
130
144
|
keepFocus(el, "data-id", () => render(rows, el));
|
|
145
|
+
syncPulses(el);
|
|
131
146
|
anchor.setAttribute("aria-expanded", "true");
|
|
132
147
|
place(anchor);
|
|
133
148
|
return true;
|
|
@@ -578,7 +578,7 @@ body.has-session #composer{flex-direction:row;align-items:flex-end;padding:10px}
|
|
|
578
578
|
body.has-session #composer textarea{min-height:0}
|
|
579
579
|
body.has-session #composeHint{display:none}
|
|
580
580
|
body.has-session #composer .actions-col{flex-wrap:nowrap;flex-shrink:0}
|
|
581
|
-
/* The model picker (start page
|
|
581
|
+
/* The model picker (the start page, and a fresh chain link: app.js syncModelPick): reads as a label, "host:model ▾", left of Start; a button under a drawn caret that opens a searchable list (model_picker.js). Its name ellipsizes within max-width. */
|
|
582
582
|
#composer .actions-col .model-pick{order:0;position:relative;display:inline-block;max-width:min(50%,380px);height:28px;min-height:28px;padding:0 22px 0 8px;border-radius:8px;overflow:hidden;background:transparent;color:var(--text-muted);font-size:12px;line-height:28px;text-align:left;text-overflow:ellipsis;transition:background .12s ease,color .12s ease}
|
|
583
583
|
#composer .actions-col .model-pick:hover,#composer .actions-col .model-pick[aria-expanded="true"]{filter:none;background:color-mix(in srgb,var(--bg-hover) 75%,transparent);color:var(--text-2)}
|
|
584
584
|
#composer .actions-col .model-pick[hidden],body.has-session #composer .actions-col .model-pick{display:none}
|
|
@@ -586,7 +586,9 @@ body.has-session #composer .actions-col{flex-wrap:nowrap;flex-shrink:0}
|
|
|
586
586
|
#composer .actions-col .model-pick.unavailable .model-pick-name{color:var(--text-faint)}
|
|
587
587
|
.model-pick::after{content:"";position:absolute;right:9px;top:50%;width:5px;height:5px;border-right:1.5px solid currentColor;border-bottom:1.5px solid currentColor;transform:translateY(-70%) rotate(45deg);pointer-events:none;transition:transform .12s ease}
|
|
588
588
|
.model-pick[aria-expanded="true"]::after{transform:translateY(-20%) rotate(225deg)}
|
|
589
|
-
|
|
589
|
+
/* A fresh link's (body.fresh-link): the session composer is one row and .actions-col is content-sized there, so a px cap keeps the text field's room and Send in view. :not([hidden]) keeps a failed /api/models' hide (this selector outranks the [hidden] one). */
|
|
590
|
+
body.has-session.fresh-link #composer .actions-col .model-pick:not([hidden]){display:inline-block;flex:0 1 auto;min-width:0;max-width:200px}
|
|
591
|
+
@media(max-width:600px){body.has-session.fresh-link #composer .actions-col .model-pick:not([hidden]){max-width:100px}}
|
|
590
592
|
/* The new chat's LLM context chip, after the picker: as quiet as it; accent once a choice is set (the info bar chip's .own). */
|
|
591
593
|
#composer .actions-col .llmctx-new{order:0;flex:0 1 auto;min-width:0;max-width:min(40%,260px);height:24px;min-height:24px;line-height:22px;padding:0 6px;border-radius:6px;border:1px solid transparent;background:transparent;color:var(--text-muted);font-size:11px;font-weight:400;overflow:hidden;text-overflow:ellipsis}
|
|
592
594
|
#composer .actions-col .llmctx-new:hover{filter:none;color:var(--text);border-color:var(--accent-border);background:var(--accent-bg)}
|
|
@@ -594,8 +596,8 @@ body.has-session #composer .actions-col{flex-wrap:nowrap;flex-shrink:0}
|
|
|
594
596
|
#composer .actions-col .llmctx-new[hidden],body.has-session #composer .actions-col .llmctx-new{display:none}
|
|
595
597
|
/* The new chat's thinking chip, after it: shorter text, so a smaller share of the row. */
|
|
596
598
|
#composer .actions-col .think-new{max-width:min(25%,140px)}
|
|
597
|
-
/* Phones:
|
|
598
|
-
@media(max-width:600px){#composer .actions-col
|
|
599
|
+
/* Phones (the key hint is gone there): the chips keep their whole text and the row wraps, Start staying right (its margin-left:auto) on the last line; only a model name wider than the row ellipsizes. */
|
|
600
|
+
@media(max-width:600px){#composer .actions-col .model-pick{flex:0 1 auto;min-width:0;max-width:100%}#composer .actions-col .llmctx-new,#composer .actions-col .think-new{flex:0 0 auto;max-width:100%}}
|
|
599
601
|
/* The model search: opaque like #commandList (a list over the conversation
|
|
600
602
|
must stay readable), above the composer, or below it (.down) when the
|
|
601
603
|
start page leaves no room above, or over the composer (.over) when
|
|
@@ -603,7 +605,7 @@ body.has-session #composer .actions-col{flex-wrap:nowrap;flex-shrink:0}
|
|
|
603
605
|
#modelPanel{position:absolute;left:10px;width:min(560px,calc(100% - 20px));bottom:calc(100% + 8px);max-height:360px;border-radius:12px;z-index:21;display:flex;flex-direction:column;overflow:hidden;background:var(--bg-panel);border:1px solid var(--glass-border);box-shadow:inset 0 1px 0 var(--glass-highlight),var(--glass-shadow)}
|
|
604
606
|
#modelPanel.down{bottom:auto;top:calc(100% + 8px)}
|
|
605
607
|
#modelPanel.over{bottom:0}
|
|
606
|
-
#modelPanel[hidden],body.has-session #modelPanel{display:none}
|
|
608
|
+
#modelPanel[hidden],body.has-session:not(.fresh-link) #modelPanel{display:none}
|
|
607
609
|
#modelPanel .model-search{flex:none;display:flex;align-items:center;gap:8px;padding:0 12px;border-bottom:1px solid var(--border)}
|
|
608
610
|
#modelPanel .model-search svg{flex:none;width:14px;height:14px;color:var(--text-faint)}
|
|
609
611
|
#modelSearch{flex:1;min-width:0;height:38px;background:transparent;border:0;color:var(--text);font-size:13px}
|
|
@@ -46,8 +46,10 @@ function readRecent() {
|
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
// +onPick+ runs after a pick (the panel is closed by then).
|
|
50
|
-
|
|
49
|
+
// +onPick+ runs after a pick (the panel is closed by then). +remember+:
|
|
50
|
+
// whether a pick is this browser's choice for a new chat (stored, and in
|
|
51
|
+
// Recent); false on a fresh chain link, where a pick is that link's only.
|
|
52
|
+
export function createModelPicker({ button, panel, input, list, composer, center, onPick, remember = () => true }) {
|
|
51
53
|
let rows = [];
|
|
52
54
|
let defaultName = "";
|
|
53
55
|
let chosen = "";
|
|
@@ -160,10 +162,12 @@ export function createModelPicker({ button, panel, input, list, composer, center
|
|
|
160
162
|
const row = shown[index];
|
|
161
163
|
if (!row) return;
|
|
162
164
|
setChosen(row.name);
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
165
|
+
if (remember()) {
|
|
166
|
+
try {
|
|
167
|
+
localStorage.setItem(MODEL_KEY, row.name);
|
|
168
|
+
localStorage.setItem(RECENT_KEY, JSON.stringify(pushRecent(readRecent(), row.name)));
|
|
169
|
+
} catch (_) {}
|
|
170
|
+
}
|
|
167
171
|
close();
|
|
168
172
|
onPick?.(row.name);
|
|
169
173
|
}
|
|
@@ -227,6 +231,17 @@ export function createModelPicker({ button, panel, input, list, composer, center
|
|
|
227
231
|
setChosen(pickModel(rows, payload.default, stored || chosen));
|
|
228
232
|
return true;
|
|
229
233
|
},
|
|
234
|
+
// A name on the button, nothing stored (a fresh link's model); one that
|
|
235
|
+
// isn't a row (an alias, a typed host:id) shows as text, no row chosen.
|
|
236
|
+
show(name) {
|
|
237
|
+
setChosen(String(name || ""));
|
|
238
|
+
},
|
|
239
|
+
// Back to the start page's choice: +stored+ while offered, else the
|
|
240
|
+
// default (no fetch; nothing before the list is in).
|
|
241
|
+
restore(stored) {
|
|
242
|
+
if (!rows.length) return;
|
|
243
|
+
setChosen(pickModel(rows, defaultName, stored));
|
|
244
|
+
},
|
|
230
245
|
chosen: () => chosen,
|
|
231
246
|
// The picked model's row (its llm_context too); null before the list.
|
|
232
247
|
chosenRow: () => rows.find((r) => r.name === chosen) || null,
|
|
@@ -50,6 +50,11 @@ export function emptySessionState() {
|
|
|
50
50
|
reasoningHistory: null,
|
|
51
51
|
toolkit: null,
|
|
52
52
|
timing: normalizeTiming(),
|
|
53
|
+
// A turn started on it while the page had it open (setTurnRunning): a
|
|
54
|
+
// fresh link's picker goes at once, before the timing re-read brings the
|
|
55
|
+
// turn's record (chains.js freshLink). Never from a payload, so a resync
|
|
56
|
+
// of the same session keeps it.
|
|
57
|
+
turnStarted: false,
|
|
53
58
|
// The event id (or, from an older worker, the event_seq) the session was
|
|
54
59
|
// rendered at (null: no live worker then), where a stream opened later
|
|
55
60
|
// has to start; set by the page's resync, not by the payload.
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
// addCommandBubble(view), addNoteBubble(note), findInTurnRoots(selector),
|
|
32
32
|
// setUsedMemories(names), showRecap(text), showGuardrailWarning(text, label),
|
|
33
33
|
// showHookNotice(data), onInitEvent(type, data), upsertCard(card),
|
|
34
|
-
// refreshCardBodies() the page's hooks
|
|
34
|
+
// refreshCardBodies(), syncModelPick() the page's hooks
|
|
35
35
|
|
|
36
36
|
import { routeChunk } from "./chunk_router.js";
|
|
37
37
|
import { extractCtxPct, generationTokens } from "./ctx.js";
|
|
@@ -55,7 +55,7 @@ export function createStreamHandlers(deps) {
|
|
|
55
55
|
resync, updateInfoBar, applyPromptOps, showSteers, markRecapStale, setTurnRunning, setLiveStatus, endTurnView,
|
|
56
56
|
appendChunk, readTurnEnd, applyFinalMarkdown, refreshTimingFromSession, makeUserBubble, addEndBubble,
|
|
57
57
|
commandBubble, addCommandBubble, addNoteBubble, findInTurnRoots, setUsedMemories, showRecap,
|
|
58
|
-
showGuardrailWarning, showHookNotice, onInitEvent, upsertCard, refreshCardBodies,
|
|
58
|
+
showGuardrailWarning, showHookNotice, onInitEvent, upsertCard, refreshCardBodies, syncModelPick,
|
|
59
59
|
} = deps;
|
|
60
60
|
|
|
61
61
|
return {
|
|
@@ -210,6 +210,9 @@ export function createStreamHandlers(deps) {
|
|
|
210
210
|
state().served = null;
|
|
211
211
|
updateInfoBar();
|
|
212
212
|
}
|
|
213
|
+
// A fresh link's picker shows the model the worker kept, a refused
|
|
214
|
+
// /model too (its pick set the button before the answer).
|
|
215
|
+
if (view.modelName) syncModelPick?.();
|
|
213
216
|
// /llm-context changed what the next turn runs under.
|
|
214
217
|
if (data.llm_context) {
|
|
215
218
|
state().llmContext = data.llm_context;
|
|
@@ -258,12 +258,14 @@ function canceledSteps(g, record, tools) {
|
|
|
258
258
|
}
|
|
259
259
|
|
|
260
260
|
// A call's row from its tool record alone (no saved parts): the record's
|
|
261
|
-
// called_as too, so "(called as bash)"
|
|
261
|
+
// called_as and via too, so "(called as bash)" and "(via call_tool)" show
|
|
262
|
+
// after a reload, as live, and
|
|
262
263
|
// its no_match ("no match" on a grep that found nothing).
|
|
263
264
|
function recordRows(tools, iteration) {
|
|
264
265
|
return tools.filter((r) => Number(r.iteration) === iteration).map((r) => ({
|
|
265
266
|
key: `${iteration}:${r.call_index ?? 0}`, tool: r.tool, status: r.status, duration_ms: r.duration_ms,
|
|
266
267
|
...(r.called_as ? { called_as: r.called_as } : {}),
|
|
268
|
+
...(r.via ? { via: r.via } : {}),
|
|
267
269
|
...noMatch(r),
|
|
268
270
|
}));
|
|
269
271
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Where the toast sits (app.js placeToast measures, this decides): in a
|
|
2
|
+
// session, +gap+ above the highest of the composer card and what opens
|
|
3
|
+
// over it (the stage while it shows, a fresh link's open model panel), so
|
|
4
|
+
// it covers none of them; null elsewhere (the CSS's 24 px from the bottom).
|
|
5
|
+
// When that leaves the toast no room on screen it stays +gap+ from the top
|
|
6
|
+
// (covering what's under it rather than going off screen).
|
|
7
|
+
// @param tops the tops (px from the viewport's top) of what shows over the
|
|
8
|
+
// card; null for one that doesn't
|
|
9
|
+
// @return {number|null} the toast's bottom offset in px
|
|
10
|
+
export function toastBottom({ inSession, viewportHeight, dockTop, tops = [], toastHeight = 0, gap = 12 }) {
|
|
11
|
+
if (!inSession) return null;
|
|
12
|
+
const top = Math.min(dockTop, ...tops.filter((t) => Number.isFinite(t)));
|
|
13
|
+
const bottom = viewportHeight - top + gap;
|
|
14
|
+
const cap = viewportHeight - toastHeight - gap;
|
|
15
|
+
return Math.round(Math.max(0, Math.min(bottom, cap)));
|
|
16
|
+
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
// The session bar's tools chip: the session's toolkit when it isn't full
|
|
2
2
|
// ("tools lean"), with the tools it leaves out and how to change them in
|
|
3
3
|
// its tooltip. The summary is the session's toolkit ({name, label,
|
|
4
|
-
// left_out, fallback}: Toolkit::Explained#summary; the
|
|
5
|
-
// a command_ran; the server's own reading of the
|
|
6
|
-
//
|
|
4
|
+
// left_out, reachable, removed, fallback}: Toolkit::Explained#summary; the
|
|
5
|
+
// worker's snapshot or a command_ran; the server's own reading of the
|
|
6
|
+
// session file has no lists). Left out is reachable (find_tools and
|
|
7
|
+
// call_tool reach them) and removed (only /toolkit add brings one back).
|
|
8
|
+
// Changing it is /toolkit (no popover yet).
|
|
7
9
|
|
|
8
10
|
import { html, nothing } from "./vendor/lit-html.js";
|
|
9
11
|
|
|
@@ -13,7 +15,12 @@ export function chipModel(summary) {
|
|
|
13
15
|
const label = summary.label || summary.name;
|
|
14
16
|
const parts = [`Toolkit: ${label}, the tools this session declares.`];
|
|
15
17
|
if (summary.fallback) parts.push(`${summary.fallback}: every tool is declared (full) until it resolves.`);
|
|
16
|
-
|
|
18
|
+
const reachable = Array.isArray(summary.reachable) ? summary.reachable : [];
|
|
19
|
+
const removed = Array.isArray(summary.removed) ? summary.removed : [];
|
|
20
|
+
if (reachable.length) {
|
|
21
|
+
parts.push(`Reachable with find_tools: ${reachable.join(", ")}.`);
|
|
22
|
+
if (removed.length) parts.push(`Removed: ${removed.join(", ")}.`);
|
|
23
|
+
} else if (Array.isArray(summary.left_out)) {
|
|
17
24
|
parts.push(summary.left_out.length ? `Not loaded: ${summary.left_out.join(", ")}.` : "Nothing left out.");
|
|
18
25
|
}
|
|
19
26
|
parts.push("/toolkit lists them; /toolkit NAME, add TOOL or remove TOOL changes them from the next turn.");
|
|
@@ -100,8 +100,10 @@ export function toolRowInnerTemplate(row, { thumbs = () => nothing, stop = nothi
|
|
|
100
100
|
const params = shown
|
|
101
101
|
? html`<span class=${`activity-params${ref?.kind ? " ref" : ""}`} title=${hover || nothing} data-ref-kind=${attr(ref?.kind)} data-ref-path=${attr(ref?.kind && ref.path)} data-ref-line=${attr(ref?.kind && ref.line)} data-ref-end-line=${attr(ref?.kind && ref.end_line)}>${shown}</span>`
|
|
102
102
|
: nothing;
|
|
103
|
-
// The name the model called the tool by when it was an alias's,
|
|
104
|
-
|
|
103
|
+
// The name the model called the tool by when it was an alias's, or the
|
|
104
|
+
// tool it was reached through (call_tool), dim.
|
|
105
|
+
const alias = row.called_as ? html`<span class="activity-alias">(called as ${row.called_as})</span>`
|
|
106
|
+
: row.via ? html`<span class="activity-alias">(via ${row.via})</span>` : nothing;
|
|
105
107
|
const duration = formatDuration(row.duration_ms);
|
|
106
108
|
const full = row.status === "running" ? "" : String(row.output || "").replace(/^\[[^\]]*\]\s*/, "");
|
|
107
109
|
const output = full
|
data/lib/samagotchi/worker.rb
CHANGED
|
@@ -496,13 +496,13 @@ module Samagotchi
|
|
|
496
496
|
# turn from context:<name> whose first message is the wake note already
|
|
497
497
|
# in the conversation (named by +turn_id+), on the same rules as a
|
|
498
498
|
# delegate report's wake turn: it counts toward session.max_wakes (a
|
|
499
|
-
# peer's answer toward context.peer_turns too), and a failure goes back
|
|
499
|
+
# peer's answer toward context.peer_turns too, unless it agrees), and a failure goes back
|
|
500
500
|
# to before the turn (the note stays, as a plain one:
|
|
501
501
|
# WorkerInbound#unmark_wake) and pauses wakes until a human's input.
|
|
502
502
|
# +context_wake+: a WorkerInbound::ContextWake.
|
|
503
503
|
def run_context_wake_turn(context_wake)
|
|
504
504
|
name = context_wake.name
|
|
505
|
-
Log.info(:worker, "context_wake_turn", name: name, in_a_row: @wakes.count!(peer: context_wake.
|
|
505
|
+
Log.info(:worker, "context_wake_turn", name: name, in_a_row: @wakes.count!(peer: context_wake.peer_turn?))
|
|
506
506
|
@turn_flow.before_prompt_turn
|
|
507
507
|
run_engine_turn(nil, continue: true, origin: context_wake.origin,
|
|
508
508
|
id: context_wake.turn_id, max_iterations: IterationLimit.for) do |result, error|
|
|
@@ -47,6 +47,9 @@ module Samagotchi
|
|
|
47
47
|
def name = delivery.name
|
|
48
48
|
# For a peer agent's answer (its own budget, WorkerWakes#peer_open?).
|
|
49
49
|
def peer? = !delivery.peer.nil?
|
|
50
|
+
# Whether the wake counts toward context.peer_turns: a peer's answer
|
|
51
|
+
# that agrees closes the round and counts toward session.max_wakes only.
|
|
52
|
+
def peer_turn? = peer? && !delivery.agreed
|
|
50
53
|
def origin = { client_id: "#{ClientId::CONTEXT_PREFIX}#{name}" }
|
|
51
54
|
end
|
|
52
55
|
|
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.70.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dmitry Dedov
|
|
@@ -504,6 +504,8 @@ files:
|
|
|
504
504
|
- lib/samagotchi/tool_call_parser.rb
|
|
505
505
|
- lib/samagotchi/tool_declarations.rb
|
|
506
506
|
- lib/samagotchi/tool_ids.rb
|
|
507
|
+
- lib/samagotchi/tool_output_cut.rb
|
|
508
|
+
- lib/samagotchi/tool_output_files.rb
|
|
507
509
|
- lib/samagotchi/tool_response.rb
|
|
508
510
|
- lib/samagotchi/tool_row_fields.rb
|
|
509
511
|
- lib/samagotchi/tool_runner.rb
|
|
@@ -531,6 +533,7 @@ files:
|
|
|
531
533
|
- lib/samagotchi/tools/output_guardrails.rb
|
|
532
534
|
- lib/samagotchi/tools/peers.rb
|
|
533
535
|
- lib/samagotchi/tools/read.rb
|
|
536
|
+
- lib/samagotchi/tools/ref_text.rb
|
|
534
537
|
- lib/samagotchi/tools/register_reminder.rb
|
|
535
538
|
- lib/samagotchi/tools/registry.rb
|
|
536
539
|
- lib/samagotchi/tools/send_note.rb
|
|
@@ -640,6 +643,7 @@ files:
|
|
|
640
643
|
- lib/samagotchi/web/public/thinking_chip.js
|
|
641
644
|
- lib/samagotchi/web/public/thinking_ticker.js
|
|
642
645
|
- lib/samagotchi/web/public/timing.js
|
|
646
|
+
- lib/samagotchi/web/public/toast_place.js
|
|
643
647
|
- lib/samagotchi/web/public/toolkit_chip.js
|
|
644
648
|
- lib/samagotchi/web/public/turn_events.js
|
|
645
649
|
- lib/samagotchi/web/public/turn_html.js
|