samagotchi 0.100.1 → 0.102.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 +50 -1
- data/bin/chi +14 -10
- data/docs/architecture.md +10 -2
- data/docs/cli.md +100 -5
- data/docs/configuration.md +26 -4
- data/docs/internals/bundles.md +1 -1
- data/docs/internals/engine.md +1 -0
- data/docs/internals/llm-context-forget.md +14 -0
- data/docs/internals/prompt-caching.md +8 -1
- data/docs/internals/working-set.md +205 -0
- data/docs/plugins.md +6 -3
- data/docs/sessions.md +28 -11
- data/docs/testing.md +8 -1
- data/lib/samagotchi/bridge/turn_accumulator.rb +16 -0
- data/lib/samagotchi/bridge.rb +9 -3
- data/lib/samagotchi/bundle_command/check.rb +5 -1
- data/lib/samagotchi/bundle_command/try/command.rb +6 -6
- data/lib/samagotchi/bundle_command/try/sandbox.rb +3 -3
- data/lib/samagotchi/bundle_command.rb +4 -4
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/commands/registry.rb +12 -1
- data/lib/samagotchi/config.rb +92 -53
- data/lib/samagotchi/engine.rb +153 -4
- data/lib/samagotchi/host_keys.rb +35 -18
- data/lib/samagotchi/idle_scheduler.rb +16 -1
- data/lib/samagotchi/llm_context_edit.rb +32 -10
- data/lib/samagotchi/llm_context_forget.rb +12 -3
- data/lib/samagotchi/llm_context_view.rb +19 -7
- data/lib/samagotchi/memory_bundle/installer.rb +2 -0
- data/lib/samagotchi/memory_bundle/manifest.rb +13 -2
- data/lib/samagotchi/message_parts.rb +8 -3
- data/lib/samagotchi/plugin/context.rb +2 -2
- data/lib/samagotchi/prompt.rb +1 -1
- data/lib/samagotchi/recap_command.rb +68 -0
- data/lib/samagotchi/recap_setup.rb +27 -14
- data/lib/samagotchi/send_command.rb +14 -11
- data/lib/samagotchi/session.rb +19 -12
- data/lib/samagotchi/session_commands.rb +44 -2
- data/lib/samagotchi/session_config.rb +411 -0
- data/lib/samagotchi/session_setup.rb +9 -9
- data/lib/samagotchi/sessions_command.rb +6 -2
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +5 -5
- data/lib/samagotchi/terminal_ui/attached_loop.rb +2 -1
- data/lib/samagotchi/terminal_ui/formatting.rb +10 -3
- data/lib/samagotchi/terminal_ui.rb +8 -6
- data/lib/samagotchi/testing/bundle.rb +3 -2
- data/lib/samagotchi/testing/fake_executable.rb +6 -16
- data/lib/samagotchi/testing/fake_executable_launcher +6 -0
- data/lib/samagotchi/tool_response.rb +10 -6
- data/lib/samagotchi/tool_runner.rb +25 -6
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +76 -11
- data/lib/samagotchi/web/public/app.js +35 -3
- data/lib/samagotchi/web/public/chip_popover.js +56 -17
- data/lib/samagotchi/web/public/commands.js +8 -2
- data/lib/samagotchi/web/public/data.js +7 -0
- data/lib/samagotchi/web/public/index.html +145 -1
- data/lib/samagotchi/web/public/info_bar.js +16 -4
- data/lib/samagotchi/web/public/info_bar_model.js +7 -3
- data/lib/samagotchi/web/public/refs.js +10 -1
- data/lib/samagotchi/web/public/stage_model.js +6 -5
- data/lib/samagotchi/web/public/stage_view.js +34 -2
- data/lib/samagotchi/web/public/stream_handlers.js +10 -1
- data/lib/samagotchi/web/public/turn_html.js +5 -0
- data/lib/samagotchi/web/public/turn_view.js +16 -4
- data/lib/samagotchi/web/public/working_set.js +612 -0
- data/lib/samagotchi/web/public/working_set_rail.js +278 -0
- data/lib/samagotchi/web/public/working_set_view.js +541 -0
- data/lib/samagotchi/web/server.rb +6 -4
- data/lib/samagotchi/worker.rb +9 -6
- data/lib/samagotchi/working_set.rb +238 -0
- data/lib/samagotchi/working_set_command.rb +119 -0
- data/lib/samagotchi/working_set_steer.rb +250 -0
- metadata +11 -2
- data/lib/samagotchi/bundle_session_settings.rb +0 -251
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
// The popover shell a session-bar chip opens (llm_context_chip.js,
|
|
2
|
-
// level_chip.js): a div.ctx-popover on the body, placed
|
|
3
|
-
// opened by a click on it (delegated on +el+)
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
2
|
+
// level_chip.js, the working set's): a div.ctx-popover on the body, placed
|
|
3
|
+
// at the chip, opened by a click on it (delegated on +el+) or by open()
|
|
4
|
+
// (another opener: the working set's rail and footprint), closed by a second
|
|
5
|
+
// click, its × (data-act="close"), a click outside or Esc; focus goes back
|
|
6
|
+
// to the opener. Another session's summary (refresh) closes it. What it
|
|
7
|
+
// draws, and what a Set or another data-act does, is the caller's. With
|
|
8
|
+
// +sheet+, a window narrower than SHEET_MAX gets it as a bottom sheet.
|
|
7
9
|
|
|
8
10
|
import { nothing, render } from "./vendor/lit-html.js";
|
|
9
11
|
|
|
10
12
|
const EDGE = 16;
|
|
11
13
|
const GAP = 8;
|
|
14
|
+
// Under this window width a +sheet+ popover is a bottom sheet.
|
|
15
|
+
export const SHEET_MAX = 600;
|
|
16
|
+
const SHEET_EDGE = 8;
|
|
12
17
|
|
|
13
18
|
// Where the popover goes for the chip's +rect+: above it, or below when
|
|
14
19
|
// the room above is short of its height and below has more; capped to the
|
|
@@ -24,26 +29,48 @@ export function popoverPlace(rect, { width, height, innerWidth, innerHeight }) {
|
|
|
24
29
|
return height > room ? { ...place, maxHeight: room } : place;
|
|
25
30
|
}
|
|
26
31
|
|
|
32
|
+
// A bottom sheet's place in a phone's window: the window's width less its
|
|
33
|
+
// edges, at its bottom, at most 60 % of its height (the rest scrolls).
|
|
34
|
+
// @return {left, bottom, width, maxHeight}
|
|
35
|
+
export function sheetPlace({ innerWidth, innerHeight }) {
|
|
36
|
+
return { left: SHEET_EDGE, bottom: SHEET_EDGE, width: innerWidth - 2 * SHEET_EDGE, maxHeight: Math.round(innerHeight * 0.6) };
|
|
37
|
+
}
|
|
38
|
+
|
|
27
39
|
// id, className: the popover's; width: its widest (px); el: where the chip
|
|
28
|
-
// is drawn; anchor: the chip's selector;
|
|
29
|
-
//
|
|
40
|
+
// is drawn; anchor: the chip's selector; openers: the selector of every
|
|
41
|
+
// element that opens it (a click there is not a click outside; default the
|
|
42
|
+
// anchor's); summary(anchor): what the form is drawn from (null: nothing
|
|
43
|
+
// opens); session(): the open session's id.
|
|
30
44
|
// show(data, opening): draws the form into the popover (opening: a fresh
|
|
31
45
|
// open, else a refresh with a newer summary); onClose(): the caller forgets
|
|
32
46
|
// its open state; onSubmit(form); onAct(name): a data-act other than close.
|
|
33
|
-
//
|
|
34
|
-
|
|
47
|
+
// sheet: a bottom sheet in a narrow window (sheetPlace).
|
|
48
|
+
// @return {pop, open, close, refresh, isOpen, opener}
|
|
49
|
+
export function createChipPopover({ id, className, width: widest, el, anchor: anchorSel, openers = anchorSel, summary,
|
|
35
50
|
session = () => null, show, onClose = () => {}, onSubmit, onAct = () => {},
|
|
36
|
-
doc = document, win = window }) {
|
|
51
|
+
sheet = false, doc = document, win = window }) {
|
|
37
52
|
const pop = doc.createElement("div");
|
|
38
53
|
pop.id = id;
|
|
39
54
|
pop.className = className;
|
|
40
55
|
pop.setAttribute("role", "dialog");
|
|
41
56
|
pop.hidden = true;
|
|
42
57
|
doc.body.appendChild(pop);
|
|
43
|
-
// The session the open form is for.
|
|
58
|
+
// The session the open form is for, and the element that opened it.
|
|
44
59
|
let openFor = null;
|
|
60
|
+
let opener = null;
|
|
45
61
|
|
|
46
62
|
function place(anchor) {
|
|
63
|
+
const narrow = sheet && win.innerWidth < SHEET_MAX;
|
|
64
|
+
pop.classList.toggle("sheet", narrow);
|
|
65
|
+
if (narrow) {
|
|
66
|
+
const at = sheetPlace({ innerWidth: win.innerWidth, innerHeight: win.innerHeight });
|
|
67
|
+
pop.style.width = `${at.width}px`;
|
|
68
|
+
pop.style.left = `${at.left}px`;
|
|
69
|
+
pop.style.top = "";
|
|
70
|
+
pop.style.bottom = `${at.bottom}px`;
|
|
71
|
+
pop.style.maxHeight = `${at.maxHeight}px`;
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
47
74
|
const width = Math.min(widest, win.innerWidth - 32);
|
|
48
75
|
pop.style.width = `${width}px`;
|
|
49
76
|
pop.style.maxHeight = "";
|
|
@@ -55,22 +82,29 @@ export function createChipPopover({ id, className, width: widest, el, anchor: an
|
|
|
55
82
|
if (at.maxHeight !== undefined) pop.style.maxHeight = `${at.maxHeight}px`;
|
|
56
83
|
}
|
|
57
84
|
|
|
85
|
+
// Opens at +anchor+ (open already: drawn anew there).
|
|
58
86
|
function open(anchor) {
|
|
59
|
-
const data = summary();
|
|
87
|
+
const data = summary(anchor);
|
|
60
88
|
if (!data) return;
|
|
61
89
|
openFor = session();
|
|
90
|
+
opener = anchor;
|
|
62
91
|
show(data, true);
|
|
63
92
|
pop.hidden = false;
|
|
64
93
|
place(anchor);
|
|
65
94
|
}
|
|
66
95
|
|
|
67
96
|
// A closed popover is empty, so each open draws a fresh form (unsent
|
|
68
|
-
// edits are gone).
|
|
69
|
-
|
|
97
|
+
// edits are gone). Focus that was in it (or +refocus+: Esc) goes back to
|
|
98
|
+
// its opener.
|
|
99
|
+
function close({ refocus = false } = {}) {
|
|
100
|
+
const had = refocus || pop.contains(doc.activeElement);
|
|
101
|
+
const back = opener;
|
|
70
102
|
pop.hidden = true;
|
|
71
103
|
openFor = null;
|
|
104
|
+
opener = null;
|
|
72
105
|
onClose();
|
|
73
106
|
render(nothing, pop);
|
|
107
|
+
if (had && back?.isConnected) back.focus?.();
|
|
74
108
|
}
|
|
75
109
|
|
|
76
110
|
function isOpen() { return !pop.hidden; }
|
|
@@ -84,6 +118,11 @@ export function createChipPopover({ id, className, width: widest, el, anchor: an
|
|
|
84
118
|
show(data, false);
|
|
85
119
|
}
|
|
86
120
|
|
|
121
|
+
// A popover's own re-placement (its content grew or shrank).
|
|
122
|
+
function replace() {
|
|
123
|
+
if (isOpen() && opener?.isConnected) place(opener);
|
|
124
|
+
}
|
|
125
|
+
|
|
87
126
|
// The click goes on to the document: another chip's popover (or the
|
|
88
127
|
// context popover) open meanwhile closes there. This one's own closer
|
|
89
128
|
// leaves its chip's click alone.
|
|
@@ -103,11 +142,11 @@ export function createChipPopover({ id, className, width: widest, el, anchor: an
|
|
|
103
142
|
else if (act) onAct(act);
|
|
104
143
|
});
|
|
105
144
|
doc.addEventListener("click", (e) => {
|
|
106
|
-
if (isOpen() && !pop.contains(e.target) && !e.target.closest?.(
|
|
145
|
+
if (isOpen() && !pop.contains(e.target) && !e.target.closest?.(openers)) close();
|
|
107
146
|
});
|
|
108
147
|
doc.addEventListener("keydown", (e) => {
|
|
109
|
-
if (e.key === "Escape" && isOpen()) close();
|
|
148
|
+
if (e.key === "Escape" && isOpen()) close({ refocus: true });
|
|
110
149
|
});
|
|
111
150
|
|
|
112
|
-
return { pop, close, refresh, isOpen };
|
|
151
|
+
return { pop, open, close, refresh, isOpen, replace, opener: () => opener };
|
|
113
152
|
}
|
|
@@ -109,13 +109,19 @@ const WEB_LOCAL_REPLIES = {
|
|
|
109
109
|
"/detach": "/detach: a terminal's command; close the tab to leave, the worker keeps running",
|
|
110
110
|
"/exit": "/exit: close the tab to leave; stop in the session bar stops the worker",
|
|
111
111
|
"/quit": "/quit: close the tab to leave; stop in the session bar stops the worker",
|
|
112
|
-
"/recap": "/recap: not in the web yet; a recap shows here by itself when you come back to an idle session, and /recap in a terminal (chi --attach) makes one now",
|
|
112
|
+
"/recap": "/recap: not in the web yet; a recap shows here by itself when you come back to an idle session, and /recap in a terminal (chi --attach) makes one now. /recap on|off|default sets this session's recap here too",
|
|
113
113
|
"/unqueue": "/unqueue: a terminal's command; here the × on a message queued for after the turn removes it",
|
|
114
114
|
"/stats": "/stats: not in the web yet; each turn shows its time, the session bar the context use and speed (hover ctx for tokens and cost), and /stats in a terminal (chi --attach) has the rest",
|
|
115
115
|
};
|
|
116
116
|
|
|
117
|
+
// The ones whose words after the name are the worker's (/recap off): the
|
|
118
|
+
// page answers only the bare name.
|
|
119
|
+
const WORKER_WITH_WORDS = new Set(["/recap"]);
|
|
120
|
+
|
|
117
121
|
export function webLocalReply(text) {
|
|
118
|
-
const
|
|
122
|
+
const words = String(text || "").trim().split(/\s+/);
|
|
123
|
+
const name = words[0].toLowerCase();
|
|
124
|
+
if (WORKER_WITH_WORDS.has(name) && words.length > 1) return null;
|
|
119
125
|
return WEB_LOCAL_REPLIES[name] || null;
|
|
120
126
|
}
|
|
121
127
|
|
|
@@ -298,6 +298,13 @@ export function openEvents(handlers = {}, opts = {}) {
|
|
|
298
298
|
};
|
|
299
299
|
}
|
|
300
300
|
|
|
301
|
+
// A session's working set as the model has it (WorkingSet: {items, at,
|
|
302
|
+
// running}), what the stage view's working set overlays on its rows
|
|
303
|
+
// (web.working_set).
|
|
304
|
+
export function getWorkingSet(id, opts = {}) {
|
|
305
|
+
return api(`/api/sessions/${encodeURIComponent(id)}/working_set`, opts);
|
|
306
|
+
}
|
|
307
|
+
|
|
301
308
|
// A session's attached context (chi context): the sources it sees, one
|
|
302
309
|
// with its text, and detach (a session's) or mute here (a project's).
|
|
303
310
|
export function listContext(id, opts = {}) {
|
|
@@ -867,6 +867,150 @@ body.stage-view .turn-stage{position:absolute;left:0;right:0;bottom:100%;margin-
|
|
|
867
867
|
.turn-stage .outline-fold{display:none}
|
|
868
868
|
.turn-stage .turn-work.outline>.gen.folded{display:block}
|
|
869
869
|
.turn-stage .ts-cloud-body .gen:first-of-type{margin-top:0;border-top:0}
|
|
870
|
+
/* The working set (web.working_set, working_set_rail.js): a rail on the right of the live slots, out of their flow (absolute,
|
|
871
|
+
the live block's full height), so it never sets their height; its lines are fixed 18 px slots, each at its place by a
|
|
872
|
+
transform (a fold fades the absorbed lines out in place and slides the ones below up once). A stage narrower than 760 px
|
|
873
|
+
has it as a row under the trail (a fixed line, part of the live block from the turn's start); a phone has neither (the
|
|
874
|
+
session bar's chip is its entry). body.working-set: the flag is on (app.js). */
|
|
875
|
+
body.working-set .turn-stage{container-type:inline-size}
|
|
876
|
+
body.working-set .ts-live{position:relative;padding-right:264px}
|
|
877
|
+
.ts-ws{--ws-line:18px;position:absolute;top:0;right:0;bottom:0;width:248px;box-sizing:border-box;padding:0 4px 0 12px;border-left:1px solid var(--border);border-radius:0 6px 6px 0;display:flex;flex-direction:column;overflow:hidden;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;line-height:var(--ws-line);color:var(--text-2);cursor:pointer}
|
|
878
|
+
.ts-ws:hover{background:var(--bg-hover)}
|
|
879
|
+
.ts-ws:focus-visible{outline:2px solid var(--accent-ring);outline-offset:1px}
|
|
880
|
+
.ws-head{flex:none;display:flex;align-items:center;gap:6px;min-width:0;height:var(--ws-line);font-family:ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;font-size:11px;font-weight:600;letter-spacing:.04em;text-transform:uppercase;color:var(--text-muted);white-space:nowrap}
|
|
881
|
+
.ws-count{margin-right:auto;font-weight:400;letter-spacing:0;color:var(--text-faint);font-variant-numeric:tabular-nums}
|
|
882
|
+
.ws-tick{display:none}
|
|
883
|
+
.ws-fold{flex:none;width:18px;height:18px;padding:0;border:0;border-radius:4px;background:transparent;color:var(--text-muted);font-size:12px;line-height:18px;opacity:0;transition:opacity .15s}
|
|
884
|
+
.ws-fold:hover:not(:disabled){filter:none;background:var(--bg-raised);color:var(--text)}
|
|
885
|
+
.ts-ws:hover .ws-fold,.ts-ws:focus-within .ws-fold,.ts-ws.collapsed .ws-fold{opacity:1}
|
|
886
|
+
@media (hover:none){.ws-fold{opacity:1}}
|
|
887
|
+
.ws-ring{flex:none;width:14px;height:14px}
|
|
888
|
+
.ws-ring circle{fill:none;stroke-width:2.4}
|
|
889
|
+
.ws-ring .track{stroke:var(--border-strong)}
|
|
890
|
+
.ws-ring .fill{stroke:var(--accent);stroke-linecap:round;transform:rotate(-90deg);transform-origin:50% 50%;transition:stroke-dasharray .4s ease}
|
|
891
|
+
.ws-ring.warn .fill{stroke:var(--warn)}
|
|
892
|
+
.ws-ring.danger .fill{stroke:var(--danger)}
|
|
893
|
+
.ws-list{position:relative;flex:1;min-height:0;overflow:hidden}
|
|
894
|
+
.ts-ws.empty .ws-list::before{content:"nothing read yet";font-family:ui-sans-serif,system-ui,sans-serif;font-style:italic;color:var(--text-faint)}
|
|
895
|
+
.ws-item,.ws-more{position:absolute;top:0;left:0;right:0;height:var(--ws-line);transition:transform .18s cubic-bezier(.3,.7,.2,1),opacity .15s ease,background-color .8s ease}
|
|
896
|
+
.ws-item{display:flex;align-items:center;gap:5px;white-space:nowrap;overflow:hidden;border-radius:4px}
|
|
897
|
+
.ws-item .k{flex:none;width:12px;text-align:center;color:var(--text-faint)}
|
|
898
|
+
.ws-item.memory .k{color:var(--think-text)}
|
|
899
|
+
.ws-item.url .k{color:var(--accent)}
|
|
900
|
+
.ws-item .t{min-width:0;overflow:hidden;text-overflow:ellipsis}
|
|
901
|
+
.ws-item .n{flex:none;color:var(--text-faint);font-variant-numeric:tabular-nums}
|
|
902
|
+
.ws-item .e{flex:none;color:var(--accent);font-weight:600;font-variant-numeric:tabular-nums}
|
|
903
|
+
.ws-item .c{flex:none;color:var(--text-faint);font-variant-numeric:tabular-nums}
|
|
904
|
+
.ws-item .n:empty,.ws-item .e:empty,.ws-item .c:empty{display:none}
|
|
905
|
+
/* The server's states (WorkingSet): stale faded; forgotten struck, faint and faded (a group only when all its members are). The words are the popover's and the titles' (colour is never alone). */
|
|
906
|
+
.ws-item.stale{opacity:.55}
|
|
907
|
+
.ws-item.forgotten{opacity:.5}
|
|
908
|
+
.ws-item.forgotten,.ws-item.dropped{opacity:.5}
|
|
909
|
+
.ws-item.forgotten .t,.ws-row.forgotten .t,.ws-foot .l.forgotten,.ws-item.dropped .t,.ws-row.dropped .t,.ws-foot .l.dropped{text-decoration:line-through;color:var(--text-faint)}
|
|
910
|
+
.ws-row.stale .k,.ws-row.stale .t,.ws-foot .l.stale{opacity:.55}
|
|
911
|
+
.ws-row.forgotten .k,.ws-row.forgotten .t,.ws-foot .l.forgotten,.ws-row.dropped .k,.ws-row.dropped .t,.ws-foot .l.dropped{opacity:.5}
|
|
912
|
+
.ws-item.enter{animation:ws-in .2s ease}
|
|
913
|
+
@keyframes ws-in{from{opacity:0}}
|
|
914
|
+
.ws-item.leave,.ws-more.leave{opacity:0;pointer-events:none}
|
|
915
|
+
/* Just entered (FLASH_MS): an accent dot for the glyph and a tint; it settles (the tint fades 800 ms). */
|
|
916
|
+
.ws-item.fresh{background:var(--accent-tint)}
|
|
917
|
+
.ws-item.fresh .k{font-size:0;display:flex;align-items:center;justify-content:center;height:var(--ws-line)}
|
|
918
|
+
.ws-item.fresh .k::before{content:"";display:inline-block;width:6px;height:6px;border-radius:50%;background:var(--accent);box-shadow:0 0 0 2px var(--accent-ring)}
|
|
919
|
+
.ws-more{padding-left:17px;font-family:ui-sans-serif,system-ui,sans-serif;font-size:11px;color:var(--text-faint);font-variant-numeric:tabular-nums}
|
|
920
|
+
/* Folded by hand (kept per browser): the head line alone at the right, the newest entry ticking through it. */
|
|
921
|
+
.ts-ws.collapsed{bottom:auto;width:auto;max-width:248px;height:var(--ws-line);border-left:0;padding-left:6px}
|
|
922
|
+
.ts-ws.collapsed .ws-list{display:none}
|
|
923
|
+
.ts-ws.collapsed .ws-count{margin-right:0}
|
|
924
|
+
.ts-ws.collapsed .ws-tick{display:block;min-width:0;overflow:hidden;text-overflow:ellipsis;margin-right:auto;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;font-weight:400;letter-spacing:0;text-transform:none;color:var(--text-2)}
|
|
925
|
+
.ts-ws.collapsed .ws-tick:not(:empty)::before{content:"+";margin-right:5px;color:var(--accent);font-weight:700}
|
|
926
|
+
.ws-tick.fresh{animation:ts-fade .3s ease}
|
|
927
|
+
/* The row form, in a stage under 760 px: one line under the trail (its room, 18 px and a gap, kept while a card waits). */
|
|
928
|
+
@container (max-width:759px){
|
|
929
|
+
body.working-set .ts-live{padding-right:0;--ws-row-h:26px}
|
|
930
|
+
.ts-ws{position:static;width:auto;height:var(--ws-line);flex-direction:row;align-items:center;gap:10px;padding:0;border-left:0;border-radius:6px}
|
|
931
|
+
.ts-ws .ws-head{flex:none}
|
|
932
|
+
.ts-ws .ws-count{margin-right:0}
|
|
933
|
+
.ts-ws .ws-list{display:flex;align-items:center;gap:12px;height:var(--ws-line)}
|
|
934
|
+
.ws-item,.ws-more{position:static;flex:none;max-width:30ch;transform:none !important;transition:none}
|
|
935
|
+
.ws-more{padding-left:0}
|
|
936
|
+
.ws-item.leave,.ws-more.leave{display:none}
|
|
937
|
+
.turn-stage.waiting .ts-ws{display:none}
|
|
938
|
+
}
|
|
939
|
+
body.working-set .turn-stage.waiting .ts-live{min-height:calc(var(--ts-live-h) + var(--ws-row-h,0px))}
|
|
940
|
+
body.working-set .turn-stage.held-wait:not(.waiting) .ts-live{min-height:max(calc(var(--ts-live-h) + var(--ws-row-h,0px)),var(--live-held,0px))}
|
|
941
|
+
/* A finished turn's footprint, under its closed block (working_set_view.js): one faint line; a click opens its list. */
|
|
942
|
+
.ws-foot{display:block;width:fit-content;max-width:calc(100% - 13px);box-sizing:border-box;margin:-4px 0 0 13px;padding:0 6px;border-radius:6px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11.5px;line-height:18px;color:var(--text-faint);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;cursor:pointer}
|
|
943
|
+
.ws-foot:hover{color:var(--text-muted);background:var(--bg-hover)}
|
|
944
|
+
.ws-foot:focus-visible{outline:2px solid var(--accent-ring);outline-offset:1px}
|
|
945
|
+
.ws-foot .w{font-family:ui-sans-serif,system-ui,sans-serif}
|
|
946
|
+
.ws-foot .sep{color:var(--border-strong)}
|
|
947
|
+
.ws-foot .e{color:var(--accent)}
|
|
948
|
+
.ws-foot .k{color:var(--think-text)}
|
|
949
|
+
.ws-foot .url .k{color:var(--accent)}
|
|
950
|
+
.ws-foot .f{text-decoration:line-through}
|
|
951
|
+
/* A step tag (s3, t2·s3, prompt): where an item entered; a button jumps to its call. */
|
|
952
|
+
.ws-step{display:inline-block;padding:0 4px;border:0;border-radius:4px;background:var(--badge-bg);font-family:ui-sans-serif,system-ui,sans-serif;font-size:10.5px;line-height:14px;font-weight:400;color:var(--text-faint);font-variant-numeric:tabular-nums;vertical-align:1px}
|
|
953
|
+
.ws-step.p{color:var(--think-text)}
|
|
954
|
+
button.ws-step{cursor:pointer}
|
|
955
|
+
button.ws-step:hover:not(:disabled){filter:none;background:var(--accent-bg);color:var(--accent)}
|
|
956
|
+
/* The expanded view (a chip popover; a bottom sheet on a phone). */
|
|
957
|
+
.ws-popover{gap:0}
|
|
958
|
+
.ws-popover .ctx-pop-head{align-items:center;flex-wrap:wrap;margin-bottom:4px}
|
|
959
|
+
.ws-scope{display:inline-flex;gap:2px}
|
|
960
|
+
.ws-scope button{padding:1px 8px;border:1px solid transparent;border-radius:999px;background:transparent;color:var(--text-muted);font-size:11px;line-height:16px}
|
|
961
|
+
.ws-scope button.on{color:var(--text-2);border-color:var(--border-strong);background:var(--bg-raised)}
|
|
962
|
+
.ws-scope button:hover:not(:disabled){filter:none;color:var(--text)}
|
|
963
|
+
.ws-popover .ctx-pop-meta{margin-top:6px}
|
|
964
|
+
.ws-popover .ws-hint{margin-top:10px;color:var(--text-faint)}
|
|
965
|
+
.ws-row{display:flex;align-items:center;gap:8px;width:100%;min-width:0;height:28px;box-sizing:border-box;padding:0 6px;border:0;border-radius:6px;background:transparent;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12.5px;line-height:28px;color:var(--text-2);text-align:left}
|
|
966
|
+
.ws-row:hover{background:var(--bg-hover)}
|
|
967
|
+
button.ws-row:hover:not(:disabled){filter:none;background:var(--bg-hover)}
|
|
968
|
+
.ws-row .k{flex:none;width:12px;text-align:center;color:var(--text-faint)}
|
|
969
|
+
.ws-row.memory .k{color:var(--think-text)}
|
|
970
|
+
.ws-row.url .k{color:var(--accent)}
|
|
971
|
+
.ws-row .t{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:inherit}
|
|
972
|
+
.ws-row a.t{text-decoration:none}
|
|
973
|
+
.ws-row a.t:hover{text-decoration:underline}
|
|
974
|
+
.ws-row .e{flex:none;color:var(--accent);font-weight:600}
|
|
975
|
+
.ws-row .n{flex:none;color:var(--text-faint)}
|
|
976
|
+
.ws-row .st{flex:none;margin-left:auto;font-family:ui-sans-serif,system-ui,sans-serif;font-size:11px;color:var(--text-faint)}
|
|
977
|
+
.ws-row .chev{flex:none;margin-left:auto;font-family:ui-sans-serif,system-ui,sans-serif;font-size:11px;color:var(--text-faint)}
|
|
978
|
+
.ws-row .ws-step{flex:none}
|
|
979
|
+
.ws-row .tk,.ws-row .c{flex:none;font-family:ui-sans-serif,system-ui,sans-serif;font-size:11px;color:var(--text-faint);font-variant-numeric:tabular-nums}
|
|
980
|
+
.ws-row.member{padding-left:26px}
|
|
981
|
+
.ws-row.fresh{background:var(--accent-tint)}
|
|
982
|
+
.ws-row.fresh .k{color:var(--accent)}
|
|
983
|
+
.ws-row.muted{opacity:.6}
|
|
984
|
+
/* Steering (drop / undrop): a row's actions on hover and keyboard focus (always where there is no hover); a pending op's row dashed warn with its pill, the footer counting them. */
|
|
985
|
+
.ws-act{flex:none;display:inline-flex;gap:4px;opacity:0}
|
|
986
|
+
.ws-row:hover .ws-act,.ws-row:focus-within .ws-act,.ws-ghead:hover .ws-act,.ws-ghead:focus-within .ws-act,.ws-row.pending .ws-act{opacity:1}
|
|
987
|
+
@media (hover:none){.ws-act{opacity:1}}
|
|
988
|
+
.ws-act-b{flex:none;padding:0 6px;border:1px solid var(--border-strong);border-radius:4px;background:transparent;font-family:ui-sans-serif,system-ui,sans-serif;font-size:11px;font-weight:400;line-height:16px;color:var(--text-muted);cursor:pointer}
|
|
989
|
+
.ws-act-b:hover:not(:disabled){filter:none;color:var(--accent);border-color:var(--accent);background:var(--accent-bg)}
|
|
990
|
+
.ws-act-b:disabled{opacity:.5;cursor:default}
|
|
991
|
+
.ws-ghead{display:flex;align-items:center;gap:4px}
|
|
992
|
+
.ws-ghead>.ws-group-head{flex:1;min-width:0}
|
|
993
|
+
.ws-row.pending{border:1px dashed var(--warn-border);background:var(--warn-bg)}
|
|
994
|
+
.ws-row.pending:hover{background:var(--warn-bg)}
|
|
995
|
+
.ws-pend{flex:none;padding:0 6px;border:1px solid var(--warn-border);border-radius:999px;background:var(--warn-bg);color:var(--warn-text);font-family:ui-sans-serif,system-ui,sans-serif;font-size:10.5px;line-height:15px;white-space:nowrap}
|
|
996
|
+
.ws-pending{margin-top:8px;padding:4px 8px;border:1px dashed var(--warn-border);border-radius:6px;background:var(--warn-bg);color:var(--warn-text);font-size:11.5px;line-height:20px}
|
|
997
|
+
.ws-pending .ws-act-b{color:var(--warn-text);border-color:var(--warn-border)}
|
|
998
|
+
.ws-popover.sheet .ws-row .ws-act{display:none}
|
|
999
|
+
.ws-strip{display:flex;gap:8px;padding:2px 6px 6px 26px}
|
|
1000
|
+
.ws-strip .ws-act-b{padding:0 14px;font-size:12.5px;line-height:28px;border-radius:6px}
|
|
1001
|
+
.ws-popover.sheet{border-radius:14px}
|
|
1002
|
+
/* A phone's sheet: the name, the scope and × on the first line, the count and the ring under them. */
|
|
1003
|
+
.ws-popover.sheet .ctx-pop-close{order:2}
|
|
1004
|
+
.ws-popover.sheet .ctx-pop-head>.ctx-pop-kind{order:3;flex:1 0 calc(100% - 30px)}
|
|
1005
|
+
.ws-popover.sheet .ctx-pop-head>.ws-ring{order:4}
|
|
1006
|
+
.ws-popover.sheet .ws-row{height:30px;line-height:30px}
|
|
1007
|
+
/* The session bar's chip, in the mem chip's place: a dot (the accent: changed since its list was last opened) and the count. */
|
|
1008
|
+
#infoBar .meta .ws-chip{display:inline-flex;align-items:center;gap:5px;padding:2px 7px;border:1px solid transparent;cursor:pointer;font-family:inherit;font-weight:400;white-space:nowrap;font-variant-numeric:tabular-nums}
|
|
1009
|
+
#infoBar .meta .ws-chip:hover:not(:disabled){filter:none;color:var(--text);border-color:var(--accent-border);background:var(--accent-bg)}
|
|
1010
|
+
.ws-dot{width:6px;height:6px;border-radius:50%;background:var(--text-faint);opacity:.6}
|
|
1011
|
+
.ws-chip.changed .ws-dot{background:var(--accent);opacity:1;box-shadow:0 0 0 2px var(--accent-ring)}
|
|
1012
|
+
@media(max-width:600px){body.working-set .ts-live{padding-right:0;--ws-row-h:0px}.ts-ws{display:none}}
|
|
1013
|
+
@media (prefers-reduced-motion:reduce){.ws-item,.ws-more,.ws-ring .fill{transition:none}.ws-item.enter,.ws-tick.fresh{animation:none}}
|
|
870
1014
|
@media(max-width:600px){
|
|
871
1015
|
.bubble.user.queued[data-delivery="queue"]{flex-wrap:wrap}
|
|
872
1016
|
.bubble.user.queued[data-delivery="queue"] .origin-label,.bubble.user.queued[data-delivery="queue"] .state-badge{order:0}
|
|
@@ -974,7 +1118,7 @@ body.has-session.composer-folded #infoBar{border-top-color:transparent}
|
|
|
974
1118
|
#infoBar .meta .id{order:-2}
|
|
975
1119
|
#infoBar .meta .status{order:-1}
|
|
976
1120
|
/* The memory chip right after the status (same order, later in the DOM): the meta scrolls, and only its start is in sight. */
|
|
977
|
-
#infoBar .meta .mem-chip{order:-1}
|
|
1121
|
+
#infoBar .meta .mem-chip,#infoBar .meta .ws-chip{order:-1}
|
|
978
1122
|
/* A parent's children chip too: its waiting part is what to see. */
|
|
979
1123
|
#infoBar .meta .children-chip{order:-1}
|
|
980
1124
|
/* A chain's ← → links too: the way to the other days. */
|
|
@@ -57,6 +57,15 @@ function kidsTemplate(kids) {
|
|
|
57
57
|
return html`<a class=${`model children-chip${kids.waitingText ? " waiting" : ""}`} href=${ALL_SESSIONS_HASH} title=${`${kids.title} (all sessions)`}>${kids.text}${waiting}</a>`;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
// The working set's chip (web.working_set), in the mem chip's place: a dot
|
|
61
|
+
// (the accent when the set changed since its list was last opened) and
|
|
62
|
+
// "set N"; nothing while the set is empty. +chip+: {n, changed} or null.
|
|
63
|
+
export function workingSetChipTemplate(chip) {
|
|
64
|
+
if (!chip) return nothing;
|
|
65
|
+
const title = `Working set: ${chip.n} item${chip.n === 1 ? "" : "s"} this session brought into context (files, memories, URLs)${chip.changed ? ", changed since you last looked" : ""}. Open the list.`;
|
|
66
|
+
return html`<button type="button" class=${`model ws-chip${chip.changed ? " changed" : ""}`} title=${title} aria-label=${title} aria-haspopup="dialog"><span class="ws-dot" aria-hidden="true"></span>set ${chip.n}</button>`;
|
|
67
|
+
}
|
|
68
|
+
|
|
60
69
|
// The info line for +bar+, nothing without a session (the start page hides
|
|
61
70
|
// the bar). +bar+:
|
|
62
71
|
// id, copied (the copy button just copied), status, model ({text, title,
|
|
@@ -65,18 +74,21 @@ function kidsTemplate(kids) {
|
|
|
65
74
|
// childrenSummary), llmContext, thinking and reasoningHistory (the chips'
|
|
66
75
|
// summaries; reasoningHistory null on a native host: no chip), toolkit
|
|
67
76
|
// (the tools chip's summary, null on full: no chip), notes
|
|
68
|
-
// ({text, title} or null), memories (the used names),
|
|
77
|
+
// ({text, title} or null), memories (the used names), workingSet
|
|
78
|
+
// (web.working_set on: {chip}, the working set's chip in place of the
|
|
79
|
+
// mem chip; null: off), dir, ctx ({text,
|
|
69
80
|
// tip}), speed ({text, tip} or null), clock (the session time's text, ""
|
|
70
81
|
// for none), worker ({badge, busy} or null).
|
|
71
82
|
export function infoBarHtml(bar) {
|
|
72
83
|
if (!bar) return nothing;
|
|
73
84
|
const attach = `chi --attach ${bar.id}`;
|
|
74
85
|
const memories = bar.memories || [];
|
|
75
|
-
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)}${thinkingChip(bar.thinking)}${reasoningHistoryChip(bar.reasoningHistory)}${llmContextChip(bar.llmContext)}${toolkitChip(bar.toolkit)}${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>`;
|
|
86
|
+
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)}${thinkingChip(bar.thinking)}${reasoningHistoryChip(bar.reasoningHistory)}${llmContextChip(bar.llmContext)}${toolkitChip(bar.toolkit)}${bar.notes ? html`<span class="model notes-chip" title=${bar.notes.title}>${bar.notes.text}</span>` : nothing}${bar.workingSet ? workingSetChipTemplate(bar.workingSet.chip) : 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>`;
|
|
76
87
|
}
|
|
77
88
|
|
|
78
|
-
// Draws +bar+ into +el+ (#infoText), with the memories in its tooltip
|
|
89
|
+
// Draws +bar+ into +el+ (#infoText), with the memories in its tooltip
|
|
90
|
+
// (with the working set on, its list says them instead).
|
|
79
91
|
export function renderInfoBar(el, bar) {
|
|
80
92
|
render(infoBarHtml(bar), el);
|
|
81
|
-
el.title = bar ? memoriesTooltip(bar.memories, bar.preloaded, bar.muted) : "";
|
|
93
|
+
el.title = bar && !bar.workingSet ? memoriesTooltip(bar.memories, bar.preloaded, bar.muted) : "";
|
|
82
94
|
}
|
|
@@ -63,7 +63,9 @@ export function infoButtons({ id, state, chain, archived, continuing, streaming
|
|
|
63
63
|
// @param restarting whether this session's Restart is under way
|
|
64
64
|
// @param running whether its turn runs (the session clock ticks)
|
|
65
65
|
// @param now the clock (ms)
|
|
66
|
-
|
|
66
|
+
// @param workingSet web.working_set on: the chip's {n, changed}, or null
|
|
67
|
+
// while the set is empty; undefined: off (the mem chip shows)
|
|
68
|
+
export function infoBarModel({ id, state, sessions, chain, cardTitle, badge, restarting, running, now, workingSet = undefined }) {
|
|
67
69
|
const tokens = state.timing.tokens;
|
|
68
70
|
const ctxTip = ctxBarTip(tokens, state.timing.memoryIndex,
|
|
69
71
|
{ tokens: state.ctxWindow, source: state.ctxWindowSource, pct: state.ctxPct,
|
|
@@ -96,6 +98,8 @@ export function infoBarModel({ id, state, sessions, chain, cardTitle, badge, res
|
|
|
96
98
|
// The model notes the session's prompt carried.
|
|
97
99
|
notes: promptNotesChip(state.promptNotes),
|
|
98
100
|
memories: state.usedMemories,
|
|
101
|
+
// On, the working set's chip stands in the mem chip's place.
|
|
102
|
+
workingSet: workingSet === undefined ? null : { chip: workingSet },
|
|
99
103
|
preloaded: state.preloadedMemories,
|
|
100
104
|
muted: state.mutedMemories,
|
|
101
105
|
dir: state.dir,
|
|
@@ -109,12 +113,12 @@ export function infoBarModel({ id, state, sessions, chain, cardTitle, badge, res
|
|
|
109
113
|
// The whole bar for the open session +id+ (null: the start page, no line).
|
|
110
114
|
// @param sessions the page's list; +continuing+, +restarting+: Sets of ids
|
|
111
115
|
// @return {buttons, bar}
|
|
112
|
-
export function sessionBar({ id, state, sessions, versions, cardTitle, continuing, restarting, streaming, running, now }) {
|
|
116
|
+
export function sessionBar({ id, state, sessions, versions, cardTitle, continuing, restarting, streaming, running, now, workingSet = undefined }) {
|
|
113
117
|
const chain = id ? chainNeighbours(sessions, id, { continues: state.continues }) : { prev: null, next: null };
|
|
114
118
|
const archived = !!sessions.find((s) => s.id === id)?.archived;
|
|
115
119
|
const buttons = infoButtons({ id, state, chain, archived, continuing: continuing.has(id), streaming });
|
|
116
120
|
if (!id) return { buttons, bar: null };
|
|
117
121
|
const badge = workerBadgeFor(id, state, versions);
|
|
118
|
-
const bar = infoBarModel({ id, state, sessions, chain, cardTitle, badge, restarting: restarting.has(id), running, now });
|
|
122
|
+
const bar = infoBarModel({ id, state, sessions, chain, cardTitle, badge, restarting: restarting.has(id), running, now, workingSet });
|
|
119
123
|
return { buttons, bar };
|
|
120
124
|
}
|
|
@@ -413,10 +413,19 @@ function lineIn(row, line) {
|
|
|
413
413
|
export function showInTurn(target, { behavior = smoothScroll() } = {}) {
|
|
414
414
|
const row = turnRowFor(target);
|
|
415
415
|
if (!row) return null;
|
|
416
|
+
return showRow(row, { line: target.line, behavior });
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Shows a tool row: what hides it opened (its step, its turn's block; a
|
|
420
|
+
// view that hides its rows its own way opens on the row's ref:reveal
|
|
421
|
+
// event), its diff (or the read showing +line+) opened, scrolled to the
|
|
422
|
+
// line (else the row) and both flashed. The working set's step tags jump
|
|
423
|
+
// here. The row.
|
|
424
|
+
export function showRow(row, { line: lineNo = null, behavior = smoothScroll() } = {}) {
|
|
416
425
|
const view = row.ownerDocument.defaultView || globalThis;
|
|
417
426
|
const reveal = new view.CustomEvent("ref:reveal", { bubbles: true, detail: {} });
|
|
418
427
|
row.dispatchEvent(reveal);
|
|
419
|
-
const line =
|
|
428
|
+
const line = lineNo ? lineIn(row, lineNo) : null;
|
|
420
429
|
const fold = line?.closest("details.activity-diff, details.activity-read") || row.querySelector("details.activity-diff");
|
|
421
430
|
for (let details = (line || fold || row).closest("details"); details; details = details.parentElement?.closest("details")) {
|
|
422
431
|
details.open = true;
|
|
@@ -225,11 +225,12 @@ export function handOffDue({ ended, pending, inUse, quietSinceMs, nowMs }) {
|
|
|
225
225
|
}
|
|
226
226
|
|
|
227
227
|
// A handed-off turn's nodes in the order the turn view leaves them: the
|
|
228
|
-
// prompt bubble (none for a reminder turn), the block,
|
|
229
|
-
//
|
|
230
|
-
//
|
|
231
|
-
|
|
232
|
-
|
|
228
|
+
// prompt bubble (none for a reminder turn), the block, its working set's
|
|
229
|
+
// footprint line (web.working_set), the extras in arrival order, the
|
|
230
|
+
// answer's pieces, the timing line, the end line, the kept cards, then the
|
|
231
|
+
// lines queued for after the turn (they run next).
|
|
232
|
+
export function handOffOrder({ prompt = null, block = null, footprint = null, extras = [], answer = [], timing = null, end = null, kept = [], queued = [] }) {
|
|
233
|
+
return [prompt, block, footprint, ...extras, ...answer, timing, end, ...kept, ...queued].filter(Boolean);
|
|
233
234
|
}
|
|
234
235
|
|
|
235
236
|
// The rows that wait for the running turn's end to run as their own: a
|
|
@@ -23,6 +23,8 @@ import { createInUseWatch } from "./stage_watch.js";
|
|
|
23
23
|
import { formatDuration } from "./timing.js";
|
|
24
24
|
import { easeScrollTo } from "./scroll.js";
|
|
25
25
|
import { readStored, writeStored } from "./storage.js";
|
|
26
|
+
import { createWorkingSetRail } from "./working_set_rail.js";
|
|
27
|
+
import { stampRows } from "./working_set_view.js";
|
|
26
28
|
|
|
27
29
|
const COLLAPSE_KEY = "chi_stage_collapsed";
|
|
28
30
|
// The turn-end read (markdown, answer_display) settles within this, or the
|
|
@@ -54,10 +56,12 @@ function readCollapsed() {
|
|
|
54
56
|
// +composerEl+ size the stage's cap.
|
|
55
57
|
// +taskStop+: the page's stopper (task_stop.js), for the stop-task button
|
|
56
58
|
// on a running task_wait. +renderMarkdown+ and +linkRefs+: the asking
|
|
57
|
-
// slot's markdown read and ref pass (stage_slots.js).
|
|
59
|
+
// slot's markdown read and ref pass (stage_slots.js). +workingSet+: the
|
|
60
|
+
// page's working set (working_set_view.js, web.working_set; null: off): its
|
|
61
|
+
// rail in the live slots, its footprint at the hand-off.
|
|
58
62
|
export function createStageView({
|
|
59
63
|
historyEl, appendToHistory, isNearBottom: historyNearBottom, sawText, sessionId, dockEl, centerEl, composerEl, taskStop = null,
|
|
60
|
-
renderMarkdown, linkRefs,
|
|
64
|
+
renderMarkdown, linkRefs, workingSet = null,
|
|
61
65
|
}) {
|
|
62
66
|
// The dock becomes a clear column: the stage's card, then the composer's
|
|
63
67
|
// (its rows moved into .dock-card, which wears the glass and the focus ring).
|
|
@@ -97,6 +101,16 @@ export function createStageView({
|
|
|
97
101
|
asking.hidden = true;
|
|
98
102
|
const now = node("div", "ts-now", live);
|
|
99
103
|
const trail = node("div", "ts-trail", live);
|
|
104
|
+
// The working set's rail: out of the live slots' flow on their right (a
|
|
105
|
+
// row under the trail in a narrow stage), so it never sets their height.
|
|
106
|
+
const rail = workingSet
|
|
107
|
+
? createWorkingSetRail({ live, stage, onOpen: (el) => workingSet.openTurn(el), onChange: () => scheduleRefresh() })
|
|
108
|
+
: null;
|
|
109
|
+
if (rail) {
|
|
110
|
+
workingSet.setSeen(rail.seenAt);
|
|
111
|
+
// The server's states change the lines (never the fold): a redraw.
|
|
112
|
+
workingSet.onServer(() => scheduleRefresh());
|
|
113
|
+
}
|
|
100
114
|
const extras = node("div", "ts-extras", scroller);
|
|
101
115
|
const answer = node("div", "ts-answer", scroller);
|
|
102
116
|
const tail = node("div", "ts-tail", scroller);
|
|
@@ -324,6 +338,11 @@ export function createStageView({
|
|
|
324
338
|
slotsView.drawNow(slots.tool, slots.quiet);
|
|
325
339
|
slotsView.drawTrail(slots.trail, flash);
|
|
326
340
|
slotsView.drawTicks(slots.ticks);
|
|
341
|
+
if (rail) {
|
|
342
|
+
const { items, ordinal } = workingSet.liveItems(turn, promptWords());
|
|
343
|
+
stampRows(blockEl, turn, ordinal);
|
|
344
|
+
rail.draw(items, { ctxPct: workingSet.ctxPct() });
|
|
345
|
+
}
|
|
327
346
|
const calls = slots.ticks.length;
|
|
328
347
|
// An ended turn says each thing once: its steps in the chip (the label
|
|
329
348
|
// of what the cloud holds), its time in the status row's timing line
|
|
@@ -348,6 +367,13 @@ export function createStageView({
|
|
|
348
367
|
tail.hidden = !tail.firstChild;
|
|
349
368
|
}
|
|
350
369
|
|
|
370
|
+
// The prompt's first words (the working set's session tags say whose
|
|
371
|
+
// turn a step was).
|
|
372
|
+
function promptWords() {
|
|
373
|
+
const text = (promptSlot.querySelector(".user-message")?.textContent || "").replace(/\s+/g, " ").trim();
|
|
374
|
+
return text.length > 60 ? `${text.slice(0, 59)}\u2026` : text;
|
|
375
|
+
}
|
|
376
|
+
|
|
351
377
|
// The next step: the live slots' waiting height is let go. In a turn
|
|
352
378
|
// with the question room that height was the whole reply's: it goes as
|
|
353
379
|
// far as the stage's scroll absorbs it (the scroll moves up by as much,
|
|
@@ -539,6 +565,7 @@ export function createStageView({
|
|
|
539
565
|
function emptySlots() {
|
|
540
566
|
for (const slot of [promptSlot, extras, answer, tail, cloudBody, queuedSlot, timingSlot]) slot.replaceChildren();
|
|
541
567
|
slotsView.reset();
|
|
568
|
+
rail?.reset();
|
|
542
569
|
promptSlot.classList.remove("full");
|
|
543
570
|
thinkingLine = null;
|
|
544
571
|
flash = null;
|
|
@@ -584,6 +611,7 @@ export function createStageView({
|
|
|
584
611
|
}
|
|
585
612
|
|
|
586
613
|
function release() {
|
|
614
|
+
workingSet?.release();
|
|
587
615
|
releaseTurn();
|
|
588
616
|
clearStage();
|
|
589
617
|
}
|
|
@@ -604,9 +632,13 @@ export function createStageView({
|
|
|
604
632
|
const { blockEl } = view.current();
|
|
605
633
|
view.chronological();
|
|
606
634
|
const block = blockEl && blockEl.parentNode === cloudBody ? blockEl : null;
|
|
635
|
+
// The turn's set joins the page's turns (a plain one's too: the turns
|
|
636
|
+
// keep their places); its footprint goes under the block.
|
|
637
|
+
const footprint = workingSet ? workingSet.handOff(view.current().turn, promptWords()) : null;
|
|
607
638
|
const pieces = handOffOrder({
|
|
608
639
|
prompt: promptSlot.firstElementChild,
|
|
609
640
|
block,
|
|
641
|
+
footprint: block ? footprint : null,
|
|
610
642
|
extras: [...extras.children],
|
|
611
643
|
answer: [...answer.children],
|
|
612
644
|
timing: timingSlot.firstElementChild,
|
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
// liveTurn, turnDom, shownTexts, questionCards, contextChips,
|
|
18
18
|
// llmContextControl, thinkingControl,
|
|
19
19
|
// reasoningHistoryControl the page's views
|
|
20
|
+
// workingSet the stage view's working set (web.working_set),
|
|
21
|
+
// null when off
|
|
20
22
|
// sentIds, earlyRestores restore.js's bookkeeping
|
|
21
23
|
// stream: {takeGuess(), restart()} the stream was opened from a
|
|
22
24
|
// guess (takeGuess: whether, and forget it); restart:
|
|
@@ -51,7 +53,7 @@ export function createStreamHandlers(deps) {
|
|
|
51
53
|
const {
|
|
52
54
|
selected, state, clientId, capabilities, historyEl,
|
|
53
55
|
liveTurn, turnDom, shownTexts, questionCards, contextChips, llmContextControl, thinkingControl, reasoningHistoryControl,
|
|
54
|
-
sentIds, earlyRestores, stream, composer, getSession,
|
|
56
|
+
workingSet = null, sentIds, earlyRestores, stream, composer, getSession,
|
|
55
57
|
resync, updateInfoBar, applyPromptOps, showSteers, markRecapStale, setTurnRunning, setLiveStatus, endTurnView,
|
|
56
58
|
appendChunk, readTurnEnd, applyFinalMarkdown, refreshTimingFromSession, makeUserBubble, addEndBubble,
|
|
57
59
|
commandBubble, addCommandBubble, addNoteBubble, findInTurnRoots, setUsedMemories, showRecap,
|
|
@@ -226,6 +228,8 @@ export function createStreamHandlers(deps) {
|
|
|
226
228
|
// the conversation (!rollback, !cmd, a continue answered no) re-reads it.
|
|
227
229
|
command_ran: async (data) => {
|
|
228
230
|
const id = selected();
|
|
231
|
+
// A queued /working-set line is no longer pending (the list's pill).
|
|
232
|
+
workingSet?.commandRan(data);
|
|
229
233
|
const view = commandView(data, clientId);
|
|
230
234
|
if (view.modelName && view.modelName !== state().model) {
|
|
231
235
|
state().model = view.modelName;
|
|
@@ -288,6 +292,8 @@ export function createStreamHandlers(deps) {
|
|
|
288
292
|
// turn: its bubble goes here, so the cards it shows come after it. One
|
|
289
293
|
// queued for after the turn (/model X mid-turn) shows waiting here.
|
|
290
294
|
command_queued: (data) => {
|
|
295
|
+
// A /working-set line queued for after the turn: pending in the list.
|
|
296
|
+
workingSet?.commandQueued(data);
|
|
291
297
|
// A card's action waits for its command_ran: shown only if it answers.
|
|
292
298
|
if ((data.anytime || data.waits) && !data.card && !commandBubble(data.command_id)) addCommandBubble(commandView(data, clientId));
|
|
293
299
|
},
|
|
@@ -307,6 +313,9 @@ export function createStreamHandlers(deps) {
|
|
|
307
313
|
used_memories_updated: (data) => {
|
|
308
314
|
if (Array.isArray(data.used_memory_names)) setUsedMemories(data.used_memory_names);
|
|
309
315
|
},
|
|
316
|
+
// The server's working set (WorkingSet), at a request and at turn end;
|
|
317
|
+
// ignored with web.working_set off.
|
|
318
|
+
working_set_updated: (data) => { workingSet?.update({ items: data.items, at: data.at }); },
|
|
310
319
|
context_status: (data) => {
|
|
311
320
|
const current = state();
|
|
312
321
|
// After /model (Engine#announce_switched_context!): the new model's
|
|
@@ -60,6 +60,11 @@ export function llmEditLine(edit) {
|
|
|
60
60
|
const tokens = Number(edit.tokens) > 0 ? ` · ~${compactTokens(Number(edit.tokens))}` : "";
|
|
61
61
|
return `✂ stubbed: ${edit.note || "stale"}${tokens}`;
|
|
62
62
|
}
|
|
63
|
+
// The user's drop (/working-set drop, the working set's list).
|
|
64
|
+
if (edit.kind === "drop") {
|
|
65
|
+
const tokens = Number(edit.tokens) > 0 ? ` · ~${compactTokens(Number(edit.tokens))}` : "";
|
|
66
|
+
return `✂ dropped by the user${tokens}`;
|
|
67
|
+
}
|
|
63
68
|
return "";
|
|
64
69
|
}
|
|
65
70
|
|