samagotchi 0.42.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 +28 -1
- data/docs/configuration.md +8 -0
- data/docs/context.md +46 -0
- data/docs/plugins.md +27 -5
- data/docs/sessions.md +1 -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/plugin/attached_context.rb +75 -11
- data/lib/samagotchi/tool_declarations.rb +2 -1
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +40 -3
- data/lib/samagotchi/web/public/app.js +65 -9
- 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 +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 +2 -0
- metadata +1 -1
|
@@ -27,27 +27,71 @@ module Samagotchi
|
|
|
27
27
|
# +name+ and +cmd+ (+hint+, +every_seconds+). A source of that name
|
|
28
28
|
# already attached stays as it is. One the user removed from the
|
|
29
29
|
# session (its name, or its URL) isn't attached again (#declined?)
|
|
30
|
-
# until the user adds it again
|
|
30
|
+
# until the user adds it again, or +force+: the user's own explicit
|
|
31
|
+
# choice (a click), which clears the declined marker (by the name,
|
|
32
|
+
# and by the URL).
|
|
31
33
|
# @return [String, nil] the source's name; nil when declined
|
|
32
34
|
# @raise [Error]
|
|
33
|
-
def attach(url: nil, name: nil, cmd: nil, why: nil, hint: nil, every_seconds: nil)
|
|
34
|
-
own =
|
|
35
|
+
def attach(url: nil, name: nil, cmd: nil, why: nil, hint: nil, every_seconds: nil, force: false)
|
|
36
|
+
own = own_location!
|
|
35
37
|
source = url ? from_url(url, name: name, why: why) : from_cmd(name: name, cmd: cmd, why: why, hint: hint, every_seconds: every_seconds)
|
|
36
|
-
|
|
38
|
+
if force
|
|
39
|
+
own.undecline(source.name, hint: url && source.hint)
|
|
40
|
+
elsif own.declined?(source.name) || (url && own.declined_hints.include?(source.hint))
|
|
41
|
+
return nil
|
|
42
|
+
end
|
|
37
43
|
return source.name if own.source(source.name)
|
|
38
44
|
|
|
39
|
-
|
|
40
|
-
source.name
|
|
45
|
+
add(own, source)
|
|
41
46
|
rescue ContextSources::Invalid, ContextProviders::Invalid => e
|
|
42
47
|
raise Error, e.message
|
|
43
48
|
end
|
|
44
49
|
|
|
45
|
-
#
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
50
|
+
# Marks +name+ (or +url+'s source, through its provider) declined in
|
|
51
|
+
# this session, as the user's removal does: #attach skips it until the
|
|
52
|
+
# user adds it again. An attached source stays (chi context rm).
|
|
53
|
+
# @return [String] the name
|
|
54
|
+
# @raise [Error]
|
|
55
|
+
def decline(url: nil, name: nil)
|
|
56
|
+
own = own_location!
|
|
57
|
+
if url
|
|
58
|
+
resolved = ContextProviders.resolve(url) or raise Error, "no installed bundle resolves #{url}"
|
|
59
|
+
name = name ? ContextSources.check_name!(name) : resolved.name
|
|
60
|
+
hint = resolved.hint
|
|
61
|
+
else
|
|
62
|
+
raise Error, "give url: or name:" if name.to_s.empty?
|
|
63
|
+
|
|
64
|
+
name = ContextSources.check_name!(name)
|
|
65
|
+
end
|
|
66
|
+
own.decline(name, hint)
|
|
67
|
+
name
|
|
68
|
+
rescue ContextSources::Invalid, ContextProviders::Invalid => e
|
|
69
|
+
raise Error, e.message
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Whether the user removed +name+ (or +url+) from this session (so
|
|
73
|
+
# #attach skips it).
|
|
74
|
+
def declined?(name = nil, url: nil)
|
|
75
|
+
own = location or return false
|
|
76
|
+
name_declined?(own, name) || (!url.to_s.empty? && own.declined_hints.include?(url))
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Records that this plugin offered +name+ (+hint+: its URL; +why+: the
|
|
80
|
+
# reason an attach from the offer gives) here, so a worker restart
|
|
81
|
+
# doesn't offer it again.
|
|
82
|
+
# @return [ContextSources::Offer]
|
|
83
|
+
# @raise [Error]
|
|
84
|
+
def mark_offered(name, hint, why: nil)
|
|
85
|
+
own_location!.mark_offered(name, hint, why: why)
|
|
86
|
+
rescue ContextSources::Invalid => e
|
|
87
|
+
raise Error, e.message
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# @return [ContextSources::Offer, nil] the offer #mark_offered recorded
|
|
91
|
+
def offered(name)
|
|
92
|
+
location&.offered(name)
|
|
49
93
|
rescue ContextSources::Invalid
|
|
50
|
-
|
|
94
|
+
nil
|
|
51
95
|
end
|
|
52
96
|
|
|
53
97
|
# This session's sources (its own, then its project's).
|
|
@@ -69,6 +113,26 @@ module Samagotchi
|
|
|
69
113
|
|
|
70
114
|
def state_dir = @host.state_dir&.call || Session.default_state_dir
|
|
71
115
|
|
|
116
|
+
# Two attaches at once (a double click): the one that lost the race
|
|
117
|
+
# finds the source there and returns its name too.
|
|
118
|
+
def add(own, source)
|
|
119
|
+
own.add(source.with(scope: own.scope))
|
|
120
|
+
source.name
|
|
121
|
+
rescue ContextSources::Invalid
|
|
122
|
+
raise unless own.source(source.name)
|
|
123
|
+
|
|
124
|
+
source.name
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# An invalid name isn't declined (and doesn't hide the URL's check).
|
|
128
|
+
def name_declined?(own, name)
|
|
129
|
+
!name.nil? && own.declined?(name)
|
|
130
|
+
rescue ContextSources::Invalid
|
|
131
|
+
false
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def own_location! = location || raise(Error, "this session has no id yet")
|
|
135
|
+
|
|
72
136
|
def location
|
|
73
137
|
id = @host.session_id.call
|
|
74
138
|
id && ContextSources.session_location(id, state_dir: state_dir)
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "json"
|
|
4
4
|
require_relative "tools/args"
|
|
5
5
|
require_relative "llm_context_strategy"
|
|
6
|
+
require_relative "tools/memory"
|
|
6
7
|
|
|
7
8
|
module Samagotchi
|
|
8
9
|
# Tool-declaration constants, protocol constants, and guidance text.
|
|
@@ -146,7 +147,7 @@ module Samagotchi
|
|
|
146
147
|
},
|
|
147
148
|
description: {
|
|
148
149
|
type: "string",
|
|
149
|
-
description: "Optional one-line description (at most
|
|
150
|
+
description: "Optional one-line description (at most #{Tools::MemoryWrite::DESCRIPTION_LIMIT} characters) appended to the managed index line; without content, it changes only that line"
|
|
150
151
|
},
|
|
151
152
|
current_model_only: {
|
|
152
153
|
type: "boolean",
|
data/lib/samagotchi/version.rb
CHANGED
data/lib/samagotchi/web/app.rb
CHANGED
|
@@ -20,6 +20,7 @@ require_relative "../session_metrics"
|
|
|
20
20
|
require_relative "../session_commands"
|
|
21
21
|
require_relative "../steer"
|
|
22
22
|
require_relative "../host_registry"
|
|
23
|
+
require_relative "../llm_context_override"
|
|
23
24
|
require_relative "../model_catalog"
|
|
24
25
|
require_relative "../model_profile"
|
|
25
26
|
require_relative "../sampling_settings"
|
|
@@ -458,7 +459,8 @@ module Samagotchi
|
|
|
458
459
|
end
|
|
459
460
|
|
|
460
461
|
# Each model's configured sampling as /model words it ("temperature=0.6
|
|
461
|
-
# (hosts.work)"), on the models that have some: the picker's tooltip
|
|
462
|
+
# (hosts.work)"), on the models that have some: the picker's tooltip;
|
|
463
|
+
# and each model's LLM context (LLMContextStrategy::Explained#summary).
|
|
462
464
|
# A config that can't be read leaves the list as it is.
|
|
463
465
|
def add_sampling(models, registry)
|
|
464
466
|
return if registry.nil? || models.empty?
|
|
@@ -466,14 +468,23 @@ module Samagotchi
|
|
|
466
468
|
settings = ConfigFile.model_settings
|
|
467
469
|
models.each do |model|
|
|
468
470
|
target = registry.resolve(model[:name])
|
|
469
|
-
|
|
470
|
-
|
|
471
|
+
names = registry.lookup_names(model[:name], target: target)
|
|
472
|
+
summary = SamplingSettings.summary(target, names: names, models: settings)
|
|
471
473
|
model[:sampling] = summary if summary
|
|
474
|
+
add_llm_context(model, target, names, settings)
|
|
472
475
|
end
|
|
473
476
|
rescue StandardError
|
|
474
477
|
nil
|
|
475
478
|
end
|
|
476
479
|
|
|
480
|
+
# The LLM context +model+ starts under (the start page's llm ctx chip);
|
|
481
|
+
# one that can't be read leaves this model without, not the others.
|
|
482
|
+
def add_llm_context(model, target, names, settings)
|
|
483
|
+
model[:llm_context] = LLMContextStrategy.explain(target, names: names, models: settings).summary(nil)
|
|
484
|
+
rescue StandardError
|
|
485
|
+
nil
|
|
486
|
+
end
|
|
487
|
+
|
|
477
488
|
# A session's LLM context strategy, apply rule and budget, each with
|
|
478
489
|
# where it came from (LLMContextStrategy::Explained#summary), as its
|
|
479
490
|
# worker would resolve them; nil when they can't be.
|
|
@@ -563,6 +574,12 @@ module Samagotchi
|
|
|
563
574
|
dir, error = scope_dir(body["dir"])
|
|
564
575
|
return error if error
|
|
565
576
|
|
|
577
|
+
# llm_context: the session's own LLM context values before its first
|
|
578
|
+
# turn (the start page's llm ctx chip; chi --llm-context), read
|
|
579
|
+
# before anything spawns so a bad one spawns nothing.
|
|
580
|
+
llm_context, error = request_llm_context(body["llm_context"])
|
|
581
|
+
return error if error
|
|
582
|
+
|
|
566
583
|
folder = dir ? { working_directory: dir } : {}
|
|
567
584
|
# model: the model the session starts on (the start page's picker,
|
|
568
585
|
# spelled as GET /api/models lists it); blank means the default.
|
|
@@ -573,6 +590,7 @@ module Samagotchi
|
|
|
573
590
|
# until that turn is saved.
|
|
574
591
|
preview = idle ? body["preview"].to_s.strip : ""
|
|
575
592
|
folder[:title] = preview unless preview.empty?
|
|
593
|
+
folder[:llm_context] = llm_context if llm_context
|
|
576
594
|
begin
|
|
577
595
|
session = @manager.spawn_session(prompt: idle ? nil : prompt.to_s, state_dir: @state_dir, **folder)
|
|
578
596
|
rescue ModelProfile::MissingModel => e
|
|
@@ -588,6 +606,25 @@ module Samagotchi
|
|
|
588
606
|
json_response(201, session_to_json(session).merge(bridge_port: port))
|
|
589
607
|
end
|
|
590
608
|
|
|
609
|
+
# The create request's "llm_context" ({strategy:, apply:, budget:},
|
|
610
|
+
# each a word as /llm-context takes it, "default" for unset) as an
|
|
611
|
+
# override: [override, nil] (nil when absent or all default), or
|
|
612
|
+
# [nil, a 400 response] for anything else.
|
|
613
|
+
def request_llm_context(raw)
|
|
614
|
+
return [nil, nil] if raw.nil?
|
|
615
|
+
|
|
616
|
+
fields = LLMContextOverride::COMMAND_WORDS.invert
|
|
617
|
+
unless raw.is_a?(Hash) && raw.all? { |key, word| fields.key?(key) && word.is_a?(String) }
|
|
618
|
+
return [nil, error_response(400, "invalid_llm_context",
|
|
619
|
+
"llm_context is an object of #{fields.keys.join(", ")} words")]
|
|
620
|
+
end
|
|
621
|
+
|
|
622
|
+
override = LLMContextOverride.update(nil, raw.to_h { |key, word| [fields.fetch(key), word] })
|
|
623
|
+
[override.empty? ? nil : override, nil]
|
|
624
|
+
rescue ArgumentError => e
|
|
625
|
+
[nil, error_response(400, "invalid_llm_context", e.message)]
|
|
626
|
+
end
|
|
627
|
+
|
|
591
628
|
def handle_show(req, id)
|
|
592
629
|
# The page's light re-reads: never the whole conversation.
|
|
593
630
|
return handle_cards_read(id) if req.params["cards"] == "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";
|
|
@@ -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 || [];
|
|
@@ -1766,9 +1810,10 @@ function updateInfoBar() {
|
|
|
1766
1810
|
if (stopBtn) stopBtn.style.display = canStopSession({ owner: current.owner, streaming: !!streamControl }) ? "" : "none";
|
|
1767
1811
|
const idShort = selected.slice(0,8);
|
|
1768
1812
|
const memText = current.usedMemories.length ? current.usedMemories.join(", ") : "—";
|
|
1769
|
-
const ctxText = current.ctxPct
|
|
1813
|
+
const ctxText = ctxBarText(current.ctxPct, current.ctxWindowSource);
|
|
1770
1814
|
const tokens = current.timing.tokens;
|
|
1771
|
-
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 });
|
|
1772
1817
|
const speedTextNow = lastSpeedText(tokens);
|
|
1773
1818
|
const avgSpeed = speedText(tokens?.avg_decode_tps, tokens?.tps_source);
|
|
1774
1819
|
const speedTip = `Decode speed of the last generation${avgSpeed ? `; session average ${avgSpeed}` : ""}`;
|
|
@@ -1939,7 +1984,10 @@ const streamHandlers = {
|
|
|
1939
1984
|
}
|
|
1940
1985
|
},
|
|
1941
1986
|
generation_started: (data) => {
|
|
1942
|
-
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
|
+
}
|
|
1943
1991
|
liveTurn.timing.setNote("");
|
|
1944
1992
|
turnDom.generationStarted(data || {});
|
|
1945
1993
|
},
|
|
@@ -2226,7 +2274,11 @@ async function handleSendTurn() {
|
|
|
2226
2274
|
}
|
|
2227
2275
|
actionBtn.disabled = true;
|
|
2228
2276
|
try {
|
|
2229
|
-
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();
|
|
2230
2282
|
addCreatedSession(s);
|
|
2231
2283
|
await select(s.id);
|
|
2232
2284
|
} catch (e) {
|
|
@@ -2439,7 +2491,10 @@ const modelPicker = createModelPicker({
|
|
|
2439
2491
|
list: $("#modelList"),
|
|
2440
2492
|
composer: $("#composer"),
|
|
2441
2493
|
center: $("#center"),
|
|
2442
|
-
onPick: () =>
|
|
2494
|
+
onPick: () => {
|
|
2495
|
+
renderNewChatLlmContext();
|
|
2496
|
+
promptEl.focus();
|
|
2497
|
+
},
|
|
2443
2498
|
});
|
|
2444
2499
|
async function loadModels() {
|
|
2445
2500
|
let payload;
|
|
@@ -2453,6 +2508,7 @@ async function loadModels() {
|
|
|
2453
2508
|
if (!modelPicker.setModels(payload, stored)) return;
|
|
2454
2509
|
modelPickEl.title = payload.warning ? `The model the new chat starts on · ${payload.warning}` : "The model the new chat starts on";
|
|
2455
2510
|
modelPickEl.hidden = false;
|
|
2511
|
+
renderNewChatLlmContext();
|
|
2456
2512
|
}
|
|
2457
2513
|
// The picker's choice for a create request; nothing (the server's default)
|
|
2458
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
|
|
|
@@ -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">
|