samagotchi 0.78.0 → 0.80.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 +44 -1
- data/README.md +7 -5
- data/docs/cli.md +24 -7
- data/docs/configuration.md +11 -6
- data/docs/internals/anthropic.md +12 -5
- data/docs/internals/bundles.md +4 -0
- data/docs/internals/engine.md +15 -5
- data/docs/plugins.md +144 -3
- data/docs/releasing.md +2 -2
- data/docs/sessions.md +1 -1
- data/docs/testing.md +32 -2
- data/lib/samagotchi/bootstrap/config_writer.rb +13 -3
- data/lib/samagotchi/bootstrap/probe.rb +153 -31
- data/lib/samagotchi/bootstrap_command.rb +92 -15
- data/lib/samagotchi/bridge.rb +1 -1
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/self_map.md +1 -1
- data/lib/samagotchi/continue_offer.rb +5 -5
- data/lib/samagotchi/engine.rb +40 -37
- data/lib/samagotchi/guardrails/approval.rb +6 -4
- data/lib/samagotchi/guardrails/gate.rb +7 -3
- data/lib/samagotchi/hooks/bundle_loader.rb +31 -16
- data/lib/samagotchi/hooks/registry.rb +44 -3
- data/lib/samagotchi/host_registry.rb +12 -4
- data/lib/samagotchi/llm/adapter_common.rb +8 -4
- data/lib/samagotchi/llm/anthropic_binding_recovery.rb +198 -0
- data/lib/samagotchi/llm/anthropic_messages.rb +39 -178
- data/lib/samagotchi/llm/anthropic_wire.rb +7 -1
- data/lib/samagotchi/llm/chat_loop.rb +21 -12
- data/lib/samagotchi/llm/http.rb +12 -7
- data/lib/samagotchi/llm/model_listing.rb +29 -1
- data/lib/samagotchi/llm/refusal_memo.rb +3 -2
- data/lib/samagotchi/plugin/card.rb +44 -0
- data/lib/samagotchi/plugin/context.rb +53 -8
- data/lib/samagotchi/plugin/http.rb +154 -0
- data/lib/samagotchi/plugin/http_transport.rb +77 -0
- data/lib/samagotchi/plugin/loader.rb +29 -10
- data/lib/samagotchi/question_answer.rb +29 -0
- data/lib/samagotchi/question_desk.rb +30 -22
- data/lib/samagotchi/reasoning_history.rb +2 -1
- data/lib/samagotchi/relay_verifier.rb +1 -1
- data/lib/samagotchi/testing/boundary.rb +156 -0
- data/lib/samagotchi/testing/bundle.rb +349 -0
- data/lib/samagotchi/testing/fake_executable.rb +42 -0
- data/lib/samagotchi/testing/fake_http.rb +137 -0
- data/lib/samagotchi/testing/helpers.rb +56 -0
- data/lib/samagotchi/testing/host.rb +76 -0
- data/lib/samagotchi/testing/minitest.rb +31 -0
- data/lib/samagotchi/testing/recorder.rb +138 -0
- data/lib/samagotchi/testing/recording_api.rb +27 -0
- data/lib/samagotchi/testing/rspec.rb +31 -0
- data/lib/samagotchi/testing/sandbox.rb +163 -0
- data/lib/samagotchi/testing/sessions.rb +77 -0
- data/lib/samagotchi/testing/verdict.rb +28 -0
- data/lib/samagotchi/testing/waiting.rb +36 -0
- data/lib/samagotchi/testing.rb +26 -0
- data/lib/samagotchi/tools/delegate_relay.rb +7 -6
- data/lib/samagotchi/tools/tool_index.rb +3 -0
- data/lib/samagotchi/turn_state.rb +28 -1
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/public/app.js +79 -245
- data/lib/samagotchi/web/public/composer_grip.js +231 -0
- data/lib/samagotchi/web/public/images.js +33 -0
- data/lib/samagotchi/web/public/index.html +73 -40
- data/lib/samagotchi/web/public/level_chip.js +11 -11
- data/lib/samagotchi/web/public/lists_view.js +19 -14
- data/lib/samagotchi/web/public/llm_context_chip.js +15 -15
- data/lib/samagotchi/web/public/model_picker.js +5 -6
- data/lib/samagotchi/web/public/scope.js +3 -3
- data/lib/samagotchi/web/public/stage_view.js +3 -2
- data/lib/samagotchi/web/public/storage.js +23 -0
- data/lib/samagotchi/web/public/strip.js +35 -9
- data/lib/samagotchi/web/public/strip_grip.js +76 -0
- data/lib/samagotchi/web/public/thinking_chip.js +5 -5
- data/lib/samagotchi/web/public/toasts.js +5 -4
- metadata +24 -1
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { GRIP_OPEN_PX, gripFloor, gripFolds } from "./composer_size.js";
|
|
2
|
+
import { COMPOSER_FOLD_KEY, composerFolded, foldHandle, opensOnKey } from "./composer_fold.js";
|
|
3
|
+
import { readStored, removeStored, writeStored } from "./storage.js";
|
|
4
|
+
|
|
5
|
+
// The composer's grip and fold (used by app.js): the grip's drag, tap and keys, the
|
|
6
|
+
// fold's state per visit and its page-wide listeners. app.js keeps the box's
|
|
7
|
+
// height (fitPrompt) and the grip's floor (+floor+ / +setFloor+), which
|
|
8
|
+
// fitPrompt reads; the floor is saved under GRIP_KEY.
|
|
9
|
+
export const GRIP_KEY = "chi_composer_floor";
|
|
10
|
+
|
|
11
|
+
// +session+: the open session's id, or null (the start page). +draft+,
|
|
12
|
+
// +waiting+, +turnRunning+, +allViewOpen+, +lightboxOpen+: what the page
|
|
13
|
+
// shows now. +imageFiles+ / +addImageFiles+: a drop's images and the chips.
|
|
14
|
+
export function createComposerGrip({
|
|
15
|
+
composeGrip, promptEl, infoCancelBtn, infoBarEl,
|
|
16
|
+
session, fitPrompt, promptMinPx, floor, setFloor,
|
|
17
|
+
draft, waiting, turnRunning, allViewOpen, lightboxOpen,
|
|
18
|
+
imageFiles, addImageFiles,
|
|
19
|
+
doc = globalThis.document, win = globalThis.window,
|
|
20
|
+
}) {
|
|
21
|
+
// The composer's fold (composer_fold.js): the grip, which is the handle
|
|
22
|
+
// while folded, and the info bar's Stop turn while folded (the composer's
|
|
23
|
+
// #cancelBtn is folded away then). +gripWillFold+: the drag in progress is
|
|
24
|
+
// past the fold threshold.
|
|
25
|
+
const gripStartTitle = composeGrip.title;
|
|
26
|
+
let gripWillFold = false;
|
|
27
|
+
// This visit's state (a visit is one select() call): opened by the page
|
|
28
|
+
// (a focus, a restored line), archived (decided once, from the visit's
|
|
29
|
+
// first getSession answer).
|
|
30
|
+
const newFoldVisit = () => ({ open: false, archived: false, decided: false });
|
|
31
|
+
let foldVisit = newFoldVisit();
|
|
32
|
+
|
|
33
|
+
// A drag down past the box folds the composer on release (a session only: the
|
|
34
|
+
// start page's grip only resizes); from folded, a pull up opens it and the box
|
|
35
|
+
// follows the pointer in the same gesture, a tap opens it and focuses the box.
|
|
36
|
+
// No click listener: a click still fires after pointerdown's preventDefault
|
|
37
|
+
// and would toggle twice.
|
|
38
|
+
composeGrip.addEventListener("pointerdown", (e) => {
|
|
39
|
+
e.preventDefault();
|
|
40
|
+
composeGrip.setPointerCapture(e.pointerId);
|
|
41
|
+
composeGrip.classList.add("dragging");
|
|
42
|
+
const tapY = e.clientY;
|
|
43
|
+
let startY = tapY;
|
|
44
|
+
let startH = promptEl.offsetHeight;
|
|
45
|
+
let opened = false;
|
|
46
|
+
const move = (ev) => {
|
|
47
|
+
if (session() && isComposerFolded()) {
|
|
48
|
+
if (startY - ev.clientY < GRIP_OPEN_PX) return;
|
|
49
|
+
setComposerFolded(false);
|
|
50
|
+
opened = true;
|
|
51
|
+
// Re-based after the unfold (its fitPrompt sized the box: the minimum
|
|
52
|
+
// when empty, the text's height with a draft).
|
|
53
|
+
startH = promptEl.offsetHeight;
|
|
54
|
+
startY = ev.clientY;
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const requested = startH + (startY - ev.clientY);
|
|
58
|
+
setFloor(gripFloor(requested, win.innerHeight, promptMinPx()));
|
|
59
|
+
fitPrompt();
|
|
60
|
+
const willFold = !!session() && gripFolds({ requestedPx: requested, shownPx: promptEl.offsetHeight });
|
|
61
|
+
if (willFold !== gripWillFold) {
|
|
62
|
+
gripWillFold = willFold;
|
|
63
|
+
composeGrip.classList.toggle("will-fold", willFold);
|
|
64
|
+
drawFoldControls();
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
const end = () => {
|
|
68
|
+
composeGrip.classList.remove("dragging");
|
|
69
|
+
composeGrip.removeEventListener("pointermove", move);
|
|
70
|
+
composeGrip.removeEventListener("pointerup", up);
|
|
71
|
+
composeGrip.removeEventListener("pointercancel", cancel);
|
|
72
|
+
const folds = gripWillFold;
|
|
73
|
+
gripWillFold = false;
|
|
74
|
+
composeGrip.classList.remove("will-fold");
|
|
75
|
+
return folds;
|
|
76
|
+
};
|
|
77
|
+
const up = (ev) => {
|
|
78
|
+
const folds = end();
|
|
79
|
+
if (folds) {
|
|
80
|
+
// The next opening starts at the minimum (or the text's height).
|
|
81
|
+
setFloor(null);
|
|
82
|
+
removeStored(GRIP_KEY);
|
|
83
|
+
setComposerFolded(true);
|
|
84
|
+
} else if (session() && isComposerFolded()) {
|
|
85
|
+
if (Math.abs(ev.clientY - tapY) < GRIP_OPEN_PX) {
|
|
86
|
+
setComposerFolded(false);
|
|
87
|
+
promptEl.focus();
|
|
88
|
+
}
|
|
89
|
+
} else {
|
|
90
|
+
writeStored(GRIP_KEY, String(floor() || ""));
|
|
91
|
+
// A pull that opened the box leaves the focus in it, as a tap does.
|
|
92
|
+
if (opened) promptEl.focus();
|
|
93
|
+
}
|
|
94
|
+
drawFoldControls();
|
|
95
|
+
};
|
|
96
|
+
const cancel = () => {
|
|
97
|
+
end();
|
|
98
|
+
drawFoldControls();
|
|
99
|
+
};
|
|
100
|
+
composeGrip.addEventListener("pointermove", move);
|
|
101
|
+
composeGrip.addEventListener("pointerup", up);
|
|
102
|
+
composeGrip.addEventListener("pointercancel", cancel);
|
|
103
|
+
});
|
|
104
|
+
composeGrip.addEventListener("dblclick", () => {
|
|
105
|
+
if (session() && isComposerFolded()) return;
|
|
106
|
+
setFloor(null);
|
|
107
|
+
removeStored(GRIP_KEY);
|
|
108
|
+
fitPrompt();
|
|
109
|
+
});
|
|
110
|
+
// Enter or Space on the focused handle folds, or opens and focuses the box
|
|
111
|
+
// (a session only; preventDefault: Space would scroll the page).
|
|
112
|
+
composeGrip.addEventListener("keydown", (e) => {
|
|
113
|
+
if (!session() || (e.key !== "Enter" && e.key !== " ")) return;
|
|
114
|
+
e.preventDefault();
|
|
115
|
+
if (isComposerFolded()) {
|
|
116
|
+
setComposerFolded(false);
|
|
117
|
+
promptEl.focus();
|
|
118
|
+
} else {
|
|
119
|
+
setComposerFolded(true);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// ── Composer fold (composer_fold.js) ───────────────────────────────────────
|
|
124
|
+
// Dragging the composer's grip down folds the session's composer away; the
|
|
125
|
+
// grip, now the handle on the info bar's top edge, pulled up or clicked, or
|
|
126
|
+
// typing on the page, brings it back. The user's own choice is saved per
|
|
127
|
+
// browser; an archived session opens folded for its visit, and expanding
|
|
128
|
+
// there saves nothing. The page's own focus or a restored line opens it
|
|
129
|
+
// for the visit only. The start page never folds.
|
|
130
|
+
function composerFoldSaved() {
|
|
131
|
+
return readStored(COMPOSER_FOLD_KEY) === "1";
|
|
132
|
+
}
|
|
133
|
+
function isComposerFolded() {
|
|
134
|
+
return !!session() && composerFolded({ saved: composerFoldSaved(), archivedVisit: foldVisit.archived, visitOpen: foldVisit.open });
|
|
135
|
+
}
|
|
136
|
+
function applyComposerFolded() {
|
|
137
|
+
const folded = isComposerFolded();
|
|
138
|
+
const was = doc.body.classList.contains("composer-folded");
|
|
139
|
+
doc.body.classList.toggle("composer-folded", folded);
|
|
140
|
+
// Its height was measured while hidden.
|
|
141
|
+
if (was && !folded) fitPrompt();
|
|
142
|
+
drawFoldControls();
|
|
143
|
+
}
|
|
144
|
+
// The user's own fold or unfold (the grip, typing): saved, unless this
|
|
145
|
+
// is an archived visit (both are for the visit then).
|
|
146
|
+
function setComposerFolded(folded) {
|
|
147
|
+
if (foldVisit.archived) {
|
|
148
|
+
foldVisit.open = !folded;
|
|
149
|
+
} else {
|
|
150
|
+
writeStored(COMPOSER_FOLD_KEY, folded ? "1" : "0");
|
|
151
|
+
foldVisit.open = false;
|
|
152
|
+
}
|
|
153
|
+
applyComposerFolded();
|
|
154
|
+
}
|
|
155
|
+
// The page opens it for this visit: nothing saved.
|
|
156
|
+
function openForVisit() {
|
|
157
|
+
if (!isComposerFolded()) return;
|
|
158
|
+
foldVisit.open = true;
|
|
159
|
+
applyComposerFolded();
|
|
160
|
+
}
|
|
161
|
+
// Every focus of the message box: in a session it opens a folded composer
|
|
162
|
+
// for the visit first (+open+ false: only when it shows already).
|
|
163
|
+
function focusPrompt({ open = true } = {}) {
|
|
164
|
+
if (session() && isComposerFolded()) {
|
|
165
|
+
if (!open) return;
|
|
166
|
+
openForVisit();
|
|
167
|
+
}
|
|
168
|
+
promptEl.focus();
|
|
169
|
+
}
|
|
170
|
+
// The grip as the fold's handle (its title, marks, aria-expanded) and Stop turn
|
|
171
|
+
// while folded. The start page's grip only resizes: no role, no fold state.
|
|
172
|
+
function drawFoldControls() {
|
|
173
|
+
const folded = isComposerFolded();
|
|
174
|
+
infoCancelBtn.style.display = folded && turnRunning() ? "" : "none";
|
|
175
|
+
if (!session()) {
|
|
176
|
+
composeGrip.removeAttribute("role");
|
|
177
|
+
composeGrip.removeAttribute("aria-expanded");
|
|
178
|
+
composeGrip.title = gripStartTitle;
|
|
179
|
+
for (const c of ["folded", "draft", "waiting"]) composeGrip.classList.remove(c);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const h = foldHandle({ folded, draft: draft(), waiting: folded && waiting() });
|
|
183
|
+
composeGrip.title = gripWillFold ? "Release to hide the message box" : h.title;
|
|
184
|
+
composeGrip.setAttribute("role", "button");
|
|
185
|
+
composeGrip.setAttribute("aria-expanded", folded ? "false" : "true");
|
|
186
|
+
composeGrip.classList.toggle("folded", h.folded);
|
|
187
|
+
composeGrip.classList.toggle("draft", h.draft);
|
|
188
|
+
composeGrip.classList.toggle("waiting", h.waiting);
|
|
189
|
+
}
|
|
190
|
+
promptEl.addEventListener("input", () => drawFoldControls());
|
|
191
|
+
// Typing on the page opens a folded composer: the key lands in the box
|
|
192
|
+
// (the class goes before the focus, so the browser types it there).
|
|
193
|
+
doc.addEventListener("keydown", (e) => {
|
|
194
|
+
if (!isComposerFolded()) return;
|
|
195
|
+
if (!opensOnKey(e, { active: doc.activeElement, allView: allViewOpen(), lightbox: lightboxOpen() })) return;
|
|
196
|
+
setComposerFolded(false);
|
|
197
|
+
promptEl.focus();
|
|
198
|
+
});
|
|
199
|
+
// A file dropped on the folded info bar: open (for the visit), then add it.
|
|
200
|
+
infoBarEl.addEventListener("dragover", (e) => {
|
|
201
|
+
if (!isComposerFolded() || ![...(e.dataTransfer?.types || [])].includes("Files")) return;
|
|
202
|
+
e.preventDefault();
|
|
203
|
+
});
|
|
204
|
+
infoBarEl.addEventListener("drop", (e) => {
|
|
205
|
+
if (!isComposerFolded()) return;
|
|
206
|
+
const files = imageFiles(e.dataTransfer);
|
|
207
|
+
if (!files.length) return;
|
|
208
|
+
e.preventDefault();
|
|
209
|
+
openForVisit();
|
|
210
|
+
addImageFiles(files);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
isComposerFolded,
|
|
215
|
+
openForVisit,
|
|
216
|
+
focusPrompt,
|
|
217
|
+
drawFoldControls,
|
|
218
|
+
// A new visit (select(), the start page): nothing opened, not decided.
|
|
219
|
+
newVisit() {
|
|
220
|
+
foldVisit = newFoldVisit();
|
|
221
|
+
applyComposerFolded();
|
|
222
|
+
},
|
|
223
|
+
// The visit's first getSession answer: an archived session opens folded.
|
|
224
|
+
decideVisit(archived) {
|
|
225
|
+
if (foldVisit.decided) return;
|
|
226
|
+
foldVisit.decided = true;
|
|
227
|
+
foldVisit.archived = !!archived;
|
|
228
|
+
applyComposerFolded();
|
|
229
|
+
},
|
|
230
|
+
};
|
|
231
|
+
}
|
|
@@ -15,6 +15,39 @@ export function imageFiles(dataTransfer) {
|
|
|
15
15
|
return files.filter((f) => /^image\//.test(f?.type || ""));
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
// The server's caps, mirrored so the composer refuses at once what a send
|
|
19
|
+
// would (spec/web/images_spec.rb pins both): ImageStore::MAX_TURN_REFS
|
|
20
|
+
// images a message, Web::App::MAX_IMAGE_UPLOAD_BYTES a file.
|
|
21
|
+
export const MAX_TURN_IMAGES = 20;
|
|
22
|
+
export const MAX_IMAGE_MB = 20;
|
|
23
|
+
const MAX_IMAGE_BYTES = MAX_IMAGE_MB * 1024 * 1024;
|
|
24
|
+
|
|
25
|
+
function filesText(n) {
|
|
26
|
+
return n === 1 ? "1 file" : `${n} files`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// What the composer takes of +files+ (a paste's, a drop's or the image
|
|
30
|
+
// button's picks) while it holds +held+ chips: images only, none over
|
|
31
|
+
// MAX_IMAGE_MB, and no more than MAX_TURN_IMAGES in all. +refused+ says
|
|
32
|
+
// why the rest stayed out, for a toast ("" when none did).
|
|
33
|
+
export function admitImages(files, held = 0) {
|
|
34
|
+
const take = [];
|
|
35
|
+
let notImage = 0;
|
|
36
|
+
let tooBig = 0;
|
|
37
|
+
let overCap = 0;
|
|
38
|
+
for (const file of files || []) {
|
|
39
|
+
if (!/^image\//.test(file?.type || "")) notImage += 1;
|
|
40
|
+
else if ((file.size || 0) > MAX_IMAGE_BYTES) tooBig += 1;
|
|
41
|
+
else if (held + take.length >= MAX_TURN_IMAGES) overCap += 1;
|
|
42
|
+
else take.push(file);
|
|
43
|
+
}
|
|
44
|
+
const why = [];
|
|
45
|
+
if (overCap) why.push(`${filesText(overCap)} not added: at most ${MAX_TURN_IMAGES} images a message`);
|
|
46
|
+
if (tooBig) why.push(`${filesText(tooBig)} not added: over ${MAX_IMAGE_MB} MB`);
|
|
47
|
+
if (notImage) why.push(`${filesText(notImage)} not added: not an image`);
|
|
48
|
+
return { take, refused: why.join(" · ") };
|
|
49
|
+
}
|
|
50
|
+
|
|
18
51
|
// A pasted screenshot is often called "image.png": number it so the chips
|
|
19
52
|
// (and what the model reads) tell them apart.
|
|
20
53
|
export function chipName(file, index) {
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
--diff-add-bg:rgba(26,127,55,.12);--diff-add-text:#116329;--diff-del-bg:rgba(207,34,46,.10);--diff-del-text:#82071e;
|
|
18
18
|
--code-text:#1f2328;--code-keyword:#cf222e;--code-string:#0a3069;--code-comment:#57606a;--code-constant:#0550ae;--code-func:#8250df;--code-type:#953800;
|
|
19
19
|
--card-hover-border:rgba(31,35,40,.18);--on-solid:#fff;--on-warn:#1f2328;--text-hover:#0d1117;--badge-bg:rgba(31,35,40,.06);--badge-ring:rgba(31,35,40,.15);--shadow-strong:rgba(31,35,40,.18);--backdrop:rgba(13,17,23,.85);--toast-shadow:0 6px 24px rgba(31,35,40,.14)}}
|
|
20
|
-
/* Icon masks shared by the phone info bar
|
|
20
|
+
/* Icon masks shared by the phone info bar, the all view's cards and the composer's image button (the stroke only shapes the mask; the icon takes currentColor). */
|
|
21
|
+
:root{--icon-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='2' y='2.75' width='12' height='10.5' rx='1.5'/%3E%3Ccircle cx='5.75' cy='6.25' r='1.1'/%3E%3Cpath d='m2.5 12.5 3.75-3.75 2.5 2.5 1.75-1.75 3 3'/%3E%3C/svg%3E")}
|
|
21
22
|
:root{--icon-archive:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='2' y='2.75' width='12' height='3.5' rx='1'/%3E%3Cpath d='M3.25 6.25v6a1 1 0 0 0 1 1h7.5a1 1 0 0 0 1-1v-6M6.5 9h3'/%3E%3C/svg%3E");--icon-unarchive:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='2' y='2.75' width='12' height='3.5' rx='1'/%3E%3Cpath d='M3.25 6.25v6a1 1 0 0 0 1 1h7.5a1 1 0 0 0 1-1v-6M8 11.5V8.25M6.5 9.75 8 8.25l1.5 1.5'/%3E%3C/svg%3E")}
|
|
22
23
|
*{box-sizing:border-box;margin:0;padding:0}
|
|
23
24
|
body{font-family: ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;background:radial-gradient(1100px 700px at 8% 0%,color-mix(in srgb,var(--accent) 13%,transparent),transparent 70%),radial-gradient(900px 700px at 100% 100%,color-mix(in srgb,var(--think) 9%,transparent),transparent 70%),var(--bg);background-attachment:fixed;color:var(--text);height:100vh;display:flex;flex-direction:column}
|
|
@@ -590,18 +591,26 @@ body.has-session #composer{flex-wrap:wrap;row-gap:6px}
|
|
|
590
591
|
#lightbox[hidden]{display:none}
|
|
591
592
|
#lightbox img{max-width:96vw;max-height:94vh;border-radius:6px}
|
|
592
593
|
#composer textarea{flex:1 1 auto;min-height:72px;resize:none;background:transparent;border:0;color:var(--text);padding:2px;border-radius:0;font-size:15px;line-height:1.45}
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
594
|
+
/* An edge grip: the thin handle on an edge between two panes (the composer's,
|
|
595
|
+
the latest-sessions strip's). The pill is the handle's look, a 36×4 px bar
|
|
596
|
+
centred on the edge, drawn on hover, while dragging or focused; the id
|
|
597
|
+
rules hold each grip's own box: height and margins (they depend on the flex
|
|
598
|
+
gap around it: none for the composer's, main's 16 px for the strip's) and
|
|
599
|
+
its own states. */
|
|
600
|
+
.edge-grip{position:relative;z-index:2;cursor:ns-resize;touch-action:none}
|
|
601
|
+
.edge-grip::after{content:"";position:absolute;left:50%;top:3px;width:36px;height:4px;margin-left:-18px;border-radius:2px;background:var(--border-strong);opacity:0;transition:opacity .12s ease}
|
|
602
|
+
.edge-grip:hover::after,.edge-grip.dragging::after{opacity:1}
|
|
603
|
+
.edge-grip:hover::after,.edge-grip.dragging::after{background:var(--accent)}
|
|
604
|
+
.edge-grip:focus-visible{outline:none}
|
|
605
|
+
.edge-grip:focus-visible::after{opacity:1;box-shadow:0 0 0 2px var(--accent-ring)}
|
|
606
|
+
#composeGrip{height:10px;margin:-5px 0}
|
|
607
|
+
#composeGrip:has(+ #composer:hover)::after{opacity:1}
|
|
608
|
+
/* Folded (composer_grip.js, composer_fold.js): the grip is the handle on the info bar's top edge, drawn always, wider so it reads as a control;
|
|
598
609
|
draft = the accent, waiting = the attention colour; .will-fold = a drag past the fold threshold (released it hides the box). */
|
|
599
610
|
body.has-session.composer-folded #composeGrip::after{opacity:1;width:48px;margin-left:-24px}
|
|
600
611
|
#composeGrip.draft::after{background:var(--accent)}
|
|
601
612
|
#composeGrip.waiting::after{background:var(--warn)}
|
|
602
613
|
#composeGrip.will-fold::after{opacity:1;background:var(--accent)}
|
|
603
|
-
#composeGrip:focus-visible{outline:none}
|
|
604
|
-
#composeGrip:focus-visible::after{opacity:1;box-shadow:0 0 0 2px var(--accent-ring)}
|
|
605
614
|
/* A phone has no hover: the pill is always there and the hit zone is taller, ~96 px wide and centred (full width would cover the
|
|
606
615
|
info bar's buttons while folded, the stage card's bottom in the stage view); it reaches 7 px below the edge, the info bar's
|
|
607
616
|
border and padding, so it never covers a button of the bar. */
|
|
@@ -619,6 +628,12 @@ body.has-session.composer-folded #composeGrip::after{opacity:1;width:48px;margin
|
|
|
619
628
|
#composer .actions-col button{white-space:nowrap;font-size:12px;padding:0 12px;height:28px;min-height:28px;line-height:28px}
|
|
620
629
|
#composer .actions-col .split{order:3;display:flex;gap:4px;margin-left:auto}
|
|
621
630
|
#composer #actionBtn{min-width:64px;font-weight:600}
|
|
631
|
+
/* The image button (app.js: a click opens #attachInput, the hidden file picker; a phone's offers the camera and the photo library):
|
|
632
|
+
an icon just left of Start / Send, quiet like the model picker; hidden in a session a terminal owns, and on a phone while a turn runs. */
|
|
633
|
+
#composer .actions-col .attach-btn{order:2;flex:none;width:28px;padding:0;border:1px solid transparent;border-radius:8px;background:transparent;color:var(--text-muted)}
|
|
634
|
+
#composer .actions-col .attach-btn::before{content:"";display:block;width:16px;height:16px;margin:auto;background:currentColor;-webkit-mask:var(--icon-image) center/contain no-repeat;mask:var(--icon-image) center/contain no-repeat}
|
|
635
|
+
#composer .actions-col .attach-btn:hover{filter:none;color:var(--text);background:color-mix(in srgb,var(--bg-hover) 75%,transparent)}
|
|
636
|
+
#composer .actions-col .attach-btn[hidden]{display:none}
|
|
622
637
|
/* In a session the composer is one row: the text field (one line, growing with its text; fitPrompt sets its height)
|
|
623
638
|
and Cancel · Send at its bottom right. The key hint is in the placeholder there, so its row goes. */
|
|
624
639
|
body.has-session #composer{flex-direction:row;align-items:flex-end;padding:10px}
|
|
@@ -636,12 +651,12 @@ body.has-session #composer .actions-col{flex-wrap:nowrap;flex-shrink:0}
|
|
|
636
651
|
/* A fresh link's (body.fresh-link): the session composer is one row and .actions-col is content-sized there, so a px cap keeps the text field's room and Send in view. :not([hidden]) keeps a failed /api/models' hide (this selector outranks the [hidden] one). */
|
|
637
652
|
body.has-session.fresh-link #composer .actions-col .model-pick:not([hidden]){display:inline-block;flex:0 1 auto;min-width:0;max-width:200px}
|
|
638
653
|
@media(max-width:600px){body.has-session.fresh-link #composer .actions-col .model-pick:not([hidden]){max-width:100px}}
|
|
639
|
-
/* The new
|
|
654
|
+
/* The new session's LLM context chip, after the picker: as quiet as it; accent once a choice is set (the info bar chip's .own). */
|
|
640
655
|
#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}
|
|
641
656
|
#composer .actions-col .llmctx-new:hover{filter:none;color:var(--text);border-color:var(--accent-border);background:var(--accent-bg)}
|
|
642
657
|
#composer .actions-col .llmctx-new.own{color:var(--accent);border-color:var(--accent-border)}
|
|
643
658
|
#composer .actions-col .llmctx-new[hidden],body.has-session #composer .actions-col .llmctx-new{display:none}
|
|
644
|
-
/* The new
|
|
659
|
+
/* The new session's thinking chip, after it: shorter text, so a smaller share of the row. */
|
|
645
660
|
#composer .actions-col .think-new{max-width:min(25%,140px)}
|
|
646
661
|
/* Phones (the key hint is gone there): the chips keep their whole text and the row wraps, Start staying right (its margin-left:auto) on the last line; only a model name wider than the row ellipsizes. */
|
|
647
662
|
@media(max-width:600px){#composer .actions-col .model-pick{flex:0 1 auto;min-width:0;max-width:100%}#composer .actions-col .llmctx-new,#composer .actions-col .think-new{flex:0 0 auto;max-width:100%}}
|
|
@@ -866,7 +881,7 @@ body.stage-view .turn-stage{position:absolute;left:0;right:0;bottom:100%;margin-
|
|
|
866
881
|
#infoBar #infoArchiveBtn:hover{color:var(--text);background:var(--bg-hover)}
|
|
867
882
|
#infoBar #infoDeleteBtn{background:transparent;color:var(--text-muted);border:1px solid transparent}
|
|
868
883
|
#infoBar #infoDeleteBtn:hover{color:var(--on-solid);background:var(--danger-bg);border-color:var(--danger-bg)}
|
|
869
|
-
/* The composer folded away (
|
|
884
|
+
/* The composer folded away (composer_grip.js: the grip dragged down, Enter/Space on it): the info bar is the page's bottom edge.
|
|
870
885
|
Stop turn comes into the bar while folded and a turn runs. */
|
|
871
886
|
body.has-session.composer-folded #composer,body.has-session.composer-folded #initRow{display:none}
|
|
872
887
|
body.has-session.composer-folded #infoBar{border-top-color:transparent}
|
|
@@ -975,8 +990,7 @@ body.has-session #notice{display:none}
|
|
|
975
990
|
#newBtn .brand-new{font-size:12px;color:var(--text-muted);display:none}
|
|
976
991
|
body.has-session #newBtn .brand-new{display:inline}
|
|
977
992
|
#newBtn:hover .brand-new{color:var(--text-2)}
|
|
978
|
-
.
|
|
979
|
-
/* The scope chip: "<project> · all", or "all projects" and where a new chat starts (start page only). */
|
|
993
|
+
/* The scope chip: "<project> · all", or "all projects" and where a new session starts (start page only). */
|
|
980
994
|
#scope{display:inline-flex;align-items:center;gap:8px;min-width:0;font-size:12px;color:var(--text-muted)}
|
|
981
995
|
#scope[hidden]{display:none}
|
|
982
996
|
#scope .scope-name{flex-shrink:0;padding:2px 9px;border:1px solid var(--border-strong);border-radius:999px;color:var(--text-2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:40vw}
|
|
@@ -989,7 +1003,7 @@ body.has-session #newBtn .brand-new{display:inline}
|
|
|
989
1003
|
body.has-session #scope .scope-where{display:none}
|
|
990
1004
|
/* A phone has no room for both; the hidden strip's pill (count, live) wins. */
|
|
991
1005
|
@media(max-width:600px){body.strip-hidden #scope .scope-where{display:none}}
|
|
992
|
-
/* A phone's top bar stays one row: "chi +" (the rest of "+ new
|
|
1006
|
+
/* A phone's top bar stays one row: "chi +" (the rest of "+ new session" goes),
|
|
993
1007
|
tighter gaps, nothing wraps, and the "← project" link cuts with an
|
|
994
1008
|
ellipsis before the pill does. */
|
|
995
1009
|
@media(max-width:600px){
|
|
@@ -998,11 +1012,9 @@ body.has-session #scope .scope-where{display:none}
|
|
|
998
1012
|
#newBtn .brand-new-word{display:none}
|
|
999
1013
|
#newBtn .brand-new,#scope .scope-back{white-space:nowrap}
|
|
1000
1014
|
#scope .scope-back{min-width:0;overflow:hidden;text-overflow:ellipsis}
|
|
1001
|
-
#
|
|
1015
|
+
#stripStatus{padding:3px 8px}
|
|
1002
1016
|
#newBtn .brand-new{font-size:16px}
|
|
1003
1017
|
}
|
|
1004
|
-
/* The ▾ glyph draws tiny at the pill's 11 px: at 20 px it is about as tall
|
|
1005
|
-
as the pill text's capitals (phone and desktop alike). */
|
|
1006
1018
|
/* The bell (notifications): quiet at the top bar's right end; filled in the accent when on, struck through when the browser blocks it. */
|
|
1007
1019
|
/* The open session's title, when no strip card in sight shows it (session_title.js): the bar's middle, cut with an ellipsis; the whole first message in its tooltip. */
|
|
1008
1020
|
#sessionTitle{flex:1;min-width:0;font-size:13px;color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
@@ -1014,10 +1026,10 @@ body.has-session #scope .scope-where{display:none}
|
|
|
1014
1026
|
@media(max-width:600px){
|
|
1015
1027
|
#topBar{flex-wrap:wrap;row-gap:2px}
|
|
1016
1028
|
#sessionTitle{flex-basis:100%;order:10;font-size:12px}
|
|
1017
|
-
#scope,#
|
|
1018
|
-
#
|
|
1029
|
+
#scope,#stripStatus{flex:1 1 0;min-width:0;max-width:max-content}
|
|
1030
|
+
#stripStatus{flex-grow:1000;overflow:hidden;text-overflow:ellipsis}
|
|
1019
1031
|
#scope .scope-name{flex-shrink:1;min-width:0}
|
|
1020
|
-
#
|
|
1032
|
+
#stripStatus:has(.live,.waiting) .pill-count{display:none}
|
|
1021
1033
|
}
|
|
1022
1034
|
#notifyBtn{margin-left:auto;flex-shrink:0;display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;padding:0;min-height:0;background:transparent;border:1px solid transparent;border-radius:999px;color:var(--text-muted)}
|
|
1023
1035
|
#notifyBtn[hidden]{display:none}
|
|
@@ -1028,7 +1040,6 @@ body.has-session #scope .scope-where{display:none}
|
|
|
1028
1040
|
#notifyBtn[data-state="on"] .bell{fill:currentColor;fill-opacity:.22}
|
|
1029
1041
|
#notifyBtn[data-state="denied"]{color:var(--text-faint)}
|
|
1030
1042
|
#notifyBtn[data-state="denied"] .slash{display:inline}
|
|
1031
|
-
#stripShow .pill-arrow{font-size:20px;line-height:0;vertical-align:-3px}
|
|
1032
1043
|
.card .folder{font-family:ui-monospace,monospace;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0;color:var(--text-3)}
|
|
1033
1044
|
.card .parent{font-family:ui-monospace,monospace;white-space:nowrap;color:var(--text-3)}
|
|
1034
1045
|
#infoBar .meta a.parent{text-decoration:none}
|
|
@@ -1098,22 +1109,42 @@ body.no-glass :is(#familyPop,#chainPop){background:var(--bg-panel);-webkit-backd
|
|
|
1098
1109
|
.scope-error{grid-column:1/-1;font-size:13px;color:var(--warn-text)}
|
|
1099
1110
|
.strip-toggle{font-size:11px;padding:3px 10px;background:transparent;color:var(--text-muted);border:1px solid var(--border);border-radius:999px}
|
|
1100
1111
|
.strip-toggle:hover{color:var(--text-2);border-color:var(--border-strong)}
|
|
1101
|
-
#
|
|
1102
|
-
#
|
|
1103
|
-
#
|
|
1104
|
-
#
|
|
1105
|
-
#
|
|
1106
|
-
|
|
1107
|
-
#stripHide:hover{text-decoration:underline;color:var(--text-2)}
|
|
1108
|
-
/* A narrow window's strip scrolls sideways, which puts its own "hide ▴" out of sight: the top bar has it instead, where the pill comes back, with a finger-sized target. */
|
|
1109
|
-
#stripHideTop{display:none;white-space:nowrap;flex-shrink:0}
|
|
1112
|
+
#stripStatus{display:none;white-space:nowrap;flex-shrink:0}
|
|
1113
|
+
#stripStatus .live{color:var(--ok-text);white-space:nowrap}
|
|
1114
|
+
#stripStatus .live-dot{display:inline-block;width:7px;height:7px;border-radius:50%;background:var(--ok);margin-right:4px;vertical-align:1px}
|
|
1115
|
+
#stripStatus .waiting{color:var(--warn-text);white-space:nowrap}
|
|
1116
|
+
#stripStatus .waiting-dot{display:inline-block;width:7px;height:7px;border-radius:50%;background:var(--warn);margin-right:4px;vertical-align:1px}
|
|
1117
|
+
/* A narrow window's strip scrolls sideways, and the grip's own target there is thin: the status pill gets a finger-sized one. */
|
|
1110
1118
|
@media(max-width:900px){
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1119
|
+
#stripStatus{min-height:30px;padding:4px 12px;font-size:12px}
|
|
1120
|
+
}
|
|
1121
|
+
body.strip-hidden #stripStatus{display:block}
|
|
1122
|
+
body.strip-hidden #topStrip, body.all-view #stripStatus{display:none}
|
|
1123
|
+
/* The strip's grip (strip_grip.js, app.js): the handle on the edge between the
|
|
1124
|
+
strip and the chat, the composer's twin (the shared .edge-grip look, its own
|
|
1125
|
+
10 px box). Its box is centred on #center's top edge: main's 16 px gap stands
|
|
1126
|
+
on both sides because the box's height and margins add up to -26 px, so the
|
|
1127
|
+
distances stay what they were without it (strip → chat 16 px; hidden, bar →
|
|
1128
|
+
chat 10 px). While the strip shows, the pill is drawn on a hover of the strip
|
|
1129
|
+
or of the grip itself, as the composer's is of its box; hidden, it is the
|
|
1130
|
+
always-on handle that brings the strip back (48 px, as the folded composer's,
|
|
1131
|
+
since it is a control then and not only a hint). */
|
|
1132
|
+
#stripGrip{height:10px;margin:-5px 0 -21px}
|
|
1133
|
+
#topStrip:hover + #stripGrip::after{opacity:1}
|
|
1134
|
+
#stripGrip.will-toggle::after{opacity:1;background:var(--accent)}
|
|
1135
|
+
body.strip-hidden #stripGrip::after{opacity:1;width:48px;margin-left:-24px}
|
|
1136
|
+
body.all-view #stripGrip{display:none}
|
|
1137
|
+
/* A phone has no hover: the pill is always drawn, and the hit zone is 28 px
|
|
1138
|
+
tall, ~96 px wide and centred on the edge (main's gap plus the box: the
|
|
1139
|
+
margins add up to -(28+16)) — the mirror of the composer's phone grip: it
|
|
1140
|
+
reaches 7 px over its own side (below the cards; it never covers one) and
|
|
1141
|
+
21 px into the transcript, with the pill on #center's top edge. The grip sits
|
|
1142
|
+
under the strip, outside its sideways scroller, so a swipe on the cards stays
|
|
1143
|
+
a scroll (touch-action:none is the grip's box, not the strip's). */
|
|
1144
|
+
@media(max-width:600px){
|
|
1145
|
+
#stripGrip{width:96px;height:28px;margin:-7px auto -37px}
|
|
1146
|
+
#stripGrip::after{top:5px;opacity:1}
|
|
1114
1147
|
}
|
|
1115
|
-
body.strip-hidden #stripShow{display:block}
|
|
1116
|
-
body.strip-hidden #topStrip, body.all-view #stripShow{display:none}
|
|
1117
1148
|
#allTile{flex:1;flex-direction:column;justify-content:center;align-items:flex-start;text-align:left;color:var(--text-2);font-size:13px;font-weight:600;width:100%}
|
|
1118
1149
|
#allTile .count{font-size:11px;font-weight:400;color:var(--text-muted)}
|
|
1119
1150
|
#allView{display:none;flex:1;min-height:0;flex-direction:column;gap:12px}
|
|
@@ -1213,14 +1244,14 @@ a{color:var(--accent)}
|
|
|
1213
1244
|
<body>
|
|
1214
1245
|
<main>
|
|
1215
1246
|
<div id="topBar">
|
|
1216
|
-
<button type="button" id="newBtn" class="brand" title="New
|
|
1247
|
+
<button type="button" id="newBtn" class="brand" title="New session"><span class="brand-name">chi</span><span class="brand-new">+<span class="brand-new-word"> new session</span></span></button>
|
|
1217
1248
|
<div id="scope" hidden></div>
|
|
1218
|
-
<button type="button" class="strip-toggle" id="
|
|
1219
|
-
<button type="button" class="strip-toggle" id="stripHideTop" title="Hide the latest sessions"><span class="pill-arrow" aria-hidden="true">▴</span> hide</button>
|
|
1249
|
+
<button type="button" class="strip-toggle" id="stripStatus" title="All sessions, with search"><span class="pill-count">sessions</span></button>
|
|
1220
1250
|
<span id="sessionTitle" hidden></span>
|
|
1221
1251
|
<button type="button" id="notifyBtn" aria-label="Notifications" aria-pressed="false" hidden><svg viewBox="0 0 16 16" width="15" height="15" aria-hidden="true"><path class="bell" d="M8 1.75a4 4 0 0 0-4 4v2.6L2.75 10.6v.9h10.5v-.9L12 8.35v-2.6a4 4 0 0 0-4-4Z"/><path class="clapper" d="M6.5 13a1.5 1.5 0 0 0 3 0"/><path class="slash" d="M2.5 2.5l11 11"/></svg></button>
|
|
1222
1252
|
</div>
|
|
1223
1253
|
<section id="topStrip" aria-label="Latest sessions" class="loading"><div class="card skeleton"><div class="line long"></div><div class="meta"><span class="line tiny" style="display:inline-block"></span></div></div><div class="card skeleton"><div class="line long"></div><div class="meta"><span class="line tiny" style="display:inline-block"></span></div></div><div class="card skeleton"><div class="line long"></div><div class="meta"><span class="line tiny" style="display:inline-block"></span></div></div></section>
|
|
1254
|
+
<div id="stripGrip" class="edge-grip" tabindex="0" role="button" aria-controls="topStrip" aria-label="Latest sessions" title="Drag up or click to hide the latest sessions"></div>
|
|
1224
1255
|
<section id="center">
|
|
1225
1256
|
<div id="stage">
|
|
1226
1257
|
<div id="markdownWarning" class="markdown-warning" hidden></div>
|
|
@@ -1231,7 +1262,7 @@ a{color:var(--accent)}
|
|
|
1231
1262
|
<div id="lightbox" hidden><img alt=""></div>
|
|
1232
1263
|
<div id="dock">
|
|
1233
1264
|
<div id="initRow" role="status" aria-live="polite" hidden></div>
|
|
1234
|
-
<div id="composeGrip" tabindex="0" aria-label="Message box" title="Drag to make the message box taller (double-click resets)"></div>
|
|
1265
|
+
<div id="composeGrip" class="edge-grip" tabindex="0" aria-label="Message box" title="Drag to make the message box taller (double-click resets)"></div>
|
|
1235
1266
|
<div id="composer" style="position:relative">
|
|
1236
1267
|
<div id="commandList" role="listbox" aria-label="Commands" hidden></div>
|
|
1237
1268
|
<div id="modelPanel" hidden>
|
|
@@ -1244,11 +1275,13 @@ a{color:var(--accent)}
|
|
|
1244
1275
|
<textarea id="prompt" placeholder="Ask chi anything…" aria-controls="commandList" aria-autocomplete="list" aria-expanded="false"></textarea>
|
|
1245
1276
|
</div>
|
|
1246
1277
|
<div class="actions-col">
|
|
1247
|
-
<button type="button" id="modelPick" class="model-pick" title="The model the new
|
|
1278
|
+
<button type="button" id="modelPick" class="model-pick" title="The model the new session starts on" aria-label="Model for the new session" aria-haspopup="listbox" aria-expanded="false" hidden><span class="model-pick-name"></span></button>
|
|
1248
1279
|
<button type="button" id="thinkingNew" class="llmctx-new think-new" aria-haspopup="dialog" hidden></button>
|
|
1249
1280
|
<button type="button" id="llmContextNew" class="llmctx-new" aria-haspopup="dialog" hidden></button>
|
|
1250
1281
|
<div id="composeHint">Enter to start · Shift+Enter for a newline</div>
|
|
1251
1282
|
<button id="cancelBtn" title="Cancel the running turn, whoever sent it" style="display:none">Stop turn</button>
|
|
1283
|
+
<button type="button" id="attachBtn" class="attach-btn" title="Add images (or paste or drop them here)" aria-label="Add images" hidden></button>
|
|
1284
|
+
<input type="file" id="attachInput" accept="image/*" multiple hidden>
|
|
1252
1285
|
<div class="split">
|
|
1253
1286
|
<button id="actionBtn">Start</button>
|
|
1254
1287
|
</div>
|
|
@@ -17,25 +17,25 @@
|
|
|
17
17
|
// formClass, popoverClass, nowClass the form's, popover's and "Now:"
|
|
18
18
|
// line's own classes
|
|
19
19
|
// when(fresh) the form's note on when it applies (fresh: the start
|
|
20
|
-
// page's new
|
|
20
|
+
// page's new session, thinking only)
|
|
21
21
|
//
|
|
22
|
-
// The start page's new
|
|
22
|
+
// The start page's new session (kind NEW_SESSION) has a thinking chip only
|
|
23
23
|
// (thinking_chip.js keeps its choice): the form shows the model's level and
|
|
24
24
|
// takes a choice for the next create.
|
|
25
25
|
|
|
26
26
|
import { createChipPopover } from "./chip_popover.js";
|
|
27
|
-
import {
|
|
27
|
+
import { NEW_SESSION, SESSION } from "./llm_context_chip.js";
|
|
28
28
|
import { html, render } from "./vendor/lit-html.js";
|
|
29
29
|
|
|
30
30
|
export const DEFAULT = "default";
|
|
31
31
|
|
|
32
|
-
// The chip: its text, tooltip and whether the session (or the new
|
|
32
|
+
// The chip: its text, tooltip and whether the session (or the new session)
|
|
33
33
|
// set its own; null without a summary (an older worker, or a host the
|
|
34
34
|
// setting means nothing on).
|
|
35
35
|
export function chipModel(cfg, summary, { kind = SESSION } = {}) {
|
|
36
36
|
if (!summary || !summary.level) return null;
|
|
37
37
|
const own = !!summary.own;
|
|
38
|
-
const what = kind ===
|
|
38
|
+
const what = kind === NEW_SESSION ? "the new session" : "this session";
|
|
39
39
|
const from = own ? what : summary.source || "the model's own";
|
|
40
40
|
const title = `${cfg.name}: ${summary.level} (${from}). Click to change it for ${what}.`;
|
|
41
41
|
return { text: `${cfg.prefix} ${summary.level}${own ? ` · ${kind}` : ""}`, title, own };
|
|
@@ -57,16 +57,16 @@ export function nowText(summary) {
|
|
|
57
57
|
|
|
58
58
|
const option = (value, label, chosen) => html`<option value=${value} ?selected=${value === chosen}>${label}</option>`;
|
|
59
59
|
|
|
60
|
-
// The popover's form (a lit-html template). For the new
|
|
61
|
-
//
|
|
60
|
+
// The popover's form (a lit-html template). For the new session (kind
|
|
61
|
+
// NEW_SESSION), +summary+ is the model's and +choice+ the level the form
|
|
62
62
|
// shows; +now+ is the summary its "Now:" line reads (refresh passes a
|
|
63
63
|
// newer one, the rest stays as the form opened).
|
|
64
64
|
export function formTemplate(cfg, summary, { running = false, kind = SESSION, choice = null, now = summary } = {}) {
|
|
65
|
-
const fresh = kind ===
|
|
65
|
+
const fresh = kind === NEW_SESSION;
|
|
66
66
|
const chosen = (fresh ? choice : summary.own) || DEFAULT;
|
|
67
67
|
const follow = summary.own && !fresh ? "follow the model" : `follow the model (${summary.level})`;
|
|
68
68
|
return html`<form class=${`${cfg.formClass} llmctx-form`}><div class="ctx-pop-head"><strong>${cfg.name}</strong
|
|
69
|
-
><span class="ctx-pop-kind">${fresh ?
|
|
69
|
+
><span class="ctx-pop-kind">${fresh ? NEW_SESSION : "this session"}</span
|
|
70
70
|
><button type="button" class="ctx-pop-close" data-act="close" aria-label="Close">×</button></div
|
|
71
71
|
><div class=${`ctx-pop-meta ${cfg.nowClass}`}>${nowText(now)}</div
|
|
72
72
|
><label class="llmctx-field"><span>Level</span><select name="level">${option(DEFAULT, follow, chosen)}${
|
|
@@ -79,7 +79,7 @@ export function formTemplate(cfg, summary, { running = false, kind = SESSION, ch
|
|
|
79
79
|
// el is where the chip is drawn (clicks are delegated: #infoText,
|
|
80
80
|
// info_bar.js); summary(): the open session's summary; session(): its id;
|
|
81
81
|
// running(): a turn runs; run(line): sends the command. The start page's
|
|
82
|
-
// (kind
|
|
82
|
+
// (kind NEW_SESSION): summary() is the picked model's, choice() the pending
|
|
83
83
|
// level and choose(next) takes a Set's (null for "follow the model"). id
|
|
84
84
|
// and anchor (the chip's selector, by default its data attribute) keep it
|
|
85
85
|
// apart from the other controls on the same element (the anchor defaults
|
|
@@ -88,7 +88,7 @@ export function createLevelControl({ cfg, el, summary, session = () => null, run
|
|
|
88
88
|
kind = SESSION, choice = () => null, choose = null,
|
|
89
89
|
id, anchor = cfg.anchor,
|
|
90
90
|
doc = document, win = window }) {
|
|
91
|
-
const fresh = kind ===
|
|
91
|
+
const fresh = kind === NEW_SESSION;
|
|
92
92
|
let initial = null;
|
|
93
93
|
// What the open form was drawn from: the model's or session's summary
|
|
94
94
|
// and the form's options.
|