samagotchi 0.42.0 → 0.44.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 +47 -1
- data/docs/configuration.md +26 -0
- data/docs/context.md +46 -0
- data/docs/internals/context-telemetry.md +4 -1
- data/docs/internals/engine.md +1 -0
- data/docs/plugins.md +27 -5
- data/docs/sessions.md +1 -1
- data/lib/samagotchi/bridge/card_store.rb +20 -9
- data/lib/samagotchi/bridge/turn_notice.rb +3 -1
- 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/context_sources.rb +40 -0
- data/lib/samagotchi/context_status.rb +18 -5
- data/lib/samagotchi/kernel_loop.rb +24 -9
- data/lib/samagotchi/llm/chat_loop.rb +4 -3
- data/lib/samagotchi/llm_context_forget.rb +6 -2
- data/lib/samagotchi/llm_context_notice.rb +96 -0
- data/lib/samagotchi/plugin/attached_context.rb +75 -11
- data/lib/samagotchi/terminal_ui/attached_loop.rb +2 -1
- data/lib/samagotchi/terminal_ui/event_renderer.rb +3 -0
- data/lib/samagotchi/terminal_ui/formatting.rb +7 -0
- data/lib/samagotchi/terminal_ui/one_shot_sink.rb +1 -0
- data/lib/samagotchi/tool_declarations.rb +2 -1
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +47 -7
- data/lib/samagotchi/web/message_parts.rb +90 -11
- data/lib/samagotchi/web/public/app.js +80 -13
- data/lib/samagotchi/web/public/ctx.js +27 -2
- data/lib/samagotchi/web/public/data.js +6 -3
- data/lib/samagotchi/web/public/index.html +12 -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 +2 -0
- data/lib/samagotchi/web/public/turn_events.js +34 -0
- data/lib/samagotchi/web/public/turn_html.js +33 -2
- data/lib/samagotchi/web/public/turn_view.js +4 -2
- metadata +2 -1
|
@@ -21,7 +21,10 @@ 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";
|
|
@@ -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";
|
|
@@ -57,7 +60,7 @@ import {
|
|
|
57
60
|
} from "./timing.js";
|
|
58
61
|
import { createLiveTurn } from "./live_turn.js";
|
|
59
62
|
import { DONE_MS, applyInitEvent, dropDone, initRowHtml, newInitTasks, seedInitTasks } from "./init_row.js";
|
|
60
|
-
import { clientLabel, commandView, isReportClient, displayWait, dropEarlyRestores, emptyRetryLine, hookNoticeLabel, initWaitLine, isOwn, sessionCommandLine, keepEarlyRestore, newClientId, noticeLine, promptOps, reminderText, restoreAction, composerAfterAck, restoreOnAck, replaysDrawnTurn, retryStatusLine, snapshotEvents, steerCutLine, startPageReplyOrHint, turnOutput, unknownCommandHint, webLocalReply, workerGoneText } from "./turn_events.js";
|
|
63
|
+
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";
|
|
61
64
|
|
|
62
65
|
const $ = (s) => document.querySelector(s);
|
|
63
66
|
const topStripEl = $("#topStrip");
|
|
@@ -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,7 +1162,10 @@ 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 || [];
|
|
@@ -1483,11 +1527,17 @@ function showSteers(steers) {
|
|
|
1483
1527
|
}
|
|
1484
1528
|
}
|
|
1485
1529
|
|
|
1530
|
+
// A ✂ row's notice data (turnDom.notice, placeTurnNotice).
|
|
1531
|
+
function llmContextNotice(data) {
|
|
1532
|
+
return { line: llmContextLine(data), cls: "llm-context", title: llmContextHover(data) };
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1486
1535
|
function showHookNotice(data, { before = null } = {}) {
|
|
1487
1536
|
removeHintIfEmpty();
|
|
1488
1537
|
const el = document.createElement("div");
|
|
1489
|
-
el.className = "bubble hook-notice" + (data.level === "warn" ? " warn" : "");
|
|
1538
|
+
el.className = "bubble hook-notice" + (data.level === "warn" ? " warn" : "") + (data.cls ? ` ${data.cls}` : "");
|
|
1490
1539
|
el.textContent = data.line || `${hookNoticeLabel(data.hook)}: ${data.text || ""}`;
|
|
1540
|
+
if (data.title) el.title = data.title;
|
|
1491
1541
|
const follow = isNearBottom();
|
|
1492
1542
|
if (before) historyEl.insertBefore(el, before);
|
|
1493
1543
|
else appendToHistory(el);
|
|
@@ -1579,18 +1629,20 @@ function placeSnapshotCards(entries) {
|
|
|
1579
1629
|
// plain answer, a step the history doesn't have) a bubble before the
|
|
1580
1630
|
// turn's answer, or before its timing line when it has none.
|
|
1581
1631
|
function placeTurnNotice(entry, before) {
|
|
1632
|
+
const extra = entry.type === "llm_context_edited" ? llmContextNotice(entry) : {};
|
|
1582
1633
|
const line = noticeLine(entry);
|
|
1583
1634
|
const turnEls = reloadedTurnEls(before);
|
|
1584
1635
|
const place = turnNoticePlace(entry);
|
|
1585
1636
|
const block = turnEls.find((el) => el.matches(".turn-work"));
|
|
1586
1637
|
const step = place && block ? block.querySelectorAll(":scope > .gen")[place.step] : null;
|
|
1587
1638
|
if (!step) {
|
|
1588
|
-
showHookNotice({ ...entry, line }, { before: turnAnswerEl(turnEls) || before });
|
|
1639
|
+
showHookNotice({ ...entry, ...extra, line }, { before: turnAnswerEl(turnEls) || before });
|
|
1589
1640
|
return;
|
|
1590
1641
|
}
|
|
1591
1642
|
const el = document.createElement("div");
|
|
1592
|
-
el.className = "hook-notice" + (entry.level === "warn" ? " warn" : "");
|
|
1643
|
+
el.className = "hook-notice" + (entry.level === "warn" ? " warn" : "") + (extra.cls ? ` ${extra.cls}` : "");
|
|
1593
1644
|
el.textContent = line;
|
|
1645
|
+
if (extra.title) el.title = extra.title;
|
|
1594
1646
|
let body = step.querySelector(":scope > .activity-body");
|
|
1595
1647
|
if (!body) {
|
|
1596
1648
|
body = document.createElement("div");
|
|
@@ -1766,9 +1818,10 @@ function updateInfoBar() {
|
|
|
1766
1818
|
if (stopBtn) stopBtn.style.display = canStopSession({ owner: current.owner, streaming: !!streamControl }) ? "" : "none";
|
|
1767
1819
|
const idShort = selected.slice(0,8);
|
|
1768
1820
|
const memText = current.usedMemories.length ? current.usedMemories.join(", ") : "—";
|
|
1769
|
-
const ctxText = current.ctxPct
|
|
1821
|
+
const ctxText = ctxBarText(current.ctxPct, current.ctxWindowSource);
|
|
1770
1822
|
const tokens = current.timing.tokens;
|
|
1771
|
-
const ctxTip = tokensTipText(tokens, undefined, current.timing.memoryIndex
|
|
1823
|
+
const ctxTip = tokensTipText(tokens, undefined, current.timing.memoryIndex,
|
|
1824
|
+
{ tokens: current.ctxWindow, source: current.ctxWindowSource, pct: current.ctxPct });
|
|
1772
1825
|
const speedTextNow = lastSpeedText(tokens);
|
|
1773
1826
|
const avgSpeed = speedText(tokens?.avg_decode_tps, tokens?.tps_source);
|
|
1774
1827
|
const speedTip = `Decode speed of the last generation${avgSpeed ? `; session average ${avgSpeed}` : ""}`;
|
|
@@ -1939,7 +1992,10 @@ const streamHandlers = {
|
|
|
1939
1992
|
}
|
|
1940
1993
|
},
|
|
1941
1994
|
generation_started: (data) => {
|
|
1942
|
-
if (data?.context_window_tokens)
|
|
1995
|
+
if (data?.context_window_tokens) {
|
|
1996
|
+
current.ctxWindow = data.context_window_tokens;
|
|
1997
|
+
current.ctxWindowSource = data.context_window_source || null;
|
|
1998
|
+
}
|
|
1943
1999
|
liveTurn.timing.setNote("");
|
|
1944
2000
|
turnDom.generationStarted(data || {});
|
|
1945
2001
|
},
|
|
@@ -2143,6 +2199,9 @@ const streamHandlers = {
|
|
|
2143
2199
|
// (a reload puts it back from the snapshot's cards).
|
|
2144
2200
|
empty_answer_retry: (data) => { turnDom.notice({ line: emptyRetryLine(data) }); },
|
|
2145
2201
|
steer_cut: (data) => { turnDom.notice({ line: steerCutLine(data) }); },
|
|
2202
|
+
// A batch of LLM context edits the server applied: a ✂ row of the
|
|
2203
|
+
// current step, its hover listing the outputs.
|
|
2204
|
+
llm_context_edited: (data) => { turnDom.notice(llmContextNotice(data)); },
|
|
2146
2205
|
// The provider is asked again after an error, or the turn waits for
|
|
2147
2206
|
// plugins' setup: a note on the live timing line until it streams.
|
|
2148
2207
|
generation_retrying: (data) => liveTurn.timing.setNote(retryStatusLine(data)),
|
|
@@ -2226,7 +2285,11 @@ async function handleSendTurn() {
|
|
|
2226
2285
|
}
|
|
2227
2286
|
actionBtn.disabled = true;
|
|
2228
2287
|
try {
|
|
2229
|
-
const s = await createIdleSession({ dir: scope.dir, model: chosenModel(), preview: prompt
|
|
2288
|
+
const s = await createIdleSession({ dir: scope.dir, model: chosenModel(), preview: prompt,
|
|
2289
|
+
llm_context: choiceWords(newChatLlmContext) });
|
|
2290
|
+
// The choice was for this chat only.
|
|
2291
|
+
newChatLlmContext = null;
|
|
2292
|
+
renderNewChatLlmContext();
|
|
2230
2293
|
addCreatedSession(s);
|
|
2231
2294
|
await select(s.id);
|
|
2232
2295
|
} catch (e) {
|
|
@@ -2439,7 +2502,10 @@ const modelPicker = createModelPicker({
|
|
|
2439
2502
|
list: $("#modelList"),
|
|
2440
2503
|
composer: $("#composer"),
|
|
2441
2504
|
center: $("#center"),
|
|
2442
|
-
onPick: () =>
|
|
2505
|
+
onPick: () => {
|
|
2506
|
+
renderNewChatLlmContext();
|
|
2507
|
+
promptEl.focus();
|
|
2508
|
+
},
|
|
2443
2509
|
});
|
|
2444
2510
|
async function loadModels() {
|
|
2445
2511
|
let payload;
|
|
@@ -2453,6 +2519,7 @@ async function loadModels() {
|
|
|
2453
2519
|
if (!modelPicker.setModels(payload, stored)) return;
|
|
2454
2520
|
modelPickEl.title = payload.warning ? `The model the new chat starts on · ${payload.warning}` : "The model the new chat starts on";
|
|
2455
2521
|
modelPickEl.hidden = false;
|
|
2522
|
+
renderNewChatLlmContext();
|
|
2456
2523
|
}
|
|
2457
2524
|
// The picker's choice for a create request; nothing (the server's default)
|
|
2458
2525
|
// 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
|
|
|
@@ -172,6 +172,8 @@ button.task-stop:hover:not(:disabled){background:var(--danger-bg-dim);filter:non
|
|
|
172
172
|
.activity-output{flex:1 1 100%;min-width:0}
|
|
173
173
|
/* An edit/write's change: a closed "diff +3 −1" under the output, lined up with it. */
|
|
174
174
|
.activity-diff{flex:1 1 100%;min-width:0;margin:2px 0 2px var(--row-gutter)}
|
|
175
|
+
/* A reloaded row's ✂ mark (an LLM context edit on its output), one line above the output. */
|
|
176
|
+
.activity-edit{flex:1 1 100%;min-width:0;margin:2px 0 0 var(--row-gutter);font-size:12px;color:var(--text-3);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;cursor:help}
|
|
175
177
|
.activity-diff>summary{cursor:pointer;font-size:12px;color:var(--text-muted);width:max-content;font-variant-numeric:tabular-nums}
|
|
176
178
|
.activity-diff>summary:hover{color:var(--text-2)}
|
|
177
179
|
.activity-diff[open]>summary{margin-bottom:4px}
|
|
@@ -212,6 +214,7 @@ button.task-stop:hover:not(:disabled){background:var(--danger-bg-dim);filter:non
|
|
|
212
214
|
.activity-params{flex:1 1 0}
|
|
213
215
|
.activity-output{margin-left:0}
|
|
214
216
|
.activity-diff{margin-left:0}
|
|
217
|
+
.activity-edit{margin-left:0}
|
|
215
218
|
.activity-row>.activity-command{margin-left:0}
|
|
216
219
|
}
|
|
217
220
|
.bubble.thinking{border-left:3px solid var(--think);background:color-mix(in srgb,var(--think) 6%,var(--bg-panel));padding:8px 12px}
|
|
@@ -374,6 +377,7 @@ button.task-stop:hover:not(:disabled){background:var(--danger-bg-dim);filter:non
|
|
|
374
377
|
.gen .hook-notice{font-size:13px;color:var(--text-3);padding:5px 2px 5px 8px;border-top:1px solid var(--border);border-left:2px solid var(--text-faint);white-space:pre-wrap;word-break:break-word}
|
|
375
378
|
.gen .activity-body>.hook-notice:first-child{border-top:none}
|
|
376
379
|
.gen .hook-notice.warn{border-left-color:var(--warn);color:var(--warn-text)}
|
|
380
|
+
.hook-notice.llm-context{border-left-style:dashed;cursor:help}
|
|
377
381
|
/* A plugin's steer (a nudge into the running turn): a row of its step, or a bubble; its text collapsed. */
|
|
378
382
|
.steer-row{font-size:13px;color:var(--text-3);border-left:2px solid var(--accent-border)}
|
|
379
383
|
.steer-row>summary{cursor:pointer;list-style:none;font-style:italic}
|
|
@@ -491,6 +495,13 @@ body.has-session #composer .actions-col{flex-wrap:nowrap;flex-shrink:0}
|
|
|
491
495
|
.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
496
|
.model-pick[aria-expanded="true"]::after{transform:translateY(-20%) rotate(225deg)}
|
|
493
497
|
@media(max-width:600px){#composer .actions-col .model-pick{max-width:60%}}
|
|
498
|
+
/* 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). */
|
|
499
|
+
#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}
|
|
500
|
+
#composer .actions-col .llmctx-new:hover{filter:none;color:var(--text);border-color:var(--accent-border);background:var(--accent-bg)}
|
|
501
|
+
#composer .actions-col .llmctx-new.own{color:var(--accent);border-color:var(--accent-border)}
|
|
502
|
+
#composer .actions-col .llmctx-new[hidden],body.has-session #composer .actions-col .llmctx-new{display:none}
|
|
503
|
+
/* Phones: picker · chip · Start on one row (the key hint is gone there); names cut short rather than wrapping, the chip first. */
|
|
504
|
+
@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
505
|
/* The model search: opaque like #commandList (a list over the conversation
|
|
495
506
|
must stay readable), above the composer, or below it (.down) when the
|
|
496
507
|
start page leaves no room above, or over the composer (.over) when
|
|
@@ -1005,6 +1016,7 @@ a{color:var(--accent)}
|
|
|
1005
1016
|
</div>
|
|
1006
1017
|
<div class="actions-col">
|
|
1007
1018
|
<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>
|
|
1019
|
+
<button type="button" id="llmContextNew" class="llmctx-new" aria-haspopup="dialog" hidden></button>
|
|
1008
1020
|
<div id="composeHint">Enter to start · Shift+Enter for a newline</div>
|
|
1009
1021
|
<button id="cancelBtn" title="Cancel the running turn, whoever sent it" style="display:none">Cancel</button>
|
|
1010
1022
|
<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
|
};
|
|
@@ -22,6 +22,8 @@ export function emptySessionState() {
|
|
|
22
22
|
ctxPct: null,
|
|
23
23
|
// Window the kernel resolved for the running generation (:generation_started).
|
|
24
24
|
ctxWindow: null,
|
|
25
|
+
// Where it came from ("server", "config", … "default": chi's guess).
|
|
26
|
+
ctxWindowSource: null,
|
|
25
27
|
status: "",
|
|
26
28
|
model: "",
|
|
27
29
|
// What the worker's server said it served for a model name: [served, asked].
|