samagotchi 0.58.0 → 0.60.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 +86 -1
- data/README.md +3 -2
- data/bin/chi +23 -2
- data/docs/architecture.md +6 -4
- data/docs/cli.md +14 -7
- data/docs/configuration.md +96 -31
- data/docs/hooks.md +12 -6
- data/docs/internals/background-tasks.md +7 -0
- data/docs/internals/engine.md +20 -12
- data/docs/internals/gemma4-contract.md +1 -0
- data/docs/internals/prompt-caching.md +4 -3
- data/docs/internals/tool-guardrails.md +2 -1
- data/docs/memory.md +1 -1
- data/docs/plugins.md +75 -6
- data/docs/sessions.md +2 -2
- data/docs/sub-agent.md +2 -2
- data/docs/testing.md +4 -2
- data/lib/samagotchi/bootstrap_command.rb +2 -1
- data/lib/samagotchi/bridge/turn_accumulator.rb +3 -0
- data/lib/samagotchi/bridge.rb +6 -2
- data/lib/samagotchi/bundles/agents-md/manifest.yml +11 -0
- data/lib/samagotchi/bundles/agents-md/plugin.rb +198 -0
- data/lib/samagotchi/bundles/core/manifest.yml +2 -1
- data/lib/samagotchi/bundles/slow-commands/manifest.yml +5 -4
- data/lib/samagotchi/bundles/slow-commands/plugin.rb +169 -16
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +1 -1
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/config.rb +44 -10
- data/lib/samagotchi/continue_offer.rb +5 -2
- data/lib/samagotchi/engine.rb +89 -15
- data/lib/samagotchi/host_config.rb +3 -1
- data/lib/samagotchi/host_registry.rb +2 -1
- data/lib/samagotchi/iteration_limit.rb +3 -2
- data/lib/samagotchi/kernel_loop.rb +35 -9
- data/lib/samagotchi/llm/backend.rb +6 -1
- data/lib/samagotchi/llm/chat_loop.rb +11 -6
- data/lib/samagotchi/llm/model_result.rb +11 -2
- data/lib/samagotchi/llm/native_backend.rb +3 -2
- data/lib/samagotchi/log_subscriber.rb +2 -1
- data/lib/samagotchi/model_settings.rb +4 -2
- data/lib/samagotchi/plugin/context.rb +16 -3
- data/lib/samagotchi/process_group.rb +76 -3
- data/lib/samagotchi/prompt_files.rb +127 -0
- data/lib/samagotchi/ref.rb +124 -0
- data/lib/samagotchi/send_command.rb +1 -1
- data/lib/samagotchi/session.rb +15 -3
- data/lib/samagotchi/session_commands.rb +15 -3
- data/lib/samagotchi/session_metrics.rb +8 -2
- data/lib/samagotchi/session_setup.rb +11 -5
- data/lib/samagotchi/steer_cut.rb +12 -8
- data/lib/samagotchi/steer_cut_after.rb +69 -0
- data/lib/samagotchi/system_prompt.rb +74 -18
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +18 -9
- data/lib/samagotchi/terminal_ui/attached_loop.rb +1 -1
- data/lib/samagotchi/terminal_ui/formatting.rb +23 -5
- data/lib/samagotchi/terminal_ui.rb +18 -6
- data/lib/samagotchi/tool_activity.rb +18 -1
- data/lib/samagotchi/tool_declarations.rb +1 -1
- data/lib/samagotchi/tools/execute.rb +318 -70
- data/lib/samagotchi/tools/task_get.rb +2 -0
- data/lib/samagotchi/tools/task_runtime.rb +96 -13
- data/lib/samagotchi/tools/task_wait.rb +23 -5
- data/lib/samagotchi/turn_flow.rb +22 -3
- data/lib/samagotchi/utf8_default.rb +1 -1
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/public/favicon.svg +1 -0
- data/lib/samagotchi/web/public/index.html +1 -0
- data/lib/samagotchi/web/public/read_view.js +3 -1
- data/lib/samagotchi/web/public/refs.js +107 -15
- data/lib/samagotchi/web/public/task_stop.js +25 -7
- data/lib/samagotchi/worker.rb +1 -0
- metadata +7 -1
|
@@ -10,6 +10,9 @@ module Samagotchi
|
|
|
10
10
|
TAIL_LINES_DEFAULT = 10
|
|
11
11
|
TAIL_LINES_MAX = 100
|
|
12
12
|
POLL_INTERVAL = 0.5
|
|
13
|
+
# A handed-over execute keeps stderr in a log of its own: at most this
|
|
14
|
+
# many of its last lines follow the output tail.
|
|
15
|
+
STDERR_TAIL_LINES_MAX = 10
|
|
13
16
|
|
|
14
17
|
def self.name = NAME
|
|
15
18
|
|
|
@@ -41,7 +44,7 @@ module Samagotchi
|
|
|
41
44
|
|
|
42
45
|
if completion_pattern
|
|
43
46
|
output_tail = output_tail(record, tail_lines)
|
|
44
|
-
return format_response(record, wait_result: "pattern_matched", output_tail: output_tail) if completion_pattern.match?(output_tail)
|
|
47
|
+
return format_response(record, wait_result: "pattern_matched", output_tail: output_tail, tail_lines: tail_lines) if completion_pattern.match?(output_tail)
|
|
45
48
|
end
|
|
46
49
|
|
|
47
50
|
return cancelled_response(task_id, tail_lines, started) if cancelled.call
|
|
@@ -55,7 +58,7 @@ module Samagotchi
|
|
|
55
58
|
refreshed, = TaskRuntime.get_record(task_id)
|
|
56
59
|
return finished_response(refreshed, tail_lines) unless refreshed.fetch("status") == "running"
|
|
57
60
|
|
|
58
|
-
format_response(refreshed, wait_result: "timeout", output_tail: output_tail(refreshed, tail_lines))
|
|
61
|
+
format_response(refreshed, wait_result: "timeout", output_tail: output_tail(refreshed, tail_lines), tail_lines: tail_lines)
|
|
59
62
|
rescue StandardError => e
|
|
60
63
|
"Error: #{e.message}"
|
|
61
64
|
end
|
|
@@ -72,7 +75,8 @@ module Samagotchi
|
|
|
72
75
|
" To keep waiting: task_wait #{task_id}. If it is stuck or no longer needed: task_stop #{task_id}.",
|
|
73
76
|
"waited: #{(monotonic_time - started).round}s"
|
|
74
77
|
]
|
|
75
|
-
format_response(record, wait_result: "canceled", output_tail: output_tail(record, tail_lines),
|
|
78
|
+
format_response(record, wait_result: "canceled", output_tail: output_tail(record, tail_lines), tail_lines: tail_lines,
|
|
79
|
+
extra: note)
|
|
76
80
|
end
|
|
77
81
|
private_class_method :cancelled_response
|
|
78
82
|
|
|
@@ -81,7 +85,8 @@ module Samagotchi
|
|
|
81
85
|
def self.finished_response(record, tail_lines)
|
|
82
86
|
return format_response(record) unless TaskRuntime.stopped_by_user?(record)
|
|
83
87
|
|
|
84
|
-
format_response(record, output_tail: output_tail(record, tail_lines),
|
|
88
|
+
format_response(record, output_tail: output_tail(record, tail_lines), tail_lines: tail_lines,
|
|
89
|
+
extra: [TaskRuntime::USER_STOP_NOTE])
|
|
85
90
|
end
|
|
86
91
|
private_class_method :finished_response
|
|
87
92
|
|
|
@@ -90,7 +95,17 @@ module Samagotchi
|
|
|
90
95
|
end
|
|
91
96
|
private_class_method :output_tail
|
|
92
97
|
|
|
93
|
-
|
|
98
|
+
# The last lines of a handed-over execute's stderr log (stderr_path),
|
|
99
|
+
# "" for a task_create task (one log) or an empty stderr.
|
|
100
|
+
def self.stderr_tail(record, tail_lines)
|
|
101
|
+
path = record["stderr_path"]
|
|
102
|
+
return "" if path.to_s.empty? || tail_lines.nil?
|
|
103
|
+
|
|
104
|
+
TaskRuntime.output_tail_lines(path, [tail_lines, STDERR_TAIL_LINES_MAX].min)
|
|
105
|
+
end
|
|
106
|
+
private_class_method :stderr_tail
|
|
107
|
+
|
|
108
|
+
def self.format_response(record, wait_result: nil, output_tail: nil, tail_lines: nil, extra: [])
|
|
94
109
|
lines = [
|
|
95
110
|
"task_id: #{record.fetch("id")}",
|
|
96
111
|
"status: #{record.fetch("status")}",
|
|
@@ -98,12 +113,15 @@ module Samagotchi
|
|
|
98
113
|
"stop_reason: #{record["stop_reason"]}",
|
|
99
114
|
"output_path: #{record.fetch("output_path")}"
|
|
100
115
|
]
|
|
116
|
+
lines << "stderr_path: #{record["stderr_path"]}" if record["stderr_path"]
|
|
101
117
|
return lines.join("\n") unless output_tail
|
|
102
118
|
|
|
103
119
|
lines << "wait_result: #{wait_result}" if wait_result
|
|
104
120
|
lines.concat(extra)
|
|
105
121
|
lines << "output_tail:"
|
|
106
122
|
lines << output_tail.rstrip unless output_tail.empty?
|
|
123
|
+
stderr_tail = stderr_tail(record, tail_lines)
|
|
124
|
+
lines.concat(["stderr_tail:", stderr_tail.rstrip]) unless stderr_tail.empty?
|
|
107
125
|
lines.join("\n")
|
|
108
126
|
end
|
|
109
127
|
private_class_method :format_response
|
data/lib/samagotchi/turn_flow.rb
CHANGED
|
@@ -42,8 +42,9 @@ module Samagotchi
|
|
|
42
42
|
[:invalid, nil]
|
|
43
43
|
end
|
|
44
44
|
|
|
45
|
-
# @return [Hash, nil] the pending continue offer: {context:, no_interrupt
|
|
46
|
-
# (context summarizes the interrupted turn for a
|
|
45
|
+
# @return [Hash, nil] the pending continue offer: {context:, no_interrupt:,
|
|
46
|
+
# run_start:, model:} (context summarizes the interrupted turn for a
|
|
47
|
+
# "no, <reason>"; run_start and model are #resume_from's)
|
|
47
48
|
attr_reader :offer
|
|
48
49
|
|
|
49
50
|
# How many times the conversation went back to a checkpoint (a failed
|
|
@@ -94,7 +95,11 @@ module Samagotchi
|
|
|
94
95
|
end
|
|
95
96
|
|
|
96
97
|
if result.resumable?
|
|
97
|
-
|
|
98
|
+
# A Continue that ran out again goes on from the run the chain
|
|
99
|
+
# started; any other turn's offer (a prompt's, a wake's) from its own.
|
|
100
|
+
run_start = (continue && resume_from) || result.run_start
|
|
101
|
+
@offer = { context: interrupted_turn_context(result), no_interrupt: no_interrupt, run_start: run_start,
|
|
102
|
+
model: @engine.effective_model_name }
|
|
98
103
|
:continue_offered
|
|
99
104
|
else
|
|
100
105
|
@offer = nil
|
|
@@ -143,6 +148,20 @@ module Samagotchi
|
|
|
143
148
|
:restored
|
|
144
149
|
end
|
|
145
150
|
|
|
151
|
+
# Where a Continue of the pending offer resumes: the run its turn (or
|
|
152
|
+
# the first turn of a chain of Continues) started (ModelResult#run_start),
|
|
153
|
+
# so the Continue keeps that run's reasoning (Engine#run_turn
|
|
154
|
+
# resume_from:). nil with no offer, and after a model change while the
|
|
155
|
+
# offer stands: another model's reasoning isn't replayed, the Continue
|
|
156
|
+
# starts cold. A cancelled or failed Continue keeps the offer, and so
|
|
157
|
+
# this; every way the offer goes takes it along.
|
|
158
|
+
# @return [Integer, nil]
|
|
159
|
+
def resume_from
|
|
160
|
+
return nil unless @offer && @offer[:model] == @engine.effective_model_name
|
|
161
|
+
|
|
162
|
+
@offer[:run_start]
|
|
163
|
+
end
|
|
164
|
+
|
|
146
165
|
# Answer the offer with no (Stop): the interrupted turn stays, as
|
|
147
166
|
# after a Ctrl-C or a new prompt (!rollback still erases it), and the
|
|
148
167
|
# model reads that it wasn't continued: a note, or the user's reason
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Samagotchi
|
|
4
|
-
# chi's files (memories,
|
|
4
|
+
# chi's files (memories, AGENTS.md, sessions, config.yml) are UTF-8,
|
|
5
5
|
# whatever the locale. Under LC_ALL=C (or with no locale at all: an app
|
|
6
6
|
# started from Finder or launchd, `env -i`) Ruby reads files as US-ASCII,
|
|
7
7
|
# and a UTF-8 memory index then breaks the system prompt
|
data/lib/samagotchi/version.rb
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#7aa5ff"/><stop offset="1" stop-color="#8957e5"/></linearGradient></defs><rect x="1" y="1" width="30" height="30" rx="7.5" fill="url(#g)"/><g fill="none" stroke="#fff" stroke-width="3.2" stroke-linecap="round" stroke-linejoin="round"><path d="M7.8 9.6c2.3-1.3 4-.7 5.2 1.4l6 10.1c1.2 2.1 2.9 2.7 5.2 1.4"/><path d="M22.6 9.2 9.4 22.8"/></g></svg>
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<title>Chi</title>
|
|
6
|
+
<link rel="icon" href="/assets/favicon.svg" type="image/svg+xml">
|
|
6
7
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
7
8
|
<style>
|
|
8
9
|
:root{color-scheme:dark;--bg:#0b0d11;--bg-panel:#151821;--bg-inset:#0f1115;--bg-raised:#22262f;--bg-hover:#1b1f27;--border:#1f232b;--border-strong:#2d333f;--text:#e6e8eb;--text-bright:#f0f3f6;--text-2:#c9d1d9;--text-3:#9aa4b2;--text-muted:#8b949e;--text-faint:#6e7681;--accent:#7aa5ff;--accent-strong:#1f6feb;--accent-bg:#1b2336;--accent-ring:rgba(122,165,255,.18);--primary:#2a68d1;--ok:#3fb950;--ok-text:#7ee787;--ok-bg:#1a2e1a;--warn:#d29922;--warn-text:#e3b341;--warn-bright:#f2cc60;--warn-bg:#2b2108;--warn-border:#6b5416;--danger:#f85149;--danger-bg:#da3633;--danger-text:#ff7b72;--danger-bg-dim:#3d1f1f;--think:#8957e5;--think-text:#d2a8ff;--accent-border:#223150;--pick-border:#79c0ff;--accent-tint:color-mix(in srgb,var(--accent) 7%,transparent);--narr:color-mix(in srgb,var(--accent) 38%,var(--text-muted));
|
|
@@ -67,8 +67,10 @@ export function readLinesHtml(row) {
|
|
|
67
67
|
const first = numbered[0].line;
|
|
68
68
|
const last = numbered.at(-1).line;
|
|
69
69
|
const label = first === last ? `line ${first}` : `lines ${first}–${last}`;
|
|
70
|
+
// A blank line of the file holds no text, only a <br>: an empty block
|
|
71
|
+
// drops out of a copied selection, a space would be copied as one.
|
|
70
72
|
const spans = lines.map(({ text, line }) => line === null
|
|
71
73
|
? `<span class="read-line read-meta">${escapeHtml(text)}</span>`
|
|
72
|
-
: `<span class="read-line" data-line="${line}">${escapeHtml(text) || "
|
|
74
|
+
: `<span class="read-line" data-line="${line}">${escapeHtml(text) || "<br>"}</span>`).join("");
|
|
73
75
|
return `<details class="activity-read"><summary>${label}</summary><pre class="read-lines"><code>${spans}</code></pre></details>`;
|
|
74
76
|
}
|
|
@@ -6,6 +6,11 @@
|
|
|
6
6
|
// an answer or a step's text (ref_scan.js; a link whose text is one too).
|
|
7
7
|
// The DOM carries data-ref-kind and the target's attributes, never what a
|
|
8
8
|
// click does: those are the ref's targets, kept per kind in REF_ACTIONS.
|
|
9
|
+
// A plugin's ref (the server's Ref: {kind, target, label?, actions}) is
|
|
10
|
+
// bound to its element by bindRef: its label and its URL actions live in a
|
|
11
|
+
// WeakMap off the DOM, and refTarget reads them first. Its actions join the
|
|
12
|
+
// kind's own (the `actions` entry); a kind the page has no table for runs
|
|
13
|
+
// only those.
|
|
9
14
|
// A plain click runs the first one available (the main target); a ref
|
|
10
15
|
// with more shows a ▾ on hover that opens a menu of all of them
|
|
11
16
|
// (ref_menu.js), and a tap opens it. A ref with a target is a tab stop
|
|
@@ -26,6 +31,55 @@ export function refAttrs(ref) {
|
|
|
26
31
|
return ` data-ref-kind="${escapeHtml(ref.kind)}"${attr("path", ref.path)}${attr("line", ref.line)}${attr("end-line", ref.end_line)}`;
|
|
27
32
|
}
|
|
28
33
|
|
|
34
|
+
// A kind, as the server's Ref allows it (Ref::KIND).
|
|
35
|
+
const KIND = /^[a-z][a-z0-9_-]{0,31}$/;
|
|
36
|
+
|
|
37
|
+
// A plugin's ref data per element (bindRef): {kind, target, label,
|
|
38
|
+
// actions}. A WeakMap, so a redrawn element takes its data with it.
|
|
39
|
+
const REF_DATA = new WeakMap();
|
|
40
|
+
|
|
41
|
+
// +url+ as the page opens it when it is an http(s) one, else null. The
|
|
42
|
+
// server checks it too (Ref, RefAction); the page never opens another
|
|
43
|
+
// scheme a plugin gave (javascript:, data:, an editor's).
|
|
44
|
+
export function httpUrl(url) {
|
|
45
|
+
if (typeof url !== "string") return null;
|
|
46
|
+
try {
|
|
47
|
+
const parsed = new URL(url.trim());
|
|
48
|
+
return parsed.protocol === "http:" || parsed.protocol === "https:" ? parsed.href : null;
|
|
49
|
+
} catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Makes +el+ the ref +ref+ (the server's Ref#to_h): the ref class and the
|
|
55
|
+
// data attributes every ref carries (a file's path and lines, as refAttrs),
|
|
56
|
+
// its label and its actions in the WeakMap. An action without an id, a
|
|
57
|
+
// label or an http(s) URL is dropped; a kind the server's Ref refuses binds
|
|
58
|
+
// nothing. A rebind replaces the target attributes. +el+, or null without one.
|
|
59
|
+
export function bindRef(el, ref) {
|
|
60
|
+
if (!el) return null;
|
|
61
|
+
if (typeof ref?.kind !== "string" || !KIND.test(ref.kind)) return el;
|
|
62
|
+
const target = ref.target || {};
|
|
63
|
+
el.classList.add("ref");
|
|
64
|
+
el.dataset.refKind = ref.kind;
|
|
65
|
+
for (const [name, value] of [["refPath", target.path], ["refLine", target.line], ["refEndLine", target.end_line]]) {
|
|
66
|
+
if (value) el.dataset[name] = String(value);
|
|
67
|
+
else delete el.dataset[name];
|
|
68
|
+
}
|
|
69
|
+
const actions = (Array.isArray(ref.actions) ? ref.actions : []).flatMap((action) => {
|
|
70
|
+
const url = httpUrl(action?.url);
|
|
71
|
+
return url && action.id && action.label ? [{ id: String(action.id), label: String(action.label), url }] : [];
|
|
72
|
+
});
|
|
73
|
+
REF_DATA.set(el, { kind: ref.kind, target, label: ref.label || null, actions });
|
|
74
|
+
return el;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// The same URL either way (a link's href against an action's URL).
|
|
78
|
+
function sameUrl(a, b) {
|
|
79
|
+
const url = httpUrl(a);
|
|
80
|
+
return !!url && url === httpUrl(b);
|
|
81
|
+
}
|
|
82
|
+
|
|
29
83
|
// The URL +template+ ({path}, {line}; web.editor) opens +path+ at +line+
|
|
30
84
|
// (1 when none) with: each path segment encoded, the slashes kept. "" with
|
|
31
85
|
// no template or path.
|
|
@@ -41,16 +95,38 @@ export function editorUrl(template, path, line) {
|
|
|
41
95
|
// editorUrl}, the server's capabilities.editor and editor_url) and
|
|
42
96
|
// +target+ (refTarget's). A secondary one (copy) joins a ref that has
|
|
43
97
|
// another, never alone. A line (a diff or read line) has its click only.
|
|
98
|
+
// A link whose text is the ref (github-pr's PR diff): its href stays the
|
|
99
|
+
// plain click's, which the browser follows. Its label is the ref's action
|
|
100
|
+
// with the same URL (the menu shows them once), else the old guess from
|
|
101
|
+
// the URL (an old session's link has no ref data).
|
|
102
|
+
const LINK = Object.freeze({
|
|
103
|
+
id: "link",
|
|
104
|
+
label: (target) => target.actions?.find((action) => sameUrl(action.url, target.href))?.label ||
|
|
105
|
+
(/\/pull\/\d+\/files\b/.test(target.href) ? "PR diff" : "Open link"),
|
|
106
|
+
available: (_viewer, target) => !!target.href,
|
|
107
|
+
run: (target, _viewer, env) => env.openLink(target.href),
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// The ref's own actions (a plugin's, bindRef), one target each, for every
|
|
111
|
+
// viewer (a URL opens on a phone too): a new tab with no opener and no
|
|
112
|
+
// referrer. One with the link's URL, or an earlier one's, isn't repeated.
|
|
113
|
+
// Not on a diff or read line (its click only), which shares its title's data.
|
|
114
|
+
const URL_ACTIONS = Object.freeze({
|
|
115
|
+
id: "actions",
|
|
116
|
+
expand: (target) => (target.actions || [])
|
|
117
|
+
.filter((action, at, all) => !sameUrl(action.url, target.href) && all.findIndex((other) => sameUrl(other.url, action.url)) === at)
|
|
118
|
+
.map((action) => Object.freeze({
|
|
119
|
+
id: `url:${action.id}`,
|
|
120
|
+
label: () => action.label,
|
|
121
|
+
available: (_viewer, target) => !target.isLine,
|
|
122
|
+
run: (_target, _viewer, env) => env.openLink(action.url),
|
|
123
|
+
hint: () => action.url,
|
|
124
|
+
})),
|
|
125
|
+
});
|
|
126
|
+
|
|
44
127
|
export const REF_ACTIONS = Object.freeze({
|
|
45
128
|
file: Object.freeze([
|
|
46
|
-
|
|
47
|
-
// the plain click's, which the browser follows.
|
|
48
|
-
Object.freeze({
|
|
49
|
-
id: "link",
|
|
50
|
-
label: (target) => (/\/pull\/\d+\/files\b/.test(target.href) ? "PR diff" : "Open link"),
|
|
51
|
-
available: (_viewer, target) => !!target.href,
|
|
52
|
-
run: (target, _viewer, env) => env.openLink(target.href),
|
|
53
|
-
}),
|
|
129
|
+
LINK,
|
|
54
130
|
Object.freeze({
|
|
55
131
|
id: "editor",
|
|
56
132
|
label: () => "Open in editor",
|
|
@@ -63,6 +139,7 @@ export const REF_ACTIONS = Object.freeze({
|
|
|
63
139
|
// The hover hint: it opens the file as it is now.
|
|
64
140
|
hint: (target) => `Open the current file in your editor${target.line ? ` at line ${target.line}` : ""}`,
|
|
65
141
|
}),
|
|
142
|
+
URL_ACTIONS,
|
|
66
143
|
// The row that touched the file, in the turn: not for a row's own
|
|
67
144
|
// title or lines (they are in the turn already).
|
|
68
145
|
Object.freeze({
|
|
@@ -80,13 +157,18 @@ export const REF_ACTIONS = Object.freeze({
|
|
|
80
157
|
run: (target, _viewer, env) => env.copy(target.path),
|
|
81
158
|
}),
|
|
82
159
|
]),
|
|
160
|
+
// Any other kind (a plugin's issue, commit, …): its link, then its actions.
|
|
161
|
+
"*": Object.freeze([LINK, URL_ACTIONS]),
|
|
83
162
|
});
|
|
84
163
|
|
|
85
164
|
// The targets +viewer+ has for +target+ (refTarget's), in order; none
|
|
86
165
|
// when only a secondary one is.
|
|
87
166
|
export function refActions(target, viewer, actions = REF_ACTIONS) {
|
|
88
167
|
if (!target) return [];
|
|
89
|
-
|
|
168
|
+
// Own keys only: a kind named like an Object property ("constructor") is
|
|
169
|
+
// any other kind.
|
|
170
|
+
const table = (Object.hasOwn(actions, target.kind) ? actions[target.kind] : actions["*"]) || [];
|
|
171
|
+
const found = table.flatMap((action) => (action.expand ? action.expand(target) : [action])).filter((action) => action.available(viewer, target));
|
|
90
172
|
return found.some((action) => !action.secondary) ? found : [];
|
|
91
173
|
}
|
|
92
174
|
|
|
@@ -97,8 +179,9 @@ const OWNERS = ".activity-row, .bubble.approval";
|
|
|
97
179
|
// A line of the owner's file: a diff line, or a read row's numbered line.
|
|
98
180
|
const LINES = ".diff-line[data-line], .read-line[data-line]";
|
|
99
181
|
|
|
100
|
-
// What a click on +el+ points at: {kind, path,
|
|
101
|
-
// href} or null
|
|
182
|
+
// What a click on +el+ points at: {kind, path, id, label, actions, line,
|
|
183
|
+
// owner, el, isLine, href} or null (+id+, +label+ and +actions+ a plugin's
|
|
184
|
+
// ref's, bindRef: the WeakMap is read first). A row title (or an approval card's path) opens its file
|
|
102
185
|
// at the lines the call named (an edit without any at its first changed
|
|
103
186
|
// line); a diff line (or a hunk header) or a read row's numbered line, in
|
|
104
187
|
// a row or a card with a ref, opens that file at that line. +el+ is the
|
|
@@ -109,7 +192,8 @@ export function refTarget(el) {
|
|
|
109
192
|
if (title) {
|
|
110
193
|
const owner = title.closest(OWNERS);
|
|
111
194
|
const href = title.matches("a[href]") ? title.getAttribute("href") : null;
|
|
112
|
-
|
|
195
|
+
const own = targetOf(title);
|
|
196
|
+
return { ...own, line: own.line || firstChange(owner), owner: owner || title, el: title, isLine: false, href };
|
|
113
197
|
}
|
|
114
198
|
const diffLine = el?.closest?.(LINES);
|
|
115
199
|
const owner = diffLine?.closest(OWNERS);
|
|
@@ -119,7 +203,13 @@ export function refTarget(el) {
|
|
|
119
203
|
}
|
|
120
204
|
|
|
121
205
|
function targetOf(el) {
|
|
122
|
-
|
|
206
|
+
const data = REF_DATA.get(el);
|
|
207
|
+
if (!data) return { kind: el.dataset.refKind, path: el.dataset.refPath || "", line: Number(el.dataset.refLine) || null, id: null, label: null, actions: [] };
|
|
208
|
+
// Bound data is the whole ref: no fallback to the attributes.
|
|
209
|
+
const { target } = data;
|
|
210
|
+
return {
|
|
211
|
+
kind: data.kind, path: target.path || "", line: Number(target.line) || null, id: target.id ?? null, label: data.label, actions: data.actions,
|
|
212
|
+
};
|
|
123
213
|
}
|
|
124
214
|
|
|
125
215
|
// An edit/write without a line range opens at its first changed line (its
|
|
@@ -235,9 +325,11 @@ function defaultOpen(url) {
|
|
|
235
325
|
else window.location.href = url;
|
|
236
326
|
}
|
|
237
327
|
|
|
238
|
-
// A link ref's href
|
|
328
|
+
// A link ref's href or a ref's URL action: a new tab, as the answer's
|
|
329
|
+
// links open, with no opener and no referrer (the page's LAN URL and
|
|
330
|
+
// session id stay here).
|
|
239
331
|
function defaultOpenLink(href) {
|
|
240
|
-
window.open(href, "_blank", "noopener");
|
|
332
|
+
window.open(href, "_blank", "noopener,noreferrer");
|
|
241
333
|
}
|
|
242
334
|
|
|
243
335
|
// One delegated listener set on +root+ for every ref on the page.
|
|
@@ -22,10 +22,30 @@ export function stopTarget(row, features) {
|
|
|
22
22
|
// A task_wait row's title without its wait ("npm test · up to 600s").
|
|
23
23
|
const WAIT_SUFFIX = / · up to \d+s$/;
|
|
24
24
|
|
|
25
|
-
// The
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
25
|
+
// The first line of an execute that was still running at its limit and
|
|
26
|
+
// went on as a task (Tools::Execute::HANDED_OVER), then its task_id line.
|
|
27
|
+
// Anchored at the start, an optional "ran as: …" line and "[execute]" lead
|
|
28
|
+
// before it: a "task_id:" line anywhere else is the command's own output.
|
|
29
|
+
// The twin of TaskRuntime.handed_over_id.
|
|
30
|
+
const HANDED_OVER = /^(?:ran as: [^\n]*\n)?(?:\[execute\]\n)?Still running at the \d+s limit:[^\n]*\ntask_id: (\S+)/;
|
|
31
|
+
|
|
32
|
+
// The task id an execute row's output hands over, else null.
|
|
33
|
+
export function handedOverId(output) {
|
|
34
|
+
return HANDED_OVER.exec(output || "")?.[1] || null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// The task id a row of this turn started: a task_create's, or an execute's
|
|
38
|
+
// hand-over (handedOverId); else null.
|
|
39
|
+
function startedId(row) {
|
|
40
|
+
if (row.tool === "execute") return handedOverId(row.output);
|
|
41
|
+
if (row.tool !== "task_create") return null;
|
|
42
|
+
return /(?:^|\n)task_id: (\S+)/.exec(row.output || "")?.[1] || null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// The command a task_create or a handed-over execute of this turn started
|
|
46
|
+
// +taskId+ with (its row's view), else a task_wait row's title for it (the
|
|
47
|
+
// server titles one by its task's command, also for a task from an earlier
|
|
48
|
+
// turn: ToolActivity.task_title), or null.
|
|
29
49
|
export function taskCommand(gens, taskId) {
|
|
30
50
|
let titled = null;
|
|
31
51
|
for (const gen of gens || []) {
|
|
@@ -33,9 +53,7 @@ export function taskCommand(gens, taskId) {
|
|
|
33
53
|
if (row.tool === "task_wait" && row.view?.task_id === taskId && row.title) {
|
|
34
54
|
titled ||= row.title.replace(WAIT_SUFFIX, "");
|
|
35
55
|
}
|
|
36
|
-
if (row
|
|
37
|
-
const id = /(?:^|\n)task_id: (\S+)/.exec(row.output || "")?.[1];
|
|
38
|
-
if (id === taskId) return row.view?.command || null;
|
|
56
|
+
if (startedId(row) === taskId) return row.view?.command || null;
|
|
39
57
|
}
|
|
40
58
|
}
|
|
41
59
|
return titled;
|
data/lib/samagotchi/worker.rb
CHANGED
|
@@ -367,6 +367,7 @@ module Samagotchi
|
|
|
367
367
|
# (re)spawn.
|
|
368
368
|
memories: @session.preloaded_memory_names,
|
|
369
369
|
muted_memories: @session.muted_memory_names,
|
|
370
|
+
appended_prompt_files: @session.appended_prompt_files,
|
|
370
371
|
reminders: {
|
|
371
372
|
callback: lambda { |due_names|
|
|
372
373
|
# A reminder is due: the loop runs a reminder turn for it once
|
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.60.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dmitry Dedov
|
|
@@ -178,6 +178,8 @@ files:
|
|
|
178
178
|
- lib/samagotchi/broadcast_command.rb
|
|
179
179
|
- lib/samagotchi/bundle_command.rb
|
|
180
180
|
- lib/samagotchi/bundle_needs.rb
|
|
181
|
+
- lib/samagotchi/bundles/agents-md/manifest.yml
|
|
182
|
+
- lib/samagotchi/bundles/agents-md/plugin.rb
|
|
181
183
|
- lib/samagotchi/bundles/btw/manifest.yml
|
|
182
184
|
- lib/samagotchi/bundles/btw/plugin.rb
|
|
183
185
|
- lib/samagotchi/bundles/check-in/manifest.yml
|
|
@@ -392,6 +394,7 @@ files:
|
|
|
392
394
|
- lib/samagotchi/process_group.rb
|
|
393
395
|
- lib/samagotchi/project_scope.rb
|
|
394
396
|
- lib/samagotchi/prompt.rb
|
|
397
|
+
- lib/samagotchi/prompt_files.rb
|
|
395
398
|
- lib/samagotchi/prompt_history.rb
|
|
396
399
|
- lib/samagotchi/prompt_literal_guard.rb
|
|
397
400
|
- lib/samagotchi/prompt_note.rb
|
|
@@ -403,6 +406,7 @@ files:
|
|
|
403
406
|
- lib/samagotchi/recap_setup.rb
|
|
404
407
|
- lib/samagotchi/recap_store.rb
|
|
405
408
|
- lib/samagotchi/redacted_inspect.rb
|
|
409
|
+
- lib/samagotchi/ref.rb
|
|
406
410
|
- lib/samagotchi/relay.rb
|
|
407
411
|
- lib/samagotchi/relay_desk.rb
|
|
408
412
|
- lib/samagotchi/relay_verifier.rb
|
|
@@ -430,6 +434,7 @@ files:
|
|
|
430
434
|
- lib/samagotchi/setting_explained.rb
|
|
431
435
|
- lib/samagotchi/steer.rb
|
|
432
436
|
- lib/samagotchi/steer_cut.rb
|
|
437
|
+
- lib/samagotchi/steer_cut_after.rb
|
|
433
438
|
- lib/samagotchi/system_prompt.rb
|
|
434
439
|
- lib/samagotchi/terminal_ui.rb
|
|
435
440
|
- lib/samagotchi/terminal_ui/attach_launcher.rb
|
|
@@ -541,6 +546,7 @@ files:
|
|
|
541
546
|
- lib/samagotchi/web/public/data.js
|
|
542
547
|
- lib/samagotchi/web/public/diff_view.js
|
|
543
548
|
- lib/samagotchi/web/public/families_view.js
|
|
549
|
+
- lib/samagotchi/web/public/favicon.svg
|
|
544
550
|
- lib/samagotchi/web/public/format.js
|
|
545
551
|
- lib/samagotchi/web/public/hold.js
|
|
546
552
|
- lib/samagotchi/web/public/images.js
|