samagotchi 0.50.0 → 0.52.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 +89 -1
- data/README.md +1 -1
- data/bin/chi +11 -3
- data/docs/architecture.md +1 -1
- data/docs/cli.md +40 -11
- data/docs/configuration.md +63 -21
- data/docs/hooks.md +3 -2
- data/docs/internals/engine.md +4 -2
- data/docs/memory.md +11 -0
- data/docs/plugins.md +2 -1
- data/docs/sessions.md +10 -5
- data/docs/testing.md +6 -1
- data/lib/samagotchi/bootstrap/config_writer.rb +6 -37
- data/lib/samagotchi/bridge/ring_buffer.rb +0 -5
- data/lib/samagotchi/bridge/turn_accumulator.rb +3 -1
- data/lib/samagotchi/bridge.rb +29 -17
- data/lib/samagotchi/bridge_client.rb +3 -1
- data/lib/samagotchi/broadcast/recipients.rb +1 -2
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +1 -1
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/children_status.rb +1 -1
- data/lib/samagotchi/commands/registry.rb +2 -1
- data/lib/samagotchi/config.rb +50 -182
- data/lib/samagotchi/config_text_edit.rb +8 -32
- data/lib/samagotchi/context_sources.rb +0 -7
- data/lib/samagotchi/context_window.rb +2 -2
- data/lib/samagotchi/continue_offer.rb +5 -4
- data/lib/samagotchi/cut_policy.rb +0 -1
- data/lib/samagotchi/delivery.rb +56 -0
- data/lib/samagotchi/engine.rb +48 -17
- data/lib/samagotchi/engine_peers.rb +1 -0
- data/lib/samagotchi/host_config.rb +183 -0
- data/lib/samagotchi/host_registry.rb +21 -13
- data/lib/samagotchi/kernel_loop.rb +28 -2
- data/lib/samagotchi/last_turn.rb +48 -0
- data/lib/samagotchi/live_versions.rb +0 -6
- data/lib/samagotchi/llm/native_tool_normalizer.rb +2 -7
- data/lib/samagotchi/llm_context_override.rb +3 -22
- data/lib/samagotchi/llm_context_strategy.rb +50 -16
- data/lib/samagotchi/memory_bundle/placeholder.rb +0 -5
- data/lib/samagotchi/model_profile.rb +4 -4
- data/lib/samagotchi/model_settings.rb +46 -0
- data/lib/samagotchi/plugin/context.rb +3 -3
- data/lib/samagotchi/plugin/sessions.rb +2 -2
- data/lib/samagotchi/queued_command.rb +35 -0
- data/lib/samagotchi/reminder_store.rb +0 -27
- data/lib/samagotchi/reply_wait.rb +11 -13
- data/lib/samagotchi/sampling_settings.rb +4 -4
- data/lib/samagotchi/self_report.rb +8 -5
- data/lib/samagotchi/send_command.rb +92 -21
- data/lib/samagotchi/session.rb +45 -24
- data/lib/samagotchi/session_commands.rb +41 -3
- data/lib/samagotchi/session_inbox.rb +36 -7
- data/lib/samagotchi/session_manager.rb +21 -10
- data/lib/samagotchi/session_metrics.rb +25 -6
- data/lib/samagotchi/session_setup.rb +30 -0
- data/lib/samagotchi/sessions_command.rb +1 -1
- data/lib/samagotchi/steer_cut.rb +21 -10
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +10 -4
- data/lib/samagotchi/terminal_ui/attached_loop.rb +48 -1
- data/lib/samagotchi/terminal_ui/formatting.rb +69 -4
- data/lib/samagotchi/terminal_ui.rb +56 -8
- data/lib/samagotchi/thinking.rb +67 -7
- data/lib/samagotchi/thinking_command.rb +72 -0
- 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/tools/builtins.rb +7 -2
- data/lib/samagotchi/tools/peers.rb +3 -2
- data/lib/samagotchi/tools/task_runtime.rb +1 -1
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/vision_support.rb +4 -3
- data/lib/samagotchi/web/app.rb +59 -7
- 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 +199 -48
- data/lib/samagotchi/web/public/data.js +8 -4
- data/lib/samagotchi/web/public/format.js +8 -4
- data/lib/samagotchi/web/public/index.html +25 -20
- data/lib/samagotchi/web/public/kept_commands.js +52 -0
- data/lib/samagotchi/web/public/model_pick.js +3 -1
- data/lib/samagotchi/web/public/session_title.js +26 -0
- data/lib/samagotchi/web/public/strip.js +4 -6
- data/lib/samagotchi/web/public/thinking_chip.js +169 -0
- data/lib/samagotchi/web/public/timing.js +15 -0
- data/lib/samagotchi/web/public/turn_events.js +26 -2
- data/lib/samagotchi/web/public/turn_html.js +3 -1
- data/lib/samagotchi/web/public/turn_view.js +328 -310
- data/lib/samagotchi/web/public/vendor/lit-html.js +8 -0
- data/lib/samagotchi/web/session_summary.rb +1 -1
- data/lib/samagotchi/worker.rb +44 -43
- data/lib/samagotchi/worker_inbound.rb +10 -7
- data/lib/samagotchi/worker_wakes.rb +0 -2
- data/lib/samagotchi/yaml_lines.rb +72 -0
- data/lib/samagotchi.rb +1 -0
- metadata +13 -1
|
@@ -26,14 +26,18 @@ import {
|
|
|
26
26
|
chipHtml as llmContextChipHtml, chipModel as llmContextChipModel, choiceWords, createLlmContextControl, NEW_CHAT,
|
|
27
27
|
withChoice,
|
|
28
28
|
} from "./llm_context_chip.js";
|
|
29
|
+
import {
|
|
30
|
+
chipHtml as thinkingChipHtml, chipModel as thinkingChipModel, createThinkingControl,
|
|
31
|
+
withChoice as withThinkingChoice,
|
|
32
|
+
} from "./thinking_chip.js";
|
|
29
33
|
import { renderStable } from "./stable_html.js";
|
|
30
34
|
import { createShownTexts } from "./shown_texts.js";
|
|
31
35
|
import { MODEL_KEY } from "./model_pick.js";
|
|
32
36
|
import { emptySessionState } from "./session_state.js";
|
|
33
37
|
import { createModelPicker } from "./model_picker.js";
|
|
34
38
|
import { chipName, chipsHtml, imageFiles, restoredChips, thumbsHtml, turnRefs, turnText } from "./images.js";
|
|
35
|
-
import { escapeHtml, memChipText, memChipTitle, noteHtml, normalize, steerRowHtml, ownerBadge, previewForCard, relativeTime, failedTurnText, modelLabel, deleteConfirmText, recapLabel, canStopSession, goneSessionNotice, withLiveStatus, withoutWorker, recapPlace, delegatedBy, childrenSummary, promptNotesChip } from "./format.js";
|
|
36
|
-
import { createTurnView,
|
|
39
|
+
import { escapeHtml, memChipText, memChipTitle, noteHtml, normalize, steerRowHtml, ownerBadge, previewForCard, firstPreview, relativeTime, failedTurnText, modelLabel, deleteConfirmText, recapLabel, canStopSession, goneSessionNotice, withLiveStatus, withoutWorker, recapPlace, delegatedBy, childrenSummary, promptNotesChip } from "./format.js";
|
|
40
|
+
import { createTurnView, turnHistoryNodes } from "./turn_view.js";
|
|
37
41
|
import { BADGES, emptyAnswerHtml, htmlElement, stateBadgeHtml, userBubbleHtml } from "./turn_html.js";
|
|
38
42
|
import { createStageView } from "./stage_view.js";
|
|
39
43
|
import { createFamilyPop, familyChipHtml, familyRowsHtml } from "./families_view.js";
|
|
@@ -56,13 +60,15 @@ import { shouldFollowScroll, keepFollowing } from "./scroll.js";
|
|
|
56
60
|
import { SHORT_WINDOW_QUERY, stripAutoHidden, stripColumns, stripShowParts } from "./strip.js";
|
|
57
61
|
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
62
|
import { createNotifications } from "./notifications.js";
|
|
63
|
+
import { tabTitle, topBarTitle } from "./session_title.js";
|
|
59
64
|
import { restartConfirmText, versionNotice, workerBadge } from "./update_notice.js";
|
|
65
|
+
import { anchorInsertIndex, commandAnchor, isShellCommandLine, keepsCommandBubble } from "./kept_commands.js";
|
|
60
66
|
import {
|
|
61
|
-
appendAboveLiveTiming, cancelLineText,
|
|
67
|
+
appendAboveLiveTiming, cancelLineText, formatDuration, mergeTiming, normalizeTiming, sessionDurationNow, STOPPED_LINE_CLASS,
|
|
62
68
|
} from "./timing.js";
|
|
63
69
|
import { createLiveTurn } from "./live_turn.js";
|
|
64
70
|
import { DONE_MS, applyInitEvent, dropDone, initRowHtml, newInitTasks, seedInitTasks } from "./init_row.js";
|
|
65
|
-
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";
|
|
71
|
+
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, steerChoiceRoute, steerCutLine, startPageReplyOrHint, turnOutput, unknownCommandHint, webLocalReply, workerGoneText } from "./turn_events.js";
|
|
66
72
|
|
|
67
73
|
const $ = (s) => document.querySelector(s);
|
|
68
74
|
const topStripEl = $("#topStrip");
|
|
@@ -71,7 +77,11 @@ const historyEl = $("#history");
|
|
|
71
77
|
const emptyEl = $("#empty");
|
|
72
78
|
const composerEl = $("#composer");
|
|
73
79
|
const infoBarEl = $("#infoBar");
|
|
74
|
-
|
|
80
|
+
// The open session's title in the top bar, when no strip card in sight
|
|
81
|
+
// shows it; the tab's base ("Chi · <project>"), set by renderScope.
|
|
82
|
+
const sessionTitleEl = $("#sessionTitle");
|
|
83
|
+
let tabBase = null;
|
|
84
|
+
let tabShown = null;
|
|
75
85
|
const infoTextEl = $("#infoText");
|
|
76
86
|
// The open session's attached context, in its own row of the session bar
|
|
77
87
|
// (updateInfoBar redraws #infoText every tick; the chips keep their popover).
|
|
@@ -112,6 +122,43 @@ const llmContextNewControl = createLlmContextControl({
|
|
|
112
122
|
id: "llmContextNewPopover",
|
|
113
123
|
anchor: "#llmContextNew",
|
|
114
124
|
});
|
|
125
|
+
// The open session's thinking chip, beside the llm ctx one: /thinking runs
|
|
126
|
+
// in the session's worker.
|
|
127
|
+
const thinkingControl = createThinkingControl({
|
|
128
|
+
el: infoTextEl,
|
|
129
|
+
escapeHtml,
|
|
130
|
+
summary: () => (selected ? current.thinking : null),
|
|
131
|
+
session: () => selected,
|
|
132
|
+
running: () => liveTurn.running,
|
|
133
|
+
run: (line) => runSessionCommand(line),
|
|
134
|
+
});
|
|
135
|
+
// The start page's thinking chip: the picked model's level, and a level
|
|
136
|
+
// for the next new chat only (sent with its create, then dropped).
|
|
137
|
+
const thinkingNewEl = $("#thinkingNew");
|
|
138
|
+
let newChatThinking = null;
|
|
139
|
+
const newChatThinkingSummary = () => (modelPickEl.hidden ? null : modelPicker.chosenRow()?.thinking || null);
|
|
140
|
+
const thinkingNewControl = createThinkingControl({
|
|
141
|
+
el: thinkingNewEl,
|
|
142
|
+
escapeHtml,
|
|
143
|
+
summary: newChatThinkingSummary,
|
|
144
|
+
kind: NEW_CHAT,
|
|
145
|
+
choice: () => newChatThinking,
|
|
146
|
+
choose: (next) => {
|
|
147
|
+
newChatThinking = next;
|
|
148
|
+
renderNewChatThinking();
|
|
149
|
+
promptEl.focus();
|
|
150
|
+
},
|
|
151
|
+
id: "thinkingNewPopover",
|
|
152
|
+
anchor: "#thinkingNew",
|
|
153
|
+
});
|
|
154
|
+
function renderNewChatThinking() {
|
|
155
|
+
const chip = thinkingChipModel(withThinkingChoice(newChatThinkingSummary(), newChatThinking), { kind: NEW_CHAT });
|
|
156
|
+
thinkingNewEl.hidden = !chip;
|
|
157
|
+
if (!chip) return thinkingNewControl.close();
|
|
158
|
+
thinkingNewEl.textContent = chip.text;
|
|
159
|
+
thinkingNewEl.title = chip.title;
|
|
160
|
+
thinkingNewEl.classList.toggle("own", chip.own);
|
|
161
|
+
}
|
|
115
162
|
// Hidden without the model's values (the list not in, an older server).
|
|
116
163
|
function renderNewChatLlmContext() {
|
|
117
164
|
const chip = llmContextChipModel(withChoice(newChatLlmSummary(), newChatLlmContext), { kind: NEW_CHAT });
|
|
@@ -342,7 +389,9 @@ function renderScope() {
|
|
|
342
389
|
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
390
|
}
|
|
344
391
|
el.hidden = false;
|
|
345
|
-
|
|
392
|
+
// The tab: "<session title> · Chi · <project>" (updateSessionTitle).
|
|
393
|
+
tabBase = scope.projectName ? `Chi · ${scope.projectName}` : "Chi";
|
|
394
|
+
updateSessionTitle();
|
|
346
395
|
// The links follow the hash (an open session, the all-sessions view).
|
|
347
396
|
el.querySelector(".scope-all:not(.scope-back)")?.addEventListener("click", (e) => {
|
|
348
397
|
e.currentTarget.href = allScopeHref(location.hash, scope.dir, location.search);
|
|
@@ -642,6 +691,8 @@ function paintActive() {
|
|
|
642
691
|
document.querySelectorAll(".family-row[data-id]").forEach((el) => {
|
|
643
692
|
el.classList.toggle("active", !!selected && el.dataset.id === selected);
|
|
644
693
|
});
|
|
694
|
+
// The open session's card may have come into the strip or left it.
|
|
695
|
+
updateSessionTitle();
|
|
645
696
|
}
|
|
646
697
|
|
|
647
698
|
// The latest sessions (as many as fit, a family one card), then the way
|
|
@@ -693,11 +744,11 @@ function noteChainFamilies(fams) {
|
|
|
693
744
|
}
|
|
694
745
|
|
|
695
746
|
// The hidden strip's pill: "▾ 5 sessions · ● 1 live · ● 1 waiting",
|
|
696
|
-
// following the list: its
|
|
697
|
-
//
|
|
747
|
+
// following the list: its sessions, the number the All sessions tile
|
|
748
|
+
// shows, and the live and waiting ones (a waiting delegate too).
|
|
698
749
|
function renderStripShow() {
|
|
699
750
|
const shown = shownSessions();
|
|
700
|
-
const { count, live, waiting } = stripShowParts(shown
|
|
751
|
+
const { count, live, waiting } = stripShowParts(shown);
|
|
701
752
|
const liveHtml = live ? ` · <span class="live"><span class="live-dot" aria-hidden="true"></span>${escapeHtml(live)}</span>` : "";
|
|
702
753
|
const waitingHtml = waiting ? ` · <span class="waiting"><span class="waiting-dot" aria-hidden="true"></span>${escapeHtml(waiting)}</span>` : "";
|
|
703
754
|
$("#stripShow").innerHTML = `<span class="pill-arrow" aria-hidden="true">\u25BE</span> ${escapeHtml(count)}${liveHtml}${waitingHtml}`;
|
|
@@ -999,6 +1050,7 @@ function applyView() {
|
|
|
999
1050
|
topStripEl.scrollLeft = 0; // a phone's strip scrolls sideways to the tile
|
|
1000
1051
|
}
|
|
1001
1052
|
document.body.classList.toggle("all-view", open);
|
|
1053
|
+
updateSessionTitle();
|
|
1002
1054
|
// Its chip was in the list just hidden (the strip, or all sessions).
|
|
1003
1055
|
chainPop.close();
|
|
1004
1056
|
if (open) {
|
|
@@ -1037,6 +1089,7 @@ function updateComposerMode() {
|
|
|
1037
1089
|
if (document.body.classList.toggle("has-session", !!selected) !== hadSession) {
|
|
1038
1090
|
hadSession = !hadSession;
|
|
1039
1091
|
fitPrompt();
|
|
1092
|
+
placeToast();
|
|
1040
1093
|
}
|
|
1041
1094
|
if (selected) {
|
|
1042
1095
|
modelPicker.close();
|
|
@@ -1046,6 +1099,11 @@ function updateComposerMode() {
|
|
|
1046
1099
|
newChatLlmContext = null;
|
|
1047
1100
|
renderNewChatLlmContext();
|
|
1048
1101
|
}
|
|
1102
|
+
thinkingNewControl.close();
|
|
1103
|
+
if (newChatThinking) {
|
|
1104
|
+
newChatThinking = null;
|
|
1105
|
+
renderNewChatThinking();
|
|
1106
|
+
}
|
|
1049
1107
|
}
|
|
1050
1108
|
const running = liveTurn.running && !!selected;
|
|
1051
1109
|
cancelBtn.style.display = running ? "" : "none";
|
|
@@ -1148,7 +1206,6 @@ function clearSelection() {
|
|
|
1148
1206
|
setCapabilities(null);
|
|
1149
1207
|
updateComposerMode();
|
|
1150
1208
|
updateInfoBar();
|
|
1151
|
-
updateFirstPeek();
|
|
1152
1209
|
promptEl.focus();
|
|
1153
1210
|
}
|
|
1154
1211
|
|
|
@@ -1182,7 +1239,6 @@ async function select(id) {
|
|
|
1182
1239
|
closeStream();
|
|
1183
1240
|
resetSessionState();
|
|
1184
1241
|
resetTurnView();
|
|
1185
|
-
updateFirstPeek();
|
|
1186
1242
|
updateInfoBar();
|
|
1187
1243
|
contextChips.load(id);
|
|
1188
1244
|
updateComposerMode();
|
|
@@ -1224,8 +1280,9 @@ async function resync({ endText = null } = {}) {
|
|
|
1224
1280
|
return;
|
|
1225
1281
|
}
|
|
1226
1282
|
if (id !== selected) return;
|
|
1283
|
+
const kept = keptCommandBubbles(id);
|
|
1227
1284
|
resetTurnView();
|
|
1228
|
-
renderSessionView(data);
|
|
1285
|
+
renderSessionView(data, { kept });
|
|
1229
1286
|
// Preview is read-only: a worker is woken only by POST /turn, so attach
|
|
1230
1287
|
// only when a live bridge exists.
|
|
1231
1288
|
selectSeq = typeof data.last_event_seq === "number" ? (data.last_event_id || data.last_event_seq) : null;
|
|
@@ -1240,10 +1297,45 @@ async function resync({ endText = null } = {}) {
|
|
|
1240
1297
|
}
|
|
1241
1298
|
}
|
|
1242
1299
|
|
|
1300
|
+
// The prompt and command bubbles in the history, in order, by kind
|
|
1301
|
+
// (kept_commands.js): a prompt, a `!cmd`'s (saved, so redrawn) or another.
|
|
1302
|
+
function historyAnchors() {
|
|
1303
|
+
const els = [...historyEl.querySelectorAll(".bubble.user, .bubble.command")];
|
|
1304
|
+
const kinds = els.map((el) => {
|
|
1305
|
+
if (el.classList.contains("user")) return "user";
|
|
1306
|
+
const line = el.querySelector(":scope > .command-line")?.textContent;
|
|
1307
|
+
return !el.classList.contains("failed") && isShellCommandLine(line) ? "shell" : "other";
|
|
1308
|
+
});
|
|
1309
|
+
return { els, kinds };
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
// This session's own command bubbles (/help, /model …: drawn by this page,
|
|
1313
|
+
// never saved) with their anchors, before a resync redraws the history.
|
|
1314
|
+
// In-flight ones (busy, queued) are left out: the snapshot draws them again.
|
|
1315
|
+
function keptCommandBubbles(id) {
|
|
1316
|
+
const { els, kinds } = historyAnchors();
|
|
1317
|
+
return els
|
|
1318
|
+
.map((el, i) => ({ el, anchor: commandAnchor(kinds, i) }))
|
|
1319
|
+
.filter(({ el }) => el.dataset.keptFor === id && !el.classList.contains("busy") && !el.classList.contains("queued"))
|
|
1320
|
+
.filter(({ el }) => keepsCommandBubble(el.querySelector(":scope > .command-line")?.textContent));
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
// The kept bubbles back into the redrawn history, each after its turn.
|
|
1324
|
+
function restoreCommandBubbles(kept) {
|
|
1325
|
+
if (!kept.length) return;
|
|
1326
|
+
historyEl.querySelector(".hint")?.remove();
|
|
1327
|
+
kept.forEach(({ el, anchor }) => {
|
|
1328
|
+
const { els, kinds } = historyAnchors();
|
|
1329
|
+
const at = anchorInsertIndex(kinds, anchor);
|
|
1330
|
+
if (at < els.length) els[at].before(el);
|
|
1331
|
+
else historyEl.appendChild(el);
|
|
1332
|
+
});
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1243
1335
|
// A session view: its messages, then the turn in progress and the queued
|
|
1244
1336
|
// prompts (a live worker's snapshot) replayed through the stream handlers,
|
|
1245
1337
|
// then a pending question.
|
|
1246
|
-
function renderSessionView(data) {
|
|
1338
|
+
function renderSessionView(data, { kept = [] } = {}) {
|
|
1247
1339
|
const s = data.session;
|
|
1248
1340
|
setMarkdownWarning(data.markdown_warning);
|
|
1249
1341
|
setCapabilities(data.capabilities);
|
|
@@ -1270,20 +1362,23 @@ function renderSessionView(data) {
|
|
|
1270
1362
|
current.promptNotes = Array.isArray(s.prompt_notes) ? s.prompt_notes : [];
|
|
1271
1363
|
current.llmContext = s.llm_context || null;
|
|
1272
1364
|
llmContextControl.refresh();
|
|
1365
|
+
current.thinking = s.thinking || null;
|
|
1366
|
+
thinkingControl.refresh();
|
|
1273
1367
|
current.parentId = s.parent_id || null;
|
|
1274
1368
|
current.continues = s.continues || null;
|
|
1275
1369
|
current.commands = Array.isArray(data.commands) ? data.commands : [];
|
|
1276
1370
|
if (data.messages && data.messages.length) {
|
|
1277
1371
|
const first = data.messages.find((m) => m.role === "user");
|
|
1278
|
-
if (first && first.content) current.firstPreview =
|
|
1372
|
+
if (first && first.content) current.firstPreview = firstPreview(first.content);
|
|
1279
1373
|
renderHistory(data.messages);
|
|
1280
1374
|
} else {
|
|
1281
1375
|
renderHistory(data.history || []);
|
|
1282
1376
|
if (!current.firstPreview && data.history && data.history.length) {
|
|
1283
|
-
const txt =
|
|
1284
|
-
if(txt) current.firstPreview = txt;
|
|
1377
|
+
const txt = firstPreview(data.history[0]);
|
|
1378
|
+
if (txt) current.firstPreview = txt;
|
|
1285
1379
|
}
|
|
1286
1380
|
}
|
|
1381
|
+
restoreCommandBubbles(kept);
|
|
1287
1382
|
// A failed turn's prompt went back to the user: shown after the
|
|
1288
1383
|
// conversation, marked failed, as it was live (prompt_restored).
|
|
1289
1384
|
if (data.failed_turn) {
|
|
@@ -1334,7 +1429,6 @@ function renderSessionView(data) {
|
|
|
1334
1429
|
// A turn ran out of iterations and waits for a yes or no.
|
|
1335
1430
|
if (data.continue_offer) questionCards.renderContinue(data.continue_offer);
|
|
1336
1431
|
updateInfoBar();
|
|
1337
|
-
updateFirstPeek();
|
|
1338
1432
|
updateComposerMode();
|
|
1339
1433
|
}
|
|
1340
1434
|
|
|
@@ -1344,7 +1438,7 @@ function renderHistory(items) {
|
|
|
1344
1438
|
return;
|
|
1345
1439
|
}
|
|
1346
1440
|
if (typeof items[0] === "object" && items[0].role) {
|
|
1347
|
-
historyEl.
|
|
1441
|
+
historyEl.replaceChildren(turnHistoryNodes(items, current.timing, { thumbs: (images) => thumbsHtml(selected, images) }));
|
|
1348
1442
|
} else {
|
|
1349
1443
|
historyEl.innerHTML = items.map((c) => `<div class="bubble output">${escapeHtml(c)}</div>`).join("");
|
|
1350
1444
|
}
|
|
@@ -1922,11 +2016,11 @@ function updateInfoBar() {
|
|
|
1922
2016
|
renderStable(infoTextEl, "");
|
|
1923
2017
|
infoTextEl.title = "";
|
|
1924
2018
|
if (stopBtn) stopBtn.style.display = "none";
|
|
2019
|
+
updateSessionTitle();
|
|
1925
2020
|
return;
|
|
1926
2021
|
}
|
|
1927
2022
|
if (stopBtn) stopBtn.style.display = canStopSession({ owner: current.owner, streaming: !!streamControl }) ? "" : "none";
|
|
1928
2023
|
const idShort = selected.slice(0,8);
|
|
1929
|
-
const memText = current.usedMemories.length ? current.usedMemories.join(", ") : "—";
|
|
1930
2024
|
const ctxText = ctxBarText(current.ctxPct, current.ctxWindowSource);
|
|
1931
2025
|
const tokens = current.timing.tokens;
|
|
1932
2026
|
const ctxTip = tokensTipText(tokens, undefined, current.timing.memoryIndex,
|
|
@@ -1957,16 +2051,38 @@ function updateInfoBar() {
|
|
|
1957
2051
|
const attachCmd = `chi --attach ${selected}`;
|
|
1958
2052
|
const copied = Date.now() - attachCopiedAt < 2000;
|
|
1959
2053
|
const copyHtml = `<button type="button" class="copy-attach" title="Copy "${escapeHtml(attachCmd)}" to attach a terminal">${copied ? "copied ✓" : "copy chi --attach"}</button>`;
|
|
1960
|
-
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>`;
|
|
2054
|
+
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)}${thinkingChipHtml(current.thinking, 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>`;
|
|
1966
2055
|
// The session clock ticks every second while a turn runs: only its text
|
|
1967
2056
|
// changes, the bar's other nodes (a tooltip, a button mid-press) stay.
|
|
1968
|
-
renderStable(infoTextEl, metaHtml
|
|
2057
|
+
renderStable(infoTextEl, metaHtml, { ".session-time": timingText });
|
|
1969
2058
|
infoTextEl.title = memoriesTooltip(current.usedMemories, current.preloadedMemories, current.mutedMemories);
|
|
2059
|
+
updateSessionTitle();
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
// The open session's title, once on screen (session_title.js): its strip
|
|
2063
|
+
// card says it when the strip is shown and has one; else the top bar. The
|
|
2064
|
+
// browser tab names it too. It follows the card's: a chain link opened with
|
|
2065
|
+
// the opener reads its chain's first link's title (cardTitle).
|
|
2066
|
+
function updateSessionTitle() {
|
|
2067
|
+
const shown = selected ? sessionsById().get(selected) : null;
|
|
2068
|
+
const first = current.firstPreview || "";
|
|
2069
|
+
const raw = !selected ? "" : (shown && isOpener(first) ? cardTitle(shown) : first);
|
|
2070
|
+
const title = raw === "\u2014" ? "" : raw; // "—": no first message yet
|
|
2071
|
+
const text = topBarTitle({
|
|
2072
|
+
title,
|
|
2073
|
+
cardShown: !!topStripEl.querySelector(".card.active"),
|
|
2074
|
+
stripHidden: document.body.classList.contains("strip-hidden"),
|
|
2075
|
+
allView: allViewOpen(),
|
|
2076
|
+
});
|
|
2077
|
+
if (sessionTitleEl.textContent !== text) sessionTitleEl.textContent = text;
|
|
2078
|
+
if (sessionTitleEl.hidden !== !text) sessionTitleEl.hidden = !text;
|
|
2079
|
+
const tip = text ? title : "";
|
|
2080
|
+
if (sessionTitleEl.title !== tip) sessionTitleEl.title = tip;
|
|
2081
|
+
const tab = tabBase === null ? null : tabTitle({ title, base: tabBase });
|
|
2082
|
+
if (tab !== null && tab !== tabShown) {
|
|
2083
|
+
tabShown = tab;
|
|
2084
|
+
notifications.setBaseTitle(tab);
|
|
2085
|
+
}
|
|
1970
2086
|
}
|
|
1971
2087
|
|
|
1972
2088
|
// The info bar's ← / → link to a chain's link (chains.js chainNeighbours):
|
|
@@ -1990,10 +2106,7 @@ function memoriesTooltip(used, preloaded, muted) {
|
|
|
1990
2106
|
}
|
|
1991
2107
|
|
|
1992
2108
|
function currentSessionDuration() {
|
|
1993
|
-
|
|
1994
|
-
return elapsedSince(current.timing.startedAt);
|
|
1995
|
-
}
|
|
1996
|
-
return current.timing.sessionDurationMs;
|
|
2109
|
+
return sessionDurationNow(current.timing, liveTurn.running);
|
|
1997
2110
|
}
|
|
1998
2111
|
|
|
1999
2112
|
// The stage takes a running turn's rows (its extras); else the history.
|
|
@@ -2024,16 +2137,6 @@ async function mergeTurnTiming(id, data) {
|
|
|
2024
2137
|
return true;
|
|
2025
2138
|
}
|
|
2026
2139
|
|
|
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
2140
|
function setUsedMemories(names) {
|
|
2038
2141
|
current.usedMemories = names.slice();
|
|
2039
2142
|
updateInfoBar();
|
|
@@ -2119,7 +2222,6 @@ const streamHandlers = {
|
|
|
2119
2222
|
if (pct !== null && !Number.isNaN(pct)) {
|
|
2120
2223
|
current.ctxPct = pct;
|
|
2121
2224
|
updateInfoBar();
|
|
2122
|
-
updateFirstPeek();
|
|
2123
2225
|
}
|
|
2124
2226
|
},
|
|
2125
2227
|
generation_started: (data) => {
|
|
@@ -2262,6 +2364,12 @@ const streamHandlers = {
|
|
|
2262
2364
|
updateInfoBar();
|
|
2263
2365
|
llmContextControl.refresh();
|
|
2264
2366
|
}
|
|
2367
|
+
// /thinking (or /model) changed the level the next turn runs at.
|
|
2368
|
+
if (data.thinking) {
|
|
2369
|
+
current.thinking = data.thinking;
|
|
2370
|
+
updateInfoBar();
|
|
2371
|
+
thinkingControl.refresh();
|
|
2372
|
+
}
|
|
2265
2373
|
if (view.resync) await resync();
|
|
2266
2374
|
// A card's action that isn't anytime (the step-limit question's
|
|
2267
2375
|
// /continue) has no bubble: nothing to remove. An anytime or queued
|
|
@@ -2308,7 +2416,6 @@ const streamHandlers = {
|
|
|
2308
2416
|
if (pct !== null) {
|
|
2309
2417
|
current.ctxPct = pct;
|
|
2310
2418
|
updateInfoBar();
|
|
2311
|
-
updateFirstPeek();
|
|
2312
2419
|
}
|
|
2313
2420
|
},
|
|
2314
2421
|
question_requested: (data) => {
|
|
@@ -2415,10 +2522,27 @@ function handleCreate() {
|
|
|
2415
2522
|
|
|
2416
2523
|
async function handleSendTurn() {
|
|
2417
2524
|
if (current.owner === "tui") return;
|
|
2418
|
-
|
|
2525
|
+
let prompt = turnText(promptEl.value, chips);
|
|
2419
2526
|
if (!prompt) return;
|
|
2420
2527
|
// An image-only send's text is its placeholders: not for ↑.
|
|
2421
2528
|
const typed = promptEl.value.trim() !== "";
|
|
2529
|
+
// /cut TEXT and /queue TEXT (the terminals' words for a delivery): the
|
|
2530
|
+
// text as a cut, or the page's own answer (/queue, the bare word).
|
|
2531
|
+
let delivery = null;
|
|
2532
|
+
const choice = steerChoiceRoute(prompt, { running: liveTurn.running && !!selected });
|
|
2533
|
+
if (choice?.reply) {
|
|
2534
|
+
if (selected) addCommandBubble({ label: null, line: prompt, text: choice.reply });
|
|
2535
|
+
else showNotice(choice.reply);
|
|
2536
|
+
promptEl.value = "";
|
|
2537
|
+
fitPrompt();
|
|
2538
|
+
return;
|
|
2539
|
+
}
|
|
2540
|
+
// The composer holds the line as typed until the ack (composerAfterAck).
|
|
2541
|
+
const line = prompt;
|
|
2542
|
+
if (choice) {
|
|
2543
|
+
prompt = choice.send;
|
|
2544
|
+
delivery = choice.delivery || null;
|
|
2545
|
+
}
|
|
2422
2546
|
if (!selected) {
|
|
2423
2547
|
// The start page's own reply (a web-local command) or the hint for a
|
|
2424
2548
|
// typo: neither needs a session, so none is made for it (2.29).
|
|
@@ -2432,10 +2556,13 @@ async function handleSendTurn() {
|
|
|
2432
2556
|
actionBtn.disabled = true;
|
|
2433
2557
|
try {
|
|
2434
2558
|
const s = await createIdleSession({ dir: scope.dir, model: chosenModel(), preview: prompt,
|
|
2435
|
-
llm_context: choiceWords(newChatLlmContext)
|
|
2559
|
+
llm_context: choiceWords(newChatLlmContext),
|
|
2560
|
+
thinking: newChatThinking || undefined });
|
|
2436
2561
|
// The choice was for this chat only.
|
|
2437
2562
|
newChatLlmContext = null;
|
|
2438
2563
|
renderNewChatLlmContext();
|
|
2564
|
+
newChatThinking = null;
|
|
2565
|
+
renderNewChatThinking();
|
|
2439
2566
|
addCreatedSession(s);
|
|
2440
2567
|
await select(s.id);
|
|
2441
2568
|
} catch (e) {
|
|
@@ -2467,16 +2594,15 @@ async function handleSendTurn() {
|
|
|
2467
2594
|
const echo = makeUserBubble(prompt, { state: "queued", own: true, images: sending });
|
|
2468
2595
|
if (!turnDom.holds?.()) historyEl.scrollTop = historyEl.scrollHeight;
|
|
2469
2596
|
if (!current.firstPreview || current.firstPreview==="—") {
|
|
2470
|
-
current.firstPreview =
|
|
2597
|
+
current.firstPreview = firstPreview(prompt);
|
|
2471
2598
|
updateInfoBar();
|
|
2472
|
-
updateFirstPeek();
|
|
2473
2599
|
}
|
|
2474
2600
|
actionBtn.disabled = true;
|
|
2475
2601
|
try {
|
|
2476
2602
|
for (const chip of sending) {
|
|
2477
2603
|
if (!chip.ref) chip.ref = await uploadImage(selected, chip.file, chip.name);
|
|
2478
2604
|
}
|
|
2479
|
-
const ack = await sendTurn(selected, prompt, { clientId, images: turnRefs(sending), history: typed });
|
|
2605
|
+
const ack = await sendTurn(selected, prompt, { clientId, images: turnRefs(sending), history: typed, delivery });
|
|
2480
2606
|
if (ack?.command_id) {
|
|
2481
2607
|
// The worker knew it as a command (its list was newer than ours):
|
|
2482
2608
|
// its command_ran shows it, not a prompt bubble.
|
|
@@ -2492,7 +2618,7 @@ async function handleSendTurn() {
|
|
|
2492
2618
|
// (and images) go back into the composer, without replacing text typed
|
|
2493
2619
|
// since (4.13). The composer still holds the text just sent: it goes.
|
|
2494
2620
|
const restored = restoreOnAck(ack?.enqueued_id, earlyRestores, { myId: clientId, sentIds });
|
|
2495
|
-
promptEl.value = composerAfterAck({ sent:
|
|
2621
|
+
promptEl.value = composerAfterAck({ sent: line, current: promptEl.value, refill: restored?.refill || "" });
|
|
2496
2622
|
fitPrompt();
|
|
2497
2623
|
clearChips();
|
|
2498
2624
|
if (restored?.images?.length) setChips(restoredChips(selected, restored.images));
|
|
@@ -2572,6 +2698,8 @@ async function runSessionCommand(line) {
|
|
|
2572
2698
|
function addCommandBubble(view) {
|
|
2573
2699
|
removeHintIfEmpty();
|
|
2574
2700
|
const div = document.createElement("div");
|
|
2701
|
+
// Page-only: kept across a resync of this session (keptCommandBubbles).
|
|
2702
|
+
if (selected) div.dataset.keptFor = selected;
|
|
2575
2703
|
drawCommandBubble(div, view);
|
|
2576
2704
|
const follow = isNearBottom();
|
|
2577
2705
|
appendToHistory(div);
|
|
@@ -2650,6 +2778,7 @@ const modelPicker = createModelPicker({
|
|
|
2650
2778
|
center: $("#center"),
|
|
2651
2779
|
onPick: () => {
|
|
2652
2780
|
renderNewChatLlmContext();
|
|
2781
|
+
renderNewChatThinking();
|
|
2653
2782
|
promptEl.focus();
|
|
2654
2783
|
},
|
|
2655
2784
|
});
|
|
@@ -2666,6 +2795,7 @@ async function loadModels() {
|
|
|
2666
2795
|
modelPickEl.title = payload.warning ? `The model the new chat starts on · ${payload.warning}` : "The model the new chat starts on";
|
|
2667
2796
|
modelPickEl.hidden = false;
|
|
2668
2797
|
renderNewChatLlmContext();
|
|
2798
|
+
renderNewChatThinking();
|
|
2669
2799
|
}
|
|
2670
2800
|
// The picker's choice for a create request; nothing (the server's default)
|
|
2671
2801
|
// while the list is not in.
|
|
@@ -2771,7 +2901,9 @@ new ResizeObserver(() => {
|
|
|
2771
2901
|
const follow = atHistoryEnd();
|
|
2772
2902
|
centerEl.style.setProperty("--dock-h", `${dockEl.offsetHeight}px`);
|
|
2773
2903
|
if (follow) historyEl.scrollTop = historyEl.scrollHeight;
|
|
2904
|
+
placeToast();
|
|
2774
2905
|
}).observe(dockEl);
|
|
2906
|
+
window.addEventListener("resize", () => placeToast());
|
|
2775
2907
|
|
|
2776
2908
|
promptEl.addEventListener("paste", (e) => {
|
|
2777
2909
|
const files = imageFiles(e.clipboardData);
|
|
@@ -2981,6 +3113,23 @@ async function setArchived(id, archive, { undo = true } = {}) {
|
|
|
2981
3113
|
showToast(`${what} ${short}`, undo ? { label: "Undo", run: () => setArchived(id, !archive, { undo: false }) } : null);
|
|
2982
3114
|
}
|
|
2983
3115
|
|
|
3116
|
+
// The toast sits above the session's composer card (measured: it grows with
|
|
3117
|
+
// a wrapping footer or a taller box), so it never covers the card's
|
|
3118
|
+
// controls; with no card in sight (the start page, all sessions) the CSS's
|
|
3119
|
+
// 24 px from the bottom.
|
|
3120
|
+
function placeToast() {
|
|
3121
|
+
const gap = 12;
|
|
3122
|
+
const dock = document.getElementById("dock");
|
|
3123
|
+
const r = dock.getBoundingClientRect();
|
|
3124
|
+
const over = document.body.classList.contains("has-session") && r.height > 0;
|
|
3125
|
+
const bottom = over ? `${Math.round(window.innerHeight - r.top + gap)}px` : "";
|
|
3126
|
+
const root = document.documentElement.style;
|
|
3127
|
+
if (root.getPropertyValue("--toast-bottom") !== bottom) {
|
|
3128
|
+
if (bottom) root.setProperty("--toast-bottom", bottom);
|
|
3129
|
+
else root.removeProperty("--toast-bottom");
|
|
3130
|
+
}
|
|
3131
|
+
}
|
|
3132
|
+
|
|
2984
3133
|
// sticky: stays until its action or its ✕ (else gone after 6 s).
|
|
2985
3134
|
let toastTimer = null;
|
|
2986
3135
|
// html: +text+ is markup this page built (its parts escaped), not words.
|
|
@@ -2988,6 +3137,7 @@ function showToast(text, action = null, { sticky = false, html = false } = {}) {
|
|
|
2988
3137
|
const el = $("#toast");
|
|
2989
3138
|
el.innerHTML = `<span>${html ? text : escapeHtml(text)}</span>${action ? `<button type="button" class="toast-action">${escapeHtml(action.label)}</button>` : ""}` +
|
|
2990
3139
|
(sticky ? `<button type="button" class="toast-close" title="Dismiss" aria-label="Dismiss">✕</button>` : "");
|
|
3140
|
+
placeToast();
|
|
2991
3141
|
el.hidden = false;
|
|
2992
3142
|
el.querySelector(".toast-action")?.addEventListener("click", () => {
|
|
2993
3143
|
el.hidden = true;
|
|
@@ -3047,6 +3197,7 @@ function applyStripHidden() {
|
|
|
3047
3197
|
document.body.classList.toggle("strip-hidden", stripAutoHidden({
|
|
3048
3198
|
saved: stripHiddenSaved(), short: shortWindow.matches, shownWhileShort: stripShownWhileShort,
|
|
3049
3199
|
}));
|
|
3200
|
+
updateSessionTitle();
|
|
3050
3201
|
}
|
|
3051
3202
|
function setStripHidden(hidden) {
|
|
3052
3203
|
if (hidden) familyPop.close();
|
|
@@ -35,10 +35,11 @@ export function listSessions(sort, order, { dir, ...opts } = {}) {
|
|
|
35
35
|
// it); either left out means the server's default. preview: that message,
|
|
36
36
|
// which names the session until its turn is saved. llm_context: the
|
|
37
37
|
// session's own LLM context words ({strategy, apply, budget}), when the
|
|
38
|
-
// start page's chip set some
|
|
39
|
-
|
|
38
|
+
// start page's chip set some; thinking: its own level ("low"), when the
|
|
39
|
+
// start page's think chip set one.
|
|
40
|
+
export function createIdleSession({ dir, model, preview, llm_context: llmContext, thinking, ...opts } = {}) {
|
|
40
41
|
const body = { idle: true, ...(dir ? { dir } : {}), ...(model ? { model } : {}), ...(preview ? { preview } : {}),
|
|
41
|
-
...(llmContext ? { llm_context: llmContext } : {}) };
|
|
42
|
+
...(llmContext ? { llm_context: llmContext } : {}), ...(thinking ? { thinking } : {}) };
|
|
42
43
|
return api("/api/sessions", { method: "POST", body: JSON.stringify(body), ...opts });
|
|
43
44
|
}
|
|
44
45
|
|
|
@@ -90,9 +91,12 @@ export function getSession(id, { tail, recent, turnId, timing, parts, cards, ...
|
|
|
90
91
|
// opts.images: refs ({file, name}) to images uploaded into the session.
|
|
91
92
|
// history: false keeps the prompt out of the shared prompt history (an
|
|
92
93
|
// image-only send, whose text is only its [image: …] placeholders).
|
|
93
|
-
|
|
94
|
+
// delivery: what the message does to a running turn ("cut": /cut TEXT);
|
|
95
|
+
// none = the server's default (the next step).
|
|
96
|
+
export function sendTurn(id, prompt, { clientId, images, history = true, delivery = null, ...opts } = {}) {
|
|
94
97
|
const body = clientId ? { prompt, client_id: clientId } : { prompt };
|
|
95
98
|
if (images?.length) body.images = images;
|
|
99
|
+
if (delivery) body.delivery = delivery;
|
|
96
100
|
if (history === false) body.history = false;
|
|
97
101
|
return api(`/api/sessions/${id}/turn`, {
|
|
98
102
|
method: "POST",
|
|
@@ -94,13 +94,17 @@ export function previewOf(lastPrompt) {
|
|
|
94
94
|
return normalized;
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
// A session's first message as the info bar's title: whitespace collapsed,
|
|
98
|
+
// cut at FIRST_PREVIEW_CHARS with an ellipsis ("" for none).
|
|
99
|
+
export function firstPreview(text) {
|
|
100
|
+
const n = normalize(text);
|
|
101
|
+
return n.length > FIRST_PREVIEW_CHARS ? `${n.slice(0, FIRST_PREVIEW_CHARS)}\u2026` : n;
|
|
102
|
+
}
|
|
103
|
+
|
|
97
104
|
export function previewForCard(session) {
|
|
98
105
|
// Unified card preview: first message (80) else previewOf(last_prompt)
|
|
99
106
|
const raw = session.first_preview || session.last_prompt || "";
|
|
100
|
-
|
|
101
|
-
if (!n) return "\u2014";
|
|
102
|
-
if (n.length > FIRST_PREVIEW_CHARS) return `${n.slice(0, FIRST_PREVIEW_CHARS)}\u2026`;
|
|
103
|
-
return n;
|
|
107
|
+
return firstPreview(raw) || "\u2014";
|
|
104
108
|
}
|
|
105
109
|
|
|
106
110
|
const DELETE_PREVIEW_CHARS = 40;
|