samagotchi 0.55.0 → 0.56.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 +30 -1
- data/docs/architecture.md +6 -2
- data/docs/cli.md +29 -7
- data/docs/configuration.md +84 -8
- data/docs/internals/background-tasks.md +1 -1
- data/docs/internals/engine.md +14 -0
- data/docs/internals/prompt-caching.md +8 -0
- data/docs/sessions.md +1 -1
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +1 -1
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/cache_confirm.rb +51 -0
- data/lib/samagotchi/client_id.rb +12 -0
- data/lib/samagotchi/config.rb +7 -3
- data/lib/samagotchi/engine.rb +102 -8
- data/lib/samagotchi/engine_peers.rb +1 -0
- data/lib/samagotchi/host_config.rb +6 -3
- data/lib/samagotchi/host_registry.rb +4 -3
- data/lib/samagotchi/kernel_loop.rb +99 -16
- data/lib/samagotchi/llm/chat_loop.rb +122 -71
- data/lib/samagotchi/llm/openai_chat.rb +2 -1
- data/lib/samagotchi/llm/stream_drops.rb +63 -0
- data/lib/samagotchi/llm/turn_settings.rb +9 -5
- data/lib/samagotchi/model_settings.rb +5 -2
- data/lib/samagotchi/reasoning_history.rb +132 -0
- data/lib/samagotchi/reasoning_history_command.rb +107 -0
- data/lib/samagotchi/self_report.rb +21 -0
- data/lib/samagotchi/session.rb +23 -2
- data/lib/samagotchi/session_commands.rb +55 -15
- data/lib/samagotchi/session_manager.rb +2 -1
- data/lib/samagotchi/session_setup.rb +7 -5
- data/lib/samagotchi/setting_explained.rb +20 -0
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +2 -1
- data/lib/samagotchi/terminal_ui/attached_loop.rb +11 -0
- data/lib/samagotchi/terminal_ui/formatting.rb +8 -5
- data/lib/samagotchi/terminal_ui.rb +13 -6
- data/lib/samagotchi/thinking.rb +20 -25
- data/lib/samagotchi/thinking_command.rb +33 -11
- data/lib/samagotchi/tools/builtins.rb +6 -2
- data/lib/samagotchi/tools/peers.rb +4 -2
- data/lib/samagotchi/tools/task_runtime.rb +2 -1
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +23 -2
- data/lib/samagotchi/web/public/app.js +20 -1
- data/lib/samagotchi/web/public/commands.js +36 -3
- data/lib/samagotchi/web/public/composer.js +5 -1
- data/lib/samagotchi/web/public/index.html +6 -3
- data/lib/samagotchi/web/public/info_bar.js +5 -3
- data/lib/samagotchi/web/public/level_chip.js +174 -0
- data/lib/samagotchi/web/public/model_pick.js +1 -1
- data/lib/samagotchi/web/public/reasoning_history_chip.js +49 -0
- data/lib/samagotchi/web/public/session_state.js +5 -0
- data/lib/samagotchi/web/public/stream_handlers.js +10 -2
- data/lib/samagotchi/web/public/thinking_chip.js +33 -138
- data/lib/samagotchi/worker.rb +17 -5
- metadata +8 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "cache_confirm"
|
|
3
4
|
require_relative "thinking"
|
|
4
5
|
|
|
5
6
|
module Samagotchi
|
|
@@ -10,6 +11,7 @@ module Samagotchi
|
|
|
10
11
|
# model, its host, then thinking.level). A change takes effect at the
|
|
11
12
|
# next turn's start (the REPL and the worker run it between turns), and
|
|
12
13
|
# the reply says what it costs the prompt cache (Thinking.cache_cost).
|
|
14
|
+
# A costly change asks first (CacheConfirm); --yes (or -y) applies it.
|
|
13
15
|
class ThinkingCommand
|
|
14
16
|
NAME = "/thinking"
|
|
15
17
|
SETTABLE = (Thinking::LEVELS - [Thinking::DEFAULT]).freeze
|
|
@@ -17,35 +19,43 @@ module Samagotchi
|
|
|
17
19
|
|
|
18
20
|
COST_NOTES = {
|
|
19
21
|
full: "On this model the whole prompt is read again (Gemma's <|think|> starts it).",
|
|
20
|
-
provider: "The request's thinking fields change: a
|
|
21
|
-
"
|
|
22
|
+
provider: "The request's thinking fields change: a hosted API (OpenRouter, Claude, OpenAI) reads the whole " \
|
|
23
|
+
"conversation again.",
|
|
22
24
|
tail: "Only the prompt's tail changes: the cache keeps the rest."
|
|
23
25
|
}.freeze
|
|
24
26
|
|
|
25
|
-
# @param engine [Engine] #thinking_explained, #thinking_override(=),
|
|
26
|
-
# #session, #thinking_cache_cost
|
|
27
|
+
# @param engine [Engine] #thinking_explained(own:), #thinking_override(=),
|
|
28
|
+
# #session, #thinking_cache_cost, #history?
|
|
27
29
|
# @param save [#call] saves the session after a change
|
|
28
30
|
def initialize(engine:, save:)
|
|
29
31
|
@engine = engine
|
|
30
32
|
@save = save
|
|
31
33
|
end
|
|
32
34
|
|
|
33
|
-
# @param args [String] what follows /thinking
|
|
34
|
-
# @
|
|
35
|
+
# @param args [String] what follows /thinking (--yes anywhere)
|
|
36
|
+
# @param confirm [Boolean] ask before a costly change (CacheConfirm);
|
|
37
|
+
# false applies it at once (chi send, a delegate, a start flag)
|
|
38
|
+
# @return [Array(String, Boolean, CacheConfirm::Confirm|nil)] the reply,
|
|
39
|
+
# whether the level changed, and the confirm it asks for (nothing
|
|
40
|
+
# changed then)
|
|
35
41
|
# @raise [ArgumentError] a word that isn't a level (with USAGE)
|
|
36
|
-
def run(args)
|
|
37
|
-
word = args
|
|
38
|
-
return [listing(@engine.thinking_explained), false] if word.empty?
|
|
42
|
+
def run(args, confirm: true)
|
|
43
|
+
word, yes = CacheConfirm.split(args)
|
|
44
|
+
return [listing(@engine.thinking_explained), false, nil] if word.empty?
|
|
39
45
|
|
|
40
46
|
level = word.to_sym
|
|
41
47
|
raise ArgumentError, "unknown thinking level #{word}\n#{USAGE}" unless Thinking::LEVELS.include?(level)
|
|
42
48
|
|
|
43
49
|
session = @engine.session or raise ArgumentError, "no session yet"
|
|
50
|
+
own = SETTABLE.include?(level) ? level : nil
|
|
44
51
|
before = @engine.thinking_explained
|
|
45
|
-
|
|
52
|
+
ask = confirm && !yes ? ask(before, own, word) : nil
|
|
53
|
+
return ask if ask
|
|
54
|
+
|
|
55
|
+
@engine.thinking_override = own
|
|
46
56
|
@save.call(session)
|
|
47
57
|
after = @engine.thinking_explained
|
|
48
|
-
[[listing(after), *notes(before, after)].join("\n"), true]
|
|
58
|
+
[[listing(after), *notes(before, after)].join("\n"), true, nil]
|
|
49
59
|
end
|
|
50
60
|
|
|
51
61
|
# The level, where it came from, and what the session sets itself.
|
|
@@ -62,6 +72,18 @@ module Samagotchi
|
|
|
62
72
|
|
|
63
73
|
private
|
|
64
74
|
|
|
75
|
+
# The reply that asks before a costly change, nil when it needn't.
|
|
76
|
+
def ask(before, own, word)
|
|
77
|
+
would = @engine.thinking_explained(own: own)
|
|
78
|
+
return nil unless before && would
|
|
79
|
+
|
|
80
|
+
cost = @engine.thinking_cache_cost
|
|
81
|
+
return nil unless CacheConfirm.needed?(cost: cost, history: @engine.history?, changed: would.level != before.level)
|
|
82
|
+
|
|
83
|
+
step = CacheConfirm.confirm(NAME, word)
|
|
84
|
+
[[listing(before), "Switching to #{would.level} restarts the prompt cache: #{COST_NOTES[cost]}", step.text].join("\n"), false, step]
|
|
85
|
+
end
|
|
86
|
+
|
|
65
87
|
def notes(before, after)
|
|
66
88
|
return [] unless before && after
|
|
67
89
|
return ["(no change to what the next turn runs under)"] if before.level == after.level
|
|
@@ -121,7 +121,8 @@ module Samagotchi
|
|
|
121
121
|
# /model), which chi self reports; unset when unknown, so a value
|
|
122
122
|
# inherited from an outer session never passes for this one. So too
|
|
123
123
|
# the session's own thinking level (SAMAGOTCHI_SESSION_THINKING, unset
|
|
124
|
-
# without one), which chi self puts first
|
|
124
|
+
# without one), which chi self puts first, and its own reasoning_history
|
|
125
|
+
# level (SAMAGOTCHI_SESSION_REASONING_HISTORY, the same way). A
|
|
125
126
|
# worker's `chi --model X` default (ModelProfile::MODEL_FROM_CLI_ENV)
|
|
126
127
|
# is unset: it was that run's, not the default of a chi run here.
|
|
127
128
|
def parent_env(kctx, env: ENV)
|
|
@@ -129,9 +130,11 @@ module Samagotchi
|
|
|
129
130
|
id = peers.respond_to?(:session_id) ? peers.session_id.to_s : ""
|
|
130
131
|
model = peers.respond_to?(:model_ref) ? peers.model_ref.to_s : ""
|
|
131
132
|
thinking = peers.respond_to?(:thinking) ? peers.thinking&.to_s : nil
|
|
133
|
+
history = peers.respond_to?(:reasoning_history) ? peers.reasoning_history&.to_s : nil
|
|
132
134
|
{ Guardrails::ParentApprovals::PARENT_SESSION_ENV => id.empty? ? "chi" : id,
|
|
133
135
|
SESSION_MODEL_ENV => model.empty? ? nil : model,
|
|
134
|
-
SESSION_THINKING_ENV => thinking
|
|
136
|
+
SESSION_THINKING_ENV => thinking,
|
|
137
|
+
SESSION_REASONING_HISTORY_ENV => history }.merge(cli_model_unset(env))
|
|
135
138
|
end
|
|
136
139
|
|
|
137
140
|
# The unsets that keep a `chi --model X` worker's default from passing
|
|
@@ -144,6 +147,7 @@ module Samagotchi
|
|
|
144
147
|
|
|
145
148
|
SESSION_MODEL_ENV = "SAMAGOTCHI_SESSION_MODEL"
|
|
146
149
|
SESSION_THINKING_ENV = "SAMAGOTCHI_SESSION_THINKING"
|
|
150
|
+
SESSION_REASONING_HISTORY_ENV = "SAMAGOTCHI_SESSION_REASONING_HISTORY"
|
|
147
151
|
|
|
148
152
|
# task_create's parent_env, less the unsets of a variable the model
|
|
149
153
|
# set in its env (a SAMAGOTCHI_DEFAULT_MODEL it asked for stays).
|
|
@@ -9,10 +9,12 @@ module Samagotchi
|
|
|
9
9
|
# delegate's approval is relayed through (relay: nil when this session
|
|
10
10
|
# can't host one), and the model the session runs on (model_ref: the
|
|
11
11
|
# resolved ref, live after /model), and the session's own thinking level
|
|
12
|
-
# (thinking: nil without one)
|
|
12
|
+
# (thinking: nil without one) and reasoning_history level
|
|
13
|
+
# (reasoning_history: nil without one). Engine hands KernelLoop one that follows its current
|
|
13
14
|
# session (Engine::PeerView); this plain one is for specs and a kernel
|
|
14
15
|
# without an Engine.
|
|
15
|
-
Peers = Struct.new(:session_id, :cwd, :state_dir, :cancelled, :relay, :model_ref, :thinking,
|
|
16
|
+
Peers = Struct.new(:session_id, :cwd, :state_dir, :cancelled, :relay, :model_ref, :thinking, :reasoning_history,
|
|
17
|
+
keyword_init: true) do
|
|
16
18
|
# @return [Boolean] the turn was canceled (:cancelled is a proc or a value)
|
|
17
19
|
def cancelled?
|
|
18
20
|
cancelled.respond_to?(:call) ? !!cancelled.call : !!cancelled
|
|
@@ -22,7 +22,8 @@ module Samagotchi
|
|
|
22
22
|
OUTPUT_TAIL_READ_BYTES = 16 * 1024
|
|
23
23
|
SANITIZED_ENV_KEYS = %w[RUBYOPT RUBYLIB BUNDLE_GEMFILE BUNDLE_BIN_PATH BUNDLER_VERSION].freeze
|
|
24
24
|
# Set by chi (Builtins.parent_env); the model's env can't change it.
|
|
25
|
-
MARKER_ENV_KEYS = %w[SAMAGOTCHI_PARENT_SESSION SAMAGOTCHI_SESSION_MODEL SAMAGOTCHI_SESSION_THINKING
|
|
25
|
+
MARKER_ENV_KEYS = %w[SAMAGOTCHI_PARENT_SESSION SAMAGOTCHI_SESSION_MODEL SAMAGOTCHI_SESSION_THINKING
|
|
26
|
+
SAMAGOTCHI_SESSION_REASONING_HISTORY].freeze
|
|
26
27
|
# task_wait and task_get add it to a task the user stopped (the web's
|
|
27
28
|
# stop-task button), so the model doesn't rerun the job.
|
|
28
29
|
# A record whose pid isn't a process group chi started (missing, 0, 1,
|
data/lib/samagotchi/version.rb
CHANGED
data/lib/samagotchi/web/app.rb
CHANGED
|
@@ -26,6 +26,7 @@ require_relative "../llm_context_override"
|
|
|
26
26
|
require_relative "../model_catalog"
|
|
27
27
|
require_relative "../model_profile"
|
|
28
28
|
require_relative "../thinking"
|
|
29
|
+
require_relative "../reasoning_history"
|
|
29
30
|
require_relative "../sampling_settings"
|
|
30
31
|
require_relative "../served_model"
|
|
31
32
|
require_relative "../project_scope"
|
|
@@ -517,7 +518,7 @@ module Samagotchi
|
|
|
517
518
|
end
|
|
518
519
|
|
|
519
520
|
# The thinking level +model+ starts at, with where it came from (the
|
|
520
|
-
# start page's think chip;
|
|
521
|
+
# start page's think chip; SettingExplained#summary, no session).
|
|
521
522
|
def add_thinking(model, target, names, settings)
|
|
522
523
|
model[:thinking] = Thinking.explain(target, names: names, models: settings).summary
|
|
523
524
|
rescue StandardError
|
|
@@ -561,7 +562,7 @@ module Samagotchi
|
|
|
561
562
|
end
|
|
562
563
|
|
|
563
564
|
# A session's thinking level with where it came from and its own
|
|
564
|
-
# (
|
|
565
|
+
# (SettingExplained#summary), as its worker would resolve it; nil
|
|
565
566
|
# when it can't be. Config and the registry only: no network.
|
|
566
567
|
def thinking_for(session)
|
|
567
568
|
registry = host_registry
|
|
@@ -573,6 +574,22 @@ module Samagotchi
|
|
|
573
574
|
nil
|
|
574
575
|
end
|
|
575
576
|
|
|
577
|
+
# A session's reasoning_history level with where it came from and its
|
|
578
|
+
# own (SettingExplained#summary), as its worker would resolve it; nil
|
|
579
|
+
# on a native host (it ignores the level) or when it can't be. Config
|
|
580
|
+
# and the registry only: no network.
|
|
581
|
+
def reasoning_history_for(session)
|
|
582
|
+
registry = host_registry
|
|
583
|
+
name = session.model_name.to_s.strip.empty? ? ModelProfile.required_model_name(nil) : session.model_name
|
|
584
|
+
target = registry.resolve(name)
|
|
585
|
+
return nil unless target.entry&.chat?
|
|
586
|
+
|
|
587
|
+
names = registry.lookup_names(session.model_typed || name, resolved: name, target: target)
|
|
588
|
+
ReasoningHistory.explain(target, names: names, session: session.reasoning_history).summary
|
|
589
|
+
rescue StandardError
|
|
590
|
+
nil
|
|
591
|
+
end
|
|
592
|
+
|
|
576
593
|
# Built again when config.yml's hosts: changed (compared as read: the
|
|
577
594
|
# YAML read is mtime-cached), so the model picker lists a host added
|
|
578
595
|
# while chi web runs. An injected registry (specs) stays.
|
|
@@ -820,6 +837,10 @@ module Samagotchi
|
|
|
820
837
|
session_json = session_json.merge(llm_context: (snapshot && snapshot["llm_context"]) || llm_context_for(session))
|
|
821
838
|
# The thinking level the next turn runs at (the think chip), the same way.
|
|
822
839
|
session_json = session_json.merge(thinking: (snapshot && snapshot["thinking"]) || thinking_for(session))
|
|
840
|
+
# Its reasoning history (the reasoning chip): the worker's (nil on a
|
|
841
|
+
# native host), else the server's reading (an older worker, none).
|
|
842
|
+
history = snapshot&.key?("reasoning_history") ? snapshot["reasoning_history"] : reasoning_history_for(session)
|
|
843
|
+
session_json = session_json.merge(reasoning_history: history)
|
|
823
844
|
raw_messages = turn_snapshot ? turn_snapshot["messages"] : session.messages
|
|
824
845
|
timing = timing_payload(id, live_metrics: snapshot && snapshot["metrics"], persisted: persisted)
|
|
825
846
|
json_response(200, {
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
chipModel as thinkingChipModel, createThinkingControl,
|
|
31
31
|
withChoice as withThinkingChoice,
|
|
32
32
|
} from "./thinking_chip.js";
|
|
33
|
+
import { createReasoningHistoryControl } from "./reasoning_history_chip.js";
|
|
33
34
|
import { renderInfoBar } from "./info_bar.js";
|
|
34
35
|
import { createShownTexts } from "./shown_texts.js";
|
|
35
36
|
import { MODEL_KEY } from "./model_pick.js";
|
|
@@ -67,7 +68,7 @@ import { createLiveTurn } from "./live_turn.js";
|
|
|
67
68
|
import { installRefClicks, setRefKinds } from "./refs.js";
|
|
68
69
|
import { DONE_MS, applyInitEvent, dropDone, initRowHtml, newInitTasks, seedInitTasks } from "./init_row.js";
|
|
69
70
|
import { isReportClient, hookNoticeLabel, llmContextNotice, newClientId, noticeLine, promptOps, snapshotEvents, workerGoneText } from "./turn_events.js";
|
|
70
|
-
import { drawCommandBubble, keptCommandBubbles, restoreCommandBubbles } from "./commands.js";
|
|
71
|
+
import { applyConfirm, drawCommandBubble, keptCommandBubbles, restoreCommandBubbles } from "./commands.js";
|
|
71
72
|
import { createComposerCommands, routeComposerLine, routeSessionLine } from "./composer.js";
|
|
72
73
|
import { composerAfterAck, dropEarlyRestores, restoreOnAck } from "./restore.js";
|
|
73
74
|
import { createStreamHandlers } from "./stream_handlers.js";
|
|
@@ -134,6 +135,15 @@ const thinkingControl = createThinkingControl({
|
|
|
134
135
|
running: () => liveTurn.running,
|
|
135
136
|
run: (line) => runSessionCommand(line),
|
|
136
137
|
});
|
|
138
|
+
// The open session's reasoning history chip, after the thinking one (a
|
|
139
|
+
// chat host's only): /reasoning-history runs in the session's worker.
|
|
140
|
+
const reasoningHistoryControl = createReasoningHistoryControl({
|
|
141
|
+
el: infoTextEl,
|
|
142
|
+
summary: () => (selected ? current.reasoningHistory : null),
|
|
143
|
+
session: () => selected,
|
|
144
|
+
running: () => liveTurn.running,
|
|
145
|
+
run: (line) => runSessionCommand(line),
|
|
146
|
+
});
|
|
137
147
|
// The start page's thinking chip: the picked model's level, and a level
|
|
138
148
|
// for the next new chat only (sent with its create, then dropped).
|
|
139
149
|
const thinkingNewEl = $("#thinkingNew");
|
|
@@ -763,6 +773,7 @@ function renderSessionView(data, { kept = [] } = {}) {
|
|
|
763
773
|
const activeTurnId = current.timing.activeTurn?.id || null;
|
|
764
774
|
llmContextControl.refresh();
|
|
765
775
|
thinkingControl.refresh();
|
|
776
|
+
reasoningHistoryControl.refresh();
|
|
766
777
|
if (data.messages && data.messages.length) renderHistory(data.messages);
|
|
767
778
|
else renderHistory(data.history || []);
|
|
768
779
|
restoreCommandBubbles(historyEl, kept);
|
|
@@ -1429,6 +1440,7 @@ function updateInfoBar() {
|
|
|
1429
1440
|
kids: childrenSummary({ id: selected }, allSessions),
|
|
1430
1441
|
llmContext: current.llmContext,
|
|
1431
1442
|
thinking: current.thinking,
|
|
1443
|
+
reasoningHistory: current.reasoningHistory,
|
|
1432
1444
|
// The model notes the session's prompt carried.
|
|
1433
1445
|
notes: promptNotesChip(current.promptNotes),
|
|
1434
1446
|
memories: current.usedMemories,
|
|
@@ -1596,6 +1608,7 @@ const streamHandlers = createStreamHandlers({
|
|
|
1596
1608
|
contextChips,
|
|
1597
1609
|
llmContextControl,
|
|
1598
1610
|
thinkingControl,
|
|
1611
|
+
reasoningHistoryControl,
|
|
1599
1612
|
sentIds,
|
|
1600
1613
|
earlyRestores,
|
|
1601
1614
|
stream: {
|
|
@@ -2284,6 +2297,12 @@ const annotateBar = createAnnotateBar({
|
|
|
2284
2297
|
},
|
|
2285
2298
|
});
|
|
2286
2299
|
historyEl.addEventListener("scroll", annotateBar.schedule);
|
|
2300
|
+
// A command bubble's Apply (CacheConfirm): sends the change's --yes line
|
|
2301
|
+
// once; its own bubble then says what it did.
|
|
2302
|
+
historyEl.addEventListener("click", (e) => {
|
|
2303
|
+
const button = e.target.closest?.("[data-confirm-line]");
|
|
2304
|
+
if (button) applyConfirm(button, runSessionCommand);
|
|
2305
|
+
});
|
|
2287
2306
|
turnDom.scrollEl?.addEventListener("scroll", annotateBar.schedule, true);
|
|
2288
2307
|
turnRoots().forEach((root) => installCopy(root));
|
|
2289
2308
|
|
|
@@ -148,13 +148,17 @@ export function continueLine(choice, reason = "") {
|
|
|
148
148
|
// it shows; its command_ran fills that bubble in (`commandId`). So is a
|
|
149
149
|
// command queued for after the running turn (`queued`): its command_queued
|
|
150
150
|
// (waits: "turn_end") draws it `waiting`, saying so, until it ran or was
|
|
151
|
-
// dropped (styled as failed).
|
|
151
|
+
// dropped (styled as failed). A change that restarts the prompt cache asks
|
|
152
|
+
// first (CacheConfirm): `confirm` is the line that applies it, and this
|
|
153
|
+
// tab's own bubble (`own`) gets an Apply button that sends it.
|
|
152
154
|
export const QUEUED_TEXT = "queued: runs after this turn";
|
|
155
|
+
export const APPLY_TEXT = "Apply (restarts the cache)";
|
|
153
156
|
|
|
154
157
|
export function commandView(event, myId) {
|
|
155
158
|
const own = isOwn(event.client_id, myId);
|
|
156
159
|
const waiting = event.waits === "turn_end";
|
|
157
160
|
return {
|
|
161
|
+
own,
|
|
158
162
|
label: own ? null : clientLabel(event.client_id),
|
|
159
163
|
line: event.line || "",
|
|
160
164
|
text: event.output || (waiting ? QUEUED_TEXT : ""),
|
|
@@ -166,6 +170,7 @@ export function commandView(event, myId) {
|
|
|
166
170
|
queued: waiting || event.queued === true,
|
|
167
171
|
waiting,
|
|
168
172
|
commandId: event.command_id || null,
|
|
173
|
+
confirm: typeof event.confirm?.line === "string" ? event.confirm.line : null,
|
|
169
174
|
// A card's action (Nudge…): no bubble unless it says something back.
|
|
170
175
|
hidden: event.card === true && !event.output && event.status !== "error",
|
|
171
176
|
};
|
|
@@ -183,8 +188,12 @@ export function historyHasCommandBubble(historyBubbles, line) {
|
|
|
183
188
|
}
|
|
184
189
|
|
|
185
190
|
// A command bubble's line and output (again, when its command_ran fills in
|
|
186
|
-
// an anytime command's bubble in place)
|
|
187
|
-
|
|
191
|
+
// an anytime command's bubble in place), and this tab's own change's Apply
|
|
192
|
+
// button when it asks first (a delegated click sends its line: app.js).
|
|
193
|
+
// Command bubbles are page-only: after a reload the button is gone, and
|
|
194
|
+
// the change is typed again.
|
|
195
|
+
export function drawCommandBubble(div, { label, line, text, busy = false, failed = false, waiting = false, commandId = null,
|
|
196
|
+
own = false, confirm = null }) {
|
|
188
197
|
div.className = "bubble command" + (busy ? " busy" : "") + (failed ? " failed" : "") + (waiting ? " queued" : "");
|
|
189
198
|
if (commandId) div.dataset.commandId = commandId;
|
|
190
199
|
div.replaceChildren();
|
|
@@ -198,6 +207,30 @@ export function drawCommandBubble(div, { label, line, text, busy = false, failed
|
|
|
198
207
|
body.textContent = text;
|
|
199
208
|
div.appendChild(body);
|
|
200
209
|
}
|
|
210
|
+
if (own && confirm) {
|
|
211
|
+
const apply = document.createElement("button");
|
|
212
|
+
apply.type = "button";
|
|
213
|
+
apply.className = "command-confirm";
|
|
214
|
+
apply.dataset.confirmLine = confirm;
|
|
215
|
+
apply.title = confirm;
|
|
216
|
+
apply.textContent = APPLY_TEXT;
|
|
217
|
+
div.appendChild(apply);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// A click on a bubble's Apply button: sends its --yes line once (the
|
|
222
|
+
// button is disabled at once), and enables it again when the line wasn't
|
|
223
|
+
// sent (+run+ resolves false or throws), so the user can try again.
|
|
224
|
+
export async function applyConfirm(button, run) {
|
|
225
|
+
if (!button || button.disabled) return;
|
|
226
|
+
button.disabled = true;
|
|
227
|
+
let sent = false;
|
|
228
|
+
try {
|
|
229
|
+
sent = await run(button.dataset.confirmLine);
|
|
230
|
+
} catch {
|
|
231
|
+
sent = false;
|
|
232
|
+
}
|
|
233
|
+
if (!sent) button.disabled = false;
|
|
201
234
|
}
|
|
202
235
|
|
|
203
236
|
// The prompt and command bubbles in the history, in order, by kind
|
|
@@ -86,14 +86,18 @@ export function createComposerCommands(deps) {
|
|
|
86
86
|
|
|
87
87
|
// A session command from a control (the llm ctx chip), not the composer:
|
|
88
88
|
// its command_ran shows its line and reply, as a typed one's would.
|
|
89
|
+
// Resolves true once the worker took it, false when it wasn't sent (no
|
|
90
|
+
// session, a refusal or a network error: the failed bubble says why).
|
|
89
91
|
async function run(line) {
|
|
90
92
|
const id = session();
|
|
91
|
-
if (!id) return;
|
|
93
|
+
if (!id) return false;
|
|
92
94
|
try {
|
|
93
95
|
await sendCommand(id, line, { clientId });
|
|
94
96
|
followWokenWorker();
|
|
97
|
+
return true;
|
|
95
98
|
} catch (e) {
|
|
96
99
|
failed(line, e.message);
|
|
100
|
+
return false;
|
|
97
101
|
}
|
|
98
102
|
}
|
|
99
103
|
|
|
@@ -307,6 +307,9 @@ button.task-stop:hover:not(:disabled){background:var(--danger-bg-dim);filter:non
|
|
|
307
307
|
.bubble.command.queued .command-output{color:var(--text-faint)}
|
|
308
308
|
.command-line{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;color:var(--text-3)}
|
|
309
309
|
.command-output{margin:6px 0 0;white-space:pre-wrap;font-size:12px;color:var(--text-2)}
|
|
310
|
+
.bubble.command .command-confirm{margin-top:8px;padding:3px 10px;border-radius:6px;font:inherit;font-size:12px;cursor:pointer;background:var(--accent-bg);color:var(--accent);border:1px solid var(--accent-border)}
|
|
311
|
+
.bubble.command .command-confirm:hover:not(:disabled){border-color:var(--accent)}
|
|
312
|
+
.bubble.command .command-confirm:disabled{opacity:.55;cursor:default}
|
|
310
313
|
.bubble.continue{border-left:3px solid var(--accent);background:var(--bg-panel);padding:10px 12px}
|
|
311
314
|
.bubble.continue.resolved{border-left-color:var(--text-muted);opacity:.8}
|
|
312
315
|
.continue-text{font-size:14px;color:var(--text);margin-bottom:6px}
|
|
@@ -704,9 +707,9 @@ body.stage-view #dock,body.stage-view #dock:focus-within{background:transparent;
|
|
|
704
707
|
#infoBar #infoDeleteBtn{background:transparent;color:var(--text-muted);border:1px solid transparent}
|
|
705
708
|
#infoBar #infoDeleteBtn:hover{color:var(--on-solid);background:var(--danger-bg);border-color:var(--danger-bg)}
|
|
706
709
|
/* The LLM context and thinking chips (llm_context_chip.js, thinking_chip.js): model-like chips that open their popovers; accent when the session set its own. */
|
|
707
|
-
#infoBar .meta .llmctx-chip,#infoBar .meta .think-chip{padding:2px 6px;border-radius:6px;font-size:11px;font-weight:400;border:1px solid transparent;cursor:pointer;font-family:inherit;white-space:nowrap}
|
|
708
|
-
#infoBar .meta .llmctx-chip:hover,#infoBar .meta .think-chip:hover{color:var(--text);border-color:var(--accent-border);background:var(--accent-bg)}
|
|
709
|
-
#infoBar .meta .llmctx-chip.own,#infoBar .meta .think-chip.own{color:var(--accent);border-color:var(--accent-border)}
|
|
710
|
+
#infoBar .meta .llmctx-chip,#infoBar .meta .think-chip,#infoBar .meta .rh-chip{padding:2px 6px;border-radius:6px;font-size:11px;font-weight:400;border:1px solid transparent;cursor:pointer;font-family:inherit;white-space:nowrap}
|
|
711
|
+
#infoBar .meta .llmctx-chip:hover,#infoBar .meta .think-chip:hover,#infoBar .meta .rh-chip:hover{color:var(--text);border-color:var(--accent-border);background:var(--accent-bg)}
|
|
712
|
+
#infoBar .meta .llmctx-chip.own,#infoBar .meta .think-chip.own,#infoBar .meta .rh-chip.own{color:var(--accent);border-color:var(--accent-border)}
|
|
710
713
|
.llmctx-form{display:flex;flex-direction:column;gap:8px;margin:0}
|
|
711
714
|
.llmctx-form .ctx-pop-head strong{font-family:inherit}
|
|
712
715
|
.llmctx-field{display:flex;align-items:center;gap:8px}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// The session bar's info line (#infoText): the open session's id, its copy
|
|
2
2
|
// chi --attach button, status, model, chain links, parent and delegates,
|
|
3
|
-
// llm ctx and
|
|
3
|
+
// llm ctx, thinking and reasoning history chips, model notes, memories, folder, ctx, speed,
|
|
4
4
|
// session clock and the worker's version badge. A lit-html template over a
|
|
5
5
|
// plain model app.js builds (updateInfoBar), so the markup lives here and
|
|
6
6
|
// runs under node --test (spec/web/public/info_bar.test.js).
|
|
@@ -15,6 +15,7 @@ import { memChipText, memChipTitle } from "./format.js";
|
|
|
15
15
|
import { ALL_SESSIONS_HASH, sessionHash } from "./route.js";
|
|
16
16
|
import { chipTemplate as llmContextChip } from "./llm_context_chip.js";
|
|
17
17
|
import { chipTemplate as thinkingChip } from "./thinking_chip.js";
|
|
18
|
+
import { chipTemplate as reasoningHistoryChip } from "./reasoning_history_chip.js";
|
|
18
19
|
|
|
19
20
|
export const PREV = "←";
|
|
20
21
|
export const NEXT = "→";
|
|
@@ -60,7 +61,8 @@ function kidsTemplate(kids) {
|
|
|
60
61
|
// id, copied (the copy button just copied), status, model ({text, title,
|
|
61
62
|
// mismatch}: format.js modelLabel), chain ({prev, next}: chainLinkTemplate's
|
|
62
63
|
// links or null), parent ({id, title} or null), kids (format.js
|
|
63
|
-
// childrenSummary), llmContext and
|
|
64
|
+
// childrenSummary), llmContext, thinking and reasoningHistory (the chips'
|
|
65
|
+
// summaries; reasoningHistory null on a native host: no chip), notes
|
|
64
66
|
// ({text, title} or null), memories (the used names), dir, ctx ({text,
|
|
65
67
|
// tip}), speed ({text, tip} or null), clock (the session time's text, ""
|
|
66
68
|
// for none), worker ({badge, busy} or null).
|
|
@@ -68,7 +70,7 @@ export function infoBarHtml(bar) {
|
|
|
68
70
|
if (!bar) return nothing;
|
|
69
71
|
const attach = `chi --attach ${bar.id}`;
|
|
70
72
|
const memories = bar.memories || [];
|
|
71
|
-
return html`<div class="meta"><span class="id" title=${bar.id}>${String(bar.id).slice(0, 8)}</span><button type="button" class="copy-attach" title=${`Copy "${attach}" to attach a terminal`}>${bar.copied ? "copied ✓" : "copy chi --attach"}</button><span class=${bar.status ? `status ${bar.status}` : "status"}>${bar.status || ""}</span><span class=${`model${bar.model.mismatch ? " served-mismatch" : ""}`} title=${bar.model.title}>${bar.model.text}</span>${bar.chain?.prev ? chainLinkTemplate(bar.chain.prev, PREV, "The previous link of this chain") : nothing}${bar.chain?.next ? chainLinkTemplate(bar.chain.next, NEXT, "The next link of this chain") : nothing}${bar.parent ? html`<a class="model parent" href=${sessionHash(bar.parent.id)} title=${bar.parent.title}>delegated by ${String(bar.parent.id).slice(0, 8)}</a>` : nothing}${kidsTemplate(bar.kids)}${llmContextChip(bar.llmContext)}${thinkingChip(bar.thinking)}${bar.notes ? html`<span class="model notes-chip" title=${bar.notes.title}>${bar.notes.text}</span>` : nothing}${memories.length ? html`<span class="model mem-chip" title=${memChipTitle(memories)}>${memChipText(memories)}</span>` : nothing}<span class="dir" title=${bar.dir || ""}><bdi>${bar.dir || ""}</bdi></span><span class="model ctx" title=${bar.ctx.tip}>${bar.ctx.text}</span>${bar.speed ? html`<span class="model speed" title=${bar.speed.tip}>${bar.speed.text}</span>` : nothing}${bar.clock ? html`<span class="model session-time">${bar.clock}</span>` : nothing}${workerBadgeTemplate(bar.worker)}</div>`;
|
|
73
|
+
return html`<div class="meta"><span class="id" title=${bar.id}>${String(bar.id).slice(0, 8)}</span><button type="button" class="copy-attach" title=${`Copy "${attach}" to attach a terminal`}>${bar.copied ? "copied ✓" : "copy chi --attach"}</button><span class=${bar.status ? `status ${bar.status}` : "status"}>${bar.status || ""}</span><span class=${`model${bar.model.mismatch ? " served-mismatch" : ""}`} title=${bar.model.title}>${bar.model.text}</span>${bar.chain?.prev ? chainLinkTemplate(bar.chain.prev, PREV, "The previous link of this chain") : nothing}${bar.chain?.next ? chainLinkTemplate(bar.chain.next, NEXT, "The next link of this chain") : nothing}${bar.parent ? html`<a class="model parent" href=${sessionHash(bar.parent.id)} title=${bar.parent.title}>delegated by ${String(bar.parent.id).slice(0, 8)}</a>` : nothing}${kidsTemplate(bar.kids)}${llmContextChip(bar.llmContext)}${thinkingChip(bar.thinking)}${reasoningHistoryChip(bar.reasoningHistory)}${bar.notes ? html`<span class="model notes-chip" title=${bar.notes.title}>${bar.notes.text}</span>` : nothing}${memories.length ? html`<span class="model mem-chip" title=${memChipTitle(memories)}>${memChipText(memories)}</span>` : nothing}<span class="dir" title=${bar.dir || ""}><bdi>${bar.dir || ""}</bdi></span><span class="model ctx" title=${bar.ctx.tip}>${bar.ctx.text}</span>${bar.speed ? html`<span class="model speed" title=${bar.speed.tip}>${bar.speed.text}</span>` : nothing}${bar.clock ? html`<span class="model session-time">${bar.clock}</span>` : nothing}${workerBadgeTemplate(bar.worker)}</div>`;
|
|
72
74
|
}
|
|
73
75
|
|
|
74
76
|
// Draws +bar+ into +el+ (#infoText), with the memories in its tooltip.
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// A level chip in the session bar and its popover: the level the next turn
|
|
2
|
+
// runs at ("think low", "reasoning all"; "· session" when the session set
|
|
3
|
+
// its own) and a form with a level select that runs the setting's command
|
|
4
|
+
// in the session's worker (the reply shows in the conversation, like a
|
|
5
|
+
// typed one). The summary is the setting's SettingExplained#summary
|
|
6
|
+
// ({level, source, own}: the worker's snapshot, a command_ran, or the
|
|
7
|
+
// server's own reading of the session file).
|
|
8
|
+
//
|
|
9
|
+
// A config names the setting (thinking_chip.js, reasoning_history_chip.js):
|
|
10
|
+
// name the popover's heading ("Thinking")
|
|
11
|
+
// prefix the chip's word ("think")
|
|
12
|
+
// command the command ("/thinking")
|
|
13
|
+
// levels the settable levels (the select's options after "follow")
|
|
14
|
+
// anchor the chip's selector (its data attribute: each config's
|
|
15
|
+
// chipTemplate draws it; clicks are delegated on the same
|
|
16
|
+
// element as the other chips')
|
|
17
|
+
// formClass, popoverClass, nowClass the form's, popover's and "Now:"
|
|
18
|
+
// line's own classes
|
|
19
|
+
// when(fresh) the form's note on when it applies (fresh: the start
|
|
20
|
+
// page's new chat, thinking only)
|
|
21
|
+
//
|
|
22
|
+
// The start page's new chat (kind NEW_CHAT) has a thinking chip only
|
|
23
|
+
// (thinking_chip.js keeps its choice): the form shows the model's level and
|
|
24
|
+
// takes a choice for the next create.
|
|
25
|
+
|
|
26
|
+
import { NEW_CHAT, SESSION, popoverPlace } from "./llm_context_chip.js";
|
|
27
|
+
import { html, nothing, render } from "./vendor/lit-html.js";
|
|
28
|
+
|
|
29
|
+
export const DEFAULT = "default";
|
|
30
|
+
|
|
31
|
+
// The chip: its text, tooltip and whether the session (or the new chat)
|
|
32
|
+
// set its own; null without a summary (an older worker, or a host the
|
|
33
|
+
// setting means nothing on).
|
|
34
|
+
export function chipModel(cfg, summary, { kind = SESSION } = {}) {
|
|
35
|
+
if (!summary || !summary.level) return null;
|
|
36
|
+
const own = !!summary.own;
|
|
37
|
+
const what = kind === NEW_CHAT ? "the new chat" : "this session";
|
|
38
|
+
const from = own ? what : summary.source || "the model's own";
|
|
39
|
+
const title = `${cfg.name}: ${summary.level} (${from}). Click to change it for ${what}.`;
|
|
40
|
+
return { text: `${cfg.prefix} ${summary.level}${own ? ` · ${kind}` : ""}`, title, own };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// The command line for a select value ("default" unsets the session's own),
|
|
44
|
+
// with --yes: picking a level in the form is the choice, and the form
|
|
45
|
+
// names the cost (CacheConfirm asks nothing then); null when it is what
|
|
46
|
+
// the form opened with.
|
|
47
|
+
export function commandLine(cfg, level, initial = null) {
|
|
48
|
+
if (initial !== null && level === initial) return null;
|
|
49
|
+
return `${cfg.command} ${level} --yes`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// "Now: low (session)", "Now: default (the model's own)".
|
|
53
|
+
export function nowText(summary) {
|
|
54
|
+
return `Now: ${summary.level} (${summary.source || "the model's own"})`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const option = (value, label, chosen) => html`<option value=${value} ?selected=${value === chosen}>${label}</option>`;
|
|
58
|
+
|
|
59
|
+
// The popover's form (a lit-html template). For the new chat (kind
|
|
60
|
+
// NEW_CHAT), +summary+ is the model's and +choice+ the level the form
|
|
61
|
+
// shows; +now+ is the summary its "Now:" line reads (refresh passes a
|
|
62
|
+
// newer one, the rest stays as the form opened).
|
|
63
|
+
export function formTemplate(cfg, summary, { running = false, kind = SESSION, choice = null, now = summary } = {}) {
|
|
64
|
+
const fresh = kind === NEW_CHAT;
|
|
65
|
+
const chosen = (fresh ? choice : summary.own) || DEFAULT;
|
|
66
|
+
const follow = summary.own && !fresh ? "follow the model" : `follow the model (${summary.level})`;
|
|
67
|
+
return html`<form class=${`${cfg.formClass} llmctx-form`}><div class="ctx-pop-head"><strong>${cfg.name}</strong
|
|
68
|
+
><span class="ctx-pop-kind">${fresh ? NEW_CHAT : "this session"}</span
|
|
69
|
+
><button type="button" class="ctx-pop-close" data-act="close" aria-label="Close">×</button></div
|
|
70
|
+
><div class=${`ctx-pop-meta ${cfg.nowClass}`}>${nowText(now)}</div
|
|
71
|
+
><label class="llmctx-field"><span>Level</span><select name="level">${option(DEFAULT, follow, chosen)}${
|
|
72
|
+
cfg.levels.map((level) => option(level, level, chosen))}</select></label
|
|
73
|
+
><div class="ctx-pop-meta">${cfg.when(fresh)}${running ? " A turn is running: it applies after this turn." : ""}</div
|
|
74
|
+
><div class="ctx-pop-actions"><button type="submit" class="llmctx-set">Set</button></div></form>`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// The chip's popover, as createLlmContextControl's: el is where the chip
|
|
78
|
+
// is drawn (clicks are delegated: #infoText, info_bar.js);
|
|
79
|
+
// summary(): the open session's summary; session(): its id; running(): a
|
|
80
|
+
// turn runs; run(line): sends the command. The start page's (kind
|
|
81
|
+
// NEW_CHAT): summary() is the picked model's, choice() the pending level
|
|
82
|
+
// and choose(next) takes a Set's (null for "follow the model"). id and
|
|
83
|
+
// anchor (the chip's selector, by default its data attribute) keep it
|
|
84
|
+
// apart from the other controls on the same element (the anchor defaults
|
|
85
|
+
// to the config's).
|
|
86
|
+
export function createLevelControl({ cfg, el, summary, session = () => null, running = () => false, run,
|
|
87
|
+
kind = SESSION, choice = () => null, choose = null,
|
|
88
|
+
id, anchor: anchorSel = cfg.anchor,
|
|
89
|
+
doc = document, win = window }) {
|
|
90
|
+
const fresh = kind === NEW_CHAT;
|
|
91
|
+
const pop = doc.createElement("div");
|
|
92
|
+
pop.id = id;
|
|
93
|
+
pop.className = `ctx-popover ${cfg.popoverClass}`;
|
|
94
|
+
pop.setAttribute("role", "dialog");
|
|
95
|
+
pop.hidden = true;
|
|
96
|
+
doc.body.appendChild(pop);
|
|
97
|
+
let initial = null;
|
|
98
|
+
let openFor = null;
|
|
99
|
+
// What the open form was drawn from: the model's or session's summary
|
|
100
|
+
// and the form's options. A closed popover is empty, so each open draws
|
|
101
|
+
// a fresh form (an unsent pick is gone).
|
|
102
|
+
let shown = null;
|
|
103
|
+
const draw = (now) => render(formTemplate(cfg, shown.data, { ...shown.opts, now }), pop);
|
|
104
|
+
|
|
105
|
+
function place(anchor) {
|
|
106
|
+
const width = Math.min(320, win.innerWidth - 32);
|
|
107
|
+
pop.style.width = `${width}px`;
|
|
108
|
+
pop.style.maxHeight = "";
|
|
109
|
+
const at = popoverPlace(anchor.getBoundingClientRect(), { width, height: pop.offsetHeight,
|
|
110
|
+
innerWidth: win.innerWidth, innerHeight: win.innerHeight });
|
|
111
|
+
pop.style.left = `${at.left}px`;
|
|
112
|
+
pop.style.top = at.top === undefined ? "" : `${at.top}px`;
|
|
113
|
+
pop.style.bottom = at.bottom === undefined ? "" : `${at.bottom}px`;
|
|
114
|
+
if (at.maxHeight !== undefined) pop.style.maxHeight = `${at.maxHeight}px`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function open(anchor) {
|
|
118
|
+
const data = summary();
|
|
119
|
+
if (!data) return;
|
|
120
|
+
initial = (fresh ? choice() : data.own) || DEFAULT;
|
|
121
|
+
openFor = session();
|
|
122
|
+
shown = { data, opts: { running: running(), kind, choice: choice() } };
|
|
123
|
+
draw(data);
|
|
124
|
+
pop.hidden = false;
|
|
125
|
+
place(anchor);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function close() {
|
|
129
|
+
pop.hidden = true;
|
|
130
|
+
initial = null;
|
|
131
|
+
openFor = null;
|
|
132
|
+
shown = null;
|
|
133
|
+
render(nothing, pop);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function isOpen() { return !pop.hidden; }
|
|
137
|
+
|
|
138
|
+
// A new summary: an open form keeps the user's pick; only "Now:" follows
|
|
139
|
+
// (the same template again: lit writes that line alone).
|
|
140
|
+
function refresh() {
|
|
141
|
+
if (!isOpen()) return;
|
|
142
|
+
const data = summary();
|
|
143
|
+
if (!data || session() !== openFor) return close();
|
|
144
|
+
draw(data);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
el.addEventListener("click", (e) => {
|
|
148
|
+
const anchor = e.target.closest(anchorSel);
|
|
149
|
+
if (!anchor) return;
|
|
150
|
+
e.stopPropagation();
|
|
151
|
+
if (isOpen()) close();
|
|
152
|
+
else open(anchor);
|
|
153
|
+
});
|
|
154
|
+
pop.addEventListener("submit", (e) => {
|
|
155
|
+
e.preventDefault();
|
|
156
|
+
const level = e.target.elements.level.value;
|
|
157
|
+
const was = initial;
|
|
158
|
+
close();
|
|
159
|
+
if (fresh) return choose(level === DEFAULT ? null : level);
|
|
160
|
+
const line = commandLine(cfg, level, was);
|
|
161
|
+
if (line) run(line);
|
|
162
|
+
});
|
|
163
|
+
pop.addEventListener("click", (e) => {
|
|
164
|
+
if (e.target.closest("[data-act]")?.dataset.act === "close") close();
|
|
165
|
+
});
|
|
166
|
+
doc.addEventListener("click", (e) => {
|
|
167
|
+
if (isOpen() && !pop.contains(e.target) && !e.target.closest?.(anchorSel)) close();
|
|
168
|
+
});
|
|
169
|
+
doc.addEventListener("keydown", (e) => {
|
|
170
|
+
if (e.key === "Escape" && isOpen()) close();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
return { close, refresh, get open() { return isOpen(); } };
|
|
174
|
+
}
|
|
@@ -22,7 +22,7 @@ export const RECENT_KEY = "chi_model_recent";
|
|
|
22
22
|
|
|
23
23
|
// The payload's models as rows: {name, host, id, isDefaultHost}, and
|
|
24
24
|
// sampling (the configured request parameters as /model words them) when
|
|
25
|
-
// the model has some, thinking (the level it starts at,
|
|
25
|
+
// the model has some, thinking (the level it starts at, SettingExplained#summary),
|
|
26
26
|
// and llm_context (the LLM context it starts under,
|
|
27
27
|
// LLMContextStrategy::Explained#summary) when the server sent one, and
|
|
28
28
|
// configured when the host declares the id (hosts.<name>.models), and
|