samagotchi 0.28.0 → 0.30.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 +84 -1
- data/docs/cli.md +4 -4
- data/docs/hooks.md +1 -1
- data/docs/plugins.md +1 -1
- data/docs/sessions.md +3 -3
- data/lib/samagotchi/bundle_command.rb +4 -2
- data/lib/samagotchi/bundles/mcp/manifest.yml +2 -2
- data/lib/samagotchi/bundles/mcp/plugin.rb +4 -3
- data/lib/samagotchi/bundles/source-links/hooks/source_links.rb +17 -4
- data/lib/samagotchi/bundles/source-links/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/command_steps.rb +35 -15
- data/lib/samagotchi/engine.rb +4 -3
- data/lib/samagotchi/guardrails/shell_lex.rb +79 -5
- data/lib/samagotchi/host_registry.rb +4 -0
- data/lib/samagotchi/kernel_loop.rb +27 -7
- data/lib/samagotchi/llm/chat_loop.rb +12 -6
- data/lib/samagotchi/memory_bundle/installer.rb +14 -0
- data/lib/samagotchi/model_list_store.rb +117 -0
- data/lib/samagotchi/model_profile.rb +43 -0
- data/lib/samagotchi/send_command.rb +5 -3
- data/lib/samagotchi/session.rb +5 -0
- data/lib/samagotchi/session_commands.rb +6 -2
- data/lib/samagotchi/session_manager.rb +9 -2
- data/lib/samagotchi/session_metrics.rb +6 -3
- data/lib/samagotchi/terminal_ui/formatting.rb +7 -4
- data/lib/samagotchi/terminal_ui.rb +35 -5
- data/lib/samagotchi/tool_activity.rb +38 -8
- data/lib/samagotchi/tools/builtins.rb +10 -7
- data/lib/samagotchi/tools/delegate.rb +14 -10
- data/lib/samagotchi/tools/execute.rb +3 -1
- data/lib/samagotchi/tools/task_runtime.rb +21 -0
- data/lib/samagotchi/turn_note.rb +10 -0
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/public/activity.js +2 -1
- data/lib/samagotchi/web/public/app.js +23 -11
- data/lib/samagotchi/web/public/command_view.js +19 -5
- data/lib/samagotchi/web/public/index.html +51 -25
- data/lib/samagotchi/web/public/sessions_list.js +21 -4
- data/lib/samagotchi/web/public/stage_model.js +23 -2
- data/lib/samagotchi/web/public/stage_view.js +16 -11
- data/lib/samagotchi/web/public/task_stop.js +11 -2
- data/lib/samagotchi/web/public/timing.js +12 -7
- data/lib/samagotchi/web/public/turn_events.js +3 -1
- data/lib/samagotchi/web/public/turn_model.js +29 -15
- data/lib/samagotchi/web/public/turn_view.js +70 -11
- data/lib/samagotchi/worker.rb +24 -7
- metadata +2 -1
|
@@ -117,13 +117,16 @@ module Samagotchi
|
|
|
117
117
|
peers = kctx.peers
|
|
118
118
|
id = peers.respond_to?(:session_id) ? peers.session_id.to_s : ""
|
|
119
119
|
model = peers.respond_to?(:model_ref) ? peers.model_ref.to_s : ""
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
120
|
+
{ Guardrails::ParentApprovals::PARENT_SESSION_ENV => id.empty? ? "chi" : id,
|
|
121
|
+
SESSION_MODEL_ENV => model.empty? ? nil : model }.merge(cli_model_unset(env))
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# The unsets that keep a `chi --model X` worker's default from passing
|
|
125
|
+
# for the default of a chi run here (nothing when it wasn't the CLI's).
|
|
126
|
+
def cli_model_unset(env = ENV)
|
|
127
|
+
return {} unless env[ModelProfile::MODEL_FROM_CLI_ENV]
|
|
128
|
+
|
|
129
|
+
{ ModelProfile::MODEL_ENV => nil, ModelProfile::MODEL_FROM_CLI_ENV => nil }
|
|
127
130
|
end
|
|
128
131
|
|
|
129
132
|
SESSION_MODEL_ENV = "SAMAGOTCHI_SESSION_MODEL"
|
|
@@ -51,16 +51,18 @@ module Samagotchi
|
|
|
51
51
|
wait = parse_wait(wait)
|
|
52
52
|
timeout = parse_timeout(timeout)
|
|
53
53
|
|
|
54
|
-
child_id = if session.to_s.strip.empty?
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
54
|
+
child_id, warning = if session.to_s.strip.empty?
|
|
55
|
+
start_child(task, parent: parent, model: model, state_dir: sd)
|
|
56
|
+
else
|
|
57
|
+
follow_up(task, session: session.to_s.strip, parent: parent, state_dir: sd)
|
|
58
|
+
end
|
|
59
59
|
return child_id if child_id.start_with?("Error:")
|
|
60
|
-
|
|
60
|
+
|
|
61
|
+
note = warning ? "Warning: #{warning}\n" : ""
|
|
62
|
+
return note + DelegateWait.call(child_id, peers: peers, timeout: timeout) if wait
|
|
61
63
|
|
|
62
64
|
started = session.to_s.strip.empty? ? "Started a delegate session" : "Sent the follow-up to delegate #{child_id[0, 8]}"
|
|
63
|
-
"session: #{child_id}\nstatus: running\n#{started}; delegate_result waits for its reply. " \
|
|
65
|
+
"#{note}session: #{child_id}\nstatus: running\n#{started}; delegate_result waits for its reply. " \
|
|
64
66
|
"It shows in chi sessions list and the web as a child of this session; the user can attach to it."
|
|
65
67
|
rescue ArgumentError => e
|
|
66
68
|
"Error: #{e.message}"
|
|
@@ -84,7 +86,7 @@ module Samagotchi
|
|
|
84
86
|
model_name: child_model(model, parent), memories: CHILD_MEMORIES,
|
|
85
87
|
parent_id: parent.id, state_dir: state_dir)
|
|
86
88
|
DelegateWait.mark_started(parent.id, child)
|
|
87
|
-
child.id
|
|
89
|
+
[child.id, child.model_warning]
|
|
88
90
|
end
|
|
89
91
|
private_class_method :start_child
|
|
90
92
|
|
|
@@ -118,8 +120,10 @@ module Samagotchi
|
|
|
118
120
|
private_class_method :follow_up
|
|
119
121
|
|
|
120
122
|
# The model as typed (spawn_session stores its resolved ref), else the
|
|
121
|
-
# parent's. No existence check:
|
|
122
|
-
#
|
|
123
|
+
# parent's. No existence check here: spawn_session checks the host
|
|
124
|
+
# (check_host!, its UnknownHost comes back as this tool's Error: line)
|
|
125
|
+
# and warns about an id the host's saved model list doesn't have
|
|
126
|
+
# (ModelProfile.model_warning, added to this tool's result).
|
|
123
127
|
def self.child_model(model, parent)
|
|
124
128
|
return parent.model_name if model.to_s.strip.empty?
|
|
125
129
|
|
|
@@ -21,6 +21,8 @@ module Samagotchi
|
|
|
21
21
|
STOP_POLL_INTERVAL_SEC = 0.05
|
|
22
22
|
WAIT_SLICE_SEC = 0.2
|
|
23
23
|
NOT_RUN_ON_STOP = "Error: not run, the user stopped the turn"
|
|
24
|
+
# The first words of a command the user's Stop killed.
|
|
25
|
+
STOPPED_BY_USER = "Error: command stopped by the user"
|
|
24
26
|
TIMEOUT_HINT = "(killed at the limit; for a long command use task_create, then task_wait)"
|
|
25
27
|
# How long the output may stay open once the shell has exited: a
|
|
26
28
|
# `server &` child keeps the pipes, and the read would never end.
|
|
@@ -67,7 +69,7 @@ module Samagotchi
|
|
|
67
69
|
# a long command, and return whatever it printed before it was killed.
|
|
68
70
|
stopped_result("Error: command timed out after #{timeout_sec}s\n#{TIMEOUT_HINT}", e)
|
|
69
71
|
rescue CommandCancelled => e
|
|
70
|
-
stopped_result("
|
|
72
|
+
stopped_result("#{STOPPED_BY_USER} after #{e.elapsed.round}s (killed; rerun it if still needed)", e)
|
|
71
73
|
rescue StandardError => e
|
|
72
74
|
"Error: #{e.message}"
|
|
73
75
|
end
|
|
@@ -129,6 +129,27 @@ module Samagotchi
|
|
|
129
129
|
end.uniq
|
|
130
130
|
end
|
|
131
131
|
|
|
132
|
+
# A task id as task_create makes them: no path parts.
|
|
133
|
+
TASK_ID = /\A[A-Za-z0-9_-]+\z/
|
|
134
|
+
|
|
135
|
+
# The command of the task +task_id+ under +root+ (a session's working
|
|
136
|
+
# directory) as its record has it, for a UI row's title; nil for no
|
|
137
|
+
# such task or an unreadable record. Reads the record only (no
|
|
138
|
+
# refresh), so it is cheap and never fails.
|
|
139
|
+
# @return [String, nil]
|
|
140
|
+
def command_of(task_id, root: Dir.pwd)
|
|
141
|
+
id = task_id.to_s.strip
|
|
142
|
+
return nil unless id.match?(TASK_ID)
|
|
143
|
+
|
|
144
|
+
path = File.join(root || Dir.pwd, TASKS_DIR, id, METADATA_FILENAME)
|
|
145
|
+
return nil unless File.file?(path)
|
|
146
|
+
|
|
147
|
+
command = JSON.parse(File.read(path))["command"]
|
|
148
|
+
command.is_a?(String) && !command.strip.empty? ? command : nil
|
|
149
|
+
rescue StandardError
|
|
150
|
+
nil
|
|
151
|
+
end
|
|
152
|
+
|
|
132
153
|
def stopped_by_user?(record) = record["stop_reason"] == "stopped_by_user"
|
|
133
154
|
|
|
134
155
|
def get_record(task_id)
|
data/lib/samagotchi/turn_note.rb
CHANGED
|
@@ -188,6 +188,16 @@ module Samagotchi
|
|
|
188
188
|
list
|
|
189
189
|
end
|
|
190
190
|
|
|
191
|
+
# A copy of +messages+ without the note at its tail when that note is a
|
|
192
|
+
# retry nudge (anything else stays: an earlier turn's note is what says
|
|
193
|
+
# why that turn ended).
|
|
194
|
+
def without_trailing_nudge(messages)
|
|
195
|
+
list = Array(messages).dup
|
|
196
|
+
index = trailing_index(list)
|
|
197
|
+
list.delete_at(index) if index && retry_nudge?(list[index])
|
|
198
|
+
list
|
|
199
|
+
end
|
|
200
|
+
|
|
191
201
|
# The index of the note at the tail of +list+ (behind context notes at
|
|
192
202
|
# most), or nil.
|
|
193
203
|
def trailing_index(list)
|
data/lib/samagotchi/version.rb
CHANGED
|
@@ -10,7 +10,8 @@ export const ROW_STATUS = Object.freeze({
|
|
|
10
10
|
RUNNING: "running",
|
|
11
11
|
OK: "ok",
|
|
12
12
|
ERROR: "error",
|
|
13
|
-
// A
|
|
13
|
+
// A call the user's Stop cut: a task_wait (the task itself runs on), a
|
|
14
|
+
// killed or never-started command. Not a failure.
|
|
14
15
|
STOPPED: "stopped",
|
|
15
16
|
// A call a guardrail denied (a hook, the guardrails bundle, a parent's
|
|
16
17
|
// or the user's Deny): it never ran, and it isn't a failed call either.
|
|
@@ -39,11 +39,11 @@ import { ALL_SESSIONS_HASH, sessionHash, sessionIdFromHash } from "./route.js";
|
|
|
39
39
|
import { allScopeHref, cardFolder, projectScopeHref, scopeDir } from "./scope.js";
|
|
40
40
|
import { shouldFollowScroll, keepFollowing } from "./scroll.js";
|
|
41
41
|
import { SHORT_WINDOW_QUERY, stripAutoHidden, stripColumns, stripShowParts } from "./strip.js";
|
|
42
|
-
import { applySessionEvent, heldOrder, listedSessions, sortedByUpdated, waitingBadge, waitingFirst, waitingSearchText, withChildrenAfterParents } from "./sessions_list.js";
|
|
42
|
+
import { applySessionEvent, heldOrder, listedSessions, sortedByUpdated, stoppedBadge, waitingBadge, waitingFirst, waitingSearchText, withChildrenAfterParents } from "./sessions_list.js";
|
|
43
43
|
import { createNotifications } from "./notifications.js";
|
|
44
44
|
import { restartConfirmText, versionNotice, workerBadge } from "./update_notice.js";
|
|
45
45
|
import {
|
|
46
|
-
appendAboveLiveTiming, cancelLineText, elapsedSince, formatDuration, mergeTiming, normalizeTiming, turnTimingText,
|
|
46
|
+
appendAboveLiveTiming, cancelLineText, elapsedSince, formatDuration, mergeTiming, normalizeTiming, STOPPED_LINE_CLASS, turnTimingText,
|
|
47
47
|
} from "./timing.js";
|
|
48
48
|
import { DONE_MS, applyInitEvent, dropDone, initRowHtml, newInitTasks, seedInitTasks } from "./init_row.js";
|
|
49
49
|
import { clientLabel, commandView, displayWait, dropEarlyRestores, emptyAnswerLine, emptyRetryLine, hookNoticeLabel, initWaitLine, isOwn, sessionCommandLine, keepEarlyRestore, newClientId, noticeLine, promptOps, reminderText, restoreAction, composerAfterAck, restoreOnAck, replaysDrawnTurn, retryStatusLine, snapshotEvents, steerCutLine, startPageReplyOrHint, turnOutput, unknownCommandHint, webLocalReply, workerGoneText } from "./turn_events.js";
|
|
@@ -328,8 +328,12 @@ function cardHtml(s, { deletable = false } = {}) {
|
|
|
328
328
|
const ctxHtml = ctx ? `<span class="ctx" title="${escapeHtml(tokensTipText(s.tokens))}">${escapeHtml(ctx)}</span>` : "";
|
|
329
329
|
const waiting = waitingBadge(s);
|
|
330
330
|
const waitingHtml = waiting ? `<span class="attn attn-${waiting.kind}" title="${escapeHtml(waiting.title)}">${escapeHtml(waiting.text)}</span>` : "";
|
|
331
|
+
// A turn a hook stopped wears its own badge, unless the session waits on
|
|
332
|
+
// the user (the waiting badge wins: the stop is behind the need).
|
|
333
|
+
const stopped = waiting ? null : stoppedBadge(s);
|
|
334
|
+
const stoppedHtml = stopped ? `<span class="stopped-badge" title="${escapeHtml(stopped.title)}">${escapeHtml(stopped.text)}</span>` : "";
|
|
331
335
|
const dotWord = waiting ? waiting.title : s.status;
|
|
332
|
-
return `<div class="card${s.archived ? " archived" : ""}${waiting ? " waiting" : ""}" data-id="${s.id}" title="${escapeHtml(tip)}${memTitle ? ` · ${escapeHtml(memTitle)}` : ""}"><div class="preview">${preview}</div>${s.recap ? `<div class="recap" title="${escapeHtml(s.recap)}">${escapeHtml(s.recap)}</div>` : ""}<div class="meta"><span class="status dot ${escapeHtml(s.status)}${waiting ? " waiting" : ""}" title="${escapeHtml(dotWord)}" aria-label="${escapeHtml(dotWord)}"></span>${waitingHtml}${timeHtml}${ctxHtml}${folderHtml}${parentChipHtml(s)}${ownerBadgeHtml(s)}${archivedHtml}${s.used_memory_names && s.used_memory_names.length ? `<span title="${escapeHtml(s.used_memory_names.join(", "))}">mem ${s.used_memory_names.length}</span>` : ""}</div>${deletable ? `<button type="button" class="card-del" data-del="${escapeHtml(s.id)}" title="Delete session" aria-label="Delete session ${escapeHtml(s.short_id)}">\u2715</button>` : ""}</div>`;
|
|
336
|
+
return `<div class="card${s.archived ? " archived" : ""}${waiting ? " waiting" : ""}" data-id="${s.id}" title="${escapeHtml(tip)}${memTitle ? ` · ${escapeHtml(memTitle)}` : ""}"><div class="preview">${preview}</div>${s.recap ? `<div class="recap" title="${escapeHtml(s.recap)}">${escapeHtml(s.recap)}</div>` : ""}<div class="meta"><span class="status dot ${escapeHtml(s.status)}${waiting ? " waiting" : ""}" title="${escapeHtml(dotWord)}" aria-label="${escapeHtml(dotWord)}"></span>${waitingHtml}${stoppedHtml}${timeHtml}${ctxHtml}${folderHtml}${parentChipHtml(s)}${ownerBadgeHtml(s)}${archivedHtml}${s.used_memory_names && s.used_memory_names.length ? `<span title="${escapeHtml(s.used_memory_names.join(", "))}">mem ${s.used_memory_names.length}</span>` : ""}</div>${deletable ? `<button type="button" class="card-del" data-del="${escapeHtml(s.id)}" title="Delete session" aria-label="Delete session ${escapeHtml(s.short_id)}">\u2715</button>` : ""}</div>`;
|
|
333
337
|
}
|
|
334
338
|
|
|
335
339
|
// A delegated session's `↳ <parent>` chip; the parent's preview is the tooltip.
|
|
@@ -956,9 +960,11 @@ function appendChunk(text) {
|
|
|
956
960
|
if (follow) historyEl.scrollTop = historyEl.scrollHeight;
|
|
957
961
|
}
|
|
958
962
|
|
|
959
|
-
|
|
963
|
+
// +stopped+: a stop someone chose (a cancel, the session's stop), drawn in
|
|
964
|
+
// the stopped style (timing.js STOPPED_LINE_CLASS); else a failure's line.
|
|
965
|
+
function addEndBubble(text, { stopped = false } = {}) {
|
|
960
966
|
const div = document.createElement("div");
|
|
961
|
-
div.className = "bubble cancel";
|
|
967
|
+
div.className = stopped ? STOPPED_LINE_CLASS : "bubble cancel";
|
|
962
968
|
div.textContent = text;
|
|
963
969
|
appendToHistory(div);
|
|
964
970
|
if (!turnDom.holds?.()) historyEl.scrollTop = historyEl.scrollHeight;
|
|
@@ -976,7 +982,7 @@ function emptyAnswerEl(emptyAnswer) {
|
|
|
976
982
|
// Everything a turn's end (completed, canceled, failed) closes: then its
|
|
977
983
|
// end line (+endText+, the cancel or failed line), then the step cards that
|
|
978
984
|
// left the collapsed block (card.js leavesBlock), where a reload puts them.
|
|
979
|
-
function endTurnView(kind, endText = null, { emptyAnswer = null } = {}) {
|
|
985
|
+
function endTurnView(kind, endText = null, { emptyAnswer = null, stopped = false } = {}) {
|
|
980
986
|
setTurnRunning(false);
|
|
981
987
|
// Every ended turn has a record (completed, canceled, failed); a worker
|
|
982
988
|
// gone mid-turn is re-rendered (resync), which counts again.
|
|
@@ -987,7 +993,7 @@ function endTurnView(kind, endText = null, { emptyAnswer = null } = {}) {
|
|
|
987
993
|
const kept = turnDom.turnEnded({ kind, endNote, emptyAnswer: !!emptyAnswer }) || [];
|
|
988
994
|
if (endNote && !endNote.isConnected) appendToHistory(endNote);
|
|
989
995
|
finishLiveTurnTiming(null, { canceled: kind === "canceled" });
|
|
990
|
-
if (endText) addEndBubble(endText);
|
|
996
|
+
if (endText) addEndBubble(endText, { stopped });
|
|
991
997
|
if (kept.length) {
|
|
992
998
|
kept.forEach((el) => appendToHistory(el));
|
|
993
999
|
if (!turnDom.holds?.()) historyEl.scrollTop = historyEl.scrollHeight;
|
|
@@ -1483,6 +1489,10 @@ function clearTimingInterval() {
|
|
|
1483
1489
|
}
|
|
1484
1490
|
|
|
1485
1491
|
let liveTurnNumber = null;
|
|
1492
|
+
// The live line sits in the stage's status row, which already says the turn
|
|
1493
|
+
// runs ("running a tool"): it drops its own "running" there. It never moves
|
|
1494
|
+
// mid-turn (finishLiveTurnTiming rewrites it before any hand-off).
|
|
1495
|
+
let liveTurnTimingInStage = false;
|
|
1486
1496
|
|
|
1487
1497
|
function startLiveTurnTiming(startedAt = new Date().toISOString(), turnId = null) {
|
|
1488
1498
|
clearTimingInterval();
|
|
@@ -1495,7 +1505,9 @@ function startLiveTurnTiming(startedAt = new Date().toISOString(), turnId = null
|
|
|
1495
1505
|
liveTurnTimingEl.className = "turn-timing live";
|
|
1496
1506
|
// The stage's status row while it holds the turn: the line moves into the
|
|
1497
1507
|
// history with it.
|
|
1498
|
-
|
|
1508
|
+
const stageHost = turnDom.timingHost?.() || null;
|
|
1509
|
+
liveTurnTimingInStage = !!stageHost;
|
|
1510
|
+
(stageHost || historyEl).appendChild(liveTurnTimingEl);
|
|
1499
1511
|
updateLiveTiming();
|
|
1500
1512
|
timingInterval = setInterval(updateLiveTiming, 1000);
|
|
1501
1513
|
}
|
|
@@ -1509,7 +1521,7 @@ function appendToHistory(el) {
|
|
|
1509
1521
|
function updateLiveTiming() {
|
|
1510
1522
|
const duration = elapsedSince(current.timing.activeTurn?.started_at);
|
|
1511
1523
|
if (liveTurnTimingEl && duration !== null) {
|
|
1512
|
-
liveTurnTimingEl.textContent = turnTimingText(liveTurnNumber, duration, { running:
|
|
1524
|
+
liveTurnTimingEl.textContent = turnTimingText(liveTurnNumber, duration, { running: !liveTurnTimingInStage }) + (liveTurnNote ? ` · ${liveTurnNote}` : "");
|
|
1513
1525
|
}
|
|
1514
1526
|
updateInfoBar();
|
|
1515
1527
|
}
|
|
@@ -1643,7 +1655,7 @@ function workerGone(id, { opened = true } = {}) {
|
|
|
1643
1655
|
const stopped = stoppingId === id;
|
|
1644
1656
|
const endText = workerGoneText({ turnRunning, stopped });
|
|
1645
1657
|
if (endText) {
|
|
1646
|
-
endTurnView("gone", endText);
|
|
1658
|
+
endTurnView("gone", endText, { stopped });
|
|
1647
1659
|
if (!stopped) setLiveStatus("idle");
|
|
1648
1660
|
}
|
|
1649
1661
|
if (current.owner === "worker") current.owner = null;
|
|
@@ -1760,7 +1772,7 @@ const streamHandlers = {
|
|
|
1760
1772
|
}
|
|
1761
1773
|
},
|
|
1762
1774
|
turn_canceled: (data) => {
|
|
1763
|
-
endTurnView("canceled", cancelLineText(data.cancellation_reason || "", data.cancelled_by));
|
|
1775
|
+
endTurnView("canceled", cancelLineText(data.cancellation_reason || "", data.cancelled_by), { stopped: true });
|
|
1764
1776
|
setLiveStatus("idle");
|
|
1765
1777
|
refreshTimingFromSession();
|
|
1766
1778
|
},
|
|
@@ -82,6 +82,24 @@ export function stepTextHtml(text) {
|
|
|
82
82
|
return out + escapeHtml(String(text).slice(at));
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
function heredocLabel({ tag, lines }) {
|
|
86
|
+
return `${tag} · ${Number(lines)} ${lines === 1 ? "line" : "lines"}`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// A step's heredoc chip: the first heredoc's tag and size, and "+N" for
|
|
90
|
+
// the others when the step has more (CommandSteps' heredocs:, as gh api
|
|
91
|
+
// -f body="$(cat <<EOF …)" -f title="$(cat <<EOF …)"), as a title with
|
|
92
|
+
// more steps reads "+N"; the hover lists them all. "" without one.
|
|
93
|
+
export function heredocChip(step) {
|
|
94
|
+
if (!step?.heredoc) return "";
|
|
95
|
+
const all = Array.isArray(step.heredocs) && step.heredocs.length > 1 ? step.heredocs : [step.heredoc];
|
|
96
|
+
const more = all.length > 1 ? ` +${all.length - 1}` : "";
|
|
97
|
+
const title = all.length > 1
|
|
98
|
+
? `${all.length} heredocs: ${all.map(heredocLabel).join(", ")}; their text in the raw command`
|
|
99
|
+
: "a heredoc: its text in the raw command";
|
|
100
|
+
return `<span class="step-chip heredoc" title="${escapeHtml(title)}">${escapeHtml(heredocLabel(all[0]))}${more}</span>`;
|
|
101
|
+
}
|
|
102
|
+
|
|
85
103
|
// The steps as a list: a label as a small heading above its step, the op
|
|
86
104
|
// that links a step to the one before, its text, its limit and heredoc
|
|
87
105
|
// as chips (beside the text, below it when the row is narrow).
|
|
@@ -89,11 +107,7 @@ export function stepsHtml(steps) {
|
|
|
89
107
|
const items = steps.map((step) => {
|
|
90
108
|
const label = step.label ? `<li class="step-label">${escapeHtml(step.label)}</li>` : "";
|
|
91
109
|
const limit = step.limit ? `<span class="step-chip" title="output limited">${escapeHtml(step.limit)}</span>` : "";
|
|
92
|
-
|
|
93
|
-
? `<span class="step-chip heredoc" title="a heredoc: its text in the raw command">${escapeHtml(step.heredoc.tag)} · ` +
|
|
94
|
-
`${Number(step.heredoc.lines)} ${step.heredoc.lines === 1 ? "line" : "lines"}</span>`
|
|
95
|
-
: "";
|
|
96
|
-
return `${label}<li class="activity-step">${opHtml(step.op)}<span class="step-body"><code>${stepTextHtml(step.text)}</code>${limit}${heredoc}</span></li>`;
|
|
110
|
+
return `${label}<li class="activity-step">${opHtml(step.op)}<span class="step-body"><code>${stepTextHtml(step.text)}</code>${limit}${heredocChip(step)}</span></li>`;
|
|
97
111
|
});
|
|
98
112
|
return `<ol class="activity-steps">${items.join("")}</ol>`;
|
|
99
113
|
}
|
|
@@ -122,45 +122,57 @@ main{flex:1;display:flex;flex-direction:column;gap:16px;padding:16px;overflow:au
|
|
|
122
122
|
.bubble.user blockquote:last-child{margin-bottom:0}
|
|
123
123
|
.bubble.tool{border-left:3px solid var(--ok);background:var(--bg-panel);font-size:13px;color:var(--text-2)}
|
|
124
124
|
.bubble.cancel{border-left:3px solid var(--danger);background:color-mix(in srgb,var(--danger) 8%,var(--bg-panel));color:var(--danger-text);font-size:13px}
|
|
125
|
+
/* A stop someone chose (a cancel, the session's stop): the ■ STOPPED rows' colours, not an error's. */
|
|
126
|
+
.bubble.cancel.stopped{border-left-color:var(--warn);background:color-mix(in srgb,var(--warn) 8%,var(--bg-panel));color:var(--warn-text)}
|
|
125
127
|
.bubble.recap{border-left:3px solid var(--think-text);background:color-mix(in srgb,var(--think) 6%,var(--bg-panel));font-size:13px}
|
|
126
128
|
.bubble.recap summary{cursor:pointer;color:var(--think-text);font-size:12px}
|
|
127
129
|
.bubble.recap .recap-body{white-space:pre-wrap;margin-top:6px}
|
|
128
130
|
.bubble.recap.stale{opacity:.7;border-left-style:dashed}
|
|
129
131
|
.activity-body{margin-top:6px}
|
|
130
|
-
.activity-row{display:flex;gap:8px;align-items:baseline;font-size:13px;color:var(--text-2);padding:5px 2px;border-top:1px solid var(--border)}
|
|
132
|
+
.activity-row{--row-gutter:22px;display:flex;gap:8px;align-items:baseline;font-size:13px;color:var(--text-2);padding:5px 2px;border-top:1px solid var(--border)}
|
|
131
133
|
.activity-row:first-child{border-top:none}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
.activity-
|
|
136
|
-
.activity-status
|
|
137
|
-
.activity-status.
|
|
134
|
+
/* A row's status is a mark in a 14 px slot, drawn from its class (no text: a copy or an annotate quote gets
|
|
135
|
+
no glyph; the word is its aria-label and title). The glyphs the stage already uses: done ✓, error and
|
|
136
|
+
blocked ✕, stopped ■, running its border spinner (still once the turn is over: a dead worker's row).
|
|
137
|
+
An exception also shows its word after the title (.activity-state). */
|
|
138
|
+
.activity-status{width:14px;text-align:center;font-size:12px;font-weight:700}
|
|
139
|
+
.activity-status.ok::before{content:"\2713";opacity:.85}
|
|
140
|
+
.activity-status.error::before,.activity-status.blocked::before{content:"\2715"}
|
|
141
|
+
.activity-status.stopped::before{content:"\25A0";font-size:10px}
|
|
142
|
+
.activity-status.running::before{content:"";display:inline-block;box-sizing:border-box;width:10px;height:10px;vertical-align:-1px;border:1.5px solid currentColor;border-right-color:transparent;border-radius:50%}
|
|
143
|
+
.turn-work:not(.done) .activity-status.running::before{animation:init-spin .8s linear infinite}
|
|
144
|
+
.activity-state{flex:none;white-space:nowrap;font-size:11px;text-transform:uppercase;letter-spacing:.03em;font-weight:600}
|
|
145
|
+
:is(.activity-status,.activity-state).running{color:var(--warn)}
|
|
146
|
+
:is(.activity-status,.activity-state).ok{color:var(--ok)}
|
|
147
|
+
:is(.activity-status,.activity-state).error{color:var(--danger)}
|
|
148
|
+
:is(.activity-status,.activity-state).stopped{color:var(--warn-text);opacity:.8}
|
|
149
|
+
:is(.activity-status,.activity-state).blocked{color:var(--danger-text)}
|
|
138
150
|
.activity-tool{color:var(--accent);font-weight:600}
|
|
139
151
|
/* The stop-task button on a running task_wait (task_stop.js): a small ghost
|
|
140
152
|
button in the danger colours, at the row's end and in the stage's now slot. */
|
|
141
153
|
button.task-stop{flex:none;margin-left:auto;padding:1px 8px;border:1px solid color-mix(in srgb,var(--danger) 45%,transparent);border-radius:6px;background:transparent;color:var(--danger-text);font-size:11.5px;line-height:18px;white-space:nowrap}
|
|
142
154
|
button.task-stop:hover:not(:disabled){background:var(--danger-bg-dim);filter:none}
|
|
143
155
|
.ts-now button.task-stop{align-self:center}
|
|
144
|
-
.activity-params{color:var(--text-muted);
|
|
156
|
+
.activity-params{color:var(--text-muted);overflow-wrap:anywhere}
|
|
145
157
|
.activity-duration{color:var(--text-faint);font-size:11px;font-variant-numeric:tabular-nums;white-space:nowrap}
|
|
146
|
-
.activity-output{margin:4px 0 2px
|
|
158
|
+
.activity-output{margin:4px 0 2px var(--row-gutter);font-size:12px;color:var(--text-muted);white-space:pre-wrap;word-break:break-word;background:var(--bg);border-radius:6px;padding:6px 8px;max-height:160px;overflow:auto}
|
|
147
159
|
/* A row's status and tool name never wrap (a phone broke them letter by
|
|
148
160
|
letter); the params take what is left. The output goes on its own line
|
|
149
161
|
under the call instead of a column beside it: on a desktop it lines up
|
|
150
|
-
under the tool name (the
|
|
151
|
-
takes the full width and the params break inside the rest of
|
|
152
|
-
line rather than drop to a line of their own. */
|
|
162
|
+
under the tool name (--row-gutter: the 14 px mark slot + the 8 px gap), on
|
|
163
|
+
a phone it takes the full width and the params break inside the rest of
|
|
164
|
+
the first line rather than drop to a line of their own. */
|
|
153
165
|
.activity-row{flex-wrap:wrap}
|
|
154
166
|
.activity-status,.activity-tool{flex-shrink:0;white-space:nowrap}
|
|
155
167
|
.activity-params{min-width:0}
|
|
156
168
|
.activity-output{flex:1 1 100%;min-width:0}
|
|
157
169
|
/* An edit/write's change: a closed "diff +3 −1" under the output, lined up with it. */
|
|
158
|
-
.activity-diff{flex:1 1 100%;min-width:0;margin:2px 0 2px
|
|
170
|
+
.activity-diff{flex:1 1 100%;min-width:0;margin:2px 0 2px var(--row-gutter)}
|
|
159
171
|
.activity-diff>summary{cursor:pointer;font-size:12px;color:var(--text-muted);width:max-content;font-variant-numeric:tabular-nums}
|
|
160
172
|
.activity-diff>summary:hover{color:var(--text-2)}
|
|
161
173
|
.activity-diff[open]>summary{margin-bottom:4px}
|
|
162
174
|
/* An execute/task_create's full command, above its output and lined up with it: as written, its own scroll. */
|
|
163
|
-
.activity-row>.activity-command{flex:1 1 100%;min-width:0;margin:4px 0 2px
|
|
175
|
+
.activity-row>.activity-command{flex:1 1 100%;min-width:0;margin:4px 0 2px var(--row-gutter)}
|
|
164
176
|
.activity-command pre{margin:0;max-height:160px;overflow:auto;white-space:pre-wrap;word-break:break-word;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;line-height:1.45;color:var(--text-2);background:var(--bg-inset);border:1px solid var(--border);border-radius:6px;padding:6px 34px 6px 8px}
|
|
165
177
|
.activity-command-cwd,.activity-command-cut{font-size:11px;color:var(--text-faint)}
|
|
166
178
|
.activity-command-cwd{margin-bottom:2px}
|
|
@@ -218,12 +230,19 @@ button.task-stop:hover:not(:disabled){background:var(--danger-bg-dim);filter:non
|
|
|
218
230
|
.turn-work:not([open]){padding:3px 10px;background:transparent;border-color:transparent;border-left-color:var(--accent);align-self:flex-start;border-radius:6px;transition:background .12s ease}
|
|
219
231
|
.turn-work:not([open]):hover{background:var(--bg-panel)}
|
|
220
232
|
.turn-work:not([open])>summary{font-size:12px;font-weight:500}
|
|
221
|
-
|
|
222
|
-
.
|
|
233
|
+
/* A step's body (thinking, narration, rows, cards) sits --step-indent right of its summary, so the thinking
|
|
234
|
+
chevron and the row marks share one column under the step's own chevron. The summary takes it back with a
|
|
235
|
+
negative margin: a child's own margin shorthand can't undo the padding. */
|
|
236
|
+
.gen{--step-indent:14px;margin-top:8px;padding-top:6px;padding-left:var(--step-indent);border-top:1px solid var(--border)}
|
|
237
|
+
.gen>summary{margin-left:calc(-1 * var(--step-indent));cursor:pointer;color:var(--text-muted);font-size:12px;font-weight:500;user-select:none;list-style:none;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
223
238
|
.gen>summary::-webkit-details-marker{display:none}
|
|
224
239
|
.gen>summary::before{content:"▸ ";color:var(--text-faint)}
|
|
225
240
|
.gen[open]>summary::before{content:"▾ "}
|
|
226
|
-
.
|
|
241
|
+
/* Open, a step whose label starts with its body's first words (turn_view.js labelParts) says only its calls. */
|
|
242
|
+
.gen[open]>summary.repeats>:is(.gen-head,.gen-sep){display:none}
|
|
243
|
+
/* A step with a failed call (status error): its chevron in the danger colour. */
|
|
244
|
+
.gen.has-failed>summary::before{color:var(--danger)}
|
|
245
|
+
.gen.live{border-top-color:transparent;background:var(--accent-tint);border-radius:8px;padding:6px 9px 8px calc(9px + var(--step-indent));margin-left:-9px;margin-right:-9px}
|
|
227
246
|
.gen.live>summary{color:var(--accent);white-space:normal}
|
|
228
247
|
/* The live step's marker: a pulsing accent dot in place of the chevron (the block's own left bar stays the only one). */
|
|
229
248
|
.gen.live>summary::before{content:"";display:inline-block;width:7px;height:7px;margin:0 8px 1px 2px;border-radius:50%;background:var(--accent);vertical-align:middle;animation:step-pulse 1.6s ease-in-out infinite}
|
|
@@ -256,6 +275,7 @@ button.task-stop:hover:not(:disabled){background:var(--danger-bg-dim);filter:non
|
|
|
256
275
|
.turn-work:not(:has(.gen~.gen,.thinking-body,.activity-body))>summary,.turn-work:not(:has(.gen~.gen,.thinking-body,.activity-body)) .gen>summary{display:none}
|
|
257
276
|
.turn-work:not(:has(.gen~.gen,.thinking-body,.activity-body)) .gen{margin:0;padding:0;border:0;background:transparent}
|
|
258
277
|
.turn-work:not(:has(.gen~.gen,.thinking-body,.activity-body)) .gen-text{margin:0;background:var(--bg-panel);border:1px solid var(--border);border-left:3px solid var(--accent);border-radius:12px;padding:12px 14px;opacity:.95}
|
|
278
|
+
@media(max-width:600px){.gen{--step-indent:8px}}
|
|
259
279
|
/* A question card asks for input like the composer card, so it wears the same glass (the recipe above); only its warm tint is its own. */
|
|
260
280
|
.bubble.question{border-left:3px solid var(--warn);background:color-mix(in srgb,var(--warn) 7%,var(--glass-bg));padding:10px 12px}
|
|
261
281
|
/* Collapsed (resolved) question: one line, like the thinking and activity blocks.
|
|
@@ -432,7 +452,8 @@ body.has-session #history{padding-bottom:calc(var(--dock-h,160px) + 32px)}
|
|
|
432
452
|
.chip .chip-remove{background:transparent;color:var(--text-3);padding:0 6px;min-height:0;font-size:14px}
|
|
433
453
|
.thumbs{display:flex;flex-wrap:wrap;gap:6px;margin-top:6px}
|
|
434
454
|
.thumb{max-height:120px;max-width:200px;border-radius:6px;border:1px solid var(--border-strong);cursor:zoom-in;object-fit:contain;background:var(--bg)}
|
|
435
|
-
.activity-row .thumbs{flex-basis:100
|
|
455
|
+
.activity-row .thumbs{flex-basis:100%;margin-left:var(--row-gutter)}
|
|
456
|
+
@media(max-width:600px){.activity-row .thumbs{margin-left:0}}
|
|
436
457
|
#lightbox{position:fixed;inset:0;z-index:50;background:var(--backdrop);display:flex;align-items:center;justify-content:center;cursor:zoom-out}
|
|
437
458
|
#lightbox[hidden]{display:none}
|
|
438
459
|
#lightbox img{max-width:96vw;max-height:94vh;border-radius:6px}
|
|
@@ -528,6 +549,7 @@ body.stage-view #dock,body.stage-view #dock:focus-within{background:transparent;
|
|
|
528
549
|
.turn-stage[data-phase="no answer"] .ts-mark,.turn-stage[data-phase="no answer"] .ts-phase{color:var(--text-muted)}
|
|
529
550
|
.turn-stage[data-phase="waiting for you"] .ts-mark{background:var(--warn)}
|
|
530
551
|
.ts-phase{color:var(--accent);font-weight:600}
|
|
552
|
+
.ts-step:empty{display:none}
|
|
531
553
|
.ts-timing{min-width:0;overflow:hidden;text-overflow:ellipsis}
|
|
532
554
|
.ts-timing .turn-timing{margin:0;font-size:12px;color:var(--text-muted);display:inline}
|
|
533
555
|
.ts-mini{min-width:0;overflow:hidden;text-overflow:ellipsis;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;color:var(--text-2)}
|
|
@@ -562,14 +584,13 @@ body.stage-view #dock,body.stage-view #dock:focus-within{background:transparent;
|
|
|
562
584
|
.ts-spin{flex:none;width:11px;height:11px;border:2px solid var(--warn);border-right-color:transparent;border-radius:50%;animation:init-spin .8s linear infinite}
|
|
563
585
|
.ts-idle{flex:none;width:11px;height:11px;border:2px solid var(--border-strong);border-radius:50%}
|
|
564
586
|
.ts-tool{flex:none;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;font-weight:600;color:var(--accent)}
|
|
565
|
-
.ts-tool.exec{color:var(--think-text)}
|
|
566
|
-
.ts-tool.edit{color:var(--ok-text)}
|
|
567
587
|
.ts-title{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;color:var(--text-2)}
|
|
568
588
|
.ts-title.quiet{font-family:inherit;font-style:italic;color:var(--text-faint)}
|
|
569
589
|
.ts-trail{height:18px;display:flex;gap:10px;overflow:hidden;font-size:12px;line-height:18px;color:var(--text-faint);white-space:nowrap}
|
|
570
590
|
.ts-trail-item{flex:none;max-width:36ch;overflow:hidden;text-overflow:ellipsis}
|
|
571
|
-
.ts-trail-item.error{color:var(--danger-text)}
|
|
591
|
+
.ts-trail-item:is(.error,.blocked){color:var(--danger-text)}
|
|
572
592
|
.ts-trail-item.warn{color:var(--warn-text)}
|
|
593
|
+
.ts-trail-item.stopped{color:var(--warn-text);opacity:.8}
|
|
573
594
|
.ts-trail-item.info,.ts-trail-item.steer{color:var(--accent)}
|
|
574
595
|
.ts-trail-item.none{font-style:italic}
|
|
575
596
|
/* A card waits: the tool row and trail give it their room. */
|
|
@@ -600,10 +621,14 @@ body.stage-view #dock,body.stage-view #dock:focus-within{background:transparent;
|
|
|
600
621
|
.ts-ticks{display:flex;flex-wrap:nowrap;justify-content:flex-end;overflow:hidden;gap:2px;flex:0 1 auto;min-width:0}
|
|
601
622
|
.ts-tick{flex:none}
|
|
602
623
|
.ts-tick{width:5px;height:12px;border-radius:1.5px;background:var(--text-faint);opacity:.55;cursor:pointer}
|
|
603
|
-
|
|
604
|
-
.ts-tick.edit{background:var(--
|
|
605
|
-
.ts-tick.other{background:var(--
|
|
624
|
+
/* exec and edit are the rows' tool blue (purple is thinking's, green a done ✓'s); other a quieter blue-grey. */
|
|
625
|
+
.ts-tick:is(.exec,.edit){background:var(--accent);opacity:.85}
|
|
626
|
+
.ts-tick.other{background:var(--narr);opacity:.7}
|
|
606
627
|
.ts-tick.error{background:var(--danger);opacity:1}
|
|
628
|
+
/* A call a guardrail blocked: its ✕ BLOCKED row's red. */
|
|
629
|
+
.ts-tick.blocked{background:var(--danger-text);opacity:1}
|
|
630
|
+
/* A call the user's Stop cut: the ■ STOPPED rows' amber, still (running blinks). */
|
|
631
|
+
.ts-tick.stopped{background:var(--warn-text);opacity:.8}
|
|
607
632
|
.ts-tick.running{background:var(--warn);opacity:1;animation:ts-blink 1s ease-in-out infinite}
|
|
608
633
|
@keyframes ts-blink{50%{opacity:.35}}
|
|
609
634
|
.ts-cloud-body{max-height:calc(var(--stage-max,60vh) * .62);overflow:auto;scrollbar-width:thin;border-left:1px solid var(--border);margin-left:6px;padding-left:10px}
|
|
@@ -617,7 +642,7 @@ body.stage-view #dock,body.stage-view #dock:focus-within{background:transparent;
|
|
|
617
642
|
.ts-defer{display:none}
|
|
618
643
|
.ts-trail-item{max-width:24ch}
|
|
619
644
|
}
|
|
620
|
-
@media (prefers-reduced-motion:reduce){.turn-stage.enter,.ts-arrive,.ts-headline.fresh,.ts-tick.running,.ts-mark{animation:none}.turn-stage.leaving{transition:none}}
|
|
645
|
+
@media (prefers-reduced-motion:reduce){.turn-stage.enter,.ts-arrive,.ts-headline.fresh,.ts-tick.running,.ts-mark,.turn-work:not(.done) .activity-status.running::before{animation:none}.turn-stage.leaving{transition:none}}
|
|
621
646
|
#infoBar{display:flex;padding:8px 14px 10px;border-top:1px solid var(--glass-border);background:transparent;border-radius:0 0 16px 16px;color:var(--text-2);gap:10px;align-items:flex-start;flex-wrap:wrap;flex-shrink:0;transition:opacity 280ms ease}
|
|
622
647
|
#infoBar.visible{display:flex}
|
|
623
648
|
#infoBar .info{flex:1;min-width:0;font-size:13px;line-height:1.4;word-break:break-word;display:flex;flex-direction:column;gap:2px}
|
|
@@ -809,6 +834,7 @@ body.all-view #center, body.all-view #topStrip{display:none}
|
|
|
809
834
|
@keyframes attn-pulse{0%,100%{box-shadow:0 0 0 0 color-mix(in srgb,var(--warn) 60%,transparent)}50%{box-shadow:0 0 0 4px color-mix(in srgb,var(--warn) 0%,transparent)}}
|
|
810
835
|
@media (prefers-reduced-motion:reduce){.card .dot.running,.card .dot.waiting{animation:none}}
|
|
811
836
|
.card .attn{font-size:11px;font-weight:600;padding:1px 7px;border-radius:999px;white-space:nowrap;flex-shrink:0;background:var(--warn-bg);color:var(--warn-text);border:1px solid var(--warn-border)}
|
|
837
|
+
.card .stopped-badge{font-size:11px;font-weight:600;padding:1px 7px;border-radius:999px;white-space:nowrap;flex-shrink:0;background:var(--warn);color:var(--bg)}
|
|
812
838
|
.status.running{background:var(--ok-bg);color:var(--ok-text)}
|
|
813
839
|
.status.idle{background:var(--bg-raised)}
|
|
814
840
|
.status.completed{background:var(--accent-bg);color:var(--accent)}
|
|
@@ -89,12 +89,29 @@ export function waitingBadge(summary) {
|
|
|
89
89
|
return { kind: waiting.reason, ...WAITING_BADGES[waiting.reason] };
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
// "" for
|
|
92
|
+
// A card's badge for a last turn a hook stopped (the same marker as
|
|
93
|
+
// `chi sessions list`'s "[looped]" / "[stopped by X]"): "looped" for
|
|
94
|
+
// loop-guard, "stopped by <name>" for any other hook; null when the last
|
|
95
|
+
// turn ended otherwise.
|
|
96
|
+
// @return null | {text, title}
|
|
97
|
+
export function stoppedBadge(summary) {
|
|
98
|
+
const by = summary?.last_turn?.stopped_by;
|
|
99
|
+
if (!by) return null;
|
|
100
|
+
return by === "loop-guard"
|
|
101
|
+
? { text: "looped", title: "loop-guard stopped the last turn" }
|
|
102
|
+
: { text: `stopped by ${by}`, title: `${by} stopped the last turn` };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// What the all view's search matches for a session: its waiting badge's words
|
|
106
|
+
// ("waiting", "question", "approval", …) and/or its stopped badge's words
|
|
107
|
+
// ("looped", "stopped by <name>"); "" for one with neither.
|
|
95
108
|
export function waitingSearchText(summary) {
|
|
109
|
+
const parts = [];
|
|
96
110
|
const badge = waitingBadge(summary);
|
|
97
|
-
|
|
111
|
+
if (badge) parts.push(`waiting ${badge.text}`);
|
|
112
|
+
const stopped = stoppedBadge(summary);
|
|
113
|
+
if (stopped) parts.push(stopped.text);
|
|
114
|
+
return parts.join(" ");
|
|
98
115
|
}
|
|
99
116
|
|
|
100
117
|
// The sessions that wait on the user (waitingOn) first, the rest after,
|
|
@@ -11,6 +11,7 @@ const READ_LIKE = new Set(["read", "memory_read", "web_fetch", "task_get", "task
|
|
|
11
11
|
const EDITS = new Set(["edit", "write", "memory_write"]);
|
|
12
12
|
const EXECS = new Set(["execute", "task_create", "task_wait", "task_stop", "delegate", "delegate_result"]);
|
|
13
13
|
const FILE_TOOLS = new Set(["read", "edit", "write"]);
|
|
14
|
+
const TASK_TOOLS = new Set(["task_wait", "task_get", "task_stop"]);
|
|
14
15
|
const TRAIL = 3;
|
|
15
16
|
// The hand-off waits this long after the stage was last in use.
|
|
16
17
|
export const QUIET_MS = 1500;
|
|
@@ -75,14 +76,30 @@ export function toolKind(name) {
|
|
|
75
76
|
return "other";
|
|
76
77
|
}
|
|
77
78
|
|
|
79
|
+
// A finished call's mark in the trail, as its row's (index.html
|
|
80
|
+
// .activity-status): error and blocked ✕, stopped (a Stop cut it) ■, else ✓.
|
|
81
|
+
export function trailMark(status) {
|
|
82
|
+
if (status === "error" || status === "blocked") return "\u2715";
|
|
83
|
+
if (status === "stopped") return "\u25A0";
|
|
84
|
+
return "\u2713";
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// A finished call's tone in the trail, its class as its row's colour:
|
|
88
|
+
// error and blocked (a guardrail's deny) red, stopped amber, else none.
|
|
89
|
+
export function trailTone(status) {
|
|
90
|
+
return status === "error" || status === "blocked" || status === "stopped" ? status : "";
|
|
91
|
+
}
|
|
92
|
+
|
|
78
93
|
function rowTitle(row) {
|
|
79
94
|
return row.title || row.params || "";
|
|
80
95
|
}
|
|
81
96
|
|
|
82
97
|
// A slot's full command (the row's view), for its hover; none for a tool
|
|
83
|
-
// without one.
|
|
98
|
+
// without one. A task tool titled by its task's command (the server's
|
|
99
|
+
// title) hovers its params instead: the id line it replaced.
|
|
84
100
|
function withCommand(slot, row) {
|
|
85
|
-
|
|
101
|
+
if (row.view?.command) return { ...slot, command: row.view.command };
|
|
102
|
+
return TASK_TOOLS.has(row.tool) && row.title && row.params ? { ...slot, command: row.params } : slot;
|
|
86
103
|
}
|
|
87
104
|
|
|
88
105
|
// A running task_wait's slot: the task it waits on and the row's key, for
|
|
@@ -113,6 +130,9 @@ const NO_ANSWER = "no answer";
|
|
|
113
130
|
// else "…";
|
|
114
131
|
// tool the running call ({name, title, kind}, and command: its full
|
|
115
132
|
// command when it has one; taskId and key: a task_wait's) or null;
|
|
133
|
+
// quiet the now row's word while no call runs: "writing…" while the
|
|
134
|
+
// step's text streams, else "thinking…"; "" once the turn has
|
|
135
|
+
// ended (nothing happens now);
|
|
116
136
|
// trail the last three finished calls, newest last (a file's title
|
|
117
137
|
// without its dir; command as for tool);
|
|
118
138
|
// ticks one per call: {kind, status, step (the gen's index)}.
|
|
@@ -143,6 +163,7 @@ export function liveSlots(turn, { narration = "", thinking = "", pending = false
|
|
|
143
163
|
phase,
|
|
144
164
|
step: ended ? gens.filter((g) => !g.answer || g.thinking || g.tools.length).length : gens.length,
|
|
145
165
|
headline,
|
|
166
|
+
quiet: ended ? "" : current?.text && !current.textDone ? "writing…" : "thinking…",
|
|
146
167
|
tool: running ? withTask(withCommand({ name: toolName(running), title: rowTitle(running), kind: toolKind(running.tool) }, running), running) : null,
|
|
147
168
|
trail,
|
|
148
169
|
ticks: rows.map(({ row, step }) => ({ kind: toolKind(row.tool), status: row.status, step })),
|