samagotchi 0.29.0 → 0.31.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 +99 -1
- data/docs/cli.md +4 -4
- data/docs/configuration.md +4 -0
- data/docs/hooks.md +1 -1
- data/docs/internals/engine.md +1 -1
- data/docs/plugins.md +1 -1
- data/docs/sessions.md +11 -4
- data/lib/samagotchi/bridge/card_store.rb +1 -1
- data/lib/samagotchi/bridge/turn_notice.rb +1 -1
- 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/delegated.md +1 -0
- data/lib/samagotchi/bundles/system/manifest.yml +3 -3
- data/lib/samagotchi/bundles/system/self_map.md +2 -1
- data/lib/samagotchi/child_reports.rb +214 -0
- data/lib/samagotchi/client.rb +35 -4
- data/lib/samagotchi/command_steps.rb +35 -15
- data/lib/samagotchi/config.rb +5 -0
- data/lib/samagotchi/engine.rb +2 -0
- data/lib/samagotchi/host_registry.rb +4 -0
- data/lib/samagotchi/idle_recap.rb +4 -0
- data/lib/samagotchi/kernel_loop.rb +63 -11
- data/lib/samagotchi/llm/chat_loop.rb +14 -7
- data/lib/samagotchi/llm/errors.rb +9 -0
- data/lib/samagotchi/log_subscriber.rb +1 -1
- data/lib/samagotchi/model_list_store.rb +117 -0
- data/lib/samagotchi/model_profile.rb +43 -0
- data/lib/samagotchi/prompt_warmup.rb +30 -5
- data/lib/samagotchi/reply_wait.rb +39 -8
- data/lib/samagotchi/send_command.rb +5 -3
- data/lib/samagotchi/session.rb +31 -5
- data/lib/samagotchi/session_inbox.rb +34 -1
- data/lib/samagotchi/session_manager.rb +41 -5
- data/lib/samagotchi/session_metrics.rb +6 -3
- data/lib/samagotchi/steer.rb +39 -7
- data/lib/samagotchi/terminal_ui/attached_loop.rb +4 -2
- data/lib/samagotchi/terminal_ui/event_renderer.rb +3 -1
- data/lib/samagotchi/terminal_ui/formatting.rb +38 -8
- data/lib/samagotchi/terminal_ui.rb +44 -5
- data/lib/samagotchi/tool_activity.rb +6 -2
- data/lib/samagotchi/tool_call_parser.rb +40 -0
- data/lib/samagotchi/tool_declarations.rb +7 -7
- data/lib/samagotchi/tools/delegate.rb +26 -13
- data/lib/samagotchi/tools/delegate_cursor.rb +91 -0
- data/lib/samagotchi/tools/delegate_wait.rb +50 -31
- data/lib/samagotchi/tools/execute.rb +3 -1
- data/lib/samagotchi/tools/task_runtime.rb +21 -2
- data/lib/samagotchi/tools/task_wait.rb +2 -2
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +8 -2
- data/lib/samagotchi/web/public/activity.js +2 -1
- data/lib/samagotchi/web/public/app.js +21 -11
- data/lib/samagotchi/web/public/command_view.js +19 -5
- data/lib/samagotchi/web/public/format.js +8 -1
- data/lib/samagotchi/web/public/index.html +14 -6
- data/lib/samagotchi/web/public/notify.js +4 -2
- data/lib/samagotchi/web/public/stage_model.js +18 -0
- 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 +29 -9
- data/lib/samagotchi/web/public/turn_model.js +16 -11
- data/lib/samagotchi/web/public/turn_view.js +49 -7
- data/lib/samagotchi/worker.rb +240 -23
- metadata +4 -1
|
@@ -6,18 +6,18 @@ require_relative "../parent_report"
|
|
|
6
6
|
require_relative "output_guardrails"
|
|
7
7
|
require_relative "peers"
|
|
8
8
|
require_relative "delegate_relay"
|
|
9
|
+
require_relative "delegate_cursor"
|
|
9
10
|
|
|
10
11
|
module Samagotchi
|
|
11
12
|
module Tools
|
|
12
13
|
# Waiting for a delegated session's next reply, shared by delegate and
|
|
13
14
|
# delegate_result: ReplyWait in the tools' words, with the status words
|
|
14
|
-
# `chi send --wait` and `chi answer` use (ParentReport.status), with a
|
|
15
|
-
# child
|
|
16
|
-
# as it was
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
# reply once or waits for a later turn.
|
|
15
|
+
# `chi send --wait` and `chi answer` use (ParentReport.status), with a
|
|
16
|
+
# DelegateCursor per child: the newest reply this parent was already
|
|
17
|
+
# given, and the child as it was when the parent last heard from it or
|
|
18
|
+
# sent it a message (ReplyWait's baseline), so a turn that ends before
|
|
19
|
+
# the wait's first look (a fast failure) still ends it. The cursors are
|
|
20
|
+
# on disk (DelegateCursors), so a respawned worker doesn't repeat a reply.
|
|
21
21
|
module DelegateWait
|
|
22
22
|
TIMEOUT_DEFAULT = 600
|
|
23
23
|
POLL_INTERVAL = 0.5
|
|
@@ -32,16 +32,6 @@ module Samagotchi
|
|
|
32
32
|
|
|
33
33
|
module_function
|
|
34
34
|
|
|
35
|
-
# @return [Hash{Array(String, String) => String}] [parent id, child id] → newest reply filename given
|
|
36
|
-
def seen
|
|
37
|
-
@seen ||= {}
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
# @return [Hash{Array(String, String) => Hash}] [parent id, child id] → ReplyWait baseline taken before sending
|
|
41
|
-
def baselines
|
|
42
|
-
@baselines ||= {}
|
|
43
|
-
end
|
|
44
|
-
|
|
45
35
|
# @return [Hash{Array(String, String) => Array<String>}] [parent id,
|
|
46
36
|
# child id] → outcome lines of that child's approvals relayed while
|
|
47
37
|
# the parent waited on another child, for its next result
|
|
@@ -69,18 +59,20 @@ module Samagotchi
|
|
|
69
59
|
cancelled = -> { peers.cancelled? }
|
|
70
60
|
relay = peers.respond_to?(:relay) ? peers.relay : nil
|
|
71
61
|
others = relay && others_for(peers.session_id, child_id, relay, sd)
|
|
72
|
-
|
|
62
|
+
cursor = DelegateCursors.get(peers.session_id, child_id, state_dir: sd)
|
|
63
|
+
baseline = cursor.baseline
|
|
73
64
|
left = timeout.to_i
|
|
74
65
|
outcomes = relayed_outcomes.delete(key) || []
|
|
75
66
|
loop do
|
|
76
67
|
started = monotonic
|
|
77
|
-
wait = ReplyWait.call(child_id, state_dir: sd, cursor:
|
|
68
|
+
wait = ReplyWait.call(child_id, state_dir: sd, cursor: cursor.reply_file, timeout: [left, 0].max,
|
|
78
69
|
poll_interval: poll_interval, cancelled: cancelled, baseline: baseline,
|
|
79
70
|
owner_grace: owner_grace, interject: others && -> { others.poll })
|
|
80
71
|
left -= monotonic - started
|
|
81
72
|
unless wait.status == :waiting_for_answer && DelegateRelay.relayable?(relay, wait.question)
|
|
82
73
|
outcomes.concat(relayed_outcomes.delete(key) || [])
|
|
83
|
-
|
|
74
|
+
advance(peers.session_id, child_id, wait, state_dir: sd)
|
|
75
|
+
return with_outcomes(finish(wait, child_id, timeout: timeout), outcomes)
|
|
84
76
|
end
|
|
85
77
|
|
|
86
78
|
outcome = DelegateRelay.call(child_id, wait.question, relay: relay, state_dir: sd, more: others&.count.to_i)
|
|
@@ -118,24 +110,43 @@ module Samagotchi
|
|
|
118
110
|
|
|
119
111
|
def monotonic = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
120
112
|
|
|
113
|
+
REPORT_NEXT_REPLY = "chi brings the child's next reply here by itself; don't wait for it with delegate_result."
|
|
114
|
+
|
|
115
|
+
# Outcomes that tell the parent something about the child's state.
|
|
116
|
+
REPORTED = %i[done no_reply error stopped waiting_for_answer worker_gone].freeze
|
|
117
|
+
|
|
118
|
+
# Move the parent's cursor past what +wait+ reported: the reply it
|
|
119
|
+
# gave, and the child as that same look loaded it (a fresh load could
|
|
120
|
+
# swallow a turn that ended in between), so a later wait looks for a
|
|
121
|
+
# later turn and a question already reported isn't reported again.
|
|
122
|
+
# Nothing moves on a timeout or a cancel.
|
|
123
|
+
# @param wait [ReplyWait::Result]
|
|
124
|
+
# @return [DelegateCursor, nil] the new cursor
|
|
125
|
+
def advance(parent_id, child_id, wait, state_dir:)
|
|
126
|
+
return nil unless REPORTED.include?(wait.status) && wait.session
|
|
127
|
+
|
|
128
|
+
DelegateCursors.update(parent_id, child_id, state_dir: state_dir) do |cursor|
|
|
129
|
+
cursor = cursor.with(reply_file: wait.file) if wait.status == :done
|
|
130
|
+
cursor.with_baseline(ReplyWait.baseline_of(wait.session))
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
121
134
|
# The tool result for how the wait ended.
|
|
122
135
|
# @param wait [ReplyWait::Result]
|
|
123
|
-
# @param
|
|
136
|
+
# @param report [Boolean] a delegate report's text (ChildReports), not
|
|
137
|
+
# the tool's: a question's last line says chi brings the next reply
|
|
124
138
|
# @return [String]
|
|
125
|
-
def finish(wait, child_id,
|
|
126
|
-
# The turn sent to is handed over: a later wait looks for a later one.
|
|
127
|
-
baselines.delete(key) if %i[done no_reply].include?(wait.status)
|
|
139
|
+
def finish(wait, child_id, timeout:, report: false)
|
|
128
140
|
status = ParentReport.status(wait)
|
|
129
141
|
case wait.status
|
|
130
142
|
when :done
|
|
131
|
-
seen[key] = wait.file
|
|
132
143
|
reply_result(child_id, status, wait.text)
|
|
133
144
|
when :error
|
|
134
145
|
result(child_id, status, "the child's worker failed: #{wait.text}; its session shows what happened")
|
|
135
146
|
when :stopped
|
|
136
147
|
result(child_id, status, "the child was stopped (chi sessions stop); delegate with session: #{child_id} starts it again with a message")
|
|
137
148
|
when :waiting_for_answer
|
|
138
|
-
result(child_id, status, waiting_text(child_id, wait.question))
|
|
149
|
+
result(child_id, status, waiting_text(child_id, wait.question, report: report))
|
|
139
150
|
when :canceled
|
|
140
151
|
canceled_result(child_id)
|
|
141
152
|
when :no_reply
|
|
@@ -153,14 +164,18 @@ module Samagotchi
|
|
|
153
164
|
# child's baseline before the follow-up goes in.
|
|
154
165
|
# @param child [Session] the child as loaded before sending
|
|
155
166
|
def mark_seen(parent_id, child, state_dir:)
|
|
156
|
-
|
|
157
|
-
|
|
167
|
+
newest = ReplyWait.newest_reply(child.id, state_dir: state_dir)
|
|
168
|
+
DelegateCursors.update(parent_id, child.id, state_dir: state_dir) do |cursor|
|
|
169
|
+
cursor.with(reply_file: newest).with_baseline(ReplyWait.baseline_of(child))
|
|
170
|
+
end
|
|
158
171
|
end
|
|
159
172
|
|
|
160
173
|
# A new child's baseline: as spawned, before its first turn ran.
|
|
161
174
|
# @param child [Session] the session spawn_session returned
|
|
162
|
-
def mark_started(parent_id, child)
|
|
163
|
-
|
|
175
|
+
def mark_started(parent_id, child, state_dir:)
|
|
176
|
+
DelegateCursors.update(parent_id, child.id, state_dir: state_dir) do |cursor|
|
|
177
|
+
cursor.with_baseline(ReplyWait.baseline_of(child, question_id: nil))
|
|
178
|
+
end
|
|
164
179
|
end
|
|
165
180
|
|
|
166
181
|
def reply_result(child_id, status, text)
|
|
@@ -188,13 +203,17 @@ module Samagotchi
|
|
|
188
203
|
# decide (it isn't relayed: Continue grants no permission): continue
|
|
189
204
|
# it with chi answer through execute, send a follow-up instead, or
|
|
190
205
|
# report back.
|
|
191
|
-
|
|
206
|
+
# A report's last line differs (+report+): "waits again" made models
|
|
207
|
+
# call delegate_result right after answering Continue.
|
|
208
|
+
def waiting_text(child_id, pending, report: false)
|
|
192
209
|
text = "Child #{child_id} is #{ParentReport.question_text(pending, session_id: child_id)}"
|
|
193
210
|
if ParentReport.continue?(pending)
|
|
194
211
|
text += "The child ran out of steps before it answered. Decide: continue it (run the chi answer command " \
|
|
195
212
|
"above with execute) if it is getting somewhere; send it a narrower follow-up with delegate " \
|
|
196
213
|
"session: #{child_id} (that drops the question); or stop it and report back to your user.\n"
|
|
197
214
|
end
|
|
215
|
+
return "#{text}#{REPORT_NEXT_REPLY}" if report
|
|
216
|
+
|
|
198
217
|
"#{text}delegate_result #{child_id} waits again once it is answered."
|
|
199
218
|
end
|
|
200
219
|
|
|
@@ -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
|
|
@@ -154,7 +154,7 @@ module Samagotchi
|
|
|
154
154
|
|
|
155
155
|
def get_record(task_id)
|
|
156
156
|
record = load_record(task_id)
|
|
157
|
-
return [nil,
|
|
157
|
+
return [nil, not_found(task_id)] unless record
|
|
158
158
|
|
|
159
159
|
[refresh_record(record), nil]
|
|
160
160
|
rescue StandardError => e
|
|
@@ -180,7 +180,7 @@ module Samagotchi
|
|
|
180
180
|
# @param by [String] "model" (task_stop) or "user" (the web's button)
|
|
181
181
|
def stop_task(task_id, by:)
|
|
182
182
|
record = load_record(task_id)
|
|
183
|
-
return [nil,
|
|
183
|
+
return [nil, not_found(task_id)] unless record
|
|
184
184
|
|
|
185
185
|
refreshed = refresh_record(record)
|
|
186
186
|
return [refreshed, nil] unless refreshed["status"] == "running"
|
|
@@ -223,7 +223,26 @@ module Samagotchi
|
|
|
223
223
|
record
|
|
224
224
|
end
|
|
225
225
|
|
|
226
|
+
# A session id (a uuid) is never a task's: task ids are a timestamp
|
|
227
|
+
# and hex (20261005093000-1a2b3c4d).
|
|
228
|
+
SESSION_ID_SHAPE = /\A\h{8}-\h{4}-\h{4}-\h{4}-\h{12}\z/
|
|
229
|
+
|
|
230
|
+
# A model mixing up ids (Qwen passes session ids to task_get and
|
|
231
|
+
# task_stop) is told which kind it passed and where task ids come from.
|
|
232
|
+
def not_found(task_id)
|
|
233
|
+
error = "Error: task not found: #{task_id}"
|
|
234
|
+
return error unless SESSION_ID_SHAPE.match?(task_id.to_s)
|
|
235
|
+
|
|
236
|
+
"#{error} (that is a session id; task ids come from task_create, and task_list lists them)"
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# Exactly the shape generate_task_id makes. Anything else (../x, an
|
|
240
|
+
# absolute path) is no task, so a model's id never leaves tmp/tasks.
|
|
241
|
+
TASK_ID_SHAPE = /\A\d{14}-\h{8}\z/
|
|
242
|
+
|
|
226
243
|
def load_record(task_id)
|
|
244
|
+
return nil unless TASK_ID_SHAPE.match?(task_id.to_s)
|
|
245
|
+
|
|
227
246
|
path = metadata_path(task_id)
|
|
228
247
|
return nil unless File.exist?(path)
|
|
229
248
|
|
|
@@ -34,7 +34,7 @@ module Samagotchi
|
|
|
34
34
|
|
|
35
35
|
loop do
|
|
36
36
|
record, error = TaskRuntime.get_record(task_id)
|
|
37
|
-
return
|
|
37
|
+
return error if error
|
|
38
38
|
|
|
39
39
|
status = record.fetch("status")
|
|
40
40
|
return finished_response(record, tail_lines) unless status == "running"
|
|
@@ -64,7 +64,7 @@ module Samagotchi
|
|
|
64
64
|
# normal finished result. Only a task still running says so.
|
|
65
65
|
def self.cancelled_response(task_id, tail_lines, started)
|
|
66
66
|
record, error = TaskRuntime.get_record(task_id)
|
|
67
|
-
return
|
|
67
|
+
return error if error
|
|
68
68
|
return finished_response(record, tail_lines) unless record.fetch("status") == "running"
|
|
69
69
|
|
|
70
70
|
note = [
|
data/lib/samagotchi/version.rb
CHANGED
data/lib/samagotchi/web/app.rb
CHANGED
|
@@ -1522,8 +1522,14 @@ module Samagotchi
|
|
|
1522
1522
|
|
|
1523
1523
|
message = { role: norm_role, content: stripped }
|
|
1524
1524
|
# Merged into the running turn (Steer::INPUT_KIND): part of it,
|
|
1525
|
-
# read by step +step
|
|
1526
|
-
|
|
1525
|
+
# read by step +step+; a wake turn's report starts its turn
|
|
1526
|
+
# (Steer.turn_prompt?). Its sender (a delegate report, chi send):
|
|
1527
|
+
# the page's label.
|
|
1528
|
+
if norm_role == "user" && Samagotchi::Steer.input?(m)
|
|
1529
|
+
message.merge!(merged: true, step: steer_step(list, index)) unless Samagotchi::Steer.turn_prompt?(m)
|
|
1530
|
+
source = (m[:source] || m["source"]).to_s
|
|
1531
|
+
message[:source] = source unless source.empty?
|
|
1532
|
+
end
|
|
1527
1533
|
# The turn it started (Engine): the page pairs it with that turn's
|
|
1528
1534
|
# record by this, not by its place.
|
|
1529
1535
|
turn_id = m[:turn_id] || m["turn_id"]
|
|
@@ -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.
|
|
@@ -43,10 +43,10 @@ import { applySessionEvent, heldOrder, listedSessions, sortedByUpdated, stoppedB
|
|
|
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
|
-
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";
|
|
49
|
+
import { clientLabel, commandView, isReportClient, 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";
|
|
50
50
|
|
|
51
51
|
const $ = (s) => document.querySelector(s);
|
|
52
52
|
const topStripEl = $("#topStrip");
|
|
@@ -142,8 +142,11 @@ let selectSeq = null;
|
|
|
142
142
|
// new turn has started on it yet: see followWokenWorker.
|
|
143
143
|
let streamFromGuess = false;
|
|
144
144
|
// The last input_merged had an origin with no bubble: its merged text
|
|
145
|
-
// (pending_input_merged) gets one.
|
|
145
|
+
// (pending_input_merged) gets one ("report": a delegate report's).
|
|
146
146
|
let unmatchedMerge = false;
|
|
147
|
+
// The running turn is one chi ran for delegate reports and its report
|
|
148
|
+
// bubble isn't drawn yet: that bubble starts the turn.
|
|
149
|
+
let wakeStart = false;
|
|
147
150
|
// The enqueued_ids this page sent (from the /turn acks): only these come back
|
|
148
151
|
// into the composer when their turn fails.
|
|
149
152
|
const sentIds = new Set();
|
|
@@ -710,6 +713,7 @@ function resetTurnView() {
|
|
|
710
713
|
questionCards.removeQuestion();
|
|
711
714
|
questionCards.forgetContinue();
|
|
712
715
|
unmatchedMerge = false;
|
|
716
|
+
wakeStart = false;
|
|
713
717
|
setTurnRunning(false);
|
|
714
718
|
releaseDisplayWait();
|
|
715
719
|
}
|
|
@@ -960,9 +964,11 @@ function appendChunk(text) {
|
|
|
960
964
|
if (follow) historyEl.scrollTop = historyEl.scrollHeight;
|
|
961
965
|
}
|
|
962
966
|
|
|
963
|
-
|
|
967
|
+
// +stopped+: a stop someone chose (a cancel, the session's stop), drawn in
|
|
968
|
+
// the stopped style (timing.js STOPPED_LINE_CLASS); else a failure's line.
|
|
969
|
+
function addEndBubble(text, { stopped = false } = {}) {
|
|
964
970
|
const div = document.createElement("div");
|
|
965
|
-
div.className = "bubble cancel";
|
|
971
|
+
div.className = stopped ? STOPPED_LINE_CLASS : "bubble cancel";
|
|
966
972
|
div.textContent = text;
|
|
967
973
|
appendToHistory(div);
|
|
968
974
|
if (!turnDom.holds?.()) historyEl.scrollTop = historyEl.scrollHeight;
|
|
@@ -980,7 +986,7 @@ function emptyAnswerEl(emptyAnswer) {
|
|
|
980
986
|
// Everything a turn's end (completed, canceled, failed) closes: then its
|
|
981
987
|
// end line (+endText+, the cancel or failed line), then the step cards that
|
|
982
988
|
// left the collapsed block (card.js leavesBlock), where a reload puts them.
|
|
983
|
-
function endTurnView(kind, endText = null, { emptyAnswer = null } = {}) {
|
|
989
|
+
function endTurnView(kind, endText = null, { emptyAnswer = null, stopped = false } = {}) {
|
|
984
990
|
setTurnRunning(false);
|
|
985
991
|
// Every ended turn has a record (completed, canceled, failed); a worker
|
|
986
992
|
// gone mid-turn is re-rendered (resync), which counts again.
|
|
@@ -991,7 +997,7 @@ function endTurnView(kind, endText = null, { emptyAnswer = null } = {}) {
|
|
|
991
997
|
const kept = turnDom.turnEnded({ kind, endNote, emptyAnswer: !!emptyAnswer }) || [];
|
|
992
998
|
if (endNote && !endNote.isConnected) appendToHistory(endNote);
|
|
993
999
|
finishLiveTurnTiming(null, { canceled: kind === "canceled" });
|
|
994
|
-
if (endText) addEndBubble(endText);
|
|
1000
|
+
if (endText) addEndBubble(endText, { stopped });
|
|
995
1001
|
if (kept.length) {
|
|
996
1002
|
kept.forEach((el) => appendToHistory(el));
|
|
997
1003
|
if (!turnDom.holds?.()) historyEl.scrollTop = historyEl.scrollHeight;
|
|
@@ -1065,14 +1071,18 @@ function untaggedOwnBubble(prompt) {
|
|
|
1065
1071
|
// Returns the bubble of the turn a turn_started starts (the stage takes it
|
|
1066
1072
|
// into its prompt line), else null.
|
|
1067
1073
|
function applyPromptOps(event) {
|
|
1068
|
-
|
|
1074
|
+
if (event.type === "turn_started") wakeStart = !!event.continue && isReportClient(event.origin?.client_id);
|
|
1075
|
+
const ops = promptOps(event, { myId: clientId, known: (id) => !!bubbleById(id), unmatchedMerge, wakeStart });
|
|
1069
1076
|
if (event.type === "input_merged") unmatchedMerge = false;
|
|
1070
1077
|
if (event.type === "pending_input_merged") unmatchedMerge = false;
|
|
1071
1078
|
let started = null;
|
|
1072
1079
|
for (const op of ops) {
|
|
1073
|
-
if (op.op === "
|
|
1080
|
+
if (op.op === "report") {
|
|
1081
|
+
unmatchedMerge = "report";
|
|
1082
|
+
} else if (op.op === "add") {
|
|
1074
1083
|
const bubble = makeUserBubble(op.prompt, { state: op.state, label: op.label, enqueuedId: op.enqueuedId, images: op.images });
|
|
1075
1084
|
if (op.state === "started") started = bubble;
|
|
1085
|
+
if (event.type === "pending_input_merged") wakeStart = false;
|
|
1076
1086
|
} else if (op.op === "tag") {
|
|
1077
1087
|
if (bubbleById(op.enqueuedId)) continue;
|
|
1078
1088
|
const echo = untaggedOwnBubble(op.prompt);
|
|
@@ -1653,7 +1663,7 @@ function workerGone(id, { opened = true } = {}) {
|
|
|
1653
1663
|
const stopped = stoppingId === id;
|
|
1654
1664
|
const endText = workerGoneText({ turnRunning, stopped });
|
|
1655
1665
|
if (endText) {
|
|
1656
|
-
endTurnView("gone", endText);
|
|
1666
|
+
endTurnView("gone", endText, { stopped });
|
|
1657
1667
|
if (!stopped) setLiveStatus("idle");
|
|
1658
1668
|
}
|
|
1659
1669
|
if (current.owner === "worker") current.owner = null;
|
|
@@ -1770,7 +1780,7 @@ const streamHandlers = {
|
|
|
1770
1780
|
}
|
|
1771
1781
|
},
|
|
1772
1782
|
turn_canceled: (data) => {
|
|
1773
|
-
endTurnView("canceled", cancelLineText(data.cancellation_reason || "", data.cancelled_by));
|
|
1783
|
+
endTurnView("canceled", cancelLineText(data.cancellation_reason || "", data.cancelled_by), { stopped: true });
|
|
1774
1784
|
setLiveStatus("idle");
|
|
1775
1785
|
refreshTimingFromSession();
|
|
1776
1786
|
},
|
|
@@ -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
|
}
|
|
@@ -26,12 +26,19 @@ export function noteHtml(note) {
|
|
|
26
26
|
// Who a steer names as its sender: a plugin's own label, chi's sender ids
|
|
27
27
|
// in words (the TUI's Formatting#steer_sender agrees;
|
|
28
28
|
// spec/shared/labels_matrix.json). "": no one named.
|
|
29
|
-
const STEER_SENDERS = { parent_agent: "parent agent", chi_send: "chi send", plugin_send: "plugin" };
|
|
29
|
+
const STEER_SENDERS = { parent_agent: "parent agent", chi_send: "chi send", plugin_send: "plugin", delegate_report: "delegate report" };
|
|
30
30
|
export function steerSender(source) {
|
|
31
31
|
const id = String(source ?? "");
|
|
32
32
|
return STEER_SENDERS[id] || id;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
// The label a user-role line gets on reload for who sent it, by its saved
|
|
36
|
+
// source: a delegate report (the live bubble's clientLabel of child:<id>);
|
|
37
|
+
// null for the user's own (and anything else, as before).
|
|
38
|
+
export function sourceLabel(source) {
|
|
39
|
+
return source === "delegate_report" ? steerSender(source) : null;
|
|
40
|
+
}
|
|
41
|
+
|
|
35
42
|
// A plugin's steer (a nudge put into the running turn): one line that
|
|
36
43
|
// names who nudged the model, its text collapsed under it. +bubble+: on its
|
|
37
44
|
// own between bubbles (no step holds it), not a row of a step.
|
|
@@ -122,6 +122,8 @@ 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}
|
|
@@ -236,6 +238,8 @@ button.task-stop:hover:not(:disabled){background:var(--danger-bg-dim);filter:non
|
|
|
236
238
|
.gen>summary::-webkit-details-marker{display:none}
|
|
237
239
|
.gen>summary::before{content:"▸ ";color:var(--text-faint)}
|
|
238
240
|
.gen[open]>summary::before{content:"▾ "}
|
|
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}
|
|
239
243
|
/* A step with a failed call (status error): its chevron in the danger colour. */
|
|
240
244
|
.gen.has-failed>summary::before{color:var(--danger)}
|
|
241
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}
|
|
@@ -545,6 +549,7 @@ body.stage-view #dock,body.stage-view #dock:focus-within{background:transparent;
|
|
|
545
549
|
.turn-stage[data-phase="no answer"] .ts-mark,.turn-stage[data-phase="no answer"] .ts-phase{color:var(--text-muted)}
|
|
546
550
|
.turn-stage[data-phase="waiting for you"] .ts-mark{background:var(--warn)}
|
|
547
551
|
.ts-phase{color:var(--accent);font-weight:600}
|
|
552
|
+
.ts-step:empty{display:none}
|
|
548
553
|
.ts-timing{min-width:0;overflow:hidden;text-overflow:ellipsis}
|
|
549
554
|
.ts-timing .turn-timing{margin:0;font-size:12px;color:var(--text-muted);display:inline}
|
|
550
555
|
.ts-mini{min-width:0;overflow:hidden;text-overflow:ellipsis;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;color:var(--text-2)}
|
|
@@ -579,14 +584,13 @@ body.stage-view #dock,body.stage-view #dock:focus-within{background:transparent;
|
|
|
579
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}
|
|
580
585
|
.ts-idle{flex:none;width:11px;height:11px;border:2px solid var(--border-strong);border-radius:50%}
|
|
581
586
|
.ts-tool{flex:none;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;font-weight:600;color:var(--accent)}
|
|
582
|
-
.ts-tool.exec{color:var(--think-text)}
|
|
583
|
-
.ts-tool.edit{color:var(--ok-text)}
|
|
584
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)}
|
|
585
588
|
.ts-title.quiet{font-family:inherit;font-style:italic;color:var(--text-faint)}
|
|
586
589
|
.ts-trail{height:18px;display:flex;gap:10px;overflow:hidden;font-size:12px;line-height:18px;color:var(--text-faint);white-space:nowrap}
|
|
587
590
|
.ts-trail-item{flex:none;max-width:36ch;overflow:hidden;text-overflow:ellipsis}
|
|
588
|
-
.ts-trail-item.error{color:var(--danger-text)}
|
|
591
|
+
.ts-trail-item:is(.error,.blocked){color:var(--danger-text)}
|
|
589
592
|
.ts-trail-item.warn{color:var(--warn-text)}
|
|
593
|
+
.ts-trail-item.stopped{color:var(--warn-text);opacity:.8}
|
|
590
594
|
.ts-trail-item.info,.ts-trail-item.steer{color:var(--accent)}
|
|
591
595
|
.ts-trail-item.none{font-style:italic}
|
|
592
596
|
/* A card waits: the tool row and trail give it their room. */
|
|
@@ -617,10 +621,14 @@ body.stage-view #dock,body.stage-view #dock:focus-within{background:transparent;
|
|
|
617
621
|
.ts-ticks{display:flex;flex-wrap:nowrap;justify-content:flex-end;overflow:hidden;gap:2px;flex:0 1 auto;min-width:0}
|
|
618
622
|
.ts-tick{flex:none}
|
|
619
623
|
.ts-tick{width:5px;height:12px;border-radius:1.5px;background:var(--text-faint);opacity:.55;cursor:pointer}
|
|
620
|
-
|
|
621
|
-
.ts-tick.edit{background:var(--
|
|
622
|
-
.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}
|
|
623
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}
|
|
624
632
|
.ts-tick.running{background:var(--warn);opacity:1;animation:ts-blink 1s ease-in-out infinite}
|
|
625
633
|
@keyframes ts-blink{50%{opacity:.35}}
|
|
626
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}
|
|
@@ -12,7 +12,9 @@ export function notifyState({ hasNotification, secureContext, permission, wanted
|
|
|
12
12
|
return wanted && permission === "granted" ? "on" : "off";
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
// A finished turn shorter than this was watched: no "done".
|
|
15
|
+
// A finished turn shorter than this was watched: no "done". Not a turn
|
|
16
|
+
// chi ran for a delegate child's report (last_turn origin delegate_report):
|
|
17
|
+
// nobody started it, so nobody watched it.
|
|
16
18
|
export const DONE_MIN_SECONDS = 10;
|
|
17
19
|
|
|
18
20
|
// The step-limit question (kind "continue": a turn ran out of iterations
|
|
@@ -66,7 +68,7 @@ export function attentionFor(prev, next) {
|
|
|
66
68
|
if (next.parent_id) return null;
|
|
67
69
|
const key = `${next.id}:${turn.ended_at}`;
|
|
68
70
|
if (turn.outcome === "failed") return { reason: "failed", sessionId: next.id, key };
|
|
69
|
-
if (turn.outcome === "completed" && Number(turn.seconds) >= DONE_MIN_SECONDS) {
|
|
71
|
+
if (turn.outcome === "completed" && (Number(turn.seconds) >= DONE_MIN_SECONDS || turn.origin === "delegate_report")) {
|
|
70
72
|
return { reason: "done", sessionId: next.id, key };
|
|
71
73
|
}
|
|
72
74
|
return null;
|
|
@@ -76,6 +76,20 @@ export function toolKind(name) {
|
|
|
76
76
|
return "other";
|
|
77
77
|
}
|
|
78
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
|
+
|
|
79
93
|
function rowTitle(row) {
|
|
80
94
|
return row.title || row.params || "";
|
|
81
95
|
}
|
|
@@ -116,6 +130,9 @@ const NO_ANSWER = "no answer";
|
|
|
116
130
|
// else "…";
|
|
117
131
|
// tool the running call ({name, title, kind}, and command: its full
|
|
118
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);
|
|
119
136
|
// trail the last three finished calls, newest last (a file's title
|
|
120
137
|
// without its dir; command as for tool);
|
|
121
138
|
// ticks one per call: {kind, status, step (the gen's index)}.
|
|
@@ -146,6 +163,7 @@ export function liveSlots(turn, { narration = "", thinking = "", pending = false
|
|
|
146
163
|
phase,
|
|
147
164
|
step: ended ? gens.filter((g) => !g.answer || g.thinking || g.tools.length).length : gens.length,
|
|
148
165
|
headline,
|
|
166
|
+
quiet: ended ? "" : current?.text && !current.textDone ? "writing…" : "thinking…",
|
|
149
167
|
tool: running ? withTask(withCommand({ name: toolName(running), title: rowTitle(running), kind: toolKind(running.tool) }, running), running) : null,
|
|
150
168
|
trail,
|
|
151
169
|
ticks: rows.map(({ row, step }) => ({ kind: toolKind(row.tool), status: row.status, step })),
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// reload. Slots are patched in place, never rebuilt from HTML: a selection
|
|
12
12
|
// and annotate survive. The rules are pure in stage_model.js.
|
|
13
13
|
import { createTurnView } from "./turn_view.js";
|
|
14
|
-
import { flashOf, handOffDue, handOffOrder, headlineOf, inUseFrom, isPlain, liveSlots, placeFor } from "./stage_model.js";
|
|
14
|
+
import { flashOf, handOffDue, handOffOrder, headlineOf, inUseFrom, isPlain, liveSlots, placeFor, trailMark, trailTone } from "./stage_model.js";
|
|
15
15
|
import { formatDuration } from "./timing.js";
|
|
16
16
|
import { taskCommand, taskStopButton } from "./task_stop.js";
|
|
17
17
|
|
|
@@ -189,20 +189,26 @@ export function createStageView({ historyEl, appendToHistory, isNearBottom: hist
|
|
|
189
189
|
stage.dataset.phase = slots.phase;
|
|
190
190
|
mark.textContent = MARKS[slots.phase] || "";
|
|
191
191
|
phaseEl.textContent = slots.phase;
|
|
192
|
-
|
|
192
|
+
const steps = `${slots.step} step${slots.step === 1 ? "" : "s"}`;
|
|
193
193
|
|
|
194
194
|
drawHeadline(slots.headline);
|
|
195
|
-
drawNow(slots.tool,
|
|
195
|
+
drawNow(slots.tool, slots.quiet);
|
|
196
196
|
drawTrail(slots.trail);
|
|
197
197
|
drawTicks(slots.ticks);
|
|
198
198
|
const calls = slots.ticks.length;
|
|
199
|
-
|
|
200
|
-
|
|
199
|
+
// An ended turn says each thing once: its steps in the chip (the label
|
|
200
|
+
// of what the cloud holds), its time in the status row's timing line
|
|
201
|
+
// (the one that moves into the history). The chip keeps the time when
|
|
202
|
+
// that line is elsewhere (a turn the stage didn't hold from its start).
|
|
203
|
+
const elapsed = endKind && !timingSlot.firstChild ? formatDuration(endedAt - startedAt) : "";
|
|
204
|
+
chipLabel.textContent = `${steps} · ${calls} tool call${calls === 1 ? "" : "s"}${elapsed ? ` · ${elapsed}` : ""}`;
|
|
201
205
|
miniTool.textContent = collapsed && slots.tool ? `${slots.tool.name} ${slots.tool.title}` : "";
|
|
202
206
|
miniTool.title = (collapsed && slots.tool?.command) || "";
|
|
203
207
|
|
|
204
208
|
live.hidden = plain || !!endKind;
|
|
205
209
|
cloud.hidden = !blockEl || blockEl.parentNode !== cloudBody;
|
|
210
|
+
// Running: the step it is on. Ended: the count, unless the chip says it.
|
|
211
|
+
stepEl.textContent = !endKind ? `step ${slots.step}` : cloud.hidden ? steps : "";
|
|
206
212
|
promptSlot.hidden = !promptSlot.firstChild;
|
|
207
213
|
extras.hidden = !extras.firstChild;
|
|
208
214
|
answer.hidden = !answer.firstChild;
|
|
@@ -225,8 +231,7 @@ export function createStageView({ historyEl, appendToHistory, isNearBottom: hist
|
|
|
225
231
|
headline.classList.add("fresh");
|
|
226
232
|
}
|
|
227
233
|
|
|
228
|
-
function drawNow(tool,
|
|
229
|
-
const quiet = gen?.text && !gen.textDone ? "writing…" : "thinking…";
|
|
234
|
+
function drawNow(tool, quiet) {
|
|
230
235
|
// A task_wait's stop button, as the row's (task_stop.js): in the key
|
|
231
236
|
// with its in-flight state, so a redraw keeps a clicked one disabled.
|
|
232
237
|
const stopId = tool?.taskId && taskStop?.target({ tool: "task_wait", status: "running", view: { task_id: tool.taskId } });
|
|
@@ -248,7 +253,7 @@ export function createStageView({ historyEl, appendToHistory, isNearBottom: hist
|
|
|
248
253
|
const command = () => taskCommand(view.current().turn?.gens, stopId);
|
|
249
254
|
now.appendChild(taskStopButton(taskStop, stopId, { command, onChange: scheduleRefresh }));
|
|
250
255
|
}
|
|
251
|
-
} else {
|
|
256
|
+
} else if (quiet) {
|
|
252
257
|
const word = node("span", "ts-title quiet", now);
|
|
253
258
|
word.textContent = quiet;
|
|
254
259
|
}
|
|
@@ -262,9 +267,9 @@ export function createStageView({ historyEl, appendToHistory, isNearBottom: hist
|
|
|
262
267
|
trail.replaceChildren();
|
|
263
268
|
if (flashing) node("span", `ts-trail-item ${flashing.kind}`, trail).textContent = `${FLASH_MARKS[flashing.kind]} ${flashing.text}`;
|
|
264
269
|
for (const item of items) {
|
|
265
|
-
const
|
|
266
|
-
const el = node("span", `ts-trail-item${
|
|
267
|
-
el.textContent = `${
|
|
270
|
+
const tone = trailTone(item.status);
|
|
271
|
+
const el = node("span", `ts-trail-item${tone ? ` ${tone}` : ""}`, trail);
|
|
272
|
+
el.textContent = `${trailMark(item.status)} ${item.name} ${item.title}`.trim();
|
|
268
273
|
if (item.command) el.title = item.command;
|
|
269
274
|
}
|
|
270
275
|
if (!flashing && !items.length) node("span", "ts-trail-item none", trail).textContent = "no tool calls yet";
|