samagotchi 0.49.0 → 0.51.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/bin/chi +2 -2
- data/docs/cli.md +33 -8
- data/docs/configuration.md +11 -9
- data/docs/hooks.md +12 -3
- data/docs/internals/engine.md +4 -2
- data/docs/sessions.md +8 -4
- data/docs/testing.md +2 -1
- data/lib/samagotchi/bridge/turn_accumulator.rb +3 -1
- data/lib/samagotchi/bridge.rb +24 -9
- data/lib/samagotchi/bridge_client.rb +3 -1
- data/lib/samagotchi/bundles/loop-guard/manifest.yml +2 -2
- data/lib/samagotchi/bundles/loop-guard/plugin.rb +13 -1
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/delivery.rb +33 -0
- data/lib/samagotchi/engine.rb +1 -1
- data/lib/samagotchi/hooks/stream_watch.rb +9 -2
- data/lib/samagotchi/host_registry.rb +17 -4
- data/lib/samagotchi/kernel_loop.rb +28 -2
- data/lib/samagotchi/llm/native_tool_normalizer.rb +2 -2
- data/lib/samagotchi/send_command.rb +124 -10
- data/lib/samagotchi/session_commands.rb +10 -2
- data/lib/samagotchi/session_inbox.rb +27 -7
- data/lib/samagotchi/session_manager.rb +13 -3
- data/lib/samagotchi/session_metrics.rb +32 -6
- data/lib/samagotchi/sessions_command.rb +84 -7
- data/lib/samagotchi/steer_cut.rb +12 -8
- data/lib/samagotchi/terminal_ui/attached_loop.rb +50 -3
- data/lib/samagotchi/terminal_ui/event_renderer.rb +4 -2
- data/lib/samagotchi/terminal_ui/formatting.rb +68 -6
- data/lib/samagotchi/terminal_ui.rb +48 -5
- data/lib/samagotchi/tool_activity.rb +3 -0
- data/lib/samagotchi/tool_runner.rb +2 -0
- data/lib/samagotchi/tools/builtin_calls.rb +33 -0
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +6 -2
- data/lib/samagotchi/web/message_parts.rb +3 -0
- data/lib/samagotchi/web/public/activity.js +6 -0
- data/lib/samagotchi/web/public/app.js +71 -29
- data/lib/samagotchi/web/public/index.html +17 -15
- data/lib/samagotchi/web/public/live_timing.js +32 -5
- data/lib/samagotchi/web/public/session_title.js +26 -0
- data/lib/samagotchi/web/public/stage_model.js +15 -0
- data/lib/samagotchi/web/public/stage_view.js +10 -4
- data/lib/samagotchi/web/public/strip.js +4 -6
- data/lib/samagotchi/web/public/timing.js +11 -0
- data/lib/samagotchi/web/public/turn_events.js +14 -2
- data/lib/samagotchi/web/public/turn_html.js +24 -1
- data/lib/samagotchi/web/public/turn_view.js +9 -3
- data/lib/samagotchi/web/session_hub.rb +11 -3
- data/lib/samagotchi/worker.rb +1 -1
- data/lib/samagotchi/worker_inbound.rb +8 -5
- data/lib/samagotchi.rb +1 -0
- metadata +3 -1
|
@@ -53,6 +53,7 @@ export function addStarted(model, event = {}, now = null) {
|
|
|
53
53
|
row.label = event.label || row.label;
|
|
54
54
|
row.params = event.params || row.params;
|
|
55
55
|
row.title = event.title || row.title;
|
|
56
|
+
row.called_as = event.called_as || row.called_as;
|
|
56
57
|
row.view = event.view || row.view;
|
|
57
58
|
markStart(row, event, now);
|
|
58
59
|
return { row, created: false };
|
|
@@ -68,6 +69,9 @@ export function addStarted(model, event = {}, now = null) {
|
|
|
68
69
|
// project-relative path, a command without its "cd … &&"); rows show it
|
|
69
70
|
// in place of the params when there is one.
|
|
70
71
|
title: event.title || null,
|
|
72
|
+
// The name the model called the tool by when it was an alias's ("bash"
|
|
73
|
+
// run as execute); the row says so.
|
|
74
|
+
called_as: event.called_as || null,
|
|
71
75
|
// The call's full arguments for the expanded row and the hovers (the
|
|
72
76
|
// server's ToolView: an execute/task_create's command, its cwd).
|
|
73
77
|
view: event.view || null,
|
|
@@ -104,6 +108,7 @@ export function addCompleted(model, event = {}, now = null) {
|
|
|
104
108
|
label: event.label,
|
|
105
109
|
params: event.activity?.params || "",
|
|
106
110
|
title: event.activity?.title,
|
|
111
|
+
called_as: event.activity?.called_as,
|
|
107
112
|
view: event.view,
|
|
108
113
|
});
|
|
109
114
|
row = synthesized.row;
|
|
@@ -113,6 +118,7 @@ export function addCompleted(model, event = {}, now = null) {
|
|
|
113
118
|
row.output_truncated = !!event.output_truncated;
|
|
114
119
|
if (event.activity?.params) row.params = event.activity.params;
|
|
115
120
|
if (event.activity?.title) row.title = event.activity.title;
|
|
121
|
+
if (event.activity?.called_as) row.called_as = event.activity.called_as;
|
|
116
122
|
// The images the tool returned (a reload in the turn view draws them from
|
|
117
123
|
// the saved parts, turn_html.js toolRowHtml).
|
|
118
124
|
if (event.images?.length) row.images = event.images;
|
|
@@ -56,13 +56,14 @@ import { shouldFollowScroll, keepFollowing } from "./scroll.js";
|
|
|
56
56
|
import { SHORT_WINDOW_QUERY, stripAutoHidden, stripColumns, stripShowParts } from "./strip.js";
|
|
57
57
|
import { applySessionEvent, batchCounts, batchTargets, batchToastText, families, familyId, familyOpen, familyWaits, filterFamilies, flattenFamilies, heldOrder, inTree, rangeIds, runBatch, listedSessions, sortedByUpdated, stoppedBadge, waitingBadge, waitingFirst, waitingSearchText } from "./sessions_list.js";
|
|
58
58
|
import { createNotifications } from "./notifications.js";
|
|
59
|
+
import { tabTitle, topBarTitle } from "./session_title.js";
|
|
59
60
|
import { restartConfirmText, versionNotice, workerBadge } from "./update_notice.js";
|
|
60
61
|
import {
|
|
61
62
|
appendAboveLiveTiming, cancelLineText, elapsedSince, formatDuration, mergeTiming, normalizeTiming, STOPPED_LINE_CLASS,
|
|
62
63
|
} from "./timing.js";
|
|
63
64
|
import { createLiveTurn } from "./live_turn.js";
|
|
64
65
|
import { DONE_MS, applyInitEvent, dropDone, initRowHtml, newInitTasks, seedInitTasks } from "./init_row.js";
|
|
65
|
-
import { clientLabel, commandView, isReportClient, displayWait, dropEarlyRestores, emptyRetryLine, hookNoticeLabel, initWaitLine, isOwn, llmContextHover, llmContextLine, sessionCommandLine, keepEarlyRestore, newClientId, noticeLine, promptOps, reminderText, restoreAction, composerAfterAck, restoreOnAck, replaysDrawnTurn, retryStatusLine, snapshotEvents, steerCutLine, startPageReplyOrHint, turnOutput, unknownCommandHint, webLocalReply, workerGoneText } from "./turn_events.js";
|
|
66
|
+
import { clientLabel, commandView, isReportClient, displayWait, dropEarlyRestores, emptyRetryLine, historyHasCommandBubble, hookNoticeLabel, initWaitLine, isOwn, llmContextHover, llmContextLine, sessionCommandLine, keepEarlyRestore, newClientId, noticeLine, promptOps, reminderText, restoreAction, composerAfterAck, restoreOnAck, replaysDrawnTurn, retryStatusLine, snapshotEvents, steerCutLine, startPageReplyOrHint, turnOutput, unknownCommandHint, webLocalReply, workerGoneText } from "./turn_events.js";
|
|
66
67
|
|
|
67
68
|
const $ = (s) => document.querySelector(s);
|
|
68
69
|
const topStripEl = $("#topStrip");
|
|
@@ -71,7 +72,11 @@ const historyEl = $("#history");
|
|
|
71
72
|
const emptyEl = $("#empty");
|
|
72
73
|
const composerEl = $("#composer");
|
|
73
74
|
const infoBarEl = $("#infoBar");
|
|
74
|
-
|
|
75
|
+
// The open session's title in the top bar, when no strip card in sight
|
|
76
|
+
// shows it; the tab's base ("Chi · <project>"), set by renderScope.
|
|
77
|
+
const sessionTitleEl = $("#sessionTitle");
|
|
78
|
+
let tabBase = null;
|
|
79
|
+
let tabShown = null;
|
|
75
80
|
const infoTextEl = $("#infoText");
|
|
76
81
|
// The open session's attached context, in its own row of the session bar
|
|
77
82
|
// (updateInfoBar redraws #infoText every tick; the chips keep their popover).
|
|
@@ -342,7 +347,9 @@ function renderScope() {
|
|
|
342
347
|
el.innerHTML = `<span class="scope-name" title="Sessions of every project">all projects</span>${backLink}${where ? `<span class="scope-where" title="New chats start in ${escapeHtml(where)}">new chat in<span class="path"><bdi>${escapeHtml(where)}</bdi></span></span>` : ""}`;
|
|
343
348
|
}
|
|
344
349
|
el.hidden = false;
|
|
345
|
-
|
|
350
|
+
// The tab: "<session title> · Chi · <project>" (updateSessionTitle).
|
|
351
|
+
tabBase = scope.projectName ? `Chi · ${scope.projectName}` : "Chi";
|
|
352
|
+
updateSessionTitle();
|
|
346
353
|
// The links follow the hash (an open session, the all-sessions view).
|
|
347
354
|
el.querySelector(".scope-all:not(.scope-back)")?.addEventListener("click", (e) => {
|
|
348
355
|
e.currentTarget.href = allScopeHref(location.hash, scope.dir, location.search);
|
|
@@ -642,6 +649,8 @@ function paintActive() {
|
|
|
642
649
|
document.querySelectorAll(".family-row[data-id]").forEach((el) => {
|
|
643
650
|
el.classList.toggle("active", !!selected && el.dataset.id === selected);
|
|
644
651
|
});
|
|
652
|
+
// The open session's card may have come into the strip or left it.
|
|
653
|
+
updateSessionTitle();
|
|
645
654
|
}
|
|
646
655
|
|
|
647
656
|
// The latest sessions (as many as fit, a family one card), then the way
|
|
@@ -693,11 +702,11 @@ function noteChainFamilies(fams) {
|
|
|
693
702
|
}
|
|
694
703
|
|
|
695
704
|
// The hidden strip's pill: "▾ 5 sessions · ● 1 live · ● 1 waiting",
|
|
696
|
-
// following the list: its
|
|
697
|
-
//
|
|
705
|
+
// following the list: its sessions, the number the All sessions tile
|
|
706
|
+
// shows, and the live and waiting ones (a waiting delegate too).
|
|
698
707
|
function renderStripShow() {
|
|
699
708
|
const shown = shownSessions();
|
|
700
|
-
const { count, live, waiting } = stripShowParts(shown
|
|
709
|
+
const { count, live, waiting } = stripShowParts(shown);
|
|
701
710
|
const liveHtml = live ? ` · <span class="live"><span class="live-dot" aria-hidden="true"></span>${escapeHtml(live)}</span>` : "";
|
|
702
711
|
const waitingHtml = waiting ? ` · <span class="waiting"><span class="waiting-dot" aria-hidden="true"></span>${escapeHtml(waiting)}</span>` : "";
|
|
703
712
|
$("#stripShow").innerHTML = `<span class="pill-arrow" aria-hidden="true">\u25BE</span> ${escapeHtml(count)}${liveHtml}${waitingHtml}`;
|
|
@@ -999,6 +1008,7 @@ function applyView() {
|
|
|
999
1008
|
topStripEl.scrollLeft = 0; // a phone's strip scrolls sideways to the tile
|
|
1000
1009
|
}
|
|
1001
1010
|
document.body.classList.toggle("all-view", open);
|
|
1011
|
+
updateSessionTitle();
|
|
1002
1012
|
// Its chip was in the list just hidden (the strip, or all sessions).
|
|
1003
1013
|
chainPop.close();
|
|
1004
1014
|
if (open) {
|
|
@@ -1037,6 +1047,7 @@ function updateComposerMode() {
|
|
|
1037
1047
|
if (document.body.classList.toggle("has-session", !!selected) !== hadSession) {
|
|
1038
1048
|
hadSession = !hadSession;
|
|
1039
1049
|
fitPrompt();
|
|
1050
|
+
placeToast();
|
|
1040
1051
|
}
|
|
1041
1052
|
if (selected) {
|
|
1042
1053
|
modelPicker.close();
|
|
@@ -1148,7 +1159,6 @@ function clearSelection() {
|
|
|
1148
1159
|
setCapabilities(null);
|
|
1149
1160
|
updateComposerMode();
|
|
1150
1161
|
updateInfoBar();
|
|
1151
|
-
updateFirstPeek();
|
|
1152
1162
|
promptEl.focus();
|
|
1153
1163
|
}
|
|
1154
1164
|
|
|
@@ -1182,7 +1192,6 @@ async function select(id) {
|
|
|
1182
1192
|
closeStream();
|
|
1183
1193
|
resetSessionState();
|
|
1184
1194
|
resetTurnView();
|
|
1185
|
-
updateFirstPeek();
|
|
1186
1195
|
updateInfoBar();
|
|
1187
1196
|
contextChips.load(id);
|
|
1188
1197
|
updateComposerMode();
|
|
@@ -1334,7 +1343,6 @@ function renderSessionView(data) {
|
|
|
1334
1343
|
// A turn ran out of iterations and waits for a yes or no.
|
|
1335
1344
|
if (data.continue_offer) questionCards.renderContinue(data.continue_offer);
|
|
1336
1345
|
updateInfoBar();
|
|
1337
|
-
updateFirstPeek();
|
|
1338
1346
|
updateComposerMode();
|
|
1339
1347
|
}
|
|
1340
1348
|
|
|
@@ -1922,11 +1930,11 @@ function updateInfoBar() {
|
|
|
1922
1930
|
renderStable(infoTextEl, "");
|
|
1923
1931
|
infoTextEl.title = "";
|
|
1924
1932
|
if (stopBtn) stopBtn.style.display = "none";
|
|
1933
|
+
updateSessionTitle();
|
|
1925
1934
|
return;
|
|
1926
1935
|
}
|
|
1927
1936
|
if (stopBtn) stopBtn.style.display = canStopSession({ owner: current.owner, streaming: !!streamControl }) ? "" : "none";
|
|
1928
1937
|
const idShort = selected.slice(0,8);
|
|
1929
|
-
const memText = current.usedMemories.length ? current.usedMemories.join(", ") : "—";
|
|
1930
1938
|
const ctxText = ctxBarText(current.ctxPct, current.ctxWindowSource);
|
|
1931
1939
|
const tokens = current.timing.tokens;
|
|
1932
1940
|
const ctxTip = tokensTipText(tokens, undefined, current.timing.memoryIndex,
|
|
@@ -1958,15 +1966,37 @@ function updateInfoBar() {
|
|
|
1958
1966
|
const copied = Date.now() - attachCopiedAt < 2000;
|
|
1959
1967
|
const copyHtml = `<button type="button" class="copy-attach" title="Copy "${escapeHtml(attachCmd)}" to attach a terminal">${copied ? "copied ✓" : "copy chi --attach"}</button>`;
|
|
1960
1968
|
const metaHtml = `<div class="meta"><span class="id" title="${escapeHtml(selected)}">${escapeHtml(idShort)}</span>${copyHtml}<span class="${statusClass}">${escapeHtml(current.status||"")}</span><span class="model${label.mismatch ? " served-mismatch" : ""}" title="${escapeHtml(label.title)}">${escapeHtml(label.text)}</span>${chainHtml}${parentHtml}${kidsHtml}${llmContextChipHtml(current.llmContext, escapeHtml)}${notesHtml}${current.usedMemories.length ? `<span class="model mem-chip" title="${escapeHtml(memChipTitle(current.usedMemories))}">${escapeHtml(memChipText(current.usedMemories))}</span>` : ""}<span class="dir" title="${escapeHtml(current.dir||"")}"><bdi>${escapeHtml(current.dir||"")}</bdi></span>${ctxText ? `<span class="model ctx" title="${escapeHtml(ctxTip)}">${escapeHtml(ctxText)}</span>` : ""}${speedTextNow ? `<span class="model speed" title="${escapeHtml(speedTip)}">${escapeHtml(speedTextNow)}</span>` : ""}${timingText ? `<span class="model session-time"></span>` : ""}${workerBadgeHtml()}</div>`;
|
|
1961
|
-
// The title line follows the card's: a chain link opened with the opener
|
|
1962
|
-
// reads its chain's first link's title (cardTitle).
|
|
1963
|
-
const shown = sessionsById().get(selected);
|
|
1964
|
-
const titleText = shown && isOpener(current.firstPreview) ? cardTitle(shown) : current.firstPreview;
|
|
1965
|
-
const previewHtml = `<div class="preview-line"><span class="preview">${escapeHtml(titleText || "—")}</span> · <span class="mem" title="${escapeHtml(current.usedMemories.join(", "))}">mem: ${escapeHtml(memText)}</span></div>`;
|
|
1966
1969
|
// The session clock ticks every second while a turn runs: only its text
|
|
1967
1970
|
// changes, the bar's other nodes (a tooltip, a button mid-press) stay.
|
|
1968
|
-
renderStable(infoTextEl, metaHtml
|
|
1971
|
+
renderStable(infoTextEl, metaHtml, { ".session-time": timingText });
|
|
1969
1972
|
infoTextEl.title = memoriesTooltip(current.usedMemories, current.preloadedMemories, current.mutedMemories);
|
|
1973
|
+
updateSessionTitle();
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
// The open session's title, once on screen (session_title.js): its strip
|
|
1977
|
+
// card says it when the strip is shown and has one; else the top bar. The
|
|
1978
|
+
// browser tab names it too. It follows the card's: a chain link opened with
|
|
1979
|
+
// the opener reads its chain's first link's title (cardTitle).
|
|
1980
|
+
function updateSessionTitle() {
|
|
1981
|
+
const shown = selected ? sessionsById().get(selected) : null;
|
|
1982
|
+
const first = current.firstPreview || "";
|
|
1983
|
+
const raw = !selected ? "" : (shown && isOpener(first) ? cardTitle(shown) : first);
|
|
1984
|
+
const title = raw === "\u2014" ? "" : raw; // "—": no first message yet
|
|
1985
|
+
const text = topBarTitle({
|
|
1986
|
+
title,
|
|
1987
|
+
cardShown: !!topStripEl.querySelector(".card.active"),
|
|
1988
|
+
stripHidden: document.body.classList.contains("strip-hidden"),
|
|
1989
|
+
allView: allViewOpen(),
|
|
1990
|
+
});
|
|
1991
|
+
if (sessionTitleEl.textContent !== text) sessionTitleEl.textContent = text;
|
|
1992
|
+
if (sessionTitleEl.hidden !== !text) sessionTitleEl.hidden = !text;
|
|
1993
|
+
const tip = text ? title : "";
|
|
1994
|
+
if (sessionTitleEl.title !== tip) sessionTitleEl.title = tip;
|
|
1995
|
+
const tab = tabBase === null ? null : tabTitle({ title, base: tabBase });
|
|
1996
|
+
if (tab !== null && tab !== tabShown) {
|
|
1997
|
+
tabShown = tab;
|
|
1998
|
+
notifications.setBaseTitle(tab);
|
|
1999
|
+
}
|
|
1970
2000
|
}
|
|
1971
2001
|
|
|
1972
2002
|
// The info bar's ← / → link to a chain's link (chains.js chainNeighbours):
|
|
@@ -2024,16 +2054,6 @@ async function mergeTurnTiming(id, data) {
|
|
|
2024
2054
|
return true;
|
|
2025
2055
|
}
|
|
2026
2056
|
|
|
2027
|
-
function updateFirstPeek() {
|
|
2028
|
-
if (current.ctxPct !== null && current.ctxPct > 20 && current.firstPreview) {
|
|
2029
|
-
firstPeekEl.textContent = current.firstPreview;
|
|
2030
|
-
firstPeekEl.classList.add("visible");
|
|
2031
|
-
} else {
|
|
2032
|
-
firstPeekEl.textContent = "";
|
|
2033
|
-
firstPeekEl.classList.remove("visible");
|
|
2034
|
-
}
|
|
2035
|
-
}
|
|
2036
|
-
|
|
2037
2057
|
function setUsedMemories(names) {
|
|
2038
2058
|
current.usedMemories = names.slice();
|
|
2039
2059
|
updateInfoBar();
|
|
@@ -2119,7 +2139,6 @@ const streamHandlers = {
|
|
|
2119
2139
|
if (pct !== null && !Number.isNaN(pct)) {
|
|
2120
2140
|
current.ctxPct = pct;
|
|
2121
2141
|
updateInfoBar();
|
|
2122
|
-
updateFirstPeek();
|
|
2123
2142
|
}
|
|
2124
2143
|
},
|
|
2125
2144
|
generation_started: (data) => {
|
|
@@ -2274,6 +2293,10 @@ const streamHandlers = {
|
|
|
2274
2293
|
queued.remove();
|
|
2275
2294
|
addCommandBubble(view);
|
|
2276
2295
|
} else if (queued) drawCommandBubble(queued, view);
|
|
2296
|
+
// A command that changed the conversation (!cmd): the redrawn history
|
|
2297
|
+
// ends with its command bubble already (the saved "!(…)\n<output>"
|
|
2298
|
+
// message renders as one) — no second bubble, or the output shows twice.
|
|
2299
|
+
else if (view.resync && historyHasCommandBubble(findInTurnRoots(".bubble.command"), view.line)) { /* drawn by the resync */ }
|
|
2277
2300
|
else addCommandBubble(view);
|
|
2278
2301
|
},
|
|
2279
2302
|
// An anytime command (/help, a plugin's /btw) runs at once, beside a
|
|
@@ -2304,7 +2327,6 @@ const streamHandlers = {
|
|
|
2304
2327
|
if (pct !== null) {
|
|
2305
2328
|
current.ctxPct = pct;
|
|
2306
2329
|
updateInfoBar();
|
|
2307
|
-
updateFirstPeek();
|
|
2308
2330
|
}
|
|
2309
2331
|
},
|
|
2310
2332
|
question_requested: (data) => {
|
|
@@ -2465,7 +2487,6 @@ async function handleSendTurn() {
|
|
|
2465
2487
|
if (!current.firstPreview || current.firstPreview==="—") {
|
|
2466
2488
|
current.firstPreview = normalize(prompt).slice(0,80) + (normalize(prompt).length>80?"…":"");
|
|
2467
2489
|
updateInfoBar();
|
|
2468
|
-
updateFirstPeek();
|
|
2469
2490
|
}
|
|
2470
2491
|
actionBtn.disabled = true;
|
|
2471
2492
|
try {
|
|
@@ -2767,7 +2788,9 @@ new ResizeObserver(() => {
|
|
|
2767
2788
|
const follow = atHistoryEnd();
|
|
2768
2789
|
centerEl.style.setProperty("--dock-h", `${dockEl.offsetHeight}px`);
|
|
2769
2790
|
if (follow) historyEl.scrollTop = historyEl.scrollHeight;
|
|
2791
|
+
placeToast();
|
|
2770
2792
|
}).observe(dockEl);
|
|
2793
|
+
window.addEventListener("resize", () => placeToast());
|
|
2771
2794
|
|
|
2772
2795
|
promptEl.addEventListener("paste", (e) => {
|
|
2773
2796
|
const files = imageFiles(e.clipboardData);
|
|
@@ -2977,6 +3000,23 @@ async function setArchived(id, archive, { undo = true } = {}) {
|
|
|
2977
3000
|
showToast(`${what} ${short}`, undo ? { label: "Undo", run: () => setArchived(id, !archive, { undo: false }) } : null);
|
|
2978
3001
|
}
|
|
2979
3002
|
|
|
3003
|
+
// The toast sits above the session's composer card (measured: it grows with
|
|
3004
|
+
// a wrapping footer or a taller box), so it never covers the card's
|
|
3005
|
+
// controls; with no card in sight (the start page, all sessions) the CSS's
|
|
3006
|
+
// 24 px from the bottom.
|
|
3007
|
+
function placeToast() {
|
|
3008
|
+
const gap = 12;
|
|
3009
|
+
const dock = document.getElementById("dock");
|
|
3010
|
+
const r = dock.getBoundingClientRect();
|
|
3011
|
+
const over = document.body.classList.contains("has-session") && r.height > 0;
|
|
3012
|
+
const bottom = over ? `${Math.round(window.innerHeight - r.top + gap)}px` : "";
|
|
3013
|
+
const root = document.documentElement.style;
|
|
3014
|
+
if (root.getPropertyValue("--toast-bottom") !== bottom) {
|
|
3015
|
+
if (bottom) root.setProperty("--toast-bottom", bottom);
|
|
3016
|
+
else root.removeProperty("--toast-bottom");
|
|
3017
|
+
}
|
|
3018
|
+
}
|
|
3019
|
+
|
|
2980
3020
|
// sticky: stays until its action or its ✕ (else gone after 6 s).
|
|
2981
3021
|
let toastTimer = null;
|
|
2982
3022
|
// html: +text+ is markup this page built (its parts escaped), not words.
|
|
@@ -2984,6 +3024,7 @@ function showToast(text, action = null, { sticky = false, html = false } = {}) {
|
|
|
2984
3024
|
const el = $("#toast");
|
|
2985
3025
|
el.innerHTML = `<span>${html ? text : escapeHtml(text)}</span>${action ? `<button type="button" class="toast-action">${escapeHtml(action.label)}</button>` : ""}` +
|
|
2986
3026
|
(sticky ? `<button type="button" class="toast-close" title="Dismiss" aria-label="Dismiss">✕</button>` : "");
|
|
3027
|
+
placeToast();
|
|
2987
3028
|
el.hidden = false;
|
|
2988
3029
|
el.querySelector(".toast-action")?.addEventListener("click", () => {
|
|
2989
3030
|
el.hidden = true;
|
|
@@ -3043,6 +3084,7 @@ function applyStripHidden() {
|
|
|
3043
3084
|
document.body.classList.toggle("strip-hidden", stripAutoHidden({
|
|
3044
3085
|
saved: stripHiddenSaved(), short: shortWindow.matches, shownWhileShort: stripShownWhileShort,
|
|
3045
3086
|
}));
|
|
3087
|
+
updateSessionTitle();
|
|
3046
3088
|
}
|
|
3047
3089
|
function setStripHidden(hidden) {
|
|
3048
3090
|
if (hidden) familyPop.close();
|
|
@@ -45,8 +45,6 @@ main{flex:1;display:flex;flex-direction:column;gap:16px;padding:16px;overflow:au
|
|
|
45
45
|
#topStrip:empty{display:grid}
|
|
46
46
|
#center{flex:1;display:flex;flex-direction:column;min-height:45vh;background:var(--bg-panel);border:1px solid var(--border);border-radius:12px;overflow:hidden}
|
|
47
47
|
#stage{flex:1;display:flex;flex-direction:column;position:relative;min-height:160px;background:var(--bg);overflow:hidden}
|
|
48
|
-
#firstPeek{display:none;padding:8px 12px;background:var(--accent-bg);color:var(--text);border-bottom:1px solid var(--border);font-size:13px;line-height:1.4;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
49
|
-
#firstPeek.visible{display:block}
|
|
50
48
|
#composer #cancelBtn{order:2;background:var(--danger-bg)}
|
|
51
49
|
#history{flex:1;overflow:auto;padding:16px max(16px,calc((100% - 960px)/2));display:flex;flex-direction:column;gap:12px;background:var(--bg);min-height:160px}
|
|
52
50
|
/* No child shrinks to fit: a held (overflow:hidden) answer bubble would, clipped under the dock with nothing to scroll. */
|
|
@@ -158,6 +156,7 @@ button.task-stop{flex:none;margin-left:auto;padding:1px 8px;border:1px solid col
|
|
|
158
156
|
button.task-stop:hover:not(:disabled){background:var(--danger-bg-dim);filter:none}
|
|
159
157
|
.ts-now button.task-stop{align-self:center}
|
|
160
158
|
.activity-params{color:var(--text-muted);overflow-wrap:anywhere}
|
|
159
|
+
.activity-alias{color:var(--text-faint);white-space:nowrap}
|
|
161
160
|
.activity-duration{color:var(--text-faint);font-size:11px;font-variant-numeric:tabular-nums;white-space:nowrap}
|
|
162
161
|
.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}
|
|
163
162
|
/* A row's status and tool name never wrap (a phone broke them letter by
|
|
@@ -681,8 +680,6 @@ body.stage-view #dock,body.stage-view #dock:focus-within{background:transparent;
|
|
|
681
680
|
#infoBar .meta .dir{font-family:ui-monospace,monospace;font-size:11px;color:var(--text-muted);max-width:48ch;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;direction:rtl;text-align:left}
|
|
682
681
|
/* The footer sits on the card's glass: its chips are tinted, not solid, so the glass reads through them. */
|
|
683
682
|
#infoBar .meta .model{font-size:11px;padding:2px 6px;border-radius:6px;background:color-mix(in srgb,var(--bg-hover) 55%,transparent);color:var(--text-3)}
|
|
684
|
-
/* The memory chip ("mem 2", when the session used some): phones only, where the "mem: …" line is hidden. */
|
|
685
|
-
#infoBar .meta .mem-chip{display:none}
|
|
686
683
|
#infoBar .meta .model.served-mismatch{background:var(--warn-bg);color:var(--warn-bright);border:1px solid var(--warn-border)}
|
|
687
684
|
/* The open session's worker runs an older chi: its version, and Restart when it can. */
|
|
688
685
|
#infoBar .meta .worker-badge{display:inline-flex;align-items:center;gap:6px;font-size:11px;padding:2px 2px 2px 6px;border-radius:6px;background:var(--warn-bg);color:var(--warn-bright);border:1px solid var(--warn-border);white-space:nowrap}
|
|
@@ -690,9 +687,6 @@ body.stage-view #dock,body.stage-view #dock:focus-within{background:transparent;
|
|
|
690
687
|
#infoBar .meta .worker-restart{padding:1px 8px;border-radius:5px;font-size:11px;font-weight:600;background:var(--bg-raised);color:var(--warn-bright);border:1px solid var(--warn-border)}
|
|
691
688
|
#infoBar .meta .worker-restart:hover:not(:disabled){background:var(--warn-border);color:var(--on-warn)}
|
|
692
689
|
#infoBar .meta .worker-restart:disabled{opacity:.7;cursor:default}
|
|
693
|
-
#infoBar .preview-line{font-size:13px;word-break:break-word;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:1;line-clamp:1;overflow:hidden;line-height:1.4;max-height:1.4em}
|
|
694
|
-
#infoBar .preview-line .preview, #infoBar .preview-line .mem{word-break:break-word}
|
|
695
|
-
#infoBar .mem{font-size:12px;color:var(--text-muted)}
|
|
696
690
|
#infoBar button{padding:4px 12px;border-radius:8px;font-size:12px;font-weight:600}
|
|
697
691
|
#infoBar #infoStopBtn{background:var(--bg-raised);color:var(--warn-text);border:1px solid var(--border-strong)}
|
|
698
692
|
#infoBar #infoStopBtn:hover{background:var(--warn-bg);border-color:var(--warn-border)}
|
|
@@ -744,12 +738,11 @@ body.stage-view #dock,body.stage-view #dock:focus-within{background:transparent;
|
|
|
744
738
|
.ctx-pop-actions button:hover{color:var(--text-hover)}
|
|
745
739
|
.ctx-pop-actions .ctx-pop-detach{color:var(--text-muted);background:transparent}
|
|
746
740
|
.ctx-pop-actions .ctx-pop-detach:hover{color:var(--danger-text);border-color:color-mix(in srgb,var(--danger) 55%,transparent)}
|
|
747
|
-
/* Phones: the footer is one line; its meta scrolls sideways (all of it stays reachable)
|
|
741
|
+
/* Phones: the footer is one line; its meta scrolls sideways (all of it stays reachable). */
|
|
748
742
|
@media(max-width:600px){
|
|
749
743
|
/* The key hint: cut off at this width, and phones have no Shift+Enter. */
|
|
750
744
|
#composeHint{display:none}
|
|
751
745
|
#infoBar{flex-wrap:nowrap;align-items:center;gap:6px;padding:6px 10px 8px}
|
|
752
|
-
#infoBar .preview-line{display:none}
|
|
753
746
|
/* No terminal on a phone to attach from. */
|
|
754
747
|
#infoBar .meta .copy-attach{display:none}
|
|
755
748
|
#infoBar .meta{flex-wrap:nowrap;overflow-x:auto;scrollbar-width:none;white-space:nowrap}
|
|
@@ -757,8 +750,8 @@ body.stage-view #dock,body.stage-view #dock:focus-within{background:transparent;
|
|
|
757
750
|
#infoBar .meta>*{flex-shrink:0}
|
|
758
751
|
#infoBar .meta .id{order:-2}
|
|
759
752
|
#infoBar .meta .status{order:-1}
|
|
760
|
-
/*
|
|
761
|
-
#infoBar .meta .mem-chip{
|
|
753
|
+
/* The memory chip right after the status (same order, later in the DOM): the meta scrolls, and only its start is in sight. */
|
|
754
|
+
#infoBar .meta .mem-chip{order:-1}
|
|
762
755
|
/* A parent's children chip too: its waiting part is what to see. */
|
|
763
756
|
#infoBar .meta .children-chip{order:-1}
|
|
764
757
|
/* A chain's ← → links too: the way to the other days. */
|
|
@@ -825,6 +818,14 @@ body.has-session #scope .scope-where{display:none}
|
|
|
825
818
|
/* The ▾ glyph draws tiny at the pill's 11 px: at 20 px it is about as tall
|
|
826
819
|
as the pill text's capitals (phone and desktop alike). */
|
|
827
820
|
/* The bell (notifications): quiet at the top bar's right end; filled in the accent when on, struck through when the browser blocks it. */
|
|
821
|
+
/* The open session's title, when no strip card in sight shows it (session_title.js): the bar's middle, cut with an ellipsis; the whole first message in its tooltip. */
|
|
822
|
+
#sessionTitle{flex:1;min-width:0;font-size:13px;color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
823
|
+
#sessionTitle[hidden]{display:none}
|
|
824
|
+
/* A phone: the title is its own row under the bar, so "chi + · pill · bell" stay one line. */
|
|
825
|
+
@media(max-width:600px){
|
|
826
|
+
#topBar{flex-wrap:wrap;row-gap:2px}
|
|
827
|
+
#sessionTitle{flex-basis:100%;order:10;font-size:12px}
|
|
828
|
+
}
|
|
828
829
|
#notifyBtn{margin-left:auto;flex-shrink:0;display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;padding:0;min-height:0;background:transparent;border:1px solid transparent;border-radius:999px;color:var(--text-muted)}
|
|
829
830
|
#notifyBtn[hidden]{display:none}
|
|
830
831
|
#notifyBtn:hover{color:var(--text-2);border-color:var(--border)}
|
|
@@ -947,7 +948,8 @@ body.all-view #center, body.all-view #topStrip{display:none}
|
|
|
947
948
|
.card.archived{opacity:.6}
|
|
948
949
|
.card.archived:hover{opacity:.85}
|
|
949
950
|
.card .archived-badge,.family-row .archived-badge{font-size:10px;padding:1px 6px;border-radius:6px;border:1px solid var(--border-strong);color:var(--text-muted)}
|
|
950
|
-
|
|
951
|
+
/* bottom: above the session's composer card when one shows (--toast-bottom, app.js placeToast). */
|
|
952
|
+
#toast{position:fixed;left:50%;bottom:var(--toast-bottom,24px);transform:translateX(-50%);width:max-content;max-width:calc(100vw - 32px);box-sizing:border-box;z-index:50;display:flex;gap:12px;align-items:center;padding:8px 14px;border-radius:10px;background:var(--bg-raised);color:var(--text);border:1px solid var(--border-strong);box-shadow:var(--toast-shadow);font-size:13px}
|
|
951
953
|
#toast[hidden]{display:none}
|
|
952
954
|
#toast .toast-action{background:transparent;border:0;color:var(--accent);font-weight:600;cursor:pointer;padding:2px 4px}
|
|
953
955
|
#toast .toast-close{background:transparent;border:0;color:var(--text-muted);cursor:pointer;padding:2px 4px;margin-left:-6px}
|
|
@@ -1016,11 +1018,11 @@ a{color:var(--accent)}
|
|
|
1016
1018
|
<div id="scope" hidden></div>
|
|
1017
1019
|
<button type="button" class="strip-toggle" id="stripShow" title="Show the latest sessions"><span class="pill-arrow" aria-hidden="true">▾</span> sessions</button>
|
|
1018
1020
|
<button type="button" class="strip-toggle" id="stripHideTop" title="Hide the latest sessions"><span class="pill-arrow" aria-hidden="true">▴</span> hide</button>
|
|
1021
|
+
<span id="sessionTitle" hidden></span>
|
|
1019
1022
|
<button type="button" id="notifyBtn" aria-label="Notifications" aria-pressed="false" hidden><svg viewBox="0 0 16 16" width="15" height="15" aria-hidden="true"><path class="bell" d="M8 1.75a4 4 0 0 0-4 4v2.6L2.75 10.6v.9h10.5v-.9L12 8.35v-2.6a4 4 0 0 0-4-4Z"/><path class="clapper" d="M6.5 13a1.5 1.5 0 0 0 3 0"/><path class="slash" d="M2.5 2.5l11 11"/></svg></button>
|
|
1020
1023
|
</div>
|
|
1021
1024
|
<section id="topStrip" aria-label="Latest sessions" class="loading"><div class="card skeleton"><div class="line long"></div><div class="meta"><span class="line tiny" style="display:inline-block"></span></div></div><div class="card skeleton"><div class="line long"></div><div class="meta"><span class="line tiny" style="display:inline-block"></span></div></div><div class="card skeleton"><div class="line long"></div><div class="meta"><span class="line tiny" style="display:inline-block"></span></div></div></section>
|
|
1022
1025
|
<section id="center">
|
|
1023
|
-
<div id="firstPeek"></div>
|
|
1024
1026
|
<div id="stage">
|
|
1025
1027
|
<div id="markdownWarning" class="markdown-warning" hidden></div>
|
|
1026
1028
|
<div id="history"></div>
|
|
@@ -1045,7 +1047,7 @@ a{color:var(--accent)}
|
|
|
1045
1047
|
<button type="button" id="modelPick" class="model-pick" title="The model the new chat starts on" aria-label="Model for the new chat" aria-haspopup="listbox" aria-expanded="false" hidden><span class="model-pick-name"></span></button>
|
|
1046
1048
|
<button type="button" id="llmContextNew" class="llmctx-new" aria-haspopup="dialog" hidden></button>
|
|
1047
1049
|
<div id="composeHint">Enter to start · Shift+Enter for a newline</div>
|
|
1048
|
-
<button id="cancelBtn" title="Cancel the running turn, whoever sent it" style="display:none">
|
|
1050
|
+
<button id="cancelBtn" title="Cancel the running turn, whoever sent it" style="display:none">Stop turn</button>
|
|
1049
1051
|
<div class="split">
|
|
1050
1052
|
<button id="actionBtn">Start</button>
|
|
1051
1053
|
</div>
|
|
@@ -1056,7 +1058,7 @@ a{color:var(--accent)}
|
|
|
1056
1058
|
<div id="contextChips" aria-label="Attached context" hidden></div>
|
|
1057
1059
|
<button class="ghost" id="infoContinueBtn" title="Continue: start the next link of this session's chain from its recap (this one is archived)" style="display:none">continue →</button>
|
|
1058
1060
|
<button class="ghost" id="infoArchiveBtn" title="Archive: hide from the lists, keep for good" style="display:none">archive</button>
|
|
1059
|
-
<button class="ghost" id="infoStopBtn" title="Stop session (terminate worker)" style="display:none">stop</button>
|
|
1061
|
+
<button class="ghost" id="infoStopBtn" title="Stop session (terminate worker)" style="display:none">stop session</button>
|
|
1060
1062
|
<button class="ghost" id="infoDeleteBtn" title="Delete session (for good)" style="display:none">delete</button>
|
|
1061
1063
|
</div>
|
|
1062
1064
|
</div>
|
|
@@ -4,15 +4,24 @@
|
|
|
4
4
|
// on (a provider retry, plugins' setup) and how many turns this page saw
|
|
5
5
|
// end. The DOM, the clock and the interval come in, so `node --test` drives
|
|
6
6
|
// it with a fake element.
|
|
7
|
-
import { elapsedSince, turnTimingText } from "./timing.js";
|
|
7
|
+
import { elapsedSince, liveDurationText, turnTimingText } from "./timing.js";
|
|
8
8
|
|
|
9
9
|
// The live line's text, or null when the turn's start is unknown. +inStage+:
|
|
10
10
|
// the stage's status row already says the turn runs, so the line drops its
|
|
11
|
-
// own "running" there.
|
|
11
|
+
// own "running" there. The duration is the live one (never "0ms": under a
|
|
12
|
+
// second it shows tenths).
|
|
12
13
|
export function liveLineText({ number, startedAt, note = "", inStage = false }, now = Date.now()) {
|
|
13
14
|
const duration = elapsedSince(startedAt, now);
|
|
14
15
|
if (duration === null) return null;
|
|
15
|
-
|
|
16
|
+
const turn = Number.isInteger(number) && number > 0 ? `turn ${number}` : "turn";
|
|
17
|
+
return `${turn}${!inStage ? " running" : ""} · ${liveDurationText(duration)}${note ? ` · ${note}` : ""}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// The live ticker's interval at +elapsedMs+ into the turn: 200 ms during the
|
|
21
|
+
// first ten seconds (the live text counts up from the first second), then
|
|
22
|
+
// the one-second tick.
|
|
23
|
+
export function liveTickMs(elapsedMs) {
|
|
24
|
+
return Number(elapsedMs) < 10000 ? 200 : 1000;
|
|
16
25
|
}
|
|
17
26
|
|
|
18
27
|
// The record a turn-end read finishes its line with: the one with the
|
|
@@ -52,18 +61,36 @@ export function createLiveTiming({
|
|
|
52
61
|
// the next one, so a turn queued behind another reads its own number while
|
|
53
62
|
// the other's re-read is still out.
|
|
54
63
|
let ended = 0;
|
|
64
|
+
// The ticker's current interval, so a tick that crossed the 10 s boundary
|
|
65
|
+
// re-arms it at the slower one.
|
|
66
|
+
let tickerMs = null;
|
|
55
67
|
|
|
56
68
|
function stopTicker() {
|
|
57
69
|
if (ticker !== null) {
|
|
58
70
|
cancel(ticker);
|
|
59
71
|
ticker = null;
|
|
72
|
+
tickerMs = null;
|
|
60
73
|
}
|
|
61
74
|
}
|
|
62
75
|
|
|
76
|
+
function armTicker(ms) {
|
|
77
|
+
stopTicker();
|
|
78
|
+
ticker = every(tick, ms);
|
|
79
|
+
tickerMs = ms;
|
|
80
|
+
}
|
|
81
|
+
|
|
63
82
|
function tick() {
|
|
64
|
-
const
|
|
83
|
+
const startedAt = timing().activeTurn?.started_at;
|
|
84
|
+
const text = liveLineText({ number, startedAt, note, inStage }, now());
|
|
65
85
|
if (el && text !== null) el.textContent = text;
|
|
66
86
|
onTick();
|
|
87
|
+
// The interval follows the elapsed time: 200 ms while the line counts
|
|
88
|
+
// up from the first second, one a second once it is on whole seconds.
|
|
89
|
+
if (ticker !== null) {
|
|
90
|
+
const elapsed = elapsedSince(startedAt, now());
|
|
91
|
+
const ms = elapsed === null ? tickerMs : liveTickMs(elapsed);
|
|
92
|
+
if (ms !== tickerMs) armTicker(ms);
|
|
93
|
+
}
|
|
67
94
|
}
|
|
68
95
|
|
|
69
96
|
return {
|
|
@@ -87,7 +114,7 @@ export function createLiveTiming({
|
|
|
87
114
|
el.className = "turn-timing live";
|
|
88
115
|
inStage = !!place(el);
|
|
89
116
|
tick();
|
|
90
|
-
|
|
117
|
+
armTicker(liveTickMs(0));
|
|
91
118
|
},
|
|
92
119
|
|
|
93
120
|
tick,
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { normalize } from "./format.js";
|
|
2
|
+
|
|
3
|
+
// Where the open session's title shows: once on screen. Its strip card says
|
|
4
|
+
// it when the strip is shown and has an `.active` card for it; otherwise
|
|
5
|
+
// (strip hidden, an older session without a card, a delegate folded into its
|
|
6
|
+
// parent's card) the top bar does. The All sessions view shows none.
|
|
7
|
+
// Plain rules, no DOM; app.js wires them (updateSessionTitle).
|
|
8
|
+
|
|
9
|
+
const TAB_TITLE_CHARS = 40;
|
|
10
|
+
|
|
11
|
+
// The top bar's text, or "" when it shows no title.
|
|
12
|
+
export function topBarTitle({ title, cardShown, stripHidden, allView }) {
|
|
13
|
+
const text = normalize(title);
|
|
14
|
+
if (!text || allView) return "";
|
|
15
|
+
if (cardShown && !stripHidden) return "";
|
|
16
|
+
return text;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// The browser tab: "<title, cut at 40> · <base>", the base alone without a
|
|
20
|
+
// session title (base = "Chi · <project>" or "Chi").
|
|
21
|
+
export function tabTitle({ title, base }) {
|
|
22
|
+
const text = normalize(title);
|
|
23
|
+
if (!text) return base;
|
|
24
|
+
const cut = text.length > TAB_TITLE_CHARS ? `${text.slice(0, TAB_TITLE_CHARS)}…` : text;
|
|
25
|
+
return `${cut} · ${base}`;
|
|
26
|
+
}
|
|
@@ -108,6 +108,21 @@ function withTask(slot, row) {
|
|
|
108
108
|
return row.tool === "task_wait" && row.view?.task_id ? { ...slot, taskId: row.view.task_id, key: row.key } : slot;
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
// A trail item's text and its hover: the text the (CSS-truncated) item shows,
|
|
112
|
+
// and the full text it hovers. A call hovers its full command when it has
|
|
113
|
+
// one, else its whole "<mark> <name> <title>" text; a flash (a hook notice)
|
|
114
|
+
// hovers its whole "<mark> <text>" line.
|
|
115
|
+
export function trailItemText(item) {
|
|
116
|
+
const text = `${trailMark(item.status)} ${item.name}${item.title ? ` ${item.title}` : ""}`;
|
|
117
|
+
return { text, hover: item.command || text };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// A trail flash's text and hover: the whole "<mark> <text>" line.
|
|
121
|
+
export function trailFlashText(flash, mark) {
|
|
122
|
+
const text = `${mark} ${flash.text}`;
|
|
123
|
+
return { text, hover: text };
|
|
124
|
+
}
|
|
125
|
+
|
|
111
126
|
// While the first step has no thinking and no tool rows the turn is plain:
|
|
112
127
|
// its text streams in the stage's answer slot, as the turn view's
|
|
113
128
|
// chrome-less block streams like a bubble.
|
|
@@ -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,
|
|
14
|
+
import { flashOf, handOffDue, handOffOrder, headlineOf, inUseFrom, isPlain, liveSlots, placeFor, trailFlashText, trailItemText, trailTone } from "./stage_model.js";
|
|
15
15
|
import { formatDuration } from "./timing.js";
|
|
16
16
|
import { taskCommand, taskStopButton } from "./task_stop.js";
|
|
17
17
|
|
|
@@ -265,12 +265,18 @@ export function createStageView({ historyEl, appendToHistory, isNearBottom: hist
|
|
|
265
265
|
if (shown.trail === key) return;
|
|
266
266
|
shown.trail = key;
|
|
267
267
|
trail.replaceChildren();
|
|
268
|
-
if (flashing)
|
|
268
|
+
if (flashing) {
|
|
269
|
+
const flash = trailFlashText(flashing, FLASH_MARKS[flashing.kind]);
|
|
270
|
+
const el = node("span", `ts-trail-item ${flashing.kind}`, trail);
|
|
271
|
+
el.textContent = flash.text;
|
|
272
|
+
el.title = flash.hover;
|
|
273
|
+
}
|
|
269
274
|
for (const item of items) {
|
|
270
275
|
const tone = trailTone(item.status);
|
|
271
276
|
const el = node("span", `ts-trail-item${tone ? ` ${tone}` : ""}`, trail);
|
|
272
|
-
|
|
273
|
-
|
|
277
|
+
const { text, hover } = trailItemText(item);
|
|
278
|
+
el.textContent = text;
|
|
279
|
+
el.title = hover;
|
|
274
280
|
}
|
|
275
281
|
if (!flashing && !items.length) node("span", "ts-trail-item none", trail).textContent = "no tool calls yet";
|
|
276
282
|
}
|
|
@@ -18,13 +18,11 @@ export function stripColumns(width) {
|
|
|
18
18
|
// And how many wait on the user (notify.js waitingOn: a question, an
|
|
19
19
|
// approval, a card), so a hidden strip still says one needs an answer.
|
|
20
20
|
// Plain text parts; live and waiting are "" when none. Zero sessions keep
|
|
21
|
-
// the bare label.
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
// session, a waiting delegate is still one thing waiting on the user
|
|
25
|
-
export function stripShowParts(sessions, { cards = null } = {}) {
|
|
21
|
+
// the bare label. The count is sessions (a folded delegate counts), the
|
|
22
|
+
// number the "All sessions" tile shows.
|
|
23
|
+
export function stripShowParts(sessions) {
|
|
26
24
|
const list = sessions || [];
|
|
27
|
-
const n =
|
|
25
|
+
const n = list.length;
|
|
28
26
|
const liveN = list.filter((s) => s && s.owner === "worker").length;
|
|
29
27
|
const waitingN = list.filter((s) => waitingOn(s)).length;
|
|
30
28
|
return {
|
|
@@ -20,6 +20,17 @@ export function turnTimingText(number, durationMs, { running = false, canceled =
|
|
|
20
20
|
return `${turn}${running ? " running" : ""} · ${formatDuration(durationMs)}${canceled ? " · canceled" : ""}`;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
// The LIVE line's duration: never milliseconds — under a second it shows
|
|
24
|
+
// tenths ("0.0s", "0.2s"), then what formatDuration prints from 1 s on
|
|
25
|
+
// (its 0.1 s precision holds to 10 s). A finished turn's line keeps
|
|
26
|
+
// formatDuration exactly as it is.
|
|
27
|
+
export function liveDurationText(durationMs) {
|
|
28
|
+
const ms = Number(durationMs);
|
|
29
|
+
if (!Number.isFinite(ms) || ms < 0) return "";
|
|
30
|
+
if (ms < 1000) return `${(ms / 1000).toFixed(1)}s`;
|
|
31
|
+
return formatDuration(ms);
|
|
32
|
+
}
|
|
33
|
+
|
|
23
34
|
// A cancel's reason as the terminal UIs name it too (Formatting::
|
|
24
35
|
// CANCEL_REASONS); anything else as it is. Both sides: spec/shared/labels_matrix.json.
|
|
25
36
|
const CANCEL_REASONS = { ctrl_c: "Ctrl-C", user: "stopped", hook: "by a hook" };
|