samagotchi 0.99.0 → 0.100.1
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 +36 -1
- data/README.md +2 -2
- data/docs/architecture.md +9 -3
- data/docs/bundle-authoring.md +1 -1
- data/docs/cli.md +1 -1
- data/docs/guardrails.md +2 -2
- data/docs/hooks.md +6 -1
- data/docs/memory.md +1 -1
- data/docs/plugins.md +67 -2
- data/docs/releasing.md +1 -1
- data/docs/sessions.md +1 -1
- data/docs/sub-agent.md +7 -2
- data/lib/samagotchi/bundles/core/manifest.yml +4 -3
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/bundles/work-cap/manifest.yml +13 -0
- data/lib/samagotchi/bundles/work-cap/plugin.rb +401 -0
- data/lib/samagotchi/engine.rb +2 -1
- data/lib/samagotchi/hooks/loader.rb +34 -6
- data/lib/samagotchi/plugin/api.rb +2 -1
- data/lib/samagotchi/plugin/context.rb +10 -2
- data/lib/samagotchi/session_metrics/usage.rb +31 -0
- data/lib/samagotchi/session_metrics.rb +48 -5
- data/lib/samagotchi/testing/host.rb +30 -1
- data/lib/samagotchi/testing/recorder.rb +5 -1
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/public/app.js +44 -8
- data/lib/samagotchi/web/public/chains.js +24 -0
- data/lib/samagotchi/web/public/index.html +7 -1
- data/lib/samagotchi/web/public/markdown_place.js +47 -1
- data/lib/samagotchi/web/public/send_turn.js +6 -2
- data/lib/samagotchi/web/public/session_actions.js +10 -5
- data/lib/samagotchi/web/public/turn_view.js +21 -7
- metadata +4 -1
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require_relative "../cancellation_controller"
|
|
4
4
|
require_relative "../plugin/context"
|
|
5
5
|
require_relative "../session_metrics/cost"
|
|
6
|
+
require_relative "../session_metrics/usage"
|
|
6
7
|
require_relative "recorder"
|
|
7
8
|
|
|
8
9
|
module Samagotchi
|
|
@@ -49,7 +50,8 @@ module Samagotchi
|
|
|
49
50
|
use_toolkit: ->(name) { recorder.use_toolkit(name, toolkits.call) },
|
|
50
51
|
load_tools: ->(names, bundle) { recorder.load_tools(names, bundle) },
|
|
51
52
|
cancel_controller: ->(handler: false) { recorder[:cancelled] ? stopped : nil },
|
|
52
|
-
cost: -> { cost(recorder[:cost]) }
|
|
53
|
+
cost: -> { cost(recorder[:cost]) },
|
|
54
|
+
usage: -> { usage(recorder[:usage]) }
|
|
53
55
|
)
|
|
54
56
|
end
|
|
55
57
|
|
|
@@ -75,6 +77,33 @@ module Samagotchi
|
|
|
75
77
|
else raise ArgumentError, "cost: a number, a Hash or a callable, not #{value.inspect}"
|
|
76
78
|
end
|
|
77
79
|
end
|
|
80
|
+
|
|
81
|
+
# The parts a test may give bundle.set(usage: {...}).
|
|
82
|
+
USAGE_PARTS = %i[turns tool_calls tool_calls_by_tool steps active_ms tool_ms_by_tool].freeze
|
|
83
|
+
|
|
84
|
+
# The SessionMetrics::Usage ctx.usage returns for the recorder's
|
|
85
|
+
# +usage+ value (Recorder::DEFAULTS): zeros for the parts not given;
|
|
86
|
+
# tool_calls defaults to the sum of tool_calls_by_tool, active_ms to
|
|
87
|
+
# the sum of tool_ms_by_tool.
|
|
88
|
+
# @return [SessionMetrics::Usage]
|
|
89
|
+
def usage(value)
|
|
90
|
+
value = value.call while value.respond_to?(:call)
|
|
91
|
+
case value
|
|
92
|
+
when nil then SessionMetrics::Usage.zero
|
|
93
|
+
when SessionMetrics::Usage then value
|
|
94
|
+
when Hash
|
|
95
|
+
parts = value.transform_keys(&:to_sym)
|
|
96
|
+
unknown = parts.keys - USAGE_PARTS
|
|
97
|
+
raise ArgumentError, "unknown usage parts: #{unknown.join(", ")}" unless unknown.empty?
|
|
98
|
+
|
|
99
|
+
by_tool = parts.fetch(:tool_calls_by_tool, {}).transform_keys(&:to_s)
|
|
100
|
+
ms_by_tool = parts.fetch(:tool_ms_by_tool, {}).transform_keys(&:to_s)
|
|
101
|
+
SessionMetrics::Usage.new(turns: parts.fetch(:turns, 0), tool_calls: parts.fetch(:tool_calls, by_tool.values.sum),
|
|
102
|
+
tool_calls_by_tool: by_tool, steps: parts.fetch(:steps, 0),
|
|
103
|
+
active_ms: parts.fetch(:active_ms, ms_by_tool.values.sum), tool_ms_by_tool: ms_by_tool)
|
|
104
|
+
else raise ArgumentError, "usage: a Hash or a callable, not #{value.inspect}"
|
|
105
|
+
end
|
|
106
|
+
end
|
|
78
107
|
end
|
|
79
108
|
|
|
80
109
|
# The hook runtime (Hooks::Runtime: event[:notify] and the other
|
|
@@ -37,7 +37,11 @@ module Samagotchi
|
|
|
37
37
|
prompt_files: [], cancelled: false, state_dir: nil, running: true, toolkit_staged: true, load_tools_staged: nil,
|
|
38
38
|
# ctx.cost: nil (zero), a Float (reported USD), a Hash of the parts
|
|
39
39
|
# (reported:, estimated:, side:, known:) or a callable giving one.
|
|
40
|
-
cost: nil
|
|
40
|
+
cost: nil,
|
|
41
|
+
# ctx.usage: nil (zero), a Hash of the parts (turns:, tool_calls:,
|
|
42
|
+
# tool_calls_by_tool:, steps:, active_ms:, tool_ms_by_tool:) or a
|
|
43
|
+
# callable giving one.
|
|
44
|
+
usage: nil
|
|
41
45
|
}.freeze
|
|
42
46
|
|
|
43
47
|
attr_reader :notices, :cards, :steers, :stops, :questions, :model_requests, :toolkit_uses, :tool_loads, :answers,
|
data/lib/samagotchi/version.rb
CHANGED
|
@@ -38,7 +38,7 @@ import { createBubbles, hintEl, outputBubbleTemplate } from "./bubbles.js";
|
|
|
38
38
|
import { createCardsPlace } from "./cards_place.js";
|
|
39
39
|
import { createStageView } from "./stage_view.js";
|
|
40
40
|
import { createMarkdownPlace } from "./markdown_place.js";
|
|
41
|
-
import {
|
|
41
|
+
import { openerFill, openTitle, restoredComposer } from "./chains.js";
|
|
42
42
|
import { createTaskStopper } from "./task_stop.js";
|
|
43
43
|
import { appendQuote } from "./annotations.js";
|
|
44
44
|
import { parsePresets } from "./annotate_presets.js";
|
|
@@ -67,7 +67,7 @@ import {
|
|
|
67
67
|
appendAboveLiveTiming, mergeTiming, normalizeTiming,
|
|
68
68
|
} from "./timing.js";
|
|
69
69
|
import { createLiveTurn } from "./live_turn.js";
|
|
70
|
-
import { applyFinalAnswer, pairAnswerRefs, setAnswerRefs } from "./answer_refs.js";
|
|
70
|
+
import { answerRefsOf, applyFinalAnswer, pairAnswerRefs, setAnswerRefs } from "./answer_refs.js";
|
|
71
71
|
import { installRefClicks } from "./refs.js";
|
|
72
72
|
import { linkRefs, touchedFiles } from "./ref_scan.js";
|
|
73
73
|
import { DONE_MS, applyInitEvent, dropDone, fadeDone, initRowTemplate, newInitTasks, seedInitTasks } from "./init_row.js";
|
|
@@ -174,6 +174,15 @@ const infoCancelBtn = $("#infoCancelBtn");
|
|
|
174
174
|
const composeHintEl = $("#composeHint");
|
|
175
175
|
|
|
176
176
|
let selected = null;
|
|
177
|
+
// The session the composer's text was last written in ("" the start page,
|
|
178
|
+
// undefined unknown): the composer is one for every session, and continue →
|
|
179
|
+
// keeps only a draft of its own (fillOpener).
|
|
180
|
+
let draftFrom;
|
|
181
|
+
// Record that the composer's text was written now, in session +id+ (default
|
|
182
|
+
// the open one). Every write of text into the composer comes through here.
|
|
183
|
+
function noteDraft(id) {
|
|
184
|
+
draftFrom = id ?? selected ?? "";
|
|
185
|
+
}
|
|
177
186
|
let streamControl = null;
|
|
178
187
|
// The session list's stream (GET /api/events); null while the page falls
|
|
179
188
|
// back to fetching (a chi web without a hub).
|
|
@@ -213,6 +222,7 @@ const markdownPlace = createMarkdownPlace({
|
|
|
213
222
|
normalize,
|
|
214
223
|
pairAnswerRefs,
|
|
215
224
|
setAnswerRefs,
|
|
225
|
+
answerRefsOf,
|
|
216
226
|
});
|
|
217
227
|
const { renderMarkdown, pairRefsIn, linkRefsIn, applyFinalMarkdown, linkTextRefs } = markdownPlace;
|
|
218
228
|
const turnDom = view === "stage"
|
|
@@ -500,6 +510,7 @@ const sessionActions = createSessionActions({
|
|
|
500
510
|
toast: showToast,
|
|
501
511
|
addCreatedSession,
|
|
502
512
|
fillOpener,
|
|
513
|
+
restoreDraft,
|
|
503
514
|
closeLiveStream: () => streamControl?.close(),
|
|
504
515
|
workerGone,
|
|
505
516
|
});
|
|
@@ -759,6 +770,7 @@ function clearNotice() {
|
|
|
759
770
|
noticeEl.textContent = "";
|
|
760
771
|
}
|
|
761
772
|
promptEl.addEventListener("input", clearNotice);
|
|
773
|
+
promptEl.addEventListener("input", () => noteDraft());
|
|
762
774
|
|
|
763
775
|
async function select(id) {
|
|
764
776
|
clearNotice();
|
|
@@ -1277,6 +1289,7 @@ const streamHandlers = createStreamHandlers({
|
|
|
1277
1289
|
// A failed turn's line back: seen, so the composer opens (this visit).
|
|
1278
1290
|
openForVisit();
|
|
1279
1291
|
promptEl.value = text;
|
|
1292
|
+
noteDraft();
|
|
1280
1293
|
fitPrompt();
|
|
1281
1294
|
},
|
|
1282
1295
|
setChips,
|
|
@@ -1372,6 +1385,7 @@ const sendTurnControl = createSendTurn({
|
|
|
1372
1385
|
clear: clearComposer,
|
|
1373
1386
|
openForVisit: () => openForVisit(),
|
|
1374
1387
|
focus: () => focusPrompt(),
|
|
1388
|
+
noteDraft,
|
|
1375
1389
|
},
|
|
1376
1390
|
historyEl,
|
|
1377
1391
|
turnDom,
|
|
@@ -1525,6 +1539,7 @@ const unqueue = createUnqueue({
|
|
|
1525
1539
|
if (promptEl.value.trim() || chips.length) return false;
|
|
1526
1540
|
openForVisit();
|
|
1527
1541
|
promptEl.value = text;
|
|
1542
|
+
noteDraft();
|
|
1528
1543
|
fitPrompt();
|
|
1529
1544
|
promptEl.focus();
|
|
1530
1545
|
return true;
|
|
@@ -1571,7 +1586,10 @@ const commandList = createCommandList({
|
|
|
1571
1586
|
list: $("#commandList"),
|
|
1572
1587
|
prompt: promptEl,
|
|
1573
1588
|
commands: () => (selected && current.owner !== "tui" ? current.commands : []),
|
|
1574
|
-
afterPick: () =>
|
|
1589
|
+
afterPick: () => {
|
|
1590
|
+
noteDraft();
|
|
1591
|
+
fitPrompt();
|
|
1592
|
+
},
|
|
1575
1593
|
});
|
|
1576
1594
|
|
|
1577
1595
|
// ↑/↓: the prompt history shared with the TUI (prompt_history.js), from a
|
|
@@ -1588,6 +1606,7 @@ function historyKey(e) {
|
|
|
1588
1606
|
if (!recalled) return false;
|
|
1589
1607
|
e.preventDefault();
|
|
1590
1608
|
promptEl.value = recalled.value;
|
|
1609
|
+
noteDraft();
|
|
1591
1610
|
promptEl.setSelectionRange(recalled.caret, recalled.caret);
|
|
1592
1611
|
fitPrompt();
|
|
1593
1612
|
return true;
|
|
@@ -1620,15 +1639,31 @@ $("#infoContinueBtn").addEventListener("click", () => continueSelected());
|
|
|
1620
1639
|
$("#infoArchiveBtn").addEventListener("click", () => sessionActions.toggleArchiveSelected());
|
|
1621
1640
|
|
|
1622
1641
|
// The new link's composer: the opener (openerText: the previous link's own,
|
|
1623
|
-
// else the constant)
|
|
1624
|
-
//
|
|
1625
|
-
|
|
1626
|
-
|
|
1642
|
+
// else the constant). A draft written in this session (or the new link, or
|
|
1643
|
+
// of unknown origin) stays instead; one carried over from another session or
|
|
1644
|
+
// the start page gives way to the opener and is returned, for the toast's
|
|
1645
|
+
// Restore draft (chains.js openerFill). +created+: the continue's 201;
|
|
1646
|
+
// +continuedId+: the session continued.
|
|
1647
|
+
// @return the displaced draft's text, or null
|
|
1648
|
+
function fillOpener(created, continuedId) {
|
|
1649
|
+
const { value, displaced } = openerFill({
|
|
1650
|
+
text: promptEl.value, hasChips: chips.length > 0, draftFrom, continuedId, linkId: created.id, created,
|
|
1651
|
+
});
|
|
1652
|
+
if (value === null) return null;
|
|
1627
1653
|
openForVisit();
|
|
1628
|
-
promptEl.value =
|
|
1654
|
+
promptEl.value = value;
|
|
1655
|
+
noteDraft(created.id);
|
|
1629
1656
|
fitPrompt();
|
|
1630
1657
|
focusPrompt();
|
|
1631
1658
|
promptEl.setSelectionRange(promptEl.value.length, promptEl.value.length);
|
|
1659
|
+
return displaced;
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
// Restore draft (the continue toast's): +draft+ back into the composer,
|
|
1663
|
+
// over the untouched +opener+ or an empty one, else after what is there
|
|
1664
|
+
// (chains.js restoredComposer); in whatever session is open now.
|
|
1665
|
+
function restoreDraft(draft, opener) {
|
|
1666
|
+
fillComposer(restoredComposer({ current: promptEl.value, opener, draft }));
|
|
1632
1667
|
}
|
|
1633
1668
|
|
|
1634
1669
|
const toasts = createToasts({ el: $("#toast"), onChange: placeToast });
|
|
@@ -1642,6 +1677,7 @@ function showToast(text, action = null, options = {}) { toasts.show(text, action
|
|
|
1642
1677
|
function fillComposer(text) {
|
|
1643
1678
|
openForVisit();
|
|
1644
1679
|
promptEl.value = text;
|
|
1680
|
+
noteDraft();
|
|
1645
1681
|
fitPrompt();
|
|
1646
1682
|
focusPrompt();
|
|
1647
1683
|
promptEl.setSelectionRange(promptEl.value.length, promptEl.value.length);
|
|
@@ -25,6 +25,30 @@ export function openerText(created) {
|
|
|
25
25
|
return typeof own === "string" && own.trim() ? own : CONTINUE_OPENER;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
// What continue → does with the composer as the new link opens (app.js
|
|
29
|
+
// fillOpener): +text+ / +hasChips+ the composer now; +draftFrom+ the session
|
|
30
|
+
// the text was last written in ("" the start page, undefined unknown);
|
|
31
|
+
// +continuedId+ the session continued, +linkId+ the new link. An empty
|
|
32
|
+
// composer gets the opener. A draft written in either session, or of unknown
|
|
33
|
+
// origin, stays (+value+ null). One carried over from another session or the
|
|
34
|
+
// start page gives way to the opener and comes back as +displaced+ (the
|
|
35
|
+
// toast's Restore draft). Its image chips stay either way: only text is
|
|
36
|
+
// replaced.
|
|
37
|
+
export function openerFill({ text, hasChips, draftFrom, continuedId, linkId, created }) {
|
|
38
|
+
const hasText = !!text?.trim();
|
|
39
|
+
if (!hasText && !hasChips) return { value: openerText(created), displaced: null };
|
|
40
|
+
if (draftFrom === undefined || draftFrom === continuedId || draftFrom === linkId) return { value: null, displaced: null };
|
|
41
|
+
return { value: openerText(created), displaced: hasText ? text : null };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// The composer after Restore draft: +draft+ (a displaced draft) replaces
|
|
45
|
+
// +current+ when that is empty or still exactly +opener+; else it goes after
|
|
46
|
+
// it, past a blank line. Never loses text typed since.
|
|
47
|
+
export function restoredComposer({ current, opener, draft }) {
|
|
48
|
+
if (!current.trim() || current === opener) return draft;
|
|
49
|
+
return `${current.replace(/\s+$/, "")}\n\n${draft}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
28
52
|
// Whether +text+ (a title or a first message) is the opener and nothing
|
|
29
53
|
// else, trimmed: "Continue where we left off. Then fix X" is a title of its own.
|
|
30
54
|
export function isOpener(text) {
|
|
@@ -345,7 +345,13 @@ button.task-stop:hover:not(:disabled){background:var(--danger-bg-dim);filter:non
|
|
|
345
345
|
--md-strong-color:var(--text-bright);--md-blockquote-color:var(--text-muted);--md-code-color:var(--text);
|
|
346
346
|
--md-pre-size:12.5px;--md-cell-size:13px
|
|
347
347
|
}
|
|
348
|
-
.md-compact
|
|
348
|
+
.gen-text.md-compact{
|
|
349
|
+
--md-heading-size:14.5px;--md-heading-color:var(--text);
|
|
350
|
+
--md-subheading-size:14px;--md-subheading-color:var(--text-2);
|
|
351
|
+
--md-strong-color:var(--text);--md-blockquote-color:var(--text-muted);--md-code-color:var(--text);
|
|
352
|
+
--md-pre-size:12.5px;--md-cell-size:13px
|
|
353
|
+
}
|
|
354
|
+
.md-compact:is(.ts-asking,.question-said-full,.gen-text){white-space:normal}
|
|
349
355
|
.md-compact :is(h1,h2,h3,h4,h5,h6){font-size:var(--md-heading-size);font-weight:600;line-height:1.4;margin:8px 0 2px;color:var(--md-heading-color)}
|
|
350
356
|
.md-compact :is(h4,h5,h6){font-size:var(--md-subheading-size);color:var(--md-subheading-color)}
|
|
351
357
|
.md-compact :is(p,ul,ol,blockquote,table,pre){margin:0 0 6px}
|
|
@@ -7,11 +7,17 @@
|
|
|
7
7
|
// The texts whose refs linkTextRefs links: the answers and the closed
|
|
8
8
|
// steps' texts.
|
|
9
9
|
const TEXTS = ".bubble.output, .gen:not(.live) > .gen-text";
|
|
10
|
+
// The closed steps' texts still shown raw: a live turn's (a reload's come
|
|
11
|
+
// rendered, turn_view.js textTemplate).
|
|
12
|
+
const RAW_STEPS = ".gen:not(.live) > .gen-text:not(.markdown)";
|
|
10
13
|
|
|
11
14
|
export function createMarkdownPlace({
|
|
12
15
|
capabilities, renderText, roots, touchedFiles, currentDir, linkRefs, applyFinalAnswer, findInTurnRoots, normalize,
|
|
13
|
-
pairAnswerRefs, setAnswerRefs = () => {},
|
|
16
|
+
pairAnswerRefs, setAnswerRefs = () => {}, answerRefsOf = () => null,
|
|
14
17
|
}) {
|
|
18
|
+
// The raw step texts already waiting for their step's open.
|
|
19
|
+
const waiting = new WeakSet();
|
|
20
|
+
|
|
15
21
|
// A text the page already shows as its markdown HTML (POST /api/render,
|
|
16
22
|
// sanitised there), or null: markdown off here (no request), or a failed
|
|
17
23
|
// read (the raw text stays).
|
|
@@ -55,6 +61,7 @@ export function createMarkdownPlace({
|
|
|
55
61
|
// turn's end and after a reload's render. A ref already made stays as it
|
|
56
62
|
// is.
|
|
57
63
|
function linkTextRefs() {
|
|
64
|
+
renderStepTexts();
|
|
58
65
|
const pageRoots = roots();
|
|
59
66
|
const touched = touchedFiles(pageRoots);
|
|
60
67
|
for (const root of pageRoots) {
|
|
@@ -66,5 +73,44 @@ export function createMarkdownPlace({
|
|
|
66
73
|
}
|
|
67
74
|
}
|
|
68
75
|
|
|
76
|
+
// The closed steps' raw texts as markdown, as the answers' (markdown on
|
|
77
|
+
// only): each from its step's first open (a turn's end would ask once per
|
|
78
|
+
// step, folded or not), as a resolved question card's kept reply. The
|
|
79
|
+
// text stays as its source (stepText); its refs, a question's, pair again
|
|
80
|
+
// on the swap, then the `path:line` ones (linkRefsIn). A null render
|
|
81
|
+
// keeps the text, and the next open asks again.
|
|
82
|
+
function renderStepTexts() {
|
|
83
|
+
if (capabilities().display !== true) return;
|
|
84
|
+
for (const root of roots()) {
|
|
85
|
+
for (const el of root.querySelectorAll(RAW_STEPS)) {
|
|
86
|
+
const step = el.parentElement;
|
|
87
|
+
if (waiting.has(el) || !step?.addEventListener) continue;
|
|
88
|
+
waiting.add(el);
|
|
89
|
+
renderOnOpen(step, el, el.textContent);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function renderOnOpen(step, el, text) {
|
|
95
|
+
let state = "raw";
|
|
96
|
+
const draw = () => {
|
|
97
|
+
if (!step.open || state !== "raw") return;
|
|
98
|
+
state = "asking";
|
|
99
|
+
renderMarkdown(text).then((html) => {
|
|
100
|
+
if (!html || el.textContent !== text) { state = html ? "done" : "raw"; return; }
|
|
101
|
+
state = "done";
|
|
102
|
+
step.removeEventListener("toggle", draw);
|
|
103
|
+
const refs = answerRefsOf(el);
|
|
104
|
+
el.setAttribute("data-copy-source", text);
|
|
105
|
+
// Sanitised on the server (POST /api/render).
|
|
106
|
+
el.innerHTML = html;
|
|
107
|
+
el.classList.add("markdown", "md-compact");
|
|
108
|
+
linkRefsIn(el, refs);
|
|
109
|
+
});
|
|
110
|
+
};
|
|
111
|
+
step.addEventListener("toggle", draw);
|
|
112
|
+
draw();
|
|
113
|
+
}
|
|
114
|
+
|
|
69
115
|
return { renderMarkdown, pairRefsIn, linkRefsIn, applyFinalMarkdown, linkTextRefs };
|
|
70
116
|
}
|
|
@@ -19,7 +19,8 @@ import { composerAfterAck, dropEarlyRestores, restoreOnAck } from "./restore.js"
|
|
|
19
19
|
// (start_page.js: pendingPick(), chosenModel(), newChoices(),
|
|
20
20
|
// clearChoices()); +takeDelivery()+ the delivery line's armed choice;
|
|
21
21
|
// +composer+: {el (the text box), button (Send/Start), chips(),
|
|
22
|
-
// setChips(list), fit(), clear(), openForVisit(), focus()
|
|
22
|
+
// setChips(list), fit(), clear(), openForVisit(), focus(), noteDraft(id) (the
|
|
23
|
+
// text was written in session id)}; +historyEl+,
|
|
23
24
|
// +turnDom+; +makeUserBubble(prompt, opts)+, +bubbleById(id)+,
|
|
24
25
|
// +addCommandBubble(view)+; +addCreatedSession(created)+, +select(id)+,
|
|
25
26
|
// +showNotice(text)+, +updateInfoBar()+, +updateComposerMode()+,
|
|
@@ -128,7 +129,10 @@ export function createSendTurn({
|
|
|
128
129
|
const restored = restoreOnAck(ack?.enqueued_id, earlyRestores, { myId: clientId, sentIds });
|
|
129
130
|
// A restored line must be seen: the composer opens for this visit.
|
|
130
131
|
if (restored?.refill && sentTo === session()) composer.openForVisit();
|
|
131
|
-
|
|
132
|
+
const before = promptEl.value;
|
|
133
|
+
promptEl.value = composerAfterAck({ sent: line, current: before, refill: restored?.refill || "" });
|
|
134
|
+
// A line back from sentTo is a draft of that session, wherever the page is now.
|
|
135
|
+
if (promptEl.value.trim() && promptEl.value !== before) composer.noteDraft(sentTo);
|
|
132
136
|
composer.fit();
|
|
133
137
|
// Only the sent chips go: images added since stay (they go up at their own send).
|
|
134
138
|
const kept = chipsAfterSend(composer.chips(), sending);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { continueRefusal, continuedText, openChildrenTemplate } from "./chains.js";
|
|
1
|
+
import { continueRefusal, continuedText, openChildrenTemplate, openerText } from "./chains.js";
|
|
2
2
|
import { cantText, deleteConfirmText, goneSessionNotice } from "./format.js";
|
|
3
3
|
import { sessionHash } from "./route.js";
|
|
4
4
|
import { inTree } from "./sessions_list.js";
|
|
@@ -14,12 +14,14 @@ import { inTree } from "./sessions_list.js";
|
|
|
14
14
|
// +refresh()+ fetches the list); +lists+ (lists_view.js: render);
|
|
15
15
|
// +select(id)+, +clearSelection()+, +showNotice(text)+, +updateInfoBar()+,
|
|
16
16
|
// +toast(text, action, options)+, +addCreatedSession(created)+,
|
|
17
|
-
// +fillOpener(created)+ (the new link's composer
|
|
17
|
+
// +fillOpener(created, continuedId)+ (the new link's composer; returns a draft
|
|
18
|
+
// from another session it displaced, or null), +restoreDraft(draft, opener)+
|
|
19
|
+
// (that draft back, never over newer text), +closeLiveStream()+ (the live
|
|
18
20
|
// stream's close, the stream left as it is) and +workerGone(id)+ the page's.
|
|
19
21
|
// +alert+ / +confirm+: the browser's.
|
|
20
22
|
export function createSessionActions({
|
|
21
23
|
selected, state, sessions, setSessions, api, eventsLive, refresh, lists,
|
|
22
|
-
select, clearSelection, showNotice, updateInfoBar, toast, addCreatedSession, fillOpener, closeLiveStream, workerGone,
|
|
24
|
+
select, clearSelection, showNotice, updateInfoBar, toast, addCreatedSession, fillOpener, restoreDraft, closeLiveStream, workerGone,
|
|
23
25
|
alert = (text) => globalThis.alert(text), confirm = (text) => globalThis.confirm(text),
|
|
24
26
|
}) {
|
|
25
27
|
// The session this tab is stopping: its stream may close before the stop
|
|
@@ -105,8 +107,11 @@ export function createSessionActions({
|
|
|
105
107
|
return;
|
|
106
108
|
}
|
|
107
109
|
await select(next);
|
|
108
|
-
|
|
109
|
-
toast
|
|
110
|
+
// A draft carried over from elsewhere gave way to the opener: the
|
|
111
|
+
// toast brings it back, and stays until closed so it can't be lost.
|
|
112
|
+
const displaced = selected() === next ? fillOpener(created, id) : null;
|
|
113
|
+
const restore = displaced ? { label: "Restore draft", run: () => restoreDraft(displaced, openerText(created)) } : null;
|
|
114
|
+
toast(continuedText(id, created), restore, { sticky: sticky || !!displaced });
|
|
110
115
|
} catch (e) {
|
|
111
116
|
const refusal = continueRefusal(e, sessions());
|
|
112
117
|
if (refusal.kind === "open") {
|
|
@@ -71,13 +71,21 @@ function thinkingTemplate({ body, wrapped = true, line = null, cls = "thinking",
|
|
|
71
71
|
// full text. None when there is nothing to say. A closed step's text is
|
|
72
72
|
// set whole (.textContent): its refs (ref_scan.js) split it into spans
|
|
73
73
|
// that a text part's update would leave behind. +at+: a reloaded step's
|
|
74
|
-
// message index, for its refs (data-answer-at, as an answer's).
|
|
75
|
-
|
|
74
|
+
// message index, for its refs (data-answer-at, as an answer's). A
|
|
75
|
+
// reloaded step's +rendered+ markdown (its message's html, sanitised on the
|
|
76
|
+
// server, as an answer's) shows in place of the text, which it keeps as
|
|
77
|
+
// its source (stepText); a live step's renders once it closed
|
|
78
|
+
// (markdown_place.js renderStepTexts).
|
|
79
|
+
function textTemplate(text, { live, sentences, placeholder, at = null, rendered = null }) {
|
|
76
80
|
if (live && sentences?.length) {
|
|
77
81
|
return html`<div class="gen-text streaming"><div class="narration">${sentences.map((s) => html`<div class="sentence">${s}</div>`)}</div></div>`;
|
|
78
82
|
}
|
|
79
83
|
if (live) return placeholder ? html`<div class="gen-text streaming">…</div>` : nothing;
|
|
80
|
-
|
|
84
|
+
if (!text) return nothing;
|
|
85
|
+
if (typeof rendered === "string" && rendered) {
|
|
86
|
+
return html`<div class="gen-text markdown md-compact" data-answer-at=${at ?? nothing} data-copy-source=${text} .innerHTML=${rendered}></div>`;
|
|
87
|
+
}
|
|
88
|
+
return html`<div class="gen-text" data-answer-at=${at ?? nothing} .textContent=${text}></div>`;
|
|
81
89
|
}
|
|
82
90
|
|
|
83
91
|
// One step, live or reloaded. +gen+: { text, thinking, tools, answer } (a
|
|
@@ -97,7 +105,7 @@ export function stepTemplate(gen, st = {}) {
|
|
|
97
105
|
const failed = !live && model.failedCount(gen.tools) > 0;
|
|
98
106
|
// A step left with its thinking only reads "thinking" once.
|
|
99
107
|
const wrapped = live || !!text || gen.tools.length > 0;
|
|
100
|
-
return html`<details class=${`gen${live ? " live" : ""}${failed ? " has-failed" : ""}`} .open=${open}><summary class=${label.repeats ? "repeats" : nothing} @click=${on.stepClick ?? nothing}>${label.content}</summary>${steers}${gen.thinking ? thinkingTemplate({ body, wrapped, line, on }) : nothing}${textTemplate(text, { live, sentences, placeholder, at })}${items.length ? html`<div class="activity-body">${items}</div>` : nothing}</details>`;
|
|
108
|
+
return html`<details class=${`gen${live ? " live" : ""}${failed ? " has-failed" : ""}`} .open=${open}><summary class=${label.repeats ? "repeats" : nothing} @click=${on.stepClick ?? nothing}>${label.content}</summary>${steers}${gen.thinking ? thinkingTemplate({ body, wrapped, line, on }) : nothing}${textTemplate(text, { live, sentences, placeholder, at, rendered: gen.html })}${items.length ? html`<div class="activity-body">${items}</div>` : nothing}</details>`;
|
|
101
109
|
}
|
|
102
110
|
|
|
103
111
|
// A hook's line (hook_notice) as a step's row. data.line is a whole row of
|
|
@@ -807,10 +815,11 @@ function defaultPlaceAnswer(appendToHistory) {
|
|
|
807
815
|
|
|
808
816
|
// The text of a reloaded block's step +iteration+ (1-based, as the snapshot's
|
|
809
817
|
// cards count), "" when it has none: a reloaded question card's asking
|
|
810
|
-
// reply.
|
|
818
|
+
// reply, as written (a rendered step keeps it as data-copy-source).
|
|
811
819
|
export function stepText(block, iteration) {
|
|
812
820
|
const step = block?.querySelectorAll(":scope > .gen")[Number(iteration) - 1];
|
|
813
|
-
|
|
821
|
+
const text = step?.querySelector(":scope > .gen-text");
|
|
822
|
+
return text?.getAttribute("data-copy-source") ?? text?.textContent ?? "";
|
|
814
823
|
}
|
|
815
824
|
|
|
816
825
|
// A re-rendered failed turn's line (a .bubble.cancel, as live's
|
|
@@ -885,7 +894,12 @@ export function turnHistoryNodes(items, timing, { thumbs }) {
|
|
|
885
894
|
if (plainThinking) {
|
|
886
895
|
parts.push(thinkingTemplate({ body: only.thinking, cls: "bubble thinking" }));
|
|
887
896
|
} else if (group.steps.length) {
|
|
888
|
-
|
|
897
|
+
// A step's text with its message's rendered markdown (html: web.markdown on).
|
|
898
|
+
const gens = group.steps.map((step) => ({
|
|
899
|
+
text: step.i === null ? "" : String(items[step.i].content ?? ""),
|
|
900
|
+
html: step.i === null ? null : items[step.i].html ?? null,
|
|
901
|
+
thinking: step.thinking || "", tools: step.tools,
|
|
902
|
+
}));
|
|
889
903
|
const steps = gens.map((gen, k) => stepTemplate(gen, {
|
|
890
904
|
// The steers the step answered first, where the live rows went.
|
|
891
905
|
steers: (group.steps[k].steers || []).map((i) => steerTemplate(i, false)),
|
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.100.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dmitry Dedov
|
|
@@ -266,6 +266,8 @@ files:
|
|
|
266
266
|
- lib/samagotchi/bundles/system/manifest.yml
|
|
267
267
|
- lib/samagotchi/bundles/system/memory_guide.md
|
|
268
268
|
- lib/samagotchi/bundles/system/self_map.md
|
|
269
|
+
- lib/samagotchi/bundles/work-cap/manifest.yml
|
|
270
|
+
- lib/samagotchi/bundles/work-cap/plugin.rb
|
|
269
271
|
- lib/samagotchi/cache_confirm.rb
|
|
270
272
|
- lib/samagotchi/cancellation_controller.rb
|
|
271
273
|
- lib/samagotchi/child_env.rb
|
|
@@ -523,6 +525,7 @@ files:
|
|
|
523
525
|
- lib/samagotchi/session_metrics/model_segment.rb
|
|
524
526
|
- lib/samagotchi/session_metrics/side_totals.rb
|
|
525
527
|
- lib/samagotchi/session_metrics/totals.rb
|
|
528
|
+
- lib/samagotchi/session_metrics/usage.rb
|
|
526
529
|
- lib/samagotchi/session_model_settings.rb
|
|
527
530
|
- lib/samagotchi/session_observer.rb
|
|
528
531
|
- lib/samagotchi/session_retention.rb
|