samagotchi 0.41.0 → 0.43.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 +56 -1
- data/docs/cli.md +8 -6
- data/docs/configuration.md +8 -0
- data/docs/context.md +46 -0
- data/docs/memory.md +18 -0
- data/docs/plugins.md +27 -5
- data/docs/sessions.md +5 -5
- data/docs/testing.md +16 -0
- data/lib/samagotchi/bundles/github-pr/manifest.yml +4 -4
- data/lib/samagotchi/bundles/github-pr/plugin.rb +177 -1
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/child_reports.rb +8 -4
- data/lib/samagotchi/context_command.rb +5 -2
- data/lib/samagotchi/context_sources.rb +40 -0
- data/lib/samagotchi/engine.rb +31 -1
- data/lib/samagotchi/model_notes.rb +7 -4
- data/lib/samagotchi/plugin/attached_context.rb +75 -11
- data/lib/samagotchi/prompt_note.rb +53 -0
- data/lib/samagotchi/self_report.rb +32 -0
- data/lib/samagotchi/session.rb +16 -1
- data/lib/samagotchi/session_commands.rb +24 -5
- data/lib/samagotchi/session_metrics.rb +21 -4
- data/lib/samagotchi/system_prompt.rb +14 -1
- data/lib/samagotchi/terminal_ui/attached_loop.rb +12 -0
- data/lib/samagotchi/terminal_ui/formatting.rb +4 -0
- data/lib/samagotchi/tool_declarations.rb +2 -1
- data/lib/samagotchi/turn_flow.rb +8 -0
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +55 -8
- data/lib/samagotchi/web/public/app.js +73 -11
- data/lib/samagotchi/web/public/ctx.js +27 -2
- data/lib/samagotchi/web/public/data.js +6 -3
- data/lib/samagotchi/web/public/format.js +24 -0
- data/lib/samagotchi/web/public/index.html +8 -0
- data/lib/samagotchi/web/public/llm_context_chip.js +151 -21
- data/lib/samagotchi/web/public/model_pick.js +3 -1
- data/lib/samagotchi/web/public/model_picker.js +2 -0
- data/lib/samagotchi/web/public/session_state.js +5 -0
- data/lib/samagotchi/web/public/timing.js +3 -1
- data/lib/samagotchi/web/public/turn_view.js +15 -2
- data/lib/samagotchi/web/session_summary.rb +2 -0
- data/lib/samagotchi/worker.rb +16 -4
- data/lib/samagotchi/worker_wakes.rb +2 -1
- metadata +2 -1
|
@@ -21,14 +21,17 @@ import {
|
|
|
21
21
|
addContext,
|
|
22
22
|
} from "./data.js";
|
|
23
23
|
import { createContextChips } from "./context_chips.js";
|
|
24
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
chipHtml as llmContextChipHtml, chipModel as llmContextChipModel, choiceWords, createLlmContextControl, NEW_CHAT,
|
|
26
|
+
withChoice,
|
|
27
|
+
} from "./llm_context_chip.js";
|
|
25
28
|
import { renderStable } from "./stable_html.js";
|
|
26
29
|
import { createShownTexts } from "./shown_texts.js";
|
|
27
30
|
import { MODEL_KEY } from "./model_pick.js";
|
|
28
31
|
import { emptySessionState } from "./session_state.js";
|
|
29
32
|
import { createModelPicker } from "./model_picker.js";
|
|
30
33
|
import { chipName, chipsHtml, imageFiles, restoredChips, thumbsHtml, turnRefs, turnText } from "./images.js";
|
|
31
|
-
import { escapeHtml, memChipText, memChipTitle, noteHtml, normalize, steerRowHtml, ownerBadge, previewForCard, relativeTime, failedTurnText, modelLabel, deleteConfirmText, recapLabel, canStopSession, goneSessionNotice, withLiveStatus, withoutWorker, recapPlace, delegatedBy, childrenSummary } from "./format.js";
|
|
34
|
+
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";
|
|
32
35
|
import { createTurnView, turnHistoryHtml } from "./turn_view.js";
|
|
33
36
|
import { BADGES, emptyAnswerHtml, htmlElement, stateBadgeHtml, userBubbleHtml } from "./turn_html.js";
|
|
34
37
|
import { createStageView } from "./stage_view.js";
|
|
@@ -39,7 +42,7 @@ import { parsePresets } from "./annotate_presets.js";
|
|
|
39
42
|
import { createAnnotateBar } from "./annotate_bar.js";
|
|
40
43
|
import { COPY_SOURCE_ATTR, copySource, copyText, installCopy, showsAnswer } from "./copy.js";
|
|
41
44
|
import { routeChunk } from "./chunk_router.js";
|
|
42
|
-
import { extractCtxPct, savedCtxPct, cardCtxText, generationTokens, lastSpeedText, speedText, tokensTipText } from "./ctx.js";
|
|
45
|
+
import { extractCtxPct, savedCtxPct, cardCtxText, ctxBarText, generationTokens, lastSpeedText, speedText, tokensTipText } from "./ctx.js";
|
|
43
46
|
import { createQuestionCards } from "./question_cards.js";
|
|
44
47
|
import { cardClass, cardInnerHtml, cardPlace, isCard, isNoticeCard, noticeInnerHtml, shownElsewhere, splitSnapshotCards, turnNoticePlace } from "./card.js";
|
|
45
48
|
import { PROMPT_LINE_PX, PROMPT_MIN_PX, gripFloor, promptHeight } from "./composer_size.js";
|
|
@@ -86,6 +89,36 @@ const llmContextControl = createLlmContextControl({
|
|
|
86
89
|
running: () => liveTurn.running,
|
|
87
90
|
run: (line) => runSessionCommand(line),
|
|
88
91
|
});
|
|
92
|
+
// The start page's LLM context chip, beside the model picker: the picked
|
|
93
|
+
// model's values, and a choice for the next new chat only (sent with its
|
|
94
|
+
// create, then dropped; never remembered: a reload or opening a session
|
|
95
|
+
// drops it too, a failed create keeps it).
|
|
96
|
+
const llmContextNewEl = $("#llmContextNew");
|
|
97
|
+
let newChatLlmContext = null;
|
|
98
|
+
const newChatLlmSummary = () => (modelPickEl.hidden ? null : modelPicker.chosenRow()?.llm_context || null);
|
|
99
|
+
const llmContextNewControl = createLlmContextControl({
|
|
100
|
+
el: llmContextNewEl,
|
|
101
|
+
escapeHtml,
|
|
102
|
+
summary: newChatLlmSummary,
|
|
103
|
+
kind: NEW_CHAT,
|
|
104
|
+
choice: () => newChatLlmContext,
|
|
105
|
+
choose: (next) => {
|
|
106
|
+
newChatLlmContext = next;
|
|
107
|
+
renderNewChatLlmContext();
|
|
108
|
+
promptEl.focus();
|
|
109
|
+
},
|
|
110
|
+
id: "llmContextNewPopover",
|
|
111
|
+
anchor: "#llmContextNew",
|
|
112
|
+
});
|
|
113
|
+
// Hidden without the model's values (the list not in, an older server).
|
|
114
|
+
function renderNewChatLlmContext() {
|
|
115
|
+
const chip = llmContextChipModel(withChoice(newChatLlmSummary(), newChatLlmContext), { kind: NEW_CHAT });
|
|
116
|
+
llmContextNewEl.hidden = !chip;
|
|
117
|
+
if (!chip) return llmContextNewControl.close();
|
|
118
|
+
llmContextNewEl.textContent = chip.text;
|
|
119
|
+
llmContextNewEl.title = chip.title;
|
|
120
|
+
llmContextNewEl.classList.toggle("own", chip.own);
|
|
121
|
+
}
|
|
89
122
|
const markdownWarningEl = $("#markdownWarning");
|
|
90
123
|
// What the page renders that a notice may stand in for (the server's
|
|
91
124
|
// Web::Capabilities, next to markdown_warning); a notice it names isn't
|
|
@@ -908,7 +941,15 @@ function updateComposerMode() {
|
|
|
908
941
|
hadSession = !hadSession;
|
|
909
942
|
fitPrompt();
|
|
910
943
|
}
|
|
911
|
-
if (selected)
|
|
944
|
+
if (selected) {
|
|
945
|
+
modelPicker.close();
|
|
946
|
+
llmContextNewControl.close();
|
|
947
|
+
// The start page's choice was for a new chat: opening a session drops it.
|
|
948
|
+
if (newChatLlmContext) {
|
|
949
|
+
newChatLlmContext = null;
|
|
950
|
+
renderNewChatLlmContext();
|
|
951
|
+
}
|
|
952
|
+
}
|
|
912
953
|
const running = liveTurn.running && !!selected;
|
|
913
954
|
cancelBtn.style.display = running ? "" : "none";
|
|
914
955
|
if (running) {
|
|
@@ -1121,11 +1162,15 @@ function renderSessionView(data) {
|
|
|
1121
1162
|
// next turn (a running turn's replay below streams over it).
|
|
1122
1163
|
const savedPct = savedCtxPct(data.timing?.context);
|
|
1123
1164
|
if (savedPct !== null) current.ctxPct = savedPct;
|
|
1124
|
-
if (data.timing?.context?.window_tokens)
|
|
1165
|
+
if (data.timing?.context?.window_tokens) {
|
|
1166
|
+
current.ctxWindow = data.timing.context.window_tokens;
|
|
1167
|
+
current.ctxWindowSource = data.timing.context.window_source || null;
|
|
1168
|
+
}
|
|
1125
1169
|
current.firstPreview = s.first_preview || previewForCard(s);
|
|
1126
1170
|
current.usedMemories = s.used_memory_names || [];
|
|
1127
1171
|
current.preloadedMemories = s.preloaded_memory_names || [];
|
|
1128
1172
|
current.mutedMemories = s.muted_memory_names || [];
|
|
1173
|
+
current.promptNotes = Array.isArray(s.prompt_notes) ? s.prompt_notes : [];
|
|
1129
1174
|
current.llmContext = s.llm_context || null;
|
|
1130
1175
|
llmContextControl.refresh();
|
|
1131
1176
|
current.parentId = s.parent_id || null;
|
|
@@ -1232,6 +1277,8 @@ async function readTurnEnd() {
|
|
|
1232
1277
|
if (id !== selected) return;
|
|
1233
1278
|
if (d.session) {
|
|
1234
1279
|
if (Array.isArray(d.session.used_memory_names)) current.usedMemories = d.session.used_memory_names;
|
|
1280
|
+
// The notes chip: the first turn's build (or a /model) records them.
|
|
1281
|
+
if (Array.isArray(d.session.prompt_notes)) current.promptNotes = d.session.prompt_notes;
|
|
1235
1282
|
if (d.session.status) current.status = d.session.status;
|
|
1236
1283
|
if (!(await mergeTurnTiming(id, d.timing))) return;
|
|
1237
1284
|
liveTurn.timing.finishLine(line);
|
|
@@ -1763,9 +1810,10 @@ function updateInfoBar() {
|
|
|
1763
1810
|
if (stopBtn) stopBtn.style.display = canStopSession({ owner: current.owner, streaming: !!streamControl }) ? "" : "none";
|
|
1764
1811
|
const idShort = selected.slice(0,8);
|
|
1765
1812
|
const memText = current.usedMemories.length ? current.usedMemories.join(", ") : "—";
|
|
1766
|
-
const ctxText = current.ctxPct
|
|
1813
|
+
const ctxText = ctxBarText(current.ctxPct, current.ctxWindowSource);
|
|
1767
1814
|
const tokens = current.timing.tokens;
|
|
1768
|
-
const ctxTip = tokensTipText(tokens, undefined, current.timing.memoryIndex
|
|
1815
|
+
const ctxTip = tokensTipText(tokens, undefined, current.timing.memoryIndex,
|
|
1816
|
+
{ tokens: current.ctxWindow, source: current.ctxWindowSource, pct: current.ctxPct });
|
|
1769
1817
|
const speedTextNow = lastSpeedText(tokens);
|
|
1770
1818
|
const avgSpeed = speedText(tokens?.avg_decode_tps, tokens?.tps_source);
|
|
1771
1819
|
const speedTip = `Decode speed of the last generation${avgSpeed ? `; session average ${avgSpeed}` : ""}`;
|
|
@@ -1778,11 +1826,14 @@ function updateInfoBar() {
|
|
|
1778
1826
|
const parentHtml = parentChip ? `<a class="model parent" href="${escapeHtml(sessionHash(current.parentId))}" title="${escapeHtml(parentChip.title)}">delegated by ${escapeHtml(String(current.parentId).slice(0, 8))}</a>` : "";
|
|
1779
1827
|
// A parent names its delegates; the link opens all sessions.
|
|
1780
1828
|
const kids = childrenSummary({ id: selected }, allSessions);
|
|
1829
|
+
// The model notes the session's prompt carried.
|
|
1830
|
+
const notes = promptNotesChip(current.promptNotes);
|
|
1831
|
+
const notesHtml = notes ? `<span class="model notes-chip" title="${escapeHtml(notes.title)}">${escapeHtml(notes.text)}</span>` : "";
|
|
1781
1832
|
const kidsHtml = kids ? `<a class="model children-chip${kids.waitingText ? " waiting" : ""}" href="${escapeHtml(ALL_SESSIONS_HASH)}" title="${escapeHtml(`${kids.title} (all sessions)`)}">${escapeHtml(kids.text)}${kids.waitingText ? ` · <span class="kids-waiting">${escapeHtml(kids.waitingText)}</span>` : ""}</a>` : "";
|
|
1782
1833
|
const attachCmd = `chi --attach ${selected}`;
|
|
1783
1834
|
const copied = Date.now() - attachCopiedAt < 2000;
|
|
1784
1835
|
const copyHtml = `<button type="button" class="copy-attach" title="Copy "${escapeHtml(attachCmd)}" to attach a terminal">${copied ? "copied ✓" : "copy chi --attach"}</button>`;
|
|
1785
|
-
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>${parentHtml}${kidsHtml}${llmContextChipHtml(current.llmContext, escapeHtml)}${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>`;
|
|
1836
|
+
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>${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>`;
|
|
1786
1837
|
const previewHtml = `<div class="preview-line"><span class="preview">${escapeHtml(current.firstPreview || "—")}</span> · <span class="mem" title="${escapeHtml(current.usedMemories.join(", "))}">mem: ${escapeHtml(memText)}</span></div>`;
|
|
1787
1838
|
// The session clock ticks every second while a turn runs: only its text
|
|
1788
1839
|
// changes, the bar's other nodes (a tooltip, a button mid-press) stay.
|
|
@@ -1933,7 +1984,10 @@ const streamHandlers = {
|
|
|
1933
1984
|
}
|
|
1934
1985
|
},
|
|
1935
1986
|
generation_started: (data) => {
|
|
1936
|
-
if (data?.context_window_tokens)
|
|
1987
|
+
if (data?.context_window_tokens) {
|
|
1988
|
+
current.ctxWindow = data.context_window_tokens;
|
|
1989
|
+
current.ctxWindowSource = data.context_window_source || null;
|
|
1990
|
+
}
|
|
1937
1991
|
liveTurn.timing.setNote("");
|
|
1938
1992
|
turnDom.generationStarted(data || {});
|
|
1939
1993
|
},
|
|
@@ -2220,7 +2274,11 @@ async function handleSendTurn() {
|
|
|
2220
2274
|
}
|
|
2221
2275
|
actionBtn.disabled = true;
|
|
2222
2276
|
try {
|
|
2223
|
-
const s = await createIdleSession({ dir: scope.dir, model: chosenModel(), preview: prompt
|
|
2277
|
+
const s = await createIdleSession({ dir: scope.dir, model: chosenModel(), preview: prompt,
|
|
2278
|
+
llm_context: choiceWords(newChatLlmContext) });
|
|
2279
|
+
// The choice was for this chat only.
|
|
2280
|
+
newChatLlmContext = null;
|
|
2281
|
+
renderNewChatLlmContext();
|
|
2224
2282
|
addCreatedSession(s);
|
|
2225
2283
|
await select(s.id);
|
|
2226
2284
|
} catch (e) {
|
|
@@ -2433,7 +2491,10 @@ const modelPicker = createModelPicker({
|
|
|
2433
2491
|
list: $("#modelList"),
|
|
2434
2492
|
composer: $("#composer"),
|
|
2435
2493
|
center: $("#center"),
|
|
2436
|
-
onPick: () =>
|
|
2494
|
+
onPick: () => {
|
|
2495
|
+
renderNewChatLlmContext();
|
|
2496
|
+
promptEl.focus();
|
|
2497
|
+
},
|
|
2437
2498
|
});
|
|
2438
2499
|
async function loadModels() {
|
|
2439
2500
|
let payload;
|
|
@@ -2447,6 +2508,7 @@ async function loadModels() {
|
|
|
2447
2508
|
if (!modelPicker.setModels(payload, stored)) return;
|
|
2448
2509
|
modelPickEl.title = payload.warning ? `The model the new chat starts on · ${payload.warning}` : "The model the new chat starts on";
|
|
2449
2510
|
modelPickEl.hidden = false;
|
|
2511
|
+
renderNewChatLlmContext();
|
|
2450
2512
|
}
|
|
2451
2513
|
// The picker's choice for a create request; nothing (the server's default)
|
|
2452
2514
|
// while the list is not in.
|
|
@@ -91,14 +91,39 @@ export function memoryIndexText(memoryIndex) {
|
|
|
91
91
|
return `~${countK(total)} tokens in this session's prompt (${scopes.map(([scope, tokens]) => `${scope} ${countK(tokens)}`).join(", ")})`;
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
// The info bar's ctx: "ctx 12%", "ctx ~12%" when the window is chi's
|
|
95
|
+
// default (no server, model list or setting gave one, so the percent is of
|
|
96
|
+
// a guess); "" without a pct.
|
|
97
|
+
export function ctxBarText(pct, windowSource = null) {
|
|
98
|
+
if (num(pct) === null) return "";
|
|
99
|
+
return `ctx ${windowSource === "default" ? "~" : ""}${Math.round(pct)}%`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// The ctx tooltip's window line ({tokens:, source:, pct:} the session's
|
|
103
|
+
// window, where it came from and the meter's value): "context: ~41.0k of
|
|
104
|
+
// 128.0k tokens (server)", as /stats names the source; "" without a window.
|
|
105
|
+
export function ctxWindowText(window) {
|
|
106
|
+
const tokens = num(window?.tokens);
|
|
107
|
+
if (!tokens || tokens <= 0) return "";
|
|
108
|
+
const pct = num(window.pct);
|
|
109
|
+
const used = pct === null ? "" : `~${countK((pct / 100) * tokens)} of `;
|
|
110
|
+
const source = window.source === "default"
|
|
111
|
+
? "chi's default, a guess: set window_tokens for this model"
|
|
112
|
+
: window.source;
|
|
113
|
+
return `context: ${used}${countK(tokens)} tokens${source ? ` (${source})` : ""}`;
|
|
114
|
+
}
|
|
115
|
+
|
|
94
116
|
// The ctx tooltip (info bar and card): +title+, then the session's tokens
|
|
95
117
|
// (in summed over every request, cached with its share, out with the
|
|
96
118
|
// reasoning part) and its cost, when it has any, and the memory indexes
|
|
97
|
-
// its prompt holds (+memoryIndex+, the snapshot's memory_index)
|
|
119
|
+
// its prompt holds (+memoryIndex+, the snapshot's memory_index); with
|
|
120
|
+
// +window+ (ctxWindowText's) the window line under the title. A
|
|
98
121
|
// delegated session keeps its own metrics, so these are this session's only.
|
|
99
|
-
export function tokensTipText(tokens, title = "Context used after the last turn", memoryIndex = null) {
|
|
122
|
+
export function tokensTipText(tokens, title = "Context used after the last turn", memoryIndex = null, window = null) {
|
|
100
123
|
const memory = memoryIndexText(memoryIndex);
|
|
101
124
|
const memoryLine = memory ? `\nmemory index: ${memory}` : "";
|
|
125
|
+
const windowText = ctxWindowText(window);
|
|
126
|
+
if (windowText) title = `${title}\n${windowText}`;
|
|
102
127
|
const prompt = num(tokens?.prompt_sum) || 0;
|
|
103
128
|
const completion = num(tokens?.completion_sum) || 0;
|
|
104
129
|
if (!prompt && !completion) return `${title}${memoryLine}`;
|
|
@@ -30,9 +30,12 @@ export function listSessions(sort, order, { dir, ...opts } = {}) {
|
|
|
30
30
|
// as a turn (with its images, once they are uploaded). dir: the folder the
|
|
31
31
|
// chat starts in; model: the model it starts on (as GET /api/models spells
|
|
32
32
|
// it); either left out means the server's default. preview: that message,
|
|
33
|
-
// which names the session until its turn is saved.
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
// which names the session until its turn is saved. llm_context: the
|
|
34
|
+
// session's own LLM context words ({strategy, apply, budget}), when the
|
|
35
|
+
// start page's chip set some.
|
|
36
|
+
export function createIdleSession({ dir, model, preview, llm_context: llmContext, ...opts } = {}) {
|
|
37
|
+
const body = { idle: true, ...(dir ? { dir } : {}), ...(model ? { model } : {}), ...(preview ? { preview } : {}),
|
|
38
|
+
...(llmContext ? { llm_context: llmContext } : {}) };
|
|
36
39
|
return api("/api/sessions", { method: "POST", body: JSON.stringify(body), ...opts });
|
|
37
40
|
}
|
|
38
41
|
|
|
@@ -301,3 +301,27 @@ export function memChipTitle(names) {
|
|
|
301
301
|
const list = names || [];
|
|
302
302
|
return list.length ? `memories: ${list.join(", ")}` : "no memories in this session";
|
|
303
303
|
}
|
|
304
|
+
|
|
305
|
+
// "model_notes_deepseek (system, 612 chars), model_notes_x (project, 80
|
|
306
|
+
// chars)": the model notes the session's prompt carried (its prompt_notes);
|
|
307
|
+
// "" without any. As PromptNote.text (spec/shared/labels_matrix.json).
|
|
308
|
+
export function promptNotesText(notes) {
|
|
309
|
+
return (Array.isArray(notes) ? notes : [])
|
|
310
|
+
.filter((note) => note && typeof note.name === "string" && note.name.trim())
|
|
311
|
+
.map((note) => {
|
|
312
|
+
const details = [note.scope, Number.isFinite(note.chars) ? `${Math.trunc(note.chars)} chars` : null].filter(Boolean);
|
|
313
|
+
return details.length ? `${note.name.trim()} (${details.join(", ")})` : note.name.trim();
|
|
314
|
+
})
|
|
315
|
+
.join(", ");
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// The info bar's model notes chip, for a session whose prompt carried
|
|
319
|
+
// some: "notes: deepseek" (the names without model_notes_), the full line
|
|
320
|
+
// its tooltip; null without notes.
|
|
321
|
+
export function promptNotesChip(notes) {
|
|
322
|
+
const text = promptNotesText(notes);
|
|
323
|
+
if (!text) return null;
|
|
324
|
+
const names = notes.filter((note) => note && typeof note.name === "string" && note.name.trim())
|
|
325
|
+
.map((note) => note.name.trim().replace(/^model_notes_/, ""));
|
|
326
|
+
return { text: `notes: ${names.join(", ")}`, title: `model notes in this session's prompt: ${text}` };
|
|
327
|
+
}
|
|
@@ -491,6 +491,13 @@ body.has-session #composer .actions-col{flex-wrap:nowrap;flex-shrink:0}
|
|
|
491
491
|
.model-pick::after{content:"";position:absolute;right:9px;top:50%;width:5px;height:5px;border-right:1.5px solid currentColor;border-bottom:1.5px solid currentColor;transform:translateY(-70%) rotate(45deg);pointer-events:none;transition:transform .12s ease}
|
|
492
492
|
.model-pick[aria-expanded="true"]::after{transform:translateY(-20%) rotate(225deg)}
|
|
493
493
|
@media(max-width:600px){#composer .actions-col .model-pick{max-width:60%}}
|
|
494
|
+
/* The new chat's LLM context chip, after the picker: as quiet as it; accent once a choice is set (the info bar chip's .own). */
|
|
495
|
+
#composer .actions-col .llmctx-new{order:0;flex:0 1 auto;min-width:0;max-width:min(40%,260px);height:24px;min-height:24px;line-height:22px;padding:0 6px;border-radius:6px;border:1px solid transparent;background:transparent;color:var(--text-muted);font-size:11px;font-weight:400;overflow:hidden;text-overflow:ellipsis}
|
|
496
|
+
#composer .actions-col .llmctx-new:hover{filter:none;color:var(--text);border-color:var(--accent-border);background:var(--accent-bg)}
|
|
497
|
+
#composer .actions-col .llmctx-new.own{color:var(--accent);border-color:var(--accent-border)}
|
|
498
|
+
#composer .actions-col .llmctx-new[hidden],body.has-session #composer .actions-col .llmctx-new{display:none}
|
|
499
|
+
/* Phones: picker · chip · Start on one row (the key hint is gone there); names cut short rather than wrapping, the chip first. */
|
|
500
|
+
@media(max-width:600px){#composer .actions-col:has(.llmctx-new:not([hidden])){flex-wrap:nowrap}#composer .actions-col .model-pick{flex:0 1 auto;min-width:0}#composer .actions-col .llmctx-new{max-width:45%;flex-shrink:4}}
|
|
494
501
|
/* The model search: opaque like #commandList (a list over the conversation
|
|
495
502
|
must stay readable), above the composer, or below it (.down) when the
|
|
496
503
|
start page leaves no room above, or over the composer (.over) when
|
|
@@ -1005,6 +1012,7 @@ a{color:var(--accent)}
|
|
|
1005
1012
|
</div>
|
|
1006
1013
|
<div class="actions-col">
|
|
1007
1014
|
<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>
|
|
1015
|
+
<button type="button" id="llmContextNew" class="llmctx-new" aria-haspopup="dialog" hidden></button>
|
|
1008
1016
|
<div id="composeHint">Enter to start · Shift+Enter for a newline</div>
|
|
1009
1017
|
<button id="cancelBtn" title="Cancel the running turn, whoever sent it" style="display:none">Cancel</button>
|
|
1010
1018
|
<div class="split">
|
|
@@ -6,6 +6,11 @@
|
|
|
6
6
|
// the session's llm_context (LLMContextStrategy::Explained#summary: the
|
|
7
7
|
// worker's snapshot, a command_ran, or the server's own reading of the
|
|
8
8
|
// session file).
|
|
9
|
+
//
|
|
10
|
+
// The start page has one too (kind "new chat"): it shows the picked
|
|
11
|
+
// model's values (GET /api/models' llm_context) and its form keeps a
|
|
12
|
+
// choice for the next create only (POST /api/sessions' llm_context), never
|
|
13
|
+
// remembered: nothing is sent unless the user sets one.
|
|
9
14
|
|
|
10
15
|
export const STRATEGIES = [
|
|
11
16
|
{ value: "none", label: "none" },
|
|
@@ -13,6 +18,8 @@ export const STRATEGIES = [
|
|
|
13
18
|
{ value: "stale,forget", label: "stale + forget (experimental)" },
|
|
14
19
|
];
|
|
15
20
|
export const APPLIES = ["payoff", "next_request", "turn_end"];
|
|
21
|
+
export const SESSION = "session";
|
|
22
|
+
export const NEW_CHAT = "new chat";
|
|
16
23
|
|
|
17
24
|
const DEFAULT = "default";
|
|
18
25
|
const LAYER_ORDER = ["stale", "forget"];
|
|
@@ -33,15 +40,34 @@ export function ownValues(summary) {
|
|
|
33
40
|
return { strategy, apply: own.apply || DEFAULT, budget };
|
|
34
41
|
}
|
|
35
42
|
|
|
36
|
-
// The
|
|
37
|
-
//
|
|
38
|
-
|
|
43
|
+
// The start page's choice for the new chat ({strategy: [layers], apply,
|
|
44
|
+
// budget_tokens}, the session file's shape; 0 is off) laid over the
|
|
45
|
+
// model's summary: what the chip shows, with the chosen fields' source
|
|
46
|
+
// "session" ("the new chat"). The model's own summary without a choice.
|
|
47
|
+
export function withChoice(summary, choice) {
|
|
48
|
+
if (!summary || !choice) return summary;
|
|
49
|
+
const merged = { ...summary, own: choice };
|
|
50
|
+
// (the budget's source and where are budget_source / budget_where)
|
|
51
|
+
const set = (field, key, value) => Object.assign(merged, { [key]: value, [`${field}_source`]: SESSION,
|
|
52
|
+
[`${field}_where`]: "the new chat" });
|
|
53
|
+
if (Array.isArray(choice.strategy)) set("strategy", "strategy", strategyValue(choice.strategy));
|
|
54
|
+
if (choice.apply) set("apply", "apply", choice.apply);
|
|
55
|
+
if (typeof choice.budget_tokens === "number") {
|
|
56
|
+
set("budget", "budget_tokens", choice.budget_tokens > 0 ? choice.budget_tokens : null);
|
|
57
|
+
}
|
|
58
|
+
return merged;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// The chip: its text, tooltip and whether the session (or the new chat)
|
|
62
|
+
// set its own; null without a summary (an older worker).
|
|
63
|
+
export function chipModel(summary, { kind = SESSION } = {}) {
|
|
39
64
|
if (!summary || !summary.strategy) return null;
|
|
40
65
|
const own = summary.strategy_source === "session" || !!summary.own;
|
|
41
66
|
const budget = summary.budget_tokens ? `${summary.budget_tokens} tokens` : "off";
|
|
67
|
+
const what = kind === NEW_CHAT ? "the new chat" : "this session";
|
|
42
68
|
const title = `LLM context strategy: ${summary.strategy} (${summary.strategy_where}); apply ${summary.apply} ` +
|
|
43
|
-
`(${summary.apply_where}); budget ${budget} (${summary.budget_where}). Click to change it for
|
|
44
|
-
return { text: `llm ctx ${summary.strategy}${own ?
|
|
69
|
+
`(${summary.apply_where}); budget ${budget} (${summary.budget_where}). Click to change it for ${what}.`;
|
|
70
|
+
return { text: `llm ctx ${summary.strategy}${own ? ` · ${kind}` : ""}`, title, own };
|
|
45
71
|
}
|
|
46
72
|
|
|
47
73
|
export function chipHtml(summary, escapeHtml) {
|
|
@@ -65,6 +91,54 @@ export function commandLine({ strategy = DEFAULT, apply = DEFAULT, budget = "" }
|
|
|
65
91
|
return `/llm-context ${fields.map((field) => `${field} ${words[field]}`).join(" ")}`;
|
|
66
92
|
}
|
|
67
93
|
|
|
94
|
+
// A budget as typed ("64000", "64k", "off", "0") in tokens, 0 for off;
|
|
95
|
+
// null when it isn't one or is out of chi's range (LLMContextOverride).
|
|
96
|
+
export function parseBudget(text) {
|
|
97
|
+
const word = String(text).trim().toLowerCase();
|
|
98
|
+
if (word === "off" || word === "0") return 0;
|
|
99
|
+
const match = /^(\d[\d_]*)(k)?$/.exec(word);
|
|
100
|
+
if (!match) return null;
|
|
101
|
+
const tokens = Number(match[1].replace(/_/g, "")) * (match[2] ? 1000 : 1);
|
|
102
|
+
if (tokens === 0) return 0; // "00", "0k": off, as chi reads them
|
|
103
|
+
return tokens >= 4000 && tokens <= 10_000_000 ? tokens : null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// The new chat's choice after a Set: +choice+ with the fields the user
|
|
107
|
+
// changed (vs +initial+, the values the form opened with) applied, a field
|
|
108
|
+
// set back to default dropped; null when nothing is left. Throws on a
|
|
109
|
+
// budget that isn't one.
|
|
110
|
+
export function mergeChoice(choice, { strategy = DEFAULT, apply = DEFAULT, budget = "" } = {}, initial = null) {
|
|
111
|
+
const next = { ...(choice || {}) };
|
|
112
|
+
const words = { strategy, apply, budget: String(budget).trim() || DEFAULT };
|
|
113
|
+
const was = initial && { strategy: initial.strategy, apply: initial.apply, budget: String(initial.budget).trim() || DEFAULT };
|
|
114
|
+
const key = { strategy: "strategy", apply: "apply", budget: "budget_tokens" };
|
|
115
|
+
for (const field of Object.keys(words)) {
|
|
116
|
+
if (was && words[field] === was[field]) continue;
|
|
117
|
+
const word = words[field];
|
|
118
|
+
if (word === DEFAULT) { delete next[key[field]]; continue; }
|
|
119
|
+
if (field === "strategy") next.strategy = word === "none" ? [] : word.split(/[\s,|]+/).filter(Boolean);
|
|
120
|
+
else if (field === "apply") next.apply = word;
|
|
121
|
+
else {
|
|
122
|
+
const tokens = parseBudget(word);
|
|
123
|
+
if (tokens === null) throw new Error("A budget is a number of tokens from 4000 (4k) to 10000000, or off.");
|
|
124
|
+
next.budget_tokens = tokens;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return Object.keys(next).length ? next : null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// The choice as POST /api/sessions' llm_context words it ({strategy:
|
|
131
|
+
// "stale,forget"|"none", apply, budget: "64000"|"off"}, the set fields);
|
|
132
|
+
// undefined without one, so the request has none.
|
|
133
|
+
export function choiceWords(choice) {
|
|
134
|
+
if (!choice) return undefined;
|
|
135
|
+
const words = {};
|
|
136
|
+
if (Array.isArray(choice.strategy)) words.strategy = strategyValue(choice.strategy);
|
|
137
|
+
if (choice.apply) words.apply = choice.apply;
|
|
138
|
+
if (typeof choice.budget_tokens === "number") words.budget = choice.budget_tokens > 0 ? String(choice.budget_tokens) : "off";
|
|
139
|
+
return Object.keys(words).length ? words : undefined;
|
|
140
|
+
}
|
|
141
|
+
|
|
68
142
|
// The strategy list, with the session's own value when it isn't one of them
|
|
69
143
|
// (forget alone, a newer chi's layer).
|
|
70
144
|
export function strategyOptions(chosen) {
|
|
@@ -84,11 +158,16 @@ export function nowText(summary) {
|
|
|
84
158
|
`budget ${budget} (${summary.budget_where})`;
|
|
85
159
|
}
|
|
86
160
|
|
|
87
|
-
|
|
88
|
-
|
|
161
|
+
// For the new chat (kind NEW_CHAT), +summary+ is the model's and +choice+
|
|
162
|
+
// the values the form shows.
|
|
163
|
+
export function formHtml(summary, escapeHtml, { running = false, kind = SESSION, choice = null } = {}) {
|
|
164
|
+
const fresh = kind === NEW_CHAT;
|
|
165
|
+
const own = fresh ? ownValues({ own: choice }) : ownValues(summary);
|
|
89
166
|
const follow = (field) => (summary[`${field}_source`] === "session" ? "follow the model" : `follow the model (${summary[field]})`);
|
|
167
|
+
const when = fresh ? "For the new chat's first turn on; not remembered for the next one."
|
|
168
|
+
: "From the next turn's start. Turning forget on or off re-reads the whole prompt once.";
|
|
90
169
|
return `<form class="llmctx-form"><div class="ctx-pop-head"><strong>LLM context</strong>` +
|
|
91
|
-
`<span class="ctx-pop-kind"
|
|
170
|
+
`<span class="ctx-pop-kind">${fresh ? NEW_CHAT : "this session"}</span>` +
|
|
92
171
|
`<button type="button" class="ctx-pop-close" data-act="close" aria-label="Close">×</button></div>` +
|
|
93
172
|
`<div class="ctx-pop-meta llmctx-now">${escapeHtml(nowText(summary))}</div>` +
|
|
94
173
|
`<label class="llmctx-field"><span>Strategy</span><select name="strategy">` +
|
|
@@ -99,20 +178,44 @@ export function formHtml(summary, escapeHtml, { running = false } = {}) {
|
|
|
99
178
|
APPLIES.map((a) => option(a, a, own.apply, escapeHtml)).join("") + `</select></label>` +
|
|
100
179
|
`<label class="llmctx-field"><span>Budget</span><input name="budget" type="text" inputmode="numeric" autocomplete="off" ` +
|
|
101
180
|
`placeholder="follow the model (64000, 64k or off)" value="${escapeHtml(own.budget)}"></label>` +
|
|
102
|
-
`<div class="ctx-pop-
|
|
103
|
-
|
|
181
|
+
`<div class="ctx-pop-error llmctx-error" hidden></div>` +
|
|
182
|
+
`<div class="ctx-pop-meta">${when}${running ? " A turn is running: set it when it ends." : ""}</div>` +
|
|
104
183
|
`<div class="ctx-pop-actions"><button type="button" class="ctx-pop-detach" data-act="reset">follow the model</button>` +
|
|
105
184
|
`<button type="submit" class="llmctx-set"${running ? " disabled" : ""}>Set</button></div></form>`;
|
|
106
185
|
}
|
|
107
186
|
|
|
187
|
+
const EDGE = 16;
|
|
188
|
+
const GAP = 8;
|
|
189
|
+
|
|
190
|
+
// Where the popover goes for the chip's +rect+: above it, or below when
|
|
191
|
+
// the room above is short of its height and below has more; capped to the
|
|
192
|
+
// room on that side (it scrolls), never past the window's edges.
|
|
193
|
+
// @return {{left, top?, bottom?, maxHeight?}}
|
|
194
|
+
export function popoverPlace(rect, { width, height, innerWidth, innerHeight }) {
|
|
195
|
+
const left = Math.max(EDGE, Math.min(rect.left, innerWidth - width - EDGE));
|
|
196
|
+
const above = rect.top - GAP - EDGE;
|
|
197
|
+
const below = innerHeight - rect.bottom - GAP - EDGE;
|
|
198
|
+
const down = height > above && below > above;
|
|
199
|
+
const room = Math.max(0, down ? below : above);
|
|
200
|
+
const place = down ? { left, top: rect.bottom + GAP } : { left, bottom: Math.max(EDGE, innerHeight - rect.top + GAP) };
|
|
201
|
+
return height > room ? { ...place, maxHeight: room } : place;
|
|
202
|
+
}
|
|
203
|
+
|
|
108
204
|
// The chip's popover. el: where the chip is drawn (#infoText, redrawn every
|
|
109
205
|
// tick: clicks are delegated); summary(): the open session's llm_context;
|
|
110
206
|
// session(): the open session's id; running(): a turn runs; run(line):
|
|
111
207
|
// sends the command.
|
|
208
|
+
// The start page's (kind NEW_CHAT): summary() is the picked model's,
|
|
209
|
+
// choice() the pending choice, and choose(next) takes a Set's (null for
|
|
210
|
+
// "follow the model") instead of a command running. id and anchor (the
|
|
211
|
+
// chip's selector) keep the two controls apart.
|
|
112
212
|
export function createLlmContextControl({ el, escapeHtml, summary, session = () => null, running = () => false, run,
|
|
213
|
+
kind = SESSION, choice = () => null, choose = null,
|
|
214
|
+
id = "llmContextPopover", anchor: anchorSel = ".llmctx-chip",
|
|
113
215
|
doc = document, win = window }) {
|
|
216
|
+
const fresh = kind === NEW_CHAT;
|
|
114
217
|
const pop = doc.createElement("div");
|
|
115
|
-
pop.id =
|
|
218
|
+
pop.id = id;
|
|
116
219
|
pop.className = "ctx-popover llmctx-popover";
|
|
117
220
|
pop.setAttribute("role", "dialog");
|
|
118
221
|
pop.hidden = true;
|
|
@@ -122,19 +225,23 @@ export function createLlmContextControl({ el, escapeHtml, summary, session = ()
|
|
|
122
225
|
let openFor = null;
|
|
123
226
|
|
|
124
227
|
function place(anchor) {
|
|
125
|
-
const rect = anchor.getBoundingClientRect();
|
|
126
228
|
const width = Math.min(360, win.innerWidth - 32);
|
|
127
229
|
pop.style.width = `${width}px`;
|
|
128
|
-
pop.style.
|
|
129
|
-
|
|
230
|
+
pop.style.maxHeight = "";
|
|
231
|
+
const at = popoverPlace(anchor.getBoundingClientRect(), { width, height: pop.offsetHeight,
|
|
232
|
+
innerWidth: win.innerWidth, innerHeight: win.innerHeight });
|
|
233
|
+
pop.style.left = `${at.left}px`;
|
|
234
|
+
pop.style.top = at.top === undefined ? "" : `${at.top}px`;
|
|
235
|
+
pop.style.bottom = at.bottom === undefined ? "" : `${at.bottom}px`;
|
|
236
|
+
if (at.maxHeight !== undefined) pop.style.maxHeight = `${at.maxHeight}px`;
|
|
130
237
|
}
|
|
131
238
|
|
|
132
239
|
function open(anchor) {
|
|
133
240
|
const data = summary();
|
|
134
241
|
if (!data) return;
|
|
135
|
-
initial = ownValues(data);
|
|
242
|
+
initial = fresh ? ownValues({ own: choice() }) : ownValues(data);
|
|
136
243
|
openFor = session();
|
|
137
|
-
pop.innerHTML = formHtml(data, escapeHtml, { running: running() });
|
|
244
|
+
pop.innerHTML = formHtml(data, escapeHtml, { running: running(), kind, choice: choice() });
|
|
138
245
|
pop.hidden = false;
|
|
139
246
|
place(anchor);
|
|
140
247
|
}
|
|
@@ -161,17 +268,39 @@ export function createLlmContextControl({ el, escapeHtml, summary, session = ()
|
|
|
161
268
|
}
|
|
162
269
|
|
|
163
270
|
el.addEventListener("click", (e) => {
|
|
164
|
-
const anchor = e.target.closest(
|
|
271
|
+
const anchor = e.target.closest(anchorSel);
|
|
165
272
|
if (!anchor) return;
|
|
166
273
|
e.stopPropagation();
|
|
167
274
|
if (isOpen()) close();
|
|
168
275
|
else open(anchor);
|
|
169
276
|
});
|
|
277
|
+
// An edit makes an error the last Set showed stale.
|
|
278
|
+
const clearError = () => {
|
|
279
|
+
const error = pop.querySelector(".llmctx-error");
|
|
280
|
+
if (error && !error.hidden) { error.hidden = true; error.textContent = ""; }
|
|
281
|
+
};
|
|
282
|
+
pop.addEventListener("input", clearError);
|
|
283
|
+
pop.addEventListener("change", clearError);
|
|
170
284
|
pop.addEventListener("submit", (e) => {
|
|
171
285
|
e.preventDefault();
|
|
172
286
|
const form = e.target;
|
|
173
|
-
const
|
|
174
|
-
|
|
287
|
+
const values = { strategy: form.elements.strategy.value, apply: form.elements.apply.value,
|
|
288
|
+
budget: form.elements.budget.value };
|
|
289
|
+
if (fresh) {
|
|
290
|
+
let next;
|
|
291
|
+
try {
|
|
292
|
+
next = mergeChoice(choice(), values, initial);
|
|
293
|
+
} catch (err) {
|
|
294
|
+
const error = pop.querySelector(".llmctx-error");
|
|
295
|
+
error.textContent = err.message;
|
|
296
|
+
error.hidden = false;
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
close();
|
|
300
|
+
choose(next);
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
const line = commandLine(values, initial);
|
|
175
304
|
close();
|
|
176
305
|
if (line) run(line);
|
|
177
306
|
});
|
|
@@ -180,11 +309,12 @@ export function createLlmContextControl({ el, escapeHtml, summary, session = ()
|
|
|
180
309
|
if (act === "close") close();
|
|
181
310
|
else if (act === "reset") {
|
|
182
311
|
close();
|
|
183
|
-
|
|
312
|
+
if (fresh) choose(null);
|
|
313
|
+
else run(commandLine());
|
|
184
314
|
}
|
|
185
315
|
});
|
|
186
316
|
doc.addEventListener("click", (e) => {
|
|
187
|
-
if (isOpen() && !pop.contains(e.target) && !e.target.closest?.(
|
|
317
|
+
if (isOpen() && !pop.contains(e.target) && !e.target.closest?.(anchorSel)) close();
|
|
188
318
|
});
|
|
189
319
|
doc.addEventListener("keydown", (e) => {
|
|
190
320
|
if (e.key === "Escape" && isOpen()) close();
|
|
@@ -19,7 +19,8 @@ export const RECENT_KEY = "chi_model_recent";
|
|
|
19
19
|
|
|
20
20
|
// The payload's models as rows: {name, host, id, isDefaultHost}, and
|
|
21
21
|
// sampling (the configured request parameters as /model words them) when
|
|
22
|
-
// the model has some
|
|
22
|
+
// the model has some, and llm_context (the LLM context it starts under,
|
|
23
|
+
// LLMContextStrategy::Explained#summary) when the server sent one. The
|
|
23
24
|
// default host's names are bare (name == id); the server's own default,
|
|
24
25
|
// unshifted when no host lists it, has no host and goes with them, shown
|
|
25
26
|
// by its name.
|
|
@@ -33,6 +34,7 @@ export function modelRows(payload) {
|
|
|
33
34
|
const id = host ? String(m.id || name) : name;
|
|
34
35
|
const row = { name, host, id, isDefaultHost: !host || name === id };
|
|
35
36
|
if (m.sampling) row.sampling = String(m.sampling);
|
|
37
|
+
if (m.llm_context && typeof m.llm_context === "object") row.llm_context = m.llm_context;
|
|
36
38
|
return row;
|
|
37
39
|
});
|
|
38
40
|
}
|
|
@@ -219,6 +219,8 @@ export function createModelPicker({ button, panel, input, list, composer, center
|
|
|
219
219
|
return true;
|
|
220
220
|
},
|
|
221
221
|
chosen: () => chosen,
|
|
222
|
+
// The picked model's row (its llm_context too); null before the list.
|
|
223
|
+
chosenRow: () => rows.find((r) => r.name === chosen) || null,
|
|
222
224
|
close,
|
|
223
225
|
isOpen,
|
|
224
226
|
};
|
|
@@ -14,11 +14,16 @@ export function emptySessionState() {
|
|
|
14
14
|
// in the info bar's tooltip).
|
|
15
15
|
preloadedMemories: [],
|
|
16
16
|
mutedMemories: [],
|
|
17
|
+
// The model notes the session's prompt carried (its prompt_notes: name,
|
|
18
|
+
// scope, chars, digest; the info bar's notes chip).
|
|
19
|
+
promptNotes: [],
|
|
17
20
|
// A delegated session's parent (the info bar's "delegated by" chip).
|
|
18
21
|
parentId: null,
|
|
19
22
|
ctxPct: null,
|
|
20
23
|
// Window the kernel resolved for the running generation (:generation_started).
|
|
21
24
|
ctxWindow: null,
|
|
25
|
+
// Where it came from ("server", "config", … "default": chi's guess).
|
|
26
|
+
ctxWindowSource: null,
|
|
22
27
|
status: "",
|
|
23
28
|
model: "",
|
|
24
29
|
// What the worker's server said it served for a model name: [served, asked].
|
|
@@ -233,8 +233,10 @@ function turnGroup(g, items, timing) {
|
|
|
233
233
|
|
|
234
234
|
// A canceled turn that ran more than one step or any call has no answer:
|
|
235
235
|
// its partial text stays in the block, as it does live (turn_view.js
|
|
236
|
-
// turnEnded promotes only a completed turn or a lone step with no tools).
|
|
236
|
+
// turnEnded promotes only a completed turn or a lone step with no tools). A
|
|
237
|
+
// failed turn never answered: its texts are steps.
|
|
237
238
|
function canceledSteps(g, record, tools) {
|
|
239
|
+
if (record?.status === "failed") return true;
|
|
238
240
|
return record?.status === "canceled" && (g.texts.length > 1 || tools.length > 0);
|
|
239
241
|
}
|
|
240
242
|
|