samagotchi 0.65.0 → 0.66.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 +48 -1
- data/README.md +2 -1
- data/bin/chi +51 -2
- data/docs/cli.md +11 -1
- data/docs/configuration.md +51 -7
- data/docs/context.md +4 -1
- data/docs/internals/engine.md +4 -1
- data/docs/internals/prompt-caching.md +10 -2
- data/docs/memory.md +1 -1
- data/docs/plugins.md +163 -2
- data/docs/releasing.md +1 -1
- data/docs/sessions.md +10 -0
- data/docs/sub-agent.md +3 -1
- data/lib/samagotchi/bundles/claude-peer/manifest.yml +2 -2
- data/lib/samagotchi/bundles/claude-peer/plugin.rb +18 -1
- data/lib/samagotchi/bundles/codex-peer/manifest.yml +21 -0
- data/lib/samagotchi/bundles/codex-peer/plugin.rb +464 -0
- data/lib/samagotchi/bundles/codex-peer/scripts/chi-peer-SKILL.md +131 -0
- data/lib/samagotchi/bundles/codex-peer/scripts/pair +92 -0
- data/lib/samagotchi/bundles/codex-peer/skill_codex_peer.md +68 -0
- data/lib/samagotchi/bundles/coordinator/manifest.yml +5 -5
- data/lib/samagotchi/bundles/coordinator/plugin.rb +24 -2
- data/lib/samagotchi/bundles/dev/manifest.yml +2 -1
- data/lib/samagotchi/bundles/system/delegated.md +2 -1
- data/lib/samagotchi/bundles/system/manifest.yml +3 -3
- data/lib/samagotchi/bundles/system/memory_guide.md +1 -1
- data/lib/samagotchi/cache_confirm.rb +9 -6
- data/lib/samagotchi/commands/registry.rb +25 -8
- data/lib/samagotchi/config.rb +8 -1
- data/lib/samagotchi/engine.rb +112 -4
- data/lib/samagotchi/kernel_loop.rb +9 -1
- data/lib/samagotchi/llm/anthropic_assembly.rb +4 -2
- data/lib/samagotchi/plugin/api.rb +80 -7
- data/lib/samagotchi/plugin/context.rb +22 -1
- data/lib/samagotchi/plugin/loader.rb +3 -2
- data/lib/samagotchi/send_command.rb +23 -7
- data/lib/samagotchi/session.rb +18 -1
- data/lib/samagotchi/session_commands.rb +16 -1
- data/lib/samagotchi/session_manager.rb +4 -1
- data/lib/samagotchi/session_setup.rb +14 -7
- data/lib/samagotchi/system_prompt.rb +16 -1
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +19 -9
- data/lib/samagotchi/terminal_ui.rb +18 -2
- data/lib/samagotchi/tool_runner.rb +13 -1
- data/lib/samagotchi/toolkit.rb +246 -0
- data/lib/samagotchi/toolkit_command.rb +117 -0
- data/lib/samagotchi/tools/registry.rb +43 -6
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +12 -0
- data/lib/samagotchi/web/public/annotate_actions.js +109 -0
- data/lib/samagotchi/web/public/annotate_bar.js +84 -2
- data/lib/samagotchi/web/public/app.js +21 -7
- data/lib/samagotchi/web/public/commands.js +4 -2
- data/lib/samagotchi/web/public/index.html +9 -0
- data/lib/samagotchi/web/public/info_bar.js +5 -3
- data/lib/samagotchi/web/public/session_state.js +3 -0
- data/lib/samagotchi/web/public/stream_handlers.js +13 -0
- data/lib/samagotchi/web/public/toolkit_chip.js +29 -0
- data/lib/samagotchi/worker.rb +2 -0
- metadata +10 -1
|
@@ -35,8 +35,15 @@ module Samagotchi
|
|
|
35
35
|
|
|
36
36
|
def initialize
|
|
37
37
|
@entries = {}
|
|
38
|
+
@toolkit = nil
|
|
38
39
|
end
|
|
39
40
|
|
|
41
|
+
# The session's resolved toolkit (Toolkit::Resolved), which the lists
|
|
42
|
+
# below and #offered filter by; nil declares every tool (full). The
|
|
43
|
+
# Engine sets it at a turn's start, before the prompt is built, and
|
|
44
|
+
# only there: a request's declared tools never change mid-turn.
|
|
45
|
+
attr_accessor :toolkit
|
|
46
|
+
|
|
40
47
|
# @return [Entry]
|
|
41
48
|
def register(name, schema:, handler:, label: nil, preview: nil, targets: nil, refs: nil, source: "core", layer: nil)
|
|
42
49
|
raise ArgumentError, "tool #{name} is already registered" if @entries.key?(name)
|
|
@@ -59,19 +66,40 @@ module Samagotchi
|
|
|
59
66
|
# @return [Entry, nil]
|
|
60
67
|
def offered(name, layers: [])
|
|
61
68
|
entry = self[name]
|
|
62
|
-
entry if entry&.offered?(layers)
|
|
69
|
+
entry if entry&.offered?(layers) && declared?(entry)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# The entry +name+ names when the session's toolkit leaves it out (a
|
|
73
|
+
# real tool the model can't call here), nil otherwise.
|
|
74
|
+
# @return [Entry, nil]
|
|
75
|
+
def left_out(name, layers: [])
|
|
76
|
+
entry = self[name]
|
|
77
|
+
entry if entry&.offered?(layers) && !declared?(entry)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# The tools +toolkit+ (the session's by default) leaves out of a turn
|
|
81
|
+
# under +layers+.
|
|
82
|
+
# @return [Array<String>] in declaration order
|
|
83
|
+
def left_out_names(layers: [], toolkit: @toolkit)
|
|
84
|
+
ordered(@entries.values.select { |entry| entry.offered?(layers) && !declared?(entry, toolkit) }).map(&:name)
|
|
63
85
|
end
|
|
64
86
|
|
|
87
|
+
# Every tool +source+ registered, declared or not (a plugin's tool set
|
|
88
|
+
# changing: Plugin::Api.apply_tools).
|
|
89
|
+
# @return [Array<Entry>]
|
|
90
|
+
def by_source(source) = @entries.values.select { |entry| entry.source == source }
|
|
91
|
+
|
|
65
92
|
# The tools a turn offers is per turn: +layers+, the turn's LLM
|
|
66
93
|
# context layers (LLMContextStrategy), adds the tools gated by them.
|
|
67
94
|
# Without layers (none, the default) the list is what it always was.
|
|
68
95
|
# @return [Array<String>] in declaration order
|
|
69
|
-
def names(layers: []) = entries(layers: layers).map(&:name)
|
|
96
|
+
def names(layers: [], toolkit: @toolkit) = entries(layers: layers, toolkit: toolkit).map(&:name)
|
|
70
97
|
|
|
71
|
-
# @return [Array<Entry>] in declaration order
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
98
|
+
# @return [Array<Entry>] in declaration order: the ones the turn's
|
|
99
|
+
# layers offer and the session's toolkit declares
|
|
100
|
+
# (+toolkit+: another one's, for what a change would declare)
|
|
101
|
+
def entries(layers: [], toolkit: @toolkit)
|
|
102
|
+
ordered(@entries.values.select { |entry| entry.offered?(layers) && declared?(entry, toolkit) })
|
|
75
103
|
end
|
|
76
104
|
|
|
77
105
|
# @return [Array<Hash>] the schemas, in declaration order
|
|
@@ -81,6 +109,15 @@ module Samagotchi
|
|
|
81
109
|
@entries.freeze
|
|
82
110
|
super
|
|
83
111
|
end
|
|
112
|
+
|
|
113
|
+
private
|
|
114
|
+
|
|
115
|
+
def declared?(entry, toolkit = @toolkit) = toolkit.nil? || toolkit.declares?(entry)
|
|
116
|
+
|
|
117
|
+
def ordered(entries)
|
|
118
|
+
core, added = entries.partition(&:core?)
|
|
119
|
+
core + added.sort_by { |entry| [entry.source.to_s, entry.name] }
|
|
120
|
+
end
|
|
84
121
|
end
|
|
85
122
|
end
|
|
86
123
|
end
|
data/lib/samagotchi/version.rb
CHANGED
data/lib/samagotchi/web/app.rb
CHANGED
|
@@ -566,6 +566,14 @@ module Samagotchi
|
|
|
566
566
|
LLMContextStrategy.for_session(session, registry: host_registry)
|
|
567
567
|
end
|
|
568
568
|
|
|
569
|
+
# A session's toolkit as its file names it ({name:, label:}), nil on
|
|
570
|
+
# full: without a worker nothing resolves it (a bundle's kits load in
|
|
571
|
+
# the worker), so the tools it leaves out aren't known here.
|
|
572
|
+
def toolkit_for(session)
|
|
573
|
+
toolkit = session.toolkit
|
|
574
|
+
toolkit && !toolkit.full? ? { name: toolkit.name, label: toolkit.label } : nil
|
|
575
|
+
end
|
|
576
|
+
|
|
569
577
|
# A session's thinking level with where it came from and its own
|
|
570
578
|
# (SettingExplained#summary), as its worker would resolve it; nil
|
|
571
579
|
# when it can't be. Config and the registry only: no network.
|
|
@@ -848,6 +856,10 @@ module Samagotchi
|
|
|
848
856
|
# native host), else the server's reading (an older worker, none).
|
|
849
857
|
history = snapshot&.key?("reasoning_history") ? snapshot["reasoning_history"] : reasoning_history_for(session)
|
|
850
858
|
session_json = session_json.merge(reasoning_history: history)
|
|
859
|
+
# Its toolkit when it isn't full (the tools chip): the worker's, which
|
|
860
|
+
# resolved it, else the name saved in the session file.
|
|
861
|
+
toolkit = snapshot&.key?("toolkit") ? snapshot["toolkit"] : toolkit_for(session)
|
|
862
|
+
session_json = session_json.merge(toolkit: toolkit)
|
|
851
863
|
raw_messages = turn_snapshot ? turn_snapshot["messages"] : session.messages
|
|
852
864
|
timing = timing_payload(id, live_metrics: snapshot && snapshot["metrics"], persisted: persisted)
|
|
853
865
|
json_response(200, {
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// Annotation actions: a bundle's pills in the Annotate bar (chi.annotation,
|
|
2
|
+
// docs/plugins.md). Each prepares a command line of its plugin with the
|
|
3
|
+
// selection filled into its template under it:
|
|
4
|
+
//
|
|
5
|
+
// /claude validate
|
|
6
|
+
// From your thinking:
|
|
7
|
+
// > the quoted line
|
|
8
|
+
//
|
|
9
|
+
// and fills the composer with it (the note goes under), or sends it at once
|
|
10
|
+
// (send: true). The pills ride on the session's command listing (the
|
|
11
|
+
// worker's snapshot), so a muted bundle's go with its command. Pure: no DOM.
|
|
12
|
+
// A module of its own: static JS is cached at unversioned URLs, and a new
|
|
13
|
+
// export on an old module would blank the page.
|
|
14
|
+
|
|
15
|
+
import { quoteBlock } from "./annotations.js";
|
|
16
|
+
|
|
17
|
+
// How many plugin pills the bar shows; the rest go behind "⋯".
|
|
18
|
+
export const INLINE_ACTIONS = 3;
|
|
19
|
+
|
|
20
|
+
// The session's commands' annotations, flattened in listing order: {label,
|
|
21
|
+
// line, template, send, command, anytime, source}. anytime: the command
|
|
22
|
+
// runs mid-turn (a send pill needs it while a turn runs).
|
|
23
|
+
export function annotationActions(commands) {
|
|
24
|
+
const out = [];
|
|
25
|
+
for (const c of Array.isArray(commands) ? commands : []) {
|
|
26
|
+
if (!c || typeof c.name !== "string" || !Array.isArray(c.annotations)) continue;
|
|
27
|
+
for (const a of c.annotations) {
|
|
28
|
+
if (!a || typeof a.label !== "string" || !a.label || typeof a.line !== "string" || !a.line) continue;
|
|
29
|
+
out.push({
|
|
30
|
+
label: a.label,
|
|
31
|
+
line: a.line,
|
|
32
|
+
template: typeof a.template === "string" && a.template ? a.template : "{quote}",
|
|
33
|
+
send: a.send === true,
|
|
34
|
+
command: c.name,
|
|
35
|
+
anytime: c.mid_turn === "anytime" || c.anytime === true,
|
|
36
|
+
source: typeof c.source === "string" ? c.source : "",
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Whether two action lists draw the same pills (the bar redraws only on a
|
|
44
|
+
// change).
|
|
45
|
+
export function sameActions(a, b) {
|
|
46
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// The selection as templates read it: {quote} the labelled block Annotate
|
|
50
|
+
// makes (annotations.js quoteBlock), {text} the selection alone with the
|
|
51
|
+
// same trimming (CRLF → LF, each line's trailing blanks, blank lines at
|
|
52
|
+
// either end; indentation kept), {source} where it came from (answer,
|
|
53
|
+
// thinking, step, tool, user). null when it holds no text.
|
|
54
|
+
export function selectionFields(raw, source = {}) {
|
|
55
|
+
const quote = quoteBlock(raw, source);
|
|
56
|
+
if (!quote) return null;
|
|
57
|
+
const lines = String(raw ?? "").replace(/\r\n?/g, "\n").split("\n").map((l) => l.replace(/\s+$/, ""));
|
|
58
|
+
while (lines.length && !lines[0]) lines.shift();
|
|
59
|
+
while (lines.length && !lines[lines.length - 1]) lines.pop();
|
|
60
|
+
return { quote, text: lines.join("\n"), source: source.kind || "answer" };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// +template+ with {quote}, {text} and {source} filled from +fields+; any
|
|
64
|
+
// other {x} stays as written.
|
|
65
|
+
export function fillTemplate(template, fields) {
|
|
66
|
+
return String(template ?? "").replace(/\{(quote|text|source)\}/g, (_, key) => String(fields?.[key] ?? ""));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// The line the pill prepares: its command line, the filled template under it.
|
|
70
|
+
export function actionLine(action, fields) {
|
|
71
|
+
return `${action.line}\n${fillTemplate(action.template, fields)}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// The composer's text after a fill pill (+line+: actionLine's):
|
|
75
|
+
// - empty: the line;
|
|
76
|
+
// - it already starts with the same command line: the filled part is
|
|
77
|
+
// appended (several selections collect under one command);
|
|
78
|
+
// - another draft: the line on top, the draft under it, after a blank line
|
|
79
|
+
// (nothing leaves chi without a click on Send).
|
|
80
|
+
export function composerWithAction(value, action, line) {
|
|
81
|
+
const draft = String(value ?? "");
|
|
82
|
+
if (!draft.trim()) return line;
|
|
83
|
+
const filled = line.slice(action.line.length + 1);
|
|
84
|
+
if (draft.trimStart().split("\n")[0].trim() === action.line) {
|
|
85
|
+
const before = draft.replace(/\s+$/, "");
|
|
86
|
+
return `${before}\n\n${filled}`;
|
|
87
|
+
}
|
|
88
|
+
return `${line.replace(/\s+$/, "")}\n\n${draft.replace(/^\s*\n/, "")}`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Whether a pill can be clicked: a fill pill always; a send pill while a
|
|
92
|
+
// turn runs only when its command runs mid-turn (anytime), else it would come
|
|
93
|
+
// back busy.
|
|
94
|
+
export function actionEnabled(action, running) {
|
|
95
|
+
return !action.send || !running || action.anytime;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// A pill's tooltip: its label and bundle, that a send pill sends at once,
|
|
99
|
+
// and why it is disabled.
|
|
100
|
+
export function actionTitle(action, running) {
|
|
101
|
+
const base = action.source ? `${action.label} (${action.source})` : action.label;
|
|
102
|
+
if (!actionEnabled(action, running)) return `${base}: ${action.command} waits for the turn to end`;
|
|
103
|
+
return action.send ? `${base}: sends at once` : base;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// The pills shown inline and the ones behind "⋯".
|
|
107
|
+
export function splitInline(actions, max = INLINE_ACTIONS) {
|
|
108
|
+
return { inline: actions.slice(0, max), more: actions.slice(max) };
|
|
109
|
+
}
|
|
@@ -7,10 +7,15 @@
|
|
|
7
7
|
// bubble rewrites its text every frame, which would collapse it by the time
|
|
8
8
|
// a button is clicked.
|
|
9
9
|
//
|
|
10
|
+
// A bundle's pills (chi.annotation) come after the presets: each prepares
|
|
11
|
+
// its command line with the selection (annotate_actions.js), at most 3
|
|
12
|
+
// inline, the rest behind "⋯".
|
|
13
|
+
//
|
|
10
14
|
// The quoting rules are annotations.js'; this is the bar.
|
|
11
15
|
|
|
12
16
|
import { annotateBarLeft, annotationSource, quoteBlock } from "./annotations.js";
|
|
13
17
|
import { presetNote } from "./annotate_presets.js";
|
|
18
|
+
import { actionEnabled, actionTitle, sameActions, selectionFields, splitInline } from "./annotate_actions.js";
|
|
14
19
|
|
|
15
20
|
// @param presets the preset texts (annotate_presets.js parsePresets)
|
|
16
21
|
// @param canAnnotate () => whether the open session takes annotations (one
|
|
@@ -21,11 +26,18 @@ import { presetNote } from "./annotate_presets.js";
|
|
|
21
26
|
// until its step closes)
|
|
22
27
|
// @param onQuote (text) runs with the quote (and a preset's note) to append
|
|
23
28
|
// to the composer; the selection is cleared and the bar hidden first
|
|
29
|
+
// @param actions () => the open session's annotation actions
|
|
30
|
+
// (annotate_actions.js annotationActions), read on every selection
|
|
31
|
+
// @param running () => whether a turn runs (a send pill may wait for it)
|
|
32
|
+
// @param onAction (action, fields) runs with a clicked pill's action and the
|
|
33
|
+
// selection's {quote, text, source}; the selection is cleared and the bar
|
|
34
|
+
// hidden first
|
|
24
35
|
// @param doc, win the page's document and window
|
|
25
36
|
// @return {schedule(), hide(), el}; schedule() re-reads the selection on the
|
|
26
37
|
// next frame (call it on scroll)
|
|
27
38
|
export function createAnnotateBar({
|
|
28
|
-
presets, canAnnotate, roots, isLive, onQuote,
|
|
39
|
+
presets, canAnnotate, roots, isLive, onQuote, actions = () => [], running = () => false, onAction = () => {},
|
|
40
|
+
doc = globalThis.document, win = globalThis.window,
|
|
29
41
|
}) {
|
|
30
42
|
const bar = doc.createElement("div");
|
|
31
43
|
bar.id = "annotateBar";
|
|
@@ -46,11 +58,63 @@ export function createAnnotateBar({
|
|
|
46
58
|
btn.title = text;
|
|
47
59
|
bar.appendChild(btn);
|
|
48
60
|
});
|
|
61
|
+
// The plugin pills: a separator, the inline ones, then ⋯ and the list
|
|
62
|
+
// it opens. Redrawn only when the session's actions change.
|
|
63
|
+
const group = doc.createElement("span");
|
|
64
|
+
group.className = "annotate-actions";
|
|
65
|
+
group.hidden = true;
|
|
66
|
+
bar.appendChild(group);
|
|
49
67
|
doc.body.appendChild(bar);
|
|
50
68
|
let pendingQuote = null;
|
|
69
|
+
let pendingFields = null;
|
|
70
|
+
let drawn = [];
|
|
71
|
+
let moreList = null;
|
|
72
|
+
|
|
73
|
+
function drawActions(list) {
|
|
74
|
+
drawn = list;
|
|
75
|
+
moreList = null;
|
|
76
|
+
group.hidden = !list.length;
|
|
77
|
+
if (!list.length) return group.replaceChildren();
|
|
78
|
+
const sep = doc.createElement("span");
|
|
79
|
+
sep.className = "annotate-sep";
|
|
80
|
+
sep.setAttribute("aria-hidden", "true");
|
|
81
|
+
const pill = (action, i) => {
|
|
82
|
+
const btn = button(action.label, "annotate-action");
|
|
83
|
+
btn.dataset.action = String(i);
|
|
84
|
+
if (action.send) btn.dataset.sends = "";
|
|
85
|
+
return btn;
|
|
86
|
+
};
|
|
87
|
+
const { inline, more } = splitInline(list);
|
|
88
|
+
const nodes = [sep, ...inline.map(pill)];
|
|
89
|
+
if (more.length) {
|
|
90
|
+
const toggle = button("⋯", "annotate-more");
|
|
91
|
+
toggle.title = "More actions";
|
|
92
|
+
toggle.setAttribute("aria-label", "More actions");
|
|
93
|
+
moreList = doc.createElement("div");
|
|
94
|
+
moreList.className = "annotate-more-list";
|
|
95
|
+
moreList.hidden = true;
|
|
96
|
+
more.forEach((action, j) => moreList.appendChild(pill(action, inline.length + j)));
|
|
97
|
+
nodes.push(toggle, moreList);
|
|
98
|
+
}
|
|
99
|
+
group.replaceChildren(...nodes);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Each pill's state for whether a turn runs (a send pill may wait).
|
|
103
|
+
function markActions() {
|
|
104
|
+
const now = running();
|
|
105
|
+
const all = [...group.children, ...(moreList ? moreList.children : [])];
|
|
106
|
+
for (const btn of all) {
|
|
107
|
+
if (btn.dataset?.action === undefined) continue;
|
|
108
|
+
const action = drawn[Number(btn.dataset.action)];
|
|
109
|
+
btn.disabled = !actionEnabled(action, now);
|
|
110
|
+
btn.title = actionTitle(action, now);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
51
113
|
|
|
52
114
|
function hide() {
|
|
53
115
|
pendingQuote = null;
|
|
116
|
+
pendingFields = null;
|
|
117
|
+
if (moreList) moreList.hidden = true;
|
|
54
118
|
bar.hidden = true;
|
|
55
119
|
}
|
|
56
120
|
|
|
@@ -67,9 +131,14 @@ export function createAnnotateBar({
|
|
|
67
131
|
// Clip to where the selection starts: one tool row, one bubble.
|
|
68
132
|
const clipped = range.cloneRange();
|
|
69
133
|
if (!source.root.contains(range.endContainer)) clipped.setEnd(source.root, source.root.childNodes.length);
|
|
70
|
-
const
|
|
134
|
+
const raw = clipped.toString();
|
|
135
|
+
const block = quoteBlock(raw, source);
|
|
71
136
|
if (!block) return hide();
|
|
72
137
|
pendingQuote = block;
|
|
138
|
+
pendingFields = selectionFields(raw, source);
|
|
139
|
+
const list = actions() || [];
|
|
140
|
+
if (!sameActions(list, drawn)) drawActions(list);
|
|
141
|
+
markActions();
|
|
73
142
|
const rects = clipped.getClientRects();
|
|
74
143
|
const rect = rects.length ? rects[rects.length - 1] : clipped.getBoundingClientRect();
|
|
75
144
|
bar.hidden = false;
|
|
@@ -109,6 +178,19 @@ export function createAnnotateBar({
|
|
|
109
178
|
bar.addEventListener("click", (e) => {
|
|
110
179
|
const btn = e.target.closest("button");
|
|
111
180
|
if (!btn || !pendingQuote) return;
|
|
181
|
+
if (btn.className === "annotate-more") {
|
|
182
|
+
moreList.hidden = !moreList.hidden;
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
if (btn.dataset.action !== undefined) {
|
|
186
|
+
const action = drawn[Number(btn.dataset.action)];
|
|
187
|
+
if (!action || btn.disabled) return;
|
|
188
|
+
const fields = pendingFields;
|
|
189
|
+
win.getSelection()?.removeAllRanges();
|
|
190
|
+
hide();
|
|
191
|
+
onAction(action, fields);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
112
194
|
const preset = btn.dataset.index === undefined ? null : presets[Number(btn.dataset.index)];
|
|
113
195
|
const text = preset ? presetNote(pendingQuote, preset) : pendingQuote;
|
|
114
196
|
win.getSelection()?.removeAllRanges();
|
|
@@ -46,6 +46,7 @@ import { createTaskStopper } from "./task_stop.js";
|
|
|
46
46
|
import { appendQuote } from "./annotations.js";
|
|
47
47
|
import { parsePresets } from "./annotate_presets.js";
|
|
48
48
|
import { createAnnotateBar } from "./annotate_bar.js";
|
|
49
|
+
import { actionLine, annotationActions, composerWithAction } from "./annotate_actions.js";
|
|
49
50
|
import { copyText, installCopy } from "./copy.js";
|
|
50
51
|
import { ctxBarText, ctxBarTip, lastSpeedText, speedText } from "./ctx.js";
|
|
51
52
|
import { createQuestionCards } from "./question_cards.js";
|
|
@@ -1476,6 +1477,8 @@ function updateInfoBar() {
|
|
|
1476
1477
|
llmContext: current.llmContext,
|
|
1477
1478
|
thinking: current.thinking,
|
|
1478
1479
|
reasoningHistory: current.reasoningHistory,
|
|
1480
|
+
// The tools chip (toolkit_chip.js): null on full.
|
|
1481
|
+
toolkit: current.toolkit,
|
|
1479
1482
|
// The model notes the session's prompt carried.
|
|
1480
1483
|
notes: promptNotesChip(current.promptNotes),
|
|
1481
1484
|
memories: current.usedMemories,
|
|
@@ -2379,18 +2382,29 @@ function showToast(text, action = null, { sticky = false } = {}) {
|
|
|
2379
2382
|
}
|
|
2380
2383
|
|
|
2381
2384
|
// Annotate (annotate_bar.js): a selection in the conversation offers
|
|
2382
|
-
// "Annotate" and the presets, which append a quote to the composer
|
|
2385
|
+
// "Annotate" and the presets, which append a quote to the composer, and the
|
|
2386
|
+
// session's plugins' pills (chi.annotation, annotate_actions.js), which fill
|
|
2387
|
+
// the composer with their command line and the quote, or send it at once.
|
|
2388
|
+
function fillComposer(text) {
|
|
2389
|
+
promptEl.value = text;
|
|
2390
|
+
fitPrompt();
|
|
2391
|
+
promptEl.focus();
|
|
2392
|
+
promptEl.setSelectionRange(promptEl.value.length, promptEl.value.length);
|
|
2393
|
+
promptEl.scrollTop = promptEl.scrollHeight;
|
|
2394
|
+
}
|
|
2395
|
+
|
|
2383
2396
|
const annotateBar = createAnnotateBar({
|
|
2384
2397
|
presets: parsePresets(document.body.dataset.annotatePresets),
|
|
2385
2398
|
canAnnotate: () => !!selected && current.owner !== "tui",
|
|
2386
2399
|
roots: turnRoots,
|
|
2387
2400
|
isLive: (root) => liveTurn.running && turnDom.containsLive(root),
|
|
2388
|
-
onQuote: (text) =>
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2401
|
+
onQuote: (text) => fillComposer(appendQuote(promptEl.value, text)),
|
|
2402
|
+
actions: () => annotationActions(current.commands),
|
|
2403
|
+
running: () => liveTurn.running,
|
|
2404
|
+
onAction: (action, fields) => {
|
|
2405
|
+
const line = actionLine(action, fields);
|
|
2406
|
+
if (action.send) runSessionCommand(line);
|
|
2407
|
+
else fillComposer(composerWithAction(promptEl.value, action, line));
|
|
2394
2408
|
},
|
|
2395
2409
|
});
|
|
2396
2410
|
historyEl.addEventListener("scroll", annotateBar.schedule);
|
|
@@ -10,7 +10,8 @@ import { anchorInsertIndex, commandAnchor, isShellCommandLine, keepsCommandBubbl
|
|
|
10
10
|
// Whether a composer line in an open session is one of its commands
|
|
11
11
|
// (+commands+: the session GET's listing), as the terminals decide
|
|
12
12
|
// (Commands::Registry#lookup): a "!" line with something after the "!", or
|
|
13
|
-
// a listed name alone or followed by
|
|
13
|
+
// a listed name alone or followed by whitespace (a newline too: an
|
|
14
|
+
// annotation action's line puts the quote under the name); a terminal's own (/stats,
|
|
14
15
|
// /exit) too, which the page answers itself (webLocalReply). An unknown
|
|
15
16
|
// "/word" (a path, a typo) goes to the model as a prompt, as in a terminal.
|
|
16
17
|
export function sessionCommandLine(text, commands) {
|
|
@@ -19,7 +20,8 @@ export function sessionCommandLine(text, commands) {
|
|
|
19
20
|
if (!line.startsWith("/")) return false;
|
|
20
21
|
if (webLocalReply(line)) return true;
|
|
21
22
|
return (Array.isArray(commands) ? commands : [])
|
|
22
|
-
.some((c) => c && typeof c.name === "string"
|
|
23
|
+
.some((c) => c && typeof c.name === "string"
|
|
24
|
+
&& (line === c.name || (line.startsWith(c.name) && /\s/.test(line[c.name.length]))));
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
// /cut TEXT and /queue TEXT: the UIs' own words for a delivery (listed
|
|
@@ -480,6 +480,15 @@ button.task-stop:hover:not(:disabled){background:var(--danger-bg-dim);filter:non
|
|
|
480
480
|
#annotateBar{position:fixed;z-index:20;display:flex;flex-wrap:wrap;gap:4px;max-width:calc(100vw - 16px)}
|
|
481
481
|
#annotateBar[hidden]{display:none}
|
|
482
482
|
#annotateBar button{font-size:12px;padding:4px 10px;border-radius:999px;box-shadow:0 2px 8px var(--shadow-strong)}
|
|
483
|
+
/* A bundle's pills (chi.annotation) after a thin separator; beyond 3, "⋯" opens the rest in a list under it. */
|
|
484
|
+
#annotateBar .annotate-actions{display:flex;flex-wrap:wrap;gap:4px;align-items:center;position:relative}
|
|
485
|
+
#annotateBar .annotate-actions[hidden]{display:none}
|
|
486
|
+
#annotateBar .annotate-sep{width:1px;align-self:stretch;margin:3px 2px;background:var(--border-strong)}
|
|
487
|
+
#annotateBar .annotate-action,#annotateBar .annotate-more{background:var(--accent-bg);color:var(--accent);border:1px solid var(--accent-border);padding:3px 10px;max-width:32ch;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
488
|
+
#annotateBar .annotate-action:disabled{opacity:.5;cursor:not-allowed}
|
|
489
|
+
#annotateBar .annotate-action[data-sends]::after{content:"↗";margin-left:4px;opacity:.75}
|
|
490
|
+
#annotateBar .annotate-more-list{position:absolute;top:calc(100% + 4px);right:0;display:flex;flex-direction:column;align-items:flex-end;gap:4px}
|
|
491
|
+
#annotateBar .annotate-more-list[hidden]{display:none}
|
|
483
492
|
#annotateBar .annotate-preset{background:var(--bg-raised);color:var(--text-2);border:1px solid var(--border-strong);padding:3px 10px;max-width:32ch;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
484
493
|
/* Copy buttons (copy.js): top-right in an answer and a code block, left of a prompt (its right edge is the page's); shown on hover or focus, always (dim) on touch. They hold no text: "copied ✓" is CSS content. */
|
|
485
494
|
.bubble.output,.bubble.user{position:relative}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// The session bar's info line (#infoText): the open session's id, its copy
|
|
2
2
|
// chi --attach button, status, model, chain links, parent and delegates,
|
|
3
|
-
// llm ctx, thinking
|
|
3
|
+
// llm ctx, thinking, reasoning history and tools chips, model notes, memories, folder, ctx, speed,
|
|
4
4
|
// session clock and the worker's version badge. A lit-html template over a
|
|
5
5
|
// plain model app.js builds (updateInfoBar), so the markup lives here and
|
|
6
6
|
// runs under node --test (spec/web/public/info_bar.test.js).
|
|
@@ -16,6 +16,7 @@ import { ALL_SESSIONS_HASH, sessionHash } from "./route.js";
|
|
|
16
16
|
import { chipTemplate as llmContextChip } from "./llm_context_chip.js";
|
|
17
17
|
import { chipTemplate as thinkingChip } from "./thinking_chip.js";
|
|
18
18
|
import { chipTemplate as reasoningHistoryChip } from "./reasoning_history_chip.js";
|
|
19
|
+
import { chipTemplate as toolkitChip } from "./toolkit_chip.js";
|
|
19
20
|
|
|
20
21
|
export const PREV = "←";
|
|
21
22
|
export const NEXT = "→";
|
|
@@ -62,7 +63,8 @@ function kidsTemplate(kids) {
|
|
|
62
63
|
// mismatch}: format.js modelLabel), chain ({prev, next}: chainLinkTemplate's
|
|
63
64
|
// links or null), parent ({id, title} or null), kids (format.js
|
|
64
65
|
// childrenSummary), llmContext, thinking and reasoningHistory (the chips'
|
|
65
|
-
// summaries; reasoningHistory null on a native host: no chip),
|
|
66
|
+
// summaries; reasoningHistory null on a native host: no chip), toolkit
|
|
67
|
+
// (the tools chip's summary, null on full: no chip), notes
|
|
66
68
|
// ({text, title} or null), memories (the used names), dir, ctx ({text,
|
|
67
69
|
// tip}), speed ({text, tip} or null), clock (the session time's text, ""
|
|
68
70
|
// for none), worker ({badge, busy} or null).
|
|
@@ -70,7 +72,7 @@ export function infoBarHtml(bar) {
|
|
|
70
72
|
if (!bar) return nothing;
|
|
71
73
|
const attach = `chi --attach ${bar.id}`;
|
|
72
74
|
const memories = bar.memories || [];
|
|
73
|
-
return html`<div class="meta"><span class="id" title=${bar.id}>${String(bar.id).slice(0, 8)}</span><button type="button" class="copy-attach" title=${`Copy "${attach}" to attach a terminal`}>${bar.copied ? "copied ✓" : "copy chi --attach"}</button><span class=${bar.status ? `status ${bar.status}` : "status"}>${bar.status || ""}</span><span class=${`model${bar.model.mismatch ? " served-mismatch" : ""}`} title=${bar.model.title}>${bar.model.text}</span>${bar.chain?.prev ? chainLinkTemplate(bar.chain.prev, PREV, "The previous link of this chain") : nothing}${bar.chain?.next ? chainLinkTemplate(bar.chain.next, NEXT, "The next link of this chain") : nothing}${bar.parent ? html`<a class="model parent" href=${sessionHash(bar.parent.id)} title=${bar.parent.title}>delegated by ${String(bar.parent.id).slice(0, 8)}</a>` : nothing}${kidsTemplate(bar.kids)}${llmContextChip(bar.llmContext)}${thinkingChip(bar.thinking)}${reasoningHistoryChip(bar.reasoningHistory)}${bar.notes ? html`<span class="model notes-chip" title=${bar.notes.title}>${bar.notes.text}</span>` : nothing}${memories.length ? html`<span class="model mem-chip" title=${memChipTitle(memories)}>${memChipText(memories)}</span>` : nothing}<span class="dir" title=${bar.dir || ""}><bdi>${bar.dir || ""}</bdi></span><span class="model ctx" title=${bar.ctx.tip}>${bar.ctx.text}</span>${bar.speed ? html`<span class="model speed" title=${bar.speed.tip}>${bar.speed.text}</span>` : nothing}${bar.clock ? html`<span class="model session-time">${bar.clock}</span>` : nothing}${workerBadgeTemplate(bar.worker)}</div>`;
|
|
75
|
+
return html`<div class="meta"><span class="id" title=${bar.id}>${String(bar.id).slice(0, 8)}</span><button type="button" class="copy-attach" title=${`Copy "${attach}" to attach a terminal`}>${bar.copied ? "copied ✓" : "copy chi --attach"}</button><span class=${bar.status ? `status ${bar.status}` : "status"}>${bar.status || ""}</span><span class=${`model${bar.model.mismatch ? " served-mismatch" : ""}`} title=${bar.model.title}>${bar.model.text}</span>${bar.chain?.prev ? chainLinkTemplate(bar.chain.prev, PREV, "The previous link of this chain") : nothing}${bar.chain?.next ? chainLinkTemplate(bar.chain.next, NEXT, "The next link of this chain") : nothing}${bar.parent ? html`<a class="model parent" href=${sessionHash(bar.parent.id)} title=${bar.parent.title}>delegated by ${String(bar.parent.id).slice(0, 8)}</a>` : nothing}${kidsTemplate(bar.kids)}${llmContextChip(bar.llmContext)}${thinkingChip(bar.thinking)}${reasoningHistoryChip(bar.reasoningHistory)}${toolkitChip(bar.toolkit)}${bar.notes ? html`<span class="model notes-chip" title=${bar.notes.title}>${bar.notes.text}</span>` : nothing}${memories.length ? html`<span class="model mem-chip" title=${memChipTitle(memories)}>${memChipText(memories)}</span>` : nothing}<span class="dir" title=${bar.dir || ""}><bdi>${bar.dir || ""}</bdi></span><span class="model ctx" title=${bar.ctx.tip}>${bar.ctx.text}</span>${bar.speed ? html`<span class="model speed" title=${bar.speed.tip}>${bar.speed.text}</span>` : nothing}${bar.clock ? html`<span class="model session-time">${bar.clock}</span>` : nothing}${workerBadgeTemplate(bar.worker)}</div>`;
|
|
74
76
|
}
|
|
75
77
|
|
|
76
78
|
// Draws +bar+ into +el+ (#infoText), with the memories in its tooltip.
|
|
@@ -48,6 +48,7 @@ export function emptySessionState() {
|
|
|
48
48
|
// Which of its reasoning the model gets back from the next turn on, with
|
|
49
49
|
// its source (the reasoning history chip), or null (a native host).
|
|
50
50
|
reasoningHistory: null,
|
|
51
|
+
toolkit: null,
|
|
51
52
|
timing: normalizeTiming(),
|
|
52
53
|
};
|
|
53
54
|
}
|
|
@@ -83,6 +84,8 @@ export function sessionStateFrom(data) {
|
|
|
83
84
|
state.thinking = s.thinking || null;
|
|
84
85
|
// null on a native host (it ignores the level) and from an older worker.
|
|
85
86
|
state.reasoningHistory = s.reasoning_history || null;
|
|
87
|
+
// The tools chip's (null on full).
|
|
88
|
+
state.toolkit = s.toolkit || null;
|
|
86
89
|
state.parentId = s.parent_id || null;
|
|
87
90
|
state.continues = s.continues || null;
|
|
88
91
|
state.commands = Array.isArray(data.commands) ? data.commands : [];
|
|
@@ -229,6 +229,11 @@ export function createStreamHandlers(deps) {
|
|
|
229
229
|
updateInfoBar();
|
|
230
230
|
reasoningHistoryControl.refresh();
|
|
231
231
|
}
|
|
232
|
+
// /toolkit changed what the next turn declares; null: full (no chip).
|
|
233
|
+
if ("toolkit" in data) {
|
|
234
|
+
state().toolkit = data.toolkit || null;
|
|
235
|
+
updateInfoBar();
|
|
236
|
+
}
|
|
232
237
|
if (view.resync) await resync();
|
|
233
238
|
// A card's action that isn't anytime (the step-limit question's
|
|
234
239
|
// /continue) has no bubble: nothing to remove. An anytime or queued
|
|
@@ -246,6 +251,9 @@ export function createStreamHandlers(deps) {
|
|
|
246
251
|
// message renders as one) — no second bubble, or the output shows twice.
|
|
247
252
|
else if (view.resync && historyHasCommandBubble(findInTurnRoots(".bubble.command"), view.line)) { /* drawn by the resync */ }
|
|
248
253
|
else addCommandBubble(view);
|
|
254
|
+
// This tab's command refused as busy (a plugin's command mid-turn): the
|
|
255
|
+
// composer was cleared when it went; its line comes back to send later.
|
|
256
|
+
if (view.own && view.busy && composer.empty()) composer.refill(view.line);
|
|
249
257
|
},
|
|
250
258
|
// An anytime command (/help, a plugin's /btw) runs at once, beside a
|
|
251
259
|
// turn: its bubble goes here, so the cards it shows come after it. One
|
|
@@ -301,6 +309,11 @@ export function createStreamHandlers(deps) {
|
|
|
301
309
|
// shows elsewhere (a hook's links in the rendered answer) isn't drawn,
|
|
302
310
|
// not even in the stage trail; a joined turn's notices come here too.
|
|
303
311
|
hook_notice: (data) => {
|
|
312
|
+
// A plugin put the session on another toolkit (ctx.use_toolkit).
|
|
313
|
+
if ("toolkit" in data) {
|
|
314
|
+
state().toolkit = data.toolkit || null;
|
|
315
|
+
updateInfoBar();
|
|
316
|
+
}
|
|
304
317
|
if (shownElsewhere(data, capabilities())) return;
|
|
305
318
|
if (!turnDom.notice(data)) showHookNotice(data);
|
|
306
319
|
},
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// The session bar's tools chip: the session's toolkit when it isn't full
|
|
2
|
+
// ("tools lean"), with the tools it leaves out and how to change them in
|
|
3
|
+
// its tooltip. The summary is the session's toolkit ({name, label,
|
|
4
|
+
// left_out, fallback}: Toolkit::Explained#summary; the worker's snapshot or
|
|
5
|
+
// a command_ran; the server's own reading of the session file has no
|
|
6
|
+
// left_out). Changing it is /toolkit (no popover yet).
|
|
7
|
+
|
|
8
|
+
import { html, nothing } from "./vendor/lit-html.js";
|
|
9
|
+
|
|
10
|
+
// The chip's text and tooltip; null without a summary (full).
|
|
11
|
+
export function chipModel(summary) {
|
|
12
|
+
if (!summary) return null;
|
|
13
|
+
const label = summary.label || summary.name;
|
|
14
|
+
const parts = [`Toolkit: ${label}, the tools this session declares.`];
|
|
15
|
+
if (summary.fallback) parts.push(`${summary.fallback}: every tool is declared (full) until it resolves.`);
|
|
16
|
+
if (Array.isArray(summary.left_out)) {
|
|
17
|
+
parts.push(summary.left_out.length ? `Not loaded: ${summary.left_out.join(", ")}.` : "Nothing left out.");
|
|
18
|
+
}
|
|
19
|
+
parts.push("/toolkit lists them; /toolkit NAME, add TOOL or remove TOOL changes them from the next turn.");
|
|
20
|
+
return { text: `tools ${label}${summary.fallback ? " ?" : ""}`, title: parts.join(" ") };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// The session bar's chip (a lit-html template; info_bar.js), nothing on
|
|
24
|
+
// full.
|
|
25
|
+
export function chipTemplate(summary) {
|
|
26
|
+
const chip = chipModel(summary);
|
|
27
|
+
if (!chip) return nothing;
|
|
28
|
+
return html`<span class="model tools-chip" title=${chip.title}>${chip.text}</span>`;
|
|
29
|
+
}
|
data/lib/samagotchi/worker.rb
CHANGED
|
@@ -878,6 +878,8 @@ module Samagotchi
|
|
|
878
878
|
# /reasoning-history or /model: the level the next turn runs under
|
|
879
879
|
# (the web's chip); nil on a native host, which ignores it.
|
|
880
880
|
event[:reasoning_history] = @engine.reasoning_history_explained&.summary if changed.intersect?(%i[reasoning_history model])
|
|
881
|
+
# /toolkit: what the next turn declares (the web's chip; nil on full).
|
|
882
|
+
event[:toolkit] = @engine.toolkit_summary if changed.include?(:toolkit)
|
|
881
883
|
event[:confirm] = confirm.to_h if confirm
|
|
882
884
|
event[:output_truncated] = true if text.length > COMMAND_OUTPUT_LIMIT
|
|
883
885
|
@engine.announce(event)
|
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.66.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dmitry Dedov
|
|
@@ -190,6 +190,11 @@ files:
|
|
|
190
190
|
- lib/samagotchi/bundles/claude-peer/scripts/chi-peer-SKILL.md
|
|
191
191
|
- lib/samagotchi/bundles/claude-peer/scripts/pair
|
|
192
192
|
- lib/samagotchi/bundles/claude-peer/skill_claude_peer.md
|
|
193
|
+
- lib/samagotchi/bundles/codex-peer/manifest.yml
|
|
194
|
+
- lib/samagotchi/bundles/codex-peer/plugin.rb
|
|
195
|
+
- lib/samagotchi/bundles/codex-peer/scripts/chi-peer-SKILL.md
|
|
196
|
+
- lib/samagotchi/bundles/codex-peer/scripts/pair
|
|
197
|
+
- lib/samagotchi/bundles/codex-peer/skill_codex_peer.md
|
|
193
198
|
- lib/samagotchi/bundles/coordinator/manifest.yml
|
|
194
199
|
- lib/samagotchi/bundles/coordinator/plugin.rb
|
|
195
200
|
- lib/samagotchi/bundles/coordinator/skill_coordinator.md
|
|
@@ -489,6 +494,8 @@ files:
|
|
|
489
494
|
- lib/samagotchi/tool_row_fields.rb
|
|
490
495
|
- lib/samagotchi/tool_runner.rb
|
|
491
496
|
- lib/samagotchi/tool_view.rb
|
|
497
|
+
- lib/samagotchi/toolkit.rb
|
|
498
|
+
- lib/samagotchi/toolkit_command.rb
|
|
492
499
|
- lib/samagotchi/tools/args.rb
|
|
493
500
|
- lib/samagotchi/tools/ask_user_question.rb
|
|
494
501
|
- lib/samagotchi/tools/builtin_calls.rb
|
|
@@ -543,6 +550,7 @@ files:
|
|
|
543
550
|
- lib/samagotchi/web/markdown_renderer.rb
|
|
544
551
|
- lib/samagotchi/web/message_parts.rb
|
|
545
552
|
- lib/samagotchi/web/public/activity.js
|
|
553
|
+
- lib/samagotchi/web/public/annotate_actions.js
|
|
546
554
|
- lib/samagotchi/web/public/annotate_bar.js
|
|
547
555
|
- lib/samagotchi/web/public/annotate_presets.js
|
|
548
556
|
- lib/samagotchi/web/public/annotations.js
|
|
@@ -608,6 +616,7 @@ files:
|
|
|
608
616
|
- lib/samagotchi/web/public/thinking_chip.js
|
|
609
617
|
- lib/samagotchi/web/public/thinking_ticker.js
|
|
610
618
|
- lib/samagotchi/web/public/timing.js
|
|
619
|
+
- lib/samagotchi/web/public/toolkit_chip.js
|
|
611
620
|
- lib/samagotchi/web/public/turn_events.js
|
|
612
621
|
- lib/samagotchi/web/public/turn_html.js
|
|
613
622
|
- lib/samagotchi/web/public/turn_model.js
|