samagotchi 0.149.0 → 0.150.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +48 -1
- data/README.md +4 -2
- data/docs/architecture.md +2 -2
- data/docs/cli.md +59 -24
- data/docs/configuration.md +5 -5
- data/docs/internals/background-tasks.md +1 -1
- data/docs/internals/bridge.md +8 -0
- data/docs/internals/bundles.md +42 -14
- data/docs/internals/objectives.md +7 -2
- data/docs/internals/web-frontend.md +1 -1
- data/docs/internals/web-push.md +2 -2
- data/docs/internals/working-set.md +5 -2
- data/docs/memory.md +47 -9
- data/docs/plugins.md +7 -1
- data/docs/sessions.md +5 -4
- data/docs/testing.md +5 -0
- data/lib/samagotchi/atomic_file.rb +3 -0
- data/lib/samagotchi/bootstrap_command.rb +5 -2
- data/lib/samagotchi/bridge/card_store.rb +52 -7
- data/lib/samagotchi/bridge.rb +1 -1
- data/lib/samagotchi/bundle_command.rb +91 -68
- data/lib/samagotchi/bundler_env.rb +24 -0
- data/lib/samagotchi/bundles/dev/manifest.yml +2 -2
- data/lib/samagotchi/bundles/github-pr/manifest.yml +3 -3
- data/lib/samagotchi/bundles/github-pr/plugin.rb +2 -1
- data/lib/samagotchi/bundles/known-names/hooks/known_names.rb +3 -1
- data/lib/samagotchi/bundles/known-names/manifest.yml +3 -2
- data/lib/samagotchi/bundles/source-links/hooks/source_links.rb +2 -2
- data/lib/samagotchi/bundles/source-links/manifest.yml +3 -3
- data/lib/samagotchi/bundles/system/manifest.yml +3 -3
- data/lib/samagotchi/bundles/system/memory_guide.md +5 -5
- data/lib/samagotchi/bundles/system/self_map.md +1 -1
- data/lib/samagotchi/chi_home.rb +4 -2
- data/lib/samagotchi/chi_shim.rb +2 -1
- data/lib/samagotchi/child_env.rb +10 -3
- data/lib/samagotchi/engine.rb +1 -1
- data/lib/samagotchi/git_env.rb +43 -0
- data/lib/samagotchi/guardrails/context.rb +2 -0
- data/lib/samagotchi/guardrails/targets.rb +4 -1
- data/lib/samagotchi/memory_bundle/asset_installer.rb +16 -2
- data/lib/samagotchi/memory_bundle/file_standing.rb +167 -0
- data/lib/samagotchi/memory_bundle/installed_bundle.rb +62 -1
- data/lib/samagotchi/memory_bundle/installer.rb +161 -52
- data/lib/samagotchi/memory_bundle/listing.rb +4 -3
- data/lib/samagotchi/memory_bundle/merger.rb +209 -8
- data/lib/samagotchi/memory_bundle/placeholder.rb +2 -1
- data/lib/samagotchi/memory_bundle/profile.rb +7 -3
- data/lib/samagotchi/memory_bundle/provenance.rb +178 -20
- data/lib/samagotchi/memory_bundle/remerge.rb +167 -0
- data/lib/samagotchi/memory_bundle/shipped_history.rb +79 -0
- data/lib/samagotchi/memory_bundle/shipped_update.rb +89 -24
- data/lib/samagotchi/memory_bundle/source.rb +3 -3
- data/lib/samagotchi/memory_bundle/status.rb +21 -3
- data/lib/samagotchi/memory_bundle/system_bundle.rb +98 -33
- data/lib/samagotchi/memory_bundle/text_merge.rb +399 -0
- data/lib/samagotchi/memory_bundle/trash.rb +58 -2
- data/lib/samagotchi/memory_bundle/uninstaller.rb +26 -6
- data/lib/samagotchi/objectives_check.rb +37 -7
- data/lib/samagotchi/project_env.rb +3 -2
- data/lib/samagotchi/self_report.rb +2 -0
- data/lib/samagotchi/testing/boundary.rb +1 -0
- data/lib/samagotchi/testing/minitest.rb +3 -0
- data/lib/samagotchi/testing/rspec.rb +3 -0
- data/lib/samagotchi/testing/sandbox.rb +3 -1
- data/lib/samagotchi/tools/delegate.rb +2 -1
- data/lib/samagotchi/tools/task_runtime.rb +13 -10
- data/lib/samagotchi/turn_flow.rb +3 -1
- data/lib/samagotchi/update_command.rb +76 -28
- data/lib/samagotchi/update_hint.rb +4 -1
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/auto_update.rb +8 -8
- data/lib/samagotchi/web/auto_update_record.rb +6 -4
- data/lib/samagotchi/web/participant_view.rb +1 -1
- data/lib/samagotchi/web/public/app.js +5 -3
- data/lib/samagotchi/web/public/block_open.js +13 -0
- data/lib/samagotchi/web/public/bubbles.js +4 -2
- data/lib/samagotchi/web/public/cards_place.js +10 -5
- data/lib/samagotchi/web/public/details_motion.js +2 -1
- data/lib/samagotchi/web/public/index.html +18 -14
- data/lib/samagotchi/web/public/stage_model.js +5 -6
- data/lib/samagotchi/web/public/stage_view.js +8 -7
- data/lib/samagotchi/web/public/turn_block.js +34 -19
- data/lib/samagotchi/web/public/turn_model.js +1 -1
- data/lib/samagotchi/web/public/update_sheet.js +8 -3
- data/lib/samagotchi/web/public/working_set_view.js +10 -7
- data/lib/samagotchi/web/push/notifier.rb +7 -9
- data/lib/samagotchi/web/session_hub.rb +2 -1
- data/lib/samagotchi/web/update_ops.rb +2 -1
- metadata +8 -1
|
@@ -197,7 +197,7 @@ module Samagotchi
|
|
|
197
197
|
end
|
|
198
198
|
|
|
199
199
|
# A newer chi this didn't install, settled: a run of its own from here.
|
|
200
|
-
# The terminal's table already told what `chi update`
|
|
200
|
+
# The terminal's table already told what `chi update` merged or left behind.
|
|
201
201
|
def adopt_installed
|
|
202
202
|
return unless @restart == "idle" && @seen
|
|
203
203
|
|
|
@@ -215,10 +215,10 @@ module Samagotchi
|
|
|
215
215
|
return Log.info(:web, "auto_update_deferred", version: version, why: result) if result.is_a?(Symbol)
|
|
216
216
|
|
|
217
217
|
rows = result.rows
|
|
218
|
-
record = AutoUpdateRecord.new(version: version, from: VERSION, at: @clock.call.iso8601, kept:
|
|
219
|
-
phase: "installed")
|
|
218
|
+
record = AutoUpdateRecord.new(version: version, from: VERSION, at: @clock.call.iso8601, kept: files_in(rows, "kept"),
|
|
219
|
+
merged: files_in(rows, "merged"), phase: "installed")
|
|
220
220
|
if result.done?
|
|
221
|
-
Log.info(:web, "auto_update_installed", version: version, kept: record.kept.size)
|
|
221
|
+
Log.info(:web, "auto_update_installed", version: version, kept: record.kept.size, merged: record.merged.size)
|
|
222
222
|
installed(record)
|
|
223
223
|
else
|
|
224
224
|
failed(record, rows)
|
|
@@ -232,7 +232,7 @@ module Samagotchi
|
|
|
232
232
|
return carry_on(save(record)) unless @restart == "never"
|
|
233
233
|
|
|
234
234
|
save(record.with(phase: "done", pushed: true))
|
|
235
|
-
@hub.publish_notice(kind: "installed", version: record.version, kept: record.kept.size)
|
|
235
|
+
@hub.publish_notice(kind: "installed", version: record.version, kept: record.kept.size, merged: record.merged.size)
|
|
236
236
|
end
|
|
237
237
|
|
|
238
238
|
# The next step of a run on record.
|
|
@@ -328,7 +328,7 @@ module Samagotchi
|
|
|
328
328
|
record = restart_workers(record) || record
|
|
329
329
|
Log.info(:web, "auto_update_done", version: record.version, restarted: record.restarted, left: record.left)
|
|
330
330
|
save(record.with(phase: record.left_to_try?(@clock.call, RETRY_FOR) ? RETRYING : "done", pushed: true))
|
|
331
|
-
@hub.publish_notice(kind: "updated", version: record.version, kept: record.kept.size,
|
|
331
|
+
@hub.publish_notice(kind: "updated", version: record.version, kept: record.kept.size, merged: record.merged.size,
|
|
332
332
|
restarted: record.restarted, left: record.left)
|
|
333
333
|
end
|
|
334
334
|
|
|
@@ -366,8 +366,8 @@ module Samagotchi
|
|
|
366
366
|
installed: gem_installed)
|
|
367
367
|
end
|
|
368
368
|
|
|
369
|
-
def
|
|
370
|
-
rows.flat_map { |row| Array(row[
|
|
369
|
+
def files_in(rows, field)
|
|
370
|
+
rows.flat_map { |row| Array(row[field]).map(&:to_s) }.uniq
|
|
371
371
|
end
|
|
372
372
|
end
|
|
373
373
|
end
|
|
@@ -13,8 +13,10 @@ module Samagotchi
|
|
|
13
13
|
# +phase+: installed, workers, web (AutoUpdate::OPEN_PHASES), retrying,
|
|
14
14
|
# done or failed. +restarted+ and +left+ count the stale workers (`workers` in
|
|
15
15
|
# the file); +at+ is when the install ended, an ISO 8601 string.
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
# +kept+ and +merged+ name the edited memory files that stayed behind and
|
|
17
|
+
# that took the update (chi update --json's).
|
|
18
|
+
AutoUpdateRecord = Data.define(:version, :from, :at, :kept, :merged, :phase, :restarted, :left, :detail, :pushed) do
|
|
19
|
+
def initialize(version:, phase:, from: nil, at: nil, kept: [], merged: [], restarted: 0, left: 0, detail: nil, pushed: false)
|
|
18
20
|
super
|
|
19
21
|
end
|
|
20
22
|
|
|
@@ -30,14 +32,14 @@ module Samagotchi
|
|
|
30
32
|
|
|
31
33
|
workers = hash["workers"].is_a?(Hash) ? hash["workers"] : {}
|
|
32
34
|
new(version: version, phase: phase, from: hash["from"], at: hash["at"],
|
|
33
|
-
kept: Array(hash["kept"]).grep(String), restarted: workers["restarted"].to_i, left: workers["left"].to_i,
|
|
35
|
+
kept: Array(hash["kept"]).grep(String), merged: Array(hash["merged"]).grep(String), restarted: workers["restarted"].to_i, left: workers["left"].to_i,
|
|
34
36
|
detail: hash["detail"], pushed: hash["pushed"] == true)
|
|
35
37
|
end
|
|
36
38
|
|
|
37
39
|
# What goes in the file, for .from_h to read back.
|
|
38
40
|
# @return [Hash{String => Object}]
|
|
39
41
|
def to_store
|
|
40
|
-
{ "version" => version, "from" => from, "at" => at, "kept" => kept, "phase" => phase,
|
|
42
|
+
{ "version" => version, "from" => from, "at" => at, "kept" => kept, "merged" => merged, "phase" => phase,
|
|
41
43
|
"workers" => { "restarted" => restarted, "left" => left }, "detail" => detail,
|
|
42
44
|
"pushed" => pushed ? true : nil }.compact
|
|
43
45
|
end
|
|
@@ -85,7 +85,7 @@ module Samagotchi
|
|
|
85
85
|
llm_context_edited merged_input objectives_updated participants_changed pending_input_merged plugin_init_finished
|
|
86
86
|
plugin_init_started plugin_init_wait prompt_restored question question_relay reasoning_history_refused
|
|
87
87
|
recap_ready reminder_injected sampling_refused session_end session_start side_usage steer_cut thinking_dropped
|
|
88
|
-
thinking_refused tool_dispatch_completed tool_dispatch_started used_memories_updated working_set_updated
|
|
88
|
+
thinking_refused tool_dispatch_completed tool_dispatch_started turn_rolled_back used_memories_updated working_set_updated
|
|
89
89
|
].freeze
|
|
90
90
|
|
|
91
91
|
# A merged input line's saved sources a participant sees: a person's
|
|
@@ -1188,8 +1188,9 @@ function renderSessionView(data, { kept = [] } = {}) {
|
|
|
1188
1188
|
restoreCommandBubbles(historyEl, kept);
|
|
1189
1189
|
// A failed turn's prompt went back to the user: shown after the
|
|
1190
1190
|
// conversation, marked failed, as it was live (prompt_restored).
|
|
1191
|
-
|
|
1192
|
-
|
|
1191
|
+
const failedPrompt = data.failed_turn ? makeUserBubble(data.failed_turn.prompt, { state: "failed" }) : null;
|
|
1192
|
+
if (failedPrompt) {
|
|
1193
|
+
failedPrompt.dataset.failedTurn = "1";
|
|
1193
1194
|
addEndBubble(failedTurnText(data.failed_turn));
|
|
1194
1195
|
}
|
|
1195
1196
|
seedSeenFromDom();
|
|
@@ -1203,7 +1204,8 @@ function renderSessionView(data, { kept = [] } = {}) {
|
|
|
1203
1204
|
// A live worker's last cards and between-turns notices, where they
|
|
1204
1205
|
// arrived; the running turn's go into its step once it is drawn.
|
|
1205
1206
|
const snapshotCards = splitSnapshotCards(data.cards, pageCapabilities);
|
|
1206
|
-
|
|
1207
|
+
// Above a failed turn's prompt: it came after them, and it is no turn.
|
|
1208
|
+
placeSnapshotCards(snapshotCards.placed, { before: failedPrompt });
|
|
1207
1209
|
// The recap saved with the session (also a stopped one's), else a live
|
|
1208
1210
|
// worker's from before saved_recap existed. After the cards: the last
|
|
1209
1211
|
// turn's check-in notice goes to the end, and the recap closes the page.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Whether a finished turn's steps block stays open: a completed turn's closes
|
|
2
|
+
// to its summary line, whatever its steps said (opened, it shows the outline).
|
|
3
|
+
// A canceled or failed turn with an outline stays open: its last narrated step
|
|
4
|
+
// is the only text the reader gets. A "gone" end (the worker left) counts as
|
|
5
|
+
// completed: its partial answer bubble carries the text, and a reload, which
|
|
6
|
+
// can't tell it from one, draws the same. The live end, the stage's hand-off
|
|
7
|
+
// and a reload all ask here.
|
|
8
|
+
//
|
|
9
|
+
// A module of its own: static JS is cached at unversioned URLs, and a new
|
|
10
|
+
// export on an old module would blank the page.
|
|
11
|
+
export function openAfterEnd(kind, outlined) {
|
|
12
|
+
return !!outlined && (kind === "canceled" || kind === "failed");
|
|
13
|
+
}
|
|
@@ -26,8 +26,10 @@ export function removeQueuedBubble(bubble) {
|
|
|
26
26
|
if (slot && slot.childElementCount === 0) slot.remove();
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
// A prompt that started a turn: not a line steered into a running one
|
|
30
|
-
|
|
29
|
+
// A prompt that started a turn: not a line steered into a running one, nor a
|
|
30
|
+
// failed turn's prompt, drawn again after the history (it went back to its
|
|
31
|
+
// sender: the session and the turn count have no such turn).
|
|
32
|
+
export const TURN_PROMPT = ".bubble.user:not(.steered):not(.failed)";
|
|
31
33
|
|
|
32
34
|
// The history's turn prompts, oldest first (what turns_since counts).
|
|
33
35
|
export function turnPromptBubbles(historyEl) {
|
|
@@ -46,7 +46,7 @@ export function turnAnswerEl(turnEls) {
|
|
|
46
46
|
// (bubbles.js createBubbles)
|
|
47
47
|
// @param followWokenWorker () follows the worker a card's command woke
|
|
48
48
|
// @param api {getSession, sendCommand} (data.js)
|
|
49
|
-
// @return {cardElement(id), upsertCard(card, {before}), placeSnapshotCards(entries),
|
|
49
|
+
// @return {cardElement(id), upsertCard(card, {before}), placeSnapshotCards(entries, {before}),
|
|
50
50
|
// refreshCardBodies(), onCardAction(event)}
|
|
51
51
|
const HELD_UNTIL_ANSWERED = /^\/objectives --card /;
|
|
52
52
|
|
|
@@ -63,9 +63,12 @@ export function createCardsPlace({
|
|
|
63
63
|
// running turn's card as a row of its step; else before +before+ or at the
|
|
64
64
|
// end of the history). A running turn's card with actions asks the user
|
|
65
65
|
// something (check-in's Nudge / Keep going / Stop): it goes under the turn's
|
|
66
|
-
// block, not into a step that collapses when the next one starts.
|
|
66
|
+
// block, not into a step that collapses when the next one starts. A turn
|
|
67
|
+
// still sliding away arrives first: its list of pieces holds this card's
|
|
68
|
+
// element, and an update may swap that (drawCard, a notice to a card).
|
|
67
69
|
function upsertCard(card, { before = null } = {}) {
|
|
68
70
|
if (!card || !card.id) return;
|
|
71
|
+
turnDom.settleFold?.();
|
|
69
72
|
const existing = cardElement(card.id);
|
|
70
73
|
if (existing) {
|
|
71
74
|
const drawn = drawCard(existing, card);
|
|
@@ -81,11 +84,13 @@ export function createCardsPlace({
|
|
|
81
84
|
}
|
|
82
85
|
|
|
83
86
|
// A snapshot's cards and notices, each above the first turn after it; a
|
|
84
|
-
// turn's own notice where it showed live (placeTurnNotice).
|
|
85
|
-
|
|
87
|
+
// turn's own notice where it showed live (placeTurnNotice). +end+: what
|
|
88
|
+
// closes the history below them (a failed turn's prompt), for those with
|
|
89
|
+
// no turn after them.
|
|
90
|
+
function placeSnapshotCards(entries, { before: end = null } = {}) {
|
|
86
91
|
const users = turnPromptBubbles(historyEl);
|
|
87
92
|
for (const entry of entries) {
|
|
88
|
-
const before = cardPlace(users, entry);
|
|
93
|
+
const before = cardPlace(users, entry) || end;
|
|
89
94
|
if (isCard(entry)) upsertCard(entry, { before });
|
|
90
95
|
else if (entry.type === "question") placeResolvedQuestion(entry, before);
|
|
91
96
|
else if (entry.type === "guardrail_warning") bubbles.showGuardrailWarning(entry.message, entry.label || "guardrails", { before });
|
|
@@ -56,7 +56,8 @@ export function installDetailsMotion({ root, selector, motion = canAnimate }) {
|
|
|
56
56
|
const summary = e.target.closest?.("summary");
|
|
57
57
|
const details = summary?.parentElement;
|
|
58
58
|
if (!details || !details.matches?.(selector) || summary.parentElement !== details) return;
|
|
59
|
-
|
|
59
|
+
// A live part of the summary (a steps block's footprint opens its own view).
|
|
60
|
+
if (e.target.closest("a, button, input, label, .ws-foot") !== null && e.target !== summary) return;
|
|
60
61
|
if (!motion(details)) return;
|
|
61
62
|
e.preventDefault();
|
|
62
63
|
if (busy.has(details)) return;
|
|
@@ -346,10 +346,16 @@ button.system-one-wrong,.system-one-wrong.marked{flex:none;padding:1px 6px;borde
|
|
|
346
346
|
.thinking-body{margin-top:6px;font-size:13px;color:var(--text-3);white-space:pre-wrap;word-break:break-word;font-style:italic;max-height:240px;overflow:auto}
|
|
347
347
|
/* The turn block (the stage's cloud, the history): one block per turn, its steps as collapsed rows, the live one open on top. */
|
|
348
348
|
.turn-work{border:1px solid var(--border);border-left:3px solid var(--accent);border-radius:12px;background:var(--bg-panel);padding:8px 12px;font-size:14px}
|
|
349
|
-
.turn-work>summary{cursor:pointer;color:var(--text-muted);font-size:13px;font-weight:600;user-select:none;list-style:none}
|
|
349
|
+
.turn-work>summary{position:relative;padding-left:13px;cursor:pointer;color:var(--text-muted);font-size:13px;font-weight:600;user-select:none;list-style:none}
|
|
350
350
|
.turn-work>summary::-webkit-details-marker{display:none}
|
|
351
|
-
.turn-work>summary::before{content:"▸
|
|
352
|
-
|
|
351
|
+
.turn-work>summary::before{content:"▸";position:absolute;left:0;color:var(--accent)}
|
|
352
|
+
/* The summary's line: flex on this span, not on <summary> (WebKit's flex on a summary has had quirks). The chevron and the steps toggle the block, the footprint opens the turn's working set (working_set_view.js footClick). */
|
|
353
|
+
.tw-sum{display:flex;align-items:baseline;gap:10px;min-width:0}
|
|
354
|
+
/* The count gives way before the footprint, which is what the line is for. */
|
|
355
|
+
.ws-foot~.turn-steps{margin-left:auto;flex:0 100 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;font-size:12px;font-weight:500;color:var(--text-muted);white-space:nowrap}
|
|
356
|
+
/* A phone has no count to tap, and the chevron is then the only way to the steps: its hit area spreads over the block's padding, the glyph stays where it is. */
|
|
357
|
+
@media (max-width:600px){.ws-foot~.turn-steps{display:none}.turn-work>summary::before{left:-12px;top:-8px;width:36px;box-sizing:border-box;padding:8px 0 8px 12px}}
|
|
358
|
+
.turn-work[open]>summary::before{content:"▾"}
|
|
353
359
|
/* Closed, a block keeps its box (the bar, the border, the width): a click changes its height only (details_motion.js); thinking and the recap the same. */
|
|
354
360
|
/* A step's body (thinking, narration, rows, cards) sits --step-indent right of its summary, so the thinking
|
|
355
361
|
chevron and the row marks share one column under the step's own chevron. The summary takes it back with a
|
|
@@ -399,11 +405,11 @@ button.system-one-wrong,.system-one-wrong.marked{flex:none;padding:1px 6px;borde
|
|
|
399
405
|
.gen:not(.live) .gen-text{font-size:14px;color:var(--text-2)}
|
|
400
406
|
.gen-text:empty{display:none}
|
|
401
407
|
.gen .activity-body{margin-top:6px}
|
|
402
|
-
/* One gen, no thinking, no tool rows: nothing to stack yet, so the narration streams like a plain answer bubble. */
|
|
403
|
-
.turn-work:not(:has(.gen~.gen,.thinking-body,.activity-body)){border:0;padding:0;background:transparent}
|
|
404
|
-
.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}
|
|
405
|
-
.turn-work:not(:has(.gen~.gen,.thinking-body,.activity-body)) .gen{margin:0;padding:0;border:0;background:transparent}
|
|
406
|
-
.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}
|
|
408
|
+
/* One gen, no thinking, no tool rows: nothing to stack yet, so the narration streams like a plain answer bubble. A step that closed with text (a line steered in after it, its answer gone from the block) is a row: its summary is the way to its text. */
|
|
409
|
+
.turn-work:not(:has(.gen~.gen,.thinking-body,.activity-body,.gen:not(.live)>.gen-text)){border:0;padding:0;background:transparent}
|
|
410
|
+
.turn-work:not(:has(.gen~.gen,.thinking-body,.activity-body,.gen:not(.live)>.gen-text))>summary,.turn-work:not(:has(.gen~.gen,.thinking-body,.activity-body,.gen:not(.live)>.gen-text)) .gen>summary{display:none}
|
|
411
|
+
.turn-work:not(:has(.gen~.gen,.thinking-body,.activity-body,.gen:not(.live)>.gen-text)) .gen{margin:0;padding:0;border:0;background:transparent}
|
|
412
|
+
.turn-work:not(:has(.gen~.gen,.thinking-body,.activity-body,.gen:not(.live)>.gen-text)) .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}
|
|
407
413
|
@media(max-width:600px){.gen{--step-indent:8px}}
|
|
408
414
|
/* 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. */
|
|
409
415
|
.bubble.question{border-left:3px solid var(--warn);background:color-mix(in srgb,var(--warn) 7%,var(--glass-bg));padding:10px 12px}
|
|
@@ -978,11 +984,11 @@ body.working-set .ts-live{position:relative;padding-right:264px}
|
|
|
978
984
|
}
|
|
979
985
|
body.working-set .turn-stage.waiting .ts-live{min-height:calc(var(--ts-live-h) + var(--ws-row-h,0px))}
|
|
980
986
|
body.working-set .turn-stage.held-wait:not(.waiting) .ts-live{min-height:max(calc(var(--ts-live-h) + var(--ws-row-h,0px)),var(--live-held,0px))}
|
|
981
|
-
/* A finished turn's footprint,
|
|
982
|
-
.ws-foot{
|
|
983
|
-
@media(hover:hover){.ws-foot:hover{
|
|
987
|
+
/* A finished turn's footprint, its block's summary line (working_set_view.js): a click opens its list. */
|
|
988
|
+
.ws-foot{flex:0 1 auto;min-width:0;padding:0 6px;border-radius:6px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12.5px;font-weight:500;color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;cursor:pointer}
|
|
989
|
+
@media(hover:hover){.ws-foot:hover{background:var(--bg-hover)}}
|
|
984
990
|
.ws-foot:focus-visible{outline:2px solid var(--accent-ring);outline-offset:1px}
|
|
985
|
-
.ws-foot .w{font-family:ui-sans-serif,system-ui,sans-serif}
|
|
991
|
+
.ws-foot .w{font-family:ui-sans-serif,system-ui,sans-serif;font-weight:600;color:var(--text-muted)}
|
|
986
992
|
.ws-foot .sep{color:var(--border-strong)}
|
|
987
993
|
.ws-foot .e{color:var(--accent)}
|
|
988
994
|
.ws-foot .k{color:var(--think-text)}
|
|
@@ -1327,8 +1333,6 @@ body.has-session.composer-folded #infoBar{border-top-color:transparent}
|
|
|
1327
1333
|
.us-row-head span{flex:1 1 0;min-width:0;text-align:right;color:var(--text-2);font-size:12px;overflow-wrap:anywhere}
|
|
1328
1334
|
.us-detail{margin-top:4px;color:var(--text-muted);font-size:12px;overflow-wrap:anywhere}
|
|
1329
1335
|
.us-kept{margin-top:6px;color:var(--warn-text);font-size:12px}
|
|
1330
|
-
.us-kept ul{margin:3px 0 0;padding-left:18px;color:var(--text-2)}
|
|
1331
|
-
.us-kept li{overflow-wrap:anywhere}
|
|
1332
1336
|
.us-foot{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:8px;margin-top:10px}
|
|
1333
1337
|
@media(max-width:600px){#updateSheet{position:fixed;inset:auto 8px 8px;width:auto;max-width:none;max-height:min(80dvh,calc(100dvh - 16px));margin:0}}
|
|
1334
1338
|
.ctx-pop-kind{font-size:11px;color:var(--text-muted)}
|
|
@@ -226,12 +226,11 @@ export function handOffDue({ ended, pending, inUse, quietSinceMs, nowMs }) {
|
|
|
226
226
|
}
|
|
227
227
|
|
|
228
228
|
// A handed-off turn's nodes in the order the turn block leaves them: the
|
|
229
|
-
// prompt bubble (none for a reminder turn), the block,
|
|
230
|
-
//
|
|
231
|
-
//
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
return [prompt, block, footprint, ...extras, ...answer, timing, end, ...kept, ...queued].filter(Boolean);
|
|
229
|
+
// prompt bubble (none for a reminder turn), the block, the extras in arrival
|
|
230
|
+
// order, the answer's pieces, the timing line, the end line, the kept cards,
|
|
231
|
+
// then the lines queued for after the turn (they run next).
|
|
232
|
+
export function handOffOrder({ prompt = null, block = null, extras = [], answer = [], timing = null, end = null, kept = [], queued = [] }) {
|
|
233
|
+
return [prompt, block, ...extras, ...answer, timing, end, ...kept, ...queued].filter(Boolean);
|
|
235
234
|
}
|
|
236
235
|
|
|
237
236
|
// The rows that wait for the running turn's end to run as their own: a
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
// stage_frame.js's, and the in-use watch that times the hand-off is
|
|
16
16
|
// stage_watch.js's.
|
|
17
17
|
import { createTurnBlock } from "./turn_block.js";
|
|
18
|
+
import { openAfterEnd } from "./block_open.js";
|
|
18
19
|
import { chipLabelText, flashOf, handOffDue, handOffOrder, headlineOf, isPlain, liveSlots, MARKS, miniToolText, placeFor, placeKind, QUEUED_CHILDREN, stepSlotText } from "./stage_model.js";
|
|
19
20
|
import { createStageSlots, node } from "./stage_slots.js";
|
|
20
21
|
import { createStageFrame, reducedMotion } from "./stage_frame.js";
|
|
@@ -687,7 +688,7 @@ export function createStageView({
|
|
|
687
688
|
// the pieces arrive in the history and it follows a reader who was at its
|
|
688
689
|
// end. Not with +immediate+ (another turn takes the stage at once), a
|
|
689
690
|
// reduced motion, a hidden page or a collapsed stage: all at once.
|
|
690
|
-
let folding = null; // { pieces, block, wasAtEnd, animate } while it slides
|
|
691
|
+
let folding = null; // { pieces, block, endKind, wasAtEnd, animate } while it slides
|
|
691
692
|
function handOff({ immediate = false } = {}) {
|
|
692
693
|
if (folding && immediate) finishFold();
|
|
693
694
|
if (!holding || !endKind) return;
|
|
@@ -695,12 +696,12 @@ export function createStageView({
|
|
|
695
696
|
view.chronological();
|
|
696
697
|
const block = blockEl && blockEl.parentNode === cloudBody ? blockEl : null;
|
|
697
698
|
// The turn's set joins the page's turns (a plain one's too: the turns
|
|
698
|
-
// keep their places); its footprint
|
|
699
|
+
// keep their places); its footprint takes the block's summary line.
|
|
699
700
|
const footprint = workingSet ? workingSet.handOff(view.current().turn, promptWords()) : null;
|
|
701
|
+
if (block && footprint) block.querySelector(":scope > summary > .tw-sum")?.prepend(footprint);
|
|
700
702
|
const pieces = handOffOrder({
|
|
701
703
|
prompt: promptSlot.firstElementChild,
|
|
702
704
|
block,
|
|
703
|
-
footprint: block ? footprint : null,
|
|
704
705
|
extras: [...extras.children],
|
|
705
706
|
answer: [...answer.children],
|
|
706
707
|
timing: timingSlot.firstElementChild,
|
|
@@ -708,7 +709,7 @@ export function createStageView({
|
|
|
708
709
|
queued: [...queuedSlot.children],
|
|
709
710
|
});
|
|
710
711
|
const animate = !immediate && !reducedMotion() && !document.hidden && !collapsed;
|
|
711
|
-
folding = { pieces, block: blockEl, wasAtEnd: historyNearBottom(), animate };
|
|
712
|
+
folding = { pieces, block: blockEl, endKind, wasAtEnd: historyNearBottom(), animate };
|
|
712
713
|
releaseTurn();
|
|
713
714
|
if (!animate) return finishFold();
|
|
714
715
|
stage.classList.add("leaving");
|
|
@@ -721,9 +722,9 @@ export function createStageView({
|
|
|
721
722
|
const fold = folding;
|
|
722
723
|
if (!fold) return;
|
|
723
724
|
folding = null;
|
|
724
|
-
//
|
|
725
|
-
//
|
|
726
|
-
if (fold.block) fold.block.open = fold.block.classList.contains("outline");
|
|
725
|
+
// The stage held it open; here it takes its place in the history:
|
|
726
|
+
// closed, unless the turn was cut short with an outline (block_open.js).
|
|
727
|
+
if (fold.block) fold.block.open = openAfterEnd(fold.endKind, fold.block.classList.contains("outline"));
|
|
727
728
|
historyEl.querySelector(":scope > .hint")?.remove();
|
|
728
729
|
for (const el of fold.pieces) {
|
|
729
730
|
historyEl.appendChild(el);
|
|
@@ -17,14 +17,17 @@
|
|
|
17
17
|
// walks childNodes (or reads firstChild/lastChild) must skip comments;
|
|
18
18
|
// element queries, :empty and textContent are unaffected.
|
|
19
19
|
//
|
|
20
|
-
// A block that holds one gen with no thinking and no tool rows hides its
|
|
20
|
+
// A block that holds one live gen with no thinking and no tool rows hides its
|
|
21
21
|
// chrome (CSS :has), so a plain answer streams like today's bubble, and is
|
|
22
|
-
// removed at the end: the answer bubble takes its place.
|
|
22
|
+
// removed at the end: the answer bubble takes its place. A lone step that
|
|
23
|
+
// closed with text keeps its chrome: a line steered in after it left it alone
|
|
24
|
+
// in the block when the answer went, and its summary is the way to its text.
|
|
23
25
|
import { authorBadge, PAIR } from "./participant_names.js";
|
|
24
26
|
import { applyRowTemplate } from "./review_apply.js";
|
|
25
27
|
import { html, nothing, render } from "./vendor/lit-html.js";
|
|
26
28
|
import { answerRefsOf, setAnswerRefs } from "./answer_refs.js";
|
|
27
29
|
import { bindRowRefs } from "./refs.js";
|
|
30
|
+
import { openAfterEnd } from "./block_open.js";
|
|
28
31
|
import { leavesBlock } from "./card.js";
|
|
29
32
|
import { failedTurnText, normalize, sourceLabel } from "./format.js";
|
|
30
33
|
import { thumbsTemplate } from "./images.js";
|
|
@@ -132,7 +135,7 @@ function nodesOf(template) {
|
|
|
132
135
|
// ---- A finished block's outline ----
|
|
133
136
|
|
|
134
137
|
// A finished block as its outline (turn_model.js outlineRuns of +gens+, its
|
|
135
|
-
// steps in order):
|
|
138
|
+
// steps in order): each narrated step a one-line row as before, each
|
|
136
139
|
// run of steps that said nothing hidden behind one "N steps" fold that
|
|
137
140
|
// shows them. The live turn's end and a reload call it alike. The folds sit
|
|
138
141
|
// between the steps, not in them (`:scope > .gen` still counts steps), each
|
|
@@ -178,7 +181,6 @@ export function outlineBlock(blockEl, gens) {
|
|
|
178
181
|
const step = e.target.closest?.(".gen.folded");
|
|
179
182
|
if (step?.parentElement === blockEl) unfold.get(step)?.();
|
|
180
183
|
});
|
|
181
|
-
blockEl.open = true;
|
|
182
184
|
return true;
|
|
183
185
|
}
|
|
184
186
|
|
|
@@ -214,7 +216,7 @@ export function createTurnBlock({
|
|
|
214
216
|
let turn = null;
|
|
215
217
|
let blockEl = null;
|
|
216
218
|
let blockSummaryEl = null;
|
|
217
|
-
let
|
|
219
|
+
let blockStepsEl = null;
|
|
218
220
|
// gen → a step's live state (newStep), its rendered nodes between
|
|
219
221
|
// part.startNode and end.
|
|
220
222
|
const genEls = new Map();
|
|
@@ -237,16 +239,20 @@ export function createTurnBlock({
|
|
|
237
239
|
blockEl = document.createElement("details");
|
|
238
240
|
blockEl.className = "turn-work";
|
|
239
241
|
blockEl.open = true;
|
|
240
|
-
blockToggled = false;
|
|
241
242
|
blockSummaryEl = document.createElement("summary");
|
|
243
|
+
const line = document.createElement("span");
|
|
244
|
+
line.className = "tw-sum";
|
|
245
|
+
blockStepsEl = document.createElement("span");
|
|
246
|
+
blockStepsEl.className = "turn-steps";
|
|
247
|
+
line.appendChild(blockStepsEl);
|
|
248
|
+
blockSummaryEl.appendChild(line);
|
|
242
249
|
blockEl.appendChild(blockSummaryEl);
|
|
243
|
-
blockSummaryEl.addEventListener("click", () => { blockToggled = true; });
|
|
244
250
|
updateBlockSummary();
|
|
245
251
|
blockHost(blockEl);
|
|
246
252
|
}
|
|
247
253
|
|
|
248
254
|
function updateBlockSummary() {
|
|
249
|
-
if (
|
|
255
|
+
if (blockStepsEl && turn) blockStepsEl.textContent = model.blockSummary(turn, { running: !turn.ended });
|
|
250
256
|
}
|
|
251
257
|
|
|
252
258
|
// The step rendered again from its state (stepTemplate). Each live step
|
|
@@ -621,6 +627,7 @@ export function createTurnBlock({
|
|
|
621
627
|
placeAnswer(pieces, { plain: true, blockEl });
|
|
622
628
|
blockEl = null;
|
|
623
629
|
blockSummaryEl = null;
|
|
630
|
+
blockStepsEl = null;
|
|
624
631
|
} else {
|
|
625
632
|
placeAnswer(bubble ? [...cards, bubble] : [...cards, ...(endNote ? [endNote] : [])], { plain: false, blockEl });
|
|
626
633
|
}
|
|
@@ -649,11 +656,12 @@ export function createTurnBlock({
|
|
|
649
656
|
if (blockEl) {
|
|
650
657
|
updateBlockSummary();
|
|
651
658
|
blockEl.classList.add("done");
|
|
652
|
-
// The outline (turn_model.js outlineRuns)
|
|
653
|
-
//
|
|
659
|
+
// The outline (turn_model.js outlineRuns), for when it is opened. Closed
|
|
660
|
+
// as a reload draws it, unless the turn was cut short (block_open.js).
|
|
661
|
+
// The stage decides again as the block reaches the history.
|
|
654
662
|
chronological();
|
|
655
663
|
const outlined = outlineBlock(blockEl, model.blockGens(turn));
|
|
656
|
-
|
|
664
|
+
blockEl.open = openAfterEnd(kind, outlined);
|
|
657
665
|
for (const el of blockEl.querySelectorAll(".plugin-card")) {
|
|
658
666
|
if (leavesBlock({ warn: el.classList.contains("warn"), kind })) kept.push(el);
|
|
659
667
|
}
|
|
@@ -682,7 +690,7 @@ export function createTurnBlock({
|
|
|
682
690
|
turn = null;
|
|
683
691
|
blockEl = null;
|
|
684
692
|
blockSummaryEl = null;
|
|
685
|
-
|
|
693
|
+
blockStepsEl = null;
|
|
686
694
|
// Clear every ticker timer before the map goes (a live step mid-dwell).
|
|
687
695
|
for (const entry of genEls.values()) stopTicker(entry);
|
|
688
696
|
genEls.clear();
|
|
@@ -821,7 +829,7 @@ function failedLineTemplate(record) {
|
|
|
821
829
|
// template, images.js thumbsTemplate). +workingSet+ (the stage's
|
|
822
830
|
// web.working_set): each block stamped with its
|
|
823
831
|
// turn's ordinal and its rows with theirs (working_set_view.js), and the
|
|
824
|
-
// turn's footprint
|
|
832
|
+
// turn's footprint on its summary line.
|
|
825
833
|
export function turnHistoryNodes(items, timing, { thumbs, workingSet = false, now = Date.now() }) {
|
|
826
834
|
const timingTemplate = (group) => {
|
|
827
835
|
const record = group.record;
|
|
@@ -861,9 +869,11 @@ export function turnHistoryNodes(items, timing, { thumbs, workingSet = false, no
|
|
|
861
869
|
};
|
|
862
870
|
const steerTemplate = (i, bubble) => steerRowTemplate({ source: items[i].source, text: items[i].content }, { bubble });
|
|
863
871
|
const parts = [];
|
|
864
|
-
// Each block's steps, in order: its outline once the nodes exist;
|
|
865
|
-
// its turn's place among the turns
|
|
872
|
+
// Each block's steps, in order: its outline once the nodes exist; how its
|
|
873
|
+
// turn ended (whether it stays open); and its turn's place among the turns
|
|
874
|
+
// (the working set's stamp).
|
|
866
875
|
const outlines = [];
|
|
876
|
+
const blockEnds = [];
|
|
867
877
|
const blockTurns = [];
|
|
868
878
|
let turnOrdinal = -1;
|
|
869
879
|
const groups = turnGroups(items, timing);
|
|
@@ -903,10 +913,14 @@ export function turnHistoryNodes(items, timing, { thumbs, workingSet = false, no
|
|
|
903
913
|
// Its text knows its message's refs (a question's), as an answer does.
|
|
904
914
|
at: group.steps[k].i,
|
|
905
915
|
}));
|
|
906
|
-
|
|
916
|
+
// Its summary line: the working set's footprint, when the turn touched
|
|
917
|
+
// something, then the steps (the live block's, stage_view.js handOff).
|
|
918
|
+
const foot = workingSet ? footprintTemplate(groupItems(group, turnOrdinal), turnOrdinal) : nothing;
|
|
919
|
+
parts.push(html`<details class="turn-work done"><summary><span class="tw-sum">${foot}<span class="turn-steps">${model.blockSummary({ gens, ended: true })}</span></span></summary>${steps}</details>`);
|
|
907
920
|
outlines.push(gens);
|
|
921
|
+
// No record: a turn saved before them, or running at the reload.
|
|
922
|
+
blockEnds.push(group.record?.status ?? "completed");
|
|
908
923
|
blockTurns.push(turnOrdinal);
|
|
909
|
-
if (workingSet) parts.push(footprintTemplate(groupItems(group, turnOrdinal), turnOrdinal));
|
|
910
924
|
}
|
|
911
925
|
// Steers with no step to sit in, or in a turn that shows no block: on
|
|
912
926
|
// their own after the prompt.
|
|
@@ -941,9 +955,10 @@ export function turnHistoryNodes(items, timing, { thumbs, workingSet = false, no
|
|
|
941
955
|
parts.push(failedLineTemplate(group.record));
|
|
942
956
|
}
|
|
943
957
|
const out = nodesOf(parts);
|
|
944
|
-
// A finished turn's block as the live end leaves it: its outline,
|
|
958
|
+
// A finished turn's block as the live end leaves it: its outline, closed
|
|
959
|
+
// unless the turn was cut short.
|
|
945
960
|
const blocks = [...out.children].filter((el) => el.matches(".turn-work"));
|
|
946
|
-
blocks.forEach((block, i) => outlineBlock(block, outlines[i] || []));
|
|
961
|
+
blocks.forEach((block, i) => { block.open = openAfterEnd(blockEnds[i], outlineBlock(block, outlines[i] || [])); });
|
|
947
962
|
if (workingSet) stampHistory(blocks, blockTurns);
|
|
948
963
|
// Each answer bubble and step text knows its message's refs
|
|
949
964
|
// (answer_refs.js; the page pairs them after the render), the index
|
|
@@ -279,7 +279,7 @@ export function isNarrated(gen) {
|
|
|
279
279
|
// them): each narrated step a row, each run of the others between (tool-only,
|
|
280
280
|
// thinking-only) one fold ({fold: [indices]}), so every mid-turn text's first
|
|
281
281
|
// line reads without a click and the steps that said nothing take one line.
|
|
282
|
-
// [] when no step said anything:
|
|
282
|
+
// [] when no step said anything: nothing to outline.
|
|
283
283
|
// A last run of one thinking-only step stays a row (no "· 1 step" at the end).
|
|
284
284
|
export function outlineRuns(gens) {
|
|
285
285
|
if (!gens.some(isNarrated)) return [];
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import { html, nothing, render } from "./vendor/lit-html.js";
|
|
2
2
|
|
|
3
|
+
// chi update's own words for the row (FileStanding owns what it says of a file).
|
|
4
|
+
// One that speaks of edited files runs long, so it gets the row's whole width.
|
|
3
5
|
function installRow(row) {
|
|
4
|
-
const
|
|
6
|
+
const count = (list) => (Array.isArray(list) ? list.length : 0);
|
|
7
|
+
const behind = count(row.kept) > 0;
|
|
8
|
+
const long = behind || count(row.merged) > 0 || count(row.earlier) > 0;
|
|
9
|
+
const status = row.status || "working";
|
|
5
10
|
const versions = [row.from, row.to].filter(Boolean).join(" → ");
|
|
6
11
|
return html`<li class="us-row">
|
|
7
|
-
<div class="us-row-head"><strong>${row.component || "chi update"}</strong
|
|
12
|
+
<div class="us-row-head"><strong>${row.component || "chi update"}</strong>${long ? nothing : html`<span>${status}</span>`}</div>
|
|
8
13
|
${versions ? html`<div class="us-detail">${versions}</div>` : nothing}
|
|
9
|
-
${
|
|
14
|
+
${long ? html`<div class=${behind ? "us-kept" : "us-detail"}>${status}</div>` : nothing}
|
|
10
15
|
</li>`;
|
|
11
16
|
}
|
|
12
17
|
|
|
@@ -95,17 +95,18 @@ function footTag(label) {
|
|
|
95
95
|
return html` <button type="button" class="ws-step p" data-ws="jump" data-at=${atAttr(first.at)} title=${title}>${text}</button>`;
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
// The footprint
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
//
|
|
98
|
+
// The footprint of a finished turn's +items+ (turn +ordinal+), nothing for a
|
|
99
|
+
// turn that touched nothing: "touched web/public/ · 9 files · ◆ 3 memories
|
|
100
|
+
// s1–5 · +1 url · edited 2". It sits in the turn block's summary line, so a
|
|
101
|
+
// span (phrasing content). A click opens the expanded view at that turn; a
|
|
102
|
+
// step tag jumps to its call.
|
|
102
103
|
export function footprintTemplate(items, ordinal) {
|
|
103
104
|
const fp = footprint(items, BUDGETS.footprint);
|
|
104
105
|
if (!fp) return nothing;
|
|
105
106
|
const sep = html` <span class="sep">·</span> `;
|
|
106
107
|
const parts = fp.labels.map((l, i) => html`${i ? sep : nothing}<span class=${`l ${l.kind}${l.state && l.state !== "live" ? ` ${l.state}` : ""}`} title=${labelTitle(l)}>${l.glyph ? html`<span class="k">${l.glyph}</span> ` : nothing}${labelText({ ...l, glyph: "" })}</span>${footTag(l)}`);
|
|
107
108
|
const text = footprintText(items, BUDGETS.footprint);
|
|
108
|
-
return html`<
|
|
109
|
+
return html`<span class="ws-foot" role="button" tabindex="0" aria-haspopup="dialog" aria-expanded="false" data-ws-turn=${ordinal} data-ws-sig=${footSig(items)} aria-label=${`Working set of this turn: ${text.replace(/^touched /, "")}. Open the list.`}><span class="w">touched</span> ${parts}${fp.tail ? html`${sep}<span class="w">${fp.tail}</span>` : nothing}${fp.edited ? html`${sep}<span class="e">edited ${fp.edited}</span>` : nothing}${fp.stale ? html`${sep}<span class="s">${fp.stale} stale</span>` : nothing}${fp.forgotten ? html`${sep}<span class="f">${fp.forgotten} forgotten</span>` : nothing}${fp.dropped ? html`${sep}<span class="f">${fp.dropped} dropped</span>` : nothing}</span>`;
|
|
109
110
|
}
|
|
110
111
|
|
|
111
112
|
// What a footprint shows that can change after it is drawn: its items'
|
|
@@ -418,10 +419,12 @@ export function createWorkingSet({ historyEl, stageRoot = () => null, state, ses
|
|
|
418
419
|
}
|
|
419
420
|
});
|
|
420
421
|
|
|
421
|
-
// A footprint: its step tag jumps, the rest opens that turn's view.
|
|
422
|
+
// A footprint: its step tag jumps, the rest opens that turn's view. Inside
|
|
423
|
+
// a <summary>, the click is cancelled so it doesn't toggle the block.
|
|
422
424
|
function footClick(e) {
|
|
423
425
|
const foot = e.target.closest?.(".ws-foot");
|
|
424
426
|
if (!foot) return;
|
|
427
|
+
if (foot.closest("summary")) e.preventDefault();
|
|
425
428
|
const tag = e.target.closest(".ws-step[data-at]");
|
|
426
429
|
if (tag) {
|
|
427
430
|
const at = parseAt(tag.dataset.at);
|
|
@@ -563,7 +566,7 @@ export function createWorkingSet({ historyEl, stageRoot = () => null, state, ses
|
|
|
563
566
|
// The rail's first-sight times, for the popover's fresh rows.
|
|
564
567
|
setSeen(fn) { seenAt = fn; },
|
|
565
568
|
// The running turn hands off: its set joins the turns; its footprint
|
|
566
|
-
//
|
|
569
|
+
// (null: it touched nothing) goes into its block's summary.
|
|
567
570
|
handOff(turn, prompt = "") {
|
|
568
571
|
const ordinal = turns.length;
|
|
569
572
|
const items = itemsOfTurn(turn, ordinal);
|
|
@@ -5,6 +5,7 @@ require "uri"
|
|
|
5
5
|
require "time"
|
|
6
6
|
require_relative "attention"
|
|
7
7
|
require_relative "../../log"
|
|
8
|
+
require_relative "../../memory_bundle/file_standing"
|
|
8
9
|
|
|
9
10
|
module Samagotchi
|
|
10
11
|
module Web
|
|
@@ -52,10 +53,10 @@ module Samagotchi
|
|
|
52
53
|
AUTO_NOTICES = {
|
|
53
54
|
"installed" => lambda do |version, notes|
|
|
54
55
|
["installed", "chi #{version} is installed",
|
|
55
|
-
"chi web and its sessions switch over when you restart them.#{
|
|
56
|
+
"chi web and its sessions switch over when you restart them.#{file_words(notes)}"]
|
|
56
57
|
end,
|
|
57
58
|
"updated" => lambda do |version, notes|
|
|
58
|
-
["updated", "chi updated to #{version}", "#{restarted_words(notes[:restarted], notes[:left])}#{
|
|
59
|
+
["updated", "chi updated to #{version}", "#{restarted_words(notes[:restarted], notes[:left])}#{file_words(notes)}"]
|
|
59
60
|
end,
|
|
60
61
|
"restart_failed" => lambda do |version, notes|
|
|
61
62
|
["restart_failed", "chi #{version} is installed; chi web didn't restart",
|
|
@@ -79,13 +80,10 @@ module Samagotchi
|
|
|
79
80
|
"#{words}."
|
|
80
81
|
end
|
|
81
82
|
|
|
82
|
-
# "
|
|
83
|
-
def self.
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
else
|
|
87
|
-
""
|
|
88
|
-
end
|
|
83
|
+
# " Merged the update into 1 file you edited. 2 files stay behind." ("" for none).
|
|
84
|
+
def self.file_words(notes)
|
|
85
|
+
words = MemoryBundle::FileStanding.counts_sentence(merged: notes[:merged].to_i, behind: notes[:kept].to_i)
|
|
86
|
+
words.empty? ? "" : " #{words}"
|
|
89
87
|
end
|
|
90
88
|
|
|
91
89
|
# A show carries +open+ only while its JSON stays this small: one
|
|
@@ -216,7 +216,8 @@ module Samagotchi
|
|
|
216
216
|
# `chi_update` frames are the page's), and not kept for a snapshot.
|
|
217
217
|
# @param kind [String] installed, updated or failed
|
|
218
218
|
# @param version [String] the release it is about
|
|
219
|
-
# @param notes [Hash] detail:, kept: (
|
|
219
|
+
# @param notes [Hash] detail:, kept:, merged: (counts of edited files that
|
|
220
|
+
# stayed behind and that took the update), restarted:, left:
|
|
220
221
|
def publish_notice(kind:, version:, **notes)
|
|
221
222
|
@monitor.synchronize { emit("chi_notice", { kind: kind, version: version, **notes }) }
|
|
222
223
|
nil
|